在 Laravel 中集成 Swoole 实现 WebSocket 服务器
在上篇教程中,我们基于 Swoole 实现了简单的 WebSocket 服务器,然后在客户端通过 JavaScript 实现了 WebSocket 客户端,并演示了 WebSocket 的握手和通信过程,今天,学院君将基于 LaravelS 扩展包把 Swoole 集成到 Laravel 项目来实现 WebSocket 服务器,以便与客户端进行 WebSocket 通信从而实现广播功能。
创建 WebSocketService 类
关于 LaravelS 扩展包我们之前在介绍基于 Swoole 实现 HTTP 服务器的时候已经提到过了,这里不再赘述,没有安装的同学可以去参考那篇教程进行安装和配置,要基于该扩展包实现 WebSocket 服务器,首先首先需要创建一个实现了 Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface
接口的 WebSocketService
类:
<?php
namespace App\Services;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Illuminate\Support\Facades\Log;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
class WebSocketService implements WebSocketHandlerInterface
{
public function __construct()
{
}
// 连接建立时触发
public function onOpen(Server $server, Request $request)
{
// 在触发 WebSocket 连接建立事件之前,Laravel 应用初始化的生命周期已经结束,你可以在这里获取 Laravel 请求和会话数据
// 调用 push 方法向客户端推送数据,fd 是客户端连接标识字段
Log::info('WebSocket 连接建立');
$server->push($request->fd, 'Welcome to WebSocket Server built on LaravelS');
}
// 收到消息时触发
public function onMessage(Server $server, Frame $frame)
{
// 调用 push 方法向客户端推送数据
$server->push($frame->fd, 'This is a message sent from WebSocket Server at ' . date('Y-m-d H:i:s'));
}
// 关闭连接时触发
public function onClose(Server $server, $fd, $reactorId)
{
Log::info('WebSocket 连接关闭');
}
}
在这个 WebSocket 服务器类中,需要实现接口中声明的方法,其实就是 WebSocket 通信事件的回调函数,和上篇教程介绍的原生实现基本一致,只是通过类进行了封装而已。
修改配置文件
接下来,打开配置文件 config/laravels.php
,启用 WebSocket 通信并将刚刚创建的服务器类配置到对应的配置项:
'websocket' => [
'enable' => true,
'handler' => \App\Services\WebSocketService::class,
],
我们还可以在 swoole
配置项中配置 WebSocket 长连接的强制关闭逻辑:
'swoole' => [
...
// 每隔 60s 检测一次所有连接,如果某个连接在 600s 内都没有发送任何数据,则关闭该连接
'heartbeat_idle_time' => 600,
'heartbeat_check_interval' => 60,
...
],
配置 Nginx 支持 WebSocket
由于 WebSocket 通信是基于 HTTP 协议的,所以,我们还要配置 HTTP 服务器来支持 WebSocket 请求,以 Nginx 为例,我们在基于 Swoole 实现 HTTP 服务器一节中添加的 Nginx 配置文件基础上进行 WebSocket 配置,为了与之前基于 PHP-FPM 作为进程管理器的站点配置区分开,创建一个新的站点配置 todoapp-s.conf
(基于待办任务项目进行测试),编辑配置文件内容如下:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream laravels {
# Connect IP:Port
server workspace:5200 weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
server {
listen 80;
server_name todo-s.test;
root /var/www/todoapp/public;
error_log /var/log/nginx/todoapp_s_error.log;
access_log /var/log/nginx/todoapp_s_access.log;
index index.php index.html index.htm;
# Nginx handles the static resources(recommend enabling gzip), LaravelS handles the dynamic resource.
location / {
try_files $uri @laravels;
}
# Response 404 directly when request the PHP file, to avoid exposing public/*.php
#location ~* \.php$ {
# return 404;
#}
# Http and WebSocket are concomitant, Nginx identifies them by "location"
# !!! The location of WebSocket is "/ws"
# Javascript: var ws = new WebSocket("ws://todo-s.test/ws");
# 处理 WebSocket 通信
location =/ws {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout: Nginx will close the connection if the proxied server does not send data to Nginx in 60 seconds; At the same time, this close behavior is also affected by heartbeat setting of Swoole.
# proxy_read_timeout 60s;
proxy_http_version 1.1;
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://laravels;
}
location @laravels {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 60s;
proxy_http_version 1.1;
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://laravels;
}
}
学院君的本地开发环境是 Laradock,所以配置的监听 IP 是 Docker 容器名称 workspace
,如果你不是在 Laradock 中开发,需要将其改成自己的 Swoole WebSocket Server 监听 IP。
配置完成后,即可构建容器并重新启动:
docker-compose build nginx
docker-compose down
docker-compose up -d nginx mysql
此外,记得在对应 Laravel 项目根目录下 .env
环境配置文件中设置如下配置项:
LARAVELS_LISTEN_IP=workspace // 这里的 IP 需要和 nginx upstream 中配置的监听 IP 保持一致
LARAVELS_DAEMONIZE=true
演示基于 Laravel 的 WebSocket 通信
在 Laravel 项目根目下启动 Swoole 服务器:
php bin/laravels start
启动成功后,就可以看到基于 Swoole 的 HTTP 服务器和 WebSocket 服务器信息了:
然后,我们基于上一篇教程创建的 WebSocket 客户端,将其中的 WebSocket Server IP 和端口修改如下:
// 初始化 WebSocket 客户端套接字并建立与服务器的连接
var socket = new WebSocket("ws://todo-s.test/ws");
在浏览器中访问这个客户端 HTML 文件,在弹出窗口输入用户名:
然后点击「确定」,即可开始建立与 Laravel WebSocket 服务器的通信:
在开发者工具栏 Network->WS 标签页同样可以看到基于 HTTP 协议的 Websocket 通信握手和建立过程:
如果我们在输入框中发送消息,即可触发 WebSocket 服务器推送消息给客户端:
同时,在 storage/logs
目录下也可以看到通信连接建立与断开的日志信息:
[2019-05-22 13:55:02] local.INFO: WebSocket 连接建立
[2019-05-22 13:55:10] local.INFO: WebSocket 连接关闭
[2019-05-22 13:56:25] local.INFO: WebSocket 连接建立
[2019-05-22 13:57:25] local.INFO: WebSocket 连接关闭
[2019-05-22 14:01:18] local.INFO: WebSocket 连接建立
[2019-05-22 14:02:20] local.INFO: WebSocket 连接关闭
在下一篇教程中,学院君将会把这个自定义的服务器作为 Laravel Echo 的服务端,在 Laravel 中实现基于 Swoole 的广播通信。
50 Comments
@学院君
我这边启动LaravelS的时候 只有HTTP 服务启动了,socket一直没有启动起来 ,也没看见有报错信息
完全照着教程来的
| PHP | 7.2.16 | | Swoole | 4.2.13 | | LaravelS | 3.5.9 | | Laravel Framework [local] | 5.5.46 | +---------------------------+---------+
@ 学院君
看下错误日志 laravels 配置文件里面的 websocket 设置开启了吗
错误日志没有任何东西, 请求日志里面有请求,socket 配置开启了得,基本上完全跟着教程做的
error during WebSocket handshake: Unexpected response code: 301
弄好了, 配置缓存没有清掉
基于 Swoole HTTP 服务器提供服务每次修改代码都要重启服务器才能看到效果 这一定程度上牺牲了 PHP 的简单便捷
@ 学院君 这里面可以实现多个WebSocket服务器吗?看到配置只能启动一个,不够用啊
laravelS 怎么清空缓存,在服务器上一直报[2019-09-02 19:14:22] [ERROR] Uncaught exception "Swoole\Exception"([99]failed to listen server port[192.168.10.10:5200], Error: Cannot assign requested address[99]) at /home/wwwroot/default/app/hangzhouyijiewenhuayishu/vendor/hhxsv5/laravel-s/src/Swoole/Server.php:59,
0 /home/wwwroot/default/app/hangzhouyijiewenhuayishu/vendor/hhxsv5/laravel-s/src/Swoole/Server.php(59): Swoole\Server->__construct('192.168.10.10', 5200, 2, 1)
1 /home/wwwroot/default/app/hangzhouyijiewenhuayishu/vendor/hhxsv5/laravel-s/src/LaravelS.php(49): Hhxsv5\LaravelS\Swoole\Server->__construct(Array)
2 /home/wwwroot/default/app/hangzhouyijiewenhuayishu/vendor/hhxsv5/laravel-s/src/Console/Portal.php(137): Hhxsv5\LaravelS\LaravelS->__construct(Array, Array)
3 /home/wwwroot/default/app/hangzhouyijiewenhuayishu/vendor/hhxsv5/laravel-s/src/Console/Portal.php(56): Hhxsv5\LaravelS\Console\Portal->start()
4 /home/wwwroot/default/app/hangzhouyijiewenhuayishu/vendor/symfony/console/Command/Command.php(255): Hhxsv5\LaravelS\Console\Portal->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
5 /home/wwwroot/default/app/hangzhouyijiewenhuayishu/bin/laravels(160): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
更改后上传视频好像 上传超过2M就报 413 Request Entity Too Large