基于 Swoole 开发实时在线聊天室(十六):轮询保持长连接优化
这两天 Swoole 生态内部因官方框架之争吵起来,我突然想起来 Swoole 聊天室项目还留了两个小尾巴,一个是长连接轮询的优化,一个是 WebSocket 通信下用户认证的优化,趁着年前把这两个小尾巴处理掉,并结束掉 Swoole 入门到实战教程,吵架是他们的事情,我们专心做技术,该用啥用啥就是。
实现方案
先来看长连接轮询问题,之前的教程中,是通过不断轮询来保持长连接的,这样处理虽然可以保持长连接,但是好像跟之前没有 Websocket 的时候使用 Ajax 轮询也没啥区别,能不能通过一个 Websocket 连接处理所有通信呢?显然可以,Socket.io 本身对此提供了支持,我们只需要仿照它的通信协议来做就好了。
由于 swooletw/laravel-swoole 这个项目对 Socket.io 客户端支持非常友好,而我们的项目中 Websocket 客户端使用的也是 Socket.io,所以我们在服务端仿照 swooletw/laravel-swoole
的服务端实现来做。
代码调整
新增 SocketIOController
首先创建一个 SocketIOController
控制器来处理客户端建立 Websocket 连接请求:
php artisan make:controller SocketIOController
编辑刚刚生成的 app/Http/Controllers/SocketIOController.php
代码如下:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class SocketIOController extends Controller
{
protected $transports = ['polling', 'websocket'];
public function upgrade(Request $request)
{
if (! in_array($request->input('transport'), $this->transports)) {
return response()->json(
[
'code' => 0,
'message' => 'Transport unknown',
],
400
);
}
if ($request->has('sid')) {
return '1:6';
}
$payload = json_encode([
'sid' => base64_encode(uniqid()),
'upgrades' => ['websocket'],
'pingInterval' => config('laravels.swoole.heartbeat_idle_time') * 1000,
'pingTimeout' => config('laravels.swoole.heartbeat_check_interval') * 1000,
]);
return response('97:0' . $payload . '2:40');
}
public function ok()
{
return response('ok');
}
}
响应数据字段说明
这里的返回数据可能看起来有点怪,这是遵循 Socket.io 通信协议的格式,以便客户端可以识别并作出正确的处理。我们简单介绍下这里返回的数据字段
'97:0' . $payload . '2:40'
其中 97
表示返回数据的长度,0
表示开启新的连接,然后是返回的负载数据 $payload
:
sid
表示本次通信的会话 ID;upgrades
表示升级的协议类型,这里是websocket
;pingInterval
表示ping
的间隔时长,可以理解为保持长连接的心跳时间;pingTimeout
表示本次连接超时时间,长连接并不意味着永远不会销毁,否则系统资源就永远不能释放了,在心跳连接发起后,超过该超时时间没有任何通信则长连接会自动断开。
再往后 2
表示客户端发出,服务端应该返回包含相同数据的 packet 进行应答(服务端返回数据以 3
作为前缀,表示应答,比如客户端发送 2probe
服务端返回 3probe
,客户端发送 2
,服务端返回 3
,后者就是心跳连接),最后 40
中的 4
表示的是消息数据,0
表示消息以字节流返回。
新增 socket.io
路由
接下来,在 routes/web.php
中新增两个路由指向上面的两个控制器方法:
Route::get('/socket.io', 'SocketIOController@upgrade');
Route::post('/socket.io', 'SocketIOController@ok');
服务端建立连接代码调整
最后在 routes/websocket.php
中调整连接建立路由代码:
WebsocketProxy::on('connect', function (WebSocket $websocket, Request $request) {
$websocket->setSender($request->fd);
});
删除发送欢迎消息代码,否则将破坏默认的应答消息数据格式,导致 Socket.io 客户端无法正常解析,不断发起客户端建立连接请求。
客户端建立连接代码调整
由于这里将 Websocket 建立连接的入口路由调整为了 /socket.io
,所以还需要调整前端 resources/js/socket.js
中的代码:
import io from 'socket.io-client';
const socket = io('http://webchats.test');
export default socket;
由于 Socket.io 建立连接默认的路径就是 socket.io
,所以可以省略对应的 path
配置,transport
配置也可以去掉,因为现在可以根据服务端返回数据自动判断使用的传输协议。
重新编译前端资源:
npm run dev
Nginx 配置调整
最好,还要调整 Nginx 虚拟主机配置,将 /ws
调整为 /socket.io
:
location ^~ /socket.io {
...
}
测试新的 Websocket 通信
重构 Nginx 容器并重启所有服务:
docker-compose build nginx
docker-compose down
docker-compose up -d nginx mysql redis
然后进入 workspace
容器启动 Websocket 服务器:
cd webchat
bin/laravels start
再次访问聊天室页面,进行登录、进入房间、聊天、退出房间等操作,在开发者控制台就可以看到 所有 Websocket 消息流都是在一个连接中完成:
这样,就完成了通过轮询保持长连接的代码优化,改为基于 Socket.io 客户端发送心跳连接的方式保持长连接(客户端发送 2
,服务端返回 3
作为应答),当然,如果心跳连接发起后,超过超时时间仍然没有任何通信,则会断开长连接:
这里面还有一个 5
,表示切换传输协议之前(比如升级到 Websocket),会测试服务器和客户端是否可以通过此传输进行通信,如果测试成功,客户端将发送升级数据包,请求服务器刷新旧传输上的缓存并切换到新传输。
5 Comments
不知道为啥我这里写97的话,前端就会解析出错,只有写98才能正常运行。
估计还是与 socket.io 客户端的通信出问题了 可能是现在的客户端代码与当时写教程的时候不一样了。。。
我这边也出现了前端解析出错Uncaught SyntaxError: Unexpected end of JSON input,可是前端包的版本是相同的,有点迷惑
@你解决了嘛 我也出现了相同的问题