news 2026/10/8 1:32:23

Midway 集成 @midwayjs/ws 构建 WebSocket 服务:从组件接入、事件处理到心跳与鉴权的完整实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Midway 集成 @midwayjs/ws 构建 WebSocket 服务:从组件接入、事件处理到心跳与鉴权的完整实践指南
  • 后端
  • 微服务
  • 云原生

【免费下载链接】midway

🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

导读

本文基于 Midway 官方扩展文档(site/docs/extensions/ws.md),系统讲解如何通过@midwayjs/ws组件在 Midway 应用中快速搭建基于 ws 的 WebSocket 服务。WebSocket 协议允许客户端(通常是浏览器)与服务端保持持久化连接,特别适合游戏、聊天室、实时推送等需要双向实时通信的场景。读完本文,你将掌握从依赖安装、组件开启、@WSController服务定义、消息收发与广播、WebSocket Server 实例获取,到心跳检查、握手鉴权、本地测试以及完整配置项的全部实操能力,并结合仓库源码理解其底层实现原理。

能力与服务场景

@midwayjs/ws是 Midway 对 Node 端 ws 模块的官方封装,让开发者可以用装饰器风格编写 WebSocket 服务,而无须直接操作底层ws的 API。其相关服务能力如下:

描述支持情况
可用于标准项目✅
可用于 Serverless❌
可用于一体化✅
包含独立主框架❌
包含独立日志❌

从源码结构看,组件提供MidwayWSFramework(对应Framework导出)与WebSocketConfiguration(对应Configuration导出,见 packages/ws/src/index.ts),命名空间为webSocket,因此它不能独立承担主框架职责,但既可以作为独立 WebSocket 服务启动,也可以附加在其他主框架(如@midwayjs/koa)之下复用 HTTP 服务。

安装依赖

在现有项目中安装 WebSocket 依赖:

$ npm i @midwayjs/ws@4 --save

或者在package.json中增加如下依赖后,重新安装:

{ "dependencies": { "@midwayjs/ws": "^4.0.0" // ... } }

组件还依赖@midwayjs/core、ws等基础模块,项目脚手架通常会一并安装。组件在启动时会注册独立的日志客户端wsLogger,日志文件名为midway-ws.log(见 packages/ws/src/configuration.ts)。

开启组件

作为独立服务启动

@midwayjs/ws可以独立提供 WebSocket 服务,在src/configuration.ts中将其导入即可:

// src/configuration.ts import { Configuration } from '@midwayjs/core'; import * as ws from '@midwayjs/ws'; @Configuration({ imports: [ws], // ... }) export class MainConfiguration { async onReady() { // ... } }

此时框架会使用webSocket配置中指定的端口(默认7001)自行创建 HTTP 服务并监听upgrade事件。

附加在其他主框架下

也可以附加在@midwayjs/koa等其他主框架下,WebSocket 服务与 Web 框架共享同一个 HTTP 服务与端口:

// 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() { // ... } }

在源码中,MidwayWSFramework.run()会先判断配置是否携带port:未配置端口时,从容器中取出HTTP_SERVER_KEY对应的共享 HTTP Server 并挂载 WebSocket 升级处理;配置了端口时才创建独立 HTTP 服务并监听该端口(见 packages/ws/src/framework.ts)。这正是"可独立启动、也可与 Web 框架共端口"两种模式的底层来源。

目录结构

WebSocket 项目的基础目录结构与传统 Midway 应用类似,只是新增了socket目录用于存放 WebSocket 业务服务代码:

. ├── package.json ├── src │ ├── configuration.ts ## 入口配置文件 │ ├── interface.ts │ └── socket ## ws 服务的文件 │ └── hello.controller.ts ├── test ├── bootstrap.js ## 服务启动入口 └── tsconfig.json

仓库测试夹具同样遵循这一约定,例如 packages/ws/test/fixtures/base-app/src/socket/api.ts 就是放在socket目录下的控制器。

提供 Socket 服务:@WSController 与 @OnWSConnection

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}`); } }

:::info 这里的ctx等价于 WebSocket 实例本身。 :::

从实现上看,OnWSConnection装饰器(以及消息、广播、断连等)会将事件元数据注册到控制器类上(WS_EVENT_KEY),框架启动时通过DecoratorManager.listModule(WS_CONTROLLER_KEY)扫描控制器,并对每个连接触发connection事件后执行对应的方法(见 packages/ws/src/framework.ts 与 packages/core/src/decorator/ws/webSocketEvent.ts)。需要特别说明的是,当前实现中 WebSocket 只支持单个命名空间("ws just one namespace"),框架只取第一个扫描到的控制器注册连接处理,因此业务上通常只需维护一个socket控制器。

消息与响应

WebSocket 通过事件监听的方式获取数据。@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 }; } }

方法的返回值会被自动回送给发起请求的客户端;当返回值为对象时,框架会通过JSON.stringify序列化后发送(源码中的formatResult处理,见 packages/ws/src/framework.ts),这也是测试中客户端拿到的是 JSON 字符串的原因。

广播:@WSBroadCast

通过@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装饰器,可以在客户端断连时做一些额外处理(如清理房间成员、释放资源)。测试夹具 packages/ws/test/fixtures/base-app-broadcast/ 展示了广播场景:两个客户端同时监听message,其中一个发送消息后,两个客户端都会收到返回结果(见 packages/ws/test/index.test.ts 的should test websocket broadcast用例)。

事件装饰器的更多能力

从 packages/core/src/decorator/ws/webSocketEvent.ts 可以看到完整的事件类型体系WSEventTypeEnum:

  • ON_CONNECTION:连接建立(@OnWSConnection)
  • ON_DISCONNECTION:连接断开(@OnWSDisConnection)
  • ON_MESSAGE:接收消息(@OnWSMessage)
  • ON_SOCKET_ERROR:socket 错误
  • EMIT:定向发送(@WSEmit(messageName, roomName))
  • BROADCAST:广播(@WSBroadCast(messageName, roomName))

其中@OnWSMessage(eventName, eventOptions)与@OnWSConnection(eventOptions)还支持传入事件级中间件(middleware数组),用于在单个事件处理前执行过滤或预处理逻辑。同时保留了几个已废弃别名:@OnMessage、@Emit、@OnDisConnection、@OnConnection,新代码应统一使用@OnWS*系列。

框架在响应时还会检查方法参数的最后一个是否为函数:若是则按ack 回调处理(将结果直接传给该回调),否则按 emit/广播逻辑发送(见 packages/ws/src/framework.ts)。

WebSocket Server 实例

组件提供的 App 即为 WebSocket Server 实例本身,可以通过@App('webSocket')注入获取:

import { Controller, App } from '@midwayjs/core'; import { Application } from '@midwayjs/ws'; @Controller() export class HomeController { @App('webSocket') wsApp: Application; }

之后即可在任意 Controller 或 Service 中操作底层 Server,例如遍历所有客户端广播消息:

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'); }); } }

Application类型(IMidwayWSApplication)是 Midway 应用接口与WebSocket.Server的交叉类型(见 packages/ws/src/interface.ts),因此既具备 Midway 的依赖注入、中间件能力,也拥有ws原生的clients、handleUpgrade、broadcast等属性与方法。

连接与事件级中间件

与 HTTP 框架类似,WebSocket 也支持中间件机制。通过useConnectionMiddleware注册的连接中间件会在每次新连接建立、事件处理之前执行:

import { Framework } from '@midwayjs/ws'; // 在 configuration 中注入 Framework 后调用 this.wsFramework.useConnectionMiddleware(async (ctx, next) => { // 连接级处理,如统计连接数、打日志 await next(); });

仓库测试夹具 packages/ws/test/fixtures/base-app-filter/ 演示了连接中间件的过滤效果:客户端发送消息后收到的是中间件拦截返回的packet error而非正常业务结果(见 packages/ws/test/index.test.ts 的should test create socket and with filter用例)。此外,@OnWSMessage、@OnWSConnection装饰器支持的事件级middleware配置,会在单个事件触发时叠加执行(见 packages/ws/src/framework.ts)。

心跳检查

服务器和客户端之间的连接有时会中断,而双方都无从感知。可以通过开启enableServerHeartbeatCheck配置由服务端主动探测并清理失效连接:

// src/config/config.default export default { // ... webSocket: { enableServerHeartbeatCheck: true, }, }

默认检查间隔为30 * 1000毫秒,可通过serverHeartbeatInterval修改(单位毫秒):

// src/config/config.default export default { // ... webSocket: { serverHeartbeatInterval: 30000, }, }

该配置生效后,服务端会每隔serverHeartbeatInterval毫秒向所有客户端发送ping包;若客户端在下一个检查周期内没有返回(通过pong置位isAlive),该连接将被自动terminate。对应实现位于 packages/ws/src/framework.ts 的startHeartBeat()方法:

  • 遍历app.clients,对isAlive === false的 socket 直接terminate();
  • 其余 socket 先置isAlive = false再ping(),等待pong事件回调置回true。

同时,connection事件处理中会注册socket.on('pong', ...)回调来恢复存活标记。组件默认配置enableServerHeartbeatCheck: false、serverHeartbeatInterval: 30000(见 packages/ws/src/configuration.ts)。

客户端如果希望感知服务端状态,可以监听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);

测试用例should test heartbeat timeout and terminate(见 packages/ws/test/index.test.ts)验证了:客户端terminate后,服务端的clients数量最终收敛为 0,说明失效连接被正确清理。

鉴权

在 WebSocket 连接建立(握手)之前,往往需要对客户端进行身份验证。从v3.20.9开始,Midway 提供onWebSocketUpgrade方法,用于在 WebSocket 握手前完成鉴权。

设置鉴权处理器

在应用启动阶段(onReady)注入Framework并注册鉴权处理器:

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; // 拒绝连接 }); } }

从源码实现看(packages/ws/src/framework.ts),run()中监听 HTTP Server 的upgrade事件:若注册了鉴权处理器,会先异步执行它;返回false或抛出异常时,会记录告警/错误日志并直接socket.destroy()拒绝连接;通过后才调用app.handleUpgrade完成 WebSocket 升级并派发connection事件。同时支持传入null来移除鉴权处理器。

鉴权处理器参数

鉴权处理器接收三个参数:

  • request:HTTP 请求对象(http.IncomingMessage),包含 URL、headers 等信息
  • socket:原始 socket 对象
  • head:WebSocket 握手的头部数据(Buffer)

处理器需要返回一个Promise<boolean>:

  • true:允许 WebSocket 连接
  • false:拒绝 WebSocket 连接

对应的类型定义为UpgradeAuthHandler(见 packages/ws/src/interface.ts),组件还将其挂载为应用方法app.onWebSocketUpgrade(handler),便于在应用实例层面调用。

获取鉴权信息

可以从多个来源获取鉴权信息:

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); });

测试夹具 packages/ws/test/fixtures/base-app-upgrade-auth/ 与用例should test onWebSocketUpgrade authentication(见 packages/ws/test/index.test.ts)验证了完整链路:无 token 或 token 无效的连接被拒绝(测试工具testConnectionRejected通过监听open/error/close与超时判断拒绝结果),携带valid-token的连接可以正常收发消息。

本地测试

配置测试端口

ws 框架可以独立启动(依附于默认的 HTTP 服务),也可以与其他 Midway 框架一起启动。

作为独立框架启动时,需要显式指定端口:

// src/config/config.default export default { // ... webSocket: { port: 3000, }, }

作为副框架启动时(例如与 koa/http 共用),由于 HTTP 框架在单测时未指定端口(supertest 自动生成端口),WebSocket 无法确定监听地址,因此可以仅在测试环境为 WebSocket 显式指定一个端口:

// src/config/config.unittest export default { // ... koa: { port: null, }, webSocket: { port: 3000, }, }

:::tip

  • 1、这里的端口仅为 WebSocket 服务在测试时启动的端口
  • 2、koa 中的端口为 null,即意味着在测试环境下,不配置端口,不会启动 http 服务

:::

测试代码

和其他 Midway 测试方法一样,使用createApp启动项目:

import { createApp, close } from '@midwayjs/mock' // 这里使用的 Framework 定义,以主框架为准 import { Framework } from '@midwayjs/koa'; describe('/test/index.test.ts', () => { it('should create app and test webSocket', async () => { const app = await createApp<Framework>(); //... await close(app); }); });

测试客户端

可以直接使用ws包编写客户端测试,也可以使用 Midway 基于ws封装的createWebSocketClient测试客户端(其实现会在连接open后 resolve 出客户端实例,见 packages/mock/src/client/ws.client.ts)。

Promise 回调写法:

import { createApp, close, createWebSocketClient } from '@midwayjs/mock'; import { sleep } from '@midwayjs/core'; // ... 省略 describe it('should test create websocket app', async () => { // 创建一个服务 const app = await createApp<Framework>(); // 创建一个客户端 const client = await createWebSocketClient(`ws://localhost:3000`); const result = await new Promise(resolve => { client.on('message', (data) => { // xxxx resolve(data); }); // 发送事件 client.send(1); }); // 判断结果 expect(JSON.parse(result)).toEqual({ name: 'harry', result: 6, }); await sleep(1000); // 关闭客户端 await client.close(); // 关闭服务端 await close(app); });

使用 Node 自带的events模块的once方法改写后,代码更简洁:

import { sleep } from '@midwayjs/core'; import { once } from 'events'; import { createApp, close, createWebSocketClient } from '@midwayjs/mock'; // ... 省略 describe it('should test create websocket app', async () => { // 创建一个服务 const app = await createApp<Framework>(process.cwd()); // 创建一个客户端 const client = await createWebSocketClient(`ws://localhost:3000`); // 发送事件 client.send(1); // 用事件的 promise 写法监听 let gotEvent = once(client, 'message'); // 等待返回 let [data] = await gotEvent; // 判断结果 expect(JSON.parse(data)).toEqual({ name: 'harry', result: 6, }); await sleep(1000); // 关闭客户端 await client.close(); // 关闭服务端 await close(app); });

两种写法效果相同,按自己习惯选择即可。仓库测试 packages/ws/test/index.test.ts 正是采用第二种写法:发送1、2分别得到{ result: 6 }、{ result: 7 },完整覆盖了消息收发、广播、心跳、鉴权与链路追踪(entry/exit span)等场景。

配置总览

默认配置

@midwayjs/ws的默认配置样例如下:

// src/config/config.default export default { // ... webSocket: { port: 7001, }, }

当@midwayjs/ws与@midwayjs/web、@midwayjs/koa、@midwayjs/express同时启用时,可以复用 Web 框架端口(此时不要再给webSocket配置port):

// src/config/config.default export default { // ... koa: { port: 7001, } webSocket: { // 这里不配置即可 }, }

配置属性说明

属性类型描述
portnumber可选,如果传递了该端口,ws 内部会创建一个该端口的 HTTP 服务。如果希望和 midway 其他的 web 框架配合使用,请不要传递该参数。
serverhttpServer可选,当传递 port 时,可以指定一个已经存在的 webServer

从配置类型IMidwayWSConfigurationOptions(见 packages/ws/src/interface.ts)可以看到组件还支持更多选项:

属性类型默认值描述
enableServerHeartbeatCheckbooleanfalse是否开启服务端心跳检查
serverHeartbeatIntervalnumber30000心跳检查间隔(毫秒)
pubClient / subClientany-发布/订阅客户端,可用于跨实例广播等场景(预留)
其余选项Partial<WebSocket.ServerOptions>-透传给ws原生WebSocket.Server的配置(如maxPayload、perMessageDeflate等)

组件启动时会将noServer强制置为true(配合 HTTP Server 的upgrade事件处理 WebSocket 升级),因此原生ws的port/server参数由 Midway 框架统一接管(见 packages/ws/src/framework.ts)。更多的启动选项可以继续参考 ws 官方文档。

小结

通过@midwayjs/ws,开发者可以用完全面向 Midway 的声明式风格快速构建 WebSocket 服务:@WSController定义服务、@OnWSConnection/@OnWSMessage/@WSBroadCast/@OnWSDisConnection处理连接生命周期与消息收发、@App('webSocket')获取底层 Server 实例、onWebSocketUpgrade实现握手前鉴权、心跳检查保证连接质量,再配合@midwayjs/mock的createWebSocketClient完成本地联调。若需深入源码,可以继续阅读 packages/ws/src/framework.ts(框架核心实现)、packages/ws/src/interface.ts(类型定义)与 packages/core/src/decorator/ws/webSocketEvent.ts(事件装饰器体系),并结合 packages/ws/test/index.test.ts 的测试用例验证各功能行为。

  • 后端
  • 微服务
  • 云原生

【免费下载链接】midway

🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载
上一篇:Opsweekly与Fitbit/Jawbone UP集成:睡眠跟踪的完整实现指南
下一篇:SonarQube容器镜像安全扫描:5步实现漏洞检测与修复

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 1:22:51

题解:洛谷 P5143 攀爬者

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华