当前位置:当前位置:首页 >应用开发 >TCP 粘包?粘包警察是什么梗? 正文

TCP 粘包?粘包警察是什么梗?

[应用开发] 时间:2025-11-05 11:23:54 来源:益强IT技术网 作者:系统运维 点击:139次

​本文围绕 TCP​ 协议展开,包粘包先来回顾下 TCP 协议的包粘包特点:

TCP 是面向连接的传输层协议。每一条TCP​ 连接只有两个端点,包粘包每一条TCP 连接只能是包粘包点对点的(一对一)。TCP 提供可靠的包粘包交付服务,保证传输的包粘包数据无差错、不丢失、包粘包不重复且有序。包粘包TCP​ 提供全双工通信,包粘包TCP​ 允许通信双方的包粘包应用进程在任何时候都能发送数据,为此TCP 连接的包粘包两端都设有发送缓存和接收缓存,用来临时存放双向通信的包粘包数据。TCP​ 是包粘包面向字节流的。(本文重点)

虽然应用程序和 TCP​ 的包粘包交互是一次一个数据块(大小不等),但 TCP​ 把应用程序交下来的包粘包数据看成仅仅是一连串的无结构的字节流。

粘包警察由来?粘包由来?

粘包警察 ,IT技术网一词首次看到是在 v2​。粘包警察认为 “粘包” 这词侮辱了 TCP​,在 TCP​ 下讨论 “粘包” 是伪命题。相反,粘包学家认为 “粘包” 就是 TCP​ 问题。遂粘包警察频频现身『TCP​粘包』帖子下,试图改正这偏见,提醒各位: TCP 是面向字节流的。

粘包由来小故事:

据说以前有一群基础不扎实的程序员经常使用 VC​ 写各种 Windows​ 客户端程序,喜欢使用 UDP​ 编程(VC​ 的 UDP​ 编程,代码简单,收发逻辑简单明)。 因为通讯应用的复杂性以及需求需要,他们尝试将多条数据放在一个 UDP​ 数据包里进行发送,遂碰到『粘包问题』。同时他们开始接触并使用 TCP​,惯性思维套用之前 UDP​ 编程方式来使用 TCP​,非常容易遇到所谓的 『粘包问题』。随着硬件升级,多物理核的 CPU 普及,多线程与并行编程开始流程,对程序员基本功提出更高的要求,b2b供应网这群人仍在并行程序使用串行思维进行编程,必定遇到『粘包问题』。 于是这群人把这个问题总结出来,称之为 『粘包问题』。

什么是粘包/拆包?

所谓粘包: 就是几个数据包粘在一起了,如果要处理得先拆包。

所谓拆包: 就是收到一批数据包碎片,要把这些碎片粘起来才能合成一个完整的数据包。

举个栗子:客户端发送数据给服务端,可能会出现以下五种情况:

栗子一:客户端分别发送完整的数据包 A 和 B,服务端先接收了完整数据包 A,没有出现拆包/粘包问题。栗子二:客户端一次一口次发送 A 和 B 粘在一起的数据包,服务端接收到这个数据包,服务端需要解析出 A 和 B,出现粘包问题。栗子三:客户端发送A|B-1​数据包和B-2数据包,服务端先接收到完整的 A 和 B 的一部分数据包 B-1,服务端需要解析出完整的 A,并等待读取完整的 B 数据包,出现粘包/拆包问题。栗子四:客户端发送A-1​数据包和B|A-2数据包,云服务器服务端接收到 A 的一部分数据包 A-1,此时需要等待接收到完整的 A 数据包,出现拆包问题。栗子五:数据包 A 较大,客户端分段发送数据包A,服务端需要多次才可以接收完数据包 A,出现拆包问题。

小结: 由于拆包/粘包问题的存在,如何识别一个完整的数据包就成了问题?难点在于如何定义一个数据包的边界。

为什么会有人说 TCP 粘包?

先来看下应用程序使用 TCP​ 套接字的流程: 对应 TCP/IP 4层协议:

应用进程调用write 时,内核从该应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区。本端TCP​ 以MSS​ 大小的或更小的块把数据传递给IP。TCP​分段加上IP​ 首部构成 IP​ 数据包,并按照其目的IP 地址查找路由表项以确定外出接口,然后把数据报传递给相应的数据链路。

这里解释下 MSS​ 和 MTU:

MTU(Maxitum Transmission Unit​) 是链路层一次最大传输数据的大小。一般来说大小为 1500byte。MSS(Maximum Segement Size​) 是指TCP 最大报文段长度,它是传输层一次发送最大数据的大小。

MTU​ 和 MSS​ 一般的计算关系为:MSS​ = MTU​ - IP​ 首部 - TCP首部。

『粘包学家』认为 TCP 粘包/拆包发生原因有三:

应用程序write 写入的字节大小大于套接字发送缓冲区大小。MSS​ +TCP​ 首部 +IP​ 首部 >MTU​,就要 TCP 分段。以太网帧的payload​ 大于MTU​ 就要进行 IP 分片。

说白了,『粘包学家』认为我怎么给你的,你就该怎么还给我。

『粘包警察』认为这根本不是 TCP 的锅:

TCP 是面向字节流:根本没有包这个概念,谈何粘包/拆包。『粘包/拆包』本质问题在于:如何从二进制流中提取数据,如何定义数据的边界。

说白了,『粘包警察』认为怎么解析数据是你应用层的问题,TCP 只管传输并提供可靠的交付服务。

拓展:Nagle 算法

Nagle​ 算法于 1984 年被福特航空和通信公司定义为 TCP/IP​ 拥塞控制方法,这使福特经营的最早的专用 TCP/IP 网络减少拥塞控制,从那以后这一方法得到了广泛应用。

优势:为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

如果每次需要发送的数据只有 1 字节,加上 20 个字节的 IP​首部 和 20 个字节的 TCP首部,每次发送的数据包大小为 41 字节,但是只有 1 字节是有效信息,这就造成了非常大的浪费。

Nagle​ 算法的规则(可参考tcp_output.c​ 文件里 tcp_nagle_check 函数注释):

如果包长度达到MSS,则允许发送;如果该包含有FIN,则允许发送;设置了TCP_NODELAY 选项,则允许发送;未设置TCP_CORK​ 选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

Linux​ 在默认情况下是开启 Nagle 算法的,在大量小数据包的场景下可以有效地降低网络开销。

可以通过Linux​ 提供的TCP_NODELAY​ 参数禁用Nagle 算法。Netty​ 中为了使数据传输延迟最小化,就默认禁用了Nagle 算法。

Tips:​ 还有一个延迟 ACK(Delay ACK​),TCP​ 何时发送 ACK 有如下规定:

当有响应数据发送的时候,ACK 会随着数据一块发送。.如果没有响应数据,ACK 就会有一个延迟,以等待是否有响应数据一块发送,但是这个延迟一般在40ms~500ms之间,一般情况下在40ms左右。如果在等待发送ACK​ 期间,第二个数据又到了,这时候就要立即发送ACK。拓展:UDP 为什么不分段?

先来回顾下 UDP 的特点:

UDP 无需建立连接。无连接状态。分组首部开销小。(首部 8字节)UDP​ 是面向报文的。(重点)

发送方 UDP​ 对应用层交下来的报文,在添加首部后就向下交付给 IP​ 层,既不合并,也不拆分,而是保留这些报文的边界; 接收方 UDP​ 对 IP​ 层交上来 UDP​ 用户数据报,在去除首部后就原封不动地交付给上层应用进程,一次交付一个完整的报文。因此报文不可分割,是 UDP​ 数据报处理的最小单位。

再看 UDP 数据报格式:

可知一个 UDP​ 数据报可携带最大用户数据长度为:2^16 - 8 = 65535 - 8 = 65527 (B)

小结下 UDP 为什么不分段?

UDP​ 协议特性:面向报文。16位UDP 长度。没有分段的能力:标记分段先后顺序的能力,即编号(ID​)、尾部编号的标识 (Flag)UDP​ 应用特性:常用于一次性传输比较少量数据的网络应用,如DNS、SNMP​ 等。

当 DNS​ 查询超过 512字节 时,协议的 TC​ 标志出现删除标志,这时则使用 TCP​ 发送。通常传统的 UDP​ 报文一般不会大于512字节。

拆包/粘包解决方案

由上文可知我们需要一种定义来数据包的边界,这也是解决拆包/粘包的唯一方法:定义应用层的通信协议。

主流协议解决方案有:

消息长度固定特定分隔符消息长度 + 消息内容

Netty 对三种常用封帧方式的支持:

方式

解码

编码

固定长度

​​FixedLengthFrameDecoder​​

简单

分隔符

​​DelimiterBasedFrameDecoder​​

简单

固定长度字段存内容长度

​​LengthFieldBasedFrameDecoder​​

​​LengthFieldPrepender​​

固定消息长度

Netty​ 中提供了类 FixedLengthFrameDecoder:

每个数据报文都需要一个固定的长度。当接收方累计读取到固定长度的报文后,就认为已经获得一个完整的消息。当发送方的数据小于固定长度时,则需要空位补齐。# 举个栗子:假定固定消息长度是 3字节,当你收到如下报文:

+---+----+------+----+

| A | BC | DEFG | HI |

+---+----+------+----+

# 将它们解码成以下 3个固定长度的数据包:

+-----+-----+-----+

| ABC | DEF | GHI |

+-----+-----+-----+

项目地址:对应代码:

ServerBootstrap b = new ServerBootstrap();

b.group(bossGroup, workerGroup)

.channel(NioServerSocketChannel.class)

.childHandler(new ChannelInitializer() {

@Override

public void initChannel(SocketChannel ch) {

ch.pipeline().addLast(new FixedLengthFrameDecoder(3));

//... ...

}

});

通过 telnet​ 去访问:telnet localhost 8088

优缺点:

优点:消息定长法使用非常简单缺点:无法很好设定固定长度的值,如果长度太大会造成字节浪费,长度太小又会影响消息传输,所以在一般情况下消息定长法不会被采用。特殊分隔符

既然接收方无法区分消息的边界,那么可以在每次发送报文的尾部加上特定分隔符,接收方就可以根据特殊分隔符进行消息拆分。

DelimiterBasedFrameDecoder 自动完成以分隔符做结束标志的消息的解码:

# 举个栗子:以下报文根据特定分隔符 `\n` 按行解析

+--------------+

| ABC\nDEF\r\n |

+--------------+

# 解析后得到:

+-----+-----+

| ABC | DEF |

+-----+-----+

项目地址:代码

ServerBootstrap b = new ServerBootstrap();

b.group(bossGroup, workerGroup)

.channel(NioServerSocketChannel.class)

.childHandler(new ChannelInitializer() {

@Override

public void initChannel(SocketChannel ch) {

// 以 & 为分隔符

ByteBuf delimiter = Unpooled.copiedBuffer("&".getBytes());

// 10 表示单条消息的最大长度,当达到该长度后扔没有查找到分隔符,就抛出异常

// TooLongFrameException,防止由于异常码流失分隔符导致的内存溢出

ch.pipeline().addLast(new DelimiterBasedFrameDecoder(10, delimiter));

// ... ...

}

});

通过 telnet​ 去访问:telnet localhost 8088

比较推荐的做法是:将消息进行编码,例如 base64 编码,然后可以选择 64 个编码字符之外的字符作为特定分隔符。

特定分隔符法在消息协议足够简单的场景下比较高效,Redis 在通信过程中采用的就是换行分隔符。

Redis 2.0​ 以后的通信统一为RESP​ 协议(Redis Serialization Protocol)RESP​ 是一个二进制安全的文本协议,工作于TCP​ 协议上。RESP​ 以行作为单位,客户端和服务器发送的命令或数据一律以\r\n(CRLF)作为换行符。消息长度 + 消息内容

消息长度 + 消息内容是项目开发中最常用的一种协议,如下展示了该协议的基本格式。

+--------|----------+

|消息头 |消息体 |

+--------|----------+

| Length | Content |

+--------|----------+

消息头中存放消息的总长度,例如使用 4 字节的 int 值记录消息的长度,消息体实际的二进制的字节数据。

接收方在解析数据时:

首先读取消息头的长度字段Len然后紧接着读取长度为Len 的字节数据,该数据即判定为一个完整的数据报文

依然以上述提到的原始字节数据为例,使用该协议进行编码后的结果如下所示:

+-----|-------|-------|----|-----+

| 2AB | 4CDEF | 4GHIJ | 1K | 2LM |

+-----|-------|-------|----|-----+

消息长度 + 消息内容的使用方式非常灵活,且不会存在消息定长法和特定分隔符法的明显缺陷。

当然在消息头中不仅只限于存放消息的长度,而且可以自定义其他必要的扩展字段:

消息版本算法类型等等

(责任编辑:系统运维)

    一. VIM高亮进入vim后,在普通模式下输入如下命令,开启php代码高亮显示   :syntax enable   :source $VIMRUNTIME/syntax/php.vim 二. VI常用命令_______________________________________________________一般模式                           光标移动__________________________________________________________h 或 向左方向键                    光标向左移动一个字符j 或 向下方向键                    光标向下移动一个字符k 或 向上方向键                    光标向上移动一个字符l 或 向右方向键                    光标向右移动一个字符Ctrl + f                           屏幕向前翻动一页(常用)Ctrl + b                           屏幕向后翻动一页(常用)Ctrl + d                           屏幕向前翻动半页Ctrl + u                           屏幕向后翻动半页+                                  光标移动到非空格符的下一列-                                  光标移动到非空格符的上一列n                           接下数字后再按空格键,光标会向右移动这一行的                                   n个字符,例如20,则光标会向右移动20个字符0                                  (这是数字0) 移动到这一行的第一个字符处(常用)$                                  移动到这一行的最后一个字符处(常用)H                                  光标移动到这个屏幕最上方的那一行M                                  光标移动到这个屏幕中央的那一行L                                  光标移动到这个屏幕最下方的那一行G                                  移动到这个文件的最后一行(常用)nG                                 移动到这个文件的第n行.例如20G,则会移动到这个文件的                                   第20行(可配合:set nu)n                           光标向下移动n行(常用)________________________________________________________________一般模式                           查找替换________________________________________________________________/word                              在光标之前查找一个名为word的字符串 word                              在光标之前查找一个名为的word字符串:n1,n2s/word1/word2/g              在第n1与n2行之间查找word1这个字符串,并将该字符串替换                                   为word2(常用):1,$s/word1/word2/g                从第一行到最后一行查找word1字符串,并将该字符串替换                                   为word2(常用):1,$s/word1/word2/ge               从第一行到最后一行查找word1字符串,并将该字符串替换                                   为word2,且在替换前显示提示符让用户确认(confirm)(常用)__________________________________________________________________一般模式                           删除 复制与粘贴__________________________________________________________________x,X                                x为向后删除一个字符,X为向前删除一个字符(常用)nx                                 向后删除n个字符dd                                 删除光标所在的那一整行(常用)ndd                                删除光标所在行的向下n行,例如,20dd则是删除20行(常用)d1G                                删除光标所在行到第一行的所有数据dG                                 删除光标所在行到最后一行的所有数据yy                                 复制光标所在行(常用)nyy                                复制光标所在行的向下n行,例如,20yy则是复制20行(常用)y1G                                复制光标所在行到第一行的所有数据yG                                 复制光标所在行到最后一行的所有数据p,P                                p为复制的数据粘贴在光标下一行,P则为粘贴在光标上一行(常用)J                                  将光标所在行与下一行的数据结合成一行u                                  恢复前一个动作(常用) ____________________________________________________________________编辑模式                          ___________________________________________________________________I,I                                插入:在当前光标所在处插入输入的文字,已存在的字符会向后                                   退(常用)a,A                                添加:由当前光标所在处的下一个字符开始输入,已存在的字符                                   会向后退(常用)o,O                                插入新的一行:从光标所在处的下一行行首开始输入字符(常用)r,R                                替换:r会替换光标所指的那一个字符:R会一直替换光标所指的                                   文字,直到按下Esc为止(常用)Esc                                退出编辑模式,回到一般模式(常用) ___________________________________________________________________命令行模式                          ___________________________________________________________________ :w                                 将编辑的数据写入硬盘文件中(常用):w!                                若文件属性为只读,强制写入该文件:q                                 退出vi(常用):q!                                若曾修改过文件,又不想保存,使用!为强制退出不保存文件:wq                                保存后退出,若为:wq!,则为强制保存后退出(常用):w [filename]                      将编辑数据保存为另一个文件(类似另存新文档):r [filename]                      在编辑的数据中,读入另一个论据的数据,亦即将filename这                                   个文件内容加到光标所在行的后面:set nu                            显示行号,设定之后,会在每一行的前面显示该行的行号:set nonu                          与set nu相反,为取消行号n1,n2 w [filename]                 将n1到n2的内容保存为filename 这个文件还是手贱,在修改了网络配置和更新后,开机,机子木有无线网卡了,有线网卡也非常诡异,必须要restart network才能连出去。在打开系统的网络连接面板时,出现系统的网络服务与此版本的网络管理器不兼容的错误。利用万能的Google,找到了解决办法。方法比较匪夷所思,我反正木有理解,但是就这么成了。。。方法就是4步走,看下面的引用:复制代码代码如下:First open Terminal and log in as root.# su After that go the correct folder.# cd /etc/NetworkManager/system-connections/Now take a look at the content of this folder. If you had a VPN connection e.g. there must be a file with the name of that connection. # ls -laNow you can remove that file or you can move it to another folder (so you can set it back if this solution does not work for your problem). To move the file to your personal folder use the following command:# mv /home// #username是你的用户名 Now the only thing left is starting the Network Manager:# NetworkManager当~~~你的网络管理界面里出现了久违的无线网卡~LOLPS:可能会出现未配置的情况,请reboot一下
    相关内容
    精彩推荐
    热门点击
    友情链接