3 处理客户端会话
Last updated
Last updated
TCPSession,用于处理和客户端的会话信息,保存了和客户端通信的socket,主要职责为
负责接收客户端消息;
创建并绑定对应的LinkUser对象;
为LinkUser对象向客户端发送消息。
其UML如下
首先,处理客户端上传消息
TCPSession中的m_socket是AsioServer传递过来的,Accecpt返回一个新的socket,之后客户端和服务端的通信就在这个socket上处理。
TCPSession定义了一个缓冲区m_szData,其大小是1024字节,会话开始后,会一直去缓冲区中读取数据,如果读到数据,则调用HandleMsg进行消息处理。
服务端如何解析客户端传上来的消息?
这就涉及到一个很重要的概念:应用层协议。
这里说的协议,是网络通信协议的简称,它是网络通信双方必须共同遵同的一组约定。
常见的应用层协议有http/https协议(超文本传输协议),SMTP(电子邮件协议),POP3(邮件读取协议),Telnet(远程终端协议),FTP(文件传输协议)等等。
作为一个网络通信服务框架,也要定义自己的应用层通信协议。
我们将一条消息分为两部分,消息头和消息体。
消息头中需要包含几个信息:
消息来源
消息类型
消息体长度
消息来源,用来标识消息发送者的身份,可以用来区别是用户客户端,还是其他的关联服务,每一个端都可以事先定义好编号,统一定义。服务端收到消息后,根据消息来源创建不同的LinkUser,进而处理这类User的一组具体协议。
消息类型,用于进一步确定此消息的用途,比如区分创建用户,登陆请求,找回密码等不同的行为。
消息来源和消息类型的定义,是为了服务框架更好的将消息进行路由,不同类型的消息进入不同的处理分支,使服务端的处理流程更清晰明了。
消息体长度,是用来确认后面消息内容的长度。从socket中取出的数据,是一段二进制数据流,这段数据可能是一条消息,也可能是多条消息,也有可能是一条消息的一部分。具体的处理,后面结合函数再详细说明。
因此,消息头的定义如下:
消息头共三个字段,占8个字节。unsigned shot的表示范围为0-65535,unsigned int的表示范围是0 ~ 2^32 -1,最大为42亿多。
消息头后面紧跟消息体,消息头中dwLength字段,表示消息体长度。
消息体可以采用多种方式进行编码,常见的编码方式有C++结构体,json,protobuf。
以注册消息为例,假如注册需要用户名(最长32字节),密码(最长16字节),邮箱(最长64字节),手机号(最长16位)
C++结构体定义如下
对于不定长的数据,也可以使用偏移+长度的方式,将数据缀在消息后面,这里先使用简单的方式。
使用时,给对应的变量赋值即可,最后整个结构体放在header后面,就是一个完整的消息。
json表示如下
json表示的好处在于自解释性,看到文本,很容易分辨出消息含义,不需要额外的配置和说明。
TCP协议中传输得是二进制字节流,asio通过async_read_some方法,从缓冲区中取出数据,保存在成员变量m_szData中,并返回了数据长度length。
因为m_szData是一个1024字节得char数组,在获取数据时也传入了最大数据长度1024,因此,一次获取数据得长度是小于等于1024。
而这些数据,可能是一条消息(图1),可能是多条消息(图2),也可能是半条消息(图3),这就需要准确得划分消息,业务层才能正确得解析并处理。
图1很好处理,我们只需先读取m_szData的前8个字节,识别第5-8字节表示的数据长度,再根据数据长度读取后面的Data数据即可。这样就能读取一个完整的消息了。
图2比图1略微复杂,但原理是一致的,同样先读取Header,识别dwLength长度,根据长度读取Data。有时消息可能只有Header,没有Data,如图2中的第三个消息,这是dwLength数值应该为0,继续读取下一个消息的Header即可。
图3中表示一条消息太长,超过1024,或者多条消息中的最后一条消息超出了本次m_szData读取的数据范围,这时就要将消息的前半部分先缓存起来,继续进行下一次缓冲区读取,并且在新的m_szData中获取消息剩余部分的数据。
所以,读取消息的逻辑是:
循环读取缓冲区m_szData中的数据,直到达到length长度
先读取一个Header的长度,即8字节,保存到当前消息缓存内(m_buf)
根据Header中的dwLength长度,读取后续的数据内容
读取完一条消息后,交由业务层处理。业务层处理时,保证消息数据一直有效(m_buf)
业务层消息处理函数执行完成后,清空上一条消息缓存,继续读取下一条消息
m_szData缓存中的数据都已经读完,但当前消息仍不完整,先在m_buf中缓存起来,下一次继续读完当前消息(消息头或消息体)
基于上述逻辑,我们实现消息读取的代码
在读取消息时,可能出现以下几种异常情况,需要对应处理
1、Header读取异常
读取不到一个完整的Header,即数据缓存中不足8个字节。
如果当前缓冲区m_szData的长度,已经达到上限(1024),则Header可能在下一次获取的数据中,正好将一个Header截为两段,这种情况是正常的。
如果当前缓冲区未达到上限,且数据读取完了,m_szData中只剩下5个字节未处理,读取的Header长度不足8个字节,则可能出现了数据异常。
此时需要打印对应的错误日志,并关掉当前的Session。
关掉Session的目的有两个作用:一是防止后续消息继续解析错误,导致业务逻辑异常;二是避免连入的客户端时非法客户端,因此不再为其服务。
2、Header长度异常
读取完Header之后,会根据Header记录的数据长度读取后面的数据,如果Header的长度数据太大(超过64K),我们认为消息是非法的,这是网络双方的一个约定。因此发现dwLength超过64K,则可能是数据读取异常,比如上一条消息标记的长度(192)与实际长度(200)不一致,导致下一条消息Header截取错了。
此时也需要打印相应的错误日志,并关掉当前的Session。
3、Data数据不够
Header读取完成之后,得到Data的数据长度,如果本次缓存读完,Data不全,可能在下次读取时补全,这是正常的;若本次缓存length没有达到最大值,后面没有数据了,但是Data仍然不全,则可能出现错误。
其表现形式和第一条,Header读取异常类似。