IM即時通訊設計 高並發聊天服務:服務器 + qt客戶端(附源碼)


來源:微信公眾號「編程學習基地」

IM即時通信程序設計

界面相對簡陋,主要界面如下

  • 登錄界面

登錄界面

  • 注冊界面

注冊界面

  • 聊天界面

聊天界面

  • 添加好友界面

添加好友界面

支持的功能

  • 注冊賬號
  • 登錄賬號
  • 添加好友
  • 群聊

群聊

  • 私聊

私聊

后續UI美化以及功能增加持續更新,關注微信公眾號「編程學習基地」最快咨詢..

IM即時通訊

本系列將帶大家從零開始搭建一個輕量級的IM服務端,麻雀雖小,五臟俱全,我們搭建的IM服務端實現以下功能

  • 注冊
  • 登錄
  • 私聊
  • 群聊
  • 好友關系

第一版只實現了IM即時通訊的基礎功能,其他功能后續增加.

設計一款高並發聊天服務需要注意什么

  1. 實時性

在網絡良好的狀態下服務器能夠及時處理用戶消息

  1. 可靠性

服務端如何防止粘包,半包,保證數據完全接收,不丟數據,不重數據

  1. 一致性

保證發送方發送順序與接收方展現順序一致

實時性就不必細說了,保證服務器能夠及時處理用戶消息就行,重點說下可靠性

如何設計可靠的消息處理服務

簡單來說就是客戶端每次發送的數據長度不定,服務端需要保證能夠解析每一個用戶發送過來的消息。

這就涉及到粘包和半包,這里說下粘包和半包是什么情況

什么是粘包

多個數據包被連續存儲於連續的緩存中,在對數據包進行讀取時無法確定發生方的發送邊界.

例如:客戶端需要給服務端發送兩條消息,發送數據如下

char msg[1024] = "hello world";
int nSend = write(sockFd, msg, strlen(msg));
nSend = write(sockFd, "粘包", strlen("粘包"));

服務端接收

char buff[1024];
read(connect_fd,buff,1024);
printf("recv msg:%s\n",buff);

結果就是服務端將兩條消息當成一條消息全部存入buff中。輸出如下

recv msg:hello world粘包

當客戶端兩條消息發的很快的時候,服務端無法判斷消息邊界導致照單全收的情況就是粘包。

什么是半包

單個數據包過大,服務端預定緩沖不夠,導致對數據包接收不全

例如:客戶端需要給服務端發送一條消息,發送數據如下

char msg[1024] = "hello world";
int nSend = write(sockFd, msg, 1024);	//發送字節大小為1024

服務端接收

char buff[128];
read(connect_fd,buff,128);
printf("recv msg:%s\n",buff);

結果就是服務端緩沖不夠,只能讀取部分包內容。

解決粘包和半包

如何解決粘包和半包的問題?

通過自定義應用協議,客戶端給數據包進行封包,服務端進行拆包。

以項目實例來說,定義包頭 + 包 +負載
傳輸協議

其實就是發送數據包的時候先發一個包頭,包頭里面有一個字段表示包的大小

包頭后緊跟着包,這個包還不是數據包,只是數據包的描述信息,例如發送消息代表一個命令,字段command用來從存儲命令,讓服務器能夠解析這是群聊數據包還是私聊數據包。包頭和包定義付下

struct DeMessageHead{
    char mark[2];   // "DE" 認證deroy的協議
    char version;
    char encoded;   //0 不加密,1 加密
    int length;
};

struct DeMessagePacket
{
    int mode;  //1 請求,2 應答,3 消息通知
    int error; //0 成功,非0,對應的錯誤碼

    int sequence;   //序列號
    int command;    //命令號
};

負載就是你真正要發送的數據包結構了,可能是msg消息,又或者其他的自定義消息。

IM通信協議

所謂“協議”是雙方共同遵守的規則.

協議有語法、語義、時序三要素:

(1)語法:即數據與控制信息的結構或格式

(2)語義:即需要發出何種控制信息,完成何種動作以及做出何種響應

(3)時序:即事件實現順序的詳細說明

一套典型的IM通信協議設計分為三層:應用層、安全層、傳輸層。

通信協議設計

應用層協議設計

在通信過程中,chat_room使用的是tcp作為傳輸層的協議,暫時未引入數據加密解密,所以未涉及安全層協議。

應用層協議選型,常見的有三種:文本協議、二進制協議、流式XML協議。

文本協議

文本協議是指 “貼近人類書面語言表達”的通訊傳輸協議,典型的協議是http協議。

一個http協議大致長成這樣:

GET / HTTP/1.1
User-Agent: curl
Host: musicml.net
Accept: */*

文本協議的特點是:

a. 可讀性好,便於調試

b. 擴展性也好(通過key:value擴展)

c. 解析效率一般(一行一行讀入,按照冒號分割,解析key和value)

d. 對二進制的支持不好 ,比如語音/視頻

二進制協議

二進制協議是指binary協議,典型是ip協議。二進制協議一般定長包頭和可擴展變長包體 ,每個字段固定了含義,此次項目設計chat_room采用的就是二進制協議作為應用層的傳輸協議。

二進制協議有這樣一些特點:

a. 可讀性差,難於調試

b. 擴展性不好 ,如果要擴展字段,舊版協議就不兼容了。

c. 解析效率超高

QQ使用的就是二進制協議

流式XML協議

這個一般場景用的比較少了,我所接觸的就是Onvif協議交互用的就是流式XML協議。

XML協議特點:

a.它是准標准協議,可以跨域互通

b.XML的優點,可讀性好,擴展性好

c.解析代價超高

d.有效數據傳輸率超低(大量的標簽)

數據傳輸格式

即時通訊應用(包括IM聊天應用、實時消息推送應用等)在選擇數據傳輸格式的時候比較糾結,不過我個人建議將Protobuf作為即時通訊應用的首選通訊協議格式。此次項目設計未使用Protobuf是因為不想導入第三方庫,怕有些同學直接勸退。

據說,手機QQ的數據傳輸協議已在使用Protobuf了,而從官方流出資料來看微信很早就在使用Protobuf(而且為了盡可能地壓縮流量,甚至對Protobuf進行了極致優化)。

此次項目使用的是二進制數據流作為數據傳輸格式,其實就是一堆結構體變量。

例如登陸的數據包定義如下:

struct LoginInfoReq{
    int m_account;
    char m_password[32];
};

服務端和客戶端雙方約定好一個數據結構就可以了,特點就是簡單。

聊天服務設計

目前采用的是多線程處理客戶端請求,即一個客戶端一個線程,這周會改成IO多路復用,用epoll來接受更高的並發。

整體設計如下:
架構

第一步:客戶端發送數據包

第二步:服務端解析數據包,傳遞給各個業務處理模塊

第三步:業務處理模塊按照通信協議解析並處理消息

消息處理

對客戶端的消息處理就是接受一個完整的數據包,傳遞給服務器。

由於采用封包-拆包作為通信的傳輸協議,所以在處理數據包的時候需要一個健壯的數據處理邏輯

此次項目處理邏輯如下

int Session::readEvent()
{
    int ret = 0;
    switch (m_type)
    {
    case RECV_HEAD:
        ret = recvHead();
        break;
    case RECV_BODY:
        ret = recvBody();
        break;
    default:
        break;
    }
    if (ret == RET_AGAIN)
        return readEvent();
    return ret;
}

先讀取頭,在讀取到head包頭之后申請body(包+負載)所需空間,再讀取body,body讀取完畢之后傳給消息分發的邏輯。

消息分發

服務端是如何區分群聊消息和私聊消息?在我們解決粘包和半包問題的時候就給出了答案。

客戶端封包結構為:包頭 + 包 +負載

傳輸協議

在Pack包里面有一個代表命令的字段 command.

struct DeMessagePacket
{
    int mode;  //1 請求,2 應答,3 消息通知
    int error; //0 成功,非0,對應的錯誤碼
    int sequence;   //序列號
    int command;    //命令號
};

服務端可客戶端雙方約定的 cmmand 如下

//命令枚舉
enum{
    CommandEnum_Registe,
    CommandEnum_Login,
    CommandEnum_Logout,
    CommandEnum_GroupChat,
    CommandEnum_AddFriend,
    CommandEnum_delFriend,
    CommandEnum_PrivateChat,
    CommandEnum_CreateGroup,
    CommandEnum_GetGroupList,
    CommandEnum_GetGroupInfo,
    CommandEnum_GetFriendInfo,
};

服務端通過switch匹配各個命令,進而對每個命令進行處理。

用戶注冊

用戶注冊請求,響應的數據格式如下

/**
 * @brief 注冊用戶信息
 */
struct RegistInfoReq{
    char m_userName[32];
    char m_password[32];
};
struct RegistInfoResp{
    int m_account;
};

在用戶注冊時,服務端生成一個唯一的賬號發送給客戶端,客戶端只能通過該賬號與服務端交互。

用戶注冊完成之后會存放在服務端的一個全局map表中,方便集中管理

typedef std::map<int,RegistInfoReq*>    mapAccountInfo;      //注冊用戶表
static mapAccountInfo   g_AccountInfoMap;   //注冊賬戶信息表

用戶登陸

用戶登陸請求,響應的數據格式如下

struct LoginInfoReq{
    int m_account;      //賬號
    char m_password[32];
};

用戶登陸成功后會創建一個用戶信息 UserInfo 並將該用戶信息添加到全局的一個用戶map表中集中管理

typedef std::map<int,UserInfo*>         mapUserInfo;          //在線用戶表
static mapUserInfo      g_UserInfoMap;      //在線用戶信息表

登陸成功之后發回給客戶端的是一個沒有負載的包,包中的error字段置0.

用戶登出

客戶端直接斷開即可,具體登出數據格式暫未實現.

群聊

此次設計中有一個公共群聊(賬號為0),所有用戶都在群聊里面。

用戶群聊請求,響應的數據格式如下

truct GroupChatReq
{
    int m_UserAccount;      //發送的賬號
    int m_msgLen;
    int m_type;             //數據類型 0:文本,1:圖片 ...
    int m_GroupAccount;     //發送群號 0:廣播
};

看着沒啥毛病但是群消息在哪?要發送的數據在哪?

還記得我們客戶端封包結構:包頭 + 包 +負載

傳輸協議

負載里面包含了 數據傳輸格式+其他數據

在群聊請求里面有一個 m_msgLen字段用來區分消息的邊界,因為客戶端發送的消息是不定長的,所以需要這么一個字段來區分消息的邊界。

私聊

用戶私聊請求,響應的數據格式如下

struct PrivateChatReq
{
    int m_UserAccount;      //發送的賬號
    int m_msgLen;
    int m_type;             //數據類型 0:文本,1:圖片 ...
    int m_FriendAccount;    //發送好友賬號
};

跟群聊類似,其實這兩個數據格式可以用同一個。

添加好友

用戶添加好友請求,響應的數據格式如下

struct AddFriendInfoReq
{
    int m_friendAccount;    //好友賬號
    int m_senderAccount;    //發送端賬號
    char m_reqInfo[64];    //請求信息 例如我是xxx
};
struct AddFriendInfoResp
{
    int m_friendAccount;    //好友賬號
    int m_senderAccount;    //發送端賬號
    int status;             //同意0,不同意-1
};

添加好友的流暢比較復雜,我在設計的時候也卡了一下。

主要流程如圖

請添加圖片描述

  1. 客戶端A給服務器發送添加好友的請求 AddFriendInfoReq,服務器解析請求將B的信息添加到客戶端A的好友表中。
  2. 服務器B給客戶端B轉發好友請求。
  3. 客戶端B同意或者拒絕,給服務器發送添加好友的響應 AddFriendInfoResp,服務器解析請求將A的信息添加到客戶端B的好友表中,將客戶端A的好友表中屬於客戶端B的好友狀態字段m_status置1或0。

獲取好友信息

用戶獲取好友信息請求,響應的數據格式如下

/*  好友請求接口封裝  */
struct GetFriendInfoResp
{
    int m_size;         //群成員大小
};
struct FriendInfo{
    char m_userName[32];//好友用戶名
    int  m_account;     //賬號
    int  m_status;      //是否添加成功 0:等待添加   1:同意
};

這里大伙可能有點蒙了,又是包頭,又是包,又是負載的,拿着數據格式到底屬於那塊的

其實數據格式(例如GetFriendInfoResp結構體)和數據都屬於負載里面的,如圖所示。

請添加圖片描述

對於通信協議為二進制的協議來說,解析起來效率是最快的。

獲取群列表

用戶獲取群列表信息請求,響應的數據格式如下

struct GetGroupListResp
{
    int m_size;             //群數量大小
};
struct GroupChatInfo
{
    char m_groupName[32]; //群名稱
    int  m_account;       //群賬號
    int  m_size;          //群大小
};

數據的傳輸同獲取好友信息,在這里群列表也有一個map表統一管理。

獲取群信息

用戶獲取群信息請求,響應的數據格式如下

struct GetGroupInfoReq
{
    int m_GroupAccount;    //群號 0:廣播   
};

struct GetGroupInfoResp
{
    char m_groupName[32];   //群名稱
    int m_GroupAccount;     //群號 0:廣播   
    int m_size;             //群成員大小
};
struct GroupUserInfo{
    char m_userName[32];
    int  m_account;     //賬號
    int  m_right;       //權限 0:群成員 1:群管 2:群主
};

這里的數據傳輸和獲取好友信息一樣。

到這里我們的服務端介紹完了,比較復雜,但是知識點超多。客戶端設計相對容易些,但是我感覺單純的終端客戶端太掉逼格了,就又寫個一個qt的客戶端,重溫了一邊qt的UI設計,簡直不要太爽,qt的客戶端設計會另外再補一篇文章。

github源碼

chat_room:https://github.com/ADeRoy/chat_room

歡迎慷慨 star


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM