基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十六):轮询保持长连接优化

这两天 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),会测试服务器和客户端是否可以通过此传输进行通信,如果测试成功,客户端将发送升级数据包,请求服务器刷新旧传输上的缓存并切换到新传输。

上一篇: 基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十五):实现用户头像上传功能

下一篇: 基于 Laravel + Swoole + Vue 搭建实时在线聊天室(十七):Websocket 通信用户认证逻辑优化