[mod] TSRPC

This commit is contained in:
2023-08-31 19:28:35 +08:00
parent f7559a5f27
commit e743a53f18
35 changed files with 2494 additions and 476 deletions

View File

@@ -1,106 +0,0 @@
import _ws from "ws"
import { Encoding } from "../Engine/CatanEngine/CSharp/System/Text/Encoding"
import { INetResponse } from "../Engine/CatanEngine/NetManagerV2/Core/INetResponse"
import WebSocketServerClass from "../NetManager/WebSocketServerClass"
/**
* Client
*/
export default class Client {
//#region private
private ws: _ws = undefined
private clientCount: number = undefined
//#endregion
//#region Lifecycle
/**
*
*/
constructor(ws: _ws, clientCount: number) {
this.ws = ws
this.clientCount = clientCount
// 當收到client消息時
ws.on('message', this.onMessage.bind(this))
// 當連線關閉
ws.on('close', this.onClose.bind(this))
}
//#endregion
//#region Custom
private onMessage(buffer: _ws.RawData): void {
// 收回來是 Buffer 格式、需轉成字串
const dataStr: string = "[" + buffer.toString().split("[").slice(1).join("[")
const json = JSON.parse(dataStr)
const method = <string>json[0]
let status = 0
const data = json[1]
const resp = {
Method: method,
Status: status,
Data: data,
IsValid: method && status === 0,
WS: this
}
if (true) {
if (data) {
console.debug(`[RPC] 收到server呼叫:(${resp.WS.clientCount}): ${resp.Method}(${JSON.stringify(resp.Data)})`)
} else {
console.debug(`[RPC] 收到server呼叫:(${resp.WS.clientCount}): ${resp.Method}()`)
}
}
WebSocketServerClass.Instance.OnDataReceived.DispatchCallback(resp)
// /// 發送消息給client
// this.SendClient(data)
// WebSocketServerClass.Instance.SendAllClient(data)
}
private onClose(): void {
console.log(`Client_${this.clientCount} Close connected`)
}
/** 發送給client */
public SendClient(req: INetResponse<any>): void {
const status = 0
const json: any[] = [req.Method]
//@ts-ignore
if (req.Data != null && req.Data != undefined && req.Data != NaN) {
json[1] = [status, req.Data]
}
if (true) {
//@ts-ignore
if (req.Data != null && req.Data != undefined && req.Data != NaN) {
console.log(`[RPC] 傳送client資料:(${this.clientCount}): ${req.Method}(${JSON.stringify(req.Data)})`)
} else {
console.log(`[RPC] 傳送client資料:(${this.clientCount}): ${req.Method}()`)
}
}
const str = JSON.stringify(json)
if (str.length > 65535) {
throw new Error('要傳的資料太大囉')
}
const strary = Encoding.UTF8.GetBytes(str)
const buffer = new Uint8Array(4 + strary.byteLength)
const u16ary = new Uint16Array(buffer.buffer, 0, 3)
u16ary[0] = strary.byteLength
buffer[3] = 0x01
buffer.set(strary, 4)
this.ws.send(buffer)
}
//#endregion
}

View File

@@ -1,28 +0,0 @@
import { INetResponse } from "../Engine/CatanEngine/NetManagerV2/Core/INetResponse";
import Lobby from "../Lobby/Lobby";
import WebSocketServerClass from "../NetManager/WebSocketServerClass";
export default class MainControlData {
constructor() {
WebSocketServerClass.Instance.OnDataReceived.AddCallback(this._serverData, this)
}
/** SERVER主動通知 */
private _serverData(req: INetResponse<any>): void {
if (req.IsValid) {
switch (req.Method) {
case "lobby.list":
Lobby.List(req);
break
case "lobby.create":
Lobby.Create(req);
break
default:
// if (GameMain.Instance && GameMain.Instance.node && GameMain.Instance.node.parent) {
// GameMain.Instance.SettingBase.OnNetDataReceived(resp)
// }
break
}
}
}
}

View File

@@ -1,50 +0,0 @@
import { LobbyCreateRequest, LobbyListRequest } from "../define/Request/LobbyRequest";
import { INetResponse } from "../Engine/CatanEngine/NetManagerV2/Core/INetResponse";
import Room from "../Room/Room";
/**
* Lobby
*/
export default class Lobby {
//#region private
private static list: Room[] = []
private static serialNumber: number = 0
//#endregion
//#region Custom
/** GetList */
public static List(req: INetResponse<any>): void {
const data = []
for (let i = 0; i < this.list.length; i++) {
const room = this.list[i];
data.push(room.SerialNumber)
}
const resp: LobbyListRequest = new LobbyListRequest(data, 0)
req.WS.SendClient(resp)
}
/** Create */
public static Create(req: INetResponse<any>): void {
const room: Room = new Room(Lobby.serialNumber, req.WS)
Lobby.serialNumber++;
this.list.push(room)
const resp: LobbyCreateRequest = new LobbyCreateRequest()
req.WS.SendClient(resp)
}
/** Join */
public static Join(req: INetResponse<any>): void {
//
}
/** Exit */
public static Exit(req: INetResponse<any>): void {
//
}
//#endregion
}

View File

@@ -1,21 +0,0 @@
// export class MainControl {
// //#region 網路相關
// /**連線(目前沒有重連機制) */
// public * ConnectAsync() {
// if (NetManager.IsConnected) {
// return
// }
// this._conn = new NetConnector(BusinessTypeSetting.UseHost, BusinessTypeSetting.UsePort/*, this._realIp*/)
// this._conn.OnDataReceived.AddCallback(this._onNetDataReceived, this)
// this._conn.OnDisconnected.AddCallback(this._onNetDisconnected, this)
// this._conn.OnLoadUIMask.AddCallback(this._oOnLoadUIMask, this)
// NetManager.Initialize(this._conn)
// cc.log("[socket] connecting...")
// // 同個connector要再次連線, 可以不用叫CasinoNetManager.Initialize(), 但要先叫CasinoNetManager.Disconnect()
// yield NetManager.ConnectAsync()
// }
// //#endregion
// }

View File

@@ -1,79 +0,0 @@
import express from "express"
import fs from "fs"
import _ws from "ws"
import Client from "../Client/Client"
import { Action } from "../Engine/CatanEngine/CSharp/System/Action"
import { INetResponse } from "../Engine/CatanEngine/NetManagerV2/Core/INetResponse"
import BaseSingleton from "../Engine/Utils/Singleton/BaseSingleton"
const SocketServer: typeof _ws.Server = _ws.Server
/**
* WebSocketServer
*/
export default class WebSocketServerClass extends BaseSingleton<WebSocketServerClass>() {
readonly OnDataReceived: Action<INetResponse<any>> = new Action<INetResponse<any>>()
//#region private
private wss: _ws.Server = undefined
private clientCount: number = 0
//#endregion
//#region Lifecycle
/**
*
*/
constructor() {
super()
//讀取憑證及金鑰
const prikey: string = fs.readFileSync("./certificate/RSA-privkey.pem", "utf8")
const cert: string = fs.readFileSync("./certificate/RSA-cert.pem", "utf8")
const cafile: string = fs.readFileSync("./certificate/RSA-chain.pem", "utf-8")
//建立憑證及金鑰
const credentials: Object = {
key: prikey,
cert: cert,
ca: cafile
}
// 用於辨識Line Channel的資訊
const config: any = {
channelSecret: process.env.channelSecret,
channelAccessToken: process.env.channelAccessToken || ""
}
const port: number = +process.env.PORT || 3000
const server = express().listen(port, () => {
console.log(`Listening on ${port}`)
})
//將 express 交給 SocketServer 開啟 WebSocket 的服務
this.wss = new SocketServer({ server })
//當有 client 連線成功時
this.wss.on('connection', this.onConnection.bind(this))
}
//#endregion
//#region Custom
private onConnection(ws: _ws): void {
const clientNum: number = this.clientCount
console.log(`Client_${clientNum} connected`)
new Client(ws, clientNum)
this.clientCount++
}
/** 發送給所有client */
public SendAllClient(data: any): void {
let clients = this.wss.clients //取得所有連接中的 client
clients.forEach(client => {
client.send(data) // 發送至每個 client
})
}
//#endregion
}

View File

@@ -1,44 +0,0 @@
import mysql from "mysql"
import Tools from "./Tools"
/**
* DBTools
*/
export default class DBTools {
//#region Custom
public static async Query(query: string): Promise<any> {
const conn: mysql.Connection = this.connect()
let resp: any = null
let run: boolean = true
conn.query(query, function (err: mysql.MysqlError, rows: any, fields: mysql.FieldInfo[]): void {
if (err) {
console.error(`${query} Error: \n${err.message}`)
run = false
}
resp = rows
run = false
})
while (run) {
await Tools.Sleep(100)
}
conn.end()
return resp
}
private static connect(): mysql.Connection {
const conn: mysql.Connection = mysql.createConnection({
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE
})
conn.connect()
return conn
}
//#endregion
}

View File

@@ -1,13 +0,0 @@
/**
* Tools
*/
export default class Tools {
//#region Custom
public static Sleep(ms: number): Promise<any> {
return new Promise(resolve => setTimeout(resolve, ms));
}
//#endregion
}

View File

@@ -0,0 +1,24 @@
import { ApiCall, BaseConnection } from "tsrpc";
import Client from "../component/Client/Client";
import User from "../component/Client/User";
import Lobby from "../component/Lobby/Lobby";
import { ReqAccountLogin, ResAccountLogin } from "../shared/protocols/PtlAccountLogin";
export default async function (call: ApiCall<ReqAccountLogin, ResAccountLogin>) {
// Error
if (!call.req.name) {
call.error('Name is empty')
return;
}
// Success
const { sn, req } = call
const { name } = req
const conn: BaseConnection<any> = call.conn
console.log(`name: ${name} is Login`)
const user = new User(name)
const client = new Client(conn, sn)
client.setUser(user)
Lobby.AddClient(client)
call.succ(0)
}

12
src/api/ApiLobbyList.ts Normal file
View File

@@ -0,0 +1,12 @@
import { ApiCall } from "tsrpc";
import Lobby from "../component/Lobby/Lobby";
import { ReqLobbyList, ResLobbyList } from "../shared/protocols/PtlLobbyList";
export default async function (call: ApiCall<ReqLobbyList, ResLobbyList>) {
const data: any[] = []
for (let i = 0; i < Lobby.Room.length; i++) {
const room = Lobby.Room[i]
data.push(room.SerialNumber)
}
call.succ(data)
}

26
src/api/ApiSend.ts Normal file
View File

@@ -0,0 +1,26 @@
import { ApiCall } from "tsrpc";
import { server } from "..";
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
// This is a demo code file
// Feel free to delete it
export default async function (call: ApiCall<ReqSend, ResSend>) {
// Error
if (call.req.content.length === 0) {
call.error('Content is empty')
return;
}
// Success
let time = new Date();
call.succ({
time: time
});
// Broadcast
server.broadcastMsg('Chat', {
content: call.req.content,
time: time
})
}

View File

@@ -1,18 +0,0 @@
// 背景執行 forever start -c ts-node -a -l line-bot-ts.log src/app.ts
// 重新背景執行 forever restart -a -l line-bot-ts.log src/app.ts
// 監聽檔案變化 "npm start"
// 連線Debug "npm run dev"
import dayjs from "dayjs"
import "dayjs/locale/zh-tw"
import dotenv from "dotenv"
import MainControlData from "./DataReceived/MainControlData"
import "./Engine/CatanEngine/CSharp/String"
import "./Engine/Utils/CCExtensions/ArrayExtension"
import "./Engine/Utils/CCExtensions/NumberExtension"
import WebSocketServerClass from "./NetManager/WebSocketServerClass"
dayjs.locale("zh-tw")
dotenv.config()
new WebSocketServerClass()
new MainControlData()

View File

@@ -0,0 +1,128 @@
import { BaseConnection } from "tsrpc"
import User from "./User"
/**
* Client
*/
export default class Client {
//#region private
private conn: BaseConnection<any> = undefined
private ws: any = undefined
private sn: number = undefined
//#endregion
//#region get set
public get User(): User {
return this.user
}
private user: User = undefined
//#endregion
//#region Lifecycle
/**
*
*/
constructor(conn: BaseConnection<any>, sn: number) {
this.conn = conn
this.ws = conn["ws"]
this.sn = sn
// // 當收到client消息時
// ws.on('message', this.onMessage.bind(this))
// // 當連線關閉
// ws.on('close', this.onClose.bind(this))
}
//#endregion
//#region Custom
/**
* setUser
*/
public setUser(user: User) {
this.user = user
}
//#endregion
//#region Server
// private onMessage(buffer: _ws.RawData): void {
// // 收回來是 Buffer 格式、需轉成字串
// const dataStr: string = "[" + buffer.toString().split("[").slice(1).join("[")
// const json = JSON.parse(dataStr)
// const method = <string>json[0]
// let status = 0
// const data = json[1]
// const resp = {
// Method: method,
// Status: status,
// Data: data,
// IsValid: method && status === 0,
// WS: this
// }
// if (true) {
// if (data) {
// console.debug(`[RPC] 收到server呼叫:(${resp.WS.clientCount}): ${resp.Method}(${JSON.stringify(resp.Data)})`)
// } else {
// console.debug(`[RPC] 收到server呼叫:(${resp.WS.clientCount}): ${resp.Method}()`)
// }
// }
// WebSocketServerClass.Instance.OnDataReceived.DispatchCallback(resp)
// // /// 發送消息給client
// // this.SendClient(data)
// // WebSocketServerClass.Instance.SendAllClient(data)
// }
// private onClose(): void {
// console.log(`Client_${this.clientCount} Close connected`)
// }
// /** 發送給client */
// public SendClient(req: INetResponse<any>): void {
// const status = 0
// const json: any[] = [req.Method]
// //@ts-ignore
// if (req.Data != null && req.Data != undefined && req.Data != NaN) {
// json[1] = [status, req.Data]
// }
// if (true) {
// //@ts-ignore
// if (req.Data != null && req.Data != undefined && req.Data != NaN) {
// console.log(`[RPC] 傳送client資料:(${this.clientCount}): ${req.Method}(${JSON.stringify(req.Data)})`)
// } else {
// console.log(`[RPC] 傳送client資料:(${this.clientCount}): ${req.Method}()`)
// }
// }
// const str = JSON.stringify(json)
// if (str.length > 65535) {
// throw new Error('要傳的資料太大囉')
// }
// const strary = Encoding.UTF8.GetBytes(str)
// const buffer = new Uint8Array(4 + strary.byteLength)
// const u16ary = new Uint16Array(buffer.buffer, 0, 3)
// u16ary[0] = strary.byteLength
// buffer[3] = 0x01
// buffer.set(strary, 4)
// this.ws.send(buffer)
// }
//#endregion
}

View File

@@ -0,0 +1,29 @@
/**
* User
*/
export default class User {
//#region get set
public get Name(): string {
return this.name
}
private name: string = undefined
//#endregion
//#region Lifecycle
constructor(name: string) {
this.name = name
}
//#endregion
//#region Custom
//#endregion
}

View File

@@ -0,0 +1,77 @@
import Client from "../Client/Client";
import Room from "../Room/Room";
/**
* Lobby
*/
export default class Lobby {
//#region private
private static clients: Client[] = []
private static serialNumber: number = 0
//#endregion
//#region get set
public static get Room(): Room[] { return this.room }
private static room: Room[] = []
//#endregion
//#region Custom
/** AddClient */
public static AddClient(client: Client): void {
this.clients.push(client)
}
// /** List */
// public static List(req: INetResponse<RpcLobbyListRequest>): void {
// const data = []
// for (let i = 0; i < this.list.length; i++) {
// const room = this.list[i]
// data.push(room.SerialNumber)
// }
// const resp: LobbyListRequest = new LobbyListRequest(data, 0)
// req.WS.SendClient(resp)
// }
// /** Create */
// public static Create(req: INetResponse<RpcLobbyCreateRequest>): void {
// const room: Room = new Room(Lobby.serialNumber, req.WS)
// Lobby.serialNumber++
// this.list.push(room)
// const resp: LobbyCreateRequest = new LobbyCreateRequest()
// req.WS.SendClient(resp)
// }
// /** Join */
// public static Join(req: INetResponse<RpcLobbyJoinRequest>): void {
// const serialNumber: number = req.Data
// for (let i = 0; i < this.list.length; i++) {
// const room = this.list[i]
// if (room.SerialNumber === serialNumber) {
// room.Join(req.WS)
// break
// }
// }
// }
// /** Exit */
// public static Exit(req: INetResponse<RpcLobbyExitRequest>): void {
// const serialNumber: number = req.Data
// for (let i = 0; i < this.list.length; i++) {
// const room = this.list[i]
// if (room.SerialNumber === serialNumber) {
// this.list.splice(i, 1)
// // room.Exit()
// // room = null
// break
// }
// }
// }
//#endregion
}

View File

@@ -31,5 +31,13 @@ export default class Room {
//#region Custom
/** Join */
public Join(ws: Client): void {
this.wsArr.forEach(otherWS => {
// otherWS.SendClient()
})
this.wsArr.push(ws)
}
//#endregion
}

View File

@@ -1,28 +0,0 @@
import { NetRequest } from "../../Engine/CatanEngine/NetManagerV2/NetRequest";
// #region Request
export type RpcExampleCodeRequest = null
export type RpcExampleCodeResponse = ExampleCodeData[]
export class ExampleCodeRequest extends NetRequest<RpcExampleCodeRequest, RpcExampleCodeResponse> {
get Method(): string {
return "example.code";
}
constructor() {
super();
}
}
// #endregion
// #region Type
export type ExampleCodeData = [
id: number,
title: number,
content: number,
time: number,
];
// #endregion

View File

@@ -1,23 +0,0 @@
import { NetResponse } from "../../Engine/CatanEngine/NetManagerV2/NetResponse"
// #region Request
export type RpcLobbyListRequest = any[]
export class LobbyListRequest extends NetResponse {
protected data: RpcLobbyListRequest
protected method: string = "lobby.list"
constructor(data: RpcLobbyListRequest = undefined, status: number = 0) { super(data, status) }
}
export type RpcLobbyCreateRequest = undefined
export class LobbyCreateRequest extends NetResponse {
protected data: RpcLobbyListRequest
protected method: string = "lobby.create"
constructor(data: RpcLobbyCreateRequest = undefined, status: number = 0) { super(data, status) }
}
// #endregion
// #region Type
// #endregion

34
src/index.ts Normal file
View File

@@ -0,0 +1,34 @@
import dayjs from "dayjs";
import "dayjs/locale/zh-tw";
import * as path from "path";
import { WsServer } from "tsrpc";
import { BaseEnumerator } from "./Engine/CatanEngine/CoroutineV2/Core/BaseEnumerator";
import "./Engine/CatanEngine/CSharp/String";
import "./Engine/Utils/CCExtensions/ArrayExtension";
import "./Engine/Utils/CCExtensions/NumberExtension";
import { serviceProto } from './shared/protocols/serviceProto';
BaseEnumerator.Init();
dayjs.locale("zh-tw")
// Create the Server
export const server = new WsServer(serviceProto, {
port: 3003,
// Remove this to use binary mode (remove from the client too)
json: true
});
// Initialize before server start
async function init() {
await server.autoImplementApi(path.resolve(__dirname, 'api'));
// TODO
// Prepare something... (e.g. connect the db)
};
// Entry function
async function main() {
await init();
await server.start();
}
main();

1
src/shared/protocols Submodule

Submodule src/shared/protocols added at 3aab251c77