前言
安装Workerman和channel就不赘述了
场景
业务中需要用tcp读取后台串口数据,然后与前端建立长连接,持续不断发送消息
问题
不明原因导致ws连接非常缓慢,怀疑是一直发消息导致阻塞
解决思路
- 使用多线程,一个线程专门发消息,一个线程与前端建立连接
- 线程通讯方式:使用channel,以消息订阅形式通讯(
在官方文档没找到通讯方式,还得靠ai) - 还有一个需求是动态的开关tcp连接,所以需要维护一个用于判断是否存在连接的全局数组
- 失败的思路:
判断第一个work线程用于发消息,会导致连上这个ws的客户端阻塞
一些小坑
- socket无法序列化
- work在每个线程都是独立的,但全局变量是唯一的
简化的代码
<?php
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Connection\AsyncUdpConnection;
require_once __DIR__ . '/vendor/autoload.php';
// 初始化一个Channel服务端
$channel_server = new Channel\Server('0.0.0.0', 2206);
// 一个不处理任何客户端连接的Worker
$task_worker = new Worker();
$task_worker->count = 1; // 只启动1个进程
$task_worker->tag = 1;
$task_worker->onWorkerStart = function($worker) {
// Channel客户端连接到Channel服务端
Channel\Client::connect('127.0.0.1', 2206);
// 初始化全局数组 --创建一个数组,该数组的索引从 0 开始,长度为 $worker->count(即 Worker 进程的数量),并且每个元素的初始值都设置为 false
$GLOBALS['noConnectWork'] = array_fill(0, $worker->count, false);
Channel\Client::on('onConnectClose', function ($worker_id) use ($worker) {
// 假设有一个全局数组存储所有无连接的 Worker 进程 ID
$GLOBALS['noConnectWork'][$worker_id] = true;
echo $worker->id . '\n';
// if ($worker->id == 0) {
// 检查是否所有 Worker 进程都没有连接
if (!in_array(false, $GLOBALS['noConnectWork'], true)) {
// 所有 Worker 进程都没有连接,可以执行关闭操作
// 这里执行关闭逻辑
Timer::del($worker->timer_id);
$worker->timer_id = null;
socket_close($worker->socket);
$worker->tag = 1;
}
// }
});
Channel\Client::on('newConnect', function ($worker_id) use ($worker) {
$ws = $worker;
// 开启udp连接,只开启一次
if ($ws->tag) {
$ws->tag = 0; //不知道为什么用不了false
// 要发布的事件名称
$event_name = 'updateTag';
// 发布某个自定义事件,订阅这个事件的客户端会收到事件数据,并触发客户端对应的事件回调
Channel\Client::publish($event_name, $ws->tag);
global $socket;
$ws->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
// 不需要设置超时
socket_set_option($ws->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 1, 'usec' => 0]);
socket_set_option($ws->socket, SOL_SOCKET, SO_REUSEADDR, 1);
// bind只能用一次
socket_bind($ws->socket, '127.0.0.1', 7111);
// global $timer_id;
// $GLOBALS['timer_id'] = Timer::add(0.1, function () use ($ws) {
$ws->timer_id = Timer::add(0.1, function () use ($ws) {
$ip = '127.0.0.1';
$port = 7111;
while (socket_recvfrom($ws->socket, $buf, 2048, 0, $ip, $port))
$res = $buf;
// 消息群发
$hex = bin2hex($res); //这里不转换会无法被游览器解析报错
$response = json_encode([
"data" => $hex,
"receipt_time" => date("Y-m-d H:i:s"),
]);
// $connection->send($response);
$event_name = 'sendData';
Channel\Client::publish($event_name, $response);
}
});
$event_name = 'updateTimer';
Channel\Client::publish($event_name, $ws->timer_id);
}
// }
});
// 要订阅的事件名称(名称可以为任意的数字和字符串组合)
$event_name = 'updateTag';
// 订阅某个自定义事件并注册回调,收到事件后会自动触发此回调
Channel\Client::on($event_name, function ($event_data) use ($worker) {
$worker->tag = $event_data;
});
$event_name = 'updateTimer';
Channel\Client::on($event_name, function ($event_data) use ($worker) {
$worker->timer_id = $event_data;
});
};
websocket部分
global $connectAr; //连接池
$connectArr = [];
$ws = new Worker('websocket://0.0.0.0:9501');
$ws->count = 1; //不能提供多的线程,防止socket多次创建
$ws->connectArr = []; //初始化连接id数组
$ws->onConnect = function ($connection) use ($ws) {
$GLOBALS['noConnectWork'][$ws->id] = false;
Channel\Client::publish('newConnect', $ws->id);
};
$ws->onWorkerStart = function ($worker) {
// Channel客户端连接到Channel服务端
Channel\Client::connect('127.0.0.1', 2206);
$event_name = 'sendData';
// 订阅某个自定义事件并注册回调,收到事件后会自动触发此回调
Channel\Client::on($event_name, function ($event_data) use ($worker) {
foreach ($worker->connections as $conn) {
$conn->send($event_data);
}
});
};
$ws->onMessage = function ($connection, $data) use ($ws) {
};
$ws->onClose = function ($connection) use ($ws) {
// 连接池空了就关闭
if (empty($ws->connectArr)) {
// 当前 Worker 进程没有连接了,发布事件
Channel\Client::publish('onConnectClose', $ws->id);
};
};
Worker::runAll();