Skip to content
字数
1375 字
阅读时间
6 分钟

什么是粘包?

TCP 是一个面向字节流的协议,它不关心应用层传递的数据包的边界。为了提高传输效率,TCP 默认启用了 Nagle 算法,该算法会在数据发送前进行缓存。如果短时间内有多个小的数据包要发送,TCP 会将它们合并(“粘”)在一起,作为单个数据包一次性发送出去。

这种机制对于文件传输等场景非常高效,但对于需要明确消息边界的应用(如 RPC 调用、即时通讯)来说,就会引发问题。

示例:客户端连续调用两次 send,分别发送 data1data2

在接收端,可能出现以下情况:

  • A. 正常:先收到 data1,再收到 data2
  • B. 粘包:先收到 data1 的一部分,然后收到 data1 的剩余部分和 data2 的全部。
  • C. 粘包:先收到 data1 的全部和 data2 的一部分,然后收到 data2 的剩余部分。
  • D. 粘包:一次性收到了 data1data2 的全部数据。

情况 B、C、D 就是典型的“粘包”现象。此外,如果一个数据包过大,也可能被 TCP 拆分成多个包发送,导致接收方需要多次接收才能得到一个完整的消息,这被称为“拆包”。粘包和拆包问题通常需要一起处理。

解决方案

方案 1:发送间隔等待

在两次 send 操作之间插入一个短暂的等待时间。

  • 优点:实现简单,几乎无需额外代码。
  • 缺点:严重牺牲传输效率,只适用于交互频率极低的场景。

方案 2:关闭 Nagle 算法

在 Node.js 中,可以通过 socket.setNoDelay(true) 来禁用 Nagle 算法,使得每次 send 都立即发送数据,不进行缓冲。

  • 优点:可以有效减少发送端的粘包。
  • 缺点
    • 增加了网络 I/O 的开销,降低了传输效率,尤其是在发送大量小数据包时。
    • 无法解决接收端的粘包问题。如果接收方处理不及时或网络状况不佳,多个数据包仍然可能在接收方的缓冲区中“粘”在一起。

方案 3:封包与拆包(推荐)

这是业界最常用、最可靠的解决方案。其核心思想是在应用层自行定义消息的边界。常见的方法有两种:

a. 固定长度消息

约定所有消息都具有相同的长度。如果消息实际内容不足,则用特殊字符(如空格)填充。

  • 优点:实现简单,接收方每次读取固定长度的数据即可。
  • 缺点:浪费带宽,灵活性差。

b. 长度前缀 + 消息体

在每个消息包的头部增加一个固定长度的字段,用于描述紧随其后的消息体的长度。

  • 流程

    1. 发送方发送数据 = 消息长度 (4字节) + 消息体
    2. 接收方
      • 先读取 4 字节,解析出消息体的长度 L
      • 然后,持续从缓冲区读取数据,直到读取了 L 字节的数据,这就构成了一条完整的消息。
      • 循环此过程,处理后续数据。
  • 优点:灵活性高,解决了粘包和拆包问题,是目前最主流的方案。

c. 特殊分隔符

在每个消息包的末尾增加一个特殊的分隔符(如换行符 \n)。

  • 流程:接收方持续读取数据,直到遇到分隔符,此时就认为之前的数据构成了一条完整的消息。
  • 优点:实现相对简单。
  • 缺点
    • 需要遍历数据来查找分隔符,效率较低。
    • 消息体本身不能包含分隔符,否则会引起歧义。
    • 适用于文本协议,如 Redis、HTTP 等。

TCP 的可靠传输机制

TCP 本身通过序列号(SYN)和确认应答(ACK)机制来保证数据传输的可靠性、有序性和完整性

  • 序列号 (Sequence Number):TCP 将每个字节的数据都进行了编号,接收方可以根据序列号对乱序的数据包进行重排。
  • 确认应答 (Acknowledgement):接收方每收到一个数据包,都会返回一个 ACK 确认包。
  • 超时重传 (Retransmission):发送方如果在一定时间内没有收到某个数据包的 ACK,就会认为该包已丢失,并进行重传。

这些机制保证了应用层最终能收到一个不多、不少、不乱的字节流,但也正是这个“流”的特性,导致了粘包问题的产生。应用层需要自己负责从这个流中识别出消息的边界。

参考阅读《TCP的那些事儿(上)》

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写