详解Swoole TCP流数据边界问题解决方案

目录
  • 1. 数据发送过程
  • 2. 什么是数据边界
    • 2.1 代码演示
  • 3.EOF 解决方案
    • 3.1 open_eof_check
    • 3.2 open_eof_split
    • 3.3 open_eof_check 和 open_eof_split 差异
  • 4. 固定包头 + 包体解决方案
  • 5. 总结
  • 6. 扩展知识
    • 6.1 字节序

1. 数据发送过程

首先由客户端将数据发往缓冲区 (服务端并不是直接收到的), 对于客户端来说,这次的数据即是发送成功了, 对于服务端是否真正的收到他是不知道的, 然后再由服务端从缓冲区中读取数据。图解:

2. 什么是数据边界

因为 TCP 是流式传输,对于服务端来说并不知道此时在缓冲区内的数据是一次请求还是两次请求的,所以在服务端接收数据时需要根据指定字符或约定长度来对数据进行分包,这个分包的标志即是数据边界。否则可能会出现一次读取两条或多条数据,造成读取、解析数据出错。

2.1 代码演示

可以用代码实现一下,假设客户端死循环往缓冲区不停输入 “1”,即相当于每次的报文内容都是 1, 那么在服务端读取时收到的数据就是随机长度的。

客户端代码:

$client = new Swoole\Client(SWOOLE_SOCK_TCP);
if ($client->connect('127.0.0.1', 9501, -1)) {
    while(true) {
        $client->send(1);
    }
}
$client->close();

服务端代码:

$server = new Swoole\Server('127.0.0.1', 9501);
$server->on('connect', function($server, $fd){
    echo "client : ".$fd." connect";
});

$server->on('receive', function($server, $fd, $from_id, $data){
    echo "receive:". $data.PHP_EOL;
});

$server->on('close', function($server){

});

运行结果

可以看到运行结果,服务端获取到的数据完全是随机的,有长有短,那么接下来我们说下如何解决这个问题。

3.EOF 解决方案

第一种解决方案类似于我们 http 请求头的分隔符,在每次发送的数据包结尾处使用 \r\n (可以配置) 来结尾, 当服务端从缓冲区中读取数据, 根据指定字符来分割数据包,EOF 有两种配置方案:

3.1 open_eof_check

首先放出配置方式:

$server->set([
    'open_eof_check' => true,
    'package_eof' => "\r\n"
]);

这种配置方式会对客户端发来的数据包进行检测, 当发现结尾是 \r\n 时,才会投递给 worker 进程, 也就是我们的 onReceive 回调,否则会一直拼接数据包,直到超出缓冲区或者超时才终止。 但此方法有一个问题是可能会一次性收到多个数据包,因为他是从数据包的结尾处来进行检查的,在数据内容中存在 \r\n 时程序并不会发现,需要我们自己在应用代码中再次使用 \r\n 来拆分数据包。

客户端运行代码

$client = new Swoole\Client(SWOOLE_SOCK_TCP);

if ($client->connect('127.0.0.1', 9501, -1)) {

    while(true) {
        $send2 = "Hello World \r\n";
        $client->send($send2);
    }
}

$client->close();

服务端代码

$server = new Swoole\Server('127.0.0.1', 9501);
$server->set([
    'open_eof_check' => true,
    'package_eof' => "\r\n"
]);

$server->on('connect', function($server, $fd){
    echo "client : ".$fd." connect";
});

$server->on('receive', function($server, $fd, $from_id, $data){
    echo "receive:". $data;
});

$server->on('close', function($server){

});

$server->start();

运行结果

3.2 open_eof_split

配置方式:

$server->set([
    'open_eof_split' => true,
    'package_eof' => "\r\n"
]);

这种配置方式,服务端会对客户端发来的数据逐个字符进行检查,遇到 \r\n 就发送给 worker 进程,可以有效实现分包,但缺点是性能比较差。

运行结果:可以看到每次接收到一个 Hello World(代码我就不贴了, 只把服务端 set 配置改一下, 其他都一样)

3.3 open_eof_check 和 open_eof_split 差异

open_eof_check 只检查接收数据的末尾是否为 EOF,因此它的性能最好,几乎没有消耗

open_eof_check 无法解决多个数据包合并的问题,比如同时发送两条带有 EOF 的数据,底层可能会一次全部返回

open_eof_split 会从左到右对数据进行逐字节对比,查找数据中的 EOF 进行分包,性能较差。但是每次只会返回一个数据包

4. 固定包头 + 包体解决方案

引用一段官方文档的描述:

包长检测提供了固定包头 + 包体这种格式协议的解析。启用后,可以保证 Worker 进程 onReceive 每次都会收到一个完整的数据包。

长度检测协议,只需要计算一次长度,数据处理仅进行指针偏移,性能非常高,推荐使用。

可见官方是推荐使用这种方式的,就是配置比其他方案要复杂一些, 首先贴一下配置:

$server->set([
// 打开包长检测特性
'package_length_check' => true,
// 包头中某个字段作为包长度的值,底层支持了 10 种长度类型。可参考 pack() 方法
'package_length_type' => 'N',
// length 长度值在包头的第几个字节。
'package_length_offset' => 8,
// 从第几个字节开始计算长度,一般有 2 种情况:
//length 的值包含了整个包(包头 + 包体),package_body_offset 为 0
//包头长度为 N 字节,length 的值不包含包头,仅包含包体,package_body_offset 设置为 N
'package_body_offset' => 16,
// 设置最大数据包尺寸,单位为字节
'package_max_length' => 81920
]);

下面是一个数据包结构例子,可以很好的体现了字段含义。

以上通信协议的设计中,包头长度为 4 个整型,16 字节,length 长度值在第 3 个整型处。因此 package_length_offset 设置为 8,0-3 字节为 type,4-7 字节为 uid,8-11 字节为 length,12-15 字节为 serid。

下面来说一下代码实现:

客户端代码:

$client = new Swoole\Client(SWOOLE_SOCK_TCP);

$data = "123456789012345678901234567890";
$type = 0x30;
$uid = 0x123;
$length = strlen($data);
$serid = 0x15;
$head = pack("N4", $type, $uid, $length, $serid);
$body = pack("a{$length}", $data);
$message = $head.$body;

if ($client->connect('127.0.0.1', 9502, -1)) {
    $client->send($message);
    echo $client->recv();
}

$client->close();

服务端代码:

$serv = new Swoole\Server('127.0.0.1', 9502);
$serv->set([
    'open_length_check'     => true,
      'package_max_length'    => 81920,
      'package_length_type'   => 'N',
      'package_length_offset' => 8,
      'package_body_offset'   => 16,
]);

$serv->on('connect', function($server, $fd){
    echo $fd. " Connect !".PHP_EOL;
});

$serv->on('receive', function($server, $fd, $from_id, $data){
    var_dump($data);            // 源数据
    $tmp = unpack("Ntype/Nuid/Nlength", $data);
    $unpacking = unpack("Ntype/Nuid/Nlength/Nserid/a{$tmp['length']}body", $data);
    var_dump($unpacking);        // 解包后数据
    $server->send($fd, " Server Receive Data: ". $unpacking['body']);
});

$serv->on('close', function($server){

});

$serv->start();

客户端运行结果

服务端运行结果

可以看到 客户端成功的把发送的数据回显, 服务端也打印出了接收到的所有数据, 其中有些字段在发送时是 16 进制的, 所以服务端在接收到之后需要进行进制转换, 我这里没有进行转换, 所以显示的数据是 10 进制的。

5. 总结

通过对比可以看出使用固定包头 + 包体的方式是效率最高的一种, 因为他是按照固定长度去读取的。期间专门去了解了 pack 函数的使用方法,但也不确定这么写到底对不对,如果有其他了解的仁兄可以慷慨解答一下,网上相关资料有点少,官方文档上也只给出了几个字段的释义。

6. 扩展知识

6.1 字节序

计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。

举例来说,数值 0x2211 使用两个字节储存:高位字节是 0x22,低位字节是 0x11。

  • 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
  • 小端字节序:低位字节在前,高位字节在后,即以 0x1122 形式储存。

这个前和后指的是内存地址,计算机处理字节时是不知道高低字节之分的,它只知道按顺序读取字节,先读第一个字节,再读第二个字节。

例如: 0x1234567 的读取顺序:

以上就是详解Swoole TCP流数据边界问题解决方案的详细内容,更多关于Swoole TCP流数据边界问题解决方案的资料请关注我们其它相关文章!

(0)

相关推荐

  • php中Swoole的热更新实现代码实例

    使用swoole_http_server替代php-fpm后,由于php长驻内存,修改了代码不能实时调试,需要去手动去重启服务,很是不方便,决定使用inotify来监控文件状态的改变,来给swoole发送reload信号,来实现swoole的热更新. 如何安装inotify就不写了,安装之后可以建立一个脚本文件,如php_reload.sh: #!/bin/sh # src 需要监控的地址 src=/home/server/Project/test/app/ /usr/bin/inotifywa

  • Swoole源码中如何查询Websocket的连接问题详解

    问题 我们项目的 Websocket Server 使用的 Swoole,最近在搭建 beta 环境的时候发现 Websocket 协议虽然升级成功了,但是会出现定时重连,心跳.数据也一直没有发送.项目的生产环境和 beta 一致,但是生产环境确没有这个问题. 定位问题 为了方便调试 Swoole,以下测试是在本地环境下进行. 查看 PHP 日志 在 PHP 日志里,发现一条错误日志: ErrorException: Swoole\WebSocket\Server::push(): the co

  • linux系统虚拟主机开启支持Swoole Loader扩展的方法

    特别说明:只是安装支持Swoole扩展,主机并没有安装服务端.目前支持版本php5.4-php7.2. 1.登陆主机控制面板,找到[远程文件下载]这个功能. 2.远程下载文件填写http://download.myhostadmin.net/vps/SwooleLoader_linux.zip 下载保存的路径填写/others/ 或者从我们下载 3.点击控制面板[php版本],选择php5.3或以上版本,示例选择的是php5.5版本(注意:php5.2版本不能编辑php.ini,所以要选择以上版

  • Centos7安装swoole扩展操作示例

    本文实例讲述了Centos7安装swoole扩展操作.分享给大家供大家参考,具体如下: 我的PHP版本为PHP 7.2.24 更新源 [root@localhost html]# yum update 安装需要支持的扩展 [root@localhost html]# yum install php72w-devel [root@localhost html]# yum install php72w-pear [root@localhost html]# yum install gcc [root

  • 在Windows系统上安装Cygwin搭建Swoole测试环境的图文教程

    前言 昨天,在本地安装 Swoole 调试环境的时候,遇到好几个坑,因为我的电脑是 Windows 系统,所以安装的是 cygwin ,但是过程并不顺利,接连出现安装终端的问题,并一步步查资料排坑,最终也顺利安装成功了,为了让其他人也能一次性就安装成功,省掉很多麻烦闹心事,我特地写了这边文章,希望对有需要的人有所帮助. 下载Swoole Swoole下载地址: https://github.com/swoole/swoole-src/releases 在浏览器中打开下载地址,滑动到下载位置,可以

  • Swoole扩展的6种模式深入详解

    前言 并发问题可以理解为两个问题 并发连接数,就是支持同时接受多少客户端TCP连接 并发请求数,每秒能处理多少请求 Swoole底层基于epoll,所以第一个问题在Swoole扩展中实际上不存在任何问题.使用Swoole可以轻松应对10万甚至100万长连接.开发者唯一需要做的就是修改 ulimit -n 将系统最大文件描述符改为 10万或更大. 不同的模型每秒能处理多少请求数,这个是应用层需要考虑的问题.而且不同的场景下有些模式无法使用.真正的难题就是在这里.实际上 工具永远是死的,而人是活的.

  • swoole锁的机制代码实例讲解

    锁,这个词我们并不陌生,主要的应用场景会发生在高并发下进行锁.今天的这篇文章咱们主要来讲解一下swoole的锁的机制,swoole_lock是如何实现的. swoole_lock类支持5种锁的类型: 文件锁 SWOOLE_FILELOCK 读写锁 SWOOLE_RWLOCK 信号量 SWOOLE_SEM 互斥锁 SWOOLE_MUTEX 自旋锁 SWOOLE_SPINLOCK 创建这些锁的过程其实就是调用构造函数的过程,调用的形式如下: swoole_lock->__construct(int

  • 详解PHP Swoole与TCP三次握手

    握手常见问题 1.连接拒绝 2.Operation now in progress 多是因为丢包.错误ip.backlog满了&阻塞&tcp_abort_on_overflow=0 3.min(maxconn, backlog) ss -lt 连接拒绝 在TCP三次握手的时候,客户端发送SYN这个包给服务端,服务端不接受这个请求,操作系统直接返回了一个RST的包,来拒绝连接的请求. 最常见的情况就是客户端去请求某个服务器,服务端没有绑定对应的端口. 测试代码如下,服务端代码: <?p

  • php使用goto实现自动重启swoole、reactphp、workerman服务的代码

    在平时使用swoole进行开发中,常常遇到这种问题,改了代码之后,手动ctrl+c中断服务,再敲命令重启服务.频繁地重启,感觉心很累. php提供了inotify扩展,调用linux的inotify系统调用,监控文件的变化. 这时候就产生了一个想法,我开一个主进程监控文件变化,再开一个子进程运行swoole服务.主进程监听到文件变化之后,干掉子进程,然后再开一个子进程运行swoole服务. 子进程如果想优雅地退出,安装个信号处理器,在退出之前做一些操作. <?php //index.php re

  • windows系统php环境安装swoole具体步骤

    Swoole原本不支持在Windows下安装的,所以我们要安装Cygwin来使用.在安装Cygwin下遇到了很多坑,百度经验上的文档不是很全,所以我把自己安装Cygwin和Swoole写下来相当于对自己的沉淀吧. 首先准备工具: Cygwin下载地址:https://www.jb51.net/softs/102136.html swoole官方下载地址:https://github.com/swoole/swoole-src/releases 一.下载Swoole和Cygwin 下载Cygwin

随机推荐