UNPKG

zby-live-sdk

Version:

This is a live SDK for weclassroom.

780 lines (735 loc) 23.4 kB
import Protocol from 'pomelo-protocol'; import Protobuf from 'pomelo-protobuf'; import LatestQueue from './latestQueue.js'; import util from './util'; import CHANNEL from '../index'; import defaultApi from '../../default/extend'; import dataReport from '../../network/dataReport'; import { pomeloBackupDomains } from '../../config/config.js'; var _protocol = Protocol; var _protobuf = Protobuf; var _package = Protocol.Package; var _message = Protocol.Message; var _JS_WS_CLIENT_TYPE = 'js-websocket'; var _JS_WS_CLIENT_VERSION = '1.0.0'; // 重连间隔时间 const reconnectInterval = 10 * 1000; class PomeloClient { constructor() { this.handshakeBuffer = { sys: { type: _JS_WS_CLIENT_TYPE, version: _JS_WS_CLIENT_VERSION }, user: {} }; this.pomeloData = {}; this.heartbeatInterval = 5000; this.heartbeatTimeout = this.heartbeatInterval * 2; this.nextHeartbeatTimeout = 0; this.heartbeatId = null; this.heartbeatTimeoutId = null; this.handshakeCallback = null; this.socket = null; this.reqId = 0; this.callBacks = {}; this.handlers = {}; this.routeMap = {}; this.initCallback = null; //自定义 this.pomeloConfig = {}; // 初始化参数 this.indexChannelInfo = {}; // 最后一条接收消息的uuid,用于消息恢复 this.lastReceiveMsgUuid = ''; // 是否正在获取缓存消息列表 this.isGettingCacheMsgList = false; // 重连定时器 this.reconnectTimer = null; // 是否在重连 this.isReconnectting = false; // 是否连接成功 this.isConnected = false; //是否在教室 this.isInRoom = false; // 是否链接成功过 this.hasConnected = false; // 当前域名重连次数 this.reconnectTimes = 0; this.latestQueueObj = new LatestQueue(); this.deleteQueueTimer = setInterval(() => { this.latestQueueObj.delete.call( this.latestQueueObj, 5000 * 4, 1000 ); }, 1000); this.failTimes = 0; this.retryTimesWhenHeartBeatFailed = 100; this.EventType = { SOCKET_IO_ERROR: 'SOCKET_IO_ERROR' }; } //初始化pomelo initPomelo(pomeloConfig, callBack) { this.pomeloConfig = pomeloConfig; let port = pomeloConfig.port ? `:${pomeloConfig.port}` : ''; var url = `wss://${pomeloConfig.host}${port}`; this.handshakeBuffer.user = pomeloConfig.user; // this.initCallback = callBack; // this.handshakeCallback = pomeloConfig.handshakeCallback; defaultApi.writeLog(`channel_log : ${url} ${JSON.stringify(pomeloConfig)} initPomelo ......`); //开始连接websocket this.initWebSocket(url, pomeloConfig.serverType); } joinRoom(pomeloConfig, callBack) { this.callback = callBack; this.isInRoom = true; pomeloConfig.port = '443'; this.indexChannelInfo = pomeloConfig; defaultApi.writeLog(`channel_log :${JSON.stringify(pomeloConfig)} the first step to join pomelo ......`); try { dataReport.pomeloJoinRoom({pomeloConfig}); } catch(e) { defaultApi.writeLog(`pomeloJoinRoom_error ${JSON.stringify(e)}`); } return this.connectGateServer(pomeloConfig); } initWebSocket(url, serverType) { var _this = this; defaultApi.writeLog(`channel_log : ${url} ${serverType} socket init......`); try { if (this.socket) { defaultApi.writeLog('channel_log : socket is closed......'); this.socket.close(); } } catch (e) { defaultApi.writeLog('pomelo socket close error'); } //initSocket const socket = new WebSocket(url); this.socket = socket; const wsTimeout = setTimeout(() => { defaultApi.writeLog('channel_log : socket is timeout...... ' + url); socket.close(); }, 10e3); try { dataReport.pomeloInitWebSocket({url}); } catch(e) { defaultApi.writeLog(`pomeloInitWebSocket_error ${JSON.stringify(e)}`); } defaultApi.writeLog('channel_log : new WebSocket......'); this.socket.binaryType = 'arraybuffer'; this.socket.onopen = evt => { clearTimeout(wsTimeout); _this.onSocketOpen(evt); defaultApi.writeLog(`channel_log : ${JSON.stringify(evt)} socket is onopen......`); }; this.socket.onmessage = evt => { _this.onSocketMessage(evt); }; this.socket.onerror = evt => { clearTimeout(wsTimeout); // new update start this.socket.onerror = null; this.socket.onclose = null; // new update end _this.onSocketError(evt); defaultApi.writeLog(`channel_log : ${JSON.stringify(evt)} socket is onerror......`); // 重连 if (serverType === 'gate') { this.getNextGateDomain(this.indexChannelInfo); this.initPomelo({ serverType: 'gate', host: this.indexChannelInfo.host, port: this.indexChannelInfo.port, log: true }); } else { this.reconnect(); } }; this.socket.onclose = evt => { clearTimeout(wsTimeout); // new update start this.socket.onerror = null; this.socket.onclose = null; // new update end defaultApi.writeLog(`channel_log : ${JSON.stringify(evt)} socket is onclose......`); if (serverType == 'chat') { // this.emit(SocketEventType.SOCKET_CLOSE, evt); defaultApi.writeLog(`[webSocket-state]:onClose---ErrorInfo: ${JSON.stringify(evt)}`); // 重连 this.reconnect(); if (this.heartbeatId) { clearTimeout(this.heartbeatId); this.heartbeatId = null; } if (this.heartbeatTimeoutId) { clearTimeout(this.heartbeatTimeoutId); this.heartbeatTimeoutId = null; } } }; defaultApi.writeLog(`===start-init-websocket${url}===`); } //onSocketOpen onSocketOpen(evt) { if (this.socket.readyState == 1) { var obj = _package.encode( _package.TYPE_HANDSHAKE, _protocol.strencode(JSON.stringify(this.handshakeBuffer)) ); this.send(obj); } } //request request(route, msg, cb) { msg = msg || {}; route = route || msg.route; if (!route) { defaultApi.writeLog('fail to send request without route.'); return; } this.reqId++; this.sendMessage(this.reqId, route, msg); this.callBacks[this.reqId] = cb; this.routeMap[this.reqId] = route; } notify(route, msg) { msg = msg || {}; this.sendMessage(0, route, msg); } //sendMessage sendMessage(reqId, route, msg) { var type = reqId ? _message.TYPE_REQUEST : _message.TYPE_NOTIFY; //compress message by protobuf var protos = this.pomeloData.protos ? this.pomeloData.protos.client : {}; if (protos[route]) { msg = _protobuf.encode(route, msg); } else { msg = _protocol.strencode(JSON.stringify(msg)); } var compressRoute = 0; msg = _message.encode(reqId, type, compressRoute, route, msg); var packet = _package.encode(_package.TYPE_DATA, msg); this.send(packet); } //onSocketMessage onSocketMessage(evt) { var msgData = _package.decode(evt.data); switch (msgData.type) { case _package.TYPE_HANDSHAKE: this.handshake(msgData.body); break; case _package.TYPE_HEARTBEAT: this.heartbeat(msgData.body); break; case _package.TYPE_DATA: // defaultApi.writeLog(`channel_log : ${JSON.stringify(msgData)} _package TYPE_DATA ......`); var msg = _message.decode(msgData.body); if (msg.id > 0) { msg.route = this.routeMap[msg.id]; delete this.routeMap[msg.id]; if (!msg.route) { return; } } msg.body = this.deCompose(msg); this.processMessage(msg); break; case _package.TYPE_KICK: defaultApi.writeLog('channel_log : _package TYPE_KICK ......'); // this.emit(SocketEventType.POMELO_MESSAGE_KICK); break; } if (this.heartbeatTimeout) { this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout; } } //onSocketError onSocketError(evt) { // this.emit(SocketEventType.SOCKET_IO_ERROR, evt); defaultApi.writeLog(`[webSocket-state]:onError---ErrorInfo: ${JSON.stringify(evt)}`); } //onSocketClose onSocketClose(evt) { // this.emit(SocketEventType.SOCKET_CLOSE, evt); defaultApi.writeLog(`[webSocket-state]:onClose---ErrorInfo: ${JSON.stringify(evt)}`); } //handshake handshake(data) { var _this = this; data = JSON.parse(_protocol.strdecode(data)); defaultApi.writeLog(`[websocket-handshake-code:${data.code}]`); // heartBeatConfig if (data.sys && data.sys.heartbeat) { this.heartbeatInterval = data.sys.heartbeat * 1000; // heartbeat interval this.heartbeatTimeout = this.heartbeatInterval * 2; // max heartbeat timeout } else { this.heartbeatInterval = 0; this.heartbeatTimeout = 0; } if (data.sys && data.sys['minHeartbeatInterval']) { this.minHeartbeatInterval = data.sys['minHeartbeatInterval'] * 1000; // heartbeat interval } else { this.minHeartbeatInterval = 3000; } if (data.sys && data.sys['retryTimesWhenHeartBeatFailed']) { this.retryTimesWhenHeartBeatFailed = data.sys['retryTimesWhenHeartBeatFailed']; } this.initPomeloData(data); // handshake-complete this.send(_package.encode(_package.TYPE_HANDSHAKE_ACK)); if (this.initCallback) { this.initCallback(this.socket); this.initCallback = null; } } //heartbeat async heartbeat(data) { var _this = this; defaultApi.writeLog('[websocket-heartbeat-state]:receive heartbeat...'); var obj = _package.encode(_package.TYPE_HEARTBEAT); if (this.failTimes) { this.failTimes = 0; console.warn('重新连上了'); defaultApi.writeLog('重新连上了'); } if (this.heartbeatTimeoutId) { clearTimeout(this.heartbeatTimeoutId); this.heartbeatTimeoutId = null; } if (this.heartbeatId) { // already in a heartbeat interval return; } this.heartbeatId = setTimeout(() => { this.heartbeatId = null; this.send(obj); defaultApi.writeLog('[pomeloclient.heartbeat]:send heartbeat...'); this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout; this.heartbeatTimeoutId = setTimeout(() => { this.heartbeatTimeoutCb(this); }, this.minHeartbeatInterval); }, this.heartbeatInterval); } //heartbeatTimeoutCb heartbeatTimeoutCb(that) { var self = this; defaultApi.writeLog('heartbeatTimeoutCb log !!!'); if (this.failTimes < this.retryTimesWhenHeartBeatFailed) { var obj = _package.encode(_package.TYPE_HEARTBEAT); self.send(obj); this.failTimes += 1; console.warn(`心跳重试第${this.failTimes}次`); defaultApi.writeLog(`心跳重试第${this.failTimes}次`); self.heartbeatTimeoutId = setTimeout( this.heartbeatTimeoutCb.bind(self), this.minHeartbeatInterval ); } else { // update start // 超时前先断开监听事件 self.socket.onmessage = null; self.socket.onerror = null; self.socket.onclose = null; // update end console.error('server heartbeat timeout'); defaultApi.writeLog('server heartbeat timeout'); // 重连 this.reconnect(); // update start // this.disconnect(true); // update end } } //processMessage processMessage(msg) { var _this = this; // eslint-disable-next-line no-extra-boolean-cast if (!!msg.body.msgId) { var replyRoute = 'connector.' + msg.route + '.ack'; var replyMsg = { msgId: msg.body.msgId, ack: 1 }; this.notify(replyRoute, replyMsg); } if (!msg || !msg.id) { switch (msg.route) { case 'onChat': break; case 'onServer': try { const onServerMsg = JSON.parse(msg.body.msg); this.dealOnServerMsg(onServerMsg, msg.body.from); } catch (error) { } break; case 'onAdd': break; case 'onLeave': break; case 'onNotice': break; case 'onKick': this.disconnect(true); break; default: } return; } var cb = this.callBacks[msg.id]; delete this.callBacks[msg.id]; if (typeof cb !== 'function') { return; } cb(msg.body); return; } //deCompose deCompose(msg) { var protos = this.pomeloData.protos ? this.pomeloData.protos.server : {}; var abbrs = this.pomeloData.abbrs; var route = msg.route; try { //Decompose route from dict if (msg.compressRoute) { if (!abbrs[route]) { console.error('illegal msg!'); defaultApi.writeLog('illegal msg!'); return {}; } route = msg.route = abbrs[route]; } if (protos[route]) { return _protobuf.decode(route, msg.body); } return JSON.parse(_protocol.strdecode(msg.body)); } catch (ex) { console.error('route, body = ' + route + ', ' + msg.body); } return msg; } //handshakeInit initPomeloData(data) { if (!data || !data.sys) { return; } this.pomeloData = this.pomeloData || {}; var dict = data.sys.dict; var protos = data.sys.protos; //Init compress dict if (dict) { this.pomeloData.dict = dict; this.pomeloData.abbrs = {}; for (var route in dict) { this.pomeloData.abbrs[dict[route]] = route; } } //Init protobuf protos if (protos) { this.pomeloData.protos = { server: protos.server || {}, client: protos.client || {} }; if (_protobuf) { _protobuf.init({ encoderProtos: protos.client, decoderProtos: protos.server }); } } } //disconnect disconnect(isSelf) { if (this.socket) { this.socket.onmessage = null; this.socket.onerror = null; this.socket.onclose = null; this.isGettingCacheMsgList = false; this.isConnected = false; if (!isSelf) { this.hasConnected = false; this.isInRoom = false; if (this.reconnectTimer) { clearInterval(this.reconnectTimer); this.reconnectTimer = null; } } if (this.socket.disconnect) { this.socket.disconnect(); } if (this.socket.close) { this.socket.close(); } defaultApi.writeLog('[webSocket-disconnect]'); } if (this.heartbeatId) { clearTimeout(this.heartbeatId); this.heartbeatId = null; } if (this.heartbeatTimeoutId) { clearTimeout(this.heartbeatTimeoutId); this.heartbeatTimeoutId = null; } if (this.deleteQueueTimer) { clearInterval(this.deleteQueueTimer); this.deleteQueueTimer = null; } } //send send(packet) { if (this.socket) { if (this.socket.readyState == 2 || this.socket.readyState == 3) { this.disconnect(true); return; } this.socket.send(packet.buffer || packet, { binary: true, mask: true }); } } //发送消息 sendChannelMessage(msgData, target = '*') { return new Promise((resolve, reject) => { this.request( 'chat.chatHandler.send', { // rid: this.indexChannelInfo.channelID, content: JSON.stringify(msgData), from: this.indexChannelInfo.userID, target: target || '*', route: 'onServer' }, data => { if (data.error) { reject({code: -1}); } resolve({code: 200}); } ); }); } getNextGateDomain(currentChannelInfo) { if (pomeloBackupDomains.length) { pomeloBackupDomains.push({host: currentChannelInfo.host, port: currentChannelInfo.port}); const domain = pomeloBackupDomains.shift(); currentChannelInfo.host = domain.host; currentChannelInfo.port = domain.port; } return currentChannelInfo; } //连接gate服务器,获取chat连接地址 async connectGateServer(channelInfo) { if (this.isReconnectting && pomeloBackupDomains.length) { channelInfo = this.getNextGateDomain(channelInfo); } await new Promise((resolve, reject) => { this.initPomelo({ serverType: 'gate', host: channelInfo.host, port: channelInfo.port, log: true }); this.initCallback = resolve.bind(this, true); }); defaultApi.writeLog('channel_log : connectGateServer,the second step to join pomelo ......' + channelInfo.host); const queryData = { uid: channelInfo.userId, rtype: 4, utype: 2, retrytime: new Date().getTime(), protocolVersion: '1.0', // uniqId: channelInfo.guid }; console.log('gate', queryData, channelInfo); const _channelInfo = Object.assign({}, channelInfo); await new Promise((resolve, reject) => { this.request('gate.gateHandler.queryEntry', queryData, data => { try { this.socket.close(); defaultApi.writeLog('channel_log :gate socket close......'); } catch (error) { // } this.socket = null; if (data.code === 500) { defaultApi.writeLog(`channel_log : ${data.code} request-gateServerFailed......`); reject('gateServerFailed'); } _channelInfo.host = data.host; _channelInfo.port = data.port; defaultApi.writeLog(`channel_log : ${data.code} request-gateServerSuccess ......`); try { dataReport.pomeloInitGate({gateRes:data}); } catch(e) { console.log('pomeloInitGate_error',e); } resolve('gateServerSuccess'); }); }); return this.connectChatServer(_channelInfo); } // 重连chat服务器 reconnectChatServer(enterData) { this.request('connector.entryHandler.enter', enterData, data => { if (data.code === 500) { this.reconnectChatServer(enterData); return; } resolve(data); }); } // 连接chat服务器 async connectChatServer(channelInfo) { defaultApi.writeLog('channel_log : connectChatServer,the third step to join pomelo ......'); await new Promise((resolve, reject) => { this.initPomelo({ serverType: 'chat', host: channelInfo.host, port: channelInfo.port, log: true }); this.initCallback = resolve.bind(this, true); }); const enterData = { uid: channelInfo.userId, rid: channelInfo.roomId, rtype: 4, username: channelInfo.userId, uname: channelInfo.userName, // ulevel: userInfo.level, //用户等级x role: channelInfo.role, classid: channelInfo.roomId, protocolVersion: '1.0', uniqId: channelInfo.guid }; return new Promise((resolve, reject) => { this.request( 'connector.entryHandler.enter', enterData, data => { if (data.code === 500) { reject(data); defaultApi.writeLog(`channel_log : ${data.code} request-chatServerFailed......`); return; } this.dealConnectSuccess(); try { dataReport.pomeloInitChat({chatRes:data}); } catch(e) { defaultApi.writeLog(`pomeloInitChat_error ${JSON.stringify(e)}`, null, 'error'); } resolve(data); defaultApi.writeLog(`channel_log : ${JSON.stringify(data)} request-chatServerSuccess......`); } ); }); } // 处理连接成功 async dealConnectSuccess() { // 获取历史列表 if (this.lastReceiveMsgUuid) { try { this.isGettingCacheMsgList = true; defaultApi.writeLog('msgRecoverStream--start'); const res = JSON.parse(util.unzip(await this.msgRecoverStream())); defaultApi.writeLog(`msgRecoverStream--end ${JSON.stringify(res)}`); if (res.code === 200) { const recoverMsgList = res.result.cacheList; recoverMsgList.forEach(msg => { const _msg = JSON.parse(msg); this.dealOnServerMsg(_msg, _msg.from); }); this.isGettingCacheMsgList = false; } } catch (error) { } } if (this.hasConnected) { // 处理公共重进逻辑 CHANNEL.reJoin(); } if (this.isReconnectting) { dataReport.pomeloState({state: 3}); } else { dataReport.pomeloState({state: 1}); } this.hasConnected = true; clearInterval(this.reconnectTimer); this.reconnectTimer = null; this.isReconnectting = false; this.isConnected = true; this.reconnectTimes = 0; } // 消息恢复 msgRecoverStream() { return new Promise((resolve, reject) => { const { userId, roomId } = this.indexChannelInfo; this.request( 'recover.recoverHandler.msgRecoverStream', { stuId: userId, rid: roomId, uuid: this.lastReceiveMsgUuid }, data => { if (data.code === 500) { reject(data); return; } resolve(data); } ); }); } // 重连 reconnect() { defaultApi.writeLog('pomelo reconnect...'); if (this.isReconnectting) { return; } if (this.socket) { // 创建实例前断开 this.disconnect(true); this.socket = null; } this.isReconnectting = true; if (!this.isConnected && this.isInRoom) { this.connectGateServer(this.indexChannelInfo).catch((err) => { dataReport.pomeloState({state: 4, desc: String(err)}); }); } this.reconnectTimer = setInterval(() => { if (!this.isConnected && this.isInRoom) { this.connectGateServer(this.indexChannelInfo).catch((err) => { dataReport.pomeloState({state: 4, desc: String(err)}); }); } }, reconnectInterval); } // 处理onserver消息 dealOnServerMsg(onServerMsg, from) { if (from === 'sdkset') { this.callback(onServerMsg); } else if (from !== this.indexChannelInfo.userId) { if (this.latestQueueObj.isRepeat(onServerMsg.uuid)) { return; } this.latestQueueObj.insert(onServerMsg.uuid, Date.now()); if (!this.isGettingCacheMsgList) { this.lastReceiveMsgUuid = onServerMsg.uuid; } this.callback(onServerMsg); } } } export default new PomeloClient();