PC中后台WebSocket新订单实时语音通知解决方案探索

业务需求

SaaS产品中后台常常会有这样的需求,业务端新下了订单,管理员登录PC端后,应该能实时接收到一条语音通知:您有新的订单,请注意查收。如果支持多点登录,在多个登录窗口还需要查看与接收到该条消息。

技术背景

SaaS产品采用PHP语言和Laravel框架开发,基于Nignx with FPM传统Web服务架构运行。应用服务器采用Docker构建,基于Laradock套件构建了专有化的容器镜像,并且通过docker-compose.yml编排快速启动和运行。

容器服务镜像版本
Nginx Web 镜像registry.cn-hongkong.aliyuncs.com/higgses/nginx:1.0.3
PHP-FPM(7.4)镜像registry.cn-hongkong.aliyuncs.com/higgses/php-fpm-74:1.2.0
PHP-FPM(7.2)镜像registry.cn-hongkong.aliyuncs.com/higgses/php-fpm-72:1.0.0
PHP-FPM(5.6)镜像registry.cn-hongkong.aliyuncs.com/higgses/php-fpm-56:1.0.1
基础服务镜像registry.cn-hongkong.aliyuncs.com/higgses/workspace-74:1.3.0
PHP-CLI(7.4)镜像registry.cn-hongkong.aliyuncs.com/higgses/php-worker-74:1.3.0
基础服务镜像registry.cn-hongkong.aliyuncs.com/higgses/dokcer-in-docker:1.0.0
RabbitMQ消息服务镜像registry.cn-hongkong.aliyuncs.com/higgses/rabbitmq:1.0.4

注:以上镜像托管在阿里云香港服务器,公开服务,欢迎使用。

解决方案

解决以上业务需求问题,最直接的办法是通过JavaScript Ajax 定时轮询请求API接口。这对业务服务器处理性能提出了要求,也增加了网络带宽消耗,在浏览器端,未做好回收机制处理,体验就非常差。通常,这样的高并发、低延迟的服务场景推荐使用WebSocket。有效降低频繁HTTP请求IO消耗,服务端也可以主动推送消息,客户端监听并处理就行。

框架集成WebSocket服务

基于Laravel框架,有几种方式构建WebSocket服务器。

第一,框架默认提供

Laravel 框架本身不提供WebSocket服务器,但提供了整合Pusher/Socket.io第三方收费产品的解决方案,降低开发难度,快速上手使用。

第二,采用PHP语言本身的WebSocket组件Laravel Websockets

组件核心依赖cboden/ratchet_,一个基于PHP语言实现的WebSocket服务基础库。开发者在框架层融合并提供了简单快速的使用教程。

第三,基于Swoole实现的WebSocket组件Larave-SLaravel-Swoole

Swoole 让PHP开发不再局限于Web领域,提供了高性能的HTTP、WebSocket等服务,在Laravel框架中,使用Swoole,也有成熟的两个组件库hhxsv5/laravel-sswooletw/laravel-swoole提供了开箱即用的加速服务,Laravel-Swoole由台湾人开发,在中文文档支持上不够友好,这里推荐Laravel-S也是本次方案使用的组件库。

经过持续研究和对比,基于Laravel框架与Swoole网络通信引擎,我们集成和部署一套完整的WebSocket服务解决方案。

构建WebSocket服务器并集成到项目

构建Laradock自定义容器镜像并增加Swoole扩展

laradock/laradock 环境默认不支持Swoole,需要自行配置和安装使用。为了在多个环境中使用Swoole,我们全部安装上该扩展。

克隆 laradock/laradock最新源码,修改文件.env.example 配置参数:

...
WORKSPACE_INSTALL_SWOOLE=true
PHP_FPM_INSTALL_SWOOLE=true
PHP_WORKER_INSTALL_SWOOLE=true
...

然后,复制.env.example.env

cp .env.example .env

特别提示:

  1. 在国内环境,构建laradock镜像,因为网络原因,建议按需购买阿里云香港ECS,安装Docker后再次构建。
  2. 设置CHANGE_SOURCE=true,国内加速构建

依次构建增加了Swoole扩展的容器镜像

docker-compose build --no-cache php-fpm

❯ docker-compose build --no-cache php-worker

❯ docker-compose build --no-cache workspace

构建完成后,可以推送到远程镜像仓库,再其他任意位置可以使用。

❯ 
Successfully built ef168f8fbe00
Successfully tagged laradock_php-fpm:latest

❯ docker tag ef168f8fbe00 registry.cn-hongkong.aliyuncs.com/higgses/php-fpm-74:1.2.0

❯ docker login --username=huiyonghkw@higgses registry.cn-hongkong.aliyuncs.com

❯ docker push registry.cn-hongkong.aliyuncs.com/higgses/php-fpm-74:1.2.0

最后,我们在docker-compose.yml 中引用制作好的容器镜像

...
### PHP-FPM ##############################################
  php-fpm:
    image: registry.cn-hongkong.aliyuncs.com/higgses/php-fpm-74:1.2.0
    container_name: higgses-php-fpm-74

### PHP Worker ############################################
  php-worker:
    image: registry.cn-hongkong.aliyuncs.com/higgses/php-worker-74:1.3.0
    container_name: higgses-php-worker-74

### Workspace Utilities ##################################
  workspace:
    image: registry.cn-hongkong.aliyuncs.com/higgses/workspace-74:1.3.0
    container_name: higgses-workspace-74
...

.env 文件中配置端口

PHP_WORKER_WEBSOCKET_PORT=1215

重新启动容器,检查全部运行容器是否安装成功Swoole扩展。

docker exec higgses-php-fpm-74 php -m|grep swoole
swoole


❯ docker exec higgses-php-worker-74 php -m |grep swoole
swoole

❯ docker exec higgses-workspace-74 php -m | grep swoole
swoole

出现以上提示,就表示容器全部支持Swoole并可以在项目中使用。

在项目中整合Laravel-S组件库

强烈建议先仔细阅读几遍文档 laravel-s/README-CN.md,加深印象和理解,融会贯通。在实践过程中,笔者遇到过很多问题和挑战,差点放弃,最后是通过多读多搜方式解决。

那些趟过去的水坑

  1. JWT授权在Laravel-S组件中不生效/TOKEN在PHP-FPM可以使用Swoole模式下不能使用:https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#cleaners
  2. PHP-FPM模式下控制器无法调用app(’swoole’) 对象:https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues-CN.md#class-swoole-does-not-exist
  3. 修改并部署了代码,运行结果就是不生效:php artisan config:cache 然后重启
  4. Nginx with Swoole HTTP Server配置后,接口访问出现302:https://github.com/hhxsv5/docker/blob/master/nginx/conf.d/laravels.conf

基础服务概览

组件服务版本
PHP7.4.33
Laravel Framework8.83.27
Swoole4.8.12
LaravelS3.7.35

通过Composer安装组件

composer require "hhxsv5/laravel-s:~3.7.0" -vvv

发布配置和二进制文件,每次升级LaravelS后,需重新publish

❯ php artisan laravels publish

按照项目实际环境,修改配置参数

...
LARAVELS_SERVER=WS
LARAVELS_LISTEN_IP=0.0.0.0
LARAVELS_LISTEN_PORT=1215
LARAVELS_DISPATCH_MODE=2
LARAVELS_WORKER_NUM=4
...

app/Services/WebSocket 目录下创建WebSocket Handler类 ,并实现接口WebSocketHandlerInterface


namespace App\Services\WebSocket;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
/**
 * @see https://wiki.swoole.com/#/start/start_ws_server
 */
class WebSocketService implements WebSocketHandlerInterface
{
    // 声明没有参数的构造函数
    public function __construct()
    {
    }
    // public function onHandShake(Request $request, Response $response)
    // {
           // 自定义握手:https://wiki.swoole.com/#/websocket_server?id=onhandshake
           // 成功握手之后会自动触发onOpen事件
    // }
    public function onOpen(Server $server, Request $request)
    {
        // 在触发onOpen事件之前,建立WebSocket的HTTP请求已经经过了Laravel的路由,
        // 所以Laravel的Request、Auth等信息是可读的,Session是可读写的,但仅限在onOpen事件中。
        // \Log::info('New WebSocket connection', [$request->fd, request()->all(), session()->getId(), session('xxx'), session(['yyy' => time()])]);
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
        $server->push($request->fd, 'Welcome to LaravelS');
    }
    public function onMessage(Server $server, Frame $frame)
    {
        // \Log::info('Received message', [$frame->fd, $frame->data, $frame->opcode, $frame->finish]);
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
        $server->push($frame->fd, date('Y-m-d H:i:s'));
    }
    public function onClose(Server $server, $fd, $reactorId)
    {
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
    }
}

重启容器,运行Larave-S验证是否安装成功。

docker exec higgses-workspace-74 php /var/www/ws/laravel-api-app/artisan config:cache

❯ docker exec higgses-workspace-74 php /var/www/ws/laravel-api-app/bin/laravels info
 _                               _  _____
| |                             | |/ ____|
| |     __ _ _ __ __ ___   _____| | (___
| |    / _` | '__/ _` \ \ / / _ \ |\___ \
| |___| (_| | | | (_| |\ V /  __/ |____) |
|______\__,_|_|  \__,_| \_/ \___|_|_____/

Speed up your Laravel/Lumen
>>> Components
+---------------------------+---------+
| Component                 | Version |
+---------------------------+---------+
| PHP                       | 7.4.33  |
| Swoole                    | 4.8.12  |
| LaravelS                  | 3.7.35  |
| Laravel Framework [local] | 8.83.27 |
+---------------------------+---------+
>>> Protocols
+----------------+--------+-----------------------------------------+---------------------+
| Protocol       | Status | Handler                                 | Listen At           |
+----------------+--------+-----------------------------------------+---------------------+
| Main HTTP      | On     | Laravel Router                          | http://0.0.0.0:1215 |
| Main WebSocket | On     | App\Services\WebSocket\WebSocketService | ws://0.0.0.0:1215   |
+----------------+--------+-----------------------------------------+---------------------+
>>> Feedback: https://github.com/hhxsv5/laravel-s

出现以上提示,就表示组件和服务安装正确。

CLI模式如何更好地运行

Swoole本身是多进程运行架构,对象常驻内存,相对于传统PHP-FPM架构,在框架测至少5倍性能提升。

按照前面步骤运行通过,还无法将环境搬到线上,比如Laravel-S组件是在CLI模式下运行,如何保障服务一直启用和稳定;PHP-FPM模式下控制器无法调用Swoole对象,无法实时推送消息到Swoole进程和WebSocket客户端。

通过Supervisord监控和管理Laravel-S服务进程,在PHP-WORKER容器中增加文件laravel-s-worker.conf并填入一下内容。

[program:laravel-s-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/cc/laravel-api-app/bin/laravels start -i
autostart=true
autorestart=true
numprocs=1
user=laradock
redirect_stderr=true

重启容器,并查看进程是否开启。

docker exec higgses-php-worker-74 ps aux
PID   USER     TIME  COMMAND
    1 root      0:01 {supervisord} /usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisord.conf
    7 laradock  0:00 {php} cc-refactor-api /var/www/cc/laravel-api-app laravels: master process
   19 laradock  0:00 {php} cc-refactor-api /var/www/cc/laravel-api-app laravels: manager process
   20 laradock  0:00 {php} cc-refactor-api /var/www/cc/laravel-api-app laravels: worker process 0
   21 laradock  0:00 {php} cc-refactor-api /var/www/cc/laravel-api-app laravels: worker process 1
   22 laradock  0:00 {php} cc-refactor-api /var/www/cc/laravel-api-app laravels: worker process 2
   23 laradock  0:00 {php} cc-refactor-api /var/www/cc/laravel-api-app laravels: worker process 3
   47 root      0:00 ps aux

出现以上提示,就表示配置正确。

使用Swoole替换PHP-FPM

要使用Laravel-S组件提供的广播功能,如:

...
/**@var \Swoole\WebSocket\Server $swoole */
$server = app('swoole');
$fd = 1; // Find fd by userId from a map [userId=>fd].
$success = $server->push($fd, 'Push data to fd#1 in Controller');
echo $success;
...

要实现推送,只有在LaravelS启动时,才会注入这个Swoole\http\server实例到容器中。糟糕的是,传统PHP-FPM运行模式下,无法运行成功。此时,必须要修改Fast-CGI网关,将PHP-FPM替换为Swoole HTTP Server。

再次修改docker-compose.yml,在NGINX依赖中增加PHP-WORKER,让其可以连接1215端口的HTTP服务。


## NGINX Server #########################################
  nginx:
    image: registry.cn-hongkong.aliyuncs.com/higgses/nginx:1.0.3
    container_name: higgses-nginx
    volumes:
      - ${APP_CODE_PATH_HOST}:${APP_CODE_PATH_CONTAINER}${APP_CODE_CONTAINER_FLAG}
      - ${NGINX_HOST_LOG_PATH}:/var/log/nginx
      - ${NGINX_SITES_PATH}:/etc/nginx/sites-available
      - ${NGINX_SSL_PATH}:/etc/nginx/ssl
    ports:
      - "${NGINX_HOST_HTTP_PORT}:80"
      - "${NGINX_HOST_HTTPS_PORT}:443"
      - "${VARNISH_BACKEND_PORT}:81"
    depends_on:
      - php-fpm
      - php-worker

再次修改虚拟站点配置文件,将PHP-FPM代理交由Swoole处理。

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
upstream swoole {
    # 通过 IP:Port 连接
    server php-worker:1215 weight=5 max_fails=3 fail_timeout=30s;
    # 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
    #server unix:/yourpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
    #server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
    #server 192.168.1.2:5200 backup;
    keepalive 16;
}

server {
    listen  80;
    server_name local.cc.com;

    root  /var/www/develop/cc/laravel-api-app/public;
    index  index.html index.htm index.php;
	access_log /var/log/nginx/lightapi.dev.cczhaoche.com_access.log;

    gzip on;
    gzip_static on;
    gzip_comp_level 9;
    gzip_buffers 4 32k;
    gzip_min_length  1k;
    gzip_types text/plain application/json application/x-javascript application/css application/xml application/xml+rss text/javascript application/x-httpd-php image/jpeg image/gif image/png image/x-ms-bmp;
    gzip_vary on;

	# Nginx处理静态资源(建议开启gzip),LaravelS处理动态资源。
    location / {
        try_files $uri @laravels;
    }

	# Http和WebSocket共存,Nginx通过location区分
    # Javascript: var ws = new WebSocket("ws://laravels.com/ws");
    location =/ws {
        proxy_http_version 1.1;
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout:如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接;同时,Swoole的心跳设置也会影响连接的关闭
        # proxy_read_timeout 60s;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://swoole;
    }

    location @laravels {
        proxy_http_version 1.1;
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout 120s;
        proxy_set_header Connection "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_pass http://swoole;
    }
}

这里php-worker 指的是PHP-FPM容器本身,也既是在docker-compose.yml 增加过的depends_on。1215端口是PHP-FPM容器中Supervisord管理的Laravel-S进程服务端口。这样,访问http//local.cc.com/index.php就会将PHP代码交由Swoole HTTP Server处理,性能提升5倍。

此时,在控制器中执行推送WebSocket。

WebSocket 客户端与服务器联通

实时通知实现流程

前面,我们已经搭建好了WebSocket服务器,为了实现新订单消息实时通知,我们先思考一下整体交互与实现步骤:

  1. 定义WebSocket服务器授权路由,添加JWT中间件验证请求
  2. WebSocket客户端携带JWT Token与WebSocket服务器建立连接
  3. WebSocket服务器验证Token是否合法,不合法主动关闭连接,并发送授权失败自定义报文
  4. WebSocket服务器获取授权用户,并将用户ID与WebSocket客户端连接FD绑定并存储,这里使用Swoole提供的共享内存Table
  5. WebSocket服务器通过授权用户ID向绑定全部的FD广播新订单自定义报文
  6. WebSocket客户端主动关闭连接,WebSocket服务器监听onClose事件,将FD从共享内存溢出,发送关闭连接自定义报文
  7. WebSocket服务器心跳检测,一段时间内连接无请求数据,则主动关闭WebSocket连接,将FD从共享内存溢出,发送关闭连接自定义报文

WebSocket服务器连接

WebSocket服务器已经搭建完成,连接需要采用WebSocket协议,可以通过下面两种方式链接。

使用端口连接

ws://127.0.0.1:1215

通过Nginx虚拟站点连接

ws://local.cc.com/ws

为验证连接是否成功,可以通过EasySwoole-WebSocket在线测试工具 工具验证。

ws1

出现以上结果,说明WebSocket服务器已经搭建成功。

编码实现业务流程

routes/web.php中,增加新的路由定义

Route::get('/ws', function () {
    // 响应状态码200的任意内容
    return 'websocket';
})->middleware(['auth:mch']);

修改WebSocket Handler类,实现上面的业务流程


<?php

namespace App\Services\WebSocket;

use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;

/**
 * @see https://www.swoole.co.uk/docs/modules/swoole-websocket-server
 */
class WebSocketService implements WebSocketHandlerInterface
{
    /**@var \Swoole\Table $wsTable */
    private $wsTable;

    // Declare constructor without parameters
    public function __construct()
    {
        $this->wsTable = app('swoole')->wsTable;
    }

    /**
     * 连接成功事件
     *
     * @param Server $server
     * @param Request $request
     * @return void
     */
    public function onOpen(Server $server, Request $request)
    {
        Log::channel('websocket')->notice('onOpen Request', [
            'content' => request()->all()
        ]);
        if (request()->has('token')) {
            //建立连接,请求头或者参数带上token
            $user = Auth::guard('mch')->user();
            if ($user) {
                Log::channel('websocket')->info('onOpen Auth User', [
                    'content' => $user,
                ]);
                $this->wsTable->set('uid:' . $user->admin_id, ['value' => $request->fd]);// 绑定uid到fd的映射
                $this->wsTable->set('fd:' . $request->fd, ['value' => $user->admin_id]);// 绑定fd到uid的映射
                foreach ($this->wsTable as $key => $row) {
                    Log::channel('websocket')->notice('onOpen Set Table', [
                        $key => $row
                    ]);
                }
                $content = [
                    'status' => 1,
                    'message' => "Welcome to Realtime Connection Center #{$request->fd}"
                ];
                //给自身发送
                $server->push($request->fd, json_encode($content));
                Log::channel('websocket')->info('onOpen Push Message', $content);
            } else {
                //未登录用户,断开连接
                $content = [
                    'status' => 0,
                    'message' => "FD#{$request->fd} connect to Realtime Connection Center error, token authorization failed.",
                ];
                Log::channel('websocket')->error('onOpen Error', $content);
                $server->push($request->fd, json_encode($content));
                $server->disconnect($request->fd);
                return;
            }
        } else {
            //未登录用户,断开连接
            $content = [
                'status' => 0,
                'message' => "FD#{$request->fd} connect to Realtime Connection Center error, token not submit.",
            ];
            Log::channel('websocket')->error('onOpen Error', $content);
            $server->push($request->fd, json_encode($content));
            $server->disconnect($request->fd);
            return;
        }
    }

    /**
     * 消息事件
     *
     * @param Server $server
     * @param Frame $frame
     * @return void
     */
    public function onMessage(Server $server, Frame $frame)
    {
        Log::channel('websocket')->notice('onMessage Recived From', [
            'fd' => $frame->fd,
            'data' => $frame->data,
            'opcode' => $frame->opcode,
            'finish' => $frame->finish
        ]);

        // 广播给自己
        foreach ($this->wsTable as $key => $row) {
            if (strpos($key, 'uid:') === 0 && $server->isEstablished($row['value'])) {
                $content = [
                    'status' => 1,
                    'message' => sprintf('Broadcast: new message "%s" from #%d', $frame->data, $frame->fd),
                ];
                Log::channel('websocket')->notice('onMessage', $content);
                $server->push($row['value'], json_encode($content));
            }
        }
    }

    /**
     * 连接关闭事件
     *
     * @param Server $server
     * @param [type] $fd
     * @param [type] $reactorId
     * @return void
     */
    public function onClose(Server $server, $fd, $reactorId)
    {
        $uid = $this->wsTable->get('fd:' . $fd);
        if ($uid !== false) {
            $this->wsTable->del('uid:' . $uid['value']); // 解绑uid映射
        }
        $this->wsTable->del('fd:' . $fd);// 解绑fd映射
        $content = [
            'status' => 1,
            'message' => "Goodbye #{$fd}",
        ];
        $server->push($fd, json_encode($content));
        Log::channel('websocket')->notice('onClose Push Message', $content);
    }
}


新建ws.html文件,填入以下代码:

<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <title>WebSocket Client</title>

    <script type="text/javascript">
        function WebSocketTest() {
            if ("WebSocket" in window) {
                alert("您的浏览器支持 WebSocket!");

                // 打开一个 web socket
                var ws = new WebSocket("ws://127.0.0.1:1215/ws?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9saWdodGFwaS5sb2NhbC5jY3poYW9jaGUuY29tXC9hZG1pblwvbWNoXC9sb2dpbiIsImlhdCI6MTY3NTMyNDI5NCwibmJmIjoxNjc1MzI0Mjk0LCJqdGkiOiJLR01kM09uaktycWlwelE0Iiwic3ViIjoxODEsInBydiI6IjFhYmVmMGNlM2Y4MzM3NTQ5MjViOTc0YjhlM2I1M2FhZWE3MDk3MjUifQ.42MgZ67A9xaipxRlJXi2ED9PeXImbUL3l9X830Oc9Vc");

                ws.onopen = function () {
                    // Web Socket 已连接上,使用 send() 方法发送数据
                    ws.send("客户端向服务器发送消息...");
                    alert("数据发送中...");
                };

                ws.onmessage = function (evt) {
                    var received_msg = evt.data;
                    console.log(received_msg);
                    setTimeout(function () { ws.send('心跳检测') }, 10000);
                };

                ws.onclose = function () {
                    // 关闭 websocket
                    console.log('连接关闭事件');
                };
            } else {
                // 浏览器不支持 WebSocket
                alert("您的浏览器不支持 WebSocket!");
            }
        }

    </script>

</head>

<body>

    <div id="sse">
        <a href="javascript:WebSocketTest()">运行 WebSocket</a>
    </div>

</body>

</html>

打开三个浏览器窗口,访问ws.html,你会看到同一个用户绑定了三个不同连接

ws2

ws3

ws4

查看服务器日志,可以看到三次连接信息

[2023-03-02 13:55:20] local.NOTICE: onOpen Request {"content":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9saWdodGFwaS5sb2NhbC5jY3poYW9jaGUuY29tXC9hZG1pblwvbWNoXC9sb2dpbiIsImlhdCI6MTY3NTMyNDI5NCwibmJmIjoxNjc1MzI0Mjk0LCJqdGkiOiJLR01kM09uaktycWlwelE0Iiwic3ViIjoxODEsInBydiI6IjFhYmVmMGNlM2Y4MzM3NTQ5MjViOTc0YjhlM2I1M2FhZWE3MDk3MjUifQ.42MgZ67A9xaipxRlJXi2ED9PeXImbUL3l9X830Oc9Vc"}} 
[2023-03-02 13:55:20] local.INFO: onOpen Auth User {"content":{"App\\Models\\Admin":{"admin_id":181}}} 
[2023-03-02 13:55:20] local.NOTICE: onOpen Set Table {"fd:1":{"value":181}} 
[2023-03-02 13:55:20] local.NOTICE: onOpen Set Table {"uid:181":{"value":1}} 
[2023-03-02 13:55:20] local.INFO: onOpen Push Message {"content":"Welcome to Realtime Connection Center #1"} 
[2023-03-02 13:55:20] local.NOTICE: onMessage Recived From {"fd":1,"data":"客户端向服务器发送消息...","opcode":1,"finish":true} 
[2023-03-02 13:55:20] local.NOTICE: onMessage {"Push Message":"Broadcast: new message \"客户端向服务器发送消息...\" from #1"} 
[2023-03-02 14:06:12] local.NOTICE: onClose Push Message {"content":"Goodbye #1"} 
[2023-03-02 14:24:33] local.NOTICE: onOpen Request {"content":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9saWdodGFwaS5sb2NhbC5jY3poYW9jaGUuY29tXC9hZG1pblwvbWNoXC9sb2dpbiIsImlhdCI6MTY3NTMyNDI5NCwibmJmIjoxNjc1MzI0Mjk0LCJqdGkiOiJLR01kM09uaktycWlwelE0Iiwic3ViIjoxODEsInBydiI6IjFhYmVmMGNlM2Y4MzM3NTQ5MjViOTc0YjhlM2I1M2FhZWE3MDk3MjUifQ.42MgZ67A9xaipxRlJXi2ED9PeXImbUL3l9X830Oc9Vc"}} 
[2023-03-02 14:24:33] local.INFO: onOpen Auth User {"content":{"App\\Models\\Admin":{"admin_id":181}}} 
[2023-03-02 14:24:33] local.NOTICE: onOpen Set Table {"fd:2":{"value":181}} 
[2023-03-02 14:24:33] local.NOTICE: onOpen Set Table {"uid:181":{"value":2}} 
[2023-03-02 14:24:33] local.INFO: onOpen Push Message {"content":"Welcome to Realtime Connection Center #2"} 
[2023-03-02 14:24:33] local.NOTICE: onMessage Recived From {"fd":2,"data":"客户端向服务器发送消息...","opcode":1,"finish":true} 
[2023-03-02 14:24:33] local.NOTICE: onMessage {"Push Message":"Broadcast: new message \"客户端向服务器发送消息...\" from #2"} 
[2023-03-02 14:24:40] local.NOTICE: onOpen Request {"content":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9saWdodGFwaS5sb2NhbC5jY3poYW9jaGUuY29tXC9hZG1pblwvbWNoXC9sb2dpbiIsImlhdCI6MTY3NTMyNDI5NCwibmJmIjoxNjc1MzI0Mjk0LCJqdGkiOiJLR01kM09uaktycWlwelE0Iiwic3ViIjoxODEsInBydiI6IjFhYmVmMGNlM2Y4MzM3NTQ5MjViOTc0YjhlM2I1M2FhZWE3MDk3MjUifQ.42MgZ67A9xaipxRlJXi2ED9PeXImbUL3l9X830Oc9Vc"}} 
[2023-03-02 14:24:40] local.INFO: onOpen Auth User {"content":{"App\\Models\\Admin":{"admin_id":181}}} 
[2023-03-02 14:24:40] local.NOTICE: onOpen Set Table {"fd:2":{"value":181}} 
[2023-03-02 14:24:40] local.NOTICE: onOpen Set Table {"fd:3":{"value":181}} 
[2023-03-02 14:24:40] local.NOTICE: onOpen Set Table {"uid:181":{"value":3}} 
[2023-03-02 14:24:40] local.INFO: onOpen Push Message {"content":"Welcome to Realtime Connection Center #3"} 
[2023-03-02 14:24:40] local.NOTICE: onMessage Recived From {"fd":3,"data":"客户端向服务器发送消息...","opcode":1,"finish":true} 
[2023-03-02 14:24:40] local.NOTICE: onMessage {"Push Message":"Broadcast: new message \"客户端向服务器发送消息...\" from #3"} 

发送广播消息

routes/web.php文件增加路由定义

Route::namespace('App\Http\Controllers')
    ->group(function () {
        Route::get('broadcast', 'AuthController@broadcast');
    });

app/Http/Controllers/AuthController.php文件,增加方法


use Jiannei\Response\Laravel\Support\Facades\Response;
use Illuminate\Support\Facades\Log;

public function broadcast()
    {
		$this->adminId = 181;
		$this->orderId = 10000;
        $server = app('swoole');
        // $fd = 1; // Find fd by userId from a map [userId=>fd].
        // /**@var \Swoole\WebSocket\Server $swoole */
        $this->wsTable = $server->wsTable;
        // $success = $server->push($fd, 'Push data to fd#1 in Controller');

        //广播给他人
        foreach ($this->wsTable as $key => $row) {
            //查询value中包含adminId的共享内存Table
            /**
                {"fd:1":{"value":181}}
                {"fd:3":{"value":181}}
                {"fd:4":{"value":181}}
                {"uid:181":{"value":4}}
             */
            $fd = explode(':', $key);

            if ($row['value'] == $this->adminId
                && strpos($key, 'fd:') === 0
                && $server->isEstablished($fd[1])
            ) {
                $content = [
                    'status' => 1,
                    'message' => '您收到了新订单消息',
                    'data' => [
                        'order_id' => $this->orderId,
                        'user_id' => $this->adminId,
                        'fd' => $fd[1],
                    ]
                ];

                Log::channel('websocket')->notice('onMessage Broadcoast Message', $content);

                $server->push($fd[1], json_encode($content));
            }
        }

        return Response::ok();
    }

打开浏览器,访问http://local.cc.com/broadcast,然后在ws.html页面查看是否收到广播消息。

ws5

关闭ws.html访问窗口,查看是否关闭WebSocket。

[2023-03-02 14:48:24] local.NOTICE: onClose Push Message {"content":"Goodbye #5"} 

至此,我们的整个新订单实时消息推送已经完成,如遇问题,邮件联系:huiyonghkw@gmail.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会勇禾口王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值