一、发现大家在写swoole的时候,没有注意到TCP通讯下的粘包和断包问题。
TCP是个"流"协议,所谓流,就是没有界限的一串数据。
故此大致会出现以下四种典型情况:
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.
C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.
D.一次性接收到了data1和data2的全部数据.
A为正常情况,无粘包或断包。
B为断包+粘包。
C为粘包+断包。
D为粘包。
处理这种情况有两种办法:
增加特殊分隔符号,就是现在的做法(但是没有被很好的执行,大部分都没有)
在包头位置增加固定字节的长度标识。
第一种:
在server的set里增加
'open_eof_split'=>true,
'package_eof'=>PHP_EOL
然后在client端
sent的时候拼接上 PHP_EOL
我个人推荐使用第二种做法,更少的性能消耗和更广泛的适用性:
不用回避特定的分隔字符。
不需要逐字符检查。
例子如下:
Client 端:
$client = new \swoole_client(SWOOLE_SOCK_TCP);
if (!$client->connect('10.19.2.187', 9999, -1)) {
exit("connect failed. Error: {$client->errCode}\n");
}
// 发送的时候将body的长度添加到包头
$body = 'hello world!!!';
$head = pack('N', strlen($body));
$client->send($head . $body);
使用pack来计算四个字节的长度
Server 端
$serv = new \swoole_server('0.0.0.0', 9999);
$serv->set([
'open_length_check' => true,
'package_length_type' => "N", // 4个字节
'package_length_offset' => 0,
'package_body_start' => 4, // 表示只计算包体的长度,不包含长头的长度
'package_max_length' => 80000, // 最大包长
'pid_file' => storage_path('pid/').'server.pid',
'worker_num' => 1,
'max_request' => 5000,
'task_worker_num' => 2,
'task_max_request' => 1000,
]);
//监听连接进入事件
$serv->on('connect', function ($serv, $fd) {
//echo "Client: Connect.\n";
});
//监听数据发送事件
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
var_dump($data);
// 解包的过程
$info = unpack('N', $data);
$len = $info[1];
$body = substr($data, - $len);
echo "server received data: {$body}\n";
$serv->task($body);
});
$serv->on('Task', function ($serv, $taskId, $fromId, $data) {
var_dump($data);
echo "onTask\n";
});
$serv->on('Finish', function ($serv, $taskId, $data) {
//echo "finish received data '{$data}'\n";
});
$serv->on('close', function ($serv, $fd) {
echo "Client: Close.\n";
});
$serv->start();
主要有两个地方:
1.在set里面添加以下的参数,这几个参数可以使用swoole内部的组包机制
'open_length_check' => true,
'package_length_type' => "N", // 4个字节
'package_length_offset' => 0,
'package_body_start' => 4, // 表示只计算包体的长度,不包含长头的长度
'package_max_length' => 80000, // 最大包长
2.在'receive' 事件 接受的数据的时候,记得去掉包头,把真正的包体取出来:
$info = unpack('N', $data);
$len = $info[1];
$body = substr($data, - $len);
二、更优雅的进程停止和重启办法
首先在server启动的参数里set中
增加pid文件记录,可以参见上面代码部分:
'pid_file' => storage_path('pid/').'server.pid',
目录建议统一放置,管理起来
然后可以使用下面的代码可以关闭和重启:
当然更建议的方法是使用supervisor
public function stop($pidFile)
{
if (file_exists($pidFile)) {
$pid = file_get_contents($pidFile);
if (!\swoole_process::kill($pid, 0)) {
echo "PID :{$pid} not exist \n";
return false;
}
$sig = SIGTERM;
\swoole_process::kill($pid, $sig);
//等待5秒
$time = time();
$flag = false;
while (true) {
usleep(1000);
if (!\swoole_process::kill($pid, 0)) {
echo "server stop at " . date("y-m-d h:i:s") . "\n";
if (is_file($pidFile)) {
unlink($pidFile);
}
$flag = true;
break;
} else {
if (time() - $time > 5) {
echo "stop server fail.try again \n";
break;
}
}
}
return $flag;
} else {
echo "pid 文件不存在,请执行查找主进程pid,kill!\n";
return false;
}
}
public function reload($pidFile)
{
if (file_exists($pidFile)) {
$sig = SIGUSR1;
$pid = file_get_contents($pidFile);
if (!\swoole_process::kill($pid, 0)) {
echo "pid :{$pid} not exist \n";
return;
}
\swoole_process::kill($pid, $sig);
echo "send server reload command at " . date("y-m-d h:i:s") . "\n";
} else {
echo "pid 文件不存在,请执行查找主进程pid,kill!\n";
}
}