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提高了大量的方法,这部分功能可能需要进一步的兼容适配
  • ...
相关推荐
前端老六喔6 小时前
🎉 开源项目推荐 | 让你的 TypeScript/React 项目瘦身更简单!
node.js·前端工程化
醉书生ꦿ℘゜এ7 小时前
npm error Cannot read properties of null (reading ‘matches‘)
前端·npm·node.js
超级土豆粉8 小时前
从0到1写一个适用于Node.js的User Agent生成库
linux·ubuntu·node.js
空中湖10 小时前
‘pnpm‘ 不是内部或外部命令,也不是可运行的程序
npm·node.js
SailingCoder12 小时前
grafana-mcp-analyzer:基于 MCP 的轻量 AI 分析监控图表的运维神器!
运维·人工智能·typescript·node.js·grafana
又又呢15 小时前
前端面试题总结——webpack篇
前端·webpack·node.js
avoidaily1 天前
使用Node.js分片上传大文件到阿里云OSS
阿里云·node.js·云计算
xd000021 天前
8.axios Http网络请求库(1)
node.js
孟孟~1 天前
npm run dev 报错:Error: error:0308010C:digital envelope routines::unsupported
前端·npm·node.js
孟孟~1 天前
npm install 报错:npm error: ...node_modules\deasync npm error command failed
前端·npm·node.js