相关知识点
如果看不明白,可以翻开我关于网络编程的其他代码;从简单到复杂的实现
- 基于信号的秒级定时器
- 长连接压测基本思路
- IO多路复用 - select 的实现和fd限制
本质上:在linux下的IO多路复用其实就是增加了套接字状态,让我们知道哪些socket有消息到达
服务端代码
<?php
// 自定义流格式请查看
// - https://www.php.net/manual/zh/function.stream-filter-register
// 注意: UNIX、TCP是流; UDP是数据包。
$addr = "tcp://0.0.0.0:6666";
$mode = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
$socket = stream_socket_server($addr, $errno, $errmsg, $mode);
if (! $socket) {
die("{$errmsg} ({$errno})" . PHP_EOL);
}
// 设定非阻塞IO
stream_set_blocking($socket, false);
// 设定读缓冲区
// - 这里设置为0时, 兼容 hhvm
stream_set_read_buffer($socket, 0);
// 在linux下,多路复用的本质其实就是增加了socket的状态
// 告诉你什么时候可以读
$queues = [];
$read = [];
$write = null;
$except = null;
while(true) {
// 获取连接
$connection = @stream_socket_accept($socket, count($queues) ? 0 : -1, $remoteAddress);
if ($connection) {
$queues[$remoteAddress] = $connection;
stream_set_blocking($connection, false);
echo "accept connection: {$remoteAddress} - total accept:" . count($queues) . PHP_EOL;
}
$read = $queues;
$write = $except = null;
// 观察 $read 的数量 --- total accept
$readyStreams = stream_select($read, $write, $except, 0, 0);
if ($readyStreams === false) {
echo "存在错误" . PHP_EOL;
} else if ($readyStreams > 0) {
foreach ($read as $address=>$connection) {
if (feof($connection) || ! is_resource($connection)) {
echo "{$address}:连接已经断开" . PHP_EOL;
stream_socket_shutdown($connection,STREAM_SHUT_RDWR);
unset($queues[$address]);
continue;
}
// 应答客户端
$buffer = stream_socket_recvfrom($connection, 655350);
$buffer = trim($buffer, "\r\n");
if ($buffer) {
$message = "收到{$buffer}";
var_dump($message);
stream_socket_sendto($connection, "{$message}\n");
}
}
}
}
echo 'stop' . PHP_EOL;
fclose($socket);
客户端代码
<?php
// 发起1020个客户端请求
define("REQUEST_CLIENT_NUM", 1020);
define("TIMER_ALARM", 1);
// 定时任务列表
$callbackMetadata = [];
// 注册事件回调
// 注意与 declare 的区别
// 时间轮算法;实现延时队列的一种常见方案
pcntl_signal(SIGALRM, function( $signal ) {
global $callbackMetadata;
$time = time();
if (! isset($callbackMetadata[$time])) {
$callbackMetadata[$time] = [];
}
foreach ($callbackMetadata[$time] as $index=>$metadata) {
list($interval, $persist, $callback, $params) = $metadata;
// 触发运行
$status = $callback (... $params);
// 继续运行
if ($persist === true && $status) {
$nextTime = $time + $interval;
$callbackMetadata[$nextTime][] = $metadata;
}
}
// alarm闹钟信号 为一次性的;需要在此设置
pcntl_alarm(TIMER_ALARM);
});
$addr = "tcp://10.166.166.166:6666";
$mode = STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT;
// 启动所有客户端
$clients = [];
for ($i=0; $i<REQUEST_CLIENT_NUM; $i++) {
// 客户端启动
$connection = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($connection, '10.166.166.166', 6666);
// 连接设置
socket_set_nonblock($connection);
// 缓存客户端
socket_getsockname($connection, $address, $port);
$clients["{$address}:{$port}"] = $connection;
}
$counter = 0;
// 设定定时任务
foreach ($clients as $addr=>$client) {
$callbackMetadata[(time() + 5)][] = [rand(1, 6), true, function ($addr) {
global $clients, $counter;
$client = $clients[$addr];
if (! $client || ! is_resource($client)) {
echo "{$addr}: 连接已经断开:" . socket_strerror(socket_last_error($client)) . PHP_EOL;
socket_close($client);
unset($clients[$addr]);
return false;
}
socket_write($client, "i'm {$addr}\n");
$counter ++ ;
return true;
}, [$addr]];
}
pcntl_alarm(1);
while(1) {
echo "send {$counter}" . PHP_EOL;
pcntl_signal_dispatch();
sleep(1);
}