How to implement an RPC component based on WebSocket

In WebSocket-based applications, data communication is often separated, that is, after the client sends a request, the server data response is completely separated from the request, and of course the server is allowed to not respond at all. RPC (Remote Procedure Call), that is, remote procedure call. In this mode, the request and response are strongly associated, and the response result of a request can only be the correct expected result, abnormal failure or timeout. So what are its application scenarios in Cocos Creator? When the first screen loading progress needs to rely on user login, is the best way to communicate synchronously? When obtaining the ranking data, does it strictly meet the one-to-one response mode of request and response? …
The following implements a pseudo RPC call. The reason why there is a “pseudo” is because my knowledge is limited, and I have not found the JS/TS synchronous blocking method, and pseudo synchronization has only found async/await.

        private readonly invokeMap: Map<string, object> = new Map<string, object>();  //缓存RPC请求
        private readonly invokeOpCode: number = -999999;  //RPC调用前后端通信协议号
        /**
         * 异步RPC调用
         * @param invokeName 远程函数唯一标识
         * @param timeout 调用超时时间,毫秒
         * @param params 远程函数参数,其数组元素仅支持number、string、Uint8Array、ByteArray、Array<number|string|Uint8Array|ByteArray>
         * @returns 远程函数调用结果,仅支持number、string、ByteArray、Array<number|string>
         */
        public async request(invokeName: number | string, params: any[], timeout: number = 3000): Promise<any> {
            const id: string = StringUtils.randomString(16, true); //RPC调用唯一标识,须保证单个连接内为唯一
            let _this = this;
            let invokeObj = { response: { data: null, err: null }, cmd: invokeName, startTime: DateUtils.localTimeMillis };
            let promise: Promise<any> = new Promise<any>((resolve, reject) => {
                const timeoutId = setTimeout(() => {
                    _this.invokeMap.delete(id);
                    reject(new RemotingError(1, '操作超时'));
                }, timeout);
                invokeObj['proxy'] = new Proxy(invokeObj['response'], {
                    set(target, property, value, receiver) {
                        target[property] = value;
                        clearTimeout(timeoutId);
                        if (property === 'data') {
                            resolve(value);
                        } else {
                            reject(new RemotingError(2, value));
                        }
                        return true;
                    }
                });
            });
            this.invokeMap.set(id, invokeObj);
            let ba: ByteArray = new ByteArray();
            ba.writeInt(this.invokeOpCode); // 协议号
            ba.writeUTF(id); // 调用唯一标识
            if (Utils.isNumber(invokeName)) { // 远程方法名是数字
                ba.writeByte(1);
                ba.writeInt(Number(invokeName));
            } else { // 远程方法名是字符串
                ba.writeByte(2);
                ba.writeUTF(String(invokeName));
            }
            // 远程方法参数
            if (!params || params.length == 0) {
                ba.writeShort(0);
            } else {
                ba.writeShort(params.length);
                for (let i = 0; i < params.length; i++) {
                    this.writeValue(ba, params[i]); // 自定义数据序列化
                }
            }
            ba.position = 0;
            this.ws.send(ba.buffer);
            return await promise;
        }

Do the following processing in WebSocket onmessage

    let ba: ByteArray = new ByteArray(ev.data);
    ba.position = 0;
    if (ba.readAvailable >= 4) {
        let opcode = ba.readInt(); //协议号
        let bytes: Uint8Array = ba.bytes.slice(4); //去除协议号后的数据
        if (opcode === this.invokeOpCode) { // 处理RPC响应
            let id: string = ba.readUTF();
            let retType: number = ba.readByte(); //返回成功与否,1或0为成功,-1为失败
            let obj: object = this.invokeMap.get(id);
            this.invokeMap.delete(id);
            if (obj) {
                //let cmd = obj['cmd'];
                //let startTime: number = obj['startTime'];
                //let endTime: number = DateUtils.localTimeMillis;
                //console.log('调用rpc方法[' + cmd + ']耗时:', (endTime - startTime));
                let proxy = obj['proxy'];
                if (proxy) {
                    if (retType == 0 || retType == 1) {
                        proxy.data = this.readValue(ba);
                    } else {
                        let err = this.readValue(ba);
                        if (Utils.isString(err)) {
                            proxy.err = err;
                        } else if (retType === -2 && err instanceof ByteArray) {
                            //处理错误码
                            let gameProxy: GameProxy = this.facade.retrieveProxy(GameProxy);
                            gameProxy.processError(err.bytes);
                        }
                    }
                }
            }
        } else if (opcode === pb.protos.MsgId.heart_beat) {
            let sc: pb.protos.ScHeartBeat = pb.protos.ScHeartBeat.decode(bytes);
            if (Utils.isNumber(sc.time)) {
                DateUtils.init(Number(sc.time));
            }
        } else {
            this.facade.sendMessage(new Message(opcode, bytes), true); //派发异步事件
        }
    }

I believe that everyone who is smart has discovered that the core principle of the above code is to assign a unique identifier within the connection to the communication protocol. Hahaha, it’s simple. The following is a simple usage example:

// 请求登录
ws.request(pb.protos.MsgId.login, [pb.protos.CsLogin.encode(param).finish()]).then(res => {
    let byteArr: ByteArray = res as ByteArray;
    let sc: pb.protos.ScLogin = pb.protos.ScLogin.decode(byteArr.bytes);
    console.log('登录成功');
    ……
}).catch(err => {
    console.log(`登录失败, ${err}`);
});

// RPC获取排行数据
// 第一个参数为远程方法名
// 第二个参数为远程方法参数,此处仅有一个参数,为proto序列化后的字节数据
ws.request(pb.protos.MsgId.rank_get_list, [pb.protos.CsRankList.encode(param).finish()]).then(res => {
    let byteArr: ByteArray = res as ByteArray;
    let sc: pb.protos.ScRankList = pb.protos.ScRankList.decode(byteArr.bytes);
    ……
}).catch(err => {
    console.log(err);
});

The above implementation still supports separate request and response, and of course also supports server-active push.

1 Like

This is a design problem of the plug-in system. The most important thing is that all the things of h5 are used directly. The mouse event cannot directly provide the coordinates relative to the origin of the current panel. The coordinates must be converted every time they are used, but no API is provided. Only the original API of h5 can be used. I can only say that I don’t know how to use it.