粘包形成原因

什么是 TCP 粘包?

TCP 粘包是指发送方发送的若干包数据 到 接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

TCP 出现粘包的原因?

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

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

image-20210327235545861

粘包代码演示

tcpServer.php

<?php
echo swoole_get_local_ip()['eth0'].":9503\n";
$server = new Swoole\Server("0.0.0.0", 9503);

$server->on('connect', function ($server, $fd){
   echo "connection open: {$fd}\n";
});

$server->on('receive', function ($server, $fd, $reactor_id, $data) {
   echo "接收到信息".$data." \n";
    $server->send($fd, "swoole: {$data}");
});

$server->on('close', function ($server, $fd) {
   echo "connection close: {$fd}\n";
});

$server->start();

tcpClient.php

<?php
$client = new Swoole\Client(SWOOLE_SOCK_TCP);

if (!$client->connect('127.0.0.1', 9503, -1)) {
    exit("connect failed. Error: {$client->errCode}\n");
}

for($i=0; $i<50; $i++) {
    $client->send('hello_');
}
echo $client->recv();

image-20210328000339499

通过截图,可以看出

client 每次 send 一个 hello_ 字符串以后,server 接收到的字符串粘连在了一起

解决方案

1、特殊字符

根据客户端与服务端相互约定的特殊的符号,对接收的数据进行分割处理

2、固定包头+包体协议(主流)

通过与在数据传输之后会在tcp的数据包中携带上数据的长度,然后呢服务端就可以根据这个长度,对于数据进行截取

特殊字符

方案说明

EOF 结束协议

通过约定结束符,来确定包数据是否发送完毕。
开启open_eof_check=true,并用package_eof来设置一个完整数据结尾字符,同时设置自动拆分open_eof_split

注意
1、要保证业务数据里不能出现package_eof设置的字符,否则将导致数据错误了。
2、可以手动拆包,去掉open_eof_split,自行 explode(“\r\n”, $data),然后循环发送

参考文献

swoole 文档:https://wiki.swoole.com/#/server/setting?id=open_eof_check

代码演示

tcpServer.php

<?php
// var_dump(swoole_get_local_ip());die;
echo swoole_get_local_ip()['eth0'].":9503\n";
$server = new Swoole\Server("0.0.0.0", 9503);

# swoole 拆包
$server->set([
   'open_eof_check' => true,   //打开EOF检测
   'package_eof'    => "\r\n", //设置EOF
]);

$server->on('connect', function ($server, $fd){
   echo "connection open: {$fd}\n";
});

$server->on('receive', function ($server, $fd, $reactor_id, $data) {
   echo "接收到信息".$data." \n";
    $server->send($fd, "swoole: {$data}");
    
    // 自己手动拆包
    // var_dump(explode("\r\n", $data));
});

$server->on('close', function ($server, $fd) {
   echo "connection close: {$fd}\n";
});

$server->start();

tcpClient.php

<?php
$client = new Swoole\Client(SWOOLE_SOCK_TCP);

if (!$client->connect('127.0.0.1', 9503, -1)) {
    exit("connect failed. Error: {$client->errCode}\n");
}

$end = "\r\n";

for($i=0; $i<50; $i++) {
    $client->send('hello_'.$end);
}
echo $client->recv();

image-20210328001322482

固定包头+包体协议

方案说明

原理是通过约定数据流的前几个字节来表示一个完整的数据有多长,从第一个数据到达之后,先通过读取固定的几个字节,解出数据包的长度,然后按这个长度继续取出后面的数据,依次循环。

image-20210328004101484

参考文献

swoole相关配置文档:https://wiki.swoole.com/#/server/setting?id=open_length_check

php的pack函数文档:https://www.php.net/manual/zh/function.pack.php

代码演示

tcpServer.php

<?php
echo swoole_get_local_ip()['eth0'].":9503\n";
$server = new Swoole\Server("0.0.0.0", 9503);

$server->set([
  'open_length_check'     => true,
  'package_max_length'    => 2 * 1024 * 1024,
  'package_length_type'   => 'n',
  'package_length_offset' => 0,
  'package_body_offset'   => 2 
]);

$server->on('connect', function ($server, $fd){
   echo "connection open: {$fd}\n";
});

$server->on('receive', function ($server, $fd, $reactor_id, $data) {
   echo "接收到信息".$data." \n";
    $server->send($fd, "swoole: {$data}");
});

$server->on('close', function ($server, $fd) {
   echo "connection close: {$fd}\n";
});

$server->start();


## 用php方法解包
// $server->on('receive', function ($server, $fd, $reactor_id, $data) {
//     $fooLen = unpack("n", substr($data, 0, 2))[1];
//     // 得到真正的数据
//     $context = substr($data, 2, $fooLen);
//     var_dump($context);
//     $server->send($fd, "Swoole: ok");
//  });

tcpClient.php

<?php
$client = new Swoole\Client(SWOOLE_SOCK_TCP);

if (!$client->connect('127.0.0.1', 9503, -1)) {
    exit("connect failed. Error: {$client->errCode}\n");
}

for($i=0; $i<50; $i++) {
    $context = '123';
    // 利用pack打包长度
    $len = pack("n", strlen($context));
    // 组包
    $send = $len . $context;
    // 发送
    $client->send($send); 
}
echo $client->recv();

image-20210328003318912