首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何设计一款高性能的即时聊天服务

如何设计一款高性能的即时聊天服务

作者头像
DeROy
发布于 2021-12-13 05:14:30
发布于 2021-12-13 05:14:30
1.4K00
代码可运行
举报
文章被收录于专栏:编程学习基地编程学习基地
运行总次数:0
代码可运行

IM即时通信程序设计

界面相对简陋,主要界面如下

  • 登录界面

登录界面

  • 注册界面

注册界面

  • 聊天界面

聊天界面

  • 添加好友界面

添加好友界面

支持的功能

  • 注册账号
  • 登录账号
  • 添加好友
  • 群聊

群聊

  • 私聊

私聊

后续UI美化以及功能增加持续更新,关注微信公众号「编程学习基地」最快咨询..

IM即时通讯

本系列将带大家从零开始搭建一个轻量级的IM服务端,麻雀虽小,五脏俱全,我们搭建的IM服务端实现以下功能

  • 注册
  • 登录
  • 私聊
  • 群聊
  • 好友关系

第一版只实现了IM即时通讯的基础功能,其他功能后续增加.

设计一款高并发聊天服务需要注意什么

  1. 实时性

在网络良好的状态下服务器能够及时处理用户消息

  1. 可靠性

服务端如何防止粘包,半包,保证数据完全接收,不丢数据,不重数据

  1. 一致性

保证发送方发送顺序与接收方展现顺序一致

实时性就不必细说了,保证服务器能够及时处理用户消息就行,重点说下可靠性

如何设计可靠的消息处理服务

简单来说就是客户端每次发送的数据长度不定,服务端需要保证能够解析每一个用户发送过来的消息。

这就涉及到粘包和半包,这里说下粘包和半包是什么情况

什么是粘包

多个数据包被连续存储于连续的缓存中,在对数据包进行读取时无法确定发生方的发送边界.

例如:客户端需要给服务端发送两条消息,发送数据如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
char msg[1024] = "hello world";
int nSend = write(sockFd, msg, strlen(msg));
nSend = write(sockFd, "粘包", strlen("粘包"));

客户端两个包几乎是同时发送

服务端接收

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
char buff[1024];
read(connect_fd,buff,1024);
printf("recv msg:%s\n",buff);

结果就是服务端将两条消息当成一条消息全部存入buff中。输出如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
recv msg:hello world粘包

当客户端两条消息发的很快的时候,服务端无法判断消息边界导致照单全收的情况就是粘包。

什么是半包

单个数据包过大,服务端预定缓冲不够,导致对数据包接收不全

例如:客户端需要给服务端发送一条消息,发送数据如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
char msg[1024] = "hello world";
int nSend = write(sockFd, msg, 1024); //发送字节大小为1024

服务端接收

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
char buff[128];
read(connect_fd,buff,128);
printf("recv msg:%s\n",buff);

结果就是服务端缓冲不够,只能读取部分包内容。

解决粘包和半包

如何解决粘包和半包的问题?

通过自定义应用协议,客户端给数据包进行封包,服务端进行拆包。

以项目实例来说,定义包头 + 包 +负载

其实就是发送数据包的时候先发一个包头,包头里面有一个字段表示包的大小

包头后紧跟着包,这个包还不是数据包,只是数据包的描述信息,例如发送消息代表一个命令,字段command用来从存储命令,让服务器能够解析这是群聊数据包还是私聊数据包。包头和包定义如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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协议大致长成这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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进行了极致优化)。

此次项目使用的是二进制数据流作为数据传输格式,其实就是一堆结构体变量。

例如登陆的数据包定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct LoginInfoReq{
    int m_account;
    char m_password[32];
};

服务端和客户端双方约定好一个数据结构就可以了,特点就是简单。

聊天服务设计

目前采用的是多线程处理客户端请求,即一个客户端一个线程,这周会改成IO多路复用,用epoll来接受更高的并发。

整体设计如下:

第一步:客户端发送数据包

第二步:服务端解析数据包,传递给各个业务处理模块

第三步:业务处理模块按照通信协议解析并处理消息

消息处理

对客户端的消息处理就是接受一个完整的数据包,传递给服务器。

由于采用封包-拆包作为通信的传输协议,所以在处理数据包的时候需要一个健壮的数据处理逻辑

此次项目处理逻辑如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct DeMessagePacket
{
    int mode;  //1 请求,2 应答,3 消息通知
    int error; //0 成功,非0,对应的错误码
    int sequence;   //序列号
    int command;    //命令号
};

服务端可客户端双方约定的 cmmand 如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//命令枚举
enum{
    CommandEnum_Registe,
    CommandEnum_Login,
    CommandEnum_Logout,
    CommandEnum_GroupChat,
    CommandEnum_AddFriend,
    CommandEnum_delFriend,
    CommandEnum_PrivateChat,
    CommandEnum_CreateGroup,
    CommandEnum_GetGroupList,
    CommandEnum_GetGroupInfo,
    CommandEnum_GetFriendInfo,
};

服务端通过switch匹配各个命令,进而对每个命令进行处理。

用户注册

用户注册请求,响应的数据格式如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * @brief 注册用户信息
 */
struct RegistInfoReq{
    char m_userName[32];
    char m_password[32];
};
struct RegistInfoResp{
    int m_account;
};

在用户注册时,服务端生成一个唯一的账号发送给客户端,客户端只能通过该账号与服务端交互。

用户注册完成之后会存放在服务端的一个全局map表中,方便集中管理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef std::map<int,RegistInfoReq*>    mapAccountInfo;      //注册用户表
static mapAccountInfo   g_AccountInfoMap;   //注册账户信息表
用户登陆

用户登陆请求,响应的数据格式如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct LoginInfoReq{
    int m_account;      //账号
    char m_password[32];
};

用户登陆成功后会创建一个用户信息 UserInfo 并将该用户信息添加到全局的一个用户map表中集中管理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef std::map<int,UserInfo*>         mapUserInfo;          //在线用户表
static mapUserInfo      g_UserInfoMap;      //在线用户信息表

登陆成功之后发回给客户端的是一个没有负载的包,包中的error字段置0.

用户登出

客户端直接断开即可,具体登出数据格式暂未实现.

群聊

此次设计中有一个公共群聊(账号为0),所有用户都在群聊里面。

用户群聊请求,响应的数据格式如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
truct GroupChatReq
{
    int m_UserAccount;      //发送的账号
    int m_msgLen;
    int m_type;             //数据类型 0:文本,1:图片 ...
    int m_GroupAccount;     //发送群号 0:广播
};

看着没啥毛病但是群消息在哪?要发送的数据在哪?

还记得我们客户端封包结构:包头 + 包 +负载

传输协议

负载里面包含了 数据传输格式+其他数据

在群聊请求里面有一个 m_msgLen字段用来区分消息的边界,因为客户端发送的消息是不定长的,所以需要这么一个字段来区分消息的边界。

私聊

用户私聊请求,响应的数据格式如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct PrivateChatReq
{
    int m_UserAccount;      //发送的账号
    int m_msgLen;
    int m_type;             //数据类型 0:文本,1:图片 ...
    int m_FriendAccount;    //发送好友账号
};

跟群聊类似,其实这两个数据格式可以用同一个。

添加好友

用户添加好友请求,响应的数据格式如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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。
获取好友信息

用户获取好友信息请求,响应的数据格式如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*  好友请求接口封装  */
struct GetFriendInfoResp
{
    int m_size;         //群成员大小
};
struct FriendInfo{
    char m_userName[32];//好友用户名
    int  m_account;     //账号
    int  m_status;      //是否添加成功 0:等待添加   1:同意
};

这里大伙可能有点蒙了,又是包头,又是包,又是负载的,拿着数据格式到底属于那块的

其实数据格式(例如GetFriendInfoResp结构体)和数据都属于负载里面的,如图所示。

获取好友信息

对于通信协议为二进制的协议来说,解析起来效率是最快的。

获取群列表

用户获取群列表信息请求,响应的数据格式如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct GetGroupListResp
{
    int m_size;             //群数量大小
};
struct GroupChatInfo
{
    char m_groupName[32]; //群名称
    int  m_account;       //群账号
    int  m_size;          //群大小
};

数据的传输同获取好友信息,在这里群列表也有一个map表统一管理。

获取群信息

用户获取群信息请求,响应的数据格式如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程学习基地 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Web端即时聊天项目实现(基于WebSocket)
 其实这个项目算是我做过的花时间最长也投入心血最多的一个项目了,当时决定开始做这个的时候我几乎什么都不会,那时我个人的情况是:
全栈程序员站长
2022/08/27
3.1K0
跟着源码学IM(十二):基于Netty打造一款高性能的IM即时通讯程序
关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。
JackJiang
2023/11/30
1.3K0
跟着源码学IM(十二):基于Netty打造一款高性能的IM即时通讯程序
基于go语言搭建高性能IM系统
前阵子看了《创业时代》,电视剧的剧情大概是这样的:IT工程师郭鑫年与好友罗维与投行精英那蓝等人一起,踏上互联网创业之路。创业开发的是一款叫做“魔晶”的IM产品。郭鑫年在第一次创业失败后,离了婚,还欠了很多外债,骑着自行车经历了西藏一次生死诀别之后产生了灵感,想要创作一款IM产品“魔晶”,“魔晶”的初衷是为了增加人与人之间的感情,虽然剧情纯属虚构,但确实让人浮想QQ当初的设想是不是就是这样的呢?
码农编程进阶笔记
2022/12/21
8120
基于go语言搭建高性能IM系统
我们说 TCP 是流式协议究竟意味着什么?
很多读者从接触网络知识以来,应该听说过这句话:TCP 协议是流式协议。那么这句话到底是什么意思呢?所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要我们人为地去给这些协议划分边界。
范蠡
2021/07/16
3K0
netty案例,netty4.1基础入门篇九《自定义编码解码器,处理半包、粘包数据》
在实际应用场景里,只要是支持sokcet通信的都可以和Netty交互,比如中继器、下位机、PLC等。这些场景下就非常需要自定义编码解码器,来处理字节码传输,并控制半包、粘包以及安全问题。那么本章节我们通过实现ByteToMessageDecoder、MessageToByteEncoder来实现我们的需求。
小傅哥
2020/02/11
1.3K0
netty案例,netty4.1基础入门篇九《自定义编码解码器,处理半包、粘包数据》
关于easyswoole实现websocket聊天室的步骤解析
在去年,我们公司内部实现了一个聊天室系统,实现了一个即时在线聊天室功能,可以进行群组,私聊,发图片,文字,语音等功能,那么,这个聊天室是怎么实现的呢?后端又是怎么实现的呢? 后端框架 在后端框架上,
仙士可
2020/02/21
2.7K1
关于easyswoole实现websocket聊天室的步骤解析
【Netty】「优化进阶」(一)粘包半包问题及解决方案
本篇博文是《从0到1学习 Netty》中进阶系列的第一篇博文,主要内容是介绍粘包半包出现的现象和原因,并结合应用案例来深入讲解多种解决方案,往期系列文章请访问博主的 Netty 专栏,博文中的所有代码全部收集在博主的 GitHub 仓库中;
sidiot
2023/08/30
1.3K0
【Netty】「优化进阶」(一)粘包半包问题及解决方案
socket网络编程(五)——粘包拆包问题
假设一个这样的场景,客户端要利用send()函数发送字符“asd”到服务端,连续发送3次,但是服务端休眠10秒之后再去缓冲池中接收。那么请问10秒之后服务端从缓冲区接收到的信息是“asd”还是“asdasdasd”呢?如果大家有去做实验的话,可以知道服务端收到的是“asdasdasd”,为什么会这样呢?按正常的话,服务端收到的应该是“asd”,剩下的两个asd要不就是收不到要不就是下次循环收到,怎么会一次性收到“asdasdasd”呢?如果要说罪魁祸首的话就是那个休眠10秒,导致数据粘包了!
一点sir
2024/01/10
3710
socket网络编程(五)——粘包拆包问题
Netty之旅二:口口相传的高性能Netty到底是什么?
高清思维导图原件(xmind/pdf/jpg)可以关注公众号:一枝花算不算浪漫 回复netty01即可。
一枝花算不算浪漫
2020/08/25
8300
Netty之旅二:口口相传的高性能Netty到底是什么?
netty案例,netty4.1基础入门篇九《自定义编码解码器,处理半包、粘包数据》
在实际应用场景里,只要是支持sokcet通信的都可以和Netty交互,比如中继器、下位机、PLC等。这些场景下就非常需要自定义编码解码器,来处理字节码传输,并控制半包、粘包以及安全问题。那么本章节我们通过实现ByteToMessageDecoder、MessageToByteEncoder来实现我们的需求。
小傅哥
2020/07/14
5240
netty案例,netty4.1基础入门篇九《自定义编码解码器,处理半包、粘包数据》
C++ 高性能服务器网络框架设计细节(节选)
这篇文章我们将介绍服务器的开发,并从多个方面探究如何开发一款高性能高并发的服务器程序。需要注意的是一般大型服务器,其复杂程度在于其业务,而不是在于其代码工程的基本框架。
范蠡
2018/10/23
2.4K0
【Netty】一些项目案例
由于网上已经有很多大佬已经做了很多相关项目案例。所以我们应该站在巨人的肩膀上,多向大佬们学习。下面主要是我收集到Netty项目,具体项目怎么实现的,我就不讲了,大佬们已经做得很简单明了。
用户3467126
2019/07/03
4.7K0
【Netty】一些项目案例
Socket粘包问题的3种解决方案,最后一种最完美!
在 Java 语言中,传统的 Socket 编程分为两种实现方式,这两种实现方式也对应着两种不同的传输层协议:TCP 协议和 UDP 协议,但作为互联网中最常用的传输层协议 TCP,在使用时却会导致粘包和半包问题,于是为了彻底的解决此问题,便诞生了此篇文章。
磊哥
2021/01/06
1.4K0
如何解决粘包问题?
大家好,我是蓝蓝,今天和出版社沟通,给大家送三本书,再次感谢出版社,大家在文末参加抽奖即可,一共三本。
我是程序员小贱
2021/07/23
1.2K0
Socket粘包问题「建议收藏」
1.:如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。关闭连接主要要双方都发送close连接(参考tcp关闭协议)。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如”hello give me sth abour yourself”,然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。 2.如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包。 3.如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
全栈程序员站长
2022/09/22
1.4K0
使用腾讯云IM搭建应用内类微信社交聊天模块实践
社交模块是目前主流应用程序最常见的功能之一。有了社交模块,用户在您的应用内,可以自由的交流互动,并添加好友,关注其他用户等等。
腾讯云音视频
2023/02/24
8.4K1
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
本文将要分享的是如何从零实现一套基于Netty框架的分布式高可用IM系统,它将支持长连接网关管理、单聊、群聊、聊天记录查询、离线消息存储、消息推送、心跳、分布式唯一ID、红包、消息同步等功能,并且还支持集群部署。
JackJiang
2023/06/09
1.4K0
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
移动互联网IM之协议设计
MelonTeam
2018/01/04
4.1K1
移动互联网IM之协议设计
Netty系列三、Netty实战篇
​ 这一篇我们就玩起来,通过一些常用的实战问题,来理解如何使用Netty进行网络编程。
全栈程序员站长
2022/11/17
1.3K0
Netty系列三、Netty实战篇
(八)高性能服务器架构设计总结1——以flamigo服务器代码为例
这篇文章算是对这个系列的一个系统性地总结。我们将介绍服务器的开发,并从多个方面探究如何开发一款高性能高并发的服务器程序。 所谓高性能就是服务器能流畅地处理各个客户端的连接并尽量低延迟地应答客户端的请求;所谓高并发,指的是服务器可以同时支持多的客户端连接,且这些客户端在连接期间内会不断与服务器有数据来往。 这篇文章将从两个方面来介绍,一个是服务器的框架,即单个服务器程序的代码组织结构;另外一个是一组服务程序的如何组织与交互,即架构。注意:本文以下内容中的客户端是相对概念,指的是连接到当前讨论的服务程序的终端,
范蠡
2018/04/04
1.1K0
推荐阅读
相关推荐
Web端即时聊天项目实现(基于WebSocket)
更多 >
LV.0
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档