1.背景
本文使用的websocket
通信框架为socket.io
,对于同一个webscoket
连接由于socket
发送的事件缺少路由分发机制,任何类型的消息都均通过socket.on
来获取,需要进一步设计去如同路由前缀匹配一般的交给各个不通模块的controller
进行处理,能够使得不同模块服务处理得到更好的解耦。
本文内容为阅读开源项目 MCSM 时所产生笔记。
2.原理
创建一个路由实例单例,它收集各个模块的中间件处理方法和服务处理方法,并通过暴露一个navigation
方法,用于为socket
注册路由实例中的中间件以及将socket
的消息以事件的形式抛出从而分发到对应的服务处理方法中,并能够存储本次连接的上下文信息session
。
3.代码示例
这是router.ts
的代码示例,这段代码实现了一个基于事件的路由和中间件系统,适用于需要处理多种事件并支持中间件逻辑的场景。通过继承 EventEmitter
,它能够灵活地处理事件,并通过中间件机制扩展功能。
typescript
// router.ts
import { EventEmitter } from "events";
import { Socket } from "socket.io";
import RouterContext from "./type";
import { Packet, responseError } from "./protocol";
class RouterApp extends EventEmitter {
public readonly middlewares: Array<Function>;
constructor() {
super();
this.middlewares = [];
}
emitRouter(event: string, ctx: RouterContext, data: any) {
try {
// service logic routing trigger point
super.emit(event, ctx, data);
} catch (error: any) {
responseError(ctx, error);
}
return this;
}
on(event: string, fn: (ctx: RouterContext, data: any) => void) {
console.info(`Register event: ${event} `);
return super.on(event, fn);
}
use(fn: (event: string, ctx: RouterContext, data: any, next: Function) => void) {
console.info(`Register middleware`);
this.middlewares.push(fn);
}
getMiddlewares() {
return this.middlewares;
}
}
// 导出单例
export const routerApp = new RouterApp();
export function navigation(socket: Socket) {
// 存储session信息
const session: any = {};
// 将所有路由中间件注册到socket连接
for (const fn of routerApp.getMiddlewares()) {
socket.use((packet, next) => {
console.log(packet)
const protocol = packet[1] as Packet;
if (!protocol)
return console.info(`会话 ${socket.id} 请求数据协议丢失`);
const ctx = new RouterContext(protocol.uuid, socket, session);
fn(packet[0], ctx, protocol.data, next);
});
}
// 将所有事件监听注册到socket中,相当于路由分发
for (const event of routerApp.eventNames()) {
socket.on(event as string, (protocol: Packet) => {
if (!protocol)
return console.info(`会话 ${socket.id} 请求数据协议丢失`);
const ctx = new RouterContext(protocol.uuid, socket, session, event.toString());
routerApp.emitRouter(event as string, ctx, protocol.data);
});
}
// 手动触发connection事件
const ctx = new RouterContext(null, socket, session);
routerApp.emitRouter("connection", ctx, null);
}
// 这里导入路由
import './auth_router'
import './chat_router'
这是一个简单的认证模块的示例
typescript
// auth_router.ts
import { responseError, responseMsg } from './protocol';
import { routerApp } from './router';
routerApp.use(async (event, ctx, _, next) => {
// 这里可以补充认证白名单路由
// if (whitelist.includes(event)) return next();
// 认证路由
if (event === "auth") return await next();
console.log('session:', ctx.session)
// 认证信息存储在session中,根据routerApp实例可知,它将持续存在与整个生命周期中
if (!ctx.session || !ctx.session.login) throw new Error("登录认证失败");
next();
});
// 认证控制器
routerApp.on("auth", (ctx, data) => {
ctx.session.login = true;
console.log('登录信息已保存')
responseMsg(ctx, "auth", true);
});
const AUTH_TIMEOUT = 6000
routerApp.on("connection", (ctx) => {
const session = ctx.session;
// 认证超时未通过则主动断开连接
setTimeout(() => {
if (!session.login) {
ctx.socket.disconnect();
}
}, AUTH_TIMEOUT);
});
这是一个简单的服务模块的示例
typescript
// chat_router.ts
import { responseError, responseMsg } from './protocol';
import { routerApp } from './router';
// 注册事件处理函数
routerApp.on('chatMessage', (ctx, data) => {
console.log('Received chat message:', data);
ctx.socket.emit('chatMessage', {
data: { message: 'Message received!' }
});
});
程序入口,当socket
连接建立后,就将router
同socket
进行绑定
typescript
// app.ts
import Koa from "koa";
import http from "http";
import { Server, Socket } from "socket.io";
import * as router from "./router";
const koaApp = new Koa();
// http相关配置暂不设置
const httpServer = http.createServer(koaApp.callback());
httpServer.listen(8080, '0.0.0.0');
const io = new Server(httpServer, {
serveClient: false,
pingInterval: 5000,
pingTimeout: 5000,
cookie: false,
path: "/socket.io",
cors: {
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE"]
},
maxHttpBufferSize: 1e8
});
io.on("connection", (socket: Socket) => {
router.navigation(socket);
socket.on("error", (err) => {
console.error("连接异常:", err);
});
socket.on("disconnect", () => {
for (const name of socket.eventNames()) socket.removeAllListeners(name);
});
});
4.调试
首先创建一个简单的客户端示例,建立连接后立即进行认证,随后每过5秒,向服务端发送一条消息
typescript
import { io, Socket } from "socket.io-client";
// 定义协议数据结构
interface IPacket {
uuid: string; // 唯一标识符
status?: 200 | 500,
data: any; // 数据内容
}
const uuid = 'man!!!'
// 连接到服务器
const socket: Socket = io("http://127.0.0.1:8080"); // 替换为你的服务器地址
// 监听连接成功事件
socket.on("connect", () => {
console.log("Connected to server!");
// 先认证
const packet: IPacket = {
uuid,
data: 'auth',
};
socket.emit("auth", packet); // 发送 chatMessage 事件
});
// 监听服务器返回的事件
socket.on("auth", (response: IPacket) => {
console.log("收到服务端消息:", response.data);
if (response.status === 200) {
console.log('已认证')
}
});
// 监听服务器返回的事件
socket.on("chatMessage", (response: IPacket) => {
console.log("收到服务端消息:", response.data);
});
// 监听连接断开事件
socket.on("disconnect", () => {
console.log("连接已断开!");
});
// 监听错误事件
socket.on("connect_error", (error: any) => {
console.error("连接异常:", error);
});
// 示例:定时发送消息
setInterval(() => {
const packet: IPacket = {
uuid,
data: { message: "你好." },
};
socket.emit("chatMessage", packet); // 发送 chatMessage 事件
}, 5000); // 每 5 秒发送一次
效果如下:
服务端输出:
客户端输出:
5.缺陷
- 中间件缺少顺序定义
socket.io
提高了大量的方法,这部分功能可能需要进一步的兼容适配- ...