zby-live-sdk
Version:
This is a live SDK for weclassroom.
780 lines (735 loc) • 23.4 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';
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();