花了一週時間開發了一個簡單的即時通訊工具,勉強算是程序原型。如今我把開發流程和一些我的的想法記錄下來。本文首先介紹程序架構和通訊接口,以後會聚焦到服務器的信號槽設計原則,接下來將解釋有關TCP通訊的粘包問題和解決方案,最後一個部分是一些改進建議。git
源碼下載:https://gitee.com/learnhow/imlite服務器
程序架構圖例:架構
1、架構方案介紹socket
主程序(MainWindow)啓動服務器(TcpServer),在收到客戶端的鏈接請求以後會開啓一個線程並建立子鏈接(TcpSocket)。目前支持的消息類型有三種,分別爲:Message——由客戶端發起的端到端消息或直接由服務器發起的廣播消息,這是即時通訊最基礎的通訊要求。MessageConnCallback——由服務器向新鏈接的終端發起的socketID反向註冊信息,目的是告知客戶端本次鏈接在服務器端生成的socketID。MessageConnecter——由服務器向全部終端發佈的一條廣播,通知全部終端其餘的終端socketID和客戶端自定義的nickname,這是實現端到端通訊的基礎。tcp
端到端通訊的原理其實是由客戶端發送一條攜帶了接收方socketID的信息。服務器在收到後會解析並進行轉發。工具
爲了讓多種消息類型可以統一,程序提供了MessageInterface接口和TcpSocketData對象。全部的消息類型都必須實現MessageInterface接口,而且在發送端和接收端都必須經過TcpSocketData來註冊消息類型包括進行序列化和反序列化。spa
NetPacket提供了消息打包和解包方法。爲發送的數據包加上一個字符串做爲包尾,而且在接收端能夠根據包尾來解包並分割爲原始報文。更詳細的信息將在第三節介紹。線程
SQLiteService實現了一種簡單的數據保存方法,因爲程序開發的重點不在於此,並無作專門設計。二次開發中應該會從新設計,這裏一帶而過。設計
2、通訊機制介紹code
本例中全部對象的通訊都採用信號槽,信號槽是Qt提供的一種消息流起色制。在命名規則上,程序按照Qt的命名方案:槽的命名使用動詞通常如今時,信號命名使用動詞過去時。而且以「誰建立誰鏈接」的原則編寫,例如:TcpServer負責建立全部的子鏈接TcpSocket,所以與它的信號槽鏈接都會放在TcpServer中實現。
接下來以客戶端向服務器端發起鏈接請求爲例作簡單介紹。服務器(TcpServer)經過incomingConnection(qintptr)方法產生socketID,馬上經過子鏈接向終端發送connectionCallback信號,接着會發送廣播向終端通知有新的鏈接加入,最後還會向UI層發送一個與顯示有關的信號(程序中表現爲在QWidgetList中增長一個Item)。
// 當有新的終端鏈接請求時被調用 void TcpServer::incomingConnection(qintptr handle) { ... clientStatusConnect(QString("%1").arg(handle)); } void TcpServer::clientStatusConnect(QString socketDescriptor) { socketnicknamemap.insert(socketDescriptor, "Guest"); // 向終端反向註冊handle MessageConnCallback callback; callback.setSocketDescriptor(socketDescriptor); emit connectionCallback(callback); ① MessageConnecter msgConn; msgConn.setConnectors(socketnicknamemap); emit terminalsPublished(msgConn); ② // 反饋信號至UI:增長終端 emit clientStatusTriggered(socketDescriptor, 1); }
其它的代碼就不一一解釋了。
3、Tcp粘包和解決方案
觀察上面的源碼片斷,當有新鏈接產生後,服務器會「同時」向終端發送兩條數據(注:①②)。客戶端的void dataRecv()方法會調用QByteArray buff = tcpSocket->readAll()讀取數據,這時有極大的概率發生「粘包」——服務器發送的兩條報文A、B,客戶端以A+B做爲一條報文讀取出來。個人解決方案是在發送端爲每段報文增長一個字符串(CF07D)做爲結束標誌,在接收端就以此標誌對報文分割。
QByteArray NetPacket::package(const QByteArray &data) { QByteArray wrap(data); wrap += splite; return wrap; } QList<QByteArray> NetPacket::unpackage(const QByteArray &wrap) { QList<QByteArray> bufflist; int pos = 0; int prev = 0; while((pos = wrap.indexOf(splite, pos)) != -1) { QByteArray part = wrap.mid(prev, pos); bufflist.push_back(part); pos += splite.length(); prev = pos; } return bufflist; }
4、問題及改進建議
(1)信號槽是一個好東西,可是不能濫用:程序中全部對象的數據流轉都是經過信號槽機制。不能否認這樣作彷佛減小了模塊耦合,可是增長了維護的難度。而且看上去也顯得「亂糟糟」的。若是一條數據同時須要通過多個對象處理,就必須爲此設計多個信號槽。
(2)把消息分開傳輸:服務器的自鏈接與客戶端之間只使用了一個socket端口通訊(8361),這樣在接收端就必須爲不一樣的消息類型設計不一樣的type值。更加合理的作法是讓客戶端與服務器同時使用多個端口進行通訊,既有利於需求擴展又不至於讓接收端的方法無限膨脹。