- 后端
- Web框架
【免费下载链接】thinkjs
Use full ES2015+ features to develop Node.js applications, Support TypeScript.
think-websocket-ws是 ThinkJS 3.x 官方 WebSocket 扩展体系中的ws适配器,它把 Node.js 社区广泛使用的 ws 为主线,结合仓库内源码与示例工程,完整讲解从安装、配置到服务端/客户端联调的实战流程,并深入剖析其底层调用链与粘性集群(sticky cluster)原理。读完本文,你将能在 ThinkJS 3.x 项目中独立搭建一套基于ws的实时通信方案,并理解emit、broadcast与消息路由背后的实现机制。
一、think-websocket-ws 在 ThinkJS 3.x 中的定位
ThinkJS 3.x 的 WebSocket 能力由两层组成:
- 扩展层:think-websocket 负责在应用就绪(
appReady)后读取websocket适配器配置,实例化具体的适配器句柄并挂载到应用上下文; - 适配器层:具体选择通信协议实现,
think-websocket-ws即基于ws的实现,与之平行的还有基于 socket.io 的 think-websocket-socket.io。
从 think-websocket/index.js 可以看到扩展层的核心逻辑:
app.on('appReady', () => { const config = helper.parseAdapterConfig(app.think.config('websocket')); const Handle = config.handle; assert(helper.isFunction(Handle), 'websocket.handle must be a function'); instance = new Handle(app.server, config, app); instance.run(); app.websocket = instance; });也就是说,只要在websocket适配器配置中把handle指向think-websocket-ws导出的类,扩展层就会自动完成实例化与启动(run())。这套"扩展 + 适配器"的架构意味着:业务 Action 中通过this.emit、this.broadcast等统一 API 调用,底层协议可随时在ws与socket.io之间切换,业务代码几乎无需改动。
两个适配器共享同一套上下文 API(wsData、websocket、isWebsocket、emit、broadcast),区别主要在协议与消息格式:ws适配器使用 JSON 字符串消息、事件名放在消息体event字段中;socket.io 适配器则使用 socket.io 自带的事件机制。
二、安装
在 ThinkJS 3.x 项目根目录执行:
npm install think-websocket-ws --save依赖关系见 packages/think-websocket-ws/package.json:核心依赖ws(约 3.2.0)、think-helper(配置解析等工具函数)与think-mock-http(把 WebSocket 消息包装成 ThinkJS 可识别的模拟 HTTP 请求)。安装时建议同时确认扩展包think-websocket已安装(它是适配器被激活的入口)。
三、配置适配器:src/config/adapter.js
编辑配置文件src/config/adapter.js,添加websocket适配器配置:
const ws = require('think-websocket-ws'); exports.websocket = { type: 'ws', common: { // common config }, ws: { handle: ws, path: '/ws', messages: [{ close: '/ws/close', open: '/ws/open', addUser: '/ws/addUser' }] } }3.1 配置结构与解析逻辑
这份配置遵循 ThinkJS 适配器通用格式,最终由 think-helper 的 parseAdapterConfig 解析:
type: 'ws'指定使用哪个适配器段;common为所有适配器共享的公共配置,会被合并进最终配置;ws段是当前type对应的具体配置,其中handle必须是一个构造函数(对应适配器实现类),path、messages等会随其余配置一并传给句柄实例。
解析后,扩展层会执行new Handle(app.server, config, app),把 Node.js HTTP 服务(app.server)、合并后的配置对象与应用实例注入适配器。对应构造函数见 think-websocket-ws/index.js。
3.2 透传给 ws.Server 的配置项
适配器构造函数使用getDestructResult从配置中挑选以下字段原样传给ws.Server(详见 think-websocket-ws/index.js):
| 配置项 | 说明(对应 ws.Server 选项) |
|---|---|
host/port | 监听地址与端口(默认不指定,随 HTTP 服务) |
path | WebSocket 服务路径,例如/ws,只有该路径下的升级请求才会被接受 |
backlog | 底层server.listen的连接排队长度 |
verifyClient | 握手校验函数,可在此做鉴权 |
handleProtocols | 子协议协商处理函数 |
noServer | 是否不自动附着 HTTP 服务,配合手动升级 |
clientTracking | 是否跟踪客户端连接(影响wss.clients) |
perMessageDeflate | 消息压缩配置(默认为 true) |
maxPayload | 单条消息最大负载(默认 100 MiB,即 104857600 字节) |
除上述选项外,配置对象中的callback会被作为ws.Server的第二个参数传入。path与wssConfig.server = server的组合是关键:适配器复用 ThinkJS 的 HTTP 服务器,仅在/ws路径上处理 WebSocket 升级,HTTP 业务不受影响。
更多ws.Server原生选项可查阅ws官方文档(README 中亦给出了指引),上述字段均为适配器显式支持透传的子集。
3.3 messages:消息路由表
messages是 WebSocket 事件与 ThinkJS Action 路径的映射表,支持单个对象或对象数组(数组用于注册多组路由,见 run() 的实现)。表中每一对事件名: Action路径的含义:
open:连接建立时触发的 Action,如/ws/open;close:连接关闭时触发的 Action,如/ws/close;- 其他键名:客户端消息中
event字段等于该键名时触发的 Action,如addUser: '/ws/addUser'。
从 registerSocket 的实现 可以看到路由表的具体消费方式:
wss.on('connection', (socket, request) => { if (messages.open) { this.mockRequst(messages.open, undefined, socket, request, true, wss); } if (messages.close) { socket.on('close', () => { this.mockRequst(messages.close, undefined, socket, undefined, false, wss); }); } for (const key in messages) { if (key === 'open' || key === 'close') continue; socket.on('message', data => { data = JSON.parse(data); if (key === data.event) { this.mockRequst(messages[key], data, socket, undefined, false, wss); } }); } });这意味着客户端发送的每一条消息都必须是符合{"event": "...", "data": ...}结构的 JSON 字符串,event字段作为路由键匹配messages表中的事件名,data字段则作为请求数据传入对应 Action。
3.4 消息如何变成一次 ThinkJS 请求
每次命中路由,适配器都会调用mockRequst(think-websocket-ws/index.js)构造一次"模拟 HTTP 请求":
const args = {url, websocketData: data, websocket: socket, wss, method: 'WEBSOCKET'}; if (open) { args.req = request; // 握手阶段的真实请求对象 } return mockHttp(args, this.app);think-mock-http 会根据url、method等参数创建http.IncomingMessage与http.ServerResponse,再调用app.callback()进入 ThinkJS 完整的中间件链路。关键点在于method: 'WEBSOCKET':ThinkJS 路由中间件会把该请求当作WEBSOCKET方法处理(参见 think-router/router.js 中ctx.method.toUpperCase()的用法),随后按 URL 解析出模块/控制器/动作,最终由 think-controller 调用xxxAction方法——这就是"用 Action 处理 WebSocket 消息"这一体验的底层来源。
四、开启粘性集群:src/config/config.js
WebSocket 是长连接,一旦连接建立,后续所有消息必须始终到达同一 worker 进程。在多进程(cluster)部署下,若不做会话粘滞,负载均衡可能把同一条连接的后续消息分发到不同 worker,导致连接状态丢失。因此 README 要求在src/config/config.js中开启粘性集群:
module.exports = { // ... stickyCluster: true // ... }该配置项默认值为false(见 thinkjs 默认配置),ThinkJS 启动时会把它传入 cluster 的 Master 与 Worker(见 application.js)。
其实现原理可以在 think-cluster 中看到完整闭环:
- Master 侧(lib/master.js):当
sticky为真时,Master 不再直接 fork 各 worker 分别监听端口,而是创建net.createServer({pauseOnConnect: true}),对每个新连接取客户端远程地址socket.remoteAddress,用stringHash(remoteAddress) % workers计算出固定 worker 编号,并通过THINK_STICKY_CLUSTER消息把 socket 转发给对应 worker; - Worker 侧(lib/worker.js):粘性模式下 worker 只监听
127.0.0.1的随机端口,收到 Master 转发来的 socket 后,在自身 HTTP 服务器上emit('connection', socket)并socket.resume()恢复连接,从而接管这条连接。
由于同一客户端 IP 的哈希结果恒定,其所有连接都会被派发到同一 worker,WebSocket 状态得以保持。注意:getRemoteAddress基于 IP 哈希,若多客户端共享出口 IP(如 NAT 场景),粘性分配粒度会以 IP 为单位,这是该方案的固有特性。
五、服务端 Action 与客户端联调
5.1 服务端控制器
配置中open、close、addUser分别映射到控制器ws的openAction、closeAction、addUserAction(因为/ws/open解析出控制器ws、动作open):
module.exports = class extends think.Controller { constructor(...arg) { super(...arg); } openAction() { console.log('ws open'); return this.success(); } closeAction() { console.log('ws close'); return this.success(); } addUserAction() { console.log('addUserAction ...'); console.log(this.wsData); // this.req.websocketData,例如 'thinkjs' console.log(this.websocket); // this.req.websocket,当前 socket 实例 console.log(this.isWebsocket); // this.isMethod('WEBSOCKET'),恒为 true return this.success(); } }Action 内可用的关键属性与方法(由 think-websocket/index.js 注入到 context 与 controller):
| API | 说明 |
|---|---|
this.wsData | 客户端消息中的data字段(旧 API 为this.data,已标记废弃) |
this.websocket | 当前连接的wssocket 实例,可访问readyState、send()等 |
this.isWebsocket | 判断当前请求是否为 WebSocket 请求(this.isMethod('WEBSOCKET')) |
this.emit(event, data) | 向当前连接定向推送事件 |
this.broadcast(event, data) | 向所有连接广播事件 |
5.2 浏览器客户端
客户端直接使用浏览器原生WebSocket,连接地址与配置中的path: '/ws'及 ThinkJS 默认端口8360(见 默认配置)对应:
var ws = new WebSocket('ws://127.0.0.1:8360/ws'); ws.onopen = function() { console.log('open...'); } ws.onerror = function(evt) { console.log(evt); } ws.onmessage = function(data) { console.log(data); } $('.send').on('click', function(evt) { var username = $.trim($('.usernameInput').val()); var room = $.trim($('.roomInput').val()); ws.send(JSON.stringify({ event: 'addUser', data: { username: username, room: room } })); });发送消息时,event字段必须命中配置中的addUser路由键,服务端才会调用addUserAction,data对象则在 Action 中通过this.wsData读取。整个双向消息格式统一为{event, data}的 JSON 协议。
六、emit:定向推送
在任意 Action 中调用this.emit(event, data),可向当前连接单独推送事件:
// server module.exports = class extends think.Controller { constructor(...arg) { super(...arg); } openAction() { this.emit('opend', 'This client opened successfully!'); return this.success(); } } // client ws.onmessage = function(data) { data = JSON.parse(data.data); console.log(data.event); // opend console.log(data.data); // This client opened successfully! }emit的底层实现在 think-websocket-ws/index.js:推送前会检查 socket 状态是否为OPEN(WS_STATUS.OPEN === 1),只有连接处于打开状态才执行socket.send(JSON.stringify({event, data})),从而避免向已关闭连接写入数据。扩展层封装见 think-websocket/index.js,它会先置res.statusCode = 200再调用适配器的emit。
七、broadcast:全局广播
需要向所有已连接客户端广播时,使用this.broadcast(event, data):
// server module.exports = class extends think.Controller { constructor(...arg) { super(...arg); } openAction() { this.broadcast('joined', 'There is a new client joined successfully!'); return this.success(); } } // client ws.onmessage = function(data) { data = JSON.parse(data.data); console.log(data.event); // joined console.log(data.data); // There is a new client joined successfully! }broadcast的实现遍历wss.clients中所有连接,仅向状态为OPEN的连接发送同样的{event, data}JSON(见 think-websocket-ws/index.js)。由于clientTracking默认开启,wss.clients会维护当前所有连接的集合,广播即对其全量遍历。这一能力非常适合做在线状态通知、房间人数刷新等场景,例如 ThinkJS 示例工程 think-demo 的 websocket 控制器 中就在openAction里广播新用户加入事件。
八、完整链路与源码索引
一次 WebSocket 消息的完整处理链路:
- 客户端
ws.send(JSON.stringify({event: 'addUser', data: {...}})); - 适配器
registerSocket注册的message监听器收到消息,JSON.parse后匹配event路由键(think-websocket-ws/index.js); mockRequst构造method: 'WEBSOCKET'的模拟请求(think-websocket-ws/index.js),交由 think-mock-http 生成请求/响应对象并进入应用回调;- think-router 解析 URL 得到控制器与动作,think-controller 调用
addUserAction; - Action 内通过
this.wsData读数据,用this.emit/this.broadcast回推消息。
便于继续深入阅读的仓库路径索引:
- 适配器实现:packages/think-websocket-ws/index.js
- 扩展层(注入 wsData/emit/broadcast):packages/think-websocket/index.js
- 扩展安装方式:packages/think-websocket/README.md
- 兄弟适配器(socket.io 版):packages/think-websocket-socket.io/index.js
- 模拟请求库:packages/think-mock-http/index.js
- 粘性集群 Master/Worker:packages/think-cluster/lib/master.js、packages/think-cluster/lib/worker.js
- 默认配置(端口 8360、stickyCluster 默认 false):packages/thinkjs/lib/config/config.js
- 适配器配置解析:packages/think-helper/index.js
- 示例工程(扩展注册与 socket.io 适配器用法):packages/think-demo/src/config/extend.js、packages/think-demo/src/config/adapter/websocket.js
九、注意事项与适用前提
- 消息协议:
think-websocket-ws要求所有业务消息均为{"event": "...", "data": ...}形式的 JSON 字符串,event必须预先在messages路由表中声明;data可以是任意可被 JSON 序列化的值。 - 连接路径:
path: '/ws'同时作用于客户端连接 URL 与ws.Server的路径过滤,两端必须一致;默认服务端口为8360,可通过src/config/config.js的port调整。 - 多进程部署:一旦使用
workers > 1且需要 WebSocket,务必开启stickyCluster: true,否则长连接无法稳定路由到同一 worker。 - 依赖版本:本适配器依赖
ws(约 3.2.0)与think-helper、think-mock-http,如需更细粒度的ws原生能力,可直接透传对应配置项,或基于 index.js 自定义适配器。
- 后端
- Web框架
【免费下载链接】thinkjs
Use full ES2015+ features to develop Node.js applications, Support TypeScript.
相关推荐
htmx WebSocket 扩展实战指南:用 ws-connect 与 ws-send 在 HTML 中实现双向实时通信
htmx WebSocket 扩展实战指南:用 ws connect 与 ws send 在 HTML 中实现双向实时通信 导读 WebSocket 扩展( w
前端Web框架think-websocket-socket.io 实战指南:在 ThinkJS 3 中集成 Socket.IO 构建 WebSocket 应用
think websocket socket.io 实战指南:在 ThinkJS 3 中集成 Socket.IO 构建 WebSocket 应用 本文以 pac
后端Web框架Midway 3 WebSocket 实战指南:基于 @midwayjs/ws 构建实时通信服务
Midway 3 WebSocket 实战指南:基于 @midwayjs/ws 构建实时通信服务 导读 本文面向 Midway 3.x 用户,系统讲解基于 @m
后端微服务云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考