IMLite輕量級即時通訊工具開發指南

花了一週時間開發了一個簡單的即時通訊工具,勉強算是程序原型。如今我把開發流程和一些我的的想法記錄下來。本文首先介紹程序架構和通訊接口,以後會聚焦到服務器的信號槽設計原則,接下來將解釋有關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值。更加合理的作法是讓客戶端與服務器同時使用多個端口進行通訊,既有利於需求擴展又不至於讓接收端的方法無限膨脹。

相關文章
相關標籤/搜索