occaecatidicta
Version:
476 lines (376 loc) • 13.6 kB
text/typescript
import * as WebSocket from 'ws';
import {Package, Message, Protocol} from 'omelox-protocol';
import {Protobuf} from 'omelox-protobuf';
import {EventEmitter} from 'events';
import {cacheClass} from "./cacheClass";
import { MyLogger } from "./my.logger";
const JS_WS_CLIENT_TYPE = 'js-ws';
const JS_WS_CLIENT_VERSION = '0.0.1';
const RES_OK = 200;
const RES_OLD_CLIENT = 501;
const CODE_DICT_ERROR = 502;
const CODE_PROTOS_ERROR = 503;
export interface IPomeloInterface {
on(event: 'close', cb): any;
on(event: 'io-error', cb): any;
on(event: 'error', cb): any;
on(event: 'heartbeat timeout', cb): any;
on(event: 'onKick', cb): any;
initAsync(params): Promise<any>;
init(params, cb);
disconnect();
request(route, msg): Promise<any>;
notify(route, msg);
}
export class Pomelo extends EventEmitter implements IPomeloInterface {
socket = null;
reqId = 0;
callbacks = {};
handlers = {};
routeMap = {};
protobuf: Protobuf = null;
heartbeatInterval = 5000;
heartbeatTimeout = this.heartbeatInterval * 2;
nextHeartbeatTimeout = 0;
gapThreshold = 100; // heartbeat gap threshold
heartbeatId = null;
heartbeatTimeoutId = null;
handshakeCallback = null;
logger: MyLogger;
handshakeBuffer = {
'sys': {
type: JS_WS_CLIENT_TYPE,
version: JS_WS_CLIENT_VERSION,
dictVersion: '' as any,
protoVersion: '' as any
},
'user': {}
};
initCallback = null;
params = null;
data: { dict: any, abbrs: any, protos: any } = {} as any;
sysCache: { dictVersion: string, protoVersion: string, dict: any, protos: any } = null;
static ClientId = 0;
constructor(useNestLogger = true, private readonly showPackageLog: boolean = true) {
super();
this.handlers[Package.TYPE_HANDSHAKE] = this.handshake.bind(this);
this.handlers[Package.TYPE_HEARTBEAT] = this.heartbeat.bind(this);
this.handlers[Package.TYPE_DATA] = this.onData.bind(this);
this.handlers[Package.TYPE_KICK] = this.onKick.bind(this);
if (useNestLogger) {
this.logger = new MyLogger('wsclient-' + ++Pomelo.ClientId);
} else {
this.logger = console as any;
}
}
initAsync(params): Promise<any> {
return new Promise(resolve => {
this.params = params;
params.debug = true;
this.initCallback = resolve;
const host = params.host;
const port = params.port;
let url = host;
if (port) {
url += ':' + port;
}
this.sysCache = cacheClass.getCache() || {} as any;
this.handshakeBuffer.sys.dictVersion = this.sysCache.dictVersion || 0;
this.handshakeBuffer.sys.protoVersion = this.sysCache.protoVersion || 0;
if (!params.type) {
this.logger.log('init websocket');
this.handshakeBuffer.user = params.user;
this.handshakeCallback = params.handshakeCallback;
this.initWebSocket(url, resolve);
}
})
}
init(params, cb) {
this.params = params;
params.debug = true;
this.initCallback = cb;
const host = params.host;
const port = params.port;
let url = 'ws://' + host;
if (port) {
url += ':' + port;
}
this.sysCache = cacheClass.getCache() || {} as any;
this.handshakeBuffer.sys.dictVersion = this.sysCache.dictVersion || 0;
this.handshakeBuffer.sys.protoVersion = this.sysCache.protoVersion || 0;
if (!params.type) {
this.logger.log('init websocket');
this.handshakeBuffer.user = params.user;
this.handshakeCallback = params.handshakeCallback;
this.initWebSocket(url, cb);
}
};
private initWebSocket(url, cb) {
this.logger.log(url);
const onopen = (event) => {
this.logger.log('[pomeloclient.init] websocket connected!');
const obj = Package.encode(Package.TYPE_HANDSHAKE, Protocol.strencode(JSON.stringify(this.handshakeBuffer)));
this.send(obj);
};
const onmessage = (event) => {
if (this.showPackageLog && event.data.byteLength != 4) {
this.logger.log('recv orgdata', event.data.byteLength, Buffer.from(event.data).toString('hex'));
}
this.processPackage(Package.decode(event.data));//, cb);
// new package arrived, update the heartbeat timeout
if (this.heartbeatTimeout) {
this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout;
}
};
const onerror = (event) => {
this.emit('io-error', event);
this.logger.log('socket error %j ', event);
};
const onclose = (event) => {
this.emit('close', event);
this.logger.log('socket close %j ', event);
};
this.socket = new WebSocket(url);
this.socket.binaryType = 'arraybuffer';
this.socket.onopen = onopen;
this.socket.onmessage = onmessage;
this.socket.onerror = onerror;
this.socket.onclose = onclose;
};
disconnect() {
if (this.socket) {
if (this.socket.disconnect) this.socket.disconnect();
if (this.socket.close) this.socket.close();
this.logger.log('disconnect');
this.socket = null;
}
if (this.heartbeatId) {
clearTimeout(this.heartbeatId);
this.heartbeatId = null;
}
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
}
};
request(route, msg): Promise<any> {
return new Promise((resolve, reject) => {
msg = msg || {};
route = route || msg.route;
if (!route) {
this.logger.log('fail to send request without route.');
return;
}
this.reqId++;
this.sendMessage(this.reqId, route, msg);
this.callbacks[this.reqId] = resolve;
this.routeMap[this.reqId] = route;
})
};
notify(route, msg) {
msg = msg || {};
this.sendMessage(0, route, msg);
};
private sendMessage(reqId, route, msg) {
const type = reqId ? Message.TYPE_REQUEST : Message.TYPE_NOTIFY;
if (this.showPackageLog) {
this.logger.log('send', reqId, route, msg);
}
//compress message by protobuf
const protos = !!this.data.protos ? this.data.protos.client : {};
if (!!protos[route]) {
msg = this.protobuf.encode(route, msg);
} else {
msg = Protocol.strencode(JSON.stringify(msg));
}
let compressRoute = false;
if (this.data.dict && this.data.dict[route]) {
route = this.data.dict[route];
compressRoute = true;
}
msg = Message.encode(reqId, type, compressRoute, route, msg);
const packet = Package.encode(Package.TYPE_DATA, msg);
if (this.showPackageLog) {
this.logger.log('send', "packet", packet.length, packet.toString('hex'));
}
this.send(packet);
};
private send(packet) {
if (!!this.socket) {
this.socket.send(packet.buffer || packet, {binary: true, mask: true});
}
};
private heartbeat(data) {
const obj = Package.encode(Package.TYPE_HEARTBEAT);
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);
this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout;
this.heartbeatTimeoutId = setTimeout(this.heartbeatTimeoutCb.bind(this), this.heartbeatTimeout);
}, this.heartbeatInterval);
};
private heartbeatTimeoutCb() {
const gap = this.nextHeartbeatTimeout - Date.now();
if (gap > this.gapThreshold) {
this.heartbeatTimeoutId = setTimeout(this.heartbeatTimeoutCb.bind(this), gap);
} else {
this.logger.error('server heartbeat timeout');
this.emit('heartbeat timeout');
this.disconnect();
}
};
private handshake(data) {
data = JSON.parse(Protocol.strdecode(data));
if (data.code === RES_OLD_CLIENT) {
this.emit('error', 'client version not fullfill');
return;
}
if (data.code !== RES_OK) {
this.emit('error', 'handshake fail');
return;
}
this.handshakeInit(data);
const obj = Package.encode(Package.TYPE_HANDSHAKE_ACK);
this.send(obj);
if (this.initCallback) {
this.initCallback(this.socket);
this.initCallback = null;
}
};
private onData(data) {
//probuff decode
const msg = Message.decode(data);
if (msg.id > 0) {
msg.route = this.routeMap[msg.id];
delete this.routeMap[msg.id];
if (!msg.route) {
return;
}
}
msg.body = this.deCompose(msg);
if (this.showPackageLog) {
this.logger.log('recv', JSON.stringify(msg), "\n\tpacket", data.length, data.toString('hex'));
}
this.processMessage(msg);
};
private onKick(data) {
this.emit('onKick', data.toString());
};
private processPackage(msg) {
if (Array.isArray(msg)) {
for (let m of msg) {
this.handlers[m.type](m.body);
}
} else {
this.handlers[msg.type](msg.body);
}
};
private processMessage(msg) {
if (!msg || !msg.id) {
// server push message
// this.logger.error('processMessage error!!!');
this.emit(msg.route, msg.body);
return;
}
//if have a id then find the callback function with the request
const cb = this.callbacks[msg.id];
delete this.callbacks[msg.id];
if (typeof cb !== 'function') {
return;
}
cb(msg.body);
return;
};
private processMessageBatch(pomelo, msgs) {
for (let i = 0, l = msgs.length; i < l; i++) {
this.processMessage(msgs[i]);
}
};
private deCompose(msg) {
const protos = !!this.data.protos ? this.data.protos.server : {};
const abbrs = this.data.abbrs;
let route = msg.route;
try {
//Decompose route from dict
if (msg.compressRoute) {
if (!abbrs[route]) {
this.logger.error('illegal msg!');
return {};
}
route = msg.route = abbrs[route];
}
if (!!protos[route]) {
return this.protobuf.decode(route, msg.body);
} else {
return JSON.parse(Protocol.strdecode(msg.body));
}
} catch (ex) {
this.logger.error('route, body = ' + route + ", " + msg.body);
}
return msg;
};
private handshakeInit(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;
}
this.initData(data);
if (typeof this.handshakeCallback === 'function') {
this.handshakeCallback(data.user);
}
};
//Initilize data used in pomelo client
private initData(data) {
if (!data || !data.sys) {
return;
}
const dictVersion = data.sys.dictVersion;
const protoVersion = data.sys.protos ? data.sys.protos.version : null;
let changed = false;
const dict = data.sys.dict || this.sysCache.dict;
const protos = data.sys.protos || this.sysCache.protos;
if (dictVersion) {
this.sysCache.dict = dict;
this.sysCache.dictVersion = dictVersion;
changed = true;
}
if (protoVersion) {
this.sysCache.protos = protos;
this.sysCache.protoVersion = protoVersion;
changed = true;
}
if (changed) {
cacheClass.saveCache(this.sysCache);
}
//Init compress dict
if (!!dict) {
this.data.dict = dict;
this.data.abbrs = {};
for (const route in dict) {
this.data.abbrs[dict[route]] = route;
}
}
//Init protobuf protos
if (!!protos) {
this.data.protos = {
server: protos.server || {},
client: protos.client || {}
};
if (!this.protobuf) {
// 要改WEB JS客户端的话 这里可能需要改一下。
this.protobuf = new Protobuf({encoderProtos: protos.client, decoderProtos: protos.server});
}
}
};
}