init 一个游戏服务器的后端demo

This commit is contained in:
ZZY 2024-06-27 14:53:43 +08:00
commit ad789c4517
15 changed files with 779 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
package-lock.json
dist/*
.*/
.env

27
package.json Normal file
View 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
View 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
View 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);
}
}

View 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'
}));
}
}
}

View 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}`);
}
}

View 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});
}
}

View 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});
}
}

View 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.");
}
}

View 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(),
});
}
}

View 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;
}
}
}

View 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
View 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
View 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
View 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"
]
}