php用workman实现websocket,用多线程解决ws卡顿、阻塞问题

本文讨论了解决Workerman中WebSocket连接缓慢的问题,通过采用多线程并发,一个线程负责发送消息,另一个线程处理WebSocket连接,利用Channel进行消息订阅。同时提到如何动态管理TCP连接以及处理socket同步问题。
摘要由CSDN通过智能技术生成

前言

安装Workerman和channel就不赘述了

场景

业务中需要用tcp读取后台串口数据,然后与前端建立长连接,持续不断发送消息

问题

不明原因导致ws连接非常缓慢,怀疑是一直发消息导致阻塞

解决思路

  1. 使用多线程,一个线程专门发消息,一个线程与前端建立连接
  2. 线程通讯方式:使用channel,以消息订阅形式通讯(在官方文档没找到通讯方式,还得靠ai
  3. 还有一个需求是动态的开关tcp连接,所以需要维护一个用于判断是否存在连接的全局数组
  4. 失败的思路:判断第一个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();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值