IM即时通讯设计 高并发聊天服务:服务器 + qt客户端(附源码)
来源:微信公众号「编程学习基地」
IM即时通信程序设计
界面相对简陋,主要界面如下
- 登录界面
- 注册界面
- 聊天界面
- 添加好友界面
支持的功能
- 注册账号
- 登录账号
- 添加好友
- 群聊
- 私聊
后续UI美化以及功能增加持续更新,关注微信公众号「编程学习基地」最快咨询..
IM即时通讯
本系列将带大家从零开始搭建一个轻量级的IM服务端,麻雀虽小,五脏俱全,我们搭建的IM服务端实现以下功能:
- 注册
- 登录
- 私聊
- 群聊
- 好友关系
第一版只实现了IM即时通讯的基础功能,其他功能后续增加.
设计一款高并发聊天服务需要注意什么
- 实时性
在网络良好的状态下服务器能够及时处理用户消息
- 可靠性
服务端如何防止粘包,半包,保证数据完全接收,不丢数据,不重数据
- 一致性
保证发送方发送顺序与接收方展现顺序一致
实时性就不必细说了,保证服务器能够及时处理用户消息就行,重点说下可靠性
如何设计可靠的消息处理服务
简单来说就是客户端每次发送的数据长度不定,服务端需要保证能够解析每一个用户发送过来的消息。
这就涉及到粘包和半包,这里说下粘包和半包是什么情况
什么是粘包
多个数据包被连续存储于连续的缓存中,在对数据包进行读取时无法确定发生方的发送边界.
例如:客户端需要给服务端发送两条消息,发送数据如下
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
};
添加好友的流畅比较复杂,我在设计的时候也卡了一下。
主要流程如图
- 客户端A给服务器发送添加好友的请求
AddFriendInfoReq
,服务器解析请求将B的信息添加到客户端A的好友表中。 - 服务器B给客户端B转发好友请求。
- 客户端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
IM即时通讯设计 高并发聊天服务:服务器 + qt客户端(附源码)的更多相关文章
- cesium结合geoserver利用WFS服务实现图层编辑(附源码下载)
前言 cesium 官网的api文档介绍地址cesium官网api,里面详细的介绍 cesium 各个类的介绍,还有就是在线例子:cesium 官网在线例子,这个也是学习 cesium 的好素材. 内 ...
- cesium结合geoserver利用WFS服务实现图层删除(附源码下载)
前言 cesium 官网的api文档介绍地址cesium官网api,里面详细的介绍 cesium 各个类的介绍,还有就是在线例子:cesium 官网在线例子,这个也是学习 cesium 的好素材. 内 ...
- 即时通信系统中实现全局系统通知,并与Web后台集成【附C#开源即时通讯系统(支持广域网)——QQ高仿版IM最新源码】
像QQ这样的即时通信软件,时不时就会从桌面的右下角弹出一个小窗口,或是显示一个广告.或是一个新闻.或是一个公告等.在这里,我们将其统称为“全局系统通知”.很多使用C#开源即时通讯系统——GGTalk的 ...
- 多线程高并发编程(12) -- 阻塞算法实现ArrayBlockingQueue源码分析(1)
一.前言 前文探究了非阻塞算法的实现ConcurrentLinkedQueue安全队列,也说明了阻塞算法实现的两种方式,使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出 ...
- 多线程高并发编程(11) -- 非阻塞队列ConcurrentLinkedQueue源码分析
一.背景 要实现对队列的安全访问,有两种方式:阻塞算法和非阻塞算法.阻塞算法的实现是使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出队和入队各一把锁LinkedBloc ...
- Netty服务端与客户端(源码一)
首先,整理NIO进行服务端开发的步骤: (1)创建ServerSocketChannel,配置它为非阻塞模式. (2)绑定监听,配置TCP参数,backlog的大小. (3)创建一个独立的I/O线程, ...
- cesium结合geoserver利用WFS服务实现图层新增(附源码下载)
前言 cesium 官网的api文档介绍地址cesium官网api,里面详细的介绍 cesium 各个类的介绍,还有就是在线例子:cesium 官网在线例子,这个也是学习 cesium 的好素材. 内 ...
- Spring Cloud 微服务实战——nacos 服务注册中心搭建(附源码)
作为微服务的基础功能之一的注册中心担任重要的角色.微服务将单体的服务拆分成不同的模块下的服务,而不同的模块的服务如果进行通信调用呢?这就需要服务注册与发现.本文将使用阿里开源项目 nacos 搭建服务 ...
- (原创)通用查询实现方案(可用于DDD)[附源码] -- 设计思路
[声明] 写作不易,转载请注明出处(http://www.cnblogs.com/wiseant/p/3988592.html). [系列文章] 通用查询实现方案(可用于DDD)[附源码] -- ...
随机推荐
- XenServer删除ISO存储!
1.用命令 df -hal 可以看到 ISO库是使用了10G的硬盘的 2.下面开始直接右键删除ISO,但看到资源还是占用着10G的 3.如果想把这10G的硬盘资源空出来的话,只要复制前面查找到挂载的路 ...
- netcore项目中IStartupFilter使用
背景: netcore项目中有些服务是在通过中间件来通信的,比如orleans组件.它里面服务和客户端会指定网关和端口,我们只需要开放客户端给外界,服务端关闭端口.相当于去掉host,这样省掉了些指定 ...
- SpringCloud升级之路2020.0.x版-38. 实现自定义 WebClient 的 NamedContextFactory
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 实现 WeClient 的 NamedContextFactory 我们要实现的是不同微服 ...
- [bzoj2432]兔农
将每一个重置为0的点作为一段,那么它会导致后面为以x x为开头的斐波拿起数列的东西,那么设这一段是以x为开头,要快速转移到下一段,就可以解决这道题目为了转移,我们要处理出下面的东西:1.求出x关于模k ...
- 基于nexus私服配置项目pom.xml和maven settings.xml文件
备注:搭建nexus私服请参考上一篇文章基于Docker搭建Maven私服Nexus,Nexus详解 一:将jar发送到nexus私服务器 1.pom.xml文件添加配置 pom.xml文件中的这个版 ...
- 【程序员翻身计划】Java高性能编程第一章-Java多线程概述
目标 重点: 线程安全的概念 线程通信的方式与应用 reactor线程模型 线程数量的优化 jdk常用命令 Netty框架的作用 难点 java运行的原理 同步关键字的原理 AQS的抽象 JUC的源码 ...
- Python的数据解析
- FESTUNG — 3. 采用 HDG 方法求解对流问题
FESTUNG - 3. 采用 HDG 方法求解对流问题[1] 1. 控制方程 线性对流问题控制方程为 \[\begin{array}{ll} \partial_t c + \nabla \cdot ...
- 【系统硬件】英伟达安培卡 vs 老推理卡硬件参数对比
欢迎关注我的公众号 [极智视界],回复001获取Google编程规范 O_o >_< o_O O_o ~_~ o_O 本文分享一下英伟达安培卡 vs 老推理 ...
- 日常Java 2021/11/9
线程的优先级 每一个Java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序.Java线程的优先级是一个整数,其取值范围是1(Thread.MIN_PRIORITY ) -10 (Thread ...