粘包形成原因
什么是 TCP 粘包?
TCP 粘包是指发送方发送的若干包数据 到 接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
TCP 出现粘包的原因?
发送方:
发送方需要等缓冲区满才发送出去,造成粘包
接收方:
接收方不及时接收缓冲区的包,造成多个包接收
粘包代码演示
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();
通过截图,可以看出:
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();
固定包头+包体协议
方案说明
原理是通过约定数据流的前几个字节来表示一个完整的数据有多长,从第一个数据到达之后,先通过读取固定的几个字节,解出数据包的长度,然后按这个长度继续取出后面的数据,依次循环。
参考文献
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();