Nodejs:简单的基于事件的socket路由系统

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连接建立后,就将routersocket进行绑定

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提高了大量的方法,这部分功能可能需要进一步的兼容适配
  • ...
相关推荐
陳陈陳17 小时前
🚀 前端流式输出革命:SSE + BFF 架构从零到一实战指南
vue.js·架构·node.js
Revolution6117 小时前
第一次运行 Node.js:终端里的 JavaScript 怎样执行
后端·面试·node.js
半句唐诗18 小时前
我是如何通过 Access Token 成功发布第一个 npm 包的
前端·npm·node.js
贩卖黄昏的熊19 小时前
NestJS简明教程——异常处理和日志
javascript·node.js·nest.js
Revolution611 天前
Node.js 是什么:前端项目里哪些事情由它完成
前端·面试·node.js
Revolution611 天前
Nest.js 是什么:怎样用它写出第一个后端接口
后端·node.js·nestjs
柳林林1 天前
解决 nvm 切换 Node.js 版本时报“没有文件扩展 ‘.vbs’ 的脚本引擎”错误
node.js
To_OC1 天前
从 0 到 1:Milvus + 大模型打造私人记忆知识库
人工智能·node.js·llm
孟陬2 天前
介绍 Node.js v22 新增的 `toArray` 方法
node.js·deno·bun
卡兹墨2 天前
针对高并发、数据量大的场景,通常会考虑采用分库分表进行优化。在这篇文章,我们重点盘点一下Node.js主流ORM框架的动态分表方案 ...
node.js