├── .npmrc ├── .npmignore ├── .gitignore ├── webpack.config.js ├── src ├── util.js ├── main.js ├── Protocal.js ├── Package.js ├── Message.js └── Pomelo.js ├── LICENSE ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | main.js 3 | test.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | test.js 4 | dist 5 | .DS_Store -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './lib/main.js', 5 | output: { 6 | path: path.resolve(__dirname, 'dist'), 7 | filename: 'pomelo-wexin-client.js', 8 | library: 'pomelo', 9 | libraryTarget: "umd" 10 | } 11 | }; -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | module.exports.copyArray = function (dest, doffset, src, soffset, length) { 2 | if ('function' === typeof src.copy) { 3 | // Buffer 4 | src.copy(dest, doffset, soffset, soffset + length); 5 | } else { 6 | // Uint8Array 7 | for (var index = 0; index < length; index++) { 8 | dest[doffset++] = src[soffset++]; 9 | } 10 | } 11 | }; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const Pomelo = require('./Pomelo'); 2 | 3 | function wsCreator({url, onError, onOpen, onMessage, onClose}) { 4 | const ws = wx.connectSocket({url:url}); 5 | ws.onError(onError); 6 | ws.onOpen(onOpen); 7 | ws.onMessage(onMessage); 8 | ws.onClose(onClose); 9 | return ws; 10 | } 11 | 12 | function wsCreatorWeb({url, onError, onOpen, onMessage, onClose}) { 13 | if (process.env.NODE_ENV !== 'production') { 14 | WebSocket = require('ws'); 15 | } 16 | const ws = new WebSocket(url); 17 | ws.onerror = onError; 18 | ws.onopen = onOpen; 19 | ws.onmessage = onMessage; 20 | ws.onclose = onClose; 21 | return ws; 22 | } 23 | 24 | function urlGenerator(host, port) { 25 | let url = 'wss://' + host; 26 | if (port) { 27 | url += '/ws/' + port + '/'; 28 | } 29 | return url; 30 | } 31 | 32 | module.exports = new Pomelo({ 33 | wsCreator, 34 | wsCreatorWeb, 35 | urlGenerator 36 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Wang Sijie (http://sijie.wang) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pomelo-weixin-client", 3 | "version": "1.10.0", 4 | "description": "pomelo微信小程序客户端", 5 | "main": "lib/main.js", 6 | "dependencies": { 7 | "events": "^2.0.0" 8 | }, 9 | "devDependencies": { 10 | "babel-cli": "^6.26.0", 11 | "babel-plugin-transform-node-env-inline": "^0.4.3", 12 | "babel-preset-es2015": "^6.24.1", 13 | "blob-to-buffer": "^1.2.8", 14 | "webpack": "^4.10.0", 15 | "webpack-cli": "^3.1.2", 16 | "ws": "^6.1.0" 17 | }, 18 | "scripts": { 19 | "test": "echo \"Error: no test specified\" && exit 1", 20 | "build": "NODE_ENV=production babel src --presets babel-preset-es2015 --plugins transform-node-env-inline --out-dir lib", 21 | "package": "webpack --config webpack.config.js", 22 | "prepublish": "npm run build" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/wangsijie/pomelo-weixin-client.git" 27 | }, 28 | "keywords": [ 29 | "pomelo", 30 | "weixin", 31 | "miniprogram", 32 | "xiaochengxu" 33 | ], 34 | "author": "Wang Sijie", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/wangsijie/pomelo-weixin-client/issues" 38 | }, 39 | "homepage": "https://github.com/wangsijie/pomelo-weixin-client#readme" 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pomelo-weixin-client 2 | 3 | pomelo客户端的微信小程序实现 4 | 5 | ## 安装 6 | 7 | ``` 8 | npm install pomelo-weixin-client 9 | ``` 10 | 11 | 或者使用单文件版,移步[releases](https://github.com/wangsijie/pomelo-weixin-client/releases) 12 | 13 | 单文件版无需使用npm,支持AMD/CMD等打包方式,可以直接require 14 | 15 | ## 使用 16 | 17 | ```js 18 | const pomelo = require('pomelo-weixin-client'); 19 | 20 | // 普通模式,用于生产环境,会生成wss://example.com/ws/3005类似的地址,需要nginx支持 21 | pomelo.init({ 22 | host: host, 23 | port: port 24 | }, function() { 25 | console.log('success'); 26 | }); 27 | 28 | // 调试模式,用于本地开发,生成普通链接ws://example.com:3005 29 | pomelo.init({ 30 | host: host, 31 | port: port, 32 | debugMode: true 33 | }, function() { 34 | console.log('success'); 35 | }); 36 | 37 | // 浏览器调试模式,用于本地开发,使用浏览器原生WebSocket,生成普通链接ws://example.com:3005 38 | pomelo.init({ 39 | host: host, 40 | port: port, 41 | debugMode: true, 42 | browserWS: true 43 | }, function() { 44 | console.log('success'); 45 | }); 46 | 47 | // 如果需要多个连接实例,可以用newInstance方法生成 48 | const pomelo2 = pomelo.newInstance(); 49 | ``` 50 | 51 | 具体使用方法见官方的[websocket](https://github.com/pomelonode/pomelo-jsclient-websocket)版本客户端说明 52 | 53 | 额外增加的功能: 54 | 55 | 全局监听消息(排除已用request设置回调的消息) 56 | 57 | ```js 58 | pomelo.on('onMessage', data => { 59 | console.log('onMessage', data) 60 | }); 61 | ``` 62 | 63 | ## 特别说明 64 | 65 | 微信仅支持wss连接(https)并且不支持自定义端口号,因此在服务器端进行额外的处理 66 | 67 | 例如以下代码: 68 | 69 | ```js 70 | pomelo.init({ 71 | host: 'example.com', 72 | port: 3005 73 | }); 74 | ``` 75 | 76 | 实际上连接的是```wss://example.com/ws/3005/``` 77 | 78 | 需要在服务器端将以上连接转换为```ws://example.com:3005``` 79 | 80 | 这里提供nginx的例子: 81 | 82 | ```nginx 83 | server 84 | { 85 | listen 443 ssl http2 default_server; 86 | listen [::]:443 ssl http2 default_server; 87 | server_name example.com; 88 | ssl on; 89 | #证书文件 90 | ssl_certificate /etc/ssl/certs/ssl-cert.crt; 91 | #私钥文件 92 | ssl_certificate_key /etc/ssl/private/ssl-cert.key; 93 | 94 | ssl_session_timeout 5m; 95 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 96 | ssl_ciphers AESGCM:ALL:!DH:!EXPORT:!RC4:+HIGH:!MEDIUM:!LOW:!aNULL:!eNULL; 97 | ssl_prefer_server_ciphers on; 98 | 99 | location /ws/3005/ { 100 | proxy_pass http://127.0.0.1:3005; 101 | proxy_http_version 1.1; 102 | proxy_set_header Upgrade $http_upgrade; 103 | proxy_set_header Connection "upgrade"; 104 | } 105 | } 106 | ``` 107 | 108 | ## License 109 | 110 | MIT © [Wang Sijie](http://sijie.wang) 111 | -------------------------------------------------------------------------------- /src/Protocal.js: -------------------------------------------------------------------------------- 1 | const {copyArray} = require('./util'); 2 | 3 | module.exports = class Protocol { 4 | /** 5 | * pomele client encode 6 | * id message id; 7 | * route message route 8 | * msg message body 9 | * socketio current support string 10 | */ 11 | static strencode(str) { 12 | var buffer = new Uint8Array(str.length * 3); 13 | var offset = 0; 14 | for (var i = 0; i < str.length; i++) { 15 | var charCode = str.charCodeAt(i); 16 | var codes = null; 17 | if (charCode <= 0x7f) { 18 | codes = [charCode]; 19 | } else if (charCode <= 0x7ff) { 20 | codes = [0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f)]; 21 | } else { 22 | codes = [0xe0 | (charCode >> 12), 0x80 | ((charCode & 0xfc0) >> 6), 0x80 | (charCode & 0x3f)]; 23 | } 24 | for (var j = 0; j < codes.length; j++) { 25 | buffer[offset] = codes[j]; 26 | ++offset; 27 | } 28 | } 29 | var _buffer = new Uint8Array(offset); 30 | copyArray(_buffer, 0, buffer, 0, offset); 31 | return _buffer; 32 | }; 33 | 34 | /** 35 | * client decode 36 | * msg String data 37 | * return Message Object 38 | */ 39 | static strdecode(buffer) { 40 | var bytes = new Uint8Array(buffer); 41 | var array = []; 42 | var offset = 0; 43 | var charCode = 0; 44 | var end = bytes.length; 45 | while (offset < end) { 46 | if (bytes[offset] < 128) { 47 | charCode = bytes[offset]; 48 | offset += 1; 49 | } else if (bytes[offset] < 224) { 50 | charCode = ((bytes[offset] & 0x3f) << 6) + (bytes[offset + 1] & 0x3f); 51 | offset += 2; 52 | } else if (bytes[offset] < 240) { 53 | charCode = ((bytes[offset] & 0x0f) << 12) + ((bytes[offset + 1] & 0x3f) << 6) + (bytes[offset + 2] & 0x3f); 54 | offset += 3; 55 | } else if (bytes[offset] < 256) { 56 | charCode = ((bytes[offset] & 0x07) << 18) + ((bytes[offset + 1] & 0x3f) << 12) + ((bytes[offset + 2] & 0x3f) << 6) + (bytes[offset + 3] & 0x3f); 57 | offset += 4; 58 | } 59 | array.push(charCode); 60 | } 61 | // 分片处理避免无法解析过大的数据(原因暂未确认 #8) 62 | var charDecoder = String.fromCodePoint ? String.fromCodePoint : String.fromCharCode; 63 | var result = ''; 64 | var chunk = 8 * 1024; 65 | var i; 66 | for (i = 0; i < array.length / chunk; i++) { 67 | result += charDecoder.apply(null, array.slice(i * chunk, (i + 1) * chunk)); 68 | } 69 | result += charDecoder.apply(null, array.slice(i * chunk)); 70 | return result; 71 | }; 72 | } -------------------------------------------------------------------------------- /src/Package.js: -------------------------------------------------------------------------------- 1 | 2 | const {copyArray} = require('./util'); 3 | 4 | const PKG_HEAD_BYTES = 4; 5 | const TYPE_HANDSHAKE = 1; 6 | const TYPE_HANDSHAKE_ACK = 2; 7 | const TYPE_HEARTBEAT = 3; 8 | const TYPE_DATA = 4; 9 | const TYPE_KICK = 5; 10 | 11 | module.exports = class Package { 12 | static get TYPE_HANDSHAKE() { 13 | return TYPE_HANDSHAKE; 14 | } 15 | static get TYPE_HANDSHAKE_ACK() { 16 | return TYPE_HANDSHAKE_ACK; 17 | } 18 | static get TYPE_HEARTBEAT() { 19 | return TYPE_HEARTBEAT; 20 | } 21 | static get TYPE_DATA() { 22 | return TYPE_DATA; 23 | } 24 | static get TYPE_KICK() { 25 | return TYPE_KICK; 26 | } 27 | /** 28 | * Package protocol encode. 29 | * 30 | * Pomelo package format: 31 | * +------+-------------+------------------+ 32 | * | type | body length | body | 33 | * +------+-------------+------------------+ 34 | * 35 | * Head: 4bytes 36 | * 0: package type, 37 | * 1 - handshake, 38 | * 2 - handshake ack, 39 | * 3 - heartbeat, 40 | * 4 - data 41 | * 5 - kick 42 | * 1 - 3: big-endian body length 43 | * Body: body length bytes 44 | * 45 | * @param {Number} type package type 46 | * @param {Uint8Array} body body content in bytes 47 | * @return {Uint8Array} new byte array that contains encode result 48 | */ 49 | static encode(type, body) { 50 | var length = body ? body.length : 0; 51 | var buffer = new Uint8Array(PKG_HEAD_BYTES + length); 52 | var index = 0; 53 | buffer[index++] = type & 0xff; 54 | buffer[index++] = (length >> 16) & 0xff; 55 | buffer[index++] = (length >> 8) & 0xff; 56 | buffer[index++] = length & 0xff; 57 | if (body) { 58 | copyArray(buffer, index, body, 0, length); 59 | } 60 | // return String.fromCharCode.apply(null,buffer); 61 | return buffer; 62 | } 63 | 64 | /** 65 | * Package protocol decode. 66 | * See encode for package format. 67 | * 68 | * @param {Uint8Array} buffer byte array containing package content 69 | * @return {Object} {type: package type, buffer: body byte array} 70 | */ 71 | static decode(buffer) { 72 | // buffer = toUTF8Array(str) 73 | var offset = 0; 74 | var bytes = new Uint8Array(buffer); 75 | var length = 0; 76 | var rs = []; 77 | while (offset < bytes.length) { 78 | var type = bytes[offset++]; 79 | length = ((bytes[offset++]) << 16 | (bytes[offset++]) << 8 | bytes[offset++]) >>> 0; 80 | var body = length ? new Uint8Array(length) : null; 81 | copyArray(body, 0, bytes, offset, length); 82 | offset += length; 83 | rs.push({ 'type': type, 'body': body }); 84 | } 85 | return rs.length === 1 ? rs[0] : rs; 86 | } 87 | } -------------------------------------------------------------------------------- /src/Message.js: -------------------------------------------------------------------------------- 1 | const Protocol = require('./Protocal'); 2 | const {copyArray} = require('./util'); 3 | 4 | const TYPE_REQUEST = 0; 5 | const TYPE_NOTIFY = 1; 6 | const TYPE_RESPONSE = 2; 7 | const TYPE_PUSH = 3; 8 | 9 | const MSG_FLAG_BYTES = 1; 10 | const MSG_ROUTE_CODE_BYTES = 2; 11 | const MSG_ID_MAX_BYTES = 5; 12 | const MSG_ROUTE_LEN_BYTES = 1; 13 | 14 | const MSG_ROUTE_CODE_MAX = 0xffff; 15 | 16 | const MSG_COMPRESS_ROUTE_MASK = 0x1; 17 | const MSG_TYPE_MASK = 0x7; 18 | 19 | module.exports = class Message { 20 | static get TYPE_REQUEST() { 21 | return TYPE_REQUEST; 22 | } 23 | static get TYPE_NOTIFY() { 24 | return TYPE_NOTIFY; 25 | } 26 | static get TYPE_RESPONSE() { 27 | return TYPE_RESPONSE; 28 | } 29 | static get TYPE_PUSH() { 30 | return TYPE_PUSH; 31 | } 32 | /** 33 | * Message protocol encode. 34 | * 35 | * @param {Number} id message id 36 | * @param {Number} type message type 37 | * @param {Number} compressRoute whether compress route 38 | * @param {Number|String} route route code or route string 39 | * @param {Buffer} msg message body bytes 40 | * @return {Buffer} encode result 41 | */ 42 | static encode(id, type, compressRoute, route, msg) { 43 | // caculate message max length 44 | var idBytes = msgHasId(type) ? caculateMsgIdBytes(id) : 0; 45 | var msgLen = MSG_FLAG_BYTES + idBytes; 46 | 47 | if (msgHasRoute(type)) { 48 | if (compressRoute) { 49 | if (typeof route !== 'number') { 50 | throw new Error('error flag for number route!'); 51 | } 52 | msgLen += MSG_ROUTE_CODE_BYTES; 53 | } else { 54 | msgLen += MSG_ROUTE_LEN_BYTES; 55 | if (route) { 56 | route = Protocol.strencode(route); 57 | if (route.length > 255) { 58 | throw new Error('route maxlength is overflow'); 59 | } 60 | msgLen += route.length; 61 | } 62 | } 63 | } 64 | if (msg) { 65 | msgLen += msg.length; 66 | } 67 | 68 | var buffer = new Uint8Array(msgLen); 69 | var offset = 0; 70 | 71 | // add flag 72 | offset = encodeMsgFlag(type, compressRoute, buffer, offset); 73 | 74 | // add message id 75 | if (msgHasId(type)) { 76 | offset = encodeMsgId(id, buffer, offset); 77 | } 78 | 79 | // add route 80 | if (msgHasRoute(type)) { 81 | offset = encodeMsgRoute(compressRoute, route, buffer, offset); 82 | } 83 | 84 | // add body 85 | if (msg) { 86 | offset = encodeMsgBody(msg, buffer, offset); 87 | } 88 | 89 | return buffer; 90 | } 91 | 92 | /** 93 | * Message protocol decode. 94 | * 95 | * @param {Buffer|Uint8Array} buffer message bytes 96 | * @return {Object} message object 97 | */ 98 | static decode(buffer) { 99 | var bytes = new Uint8Array(buffer); 100 | var bytesLen = bytes.length || bytes.byteLength; 101 | var offset = 0; 102 | var id = 0; 103 | var route = null; 104 | 105 | // parse flag 106 | var flag = bytes[offset++]; 107 | var compressRoute = flag & MSG_COMPRESS_ROUTE_MASK; 108 | var type = (flag >> 1) & MSG_TYPE_MASK; 109 | 110 | // parse id 111 | if (msgHasId(type)) { 112 | var m = parseInt(bytes[offset]); 113 | var i = 0; 114 | do { 115 | var m = parseInt(bytes[offset]); 116 | id = id + ((m & 0x7f) * Math.pow(2, (7 * i))); 117 | offset++; 118 | i++; 119 | } while (m >= 128); 120 | } 121 | 122 | // parse route 123 | if (msgHasRoute(type)) { 124 | if (compressRoute) { 125 | route = (bytes[offset++]) << 8 | bytes[offset++]; 126 | } else { 127 | var routeLen = bytes[offset++]; 128 | if (routeLen) { 129 | route = new Uint8Array(routeLen); 130 | copyArray(route, 0, bytes, offset, routeLen); 131 | route = Protocol.strdecode(route); 132 | } else { 133 | route = ''; 134 | } 135 | offset += routeLen; 136 | } 137 | } 138 | 139 | // parse body 140 | var bodyLen = bytesLen - offset; 141 | var body = new Uint8Array(bodyLen); 142 | 143 | copyArray(body, 0, bytes, offset, bodyLen); 144 | 145 | return { 146 | 'id': id, 'type': type, 'compressRoute': compressRoute, 147 | 'route': route, 'body': body 148 | }; 149 | } 150 | } 151 | 152 | var msgHasId = function (type) { 153 | return type === TYPE_REQUEST || type === TYPE_RESPONSE; 154 | }; 155 | 156 | var msgHasRoute = function (type) { 157 | return type === TYPE_REQUEST || type === TYPE_NOTIFY || 158 | type === TYPE_PUSH; 159 | }; 160 | 161 | var caculateMsgIdBytes = function (id) { 162 | var len = 0; 163 | do { 164 | len += 1; 165 | id >>= 7; 166 | } while (id > 0); 167 | return len; 168 | }; 169 | 170 | var encodeMsgFlag = function (type, compressRoute, buffer, offset) { 171 | if (type !== TYPE_REQUEST && type !== TYPE_NOTIFY && 172 | type !== TYPE_RESPONSE && type !== TYPE_PUSH) { 173 | throw new Error('unkonw message type: ' + type); 174 | } 175 | 176 | buffer[offset] = (type << 1) | (compressRoute ? 1 : 0); 177 | 178 | return offset + MSG_FLAG_BYTES; 179 | }; 180 | 181 | var encodeMsgId = function (id, buffer, offset) { 182 | do { 183 | var tmp = id % 128; 184 | var next = Math.floor(id / 128); 185 | 186 | if (next !== 0) { 187 | tmp = tmp + 128; 188 | } 189 | buffer[offset++] = tmp; 190 | 191 | id = next; 192 | } while (id !== 0); 193 | 194 | return offset; 195 | }; 196 | 197 | var encodeMsgRoute = function (compressRoute, route, buffer, offset) { 198 | if (compressRoute) { 199 | if (route > MSG_ROUTE_CODE_MAX) { 200 | throw new Error('route number is overflow'); 201 | } 202 | 203 | buffer[offset++] = (route >> 8) & 0xff; 204 | buffer[offset++] = route & 0xff; 205 | } else { 206 | if (route) { 207 | buffer[offset++] = route.length & 0xff; 208 | copyArray(buffer, offset, route, 0, route.length); 209 | offset += route.length; 210 | } else { 211 | buffer[offset++] = 0; 212 | } 213 | } 214 | 215 | return offset; 216 | }; 217 | 218 | var encodeMsgBody = function (msg, buffer, offset) { 219 | copyArray(buffer, offset, msg, 0, msg.length); 220 | return offset + msg.length; 221 | }; -------------------------------------------------------------------------------- /src/Pomelo.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const Message = require('./Message'); 3 | const Protocol = require('./Protocal'); 4 | const Package = require('./Package'); 5 | 6 | const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10; 7 | 8 | const JS_WS_CLIENT_TYPE = 'js-websocket'; 9 | const JS_WS_CLIENT_VERSION = '0.0.1'; 10 | 11 | const RES_OK = 200; 12 | const RES_FAIL = 500; 13 | const RES_OLD_CLIENT = 501; 14 | 15 | function blobToBuffer(blob, cb) { 16 | if (process.env.NODE_ENV !== 'production') { 17 | const toBuffer = require('blob-to-buffer'); 18 | if (Buffer.isBuffer(blob)) { 19 | return cb(blob); 20 | } 21 | return toBuffer(blob, cb); 22 | } 23 | const fileReader = new FileReader(); 24 | fileReader.onload = (event) => { 25 | const buffer = event.target.result; 26 | cb(buffer); 27 | }; 28 | fileReader.readAsArrayBuffer(blob); 29 | } 30 | 31 | function defaultDecode(data) { 32 | const msg = Message.decode(data); 33 | msg.body = JSON.parse(Protocol.strdecode(msg.body)); 34 | return msg; 35 | } 36 | function defaultEncode(reqId, route, msg) { 37 | const type = reqId ? Message.TYPE_REQUEST : Message.TYPE_NOTIFY; 38 | msg = Protocol.strencode(JSON.stringify(msg)); 39 | const compressRoute = 0; 40 | return Message.encode(reqId, type, compressRoute, route, msg); 41 | } 42 | function defaultUrlGenerator(host, port) { 43 | let url = 'ws://' + host; 44 | if (port) { 45 | url += ':' + port; 46 | } 47 | return url; 48 | } 49 | 50 | module.exports = class Pomelo extends EventEmitter { 51 | constructor(args) { 52 | super(args); 53 | const {wsCreator, wsCreatorWeb, urlGenerator = defaultUrlGenerator} = args; 54 | this.wsCreator = wsCreator; 55 | this.wsCreatorWx = wsCreator; 56 | this.wsCreatorWeb = wsCreatorWeb; 57 | this.urlGenerator = urlGenerator; 58 | 59 | this.reconnect = false; 60 | this.reconncetTimer = null; 61 | this.reconnectAttempts = 0; 62 | this.reconnectionDelay = 5000; 63 | 64 | this.handshakeBuffer = { 65 | 'sys': { 66 | type: JS_WS_CLIENT_TYPE, 67 | version: JS_WS_CLIENT_VERSION, 68 | rsa: {} 69 | }, 70 | 'user': { 71 | } 72 | }; 73 | 74 | this.heartbeatInterval = 0; 75 | this.heartbeatTimeout = 0; 76 | this.nextHeartbeatTimeout = 0; 77 | this.gapThreshold = 100; // heartbeat gap threashold 78 | this.heartbeatId = null; 79 | this.heartbeatTimeoutId = null; 80 | this.handshakeCallback = null; 81 | 82 | this.callbacks = {}; 83 | this.handlers = {}; 84 | this.handlers[Package.TYPE_HANDSHAKE] = this.handshake.bind(this); 85 | this.handlers[Package.TYPE_HEARTBEAT] = this.heartbeat.bind(this); 86 | this.handlers[Package.TYPE_DATA] = this.onData.bind(this); 87 | this.handlers[Package.TYPE_KICK] = this.onKick.bind(this); 88 | 89 | this.reqId = 0; 90 | } 91 | handshake(data) { 92 | data = JSON.parse(Protocol.strdecode(data)); 93 | if (data.code === RES_OLD_CLIENT) { 94 | this.emit('error', 'client version not fullfill'); 95 | return; 96 | } 97 | 98 | if (data.code !== RES_OK) { 99 | this.emit('error', 'handshake fail'); 100 | return; 101 | } 102 | this.handshakeInit(data); 103 | 104 | const obj = Package.encode(Package.TYPE_HANDSHAKE_ACK); 105 | this.send(obj); 106 | this.initCallback && this.initCallback(this.socket); 107 | } 108 | handshakeInit(data) { 109 | if (data.sys && data.sys.heartbeat) { 110 | this.heartbeatInterval = data.sys.heartbeat * 1000; // heartbeat interval 111 | this.heartbeatTimeout = this.heartbeatInterval * 2; // max heartbeat timeout 112 | } else { 113 | this.heartbeatInterval = 0; 114 | this.heartbeatTimeout = 0; 115 | } 116 | 117 | typeof this.handshakeCallback === 'function' && this.handshakeCallback(data.user); 118 | } 119 | heartbeat(data) { 120 | if (!this.heartbeatInterval) { 121 | return; 122 | } 123 | 124 | const obj = Package.encode(Package.TYPE_HEARTBEAT); 125 | if (this.heartbeatTimeoutId) { 126 | clearTimeout(this.heartbeatTimeoutId); 127 | this.heartbeatTimeoutId = null; 128 | } 129 | 130 | if (this.heartbeatId) { 131 | // already in a heartbeat interval 132 | return; 133 | } 134 | this.heartbeatId = setTimeout(() => { 135 | this.heartbeatId = null; 136 | this.send(obj); 137 | 138 | this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout; 139 | this.heartbeatTimeoutId = setTimeout(() => this.heartbeatTimeoutCb(), this.heartbeatTimeout); 140 | }, this.heartbeatInterval); 141 | } 142 | heartbeatTimeoutCb() { 143 | var gap = this.nextHeartbeatTimeout - Date.now(); 144 | if (gap > this.gapThreshold) { 145 | this.heartbeatTimeoutId = setTimeout(() => this.heartbeatTimeoutCb(), gap); 146 | } else { 147 | console.error('server heartbeat timeout'); 148 | this.emit('heartbeat timeout'); 149 | this.disconnect(); 150 | } 151 | } 152 | reset() { 153 | this.reconnect = false; 154 | this.reconnectionDelay = 1000 * 5; 155 | this.reconnectAttempts = 0; 156 | clearTimeout(this.reconncetTimer); 157 | } 158 | init(params, cb) { 159 | this.initCallback = cb; 160 | 161 | this.params = params; 162 | const {host, port, user, handshakeCallback, encode = defaultEncode, decode = defaultDecode, debugMode, browserWS} = params; 163 | 164 | this.encode = encode; 165 | this.decode = decode; 166 | 167 | if (debugMode) { 168 | this.url = defaultUrlGenerator(host, port); 169 | } 170 | else { 171 | this.url = this.urlGenerator(host, port); 172 | } 173 | 174 | if (browserWS) { 175 | this.wsCreator = this.wsCreatorWeb; 176 | this.browserWS = browserWS; 177 | } 178 | 179 | this.handshakeBuffer.user = user; 180 | this.handshakeCallback = handshakeCallback; 181 | this.connect(); 182 | } 183 | connect() { 184 | const params = this.params; 185 | const maxReconnectAttempts = params.maxReconnectAttempts || DEFAULT_MAX_RECONNECT_ATTEMPTS; 186 | const reconnectUrl = this.url; 187 | 188 | const onOpen = event => { 189 | if (!!this.reconnect) { 190 | this.emit('reconnect'); 191 | } 192 | this.reset(); 193 | const obj = Package.encode(Package.TYPE_HANDSHAKE, Protocol.strencode(JSON.stringify(this.handshakeBuffer))); 194 | this.send(obj); 195 | }; 196 | const onMessage = event => { 197 | if (this.browserWS) { 198 | blobToBuffer(event.data, (buffer) => { 199 | this.processPackage(Package.decode(buffer)); 200 | // new package arrived, update the heartbeat timeout 201 | if (this.heartbeatTimeout) { 202 | this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout; 203 | } 204 | }); 205 | } else { 206 | this.processPackage(Package.decode(event.data)); 207 | // new package arrived, update the heartbeat timeout 208 | if (this.heartbeatTimeout) { 209 | this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout; 210 | } 211 | } 212 | }; 213 | const onError = event => { 214 | this.emit('io-error', event); 215 | console.error('socket error: ', event); 216 | }; 217 | const onClose = event => { 218 | this.emit('close', event); 219 | this.emit('disconnect', event); 220 | if (!!params.reconnect && this.reconnectAttempts < maxReconnectAttempts) { 221 | this.reconnect = true; 222 | this.reconnectAttempts++; 223 | this.reconncetTimer = setTimeout(() => this.connect(), this.reconnectionDelay); 224 | this.reconnectionDelay *= 2; 225 | } 226 | }; 227 | 228 | // socket = wx.connectSocket({ url: reconnectUrl }); 229 | this.socket = this.wsCreator({ 230 | url: reconnectUrl, 231 | onError, 232 | onOpen, 233 | onMessage, 234 | onClose 235 | }); 236 | } 237 | disconnect() { 238 | if (this.socket) { 239 | this.socket.close(); 240 | this.socket = false; 241 | } 242 | 243 | if (this.heartbeatId) { 244 | clearTimeout(this.heartbeatId); 245 | this.heartbeatId = null; 246 | } 247 | if (this.heartbeatTimeoutId) { 248 | clearTimeout(this.heartbeatTimeoutId); 249 | this.heartbeatTimeoutId = null; 250 | } 251 | } 252 | request(route, msg, cb) { 253 | if (arguments.length === 2 && typeof msg === 'function') { 254 | cb = msg; 255 | msg = {}; 256 | } else { 257 | msg = msg || {}; 258 | } 259 | route = route || msg.route; 260 | if (!route) { 261 | return; 262 | } 263 | 264 | this.reqId++; 265 | this.sendMessage(this.reqId, route, msg); 266 | 267 | this.callbacks[this.reqId] = cb; 268 | } 269 | notify(route, msg) { 270 | msg = msg || {}; 271 | this.sendMessage(0, route, msg); 272 | } 273 | sendMessage(reqId, route, msg) { 274 | msg = this.encode(reqId, route, msg); 275 | 276 | const packet = Package.encode(Package.TYPE_DATA, msg); 277 | this.send(packet); 278 | } 279 | send(packet) { 280 | if (this.browserWS) { 281 | this.socket.send(packet.buffer); 282 | } else { 283 | this.socket.send({ data: packet.buffer }); 284 | } 285 | } 286 | onData(msg) { 287 | msg = this.decode(msg); 288 | this.processMessage(msg); 289 | } 290 | onKick(data) { 291 | data = JSON.parse(Protocol.strdecode(data)); 292 | this.emit('onKick', data); 293 | } 294 | processMessage(msg) { 295 | if (!msg.id) { 296 | this.emit('onMessage', msg.route, msg.body); 297 | this.emit(msg.route, msg.body); 298 | return; 299 | } 300 | 301 | //if have a id then find the callback function with the request 302 | const cb = this.callbacks[msg.id]; 303 | 304 | delete this.callbacks[msg.id]; 305 | typeof cb === 'function' && cb(msg.body); 306 | } 307 | processPackage(msgs) { 308 | if (Array.isArray(msgs)) { 309 | for (let i = 0; i < msgs.length; i++) { 310 | const msg = msgs[i]; 311 | this.handlers[msg.type](msg.body); 312 | } 313 | } else { 314 | this.handlers[msgs.type](msgs.body); 315 | } 316 | } 317 | newInstance() { 318 | return new Pomelo({ 319 | wsCreator: this.wsCreatorWx, 320 | wsCreatorWx: this.wsCreatorWx, 321 | wsCreatorWeb: this.wsCreatorWeb, 322 | urlGenerator: this.urlGenerator 323 | }); 324 | } 325 | } --------------------------------------------------------------------------------