粘包形成原因

什么是 TCP 粘包?

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

TCP 出现粘包的原因?

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

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

image-20210327235545861

粘包代码演示

tcpServer.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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

1
2
3
4
5
6
7
8
9
10
11
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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