【swoole.1.04】udp协议通信和粘包问题

一、udp协议通信

之前所使用的客户端都是tcp协议通信,swoole在支持tcp协议的同时还支持udp协议通信。

udp服务端和客户端

由于udp协议的特性,udp服务端和客户端创建比较简单

服务器创建请查看文档:swoole手册-创建UDP服务器

//创建Server对象,监听 127.0.0.1:9502端口,类型为SWOOLE_SOCK_UDP
$serv = new swoole_server("127.0.0.1", 9502, SWOOLE_PROCESS, SWOOLE_SOCK_UDP); 

//监听数据接收事件
$serv->on('Packet', function ($serv, $data, $clientInfo) {
    $serv->sendto($clientInfo['address'], $clientInfo['port'], "Server ".$data);
    var_dump($clientInfo);
});

//启动服务器
$serv->start(); 

UDP服务器与TCP服务器不同,UDP没有连接的概念。启动Server后,客户端无需Connect,直接可以向Server监听的9502端口发送数据包。对应的事件为onPacket。

  1. $clientInfo是客户端的相关信息,是一个数组,有客户端的IP和端口等内容
  2. 调用 $server->sendto 方法向客户端发送数据

根据文档,一个udp服务端不需要关心connect,不需要关心close,只要注册接收数据事件onPacket即可使用。同时服务端和客户端之间的通信是用sendto方法来发送的而不是send方法

    /**
     * 向任意IP:PORT的服务器发送数据包,仅支持UDP/UDP6的client
     * @param $ip
     * @param $port
     * @param $data
     * @return bool
     */
    function sendto($ip, $port, $data) {}

同时,packet回调和tcp协议的receive回调不同

packet:function onPacket(Swoole\Server $server, string $data, array $client_info); receive:function onReceive(swoole_server $server, int $fd, int $reactor_id, string $data);

从上面可以看见,onReceive回调会透传socket连接的fd,而onPacket并没有fd,只有一个client_info,从文档中得知,client_info中含有的信息为address/port/server_socket等多项客户端信息数据

自己实验一下看看

服务端代码:

<?php
//  创建客户端
$client = new \Swoole\Client(SWOOLE_SOCK_UDP);

//  发送数据
$client->sendto('127.0.0.1', 9001, '我来了');

//  接收数据
var_dump($client->recv());

客户端代码:

<?php
//  创建客户端
$client = new \Swoole\Client(SWOOLE_SOCK_UDP);

//  发送数据
$client->sendto('127.0.0.1', 9001, '我来了');

//  接收数据
var_dump($client->recv());

服务端结果:

[root@iZbp1acp86oa3ixxw4n1dpZ 1.04]# php server.php 
string(9) "我来了"
array(4) {
  ["server_socket"]=>
  int(3)
  ["server_port"]=>
  int(9001)
  ["address"]=>
  string(9) "127.0.0.1"
  ["port"]=>
  int(35352)
}

client_info中包含的信息有4个,最重要的2个信息:

  1. address:客户端ip
  2. port:客户端端口

有了这2个信息,服务端就可以对客户端用sendto做出应答,修改一下代码,在服务端加入应答的代码

$server->on('packet', function (\Swoole\Server $server, string $data, array $clientInfo) {
    var_dump($data);
    $server->sendto($clientInfo['address'], $clientInfo['port'], "你说:" . $data);
});

运行观察一下

[root@iZbp1acp86oa3ixxw4n1dpZ 1.04]# php client.php 
string(18) "你说:我来了"

客户端成功接收到了服务端的应答。

二、tcp和udp的区别

swoole中的tcp和udp区别如此大,其根本原因是在于tcp和udp的特点。

初学者如何理解网络协议?

百度百科-网络协议

网络协议各类简介

有关详细的网络协议知识可以参考上述文章,在附录我也会增加一本尹圣雨编写的TCP&IP网络编程,有兴趣的可以深入了解两个协议的区别

首先我们要知道,什么是网络协议?

网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。

简单的说就是,网络协议相当于通信所用的语言。服务端听得懂中文,客户端也说中文,双方就能交流。如果客户端说英文,服务端不懂,那就没法继续交流下去。

还记不记得第一篇文章,我使用浏览器请求TCP客户端,TCP客户端是可以接收到请求的,那么http协议和tcp协议是不同的网络协议,为什么tcp协议的服务端可以接收到HTTP协议的客户端的请求呢?

因为协议是分层的,自底而上分別是数据链路层、网络层、传输层和应用层。每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务,而HTTP协议(应用层)正式基于TCP协议(传输层)实现的,上层协议使用下层协议的规范和服务,作为基于TCP协议的HTTP协议,当然也符合TCP协议所要求的数据和传输方式

HTTP和TCP的区别和联系

TCP和UDP有什么区别呢

一文搞懂TCP与UDP的区别

可以通过上述文章了解详细区别,在此我举出一些个人理解同时借用下文章内的表格

UDP TCP
是否连接 面向无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
适用场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输
  1. TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

  2. TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付

  3. tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

  4. UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。

  5. TCP对系统资源要求较多,UDP对系统资源要求较少。

TCP协议的特点是可靠的服务,他会保证传送的数据正确,UDP协议的特点是尽最大努力交付,不会管数据是否完整送达(可以通过应用处理,但是协议本身是不在乎数据送达的)

三、TCP的粘包、分包和处理

什么是粘包和分包?

粘包分包相关内容请参考:TCP粘包,UDP不存在粘包问题

正是因为上面所提到的TCP提供的可靠服务导致了TCP在某些情况下变得不那么可靠。那么TCP是怎么达到自己数据可靠的目的的呢?

文章中有提到:由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象(确切来讲,对于基于TCP协议的应用,不应用包来描述,而应 用 流来描述)。

也就是说,粘包会在两种情况下产生:

  1. 发送方:发送方需要等缓冲区满才发送出去,造成粘包。

  2. 接收方:接收方不及时接收缓冲区的包,造成多个包接收。

分包粘包的问题

作为一个程序员,要解决一个bug,首先需要复现这个bug,让我复现一下上面两种情况(没错我要写bug了)

1)发送方:发送方需要等缓冲区满才发送出去,造成粘包。

这种情况很简单,当我瞬间向客户端发送N个数据包的时候很容易复现这个问题,下面上代码

服务端:

<?php
//  创建server对象,监听所有ip,9001端口
$server = new \Swoole\Server('0.0.0.0', 9001);

//  修改配置
$server->set([
    'worker_num' => 2,
    'heartbeat_idle_time' => 8,
    'heartbeat_check_interval' => 3
]);

//  监听连接进入事件
$server->on('connect', function (\Swoole\Server $server, int $fd, int $reactorId) {
//    $server->send($fd, str_repeat("abc", 1024 * 512));
    for ($i = 0; $i < 9; $i++) {
        $server->send($fd, '123');
    }
});

//  监听数据接收事件
$server->on('receive', function (swoole_server $server, int $fd, int $reactor_id, string $data) {
    echo "触发数据接收回调,已接收:{$data}" . PHP_EOL;
});

//  监听连接关闭事件
$server->on('close', function () {
    echo "触发关闭回调" . PHP_EOL;
});

//  启动服务器
$server->start();

客户端:

<?php
//  创建客户端
$client = new \Swoole\Client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);

//  连接服务器事件
$client->on('connect', function (\Swoole\Client $client) {
    $client->send('我来了');
});

//  接收消息事件
$client->on('receive', function (\Swoole\Client $client, $data) {
    echo "收到了:" . $data . PHP_EOL;
//    echo strlen($data) . PHP_EOL;
});

//  出错事件
$client->on('error', function (\Swoole\Client $client) {
    echo '出错了' . PHP_EOL;
});

//  关闭连接事件
$client->on('close', function (\Swoole\Client $client) {
    echo '关闭了' . PHP_EOL;
});

//  连接到服务器
$client->connect('127.0.0.1', 9001);

//  关闭连接
//$client->close();

服务端往客户端发送了9次123,理想情况下,客户端应该显示

收到了:123 收到了:123 收到了:123 ... 收到了:123

然而现实是残酷的,客户端显示如下

[root@iZbp1acp86oa3ixxw4n1dpZ 02.pack]# php client.php 
收到了:123123123123123123123123123

这就是上面所说的发送方需要等缓冲区满才发送出去,造成粘包。服务端一瞬间发送了9个小包,这9个小包在TCP的缓冲区被打包了起来,变成了123123123123123123123123123,一次性发送给了客户端。客户端收到123123123123123123123123123并没有办法区分这到底是不是一个正常的数据流,只能认为这是一个包展示到了命令行。每次send停止一下会收到正常的数据流,这里就不演示了。

2)接收方:接收方不及时接收缓冲区的包,造成多个包接收。

话不多说,直接上代码。

修改服务端发送数据包的代码:

//  监听连接进入事件
$server->on('connect', function (\Swoole\Server $server, int $fd, int $reactorId) {
    $message = str_repeat("abc", 1024 * 512);
    var_dump("我发送了" . strlen($message) . "个字符");
    $server->send($fd, $message);
});

因为这种情况下数据包会非常大,所以客户端就只展示大小就可以了:

//  接收消息事件
$client->on('receive', function (\Swoole\Client $client, $data) {
    var_dump("我收到了" . strlen($data) . "个字符");
});

理想情况下,发送和收到的字符数量应该是一致的。现实是这样的:

服务端:

[root@iZbp1acp86oa3ixxw4n1dpZ 02.pack]# php server.php 
string(28) "我发送了1572864个字符"

客户端:

[root@iZbp1acp86oa3ixxw4n1dpZ 02.pack]# php client.php 
string(26) "我收到了65536个字符"
string(26) "我收到了65536个字符"
string(26) "我收到了65536个字符"
...
string(26) "我收到了65536个字符"

TCP协议数据包是没有边界的,数据量太大TCP协议就会将数据包分割成不同的数据流发出,于是客户端就recv到了多次分开的数据。

解决分包粘包

解决分包粘包有两种方法,分别是EOF检测和长度检查

1)EOF检测

EOF=End Of File,即在每段数据中间加入固定结束字符串,当出现粘包问题的时候只要找到EOF然后将包分开即可,直接上代码感受一下。

服务端修改发送数据:

//  监听连接进入事件
$server->on('connect', function (\Swoole\Server $server, int $fd, int $reactorId) {
    $eof = "在这停顿";
    for ($i = 0; $i < 9; $i++) {
        $server->send($fd, '123' . $eof);
    }
});

客户端修改receive回调:

//  接收消息事件
$client->on('receive', function (\Swoole\Client $client, $data) {
    $eof = "在这停顿";
    $data = explode($eof, $data);
    foreach ($data as $v) {
        if (!empty($v)) {
            echo "收到了:" . $v . PHP_EOL;
        }
    }
});

客户端实际效果:

[root@iZbp1acp86oa3ixxw4n1dpZ 02.pack]# php client.php 
收到了:123
收到了:123
收到了:123
收到了:123
收到了:123
收到了:123
收到了:123
收到了:123
收到了:123

但是这个方法有很多缺点

  1. 包内不能含有EOF字符,不然会误拆包
  2. 效率低下,每次收到数据包需要先扫描整个字符串来匹配EOF

除了EOF大法,我们还有另一种方法,就是长度检查

2)长度检查

长度检查的原理很简单,就是在发送数据前在数据前面加上包头,标示数据包长度和长度类型,在PHP中发送方可以使用函数 pack 来打包一个包头,告诉接收方数据包的长度,接收方使用 unpack 拆出包头获取数据包的真实长度然后进行截取。

使用方法很简单,pack和unpack的使用方法请自行查看文档

直接就粘包问题上代码:

服务端:

//  监听连接进入事件
$server->on('connect', function (\Swoole\Server $server, int $fd, int $reactorId) {
    for ($i = 0; $i < 9; $i++) {
        $message = 123;
        $pack = pack('N', strlen($message));
        $server->send($fd, $pack . $message);
    }
});

客户端:

//  接收消息事件
$client->on('receive', function (\Swoole\Client $client, $data) {
    $packLength = 4;  //  包头大小为固定4字节
    while ($data) {
        $length = unpack('N', $data)[1];
        $message = substr($data, $packLength, $length);
        var_dump("我收到了:" . $message);
        $data = substr($data, $packLength + $length);
    }
});

客户端显示如下:

[root@iZbp1acp86oa3ixxw4n1dpZ 02.pack]# php client.php 
string(18) "我收到了:123"
string(18) "我收到了:123"
string(18) "我收到了:123"
string(18) "我收到了:123"
string(18) "我收到了:123"
string(18) "我收到了:123"
string(18) "我收到了:123"
string(18) "我收到了:123"
string(18) "我收到了:123"

因为只需要打包解包就能知道数据包的真实大小,拆包的时候不需要扫描全字符串就能拆出正确的数据包,数据处理仅进行指针偏移,所以性能非常高,推荐使用。

至于分包的问题,只要将第一次未拆包未完的数据和下一次的数据拼成一个数据流就可以啦!这里就不演示了。下一篇文章向大家展示如何用swoole自带的功能解决粘包分包问题。

附录

服务器创建请查看文档:swoole手册-创建UDP服务器

packet回调

receive回调

初学者如何理解网络协议?

百度百科-网络协议

网络协议各类简介

HTTP和TCP的区别和联系

一文搞懂TCP与UDP的区别

TCP粘包,UDP不存在粘包问题

pack

unpack

实践《TCP/IP网络编程》PDF+代码+《图解TCP/IP》PDF+对比分析

!!支持正版,觉得TCP&IP网络编程(尹圣雨)有用的朋友希望可以自行购买一本或购买正版电子书!!

程序幼儿员-龚学鹏
请先登录后发表评论
  • latest comments
  • 总共0条评论