背景
最近沉迷WebRTC技术,想用WebRTC试做一个快启动的文件传输PWA应用。涉及到使用websocket服务器传输WebRTC的信令,全栈的话自然是选择Nodejs做服务器了。
选型
因为项目体量肯定不是很大,所以所有选型都以高性能轻量为主要目标。前端选择的是solidjs+tailwindcss,后端的话思虑了很久,最开始想用的是fastify + ws,但是后来想了想,这个项目对HTTP的依赖程度不高,所有的资源传输都可以用websocket和WebRTC实现,所以就完全不用HTTP服务器了,又考虑到socket.io的封装程度更高,提供了Namespace、room等功能,所以根据官网介绍选择了uWebSockets.js + socket.io做服务器。
内存占用 | Socket.IO
但是uWebSockets.js + socket.io的封装程度不好,现成框架对这俩组合的支持程度较低,所以我自己仿照nestjs的websocket服务,封装了一部分装饰器和模块的逻辑实现。
思路
就像nestjs一样,main.ts中编写初始化框架、全局配置等的逻辑;而其他业务方面的代码,则使用模块的方式组织,模块有各自的namespace,维护namespace的事件监听、生命周期、自己内部的逻辑等,各个模块将在框架启动时实例化;
依赖及TS配置
- Nodejs v16以上,因为uWebSockets.js需要v16以上的环境
- pnpm 可选
- uWebSockets.js + socket.io + typescript + reflect-metadata
reflect-metadata是干什么用的?
reflect-metadata库是一个对ES6 Reflect在元数据扩展方面能力的缺失而实现的一个polyfill,简单来说,就是可以通过Reflect对类、类成员添加一些元数据而不破坏这个类及类成员的构成。
TS配置
务必开启以下两个配置,因为项目中会用到大量装饰器。
"experimentalDecorators": true,
"emitDecoratorMetadata": true
虽然TS里面Decorator的实现已经与当前TC39 Decorator Stage3 的实现大相径庭了,但是个人项目使用一时半会儿还不要紧,不过实际项目最好做好准备,尽量不使用TS实现的Stage 2装饰器,等待原生装饰器语法成为正式标准。
代码实现
Socket.IO配合uWebSockets.js的官方示例代码
首先引入uWebSockets.js与socket.io,并将他们实例化
然后将socket.io与uWebSocket.js关联,使socket.io底层由uWebSockets.js实现
注册socket.io事件监听,执行逻辑
启动uWebSockets.js
const { App } = require("uWebSockets.js");
const { Server } = require("socket.io");
const app = new App();
const io = new Server();
io.attachApp(app);
io.on("connection", (socket) => {
// ...
});
app.listen(3000, (token) => {
if (!token) {
console.warn("port already in use");
}
});
改造
在入口文件处,引入reflect-metadata,以启用Reflect polyfill
初始化uWebSockets.js,开始监听端口
关键函数constructModule,在启动时将所有主模块实例化。
import "reflect-metadata";
import { App } from "uWebSockets.js";
import { constructModule } from "./factory/constructModules";
import { AppModule } from "./src/app.module";
async function boostrap () {
/** uWebsocket entry */
const app = App();
constructModule(app, [
AppModule
], {
transports: ['websocket']
});
const port = 3000;
app.listen(port, token => {
if(!token) console.warn(`port ${port} already in use`);
});
}
boostrap();
constructModule函数
遍历传入的主模块数组,执行以下操作:
1.取出记录了需要注入的子模块的metadata;取出记录了server options的metadata
2.创建一个socket.io的Server实例,并绑定uWebSockets.js。
3.实例化所有子模块,getMetadata取得子模块的namespace配置,开始监听该namespace,如果有onConnection或onDisconnecting成员,则添加connection或disconnecting监听。
4.之后注入子模块中的server字段(注入Server实例)与namespace字段(注入Namespace实例)。
5.最后监听namespace的connect事件,在一个socket进入时为其添加模块中注明的事件监听器。
namespace配置、server字段、socket事件监听函数应该怎么获得呢?使用装饰器,为类、类成员添加修饰,在实例化时读取到它们,然后处理。
import { Namespace, Server, ServerOptions, Socket } from "socket.io";
import { ChildModuleClass, DecoratorMetadataName, ListenersMetadata, NamespaceOptions } from "./SocketDecorators";
import { TemplatedApp } from "uWebSockets.js";
function createGlobalServer (app: TemplatedApp, serverOpt?: Partial<ServerOptions>) {
const io = new Server(serverOpt);
io.attachApp(app);
return io;
}
function bindDecoratorListeners (self: ChildModuleClass, socket: Socket) {
/** mount listener */
const listeners: ListenersMetadata = Reflect.getMetadata(DecoratorMetadataName.EventListener, self);
listeners?.forEach(listener => {
socket.on(listener.name, (...args: any) => listener.listener.call(self, socket, ...args))
});
}
function analyseDecoratorValues (self: ChildModuleClass) {
const map = new Map<DecoratorMetadataName, string>();
const ownKeys = Object.keys(self);
for(let key of ownKeys) {
/** get websocket client socket propertyKey */
if(Reflect.getMetadata(DecoratorMetadataName.SocketProperty, self, key)) {
map.set(DecoratorMetadataName.SocketProperty, key);
continue;
}
/** get websocket namespace propertyKey */
if(Reflect.getMetadata(DecoratorMetadataName.NamespaceProperty, self, key)) {
map.set(DecoratorMetadataName.NamespaceProperty, key);
continue;
}
/** get websocket server propertyKey */
if(Reflect.getMetadata(DecoratorMetadataName.ServerProperty, self, key)) {
map.set(DecoratorMetadataName.ServerProperty, key);
continue;
}
}
return map;
}
/** bind namespace and server */
function bindDecoratorValuesBeforeConnect (
map: Map<DecoratorMetadataName, string>,
self: ChildModuleClass,
nsp: Namespace,
ioInstance: Server
) {
const nspPropertyKey = map.get(DecoratorMetadataName.NamespaceProperty);
const serverPropertyKey = map.get(DecoratorMetadataName.ServerProperty);
if(nspPropertyKey) {
Reflect.set(self, nspPropertyKey, nsp);
}
if(serverPropertyKey) {
Reflect.set(self, serverPropertyKey, ioInstance);
}
}
/** bind socket */
function bindDecoratorValuesAfterConnect (
map: Map<DecoratorMetadataName, string>,
self: ChildModuleClass,
socket: Socket
) {
const socketPropertyKey = map.get(DecoratorMetadataName.SocketProperty);
if(socketPropertyKey) {
Reflect.set(self, socketPropertyKey, socket);
}
}
/**
* construct child module
* inject provider
*/
function constructChildModule (
modules: (new () => ChildModuleClass)[],
io: Server
) {
modules.forEach(ModuleItem => {
const instance = new ModuleItem();
const opt: NamespaceOptions = Reflect.getMetadata(DecoratorMetadataName.NamespaceModule, ModuleItem);
const { namespace } = opt ?? {};
const nsp = io?.of(namespace ?? '/');
const decoratorMap = analyseDecoratorValues(instance);
bindDecoratorValuesBeforeConnect(decoratorMap, instance, nsp, io);
nsp.on('connection', (socket) => {
bindDecoratorValuesAfterConnect(decoratorMap, instance, socket);
bindDecoratorListeners(instance, socket);
instance.onConnection?.(socket);
socket.on('disconnect', (...args) => {
if(instance?.onDisconnect) instance.onDisconnect.call(instance, ...args);
});
socket.on('disconnecting', (...args) => {
if(instance?.onDisconnecting) instance.onDisconnecting.call(instance, ...args);
});
});
});
}
/**
* construct global instance of socket.io
* construct module
*/
export function constructModule (
app: TemplatedApp,
modules: (new () => any)[],
globalServerOpt?: Partial<ServerOptions>
) {
modules.forEach(ModuleItem => {
const childModules = Reflect.getMetadata(DecoratorMetadataName.ChildModules, ModuleItem);
const serverOpts = Reflect.getMetadata(DecoratorMetadataName.ServerOptions, ModuleItem);
const io = createGlobalServer(app, {
...globalServerOpt,
...serverOpts
});
constructChildModule(childModules, io);
});
}
装饰器
共有ServerModule、NamespaceModule、Subscribe、WebsocketServer、WebsocketNamespace四个装饰器,为类、类成员注入metadata,metadata中保存相应信息,在实例化时取出并应用。
Subscribe装饰器会将所有监听事件放入一个数组中,而非直接为该成员添加eventName metadata,是因为class创建的类中,类方法都未挂载在this上,而是挂载在原型链上,无法通过遍历类成员获得。
import { Socket, Namespace, Server, ServerOptions } from "socket.io";
/** 注入Metadata的name */
export enum DecoratorMetadataName {
ChildModules = '[[childmodules]]',
ServerOptions = '[[serveroptions]]',
NamespaceModule = '[[wsopt]]',
EventListener = '[[listener]]',
ServerProperty = '[[serverprop]]',
NamespaceProperty = '[[namespaceprop]]',
SocketProperty = '[[socketprop]]'
}
export interface ChildModuleClass {
socket?: Socket,
namespace?: Namespace,
server?: Server,
[key: string | number | symbol]: any
}
/** 定义server模块,该模块为主模块 */
export function ServerModule(childModules?: ChildModuleClass[], serverOpt?: ServerOptions): ClassDecorator {
return function (target: Function) {
Reflect.defineMetadata(DecoratorMetadataName.ChildModules, childModules, target);
Reflect.defineMetadata(DecoratorMetadataName.ServerOptions, serverOpt, target);
}
}
/** 定义namespace模块,该模块为子模块 */
export interface NamespaceOptions {
namespace?: string;
}
export function NamespaceModule(namespaceOpt?: NamespaceOptions): ClassDecorator {
return function (target: Function) {
Reflect.defineMetadata(DecoratorMetadataName.NamespaceModule, namespaceOpt,target);
}
}
/** 订阅消息装饰器 */
export type ListenersMetadata = Array<{
name: string,
listener: (...args: any[]) => void
}>
export function Subscribe(eventName?: string): MethodDecorator {
return function (target: any, _, descriptor) {
const listeners = Reflect.getMetadata(DecoratorMetadataName.EventListener, target) ?? [];
Reflect.defineMetadata(
DecoratorMetadataName.EventListener,
listeners.concat([{
name: eventName,
listener: descriptor.value,
}]),
target
);
}
}
export function WebsocketServer(target: object, propertyKey: string | symbol) {
Reflect.defineMetadata(DecoratorMetadataName.ServerProperty, true, target, propertyKey)
}
export function WebsocketNamespace(target: object, propertyKey: string | symbol) {
Reflect.defineMetadata(DecoratorMetadataName.NamespaceProperty, true, target, propertyKey)
}
/**
* 已废弃装饰器,用于注入Socket实例
* 因Socket实例在connect事件后才会实例化,所以在模块constructor中无法获取到该实例
*/
export function SocketInstance(target: object, propertyKey: string | symbol) {
Reflect.defineMetadata(DecoratorMetadataName.SocketProperty, true, target, propertyKey)
}
装饰器使用方法
主模块
import { ServerModule } from "../factory/SocketDecorators";
import { UserModule } from "./user/user.module";
import { ExchangeModule } from "./exchange/exchange.module";
@ServerModule(
[UserModule, ExchangeModule],
{ pingInterval: 3000 }
)
export class AppModule {}
子模块
import { WebsocketServer, Subscribe, NamespaceModule, SocketInstance } from "../../factory/SocketDecorators";
import { Server, Socket } from "socket.io";
@NamespaceModule({ namespace: '/user' })
export class UserModule {
@WebsocketServer server?: Server;
@SocketInstance socket?: Socket;
constructor() {}
onConnection() {
console.log('user connection', this.socket?.id);
}
@Subscribe('token')
public handleToken(socket: Socket, data: any) {
console.log('token', data);
}
}
全部代码
目前是未完成版本,之后可能会对其进行迭代优化文章来源:https://www.toymoban.com/news/detail-475812.html
TransferS/backend at master · YThinker/TransferS · GitHub文章来源地址https://www.toymoban.com/news/detail-475812.html
到了这里,关于使用TS装饰器从0封装一个socket.io服务器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!