TCP粘包和拆包问题
TCP
是⼀个
“
流
”
协议,所谓流,就是
没有界限的⼀⻓串⼆进制数据
。
TCP
作为传输层协议并不了解上层业务数据的具体含义,它会根据TCP
缓冲区的实际情况进⾏数据包的划分,所以在业务上认为是⼀个完整的包,可能会被TCP
拆分成多个包进⾏发送,也有可能把多个⼩的包封装成⼀个⼤的数据包发送,这就是所谓的
TCP
粘包和拆包问题。
产⽣
TCP
粘包和拆包的原因
我们知道
TCP
是以流动的⽅式传输数据的,传输的
最⼩单位
为⼀个报⽂段(
Segment
)。
TCP Header
中有个Options标识位。常⻅的标识位为
MSS
(
Maximum Segment Size
)指的是,连接层每次传输的数据有个最⼤限制MTU
(
Maximum Transmission Unit
),⼀般是
1500bit
,超过这个量要分成多个报⽂段,
MSS
则是这个最⼤限制减去TCP
的
header
,光是要传输的数据的⼤⼩,⼀般为
1460bit
。换算成字节,也就是
180
多字节。 TCP
为提⾼性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了以后,再将缓冲中的数据发送到接收⽅。同理,接收⽅也有缓冲区这样的机制来接受数据。 发⽣TCP
粘包、拆包主要是以下原因:
(
1
)应⽤程序写⼊数据⼤于套接字缓冲区⼤⼩,会发⽣拆包;
(
2
)应⽤程序写⼊数据⼩于套接字缓冲区⼤⼩,⽹卡将应⽤多次写⼊的数据发送到⽹络上,这将会发送粘包;
(
3
)进⾏
MSS
(最⼤报⽂⻓度)⼤⼩的TCP分段,当
TCP
报⽂⻓度
——TCP header
⻓度
>MSS
的时候会发⽣拆包;
(
4
)接收⽅法不及时读取套接字缓冲区数据,这将发⽣粘包。
如何处理粘包和拆包
假设应⽤层协议是
http
我从浏览器中访问了⼀个⽹站,⽹站服务器给我发了
200k
的数据。建⽴连接的时候,通告的
MSS
是
50k
,所以为了防⽌ip
层分⽚,
tcp
每次只会发送
50k
的数据,⼀共发了
4
个
tcp
数据包。如果我⼜访问了另⼀个⽹站,这个⽹站给我发了100k
的数据,这次
tcp
会发出
2
个包,问题是,客户端收到
6
个包,怎么知道前
4
个包是⼀个⻚⾯,后两个是⼀个⻚⾯。既然是tcp
将这些包分开了,那
tcp
会将这些包重组吗,它送给应⽤层的是什么?这是 我⾃⼰想的⼀个场景,正式⼀点讲的话,这个现象叫拆包
。
我们再考虑⼀个问题。
tcp
中有⼀个
negal
算法,⽤途是这样的:通信两端有很多⼩的数据包要发送,虽然传送的数据很少,但是流程⼀点没少,也需要tcp
的各种确认,校验。这样⼩的数据包如果很多,会造成⽹络资源很⼤的浪费,
negal
算法做了这样⼀件事,当来了⼀个很⼩的数据包,我不急于发送这个包,⽽是等来了更多的包,将这些⼩包组合成⼤包之后⼀并发送,不就提⾼了⽹络传输的效率的嘛。这个想法收到了很好的效果,但是我们想⼀下,如果是分属于两个不同⻚⾯的包,被合并在了⼀起,那客户那边如何区分它们呢?这就是粘包问题。
从粘包问题我们更可以看出为什么
tcp
被称为流协议,因为它就跟⽔流⼀样,是没有边界的,没有消息的边界保护机制,所以tcp
只有流的概念,没有包的概念
。
我们还需要有两个概念:
(
1
)
⻓连接
:
Client
⽅与
Server
⽅先建⽴通讯连接,连接建⽴后不断开, 然后再进⾏报⽂发送和接收。
(
2
)
短连接
:
Client
⽅与
Server
每进⾏⼀次报⽂收发交易时才进⾏通讯连接,交易完毕后⽴即断开连接。此种⽅式常⽤于⼀点对多点 通讯,⽐如多个Client
连接⼀个
Server
。
实际
,我想象的
关于粘包的场景是不对的
,
http
连接是短连接,请求之后,收到回答,⽴⻢断开连接,不会出现粘包。 拆包现象是有可能存在的。
处理拆包这⾥提供两种⽅法:
(
1
)通过包头
+
包⻓
+
包体的协议形式,当服务器端获取到指定的包⻓时才说明获取完整。
(
2
) 指定包的结束标识,这样当我们获取到指定的标识时,说明包获取完整。
处理粘包我们从上⾯的分析看到,虽然像http这样的短连接协议不会出现粘包的现象,但是⼀旦建⽴了⻓连接, 粘包还是有可能会发⽣的。处理粘包的⽅法如下:
(
1
)发送⽅对于发送⽅造成的粘包问题,可以通过关闭
Nagle
算法来解决,使⽤
TCP_NODELAY
选项来关闭算法。
(
2)接收⽅没有办法来处理粘包现象,只能将问题交给应⽤层来处理。应⽤层的解决办法简单可⾏,不仅能解决 接收⽅的粘包问题,还可以解决发送⽅的粘包问题。解决办法:循环处理,应⽤程序从接收缓存中读取分组时,读完⼀条数据,就应该循环读取下⼀条数据,直到所有数据都被处理完成。
判断每条数据的⻓度的⽅法有两种:
a.
格式化数据:每条数据有固定的格式(开始符,结束符),这种⽅法简单易⾏,但是选择开始符和结束符时⼀定要确保每条数据的内部不包含开始符和结束符。
b.
发送⻓度:发送每条数据时,将数据的⻓度⼀并发送,例如规定数据的前
4
位是数据的⻓度,应⽤层在处理时可以根据⻓度来判断每个分组的开始和结束位置。