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提高了大量的方法,这部分功能可能需要进一步的兼容适配
  • ...
相关推荐
PyAIGCMaster18 分钟前
国内 npm 镜像源推荐
前端·npm·node.js
Jice1918 分钟前
Node.js回调地狱
node.js
NoneCoder27 分钟前
Node.js系列(5)--数据库操作指南
数据库·node.js
m0_748251352 小时前
Windows 上彻底卸载 Node.js
windows·node.js
red润2 小时前
理解 Node.js 中的 process`对象与常用操作
前端·javascript·node.js
群联云防护小杜13 小时前
分布式节点池:群联云防护抗DDoS的核心武器
前端·网络·分布式·udp·npm·node.js·ddos
eli96016 小时前
node-ddk, electron组件, 自定义本地文件协议,打开本地文件
前端·javascript·electron·node.js
混血哲谈20 小时前
如果我的项目是用ts写的,那么如何使用webpack的动态导入功能呢?
前端·webpack·node.js
前端花园20 小时前
10分钟内手把手教你探索AI+blender自动建模
前端·node.js
渗透测试老鸟-九青20 小时前
挖洞日记 | Webpack实操
前端·安全·web安全·webpack·node.js·区块链·智能合约