UNPKG

zby-live-sdk

Version:

This is a live SDK for weclassroom.

417 lines (366 loc) 11.5 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'; import { EventEmitter } from 'eventemitter3'; const _JS_WS_CLIENT_TYPE = 'js-websocket'; const _JS_WS_CLIENT_VERSION = '1.0.0'; export class BaseClient extends EventEmitter { constructor() { this.handshakeBuffer = { sys: { type: _JS_WS_CLIENT_TYPE, version: _JS_WS_CLIENT_VERSION }, user: {} }; this.socket = null; this.heartbeatInterval = 5000; this.heartbeatTimeout = this.heartbeatInterval * 2; this.heartbeatId = null; this.heartbeatTimeoutId = null; this.isConnected = false; this.reconnectTimer = null; this.reconnectInterval = 10000; this.failTimes = 0; this.retryTimesWhenHeartBeatFailed = 3; } // 通用连接逻辑 connect(url) { return new Promise((resolve, reject) => { this.socket = new WebSocket(url); this.socket.binaryType = 'arraybuffer'; const timeout = new setTimeout(() => { this.log('socket is timeout...... ' + url); this.socket.close(); }, 10e3); this.socket.onopen = () => { this.log('socket is open...... ' + url); clearTimeout(timeout); this.handleOpen(); resolve(); }; this.socket.onmessage = (evt) => this.handleMessage(evt); this.socket.onerror = (err) => { this.isConnected = false; this.handleError(err); reject(err); }; this.socket.onclose = () => { this.isConnected = false; this.handleClose(); }; }); } send(packet) { if (this.socket) { if (this.socket.readyState === WebSocket.CLOSED || this.socket.readyState === WebSocket.CLOSING ) { this.disconnect(true); return; } this.socket.send(packet.buffer || packet, { binary: true, mask: true }); } } request(route, msg, cb) { msg = msg || {}; route = route || msg.route; if (!route) { this.log('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(reqId, route, msg) { const type = reqId ? Protocol.Message.TYPE_REQUEST : Protocol.Message.TYPE_NOTIFY; const protos = this.pomeloData.protos ? this.pomeloData.protos.client : {}; if (protos[route]) { msg = Protobuf.encode(route, msg); } else { msg = Protocol.strencode(JSON.stringify(msg)); } msg = Protocol.Message.encode(reqId, type, compressRoute, route, msg); packet = Protocol.Package.encode(Protocol.Package.TYPE_DATA, msg); this.send(packet); } // 通用消息处理 handleMessage(evt) { const data = Protocol.Package.decode(evt.data); this.log('Received message:' + JSON.stringify(data)); switch (data.type) { case Protocol.Package.TYPE_HEARTBEAT: this.handleHeartbeatMsg(); break; case Protocol.Package.TYPE_HANDSHAKE: this.handleHandshake(data.body); break; case Protocol.Package.TYPE_DATA: this.handleData(data); break; case Protocol.Package.TYPE_CLOSE: this.handleClose(); break; case Protocol.Package.TYPE_KICK: this.handleKick(data); break; default: this.log('Unknown message type: ' + data.type); } } handleOpen() { this.log('WebSocket connection opened'); this.isConnected = true; if (this.socket.readyState === WebSocket.OPEN) { const data = Protocol.Package.encode( Protocol.Package.TYPE_HANDSHAKE, Protocol.strencode(JSON.stringify(this.handshakeBuffer)) ); this.send(data); } } // 通用错误处理 handleError(err) { console.error('WebSocket error:', err); this.reconnect(); } // 通用关闭处理 handleClose() { console.warn('WebSocket closed'); this.reconnect(); } handleData(data) { const msg = Protocol.Message.decode(data.body); if (msg.id > 0) { msg.route = this.routeMap[msg.id]; delete this.routeMap[msg.id]; if (!msg.route) { this.log('Unknown route: ' + msg.route); return; } } msg.body = this.deCompose(msg); if (msg.body.msgId) { const replyRoute = `connector.${msg.route}.ack`; const 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) { this.log('onServer msg error: ' + error); } break; case 'onAdd': break; case 'onLeave': break; case 'onNotice': break; case 'onKick': this.disconnect(); break; default: } return; } const cb = this.callBacks[msg.id]; delete this.callBacks[msg.id]; if (!cb || typeof cb !== 'function') { this.log('Unknown callback for message id: ' + msg.id); return; } cb(msg.body); } handleHandshake(data) { data = JSON.parse(Protocol.strdecode(data)); this.log('Handshake response: ' + JSON.stringify(data)); 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.emit('handshake', data); this.send(Protocol.Package.encode(Protocol.Package.TYPE_HANDSHAKE_ACK)); } handleHeartbeatMsg() { this.log('[websocket-heartbeat-state]:receive heartbeat.'); const data = Protocol.Package.encode(Protocol.Package.TYPE_HEARTBEAT); if (this.failTimes) { this.failTimes = 0; this.log('[websocket-heartbeat-state]:heartbeat received again.'); } if (this.heartbeatTimeoutId) { clearTimeout(this.heartbeatTimeoutId); this.heartbeatTimeoutId = null; } if (this.heartbeatId) { return; } this.heartbeatId = setTimeout(() => { this.log('[websocket-heartbeat-state]:send heartbeat.'); this.heartbeatId = null; this.send(data); this.heartbeatTimeoutId = setTimeout(() => { this.heartbeatTimeoutCb(); }, this.minHeartbeatInterval); }, this.heartbeatInterval); } heartbeatTimeoutCb() { this.log('[websocket-heartbeat-state]:heartbeat timeout.'); this.failTimes++; if (this.failTimes < this.retryTimesWhenHeartBeatFailed) { const data = Protocol.Package.encode(Protocol.Package.TYPE_HEARTBEAT); this.send(data); this.failTimes += 1; this.log(`[websocket-heartbeat-state]:heartbeat retry ${this.failTimes} times.`); this.heartbeatTimeoutId = setTimeout(() => { this.heartbeatTimeoutCb(); }, this.minHeartbeatInterval); } else { this.socket.onclose = null; this.socket.onerror = null; this.socket.onmessage = null; this.log('[websocket-heartbeat-state]:server heartbeat failed.'); this.emit('heartbeatTimeout'); } } // 断开连接 disconnect() { if (this.socket) { this.socket.close(); this.socket = null; } 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; } } 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]) { this.log('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) { this.log('route, body = ' + route + ', ' + msg.body + ', ' + ex); } return msg; } log(msg) { defaultApi.writeLog(msg, 'channel-pomelo'); } } export class GateClient extends BaseClient { constructor() { super(); } // Gate 特定逻辑:连接 Gate 服务器并获取 Chat 地址 async connectServer(channelInfo) { const url = `wss://${channelInfo.host}:${channelInfo.port}`; await this.connect(url); console.log('Connected to Gate server'); } // 请求 Chat 服务器地址 requestChatServerAddress(channelInfo) { const query = { uid: channelInfo.userId, rtype: 4, utype: 2, retrytime: new Date().getTime(), protocolVersion: '1.0', }; return new Promise((resolve, reject) => { this.request('gate.gateHandler.queryEntry', query, data => { try { this.socket.close(); defaultApi.writeLog('channel_log :gate socket close......'); } catch (e) { } if (data.code === 500) { defaultApi.writeLog(`channel_log : ${data.code} request-gateServerFailed......`); reject('gateServerFailed'); } }); this.socket.onmessage = (evt) => { const data = JSON.parse(evt.data); if (data.code === 200) { resolve(data.chatServerInfo); } else { reject(data); } }; }); } } export class ChatClient extends BaseClient { constructor() { super(); } // Chat 特定逻辑:连接 Chat 服务器并处理消息 async connectServer(chatServerInfo) { const url = `wss://${chatServerInfo.host}:${chatServerInfo.port}`; await this.connect(url); console.log('Connected to Chat server'); } // 处理聊天消息 handleMessage(evt) { const data = JSON.parse(evt.data); if (data.type === 'chat') { console.log('Chat message received:', data.message); } else { super.handleMessage(evt); // 调用基类的通用处理逻辑 } } }