UNPKG

futu-api

Version:

Futu Websocket API for Node.js

467 lines (434 loc) 17.2 kB
/** * @fileOverview 富途OpenD websocket js接口封装 * @author dream * @version 0.1 */ import util from 'util' import bytebuffer from 'bytebuffer' import protobuf from "protobufjs"; import protoRoot from "./proto.js"; const ftApiCmdID = { Init: 1, //初始化 OpenDisConnect: 2, //websocket和opend之间的连接断开了,用push通知 } const ftWebsocketRecvError = { ErrorSign: 1, //没有解析到正确的sign } /** * @readonly * @enum {number} * @description 回调中的error枚举值 */ const ftWebsocketError = { WEBSOCKET_ERROR_TIMEOUT: 1, //没有收到opend的回包超时 WEBSOCKET_ERROR_OPEND_TIMEOUT: 2, //opend自己的超时,可能是opend跟服务器之间断了 WEBSOCKET_ERROR_DISCONNECT: 3, //关闭websocket和opend之间的连接 } var ftWebsocketSection = 1; const ftWebsocketHeadLength = 44; //包头大小 const ftWebsocketHeadSign = "ft-v1.0"; /** * @name ftWebsocketBase * @class websocket封装工具类 * @constructor ftWebsocketBase * @classdesc websocket封装工具类 * @example websocketQotSub() { //演示一个订阅请求的发送 const QotSubPBMessageRequest = protoRoot.lookup("Qot_Sub.Request"); // 请求体message const QotSecurityPBMessageRequest = protoRoot.lookup( "Qot_Common.Security" ); // 请求体message let securit1 = QotSecurityPBMessageRequest.create({ market: 1, code: "00700" }); let securit2 = QotSecurityPBMessageRequest.create({ market: 2, code: "999010" }); let subReq = { c2s: { securityList: [securit1, securit2], subTypeList: [1, 2, 4], isSubOrUnSub: true, isRegOrUnRegPush: true } }; let message = QotSubPBMessageRequest.encode( QotSubPBMessageRequest.create(subReq) ).finish(); this.$websocket .sendBuff(3001, message) .then(response => { if (response.buff) { const QotSubPBMessageResponse = protoRoot.lookup( "Qot_Sub.Response" ); // 回包message const buf = protobuf.util.newBuffer(response.buff); const subResult = QotSubPBMessageResponse.decode(buf); console.log(subResult); } }) .catch(error => { console.log("login error:", error); }); } */ class ftWebsocketBase { constructor() { this.websock = null; this.wsuri = "wss://127.0.0.1:8080"; this.initOpenDConfigBuff = null; this.timeout = 5000; //5s this.reconnectTimeout = 1000; //每秒重连一次 this.promisePool = {}; this.message = ""; this.state = { closing: false, login: false, }; this.pushCalls = new Map(); this.connID = 0; } /** * * @description 初始化设置websocket连接的ip和端口号 * @param {string} ip ip地址,如127.0.0.1 * @param {number} port 端口号,如 8080 * @memberof ftWebsocketBase */ setWsConfig(ip, port, ssl) { if (ip !== null && port !== null) { let wsuri; if (ssl == false) { wsuri = util.format("ws://%s:%d", ip, port); } else { wsuri = util.format("wss://%s:%d", ip, port); } if (wsuri != this.wsuri) { this.close(); this.wsuri = wsuri; } } } packBuff(cmd, section, buff) { var pbuff = new bytebuffer(); pbuff.writeUTF8String(ftWebsocketHeadSign); let completeLength = 8 - bytebuffer.calculateString(ftWebsocketHeadSign); if (completeLength > 0) { for (let i = 0; i < completeLength; i++) { pbuff.writeByte(0); } } pbuff.writeUInt32(cmd); pbuff.writeUInt64(section); if (buff instanceof Uint8Array) { pbuff.append(buff); } else if (buff instanceof ArrayBuffer) { pbuff.append(new Uint8Array(buff)); } pbuff.flip(); return pbuff.toArrayBuffer(); } // 解包函数 unpackBuff(buff) { if (buff instanceof ArrayBuffer) { var pbuff = new bytebuffer(buff.byteLength, false); let result = new Object() pbuff.append(buff); pbuff.flip(); result.sign = pbuff.readUTF8String(8); result.cmd = pbuff.readUInt32(); result.section = pbuff.readUInt64(); result.error = pbuff.readInt32(); //https://github.com/nodejs/node/issues/4775 result.errmsg = pbuff.readUTF8String(20).replace(/\0/g, ''); if (buff.byteLength > ftWebsocketHeadLength) { let data = pbuff.readBytes(buff.byteLength - ftWebsocketHeadLength); result.buff = data.toArrayBuffer(); } return result; } return null; } /** * @description 数据发送,适用于一应一答的场景,发送数据后,必然回包或超时 * @see ftWebsocketError * @example this.$websocket .sendBuff(3001, message) //已经序列化好的pb,直接透传给opend,所以务必保证cmd和二进制对应 .then(response => { if (response.error == 0 && response.buff) { //自己去解包pb包吧,某些协议有可能对buff并不关心 } }) .catch(error => { console.log("login error:", error); //error包括超时等各种情况 }); * @param {Number} cmd 命令字,如1001,注意100以内的命令字可能被征用做内部通讯,参考https://futunnopen.github.io/futu-api-doc/protocol/intro.html * @param {Uint8Array} buff 发送的二进制数据,Uint8Array或者ArrayBuffer格式 protobufjs库编码后就是Uint8Array的,可以为空,表示空包协议 * @param {Number} timeout 协议超时时间,可以默认不填,默认5s超时 * @return {Promise} 如果有数据正确返回,将会在then得到{cmd:Number;buff:ArrayBuffer;error:Number;errmsg:string},超时等错误会在catch返回 * @async * @memberof ftWebsocketBase */ sendBuff(cmd, buff, timeout) { var that = this; //超时回调 function timeoutCallBack(section, timeout) { return new Promise(function (resolve, reject) { let timer = setTimeout(() => { if (that.promisePool[section] != null) { reject('timeout'); console.log("section:", section, " timeout"); delete that.promisePool[section]; } }, timeout); Object.assign(that.promisePool[section], { timer }); }); } //数据接收回调填充数据 function requestCallBack(section) { return new Promise((resolve, reject) => { Object.assign(that.promisePool[section], { section, resolve, reject, "socket": that.websock, }); }); } var section = 0; if (this.websock && this.websock.readyState == WebSocket.OPEN) { if (this.state.login || cmd == ftApiCmdID.Init) //没有初始化之前不允许请求 { section = ++ftWebsocketSection; this.promisePool[section] = new Object; let arrayBuff = this.packBuff(cmd, section, buff); this.websock.send(arrayBuff); if (timeout == null || timeout == undefined) { timeout = this.timeout } return Promise.race([requestCallBack(section), timeoutCallBack(section, timeout)]) } } return new Promise((resolve, reject) => { reject('error websock not ready'); }); } recvBuff(buff) { let result = this.unpackBuff(buff); let error = 0; if (result !== null && result.sign !== null && result.sign !== undefined) { if (result.sign.indexOf(ftWebsocketHeadSign) == -1) { console.log(result.sign, "||", ftWebsocketHeadSign); error = ftWebsocketRecvError.ErrorSign; return { error, result }; //错误的回包数据,直接过滤掉 } delete result.sign; //鉴定完成后,这个数据对后继解析没任何意义了 let section = 0; if (result.section !== null && result.section !== undefined) { section = result.section; } delete result.section; //对后继分析没任何意义了 const req = this.promisePool[section] // 在promisePool里面找得到的是请求->回包结构,另外一种是push if (req != null) { req.resolve(result); delete this.promisePool[section]; if (req.timer != null) { clearTimeout(req.timer); req.timer = null; } } else { this.pushCalls.forEach(function (f) { f(result); }); } } return { error, result }; } /** * 注册push回调,回调链可以注册多个,务必需要反注册 * @param {*} key 用于反注册时候使用的key * @param {*} func 回调函数,需要满足接收一个参数,参数是传回的整个包 * @example created() { this.$websocket.regPushCallback(this, this.onPush.bind(this)); //map结构,只要key唯一,允许反复注册,只有一次调用 }, destroyed() { this.$websocket.unregPushCallback(this); }, * @memberof ftWebsocketBase */ regPushCallback(key, func) { this.pushCalls.set(key, func); } /** * 反注册push回调 * @param {*} key 用于反注册时候使用的key * @memberof ftWebsocketBase */ unregPushCallback(key) { this.pushCalls.delete(key) } /** * 关闭socket * * @memberof ftWebsocketBase */ close() { this.state.closing = true; this.killReconnectTimer(); if (this.websock) { this.websock.close() } this.rejectAll(); } rejectAll() { if (this.promisePool !== null && this.promisePool !== undefined) { for (var req in Object.values(this.promisePool)) { if (req != null) { if (req.reject != null) { req.reject('close'); } if (req.timer != null) { clearTimeout(req.timer); req.timer = null; } } } } this.promisePool = {}; } reconnect(timeout) { this.killReconnectTimer(); this.reconnectTimer = setTimeout(() => { if (this.websock == null || this.websock.readyState != WebSocket.OPEN) { this.websock = null this.initWebSocket(); this.reconnectTimer = null; } }, timeout); } isReadyConnect() { if (this.websock == null || this.websock.readyState != WebSocket.OPEN) { return false; } return this.state.login } killReconnectTimer() { if (this.reconnectTimer !== undefined && this.reconnectTimer !== null) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } } /** * 初始化websocket * * @param {Uint8Array} configBuffer 参考InitWebSocket.proto,非必填字段, * @memberof ftWebsocketBase * @example created() { this.$websocket.setWsConfig("127.0.0.1", 8888); this.$websocket.regPushCallback(this, this.onPush.bind(this)); //map结构,只要key唯一,允许反复注册,只有一次调用 this.$websocket.initWebSocket(); //如果连接断开,它会自动重连 }, destroyed() { this.$websocket.unregPushCallback(this); this.$websocket.close(); //断开websocket连接 }, */ initWebSocket(configBuffer) { if (this.websock != null) { console.debug("websock is not null"); //清理回调 this.websock.onmessage = null; this.websock.onopen = null; this.websock.onerror = null; this.websock.onclose = null; delete this.websock; } this.killReconnectTimer(); //定时器要停下 this.promisePool = {}; //上次的回调清空 this.state.closing = false; if (configBuffer !== null && configBuffer !== undefined && configBuffer instanceof Uint8Array) { this.initOpenDConfigBuff = configBuffer } this.websock = new WebSocket(this.wsuri); this.websock.binaryType = "arraybuffer"; this.websock.onmessage = (e) => { let msg = this.recvBuff(e.data) if (this.onmessage != undefined && typeof this.onmessage == 'function') { this.onmessage(msg); } }; this.websock.onopen = () => { //连接建立之后执行send方法发送数据 this.sendBuff(ftApiCmdID.Init, this.initOpenDConfigBuff, 20000) .then(response => { if (response.buff != null && response.error == 0) { //解包获取连接ID const initInfo = protoRoot.lookup("InitWebSocket.Response"); const buf = protobuf.util.newBuffer(response.buff); const initResult = initInfo.decode(buf); this.connID = initResult.s2c.connID; console.debug("登录成功"); if (!this.state.closing) { this.state.login = true; // 登录成功了回调一下 if (this.onlogin != undefined && typeof this.onlogin == 'function') { this.onlogin(true, response); } } } else { if (this.onlogin != undefined && typeof this.onlogin == 'function') { this.onlogin(false, response.error); } this.state.login = false; this.close(); //如果初始化失败,理解为用户环境配置用问题,不再尝试了 } }) .catch(error => { console.debug("login error:", error); if (this.onlogin != undefined && typeof this.onlogin == 'function') { this.onlogin(false, error); } this.state.login = false; this.close(); //如果初始化的时候20秒都没回应,理解为用户环境配置用问题,不再尝试了 }); }; this.websock.onerror = (e) => { //连接建立失败重连 this.message = "发生异常"; console.debug("发生异常", e); if (this.onerror != undefined && typeof this.onerror == 'function') { this.onerror(e); } // this.state.login = false; // this.reconnect(10000); }; this.websock.onclose = (e) => { //关闭 this.message = "断开连接"; console.debug("断开连接", e); this.rejectAll(); if (!this.state.closing) //非自主断开的,则重连上去 { this.reconnect(this.reconnectTimeout); } this.state.login = false; if (this.onclose != undefined && typeof this.onclose == 'function') { this.onclose(e); } }; return this; } } export default ftWebsocketBase