diff --git a/assets/Script/server/connection.ts b/assets/Script/server/connection.ts index bbc1642..16d4b72 100644 --- a/assets/Script/server/connection.ts +++ b/assets/Script/server/connection.ts @@ -1,5 +1,62 @@ +import {WebSocket} from "ws" +import * as http from "http" +import { HttpUtil } from "./models/HttpUtil" +import { buffer } from "stream/consumers" +export interface WsConnectionOptions { + connId: number + ws: WebSocket + httpReq: http.IncomingMessage + onClose: (conn: WsConnection, code: number, reason: string) => void + onRecvData: (data: Buffer) => void; +} export class WsConnection { - + options: WsConnectionOptions + ip!: string + + constructor (options: WsConnectionOptions) { + this.options = options; + } + + getConnId() : number { + return this.options.connId; + } + + getIp() : string { + return this.ip; + } + + getWs() : WebSocket { + return this.options.ws; + } + + init(options: WsConnectionOptions) { + this.options = options; + + this.ip = HttpUtil.getClientIp(options.httpReq); + + // todo: log + this.options.ws.onclose = e => { this.options.onClose(this, e.code, e.reason);} + this.options.ws.onerror = e => { console.log("[client_err] ", e.error)} + this.options.ws.onmessage = e => { + if (Buffer.isBuffer(e.data)) { + this.options.onRecvData(e.data); + } else { + console.log("[dataType_err]", e.data); + } + } + } + + close(reason?: string) { + if (this.options.ws && this.options.ws.readyState == WebSocket.OPEN) { + this.options.ws.close(1000, reason || 'Server Closed'); + } + this.options.ws.onopen = this.options.ws.onclose = this.options.ws.onmessage = this.options.ws.onerror = undefined as any; + this.ip = undefined; + } + + getIsClosed(): boolean { + return this.options.ws.readyState !== WebSocket.OPEN; + } } \ No newline at end of file diff --git a/assets/Script/server/models/Counter.ts b/assets/Script/server/models/Counter.ts new file mode 100644 index 0000000..bbb78d2 --- /dev/null +++ b/assets/Script/server/models/Counter.ts @@ -0,0 +1,25 @@ +export class Counter { + + private _min: number; + private _max: number; + private _last: number; + + constructor(min: number = 1, max: number = Number.MAX_SAFE_INTEGER) { + this._min = min; + this._max = max; + this._last = max; + } + + /** 复位:从新从0开始计数 */ + reset() { + this._last = this._max; + } + + getNext() { + return this._last >= this._max ? (this._last = this._min) : ++this._last; + } + + get last() { + return this._last; + } +} \ No newline at end of file diff --git a/assets/Script/server/models/HttpUtil.ts b/assets/Script/server/models/HttpUtil.ts new file mode 100644 index 0000000..b2aa289 --- /dev/null +++ b/assets/Script/server/models/HttpUtil.ts @@ -0,0 +1,16 @@ +import * as http from "http"; + +export class HttpUtil { + static getClientIp(req: http.IncomingMessage) { + var ipAddress; + var forwardedIpsStr = req.headers['x-forwarded-for'] as string | undefined; + if (forwardedIpsStr) { + var forwardedIps = forwardedIpsStr.split(','); + ipAddress = forwardedIps[0]; + } + if (!ipAddress) { + ipAddress = req.socket.remoteAddress; + } + return ipAddress ? ipAddress.replace(/^::ffff:/, '') : ''; + }; +} \ No newline at end of file diff --git a/assets/Script/server/models/connMgr.ts b/assets/Script/server/models/connMgr.ts new file mode 100644 index 0000000..5b780bc --- /dev/null +++ b/assets/Script/server/models/connMgr.ts @@ -0,0 +1,32 @@ +import { WsConnection } from "../connection"; + +export class ConnManager { + private static _idConnMap : {[connId: number]: WsConnection | undefined}; + private static activeConnNum : number = 0; + + public static getActiveConnNum() { + return this.activeConnNum; + } + + public static getConn(connId : number) { + return this._idConnMap[connId]; + } + + public static addConn(conn : WsConnection) { + this._idConnMap[conn.options.connId] = conn + this.activeConnNum++; + } + + public static remConn(conn : WsConnection) { + this._idConnMap[conn.options.connId] = undefined; + this.activeConnNum--; + } + + public static sendMsg(connId: number, msg : Buffer) { + let conn = this._idConnMap[connId]; + if (conn === undefined) { + console.log("conn is not exists"); + } + conn.options.ws.send(msg) + } +} \ No newline at end of file diff --git a/assets/Script/server/webSock.ts b/assets/Script/server/webSock.ts index d84b571..a8a2aeb 100644 --- a/assets/Script/server/webSock.ts +++ b/assets/Script/server/webSock.ts @@ -1,6 +1,9 @@ import * as WebSocket from 'ws'; import { Server as WebSocketServer } from 'ws'; import {WsConnection} from "./connection" +import * as http from "http" +import { Counter } from './models/Counter'; +import { ConnManager } from './models/connMgr'; enum WsServerStatus { Initializing, @@ -9,7 +12,91 @@ enum WsServerStatus { Closed, } -export class WsServer { - private _id2conn : {[connId : string] : WsConnection | undefined} +export interface WsServerOptions { + host: string + port: number + timeout: number + onClientConnect: (ws : WebSocket, httpReq: http.IncomingMessage) => void +} + +const defaultWsServerOptions: WsServerOptions = { + host: "0.0.0.0", + port: 8080, + timeout: 3000, + onClientConnect: undefined, +} + +export class WsServer { + private status: WsServerStatus = WsServerStatus.Closed; + private options: WsServerOptions; + + private _wsServer: WebSocketServer; + private _connIdCounter = new Counter(1); + + constructor (options?: WsServerOptions) { + this.options = Object.assign({}, defaultWsServerOptions, options); + if (this.options.onClientConnect === undefined) { + this.options.onClientConnect = this._onClientConnect; + } + } + + start() { + if (this._wsServer) { + throw new Error('Server already started'); + } + this.status = WsServerStatus.Initializing + this._wsServer = new WebSocketServer({port: this.options.port, host: this.options.host}, () => { + console.log(`server started, listening on ${this.options.host}:${this.options.port}...`) + this.status = WsServerStatus.Inited + }) + this._wsServer.on("connection", this.options.onClientConnect) + this._wsServer.on("error", e => { + console.log("[server_error]", e) + }) + } + + private _onClientConnect = (ws: WebSocket, httpReq: http.IncomingMessage) => { + // 服务不可用 不接受新的连接 + if (this.status !== WsServerStatus.Inited) { + ws.close(); + return; + } + + let connId = this.getNextConnId() + if (isNaN(connId)) { + ws.close(-1, "无法建立更多的连接"); + return; + } + + let conn = new WsConnection({ + connId: connId, + ws: ws, + httpReq: httpReq, + onClose: this._onClientClose, + onRecvData: v => { this.onData(conn, v) } + }); + ConnManager.addConn(conn); + + console.log('[Connected]', `ActiveConn=${ConnManager.getActiveConnNum()}`) + }; + + getNextConnId(): number { + for (let i = 0; i < 1000; ++i) { + let connId = this._connIdCounter.getNext(); + if (!ConnManager.getConn(connId)) { + return connId; + } + } + return NaN; + } + + private _onClientClose = (conn: WsConnection, code: number, reason: string) => { + ConnManager.remConn(conn) + console.log('[Disconnected]', `Code=${code} ${reason ? `Reason=${reason} ` : ''} ActiveConn=${ConnManager.getActiveConnNum()}`) + } + + private onData(conn : WsConnection, data: Buffer) { + console.log("conn on data", data) + } } diff --git a/package-lock.json b/package-lock.json index 4f6aad5..8f9b533 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,30 @@ "name": "cocos_creator_framework", "version": "3.5.1", "dependencies": { + "@types/node": "^18.7.13", + "@types/ws": "^8.5.3", + "http": "^0.0.1-security", "ws": "^8.8.1" } }, + "node_modules/@types/node": { + "version": "18.7.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", + "integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==" + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/http": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", + "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" + }, "node_modules/ws": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", @@ -33,6 +54,24 @@ } }, "dependencies": { + "@types/node": { + "version": "18.7.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", + "integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==" + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "requires": { + "@types/node": "*" + } + }, + "http": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", + "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" + }, "ws": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", diff --git a/package.json b/package.json index fd77304..c47351b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "version": "3.5.1" }, "dependencies": { + "@types/node": "^18.7.13", + "@types/ws": "^8.5.3", + "http": "^0.0.1-security", "ws": "^8.8.1" } }