pomelo-client-node-ws
Version:
pomelo client written in node.js based ws
478 lines (406 loc) • 12.4 kB
JavaScript
const JS_WS_CLIENT_TYPE = 'js-websocket'
const JS_WS_CLIENT_VERSION = '0.0.1'
const WebSocket = require('ws')
const Protocol = require('@yangfch3/pomelo-protocol')
const Protobuf = require('./lib/pomelo-protobuf/protobuf')
const decodeIO_protobuf = require('pomelo-decodeio-protobuf')
let decodeIO_encoder = null
let decodeIO_decoder = null
const Package = Protocol.Package
const Message = Protocol.Message
const EventEmitter = require('events')
const rsa = require('./lib/rsasign/rsa')
const localStorage = require('store')
const RES_OK = 200
const RES_FAIL = 500
const RES_OLD_CLIENT = 501
const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10
class Pomelo extends EventEmitter {
constructor () {
super()
this.protobuf = new Protobuf()
this.socket = null
this.reqId = 0
this.callbacks = {}
this.handlers = {}
//Map from request id to route
this.routeMap = {}
this.dict = {} // route string to code
this.abbrs = {} // code to route string
this.serverProtos = {}
this.clientProtos = {}
this.protoVersion = 0
this.heartbeatInterval = 0
this.heartbeatTimeout = 0
this.nextHeartbeatTimeout = 0
this.gapThreshold = 100 // heartbeat gap threashold
this.heartbeatId = null
this.heartbeatTimeoutId = null
this.handshakeCallback = null
this.decode = null
this.encode = null
this.reconnect = false
this.reconncetTimer = null
this.reconnectUrl = null
this.reconnectAttempts = 0
this.reconnectionDelay = 5000
this.maxReconnectAttempts = DEFAULT_MAX_RECONNECT_ATTEMPTS
this.useCrypto = false
this.handshakeBuffer = {
'sys': {
type: JS_WS_CLIENT_TYPE,
version: JS_WS_CLIENT_VERSION
},
'user': {}
}
this.initCallback = null
}
init (params, cb) {
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)
this.initCallback = cb
let host = params.host
let port = params.port
let scheme = params.scheme || 'ws'
this.encode = params.encode || this.defaultEncode
this.decode = params.decode || this.defaultDecode
let url = scheme + '://' + host
if (port) {
url += ':' + port
}
this.handshakeBuffer.user = params.user
if (params.encrypt) {
this.useCrypto = true
rsa.generate(1024, '10001')
this.handshakeBuffer.sys.rsa = {
rsa_n: rsa.n.toString(16),
rsa_e: rsa.e
}
}
this.handshakeCallback = params.handshakeCallback
this.connect(params, url, cb)
}
connect (params, url, cb) {
let self = this
params = params || {}
this.maxReconnectAttempts = params.maxReconnectAttempts || DEFAULT_MAX_RECONNECT_ATTEMPTS
this.reconnectUrl = url
//Add protobuf version
if (localStorage.get('protos') && this.protoVersion === 0) {
let protos = JSON.parse(localStorage.get('protos'))
this.protoVersion = protos.version || 0
this.serverProtos = protos.server || {}
this.clientProtos = protos.client || {}
if (this.protobuf) {
this.protobuf.init({
encoderProtos: this.clientProtos,
decoderProtos: this.serverProtos
})
}
if (decodeIO_protobuf) {
decodeIO_encoder = decodeIO_protobuf.loadJson(this.clientProtos)
decodeIO_decoder = decodeIO_protobuf.loadJson(this.serverProtos)
}
}
//Set protoversion
this.handshakeBuffer.sys.protoVersion = this.protoVersion
let onopen = function () {
if (self.reconnect) {
self.emit('reconnect')
} else {
self.emit('connected')
}
self.reset()
let obj = Package.encode(Package.TYPE_HANDSHAKE, Protocol.strencode(JSON.stringify(self.handshakeBuffer)))
self.send(obj)
}
let onmessage = function (event) {
self.processPackage(Package.decode(event.data), cb)
// new package arrived, update the heartbeat timeout
if (self.heartbeatTimeout) {
self.nextHeartbeatTimeout = Date.now() + self.heartbeatTimeout
}
}
let onerror = function (event) {
self.emit('io-error', event)
console.error('socket error: ', event)
}
let onclose = function (event) {
self.emit('close', event)
self.emit('disconnect', event)
console.warn('socket close: ', event.target.url, event)
if (params.reconnect && self.reconnectAttempts < self.maxReconnectAttempts) {
self.reconnect = true
self.reconnectAttempts++
self.reconncetTimer = setTimeout(function () {
self.connect(params, self.reconnectUrl, cb)
}, self.reconnectionDelay)
self.reconnectionDelay *= 2
}
}
self.socket = new WebSocket(url)
console.log('connect to ' + url)
self.socket.binaryType = 'arraybuffer'
self.socket.onopen = onopen.bind(this)
self.socket.onmessage = onmessage.bind(this)
self.socket.onerror = onerror.bind(this)
self.socket.onclose = onclose.bind(this)
}
defaultDecode (data) {
//probuff decode
let 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)
return msg
}
defaultEncode (reqId, route, msg) {
let type = reqId ? Message.TYPE_REQUEST : Message.TYPE_NOTIFY
//compress message by protobuf
if (this.protobuf && this.clientProtos[route]) {
msg = this.protobuf.encode(route, msg)
} else if (decodeIO_encoder && decodeIO_encoder.lookup(route)) {
let Builder = decodeIO_encoder.build(route)
msg = new Builder(msg).encodeNB()
} else {
msg = Protocol.strencode(JSON.stringify(msg))
}
let compressRoute = 0
if (this.dict && this.dict[route]) {
route = this.dict[route]
compressRoute = 1
}
return Message.encode(reqId, type, compressRoute, route, msg)
}
disconnect () {
if (this.socket) {
if (this.socket.disconnect) this.socket.disconnect()
if (this.socket.close) this.socket.close()
console.log('disconnect')
this.socket = null
}
if (this.heartbeatId) {
clearTimeout(this.heartbeatId)
this.heartbeatId = null
}
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId)
this.heartbeatTimeoutId = null
}
}
reset () {
this.reconnect = false
this.reconnectionDelay = 1000 * 5
this.reconnectAttempts = 0
clearTimeout(this.reconncetTimer)
}
request (route, msg, cb) {
if (arguments.length === 2 && typeof msg === 'function') {
cb = msg
msg = {}
} else {
msg = msg || {}
}
route = route || msg.route
if (!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) {
if (this.useCrypto) {
msg = JSON.stringify(msg)
let sig = rsa.signString(msg, 'sha256')
msg = JSON.parse(msg)
msg['__crypto__'] = sig
}
if (this.encode) {
msg = this.encode(reqId, route, msg)
}
let packet = Package.encode(Package.TYPE_DATA, msg)
this.send(packet)
}
send (packet) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(packet)
} else {
console.warn('socket is not open: readyState ' + (this.socket ? this.socket.readyState : -1))
}
}
heartbeat () {
let self = this
if (!this.heartbeatInterval) {
// no heartbeat
return
}
let 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(function () {
self.heartbeatId = null
self.send(obj)
self.nextHeartbeatTimeout = Date.now() + self.heartbeatTimeout
self.heartbeatTimeoutId = setTimeout(function () {
self.heartbeatTimeoutCb()
}, self.heartbeatTimeout)
}, this.heartbeatInterval)
}
heartbeatTimeoutCb () {
let self = this
let gap = this.nextHeartbeatTimeout - Date.now()
if (gap > this.gapThreshold) {
this.heartbeatTimeoutId = setTimeout(function () {
self.heartbeatTimeoutCb()
}, gap)
} else {
console.error('server heartbeat timeout')
this.emit('heartbeat timeout')
this.disconnect()
}
}
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)
let obj = Package.encode(Package.TYPE_HANDSHAKE_ACK)
this.send(obj)
if (this.initCallback) {
this.initCallback(this.socket)
}
}
onData (data) {
let msg = data
if (this.decode) {
msg = this.decode(msg)
}
this.processMessage(msg)
}
onKick (data) {
data = JSON.parse(Protocol.strdecode(data))
this.emit('onKick', data)
}
processPackage (msgs) {
if (Array.isArray(msgs)) {
for (let i = 0; i < msgs.length; i++) {
let msg = msgs[i]
this.handlers[msg.type](msg.body)
}
} else {
this.handlers[msgs.type](msgs.body)
}
}
processMessage (msg) {
if (!msg.id) {
// server push message
this.emit('__CLIENT_ROUTE', msg.route, msg.body)
this.emit(msg.route, msg.body)
return
}
//if have a id then find the callback function with the request
let cb = this.callbacks[msg.id]
delete this.callbacks[msg.id]
if (typeof cb !== 'function') {
return
}
cb(msg.body)
}
processMessageBatch (pomelo, msgs) {
for (let i = 0, l = msgs.length; i < l; i++) {
this.processMessage(msgs[i])
}
}
deCompose (msg) {
let route = msg.route
//Decompose route from dict
if (msg.compressRoute) {
if (!this.abbrs[route]) {
return {}
}
route = msg.route = this.abbrs[route]
}
if (this.protobuf && this.serverProtos[route]) {
return this.protobuf.decodeStr(route, msg.body)
} else if (decodeIO_decoder && decodeIO_decoder.lookup(route)) {
return decodeIO_decoder.build(route).decode(msg.body)
} else {
return JSON.parse(Protocol.strdecode(msg.body))
}
}
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
initData (data) {
if (!data || !data.sys) {
return
}
let dict = data.sys.dict
let protos = data.sys.protos
//Init compress dict
if (dict) {
this.dict = dict
this.abbrs = {}
for (let route in dict) {
if (dict.hasOwnProperty(route)) {
this.abbrs[dict[route]] = route
}
}
}
//Init protobuf protos
if (protos) {
this.protoVersion = protos.version || 0
this.serverProtos = protos.server || {}
this.clientProtos = protos.client || {}
//Save protobuf protos to localStorage
localStorage.set('protos', JSON.stringify(protos))
if (this.protobuf) {
this.protobuf.init({
encoderProtos: protos.client,
decoderProtos: protos.server
})
}
if (decodeIO_protobuf) {
decodeIO_encoder = decodeIO_protobuf.loadJson(this.clientProtos)
decodeIO_decoder = decodeIO_protobuf.loadJson(this.serverProtos)
}
}
}
}
module.exports = Pomelo