在了解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服务器[粘包]的更多相关文章

  1. Netty入门系列(2) --使用Netty解决粘包和拆包问题

    前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络 ...

  2. C/C++ socket编程教程之九:TCP的粘包问题以及数据的无边界性

    C/C++ socket编程教程之九:TCP的粘包问题以及数据的无边界性 上节我们讲到了socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发 ...

  3. TCP的粘包现象

    看面经时,看到有面试官问TCP的粘包问题.想起来研一做购物车处理数据更新时遇到粘包问题,就总结一下吧. 1 什么是粘包现象 TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看, ...

  4. TCP拆包粘包之分隔符解码器

    TCP以流的方式进行数据传输,上层的应用协议为了对消息进行区分,往往采用如下4种方式. (1)消息长度固定,累计读取到长度总和为定长LEN的报文后,就认为读取到了一个完整的消息:将计数器置位,重新开始 ...

  5. python 网络编程之TCP传输&粘包传输

    只有TCP有粘包现象,UDP永远不会粘包. 所谓粘包问题主要还是C/S两端数据传输时 因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的 根本原因:粘包是由TCP协议本身造成的,T ...

  6. python 全栈开发,Day35(TCP协议 粘包现象 和解决方案)

    一.TCP协议 粘包现象 和解决方案 黏包现象让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)执行远程命令的模块 需要用到模块subprocess sub ...

  7. TCP通信粘包问题分析和解决

    转载至https://www.cnblogs.com/kex1n/p/6502002.html 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发 ...

  8. TCP通信粘包问题分析和解决(全)(转)

    TCP通信粘包问题分析和解决(全) 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送 ...

  9. Netty—TCP的粘包和拆包问题

    一.前言 虽然TCP协议是可靠性传输协议,但是对于TCP长连接而言,对于消息发送仍然可能会发生粘贴的情形.主要是因为TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分.有可能将一个大数据包 ...

  10. TCP协议粘包问题详解

    TCP协议粘包问题详解 前言 在本章节中,我们将探讨TCP协议基于流式传输的最大一个问题,即粘包问题.本章主要介绍TCP粘包的原理与其三种解决粘包的方案.并且还会介绍为什么UDP协议不会产生粘包. 基 ...

随机推荐

  1. NewStarCTF 2023 公开赛道 WEEK2|CRYPTO全解

    一.滴啤 题目信息 from Crypto.Util.number import * import gmpy2 from flag import flag def gen_prime(number): ...

  2. [转帖]事务上的等待事件 —— enq: TX - contention

    TX锁是保护事务的,事务结束时便会释放.因此,为获得TX锁为等待的会话,要等到拥有锁的会话的事务结束为止. SQL> select name,parameter1,parameter2,para ...

  3. [转帖]SSH交互式脚本StrictHostKeyChecking选项 benchmode=yes

    https://www.cnblogs.com/klb561/p/11013774.html SSH 公钥检查是一个重要的安全机制,可以防范中间人劫持等黑客攻击.但是在特定情况下,严格的 SSH 公钥 ...

  4. [转帖]AMD第四代宵龙 9174F 亮眼

    https://www.amd.com/zh-hans/processors/epyc-9004-series#%E8%A7%84%E6%A0%BC 型号规格   型号 CPU 核心数量 线程数量 最 ...

  5. [转帖]RPC 框架总结与进阶

    https://www.cnblogs.com/xiaojiesir/p/15579418.html 框架总结 Netty 服务端启动 Netty 提供了 ServerBootstrap 引导类作为程 ...

  6. zabbix监控进程和监控日志

    zabbix监控进程和监控日志 文章目录 zabbix监控进程和监控日志 一.自定义监控进程 1.新建脚本存放目录 2.修改zabbix_agentd.conf文件 3.zabbix server端进 ...

  7. MYsql备份恢复简单过程

    1. 备份数据库 建完数据库更新完补丁之后进行数据库的备份操作. mysqldump -uroot --databases yourdatabase -p > /home/yourdatabas ...

  8. 基于javaPoet的缓存key优化实践

    一. 背景 在一次系统opsreview中,发现了一些服务配置了@Cacheable注解.@cacheable 来源于spring cache框架中,作用是使用aop的方式将数据库中的热数据缓存在re ...

  9. AsNoTracking()非跟踪数据 查询

    刚开始学习使用EF ,做项目时需要查询数据将数据显示在datagrid中,使用如下方法: query是IQueryable的 在一次看别人写的代码的时候,发现了AsNoTracking()这个方法,并 ...

  10. 手撕Vuex-实现getters方法

    经上一篇章介绍,完成了实现共享数据的功能,实现方式是在 Store 构造函数中将创建 Store 时将需要共享的数据添加到 Store 上面,这样将来我们就能通过 this.$store 拿到这个 S ...