1. 即時通訊簡述
即時通訊是端開發工作中常見的需求,本篇文章以作者工作中使用FLutter開發社交軟件即時通訊需求為背景,描述一下即時通訊功能設計的要點。
2. 重要概念
即時通訊需要前后端配合,約定消息格式與消息內容。本次IM客戶端需求開發使用了公司已有的基於Socket.io搭建的后台,下文描述涉及到的一些概念。
2.1 WebSocket協議
WebSocket是一種在單個TCP連接上進行全雙工通信的協議。WebSocket協議與傳統的HTTP協議的主要區別為,WebSocket協議允許服務端主動向客戶端推送數據,而傳統的HTTP協議服務器只有在客戶端主動請求之后才能向客戶端發送數據。在沒有WebSocket之前,即時通訊大部分采用長輪詢方式。
2.2 Socket.io和WebSocket的區別
Socket.io不是WebSocket,它只是將WebSocket和輪詢 (Polling)機制以及其它的實時通信方式封裝成了通用的接口,並且在服務端實現了這些實時機制的相應代碼。也就是說,WebSocket僅僅是Socket.io實現即時通信的一個子集。因此WebSocket客戶端連接不上Socket.io服務端,當然Socket.io客戶端也連接不上WebSocket服務端。
2.3 服務端socket消息
理解了服務端socket消息也就理解了服務器端的即時通訊邏輯,服務器發出的socket消息可以分為兩種:
-
服務器主動發出的消息:
例如,社交軟件中的A用戶給B用戶發出了消息,服務器在收到A用戶的消息后,通過socket鏈接,將A用戶的消息轉發給B用戶,B用戶客戶端接收到的消息就屬於服務器主動發出的。其他比較常見的場景例如直播軟件中,全平台用戶都會收到的禮物消息廣播。
-
服務器在接收到客戶端消息后的返回消息:
例如,長鏈接心跳機制,客戶端向服務器發送ping消息,服務器在成功接受客戶端的ping消息后返回的pong消息就屬於服務器的返回消息。其他常見的場景如社交軟件中A用戶給B用戶發出了消息,服務器在收到A用戶的消息后,給A客戶端返回一條消息,供A客戶端了解消息的發送狀態,判斷發送是否成功。大部分場景,服務器在接收到客戶端主動發出的消息之后都需要返回一條消息。
3. 客戶端實現流程
幾個設計客戶端即時通訊的重點。
3.1 心跳機制
所謂心跳就是客戶端發出ping消息,服務器成功收到后返回pong消息。當客戶端一段時間內不在發送ping消息,視為客戶端斷開,服務器就會主動關閉socket鏈接。當客戶端發送ping消息,服務器一段時間內沒有返回pong消息,視為服務器斷開,客戶端就會啟動重連機制。
3.2 重連機制
重連機制為客戶端重新發起連接,常見的重連條件如下:
- 客戶端發送ping消息,服務器一段時間內沒有返回pong。
- 客戶端網絡斷開。
- 服務器主動斷開連接。
- 客戶端主動連接失敗。
當出現極端情況(客戶端斷網)時,頻繁的重連可能會導致資源的浪費,可以設置一段時間內的最大重連次數,當重連超過一定次數時,休眠一段時間。
3.3 消息發送流程
- 將消息存儲到本地數據庫,發送狀態設為等待。
- 發送socket消息。
- 接收到服務器返回的socket消息后,將本地數據庫等待狀態的消息改為成功。
注意事項:
將消息存儲到本地數據庫時需要生成一個id存入數據庫,同時傳給服務器,當收到消息時根據id判斷更新本地數據庫的哪一條消息。
3.4 消息接收流程
3.5 其他相關
- 聊天頁消息的排序:在查詢本地數據庫時使用
order by
按時間排序。 - 消息列表:也推薦做本地存儲,當收到消息的時候需要先判斷本地消息列表是否有當前消息用戶的對話框,如果沒有就先插入,有就更新。消息列表的維護就不展開說了,感興趣可以看代碼。
- 圖片語音消息:將圖片和語言先上傳到專門的服務器上(各種專門的雲存儲服務器),sokcet消息和本地存儲傳遞的是雲服務器上的URL。
- 多人聊天(群聊):與單人聊天邏輯基本一致,區別位本地數據庫需要添加一個會話ID字段,打開一個群就查詢對應會話ID的數據。聊天消息不再是誰發給誰,而是在哪個群聊下。
4. 客戶端Flutter代碼
把部分代碼貼上來,完整項目在作者的github上。
4.1 心跳機制
heart() {
pingTimer = Timer.periodic(Duration(seconds: 30), (data) {
if (pingWaitTime >= 60) {
socket.connect();
pingWaitTime = 0;
pingWaitTimer!.cancel();
ping();
}
if (!pingWaitFlag) ping();
});
}
ping() {
debugPrint("ping");
String pingData =
'{"type":"ping","payload":{"front":true},"msg_id":${DateTime.now().millisecondsSinceEpoch}}';
socket.emit("message", pingData);
pingWaitFlag = true;
pingWaitTime = 0;
pingWaitTimer = Timer.periodic(Duration(seconds: 1), (data) {
pingWaitTime++;
print(data.hashCode);
if (pingWaitTime % 10 == 0) debugPrint(pingWaitTime.toString());
});
}
//pong
if (socketMessage.type == PONG && socketMessage.code == 1000) {
pingWaitFlag = false;
pingWaitTimer!.cancel();
pingWaitTime = 0;
}
4.2 本地數據庫設計
數據庫表的設計是比較重要的,理解了數據庫設計,讀代碼也就無壓力了。
//消息表
CREATE TABLE chatDetail (
chat_id TEXT PRIMARY KEY,//主鍵
from_id TEXT,//發送人
to_id TEXT,//接收人
created_at TEXT,
content TEXT,//消息內容
image TEXT,//UI展示用,用戶頭像
name TEXT,//UI展示用,用戶名
sex TEXT,//UI展示用,用戶性別
status TEXT,//消息狀態
type INTEGER,//消息類型,圖片/文字/語音等
chat_object_id TEXT//聊天對象ID,對當前用戶而言的聊天對象,是一系列本地操作的核心
)
//消息列表表
CREATE TABLE chatList (
cov_id TEXT,
unread_count INTEGER,
last_msg_text TEXT,
last_msg_at TEXT,
image TEXT,
name TEXT,
sex TEXT,
chat_object_id TEXT PRIMARY KEY)
5. 總結
無論是Flutter技術,或是IOS/Android/Web。只要掌握了即時通訊的核心開發流程,不同的技術只是API有些變化。API往往看文檔就能解決,大前端或是特定平台的工程師還是要掌握核心開發流程,會幾種做同樣事情的API意義不大。
demo寫的比較簡單,有問題可以評論。