init 一个游戏服务器的后端demo
This commit is contained in:
commit
ad789c4517
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
dist/*
|
||||||
|
.*/
|
||||||
|
.env
|
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "ws-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "server.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && node dist/server.js",
|
||||||
|
"start": "ts-node ./src/main",
|
||||||
|
"test": "ts-node ./src/main",
|
||||||
|
"err": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"ws": "^8.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node20": "^20.1.4",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/node": "^20.14.2",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.5.2"
|
||||||
|
}
|
||||||
|
}
|
15
server.ts
Normal file
15
server.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { IncomingMessage } from "http";
|
||||||
|
import { RawData, WebSocket } from "ws";
|
||||||
|
import RoomProto from "./src/RoomProto/RoomProto";
|
||||||
|
import Manager from "./src/RoomProto/Manager";
|
||||||
|
import SecurityManager from "./src/RoomProto/utils/secure";
|
||||||
|
|
||||||
|
const manager = new Manager("server");
|
||||||
|
|
||||||
|
export default function connectHandle(ws: WebSocket, req: IncomingMessage) {
|
||||||
|
const ip = req.socket.remoteAddress;
|
||||||
|
console.log(`Client connected ${ip}`);
|
||||||
|
|
||||||
|
let globalId: string = SecurityManager.generateId();
|
||||||
|
let func = new RoomProto(globalId, ws, manager);
|
||||||
|
}
|
87
src/RoomProto/Manager.ts
Normal file
87
src/RoomProto/Manager.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import WebSocket from "ws";
|
||||||
|
import { UserContainer, UserGroup } from "./utils/user";
|
||||||
|
import { User, userIdType } from "./utils/user";
|
||||||
|
import SecurityManager from "./utils/secure";
|
||||||
|
|
||||||
|
export default class Manager {
|
||||||
|
private security = new SecurityManager();
|
||||||
|
private users = new UserContainer();
|
||||||
|
private sessions = new Map<string, UserGroup>();
|
||||||
|
private regions = new Map<string, UserGroup>();
|
||||||
|
constructor(private serverName: string) {
|
||||||
|
this.regions.set(this.serverName, new UserGroup(this.users));
|
||||||
|
}
|
||||||
|
|
||||||
|
generateId(): string {
|
||||||
|
return SecurityManager.generateId();
|
||||||
|
}
|
||||||
|
|
||||||
|
createSession(name:string,
|
||||||
|
maxUsers: number,
|
||||||
|
sessionId: string = SecurityManager.generateId()): UserGroup {
|
||||||
|
var group = new UserGroup(this.users, name, maxUsers);
|
||||||
|
this.sessions.set(sessionId, group);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSession(sessionId: string) {
|
||||||
|
return this.sessions.get(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForSession(sessionId: string, userId: userIdType): boolean | undefined {
|
||||||
|
if (this.addSession(sessionId, userId) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let session = this.sessions.get(sessionId);
|
||||||
|
if (session?.getNowUsers() !== session?.getMaxUsers()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSession(sessionId: string, userId: userIdType): boolean {
|
||||||
|
let session = this.sessions.get(sessionId);
|
||||||
|
if (session === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return session.addUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSession(sessionId: string): boolean {
|
||||||
|
return this.sessions.delete(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateUser(name: string, fingerprint: string, ws: WebSocket,
|
||||||
|
userId: string = SecurityManager.generateId()) {
|
||||||
|
const user = new User(name,
|
||||||
|
this.security.generateToken(userId, fingerprint), ws);
|
||||||
|
this.users.addUser(userId, user);
|
||||||
|
return { userId, user };
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser(id: userIdType) {
|
||||||
|
return this.users.getUser(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
addToRegion(id: userIdType, token: string, regionId: string): boolean {
|
||||||
|
if (token === undefined || token === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.security.verifyToken(token) == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.regions.get(regionId)?.addUser(id) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIterRegion(regionId: string): IterableIterator<[userIdType,User]> | undefined {
|
||||||
|
return this.regions.get(regionId)?.getUsersIterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
getIterRegions(): IterableIterator<[string,UserGroup]> {
|
||||||
|
return this.regions.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser(id: userIdType) {
|
||||||
|
this.users.removeUser(id);
|
||||||
|
}
|
||||||
|
}
|
42
src/RoomProto/RoomProto.ts
Normal file
42
src/RoomProto/RoomProto.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import WebSocket from "ws";
|
||||||
|
import { RPMessage } from "./utils/type";
|
||||||
|
import BaseProto from "./type/BaseProto";
|
||||||
|
import Manager from "./Manager";
|
||||||
|
import UserProto from "./type/UserProto";
|
||||||
|
import RegionProto from "./type/RegionProto";
|
||||||
|
import MsgProto from "./type/MsgProto";
|
||||||
|
import SessionProto from "./type/SessionProto";
|
||||||
|
|
||||||
|
export default class RoomProto extends BaseProto {
|
||||||
|
user = new UserProto(this.id, this.ws, this.manager);
|
||||||
|
region = new RegionProto(this.id, this.ws, this.manager);
|
||||||
|
constructor(id: string, ws: WebSocket, manager: Manager) {
|
||||||
|
super(id, ws, manager);
|
||||||
|
this.setupMsgListener();
|
||||||
|
ws.on('close', this.onDisconnect.bind(this));
|
||||||
|
ws.on('error', console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
override handleMessage(msg: RPMessage) {
|
||||||
|
console.log(msg);
|
||||||
|
switch (msg.type) {
|
||||||
|
case "user":
|
||||||
|
this.user.handleMessage(msg);
|
||||||
|
break;
|
||||||
|
case "region":
|
||||||
|
this.region.handleMessage(msg);
|
||||||
|
break;
|
||||||
|
case "session":
|
||||||
|
new SessionProto(this.id, this.ws, this.manager).handleMessage(msg);
|
||||||
|
break;
|
||||||
|
case "msg":
|
||||||
|
new MsgProto(this.id, this.ws, this.manager).handleMessage(msg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return this.ws.send(JSON.stringify({
|
||||||
|
code: '0001',
|
||||||
|
data: 'Unknown Type'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
src/RoomProto/type/BaseProto.ts
Normal file
44
src/RoomProto/type/BaseProto.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import WebSocket, { RawData } from "ws";
|
||||||
|
import { RPMessage } from "../utils/type";
|
||||||
|
import Manager from "../Manager";
|
||||||
|
|
||||||
|
export default abstract class ProtocolBase {
|
||||||
|
|
||||||
|
constructor(protected id: string,
|
||||||
|
protected ws: WebSocket,
|
||||||
|
protected manager: Manager) {
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract handleMessage(message: RPMessage): void;
|
||||||
|
|
||||||
|
protected setupMsgListener() {
|
||||||
|
this.ws.on('message', (message: RawData) => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(message.toString());
|
||||||
|
this.handleMessage(json);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing message:', error);
|
||||||
|
this.sendError('0002', 'Invalid message format');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected send(data: any) {
|
||||||
|
this.ws.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendError(code: string, message: string) {
|
||||||
|
this.send({ code, data: {"_": message} });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendUnknownCommand() {
|
||||||
|
this.sendError('0003', 'Unknown command');
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDisconnect() {
|
||||||
|
// 处理断开连接的逻辑
|
||||||
|
if (this.id === null) return;
|
||||||
|
this.manager.removeUser(this.id);
|
||||||
|
console.log(`Client disconnected ${this.id}`);
|
||||||
|
}
|
||||||
|
}
|
20
src/RoomProto/type/MsgProto.ts
Normal file
20
src/RoomProto/type/MsgProto.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { RPMessage } from "../utils/type";
|
||||||
|
import BaseProto from "./BaseProto";
|
||||||
|
|
||||||
|
export default class MsgProto extends BaseProto {
|
||||||
|
override handleMessage(msg: RPMessage) {
|
||||||
|
switch (msg.cmd) {
|
||||||
|
case "echo":
|
||||||
|
this.sendRes("echo", "0000", msg.data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.sendUnknownCommand();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendRes(cmd: string, code: string, data: any) {
|
||||||
|
console.log(cmd, data);
|
||||||
|
this.send({ type:'msg', cmd, code, data});
|
||||||
|
}
|
||||||
|
}
|
81
src/RoomProto/type/RegionProto.ts
Normal file
81
src/RoomProto/type/RegionProto.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { RPMessage } from "../utils/type";
|
||||||
|
import BaseProto from "./BaseProto";
|
||||||
|
|
||||||
|
export default class RegionProto extends BaseProto {
|
||||||
|
override handleMessage(msg: RPMessage): void {
|
||||||
|
if (msg.uid === undefined) {
|
||||||
|
return this.sendError("0010", "uid is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg.cmd) {
|
||||||
|
case "list":
|
||||||
|
this.list(msg.uid, msg.data);
|
||||||
|
break;
|
||||||
|
case "inspect":
|
||||||
|
this.inspect(msg.uid, msg.data);
|
||||||
|
break;
|
||||||
|
case "add":
|
||||||
|
this.add(msg.uid, msg.token, msg.data);
|
||||||
|
break;
|
||||||
|
case "remove":
|
||||||
|
this.remove(msg.uid, msg.data);
|
||||||
|
break;
|
||||||
|
case "create":
|
||||||
|
this.create(msg.uid, msg.data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return this.sendUnknownCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private add(uid: string, token: string | undefined, data: any) {
|
||||||
|
if (token === undefined) {
|
||||||
|
return this.sendError("0010", "token is required");
|
||||||
|
}
|
||||||
|
if (this.manager.addToRegion(uid, token, data.regionId)) {
|
||||||
|
this.sendRes("add", "0000", undefined);
|
||||||
|
} else {
|
||||||
|
this.sendError("0011", "region not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private remove(uid: string, data: any) {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private list(uid: string, data: any) {
|
||||||
|
const usersData = [];
|
||||||
|
for (const [regionId, UserGroup] of this.manager.getIterRegions()) {
|
||||||
|
usersData.push({ id: regionId, name: UserGroup.getName(),
|
||||||
|
nowUsers: UserGroup.getNowUsers(),
|
||||||
|
maxUsers: UserGroup.getMaxUsers(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.sendRes("list", "0000", { "_": usersData });
|
||||||
|
}
|
||||||
|
|
||||||
|
private inspect(uid: string, data: any) {
|
||||||
|
if (data?.regionId === undefined) {
|
||||||
|
return this.sendError("0010", "regionId is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const region = this.manager.getIterRegion(data.regionId);
|
||||||
|
if (region === undefined) {
|
||||||
|
return this.sendError("0011", "region not found");
|
||||||
|
}
|
||||||
|
const usersData = [];
|
||||||
|
for (const [userId, User] of region) {
|
||||||
|
usersData.push({ id: userId, name: User.getName(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.debug("inspect: ", usersData);
|
||||||
|
this.sendRes("inspect", "0000", { "_": usersData });
|
||||||
|
}
|
||||||
|
|
||||||
|
private create(uid: string, data: any) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendRes(cmd: string, code: string, data: any) {
|
||||||
|
this.send({ type:'region', cmd, code, data});
|
||||||
|
}
|
||||||
|
}
|
136
src/RoomProto/type/SessionProto.ts
Normal file
136
src/RoomProto/type/SessionProto.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { RPMessage } from "../utils/type";
|
||||||
|
import BaseProto from "./BaseProto";
|
||||||
|
|
||||||
|
export default class SessionProto extends BaseProto {
|
||||||
|
override handleMessage(msg: RPMessage): void {
|
||||||
|
if (msg.uid === undefined) {
|
||||||
|
return this.sendError("0010", "uid is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg.cmd) {
|
||||||
|
case "create":
|
||||||
|
this.create(msg.uid, msg.token, msg.data);
|
||||||
|
break;
|
||||||
|
case "ackCreate":
|
||||||
|
this.ackCreate(msg.uid, msg.code, msg.data);
|
||||||
|
break;
|
||||||
|
case "exit":
|
||||||
|
this.exit(msg.uid, msg.token, msg.data);
|
||||||
|
break;
|
||||||
|
case "sendAll":
|
||||||
|
this.sendAll(msg.uid, msg.token, msg.data);
|
||||||
|
break;
|
||||||
|
case "res":
|
||||||
|
this.res(msg.cmd, msg.code, msg.data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.sendUnknownCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res(cmd: string, code: string | undefined, data: any) {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ackCreate(uid: string, code: string | undefined, data: any) {
|
||||||
|
let session = this.manager.getSession(data.sessionId);
|
||||||
|
if (code !== "0000") {
|
||||||
|
this.manager.removeSession(data.sessionId);
|
||||||
|
return session?.sendToAll(JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
cmd: "ackCreate",
|
||||||
|
code: "0000",
|
||||||
|
data: { res: false, sessionId: data.sessionId }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = this.manager.waitForSession(data.sessionId, uid);
|
||||||
|
if (res === false) {
|
||||||
|
this.manager.removeSession(data.sessionId);
|
||||||
|
return session?.sendToAll(JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
cmd: "ackCreate",
|
||||||
|
code: "0000",
|
||||||
|
data: { res: false, sessionId: data.sessionId }
|
||||||
|
}));
|
||||||
|
} else if (res === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session?.sendToAll(JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
cmd: "ackCreate",
|
||||||
|
code: "0000",
|
||||||
|
data: {
|
||||||
|
res: true,
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
create(uid: string, token: string | undefined, data: any) {
|
||||||
|
const usersId = data._ as string[]
|
||||||
|
if (usersId === undefined) {
|
||||||
|
return this.sendError("0011", "usersId is required");
|
||||||
|
}
|
||||||
|
let sessionid = this.manager.generateId();
|
||||||
|
for (let id of usersId) {
|
||||||
|
console.debug("id: ", id);
|
||||||
|
let user = this.manager.getUser(id)
|
||||||
|
if (user === undefined) {
|
||||||
|
return this.sendError("0011", "user not found");
|
||||||
|
}
|
||||||
|
user.send(JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
cmd: "ackCreate",
|
||||||
|
code: "0000",
|
||||||
|
data: {
|
||||||
|
sessionId: sessionid,
|
||||||
|
reqUserId: uid,
|
||||||
|
reqUserName: user.getName(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
this.manager.getUser(uid)?.send(JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
cmd: "create",
|
||||||
|
code: "0000",
|
||||||
|
}));
|
||||||
|
|
||||||
|
var group = this.manager.createSession(uid, usersId.length + 1, sessionid);
|
||||||
|
this.manager.addSession(sessionid, uid);
|
||||||
|
group.on('GroupUserRemove', () => {
|
||||||
|
group.sendToAll(JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
cmd: "exit",
|
||||||
|
code: "0000",
|
||||||
|
data: { "_": "sessionDelete" }
|
||||||
|
}));
|
||||||
|
this.manager.removeSession(sessionid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendRes(cmd: string, code: string, data: any) {
|
||||||
|
this.send({ type:'session', cmd, code, data});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendAll(uid: string, token: string | undefined, data: any) {
|
||||||
|
var session = this.manager.getSession(data.sessionId);
|
||||||
|
if (session == null) {
|
||||||
|
this.sendRes("sendAll", "0404", { "msg": "session not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.sendToAll(JSON.stringify({
|
||||||
|
type: "session",
|
||||||
|
cmd: "sendAll",
|
||||||
|
code: "0000",
|
||||||
|
data: {
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
reqUserId: uid,
|
||||||
|
reqUserName: this.manager.getUser(uid)?.getName(),
|
||||||
|
msg: data.msg,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
exit(uid: string, token: string | undefined, data: any) {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
73
src/RoomProto/type/UserProto.ts
Normal file
73
src/RoomProto/type/UserProto.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { RPMessage } from "../utils/type";
|
||||||
|
import BaseProto from "./BaseProto";
|
||||||
|
|
||||||
|
export default class UserProto extends BaseProto {
|
||||||
|
override handleMessage(msg: RPMessage) {
|
||||||
|
if (msg.cmd === "init") {
|
||||||
|
this.init(msg.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.uid === undefined) {
|
||||||
|
return this.sendError("0010", "uid is required");
|
||||||
|
}
|
||||||
|
switch (msg.cmd) {
|
||||||
|
case "login":
|
||||||
|
this.login(msg.uid, msg.data);
|
||||||
|
break;
|
||||||
|
case "logout":
|
||||||
|
this.logout(msg.uid, msg.data);
|
||||||
|
break;
|
||||||
|
case "rename":
|
||||||
|
this.rename(msg.uid, msg.data);
|
||||||
|
break;
|
||||||
|
case "exit":
|
||||||
|
this.exit(msg.uid, msg.token??"token", msg.data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return this.sendUnknownCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private exit(uid: string, token: string, data: any) {
|
||||||
|
let user = this.manager.getUser(uid);
|
||||||
|
if (user === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (token != user.getToken()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.manager.removeUser(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private rename(uid: string, data: any) {
|
||||||
|
let user = this.manager.getUser(uid);
|
||||||
|
if (user === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
user.setName(data._ ? data._: user.getName());
|
||||||
|
this.sendRes('rename', '0000', {"_": data});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendRes(cmd: string, code: string, data: any) {
|
||||||
|
this.send({ type:'user', cmd, code, data});
|
||||||
|
}
|
||||||
|
|
||||||
|
private login(uid: string, data: any) {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
private logout(uid: string, data: any) {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
private init(data: any) {
|
||||||
|
const fingerPrint = data?.fingerprint || "Godot Game";
|
||||||
|
const userName = data?.name || 'unknown name';
|
||||||
|
const { user } = this.manager.generateUser(userName, fingerPrint,
|
||||||
|
this.ws, this.id);
|
||||||
|
console.debug(`init user : ${this.id} ${userName}`);
|
||||||
|
this.sendRes('init', '0000', {
|
||||||
|
userId: this.id,
|
||||||
|
userName,
|
||||||
|
token: user.getToken(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
42
src/RoomProto/utils/secure.ts
Normal file
42
src/RoomProto/utils/secure.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { randomBytes, randomUUID } from "crypto";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
export default class SecurityManager {
|
||||||
|
private secretKey: string; // 用于JWT签名的密钥,请确保在生产环境中妥善保管
|
||||||
|
|
||||||
|
constructor(secret?: string) {
|
||||||
|
this.secretKey = secret ?? SecurityManager.generateSecretKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成ID
|
||||||
|
static generateId(): string {
|
||||||
|
return randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateSecretKey(byteLen: number = 32): string {
|
||||||
|
return randomBytes(byteLen).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成Token
|
||||||
|
generateToken(userId: string, fingerprint: string, additionalInfo?: any): string {
|
||||||
|
const payload = {
|
||||||
|
userId,
|
||||||
|
fingerprint,
|
||||||
|
...additionalInfo,
|
||||||
|
iat: Math.floor(Date.now() / 1000), // 发行时间
|
||||||
|
exp: Math.floor(Date.now() / 1000) + (60 * 60), // 过期时间,例如1小时后
|
||||||
|
};
|
||||||
|
return jwt.sign(payload, this.secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证Token
|
||||||
|
verifyToken(token: string): any | null {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, this.secretKey);
|
||||||
|
return decoded;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token verification failed:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
src/RoomProto/utils/type.ts
Normal file
24
src/RoomProto/utils/type.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
type user_id = string;
|
||||||
|
type token = string;
|
||||||
|
|
||||||
|
export type user = {
|
||||||
|
id: user_id;
|
||||||
|
name: string;
|
||||||
|
token: token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JsonHead {
|
||||||
|
type: string;
|
||||||
|
action?: string;
|
||||||
|
id?: string;
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RPMessage {
|
||||||
|
type: string;
|
||||||
|
cmd: string;
|
||||||
|
uid?: string;
|
||||||
|
token?: string;
|
||||||
|
data?: any;
|
||||||
|
code?: string;
|
||||||
|
}
|
152
src/RoomProto/utils/user.ts
Normal file
152
src/RoomProto/utils/user.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import WebSocket from "ws";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
|
export type userIdType = string;
|
||||||
|
type userNameType = string;
|
||||||
|
type userTokenType = string;
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
private name: userNameType;
|
||||||
|
private token: userTokenType;
|
||||||
|
private ws: WebSocket;
|
||||||
|
|
||||||
|
constructor(name: userNameType, token: userTokenType, ws: WebSocket) {
|
||||||
|
this.name = name;
|
||||||
|
this.ws = ws;
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(message: string) {
|
||||||
|
console.log(`${this.getName()} 收到消息: ${message}`);
|
||||||
|
// 这里可以是更复杂的逻辑处理消息
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message: string) {
|
||||||
|
this.ws.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): userNameType {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name: userNameType) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken(): userTokenType {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWs(): WebSocket {
|
||||||
|
return this.ws;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserContainer extends EventEmitter {
|
||||||
|
private users;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.users = new Map<userIdType, User>();
|
||||||
|
}
|
||||||
|
|
||||||
|
addUser(id: userIdType ,user: User) {
|
||||||
|
this.users.set(id, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser(id: userIdType) {
|
||||||
|
this.users.get(id)?.dispose();
|
||||||
|
this.emit("userRemove", id);
|
||||||
|
this.users.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser(id: userIdType) {
|
||||||
|
return this.users.get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserGroup extends EventEmitter {
|
||||||
|
private users;
|
||||||
|
constructor(private userContainer: UserContainer,
|
||||||
|
private name: string = "default",
|
||||||
|
private maxUsers: number = 15,
|
||||||
|
private garbageType: "auto" | "manual" = "auto") {
|
||||||
|
super();
|
||||||
|
this.users = new Map<userIdType, User>();
|
||||||
|
userContainer.on('userRemove', this.removeUser.bind(this));
|
||||||
|
};
|
||||||
|
|
||||||
|
addUser(userId: userIdType): boolean {
|
||||||
|
if (this.users.size === this.maxUsers) {
|
||||||
|
console.error(`房间 ${this.name} 已满员,无法添加新用户。`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const user = this.userContainer.getUser(userId);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
this.users.set(userId, user);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error(`尝试添加的用户ID ${userId} 对应的用户实例不存在。`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser(user: userIdType): boolean {
|
||||||
|
var res = this.users.delete(user);
|
||||||
|
this.emit('GroupUserRemove', user, this.users.size);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToAll(message: string) {
|
||||||
|
for (const user of this.users.values()) {
|
||||||
|
user.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaxUsers(maxUsers: number) {
|
||||||
|
this.maxUsers = maxUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxUsers() {
|
||||||
|
return this.maxUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNowUsers() {
|
||||||
|
return this.users.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToAllExcept(message: string, excludeUserId?: userIdType) {
|
||||||
|
for (const [userId, user] of this.users.entries()) {
|
||||||
|
if (excludeUserId !== userId) {
|
||||||
|
user.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTo(message: string, userId: userIdType) {
|
||||||
|
const user = this.users.get(userId);
|
||||||
|
if (user) {
|
||||||
|
user.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsersIterator(): IterableIterator<[userIdType, User]> {
|
||||||
|
return this.users.entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUser(userId: userIdType):boolean {
|
||||||
|
return this.users.has(userId);
|
||||||
|
}
|
||||||
|
}
|
17
src/main.ts
Normal file
17
src/main.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// server.js
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import connectHandle from '../server';
|
||||||
|
|
||||||
|
import { config } from "dotenv";
|
||||||
|
config();
|
||||||
|
|
||||||
|
const port = parseInt(process.env.PORT || "8077", 10);
|
||||||
|
const wss = new WebSocketServer({ port });
|
||||||
|
|
||||||
|
wss.on('connection', (socket, req) => {
|
||||||
|
connectHandle(socket, req);
|
||||||
|
});
|
||||||
|
|
||||||
|
wss.on('listening', () => {
|
||||||
|
console.log(`WebSocket server is listening on ${port}`);
|
||||||
|
});
|
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
// "skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"server.ts"
|
||||||
|
, "src/controller/user.ts", "src/controller/session.ts", "src/controller/region.ts", "src/controller/group.ts", "server.ts" ],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user