WebSocket
ws 模块是 Node 端的一个 WebSocket 协议的实现,该协议允许客户端(一般是浏览器)持久化和服务端的连接. 这种可以持续连接的特性使得 WebSocket 特别适合用于适合用于游戏或者聊天室等使用场景。
Midway 提供了对 ws 模块的支持和封装,能够简单的创建一个 WebSocket 服务。
相关信息:
提供服务
| 描述 | |
|---|---|
| 可用于标准项目 | ✅ |
| 可用于 Serverless | ❌ |
| 可用于一体化 | ✅ |
| 包含独立主框架 | ❌ |
| 包含独立日志 | ❌ |
安装依赖
在现有项目中安装 WebSocket 的依赖。
$ npm i @midwayjs/ws@3 --save
或者在 package.json 中增加如下依 赖后,重新安装。
{
"dependencies": {
"@midwayjs/ws": "^3.0.0",
// ...
},
}
开启组件
@midwayjs/ws 可以作为独立主框架使用。
// src/configuration.ts
import { Configuration } from '@midwayjs/core';
import * as ws from '@midwayjs/ws';
@Configuration({
imports: [ws],
// ...
})
export class MainConfiguration {
async onReady() {
// ...
}
}
也可以附加在其他的主框架下,比如 @midwayjs/koa 。
// src/configuration.ts
import { Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as ws from '@midwayjs/ws';
@Configuration({
imports: [koa, ws],
// ...
})
export class MainConfiguration {
async onReady() {
// ...
}
}
目录结构
下面是 WebSocket 项目的基础目录结构,和传统应用类似,我们创建了 socket 目录,用户存放 WebSocket 业务的服务代码。
.
├── package.json
├── src
│ ├── configuration.ts ## 入口配置文件
│ ├── interface.ts
│ └── socket ## ws 服务的文件
│ └── hello.controller.ts
├── test
├── bootstrap.js ## 服务启动入口
└── tsconfig.json
提供 Socket 服务
Midway 通过 @WSController 装饰器定义 WebSocket 服务。
import { WSController } from '@midwayjs/core';
@WSController()
export class HelloSocketController {
// ...
}
当有客户端连接时,会触发 connection 事件,我们在代码中可以使用 @OnWSConnection() 装饰器来修饰一个方法,当每个客户端第一次连接服务时,将自动调用该方法。
import { WSController, OnWSConnection, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/ws';
import * as http from 'http';
@WSController()
export class HelloSocketController {
@Inject()
ctx: Context;
@OnWSConnection()
async onConnectionMethod(socket: Context, request: http.IncomingMessage) {
console.log(`namespace / got a connection ${this.ctx.readyState}`);
}
}
这里的 ctx 等价于 WebSocket 实例。
消息和响应
WebSocket 是通过事件的监听方式来获取数据。Midway 提供了 @OnWSMessage() 装饰器来格式化接收到的事件,每次客户端发送事件,被修饰 的方法都将被执行。
import { WSController, OnWSMessage, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/ws';
@WSController()
export class HelloSocketController {
@Inject()
ctx: Context;
@OnWSMessage('message')
async gotMessage(data) {
return { name: 'harry', result: parseInt(data) + 5 };
}
}
我们可以通过 @WSBroadCast 装饰器将消息发送到所有连接的客户端上。
import { WSController, OnWSConnection, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/ws';
@WSController()
export class HelloSocketController {
@Inject()
ctx: Context;
@OnWSMessage('message')
@WSBroadCast()
async gotMyMessage(data) {
return { name: 'harry', result: parseInt(data) + 5 };
}
@OnWSDisConnection()
async disconnect(id: number) {
console.log('disconnect ' + id);
}
}
通过 @OnWSDisConnection 装饰器,在客户端断连时,做一些额外处理。
WebSocket Server 实例
该组件提供的 App 即为 WebSocket Server 实例本身,我们可以如下获取。
import { Controller, App } from '@midwayjs/core';
import { Application } from '@midwayjs/ws';
@Controller()
export class HomeController {
@App('webSocket')
wsApp: Application;
}
比如,我们可以在其他 Controller 或者 Service 中广播消息。
import { Controller, App } from '@midwayjs/core';
import { Application } from '@midwayjs/ws';
@Controller()
export class HomeController {
@App('webSocket')
wsApp: Application;
async invoke() {
this.wsApp.clients.forEach(ws => {
// ws.send('something');
});
}
}
心跳检查
有时服务器和客户端之间的连接可能会中断,服务器和客户端都不知道连接的断开情况。
可以通过启用 enableServerHeartbeatCheck 配置心跳检查主动断开请求。
// src/config/config.default
export default {
// ...
webSocket: {
enableServerHeartbeatCheck: true,
},
}
默认检查时间为 30*1000 毫秒,可以通过 serverHeartbeatInterval 进行修改,配置单位为毫秒。
// src/config/config.default
export default {
// ...
webSocket: {
serverHeartbeatInterval: 30000,
},
}
这一配置每隔一段时间会自动发送 ping 包,客户端若没有在下一个时间间隔返回消息,则会被自动 terminate 。
客户端如果希望知道服务端的状态,可以通过监听 ping 消息来实现。
import WebSocket from 'ws';
function heartbeat() {
clearTimeout(this.pingTimeout);
// 每次接收 ping 之后,延迟等待,如果下一次未拿到服务端 ping 消息,则认为出现问题
this.pingTimeout = setTimeout(() => {
// 重连或者中止
}, 30000 + 1000);
}
const client = new WebSocket('wss://websocket-echo.com/');
// ...
client.on('ping', heartbeat);
鉴权
在 WebSocket 连接建立之前,您可能需要对客户端进行身份验证。从 v3.20.9 开始 Midway 提供了 onWebSocketUpgrade 方法来在 WebSocket 握手前进行鉴权。
设置鉴权处理器
您可以在应用启动时设置鉴权处理器:
import { Configuration, Inject } from '@midwayjs/core';
import { Framework } from '@midwayjs/ws';
@Configuration()
export class WSConfiguration {
@Inject()
wsFramework: Framework;
async onReady() {
// 设置升级前鉴权处理器
this.wsFramework.onWebSocketUpgrade(async (request, socket, head) => {
// 从 URL 参数中获取 token
const url = new URL(request.url, `http://${request.headers.host}`);
const token = url.searchParams.get('token');
// 验证 token
if (token === 'valid-token') {
return true; // 允许连接
}
return false; // 拒绝连接
});
}
}
鉴权处理器参数
鉴权处理器接收三个参数:
request: HTTP 请求对象 (http.IncomingMessage)socket: 原始 socket 对象head: WebSocket 握手的头部数据 (Buffer)
处理器需要返回一个 Promise<boolean>:
true: 允许 WebSocket 连接false: 拒绝 WebSocket 连接
获取鉴权信息
您可以从多个来源获取鉴权信息:
URL 参数
this.wsFramework.onWebSocketUpgrade(async (request, socket, head) => {
const url = new URL(request.url, `http://${request.headers.host}`);
const token = url.searchParams.get('token');
const userId = url.searchParams.get('userId');
// 验证逻辑
return await this.validateToken(token, userId);
});
请求头
this.wsFramework.onWebSocketUpgrade(async (request, socket, head) => {
const authorization = request.headers.authorization;
if (!authorization) {
return false;
}
const token = authorization.replace('Bearer ', '');
return await this.validateToken(token);
});
Cookie
this.wsFramework.onWebSocketUpgrade(async (request, socket, head) => {
const cookie = request.headers.cookie;
if (!cookie) {
return false;
}
// 解析 cookie 获取 session 信息
const sessionId = this.parseCookie(cookie).sessionId;
return await this.validateSession(sessionId);
});