十二、day12
之前的粘包处理是基于消息头包含的消息体长度进行对应的切包操作,但并不完整。一般来说,消息头仅包含数据域的长度,但是如果要进行逻辑处理,就需要传递一个id字段表示要处理的消息id,当然可以不在包头传id字段,将id序列化到消息体也是可以的,但是我们为了便于处理也便于回调逻辑层对应的函数,最好是将id写入包头。
之前我们设计的消息结构是这样的
而本节需要加上id字段
在此之前,先完整的复习一下基于boost::asio实现的服务器逻辑层结构
1. 服务器架构设计
1)asio底层通信
前面的asio底层通信过程如下图所示
1)首先,在应用层调用async_read时,相当于在io_context中注册了一个读事件,表示程序希望在指定socket上进行异步读取操作,并提供一个读回调函数以在读取完成后做相应的处理;
2)io_context用于管理所有异步操作和相应的回调函数,且当async_read被调用时,asio会将该socket对应的文件描述符、相应的读事件和回调函数注册到系统内部的模型中(数据结构);当io_context启动时,即io_context.run时,asio根据系统使用对应的模型管理这些事件(windows是iocp,linux是epoll);
3)模型进入一个死循环,会监听所有注册的socket,并监测其状态(可读?可写?),如果socket的状态发生变化(事件被触发),模型将该事件放入就绪事件队列中;
4)io_context::run 在轮询就绪事件队列时,会依次调用每个就绪事件的回调函数(已经放在就绪事件队列中),每个回调函数都包含了处理读操作的逻辑,比如读取数据、处理错误等。
2)逻辑层结构
而服务器架构除了上面的内容之外,一般还有一个逻辑层。
一般在解析完对端发送的数据之后,还要对该请求做更进一步地处理,比如根据不同的消息id执行不同的逻辑层函数或不同的操作,比如读数据库、写数据库,还比如游戏中可能需要给玩家叠加不同的buff、增加积分等等,这些都需要交给逻辑层处理,而不仅仅是把消息发给对端。
上图所示的是一个完成的服务器架构,一般需要将逻辑层独立出来,因为如果在解析完对端数据后需要执行一些复杂的操作,比如玩家需要叠加各自buff或者技能,此时可能会耗时1s甚至更多,如果没有独立的逻辑层进行操作,那么系统会一直停留在执行回调函数那一步,造成阻塞,直至操作结束。
而逻辑层是独立的,回调函数只需将数据投递给逻辑队列(回调函数将数据放入队列中之后系统会运行下一步,便不会被阻塞),逻辑系统会自动从队列中取数据并做相应操作,如果需要在执行完操作之后做相应回复,那么逻辑系统会调用写事件并注册写回调给asio网络层,网络层就是asio底层通信的网络层步骤。
以上操作是基于单线程,如果是在多线程的情况下,阻塞的情况会不会好一些?
asio的多线程有两种模式。
1)第一种模式是启动n个线程,每个线程负责一个io_context,每一个io_context负责一部分的socket。比如现在有两个io_context,一个是负责socket的id为奇数的io_context,一个是负责socket的id为偶数的io_context,但同样会造成阻塞的情况。因为不管是多线程还是单线程,只要在线程中有一个io_context中运行,那么它负责的那部分回调函数的处理操作如果比较复杂时,仍会造成阻塞的情况。
2)第二种模式是一个io_context跑在多个线程中,即多个线程共享一个io_context。这种模式下不会造成之前的那种阻塞情况,因为在就绪事件队列中的事件不是一个线程处理了,而是不同的线程共享一个就绪事件队列,不同线程会触发执行不同的回调函数,即使某个回调处理的比较慢,但由于其他事件被不同线程处理了,系统并不需要阻塞等待该回调处理完成之后在执行处理其他回调。
虽然模式二办法会解决系统阻塞、超时的问题,但在现实中需要有一个逻辑层独立于网络层和应用层,这样可以极大地提高网络线程的收发能力,用多线程的方式管理网络层。
2. 完善粘包处理操作
之前的消息结构并不完善,缺少一个消息id,本节进行代码的相应改进。
首先,之前的消息节点被收发共用,只不过收数据用的是第一种构造函数,发数据用的是第二种构造函数。为了减少耦合和歧义,需要重新设计消息节点。
1) 消息节点
重新构建一个MsgNode类,并派生出RecvNode 和SendNode
- MsgNode 表示消息节点的基类,头部的消息用该结构存储
- RecvNode 表示接收消息的节点
- SendNode 表示发送消息的节点
1 |
|
具体实现为:
1 |
|
Const.h 定义为
1 |
|
构建SendNode节点时,需要将消息id和消息长度转换为网络序,然后写入数据域_msg ,前2字节存储id,id后为消息长度,偏移4字节后为消息体内容。
2)Session类
Session类和前面差不多,不过需要把收发的逻辑做相应的修改
首先,队列_send_que、消息头结构、消息体结构需要重新声明,分别使用SendNode,RecvNode,MsgNode作为元素类型。
1 | std::queue<std::shared_ptr<SendNode> > _send_que; |
Session的构造函数也需要做相应变化,消息头结构的大小更改为4字节,包括id和消息体长度
1 | CSession(boost::asio::io_context& ioc, CServer* server) : _socket(ioc), _server(server), _b_close(false), |
重新定义Send函数,两个Send的重载都需要重新定义
参数列表增加msgid,构造发送节点时需输入三个参数msg, max_length, msgid(发送内容,内容长度,消息id)
1 | void CSession::Send(char* msg, int max_length, short msgid) { |
读回调也需更改,在文章10中haddle_write函数的基础上做修改,可参考该文章
https://zhuanlan.zhihu.com/p/722233898
1 | void CSession::HandleRead(const boost::system::error_code& error, size_t bytes_transferred, |
HandleRead函数中新增一段读取消息id的代码
首先,当消息头节点**_recv_head_node填充完毕后,获取头节点中存储的消息id并转换为本地字节序,并判断id的合法性;然后,解析消息长度,并构建消息体节点_recv_msg_node**,读取剩下的消息体内容
1 | short msg_id = 0; // 获取消息id |
3)客户端
客户端也需额外收发消息id
1 |
|