☰
什么是TCP粘包/拆包?PHP(或Swoole)开发中如何解决?
2026/10/20 8:45:05 网站建设 项目流程

TCP是字节流,它只负责源源不断传输一串连续字节,TCP本身没有消息边界。
把TCP想象成一根持续流水的水管,你从一端倒入一段段消息,水流到对面,不会自动标记“这是第一条消息结束、第二条开始”。
粘包:发送方先后送出2条独立消息,接收端一次读取,一次性拿到两段数据拼在一起,就像两个快递被装进同一个大袋子送过来。
拆包:一条完整消息太大,内核缓冲区限制,这条消息被切成两半分两次送到接收端;好比一个大件快递,拆成两个包裹分两趟运送。

注意重点:粘包不是TCP的bug,是TCP字节流特性带来的现象;UDP不会粘包,UDP自带数据包边界。

一、产生粘包/拆包的两类根源

  1. 发送缓冲区合并(发送端粘包)
    发送方短时间连续write两块小数据,操作系统TCP发送缓冲区会把小块数据攒在一起,合并成一个TCP报文发出(Nagle算法),减少发包次数。
    好比你短时间寄出两个小包裹,快递员为了省油,攒到一起打包发货。

  2. 接收缓冲区读取大小限制(接收端拆分/合并)
    接收端调用读取接口时,一次读取的字节数不一定刚好等于一条业务消息长度。缓冲区里面数据有多有少,读出来的片段可能是半条消息,或者多条消息挤在一起。

拆包:一条消息太长,超过MSS(最大报文段长度),TCP底层自动切割成多个包传输。

二、Swoole/PHP场景下,典型现象

以Swoole的TCP服务(非HTTP/WebSocket)举例:
客户端连续发两条消息:hello1、hello2
服务端onReceive回调,有可能一次性收到字符串hello1hello2→粘包。
如果消息很长,服务端第一次只收到前半段,下一次回调收到后半段 →拆包。

补充:HTTP、WebSocket不用你手动处理粘包!
HTTP协议、WebSocket帧协议自带内置消息边界,协议底层已经封装好解析规则。Swoole内置解析,开发者直接拿到完整请求/帧,不用自己处理。
只有自定义TCP私有协议(自己写TCP客户端服务端通信),才需要手动解决粘包拆包。

三、PHP/Swoole主流4种解决方案(本质:给字节流加上业务消息边界)

方案1:固定分隔符(换行符 \n 特殊符号)

每条消息末尾追加固定标记,例如每条JSON末尾加换行\n。
接收端不断缓存收到的数据,按分隔符切割,取出完整消息,剩下不完整字节继续留在缓冲区等待后续数据。
优点:简单,适合文本消息;缺点:消息内容里面不能出现这个分隔符,否则解析错乱。

方案2:长度头(最常用、工业标准,推荐Swoole私有TCP)

每条消息 =【固定长度的消息头(记录后面消息体字节长度) + 消息体】
示例:4字节大端整数,表示消息体长度N,后面紧跟N字节业务数据。
接收逻辑:

  1. 先缓存收到的字节;
  2. 先读够4字节长度头,解析出消息体长度;
  3. 继续等缓冲区攒够N字节消息体,取出完整业务消息;剩余字节继续保留。

Swoole文档推荐方案,二进制通信首选,无转义麻烦。

方案3:固定消息长度

约定每一条消息永远固定多少字节。不够长度就填充占位字符。
缺点:灵活性极差,消息大小不能变化,业务极少使用。

方案4:关闭Nagle算法(只能缓解,不能根治粘包)

setsockopt(TCP_NODELAY),关闭Nagle,小包不会等待合并。
⚠️误区重点:开启TCP_NODELAY只是减少发送端合并小包的概率,依然存在接收缓冲区带来的粘包拆包,不能彻底解决,不能当成最终方案。

四、Swoole开发落地要点

  1. 必须在服务端维护接收缓冲区(字符串buffer),onReceive收到数据追加到buffer,循环解析buffer提取完整包,剩余半截数据留在buffer,等待下一次onReceive数据到来。
  2. 不要假设一次onReceive回调就等于一条业务消息,这是新手最常见的坑。
  3. WebSocket、HTTP不需要自己实现分包逻辑,Swoole底层协议层已经封装长度解析。

五、一句话总结

TCP没有消息边界,字节流会出现粘包、拆包;这不是故障。解决的核心思路就是自定义协议标记消息边界,Swoole做私有TCP通信优先使用「长度头方案」。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询