业务需求
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-S与Laravel-Swoole
Swoole 让PHP开发不再局限于Web领域,提供了高性能的HTTP、WebSocket等服务,在Laravel
框架中,使用Swoole,也有成熟的两个组件库hhxsv5/laravel-s
和 swooletw/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
特别提示:
- 在国内环境,构建laradock镜像,因为网络原因,建议按需购买阿里云香港ECS,安装Docker后再次构建。
- 设置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,加深印象和理解,融会贯通。在实践过程中,笔者遇到过很多问题和挑战,差点放弃,最后是通过多读多搜方式解决。
那些趟过去的水坑
- JWT授权在Laravel-S组件中不生效/TOKEN在PHP-FPM可以使用Swoole模式下不能使用:https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#cleaners
- PHP-FPM模式下控制器无法调用
app(’swoole’)
对象:https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues-CN.md#class-swoole-does-not-exist- 修改并部署了代码,运行结果就是不生效:
php artisan config:cache
然后重启- Nginx with Swoole HTTP Server配置后,接口访问出现302:https://github.com/hhxsv5/docker/blob/master/nginx/conf.d/laravels.conf
…
基础服务概览
组件服务 | 版本 |
---|---|
PHP | 7.4.33 |
Laravel Framework | 8.83.27 |
Swoole | 4.8.12 |
LaravelS | 3.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服务器,为了实现新订单消息实时通知,我们先思考一下整体交互与实现步骤:
- 定义WebSocket服务器授权路由,添加JWT中间件验证请求
- WebSocket客户端携带JWT Token与WebSocket服务器建立连接
- WebSocket服务器验证Token是否合法,不合法主动关闭连接,并发送授权失败自定义报文
- WebSocket服务器获取授权用户,并将用户ID与WebSocket客户端连接FD绑定并存储,这里使用Swoole提供的共享内存Table
- WebSocket服务器通过授权用户ID向绑定全部的FD广播新订单自定义报文
- WebSocket客户端主动关闭连接,WebSocket服务器监听onClose事件,将FD从共享内存溢出,发送关闭连接自定义报文
- WebSocket服务器心跳检测,一段时间内连接无请求数据,则主动关闭WebSocket连接,将FD从共享内存溢出,发送关闭连接自定义报文
WebSocket服务器连接
WebSocket服务器已经搭建完成,连接需要采用WebSocket协议,可以通过下面两种方式链接。
使用端口连接
ws://127.0.0.1:1215
通过Nginx
虚拟站点连接
ws://local.cc.com/ws
为验证连接是否成功,可以通过EasySwoole-WebSocket在线测试工具 工具验证。
出现以上结果,说明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,你会看到同一个用户绑定了三个不同连接
查看服务器日志,可以看到三次连接信息
[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
页面查看是否收到广播消息。
关闭ws.html
访问窗口,查看是否关闭WebSocket。
[2023-03-02 14:48:24] local.NOTICE: onClose Push Message {"content":"Goodbye #5"}
至此,我们的整个新订单实时消息推送已经完成,如遇问题,邮件联系:huiyonghkw@gmail.com