zby-live-sdk
Version:
This is a live SDK for weclassroom.
417 lines (366 loc) • 11.5 kB
JavaScript
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); // 调用基类的通用处理逻辑
}
}
}