作者:小傅哥
部落格:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收獲!😄
一、前言
這知識學的,根本沒有忘的快呀?!
是不是感覺很多資料,
點收藏起來爽
、
看視訊時候嗨
讀文章當時會
,隻要過了那個勁,就完了,根本不記得這裡面都講了啥。時間浪費了,東西還沒學到手,這是為啥?
其實因為學習也分為上策、中策和下策:
- 下策:眼睛看就行,坐着、窩着、躺着,都行,反正也不累,還能一邊回複下吹水的微信群
- 中策:看完的資料做筆記整理歸納,長期積累資料
- 上策:實踐、上手、應用、調試、歸納、整理資料,總結經驗輸出文檔
綜上,下策學起來很快感覺自己好像會了不少,中策有點要動手了懶不想動,上策就很耗時耗力了要自己對每一個知識點都能事必躬親到親力親為。就這樣你在學習的時候不自覺的就選擇了下策,是以其實并沒有學到什麼。
學習能把知識學到手,講究的是實踐,在小傅哥編寫的文章中,基本都是以實踐代碼驗證結果為核心,講述文章内容。😁從小我就喜歡動手,就以一個即時通信的項目為例,已經基于不同技術方案實作了5、6次,僅為了實踐技術,截圖如下:
- 有些是剛學完Socket和Swing的時候,想動手試試這些技術能不能寫個QQ出來。
- 也有的是因為實習教育訓練需要完成的項目,不過在有了一些基礎後,一周時間就能寫完全部功能。
- 雖然這些項目在現在看上去還是醜醜的界面,以及代碼邏輯可能也不是那麼完善。但放在學習階段的每一次實作中,都能為自己帶來很多技術上的成長。
那麼,這次IM實踐的機會給你,希望你能用的上!接下來我會給你介紹一個IM的系統架構、通信協定、單聊群聊、表情發送、UI事件驅動等各項内容,以及提供全套的源碼讓你可以上手學習。
二、示範
在開始學習之前,先給大家示範下這套仿照PC端微信界面的IM系統運作效果。
聊天頁面
添加好友
視訊示範
https://www.bilibili.com/video/BV1BZ4y1W7fC
三、系統設計
在這套
IM
中,服務端采用
DDD
領域驅動設計模式進行搭建。将 Netty 的功能交給
SpringBoot
進行啟停控制,同時在服務端搭建控制台可以非常友善的操作通信系統,進行使用者和通信管理。在用戶端的建設上采用
UI
分離的方式進行搭建,以保證業務代碼與
UI
展示分離,做到非常易于擴充的控制。
另外在功能實作上包括;完美仿照微信桌面版用戶端、登入、搜尋添加好友、使用者通信、群組通信、表情發送等核心功能。如果有對于實際需要使用的功能,可以按照這套系統架構進行擴充。
- UI開發:使用
與JavaFx
搭建UI桌面工程,逐漸講解登入框體、聊天框體、對話框、好友欄等各項UI展示及操作事件。進而在這一章節中讓Java 程式員學會開發桌面版應用。Maven
- 架構設計:在這一章節中我們會使用DDD領域驅動設計的四層模型結構與Netty結合使用,架構出合理的分層架構。同時還有相應庫表功能的設計。相信這些内容學習後,你一定也可以假設出更好的架構。
- 功能實作:這部分我們主要将通信中的各項功能逐漸實作,包括;登入、添加好友、對話通知、消息發送、斷線重連等各項功能。最終完成整個項目的開發,同時也可以讓你從實踐中學會技能。
四、UI開發
1. 整體結構定義、側邊欄
聊天窗體,相對于登陸窗體來說,聊天窗體的内容會比較多,同時也會相對複雜一些。是以我們會分章節的逐漸來實作這些窗體以及事件和接口功能。在本篇文章中我們會主要講解聊天框體的搭建以及側邊欄 UI 開發。
- 首先是我們整個聊天主窗體的定義,是一塊空白面闆,并去掉預設的邊框按鈕 (最小化、退出等)
- 之後是我們左側邊欄,我們稱之為條形 Bar,功能區域的實作。
- 最後添加窗體事件,當點選按鈕時變換
中的填充資訊。内容面闆
2. 對話聊天框
對話框選中後的内容區域展現,也就是使用者之間資訊發送和展現。從整體上看這是一個關聯的過程,點選左側的對話框使用者,右側就有相應内容的填充。那麼右側被填充對話清單 ListView 需要與每一個對話使用者關聯,點選聊天使用者的時候,是通過反複切換填充的過程。
- 點選左側的每一個對話框體,右側聊天框填充内容即随之變化。同時還有相應的對話名稱也會也變化。
- 對話框中左側展示好友發送的資訊,右側展示個人發送的資訊。同時消息内容會随着内容的增多而增加高度和寬度。
- 最下面是文本輸入框,在後面的實作裡我們文本輸入框采用公用的方式進行設計,當然你也可以設計為單獨的個人使用。
3. 好友欄
大家都經常使用 PC 端的微信,可以知道在好友欄裡是分了幾段内容的,其中包含;新的朋友、公衆号、群組和最下面的好友。
- 最上面的搜尋框這部分内容不變,和前面的一樣。我們目前使用的方式是 fxml 設計,例如這部分是通用功能,可以抽取出來放到代碼中,設計成一個元件元素類。
- 經過我們的分析,在使用 JavaFx 元件開發為基礎下,這部分是一種嵌套 ListView,也就是最底層的面闆是一個 ListView,好友和群組有各是一個 ListView,這樣處理後我們會很友善的進行資料填充。
- 另外這樣的結構主要有利于在我們程式運作過程中,如果你添加了好友,那麼我們需要将好友資訊重新整理到好友欄中,而在資料填充的時候,為了更加便捷高效,是以我們設計了嵌套的 ListView。如果還不是特别了解,可以從後續的代碼中獲得答案。
4. 事件定義
在桌面版 UI 開發中,為了能使 UI 與業務邏輯隔離,需要在我們把 UI 打包後提供出操作界面的展示效果的接口以及界面操作事件抽象類。那麼可以按照下圖了解;
序号 | 接口名 | 描述 |
---|---|---|
1 | void doShow() | 打開視窗 |
2 | void setUserInfo(String userId, String userNickName, String userHead) | 設定登陸使用者 ID、昵稱、頭像 |
3 | void addTalkBox(int talkIdx, Integer talkType, String talkId, String talkName, String talkHead, String talkSketch, Date talkDate, Boolean selected) | 填充對話框清單 |
4 | void addTalkMsgUserLeft(String talkId, String msg, Date msgData, Boolean idxFirst, Boolean selected, Boolean isRemind) | 填充對話框消息 - 好友 (别人的消息) |
- 以上這些接口就是我們目前 UI 為外部提供的所有行為接口,這些接口的一個鍊路描述就是;打開視窗、搜尋好友、添加好友、打開對話框、發送消息。
五、通信設計
1. 系統架構
在前面我們說到更适合的架構,才是符合你當下需要最好的架構。那麼怎麼設計這樣架構呢,基本就是要找到符合點的目标。我們之是以這樣設計是為什麼,那麼在這個系統裡有如下幾點;
- 我們系統在服務端要有 web 頁面進行管理通信使用者以及服務端的控制和監控。
- 資料庫的對象類,不要被外部污染,要有隔離性。比如說;你的資料庫類暴漏給外部做展示類使用了,那麼現在需要增加一個字段,而這個字段又不是你資料庫存在的屬性。那麼這個時候就已經把資料庫類污染了。
- 因為目前我們都是在 Java 語言下實作 Netty 通信,那麼服務端與用戶端都會需要使用到通信過程中的協定定義和解析。那麼我們需要抽離這一層對外提供 Jar 包。
- 接口、業務處理、底層服務、通信互動,要有明确的區分和實作,避免造成混亂難以維護。
結合我們上面這四點的目标,你頭腦中有什麼模型結構展現了呢?以及相應的技術棧選擇上是否有計劃了?接下來我們會介紹兩種架構設計的模型,一種是你非常熟悉的
MVC
,另外一種是你可能聽說過的
DDD
領域驅動設計。
2. 通信協定
從圖稿上來看,我們在傳輸對象的時候需要在傳輸包中添加一個 幀辨別 以此來判斷目前的業務對象是哪個對象,也就可以讓我們的業務更加清晰,避免使用大量的 if 語句判斷。
協定架構
agreement
└── src
├── main
│ ├── java
│ │ └── org.itstack.naive.chat
│ │ ├── codec
│ │ │ ├── ObjDecoder.java
│ │ │ └── ObjEncoder.java
│ │ ├── protocol
│ │ │ ├── demo
│ │ │ ├── Command.java
│ │ │ └── Packet.java
│ │ └── util
│ │ └── SerializationUtil.java
│ ├── resources
│ │ └── application.yml
│ └── webapp
│ └── chat
│ └── res
│ └── index.html
└── test
└── java
└── org.itstack.demo.test
└── ApiTest.java
協定包
public abstract class Packet {
private final static Map<Byte, Class<? extends Packet>> packetType = new ConcurrentHashMap<>();
static {
packetType.put(Command.LoginRequest, LoginRequest.class);
packetType.put(Command.LoginResponse, LoginResponse.class);
packetType.put(Command.MsgRequest, MsgRequest.class);
packetType.put(Command.MsgResponse, MsgResponse.class);
packetType.put(Command.TalkNoticeRequest, TalkNoticeRequest.class);
packetType.put(Command.TalkNoticeResponse, TalkNoticeResponse.class);
packetType.put(Command.SearchFriendRequest, SearchFriendRequest.class);
packetType.put(Command.SearchFriendResponse, SearchFriendResponse.class);
packetType.put(Command.AddFriendRequest, AddFriendRequest.class);
packetType.put(Command.AddFriendResponse, AddFriendResponse.class);
packetType.put(Command.DelTalkRequest, DelTalkRequest.class);
packetType.put(Command.MsgGroupRequest, MsgGroupRequest.class);
packetType.put(Command.MsgGroupResponse, MsgGroupResponse.class);
packetType.put(Command.ReconnectRequest, ReconnectRequest.class);
}
public static Class<? extends Packet> get(Byte command) {
return packetType.get(command);
}
/**
* 擷取協定指令
*
* @return 傳回指令值
*/
public abstract Byte getCommand();
}
3. 添加好友
- 從上面的流程中可以看到,這裡包含了兩部分内容;(1) 搜尋好友,(2) 添加好友。當天就完成好友後,好友會出現到我們的好友欄中。
- 并且這裡面我們采用的是單方面同意加好友,也就是你添加一個好友的時候,對方也同樣有你的好友資訊。
- 如果你的業務中是需要添加好友并同意的,那麼可以在發起好友添加的時候,添加一條狀态資訊,請求加好友。對方同意後,兩個使用者才能成為好友并進行通信。
添加好友,案例代碼
public class AddFriendHandler extends MyBizHandler<AddFriendRequest> {
public AddFriendHandler(UserService userService) {
super(userService);
}
@Override
public void channelRead(Channel channel, AddFriendRequest msg) {
// 1. 添加好友到資料庫中[A->B B->A]
List<UserFriend> userFriendList = new ArrayList<>();
userFriendList.add(new UserFriend(msg.getUserId(), msg.getFriendId()));
userFriendList.add(new UserFriend(msg.getFriendId(), msg.getUserId()));
userService.addUserFriend(userFriendList);
// 2. 推送好友添加完成 A
UserInfo userInfo = userService.queryUserInfo(msg.getFriendId());
channel.writeAndFlush(new AddFriendResponse(userInfo.getUserId(), userInfo.getUserNickName(), userInfo.getUserHead()));
// 3. 推送好友添加完成 B
Channel friendChannel = SocketChannelUtil.getChannel(msg.getFriendId());
if (null == friendChannel) return;
UserInfo friendInfo = userService.queryUserInfo(msg.getUserId());
friendChannel.writeAndFlush(new AddFriendResponse(friendInfo.getUserId(), friendInfo.getUserNickName(), friendInfo.getUserHead()));
}
}
4. 消息應答
- 從整體的流程可以看到,在使用者發起好友、群組通信的時候,會觸發一個事件行為,接下來用戶端向服務端發送與好友的對話請求。
- 服務端收到對話請求後,如果是好友對話,那麼需要儲存與好友的通信資訊到對話框中。同時通知好友,我與你要通信了。你在自己的對話框清單中,把我加進去。
- 那麼如果是群組通信,是可以不用這樣通知的,因為不可能把還沒有線上的所有群組使用者全部通知(人家還沒登入呢),是以這部分隻需要在使用者上線收到資訊後,建立出對話框到清單中即可。可以仔細了解下,同時也可以想想其他實作的方式。
消息應答,案例代碼
public class MsgHandler extends MyBizHandler<MsgRequest> {
public MsgHandler(UserService userService) {
super(userService);
}
@Override
public void channelRead(Channel channel, MsgRequest msg) {
logger.info("消息資訊處理:{}", JSON.toJSONString(msg));
// 異步寫庫
userService.asyncAppendChatRecord(new ChatRecordInfo(msg.getUserId(), msg.getFriendId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));
// 添加對話框[如果對方沒有你的對話框則添加]
userService.addTalkBoxInfo(msg.getFriendId(), msg.getUserId(), Constants.TalkType.Friend.getCode());
// 擷取好友通信管道
Channel friendChannel = SocketChannelUtil.getChannel(msg.getFriendId());
if (null == friendChannel) {
logger.info("使用者id:{}未登入!", msg.getFriendId());
return;
}
// 發送消息
friendChannel.writeAndFlush(new MsgResponse(msg.getUserId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));
}
}
5. 斷線重連
-
從上述流程中我們看到,當網絡連接配接斷開以後,會像服務端發送重新連結的請求。
那麼在這個發起連結的過程,和系統的最開始連結有所差別。斷線重連是需要将使用者的 ID 資訊一同- - 發送給服務端,好讓服務端可以去更新使用者與通信管道 Channel 的綁定關系。
- 同時還需要更新群組内的重連資訊,把使用者的重連加入群組映射中。此時就可以恢複使用者與好友和群組的通信功能。
// Channel 狀态定時巡檢;3 秒後每 5 秒執行一次
scheduledExecutorService.scheduleAtFixedRate(() -> {while (!nettyClient.isActive()) {System.out.println("通信管道巡檢:通信管道狀态" + nettyClient.isActive());
try {System.out.println("通信管道巡檢:斷線重連 [Begin]");
Channel freshChannel = executorService.submit(nettyClient).get();
if (null == CacheUtil.userId) continue;
freshChannel.writeAndFlush(new ReconnectRequest(CacheUtil.userId));
} catch (InterruptedException | ExecutionException e) {System.out.println("通信管道巡檢:斷線重連 [Error]");}
}
}, 3, 5, TimeUnit.SECONDS);
6. 叢集通信
- 跨服務之間案例采用redis的釋出和訂閱進行傳遞消息,如果你是大型服務可以使用zookeeper
- 使用者A在發送消息給使用者B時候,需要傳遞B的channeId,以用于服務端進行查找channeId所屬是否自己的服務内
- 單台機器也可以啟動多個Netty服務,程式内會自動尋找可用端口
六、源碼下載下傳
本項目是作者小傅哥使用JavaFx、Netty4.x、SpringBoot、Mysql等技術棧和偏向于DDD領域驅動設計方式,搭建的仿桌面版微信實作通信核心功能。
這套
IM
代碼分為了三組子產品;UI、用戶端、服務端。之是以這樣拆分,是為了将UI展示與業務邏輯隔離,使用事件和接口進行驅動,讓代碼層次更加幹淨整潔易于擴充和維護。
工程 | 介紹 | |
---|---|---|
itstack-naive-chat-ui | 使用JavaFx開發的UI端,在我們的UI端中提供了;登入框體、聊天框體,同時在聊天框體中有大量的行為互動界面以及接口和事件。最終我的UI端使用Maven打包的方式向外提供Jar包,以此來達到UI界面與業務行為流程分離。 | |
itstack-naive-chat-client | 用戶端是我們的通信核心工程,主要使用Netty4.x作為我們的socket架構來完成通信互動。并且在此工程中負責引入UI的Jar包,完成UI定義的事件(登入驗證、搜尋添加好友、對話通知、發送資訊等等),以及需要使用我們在服務端工程定義的通信協定來完成資訊的互動操作。 | |
itstack-navie-chat-server | 服務端同樣使用Netty4.x作為socket的通信架構,同時在服務端使用Layui作為管理背景的頁面,并且我們的服務端采用偏向于DDD領域驅動設計的方式與Netty集合,以此來達到我們的架構結構整潔幹淨易于擴充。 | |
itstack.sql | 系統工程資料庫表結構以及初始化資料資訊,共計6張核心表;使用者表、群組表、使用者群組關聯表、好友表、對話表以及聊天記錄表。使用者在實際業務開發中可以自行拓展完善,目前庫表結構隻以核心功能為基礎。 |
- 源碼擷取:https://github.com/fuzhengwei/NaiveChat 親,源碼給我點個Star,不要白皮襖!!!
七、總結
- 此IM系統涉及到的技術棧内容較多,Netty4.x、SpringBoot、Mybatis、Mysql、JavaFx、layui等技術棧的使用,以及整個系統架構結構采用DDD四層架構+Socket子產品的方式進行搭建,所有的UI都以前後端分離事件驅動方式進行設計,在這個過程中隻要你能堅持學習下來,那麼一定會收獲非常多的内容。足夠吹牛啦!🌶
- 任何一個新技術棧的學習過程都會包括這樣一條路線;運作HelloWorld、熟練使用API、項目實踐以及最後的深度源碼挖掘。 那麼在聽到這樣一個需求時候,Java程式員肯定會想到一些列的技術知識點來填充我們項目中的各個子產品,例如;界面用JavaFx、Swing等,通信用Socket或者知道Netty架構、服務端控制用MVC模型加上SpringBoot等。但是怎麼将這些各個技術棧合理的架設出我們的系統确是學習、實踐、成長過程中最重要的部分。
公衆号:bugstack蟲洞棧 | 作者小傅哥多年從事一線網際網路 Java 開發的學習曆程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心内容。如果能為您提供幫助,請給予支援(關注、點贊、分享)!