怎么理解 TCP 粘包与拆包?
记得一个大佬跟我说过, “TCP 哪有什么粘包拆包的问题, 人家本来就是流式协议, 你用它传你的结构化数据, 你是做数据的序列化和反序列化, 而不是在克服 TCP 的缺点.”
这样的理解是否准确呢, 大家什么看法. 本菜鸡感到疑惑~
记得一个大佬跟我说过, “TCP 哪有什么粘包拆包的问题, 人家本来就是流式协议, 你用它传你的结构化数据, 你是做数据的序列化和反序列化, 而不是在克服 TCP 的缺点.”
这样的理解是否准确呢, 大家什么看法. 本菜鸡感到疑惑~
TCP 流式传输本质上并没有解决上层应用程序员的问题,程序员要的是 类似 HTTP Websocket 这种一问一答的设计,而且这种设计的可靠性由底层来维护,上层的应用逻辑不需要写代码来尽可能维护这个数据传输可靠性,但是除了 TCP 一个能打都没有,你就只能用 UDP,但 UDP 既不保证 包的顺序,也不保证包一定送到,还得写一大堆逻辑去维护会话状态,所以大家纷纷用 TCP 来做可靠性,像 HTTP 里面 实际上就是浏览器用 HTTP 头告诉 http 服务器,我剩下的 body 内容有多少,服务器兄弟你慢慢读,读完这么多就算是我一个 http 请求的包,服务器兄弟你千万别搞混球了把下一个 http 的报文头 一起当一个 http 包给解析了。
工作是负责搭一个 Server, 用来接收一些智能家电传上来的数据;
这些数据是硬件厂商自定义的协议, 开头和结尾有一些标记 整体格式如 [ 起始码 长度 数据 结束码 ]
对于我服务器来说, 我当然是希望每次取到的消息都是一条完整的消息, 不多不少不错刚刚好
但是实际上, 因为对于 tcp 来说这个, 他在传输的时候, 并不会理会你应用层是怎么定义的, 只管往外发
我上面说的一条数据, 在到达我服务器的时候, 可能被分成了好几个数据包
这个时候就是所谓的拆包了, 如果服务端不对取到的数据做合法校验的话, 就会出问题了
服务端这个时候就要对取到的数据, 根据应用层定义的协议, 当满足协议的时候, 才将数据从 buffer 中取出
然后再进行业务操作.
关键是理解 TCP 的流的概念吧~ 这个流是一个没头没尾的东西, 所以需要长度, 标记符之类的来定义数据的边界
个人的理解大致是这样吧.
至于为什么一定要有区分?设备几毫秒发送几十个包都有,经常发生的情况是一个包附带好几个剩下的包作为一次发送的数据。为了确保每一个包内容完整无错,所以必须要有处理粘包的逻辑,最常见的就是固定包头包尾+包内容转义+内容长度位+校验码
有些 iot 设备发包频率低,可能几十秒才发一个包,理论上这样随便基于 tcp 自定义协议都可以,粘不粘包完全不用考虑,但是实际情况是,一个服务器可能会处理上万十万个 iot 设备连接,你这个设备发送的报文后面马上跟着其它设备的报文完全是可能发生的
2.遇到 [黏包问题] 的人,基本上都是因为这方面学习不过关。
3. [黏包问题] 的发展史是:
很多年前,一堆基础不扎实的黑客,以及上一代的程序员们,经常用 VC 写各种 Windows 客户端程序。一方面他们需要进行通信编程,另一方面由于这部分内容对他们的水平而言比较深奥,他们掌握不好,因而采用“如何简单就如何来”的策略,首当其冲就是 udp 。
VC 的 UDP 编程,一条数据发一个数据包,自然不存在界限问题;同时 VC 的 UDP 编程,代码简单,收发逻辑简单明了,因此深受这群人的欢迎与追捧。
接着,天时地利人和,这群人的时代,恰好是单物理核 CPU 大行其道,最多也只会遇上单物理核的超线程 CPU,因此即使把 UDP 换成 TCP,只要模块拆分得当,就算是并行的程序,也不易黏包。不过也只是不易而已,这些程序时刻有黏包出错的风险,因此,他们那时的程序,很不稳定,经常需要关闭重启。
他们的历史,也就到此为止了。
由于通信应用的复杂性,以及用户需求进一步提出更高的要求,当他们尝试多条数据放在一个 UDP 数据包里进行发送时,就会出现所谓的 [黏包] 问题。这是其一。他们所谓的 [粘包] ,也就是他们本来的收发逻辑是,一次发送或接收,只处理一个数据包,然后一个数据包里,只存放一条数据。现在他们把多条数据放在一个数据包里,甚至多条数据放在多个数据包里,于是在发送与接收的业务就全变了,同时又需要引入合并、拆分、处理顺序以及考虑性能问题,整个流程变得非常复杂,他们处理不了,程序经常出现问题。
UDP 功能有限,因此他们总会需要用到 TCP 。此时,由于水平的局限性,当他们按照 UDP 的方式来进行 TCP 通信时,就非常容易遇到所谓的 [黏包] 问题。这是其二。
这帮人水平本来就欠缺提高,再遇上那时双物理核以及更多物理核的 CPU 开始普及,多线程与并行编程就跟着开始流行,这对于程序员提出了更高的要求。此时,这群人在并行程序里按照以前串行程序的老路子进行通信,注定会遇到黏包问题。这是其三。
以上三种场景,被这帮人把这个问题总结出来,称之为 [黏包] 问题,并且在网络各处留下这个问题的详述,以及一堆古灵精怪嘀笑皆非的解决方案。
TCP 本来是一个流传输协议(数据不分块,保证有序,也就是只保证多次 recv 的数据依顺序拼起来保持原样),偏要把它当成一个消息传输协议(数据分块,保证有序,保证一次 send 对应一次 recv )来用。
不巧的是在环回网络以及短途网络里调试时,这种误用并不会出问题。
如果不是性能或者效率原因,建议用 WebSocket,这是正儿八经的消息协议,可以省掉定界的麻烦。
建议通过一个实际场景去学习
尝试写个远程调用 或者跨语言调用 的例子
写出来 你就不会再想粘包的概念了
它并不关心 Payload 中的数据格式,这里所说的“包”的包实际上是上层应用层协议的 Header+body,并不是 TCP 数据包。
定义应用层协议时,一般需要满足下面三种需求之一:
– 固定协议包长度(可拓展性低)
– 指定协议包终结符,例如 redis 的 RECP 协议
– 指定一个 Length 字段,表明协议包长度,例如 HTTP 协议
根据这三个需求,在处理 TCP 流时找到应用层协议包的分界,就能拆解出一个个单独的应用层包。
我原来是粘包警察
现在是面试别人的时候,故意挖坑,谁掉进去就直接不通过( ̄∇ ̄)
如果只是为了传输数据,谁没事手撸 TCP 啊,protobuf 不香吗?
这个字节流可以理解为 C#中的 Stream 或者 IEnumerable<byte>这两个编程接口。
TCP 的粘包是因为,本身就没有包的概念。你取到的字节数组有概率和传输的字节数组的“窗口”相同,给了你“包”的错觉,只是因为运气好罢了(从 IP 层的缓冲取走数据及时,并且中间的链路畅通)。
个人认为,粘包说法很形象,没必要揪着流式协议这一点不放。
@Orenoid @Mutoo 实际上这名字也不叫拆包叫分帧……
上面某篇文章看了下,IP 叫包,TCP 叫段,UDP 叫报,SCTP 和 WebSocket 叫消息,比包包包包清晰多了。
实际上拆包并包应当是存在的,比如 TCP 一个段的数据超过 IP 包大小上限时 src 发生拆包,之后 dst 再并包提交给 TCP 层。
至于反过来两个段分别放两个包提交出去不会被 IP 层粘在一起,除非是 TCP 中继,解包重构流再重新分段发包。
水按一杯一杯地来,是你的需求,不是说你把水看成流就能绕过去了。你看成流你也要把水装到一个个杯子里。而且实际编程中,你也不是想控制一次取多少就取多少,实际中因为 API 的关系水是一桶一桶来的。
不过我觉得这个不是重点,只要做过这一块相关的开发,就都能理解大家说的其实是一个问题。我想强调的点还是在于,不该使用“粘包”这个叫法,是因为很容易误导新手。比如上面一位老哥提到的,“在编程实践中不能理解明明我 send 了 5 次为什么我 recv 一次就收到了所有的数据”,新手本来就理解不透彻,然后网上一搜,再看到一些不懂装懂的“粘包”文章,就被彻底带歪了。
2.需要实现把传输层的 TCP 数据流拆成应用层的包,也是客观事实。
(无论你是否接受发明粘包与拆包这一对名词,只要传输层用了 TCP,应用层又想用包,就得这么干没错吧)
以上两点,这应该是没有什么可争议的。
1 本来就是设计目的 2 是一个类似封装的事,是分层模型自然而然的事。
搞不清楚的话,建议重新系统学习 计算机网络 课程。
@k8ser 这里又有一个词义问题,拆包到底是 1. 给定一个包,解析包并提取包体;还是 2. 给定一个包,分拆成 N 个包;还是 3. 给定流,分拆成若干包? 1 是 unpack 。2 是 split packet ? repack ? 3 才是主题提出的问题,没有明确名称但 framing 是一个选项
@eastphoton (其实 TCP 数据流不一定要解析成包,如果 HTTP/1.0 那样不允许 keep alive 就没这个问题,其次还有像 telnet 那样不进行重新封装在上层仍然是流的话也不需要)
小包问题我想知道你说的情况有实际应用吗
我觉得对于需要 tlv 编码的应用来讲
用流语义好像并没有带来收益
包大小变换问题其实会发生在 MTU 改变、以及数据通过隧道导致内外包限不同的情况
前提条件是 TLV 的话确实流没什么好处,流是为了避免这一层作太多强制。
算是历史原因。
如果大家都用 SCTP 或者 SCTP 比 TCP 、UDP 更早被设计出来的话也没 TLV 这茬了。