项目中用到了websocket长链接, 记录下结合swoole如何实现这个功能

项目中之所以要用websocket主要是想实现用户在回收设备上扫码投递瓶子之后,将投递的瓶子数据推送到用户小程序端进行同步展示, 这样用户在设备上投递完瓶子后, 在小程序上就能同时看到相应变化, 给用户一个更好的使用体验
面向过程风格代码
//引入redis
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('***'); //redis密码
$redis->select(15);//选择使用的redis库

//创建websocket服务端
$server = new Swoole\Server('0.0.0.0', 9502, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$server->set(array(
    'ssl_cert_file' => __DIR__.'/config/fullchain.pem',
    'ssl_key_file' => __DIR__.'/config/privkey.pem',
    'ssl_verify_peer' => false,
    'ssl_allow_self_signed' => true,
    'log_file' => __DIR__ . '/cert/' . date('Ymd') . '.log',
));

//监听WebSocket连接打开事件。
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
    $returnData = ['fid' => $request->fd];
    $ws->push($request->fd, json_encode($returnData));
});

//监听WebSocket消息事件。
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
    echo "Message: {$frame->data}\n";
    $ws->push($frame->fd, "server: {$frame->data}");
    $data = json_decode($frame->data, true);
    //用redis存储小程序用户id与wesocket的链接标识fd的对应关系
    if (isset($data['uid']) && $data['uid']) {
        $oldFid = $redis->hGet('user:links', $data['uid']);
        if ($oldFid != $frame->fd) {
            $redis->hSet('user:links', $data['uid'], $frame->fd);
        }
        $returnData = [
            'uid' => $data['uid'],
            'fid' => $frame->fd
        ];
        //给对应链接推送个消息
        $ws->push($frame->fd, json_encode($returnData));
    }
});
//监听WebSocket连接关闭事件。
$server->on('close', function ($server, $fd) {
    echo "client {$fd} closed\n";
});

//设置onRequest回调,WebSocket\Server 也可以同时作为 HTTP 服务器,
//这样就可以通过接收HTTP请求来触发webSocket的推送, 这样就可以在程序中主动触发推送了
$server->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
    global $server;//调用外部的server
    $post = $request->post ?: [];//获取post请求传递的数据
    if (isset($post['fd']) && $ws->isEstablished(intval($request->post['fd']))) {
        $ws->push($request->post['fd'], $request->post['message']);
    }
    //可以通过$server->connections 遍历所有websocket连接用户的fd,给所有用户推送    
});
$server->start();
面向对象风格代码
//声明一个WebSocketServer 服务类
class WebSocketServer
{
public $server;

public function __construct()
{
$this->server = new Swoole\WebSocket\Server("0.0.0.0", 9502);
$this->server->on('open', function (Swoole\WebSocket\Server $server, $request) {
    echo "server: handshake success with fd{$request->fd}\n";
});
$this->server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
    $server->push($frame->fd, "this is server");
});
$this->server->on('close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});
$this->server->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
    // 接收http请求从get获取message参数的值,给用户推送
    // $this->server->connections 遍历所有websocket连接用户的fd,给所有用户推送
    foreach ($this->server->connections as $fd) {
        // 需要先判断是否是正确的websocket连接,否则有可能会push失败
        if ($this->server->isEstablished($fd)) {
            $this->server->push($fd, $request->get['message']);
        }
    }
});
$this->server->start();
}
}

//实例化这个服务类,这样就启动了websocket服务了
new WebSocketServer();

上面分别用了"面向过程风格"与"面向对象风格"演示了如何通过swoole创建一websocket服务端, 有下面几点需要注意的:

  • 通过设置onRequest回调, 在创建websocket服务端的时候同时内部也起了一个http服务器,监听9502端口, 这样在程序中如果想给某个用户主动推送数据的时候, 就可以借助这个9502端口的http服务, 发送post请求, 参数传递要发送请求的websocket链接标识fd以及要传递信息, 这样就可以主动在程序中向用户推送数据了
  • Swoole\Http\Request $request的post属性中存储的是HTTP请求的POST参数,格式为数组
//post属性
Swoole\Http\Request->post: array
//示例
echo $request->post['hello'];
// 获取所有POST参数
var_dump($request->post);


//get属性
//存的是HTTP请求的GET参数, 相当于PHP中的$_GET, 格式为数组
Swoole\Http\Request->get: array
// 如:index.php?hello=123
echo $request->get['hello'];
// 获取所有GET参数
var_dump($request->get);
  • 讲几个配置项
  • ssl_cert_file / ssl_key_file

    设置SSL隧道加密, 设置值为一个文件名字符串, 指定cert证书和key私钥的路径信息.
    wss应用中, 发起WebSocket连接的页面必须使用HTTPS, 且HTTPS应用浏览器必须信任证书才能浏览网页, 浏览器如果不信任sll证书将无法使用wss.
  • log_file

    指定Swoole错误日志文件. 在 Swoole 运行期发生的异常信息会记录到这个文件中,默认会打印到屏幕。开启守护进程模式后 (daemonize => true),标准输出将会被重定向到 log_file。在 PHP 代码中 echo/var_dump/print 等打印到屏幕的内容会写入到 log_file 文件。
  • debug_mode 调试模式

    设置日志模式为 debug 调试模式,只有编译时开启了 --enable-debug 才有作用。
    $server->set([
    'debug_mode' => true
    ])
    
  • daemonize 守护进程化(默认false)

    设置 daemonize => true 时,程序将转入后台作为守护进程运行。长时间运行的服务器端程序必须启用此项。如果不启用守护进程,当 ssh 终端退出后,程序将被终止运行。
  • 启用守护进程后,标准输入和输出会被冲定向到log_file.
  • 如果没有设置log_file, 将重定向到/dev/null, 所有打印屏幕的信息都会被丢弃
  • 使用supervisord或者systemd管理Swoole服务的时候, 请勿设置daemonize=true. 主要因为两者机制不同

实际测试看看效果

在宝塔面板中通过supervisord进行守护进程管理:

image.png

守护进程启动后,就可以进行websocket链接了

image.png

连接成功之后就可以给websocket服务端发送消息了

image.png

如果这个时候想测试通过服务器给某个用户推送数据,就可以借助启动的HTTP服务

// 写一个测试方法,
public function testPush()
{
 $userId = $this->request->param('user_id',3075);
    $redis = RedisService::getInstance();
    $redis->select(15);
    $fd = $redis->hGet('user:links', $userId);
    if (!$fd) {
        return frontReturn(0, 'websockt未连接');
    }
    $domain = 'https://***.demo-domain.com';
    $port = 9502;
    $listNew = ['test' => 'data'];
    Http::post("{$domain}:{$port}", [
        'fd' => $fd,
        'message' => json_encode(['list_new' => $listNew])
    ]);
    return frontReturn(1, 'ok',['list_new' => $listNew]);
}

然后请求接口如下:

image.png

接口请求完成之后,再看刚刚的websocket连接界面,就会收到一条新的消息:

image.png


daoheng
1 声望0 粉丝

活到老,学到老