#1.粘包现象 每个TCP 长连接都有自己的socket缓存buffer,默认大小是8K,可支持手动设置。粘包是TCP长连接中最常见的现象,如下图
socket缓存中有5帧(或者说5包)心跳数据,包头即F0 AA 55 0F(十六进制),通过数包头数据我们确认出来缓存里有5帧心跳包,但是5帧数据彼此头尾相连粘合在了一起,这种常见的TCP缓存现象,我们称之为粘包。
#2.粘包原因 ##2.1. 同一客户端连续发送 同一客户端连续发送心跳数据,当TCP服务端还来不及解析(如果解析完会把缓存清掉)。造成了同一缓存数据包的粘合。
##2.2. 网络拥塞造成粘包 当某一时刻发生了网络拥塞,一会之后,突然网络畅通,TCP服务端收到同一客户端的多个心跳包,多个数据包会在TCP服务端的缓存中进行了粘合。
##2.3. 服务端卡死了 当服务端因为计算量过大或者其他的原因,计算缓慢,来不及处理TCP Socket缓存中的数据,多个心跳包(或者其他报文)也会在socket缓存中首尾相连,粘包。
总而言之,就是多个数据包在同一个TCP socket缓存中进行了首尾相连现象,即为粘包现象。
#3. 粘包的危害 由于粘包现象存在的客观性,我们必须人为地在程序逻辑里将其区分,如果不去区分,任由各个数据包进行粘连,有以下几点危害: ##3.1. 无法正确解析数据包 服务端会不断识别为无效包,告诉客户端,客户端会再次上报,因此会增加客户端服务端的运行压力,如果本身运算量很大,则会出现一些异常奔溃现象。
##3.2. 错误数据包被错误解析 无巧不成书,如果错误的粘包,凑巧被服务端进行成功解析,则会进行错误的Handler 处理。这样的错误处理方式危害会超过3.1。
##3.3. 进入死循环 如果频率过快,则会出现这种现象,服务器不断识别粘包为无效包,客户端不断上报,以此消耗CPU的占用率。
综上,我们必须要进行TCP的粘包处理,这是软件系统健壮性跟异常处理机制的基础。
#4. 粘包的逻辑处理方式 ##4.1. 根据包尾特征参数进行区分 规定几个字节为每帧TCP报文的包尾特征(比如4个字节),检索整个socket缓存字节,每当检测到包尾特征字节的时候,就划分报文,以此来正确分割粘包。 特征:需要检测每个字节,效率较低,适合短报文,如果报文很长则不适合。
##4.2. 根据包头包尾特征参数进行区分 与4.1相似,多了包头检测部分。 特征:只需检测第一帧的每个字节,第二帧只需检测包头部分,适合长报文
##4.3. 根据报文长度来进行粘包区分 根据报文长度偏置值,读第一帧的报文,从粘包中(socket缓存)划分出第一帧正确报文,找第二帧的报文长度,划分第二帧,以此划分到底。 举例:如下长度偏置为5(从0开始计算),即第6,第7字节为报文长度字节。
特征:只需检测报文长度部分,适合长短报文的粘包划分。
#5. 根据报文长度来区分粘包的代码落地——基于NewLife.Net的管道处理 ##5.1. NewLife.Net管道架构处理方式 Newlife.Net管道架构的设计,参考了java的Netty开源框架,因此大部分Netty的编解码器都可以在此使用。 具体在代码中的表现为
_pemsServer.Add(new StickPackageSplit { Size = 2 });即将LengthCodec这个编解码器加入到了管道中去,所有的message都会经过LengthCodec这里主要是解码功能,没有进行编码,解码成功后(粘包根据长度划分出多个有效包)推送到OnReceive方法中去。Size = 2表示报文长度是2个字节。 ##5.2. 跟http的管道类比 与Net Core 的WEBAPI项目的管道添加,是否发现似曾相识?
app.UseAuthentication(); app.UseRequestLog(); app.UseCors(_defaultCorsPolicyName); app.UseMvc();管道添加的先后顺序即数据流流经管道的顺序。只是没去追求是先有socket的管道处理机制,还是http 上下文的管道处理机制。但是道理是相同的。
##5.3.拆分粘包解码器(根据长度解码) ###5.3.1. 长度偏移地址Offset属性 长度所在位置的偏移地址。默认为5,解释详见4.3。
// // 摘要: // 长度所在位置 public int Offset { get; set; } = 5;###5.3.2.长度字节数Size属性 本文讨论长度字节数为2,详见4.3
// // 摘要: // 长度占据字节数,1/2/4个字节,0表示压缩编码整数,默认2 public int Size { get; set; } = 2;