Swoole从入门到入土(6)——TCP服务器[粘包]
在了解Swoole下如何处理粘包问题之前,我们需要先了解什么是“粘包”。我们以下面这张图进行普及:

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下几种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
(5)如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
归根到底,会发生粘包或半包的问题,原因在于服务端不知道客户端发过来的数据一个包有多大。所以兵来将挡,我们只需要让服务端知道每个业务层包的边界,事情就迎刃而解了。这里就需要 2 个操作来解决:
- 分包:Server 收到了多个数据包,需要拆分数据包
- 合包:Server 收到的数据只是包的一部分,需要缓存数据,合并成完整的包
就因为这个问题,所以 TCP 网络通信时需要设定通信协议。常见的 TCP 通用网络通信协议有 HTTP、HTTPS、FTP、SMTP、POP3、IMAP、SSH、Redis、Memcache、MySQL 。
除了通用协议外还可以自定义协议。重点来了:Swoole 支持了 2 种类型的自定义网络通信协议: EOF结束符协议 和 固定包头+包体协议。
1、EOF结束符协议
1)解决分包与合包
$server = new Swoole\Server('0.0.0.0', 9501);
$server->set([
'max_wait_time'=>60,
'reload_async'=>true,
'worker_num'=>1,
'task_worker_num'=>1,
'task_max_request'=>100,
'open_eof_split' => true,
'package_eof' => "abc",
]);
看上面两个红色的配置:
open_eof_split:启用 EOF 自动分包
这个参数启用后,底层会从数据包中间查找 EOF,并拆分数据包。onReceive 每次仅收到一个以 EOF 字串结尾的数据包。
package_eof:设置 EOF 字符串。
这个配置需要与与 open_eof_check 或者 open_eof_split 配合使用,并且最大只允许传入 8 个字节的字符串。
根据以上的配置进行实验:
客户端发送两个包,分别是包1:123abc456a,包2:bc789abc
服务端则触发了三个onReceive事件,收到的data分别是:123abc / 456abc / 789abc
是不是很神奇,swoole底层根据package_eof配置设置的结束符帮我们把客户端发过来的数据包进行分包与合包,让我们在onRecieve业务层拿到的数据都是完整的业务数据包。但是,方便的同时带来的代价就是效率较低。open_eof_split这个配置让swoole需要遍历整个数据包的内容,查找 EOF,因此会消耗大量 CPU 资源。假设每个数据包为 2M,每秒 10000 个请求,这可能会产生 20G 条 CPU 字符匹配指令。
为了解决这个问题,我们一般让swoole帮我们解决分包问题,合包由我们在业务代码层面进行处理。接下来,请大家往下阅读。
2)只解决分包
$server = new Swoole\Server('0.0.0.0', 9501);
$server->set([
'max_wait_time'=>60,
'reload_async'=>true,
'worker_num'=>1,
'task_worker_num'=>1,
'task_max_request'=>100,
'open_eof_check' => true,
'package_eof' => "abc",
]);
这里的两个红包配置,与上面不同的是这里用的是open_eof_check,而不是open_eof_split。
open_eof_check:打开 EOF 检测【默认值:false】
此选项将检测客户端连接发来的数据,当数据包结尾是指定的字符串时才会投递给 Worker 进程。否则会一直拼接数据包,直到超过缓存区或者超时才会中止。当出错时底层会认为是恶意连接,丢弃数据并强制关闭连接。
注意:此配置仅对 STREAM(流式的) 类型的 Socket 有效,如 TCP 、Unix Socket Stream。EOF 检测不会从数据中间查找 eof 字符串,所以 Worker 进程可能会同时收到多个数据包。
根据以上的配置进行实验:
客户端发送两个包,分别是包1:123abc456a,包2:bc789abc
服务端则触发了一个onReceive事件,收到的data是:123abc456abc789abc
看到与open_eof_split的区别没?我们在onRecieve收到的是多个业务数据包合并在一样的数据,swoole保证提交到业务层是一个或多个完整的数据包,我们需要在业务层用explode()进行拆分。虽然需要我们额外的拆分动作,但这一切都是值得的,因为swoole效率高出了好多,因为swoole只检查数据包结尾是否是指定的eof字符串。
2、固定包头+包体协议
固定包头的方法非常通用,在服务器端程序中经常能看到。这种协议的特点是一个数据包总是由包头 + 包体 2 部分组成。包头由一个字段指定了包体或整个包的长度,长度一般是使用 2 字节 /4 字节整数来表示。服务器收到包头后,可以根据长度值来精确控制需要再接收多少数据就是完整的数据包。Swoole 的配置可以很好的支持这种协议,可以灵活地设置 4 项参数应对所有情况。
$server = new Swoole\Server("0.0.0.0", 9501);
$server->set(array(
'open_length_check' => true,
'package_max_length' => 81920,
'package_length_type' => 'n', //see php pack()
'package_length_offset' => 0,
'package_body_offset' => 2,
));
让我们一起了解这些配置的含义:
open_length_check:打开包长检测特性【默认值:false】
长度检测协议,只需要计算一次长度,数据处理仅进行指针偏移,性能非常高,推荐使用。
pack_max_length:设置最大数据包尺寸,单位为字节。【默认值:2M 即 2 * 1024 * 1024,最小值为 64K】(此参数不宜设置过大,否则会占用很大的内存)
--open_length_check:当发现包长度超过 package_max_length,将直接丢弃此数据,并关闭连接,不会占用任何内存;
--open_eof_check:因为无法事先得知数据包长度,所以收到的数据还是会保存到内存中,持续增长。当发现内存占用已超过 package_max_length 时,将直接丢弃此数据,并关闭连接;
--open_http_protocol:GET 请求最大允许 8K,而且无法修改配置。POST 请求会检测 Content-Length,如果 Content-Length 超过 package_max_length,将直接丢弃此数据,发送 http 400 错误,并关闭连接;
package_length_type:长度值的类型,接受一个字符参数,与 PHP 的 pack 函数一致。
目前 Swoole 支持 10 种类型:

不知道大小端与网络字节序是什么意思?请看文末的高级话题科普:)
package_length_offset:length 长度值在包头的第几个字节。
package_body_offset:从第几个字节开始计算长度,一般有 2 种情况:
-length 的值包含了整个包(包头 + 包体),package_body_offset 为 0
-包头长度为 N 字节,length 的值不包含包头,仅包含包体,package_body_offset 设置为 N
------高级话题分割线------
大小端与网络字节序
“大端”和”小端”表示多字节值的哪一端存储在该值的起始地址处;小端存储在起始地址处,即是小端字节序;大端存储在起始地址处,即是大端字节序;具体的说:
①大端字节序(Big Endian):最高有效位存于最低内存地址处,最低有效位存于最高内存处;
②小端字节序(Little Endian):最高有效位存于最高内存地址,最低有效位存于最低内存处。
如下图:当以不同的存储方式,存储数据为0x12345678时:

UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的。
所以说,网络字节序是大端字节序(划重点,请做好笔记)
--------------------------- 我是可爱的分割线 ----------------------------
最后博主借地宣传一下,漳州编程小组招新了,这是一个面向漳州青少年信息学/软件设计的学习小组,有意向的同学点击链接,联系我吧。
Swoole从入门到入土(6)——TCP服务器[粘包]的更多相关文章
- Netty入门系列(2) --使用Netty解决粘包和拆包问题
前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络 ...
- C/C++ socket编程教程之九:TCP的粘包问题以及数据的无边界性
C/C++ socket编程教程之九:TCP的粘包问题以及数据的无边界性 上节我们讲到了socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发 ...
- TCP的粘包现象
看面经时,看到有面试官问TCP的粘包问题.想起来研一做购物车处理数据更新时遇到粘包问题,就总结一下吧. 1 什么是粘包现象 TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看, ...
- TCP拆包粘包之分隔符解码器
TCP以流的方式进行数据传输,上层的应用协议为了对消息进行区分,往往采用如下4种方式. (1)消息长度固定,累计读取到长度总和为定长LEN的报文后,就认为读取到了一个完整的消息:将计数器置位,重新开始 ...
- python 网络编程之TCP传输&粘包传输
只有TCP有粘包现象,UDP永远不会粘包. 所谓粘包问题主要还是C/S两端数据传输时 因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的 根本原因:粘包是由TCP协议本身造成的,T ...
- python 全栈开发,Day35(TCP协议 粘包现象 和解决方案)
一.TCP协议 粘包现象 和解决方案 黏包现象让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)执行远程命令的模块 需要用到模块subprocess sub ...
- TCP通信粘包问题分析和解决
转载至https://www.cnblogs.com/kex1n/p/6502002.html 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发 ...
- TCP通信粘包问题分析和解决(全)(转)
TCP通信粘包问题分析和解决(全) 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送 ...
- Netty—TCP的粘包和拆包问题
一.前言 虽然TCP协议是可靠性传输协议,但是对于TCP长连接而言,对于消息发送仍然可能会发生粘贴的情形.主要是因为TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分.有可能将一个大数据包 ...
- TCP协议粘包问题详解
TCP协议粘包问题详解 前言 在本章节中,我们将探讨TCP协议基于流式传输的最大一个问题,即粘包问题.本章主要介绍TCP粘包的原理与其三种解决粘包的方案.并且还会介绍为什么UDP协议不会产生粘包. 基 ...
随机推荐
- 0xGame 2023【WEEK1】Crypto全解
What's CBC? 题目信息 from Crypto.Util.number import * from secret import flag,key def bytes_xor(a,b): a, ...
- vim工具极简总结
vim工具总结 背景 很多操作记不住. 想着总结当笔记使用. 备忘 基本总结 vim somefile 打开/新建文件 i/a/insert按键 进入插入模式 insert 连续两次 进入替换模式 e ...
- 虚拟化平台IO劣化分析
虚拟化平台IO劣化分析 背景 最近同事让帮忙做几个虚拟机进行性能测试. 本来应该搭建CentOS/Winodws平台进行相关的测试工作. 但是为了环境一致性, 使用了ESXi6.7 进行虚拟化 然后这 ...
- [转帖]深入理解mysql-第十章 mysql查询优化-Explain 详解(上)
目录 一.初识Explain 二.执行计划-table属性 三.执行计划-id属性 四.执行计划-select_type属性 一条查询语句在经过MySQL查询优化器的各种基于成本和规则的优化会后生成一 ...
- [转帖]yum 下载全量依赖 rpm 包及离线安装(终极解决方案)
简介 通常生产环境由于安全原因都无法访问互联网.此时就需要进行离线安装,主要有两种方式:源码编译.rpm包安装.源码编译耗费时间长且缺乏编译环境,所以一般都选择使用离线 rpm 包安装. 验证环境 C ...
- iptables 命令学习
iptables 命令学习 摘要 Linux 早起版本使用netfilter进行数据包过滤. 最新的版本开始改用 ebpf的方式进行内核编程式的包过滤. netfilter 可以理解为内核态的一个处理 ...
- 如何查看服务器的Raid缓存等配置的情况
摘要 最近总遇到同一批机器的IO不一样的情况. 感觉可能跟硬件设备和Raid卡的设置不一样有关系. 所以今天学习研究了下storcli的命令. 希望能够进行一些数据的收集. Storcli简介 sto ...
- 总结: Redis 查看key大小的简单总结
Redis 查看key大小的简单总结 第一步: 安装rdbtools 吐槽一下 python 非常不熟悉 第一步 安装epel以及python等工具 yum install epel-release ...
- git创建分支出现 fatal: Not a valid object name: 'master'.
今天使用git 创建新分支的时候 出现了一个错误 fatal: Not a valid object name: 'master'. 问题描述:一个非法的master,原因:本地还没有创建master ...
- ClickHouse(22)ClickHouse集成HDFS表引擎详细解析
HDFS 这个引擎提供了与Apache Hadoop生态系统的集成,允许通过ClickHouse管理HDFS上的数据.这个引擎提供了Hadoop的特定功能. 用法 ENGINE = HDFS(URI, ...