tunnel.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. var requestLib = require('./request');
  2. var wxTunnel = require('./wxTunnel');
  3. /**
  4. * 当前打开的信道,同一时间只能有一个信道打开
  5. */
  6. var currentTunnel = null;
  7. // 信道状态枚举
  8. var STATUS_CLOSED = Tunnel.STATUS_CLOSED = 'CLOSED';
  9. var STATUS_CONNECTING = Tunnel.STATUS_CONNECTING = 'CONNECTING';
  10. var STATUS_ACTIVE = Tunnel.STATUS_ACTIVE = 'ACTIVE';
  11. var STATUS_RECONNECTING = Tunnel.STATUS_RECONNECTING = 'RECONNECTING';
  12. // 错误类型枚举
  13. var ERR_CONNECT_SERVICE = Tunnel.ERR_CONNECT_SERVICE = 1001;
  14. var ERR_CONNECT_SOCKET = Tunnel.ERR_CONNECT_SOCKET = 1002;
  15. var ERR_RECONNECT = Tunnel.ERR_RECONNECT = 2001;
  16. var ERR_SOCKET_ERROR = Tunnel.ERR_SOCKET_ERROR = 3001;
  17. // 包类型枚举
  18. var PACKET_TYPE_MESSAGE = 'message';
  19. var PACKET_TYPE_PING = 'ping';
  20. var PACKET_TYPE_PONG = 'pong';
  21. var PACKET_TYPE_TIMEOUT = 'timeout';
  22. var PACKET_TYPE_CLOSE = 'close';
  23. // 断线重连最多尝试 5 次
  24. var DEFAULT_MAX_RECONNECT_TRY_TIMES = 5;
  25. // 每次重连前,等待时间的增量值
  26. var DEFAULT_RECONNECT_TIME_INCREASE = 1000;
  27. function Tunnel(serviceUrl) {
  28. if (currentTunnel && currentTunnel.status !== STATUS_CLOSED) {
  29. throw new Error('当前有未关闭的信道,请先关闭之前的信道,再打开新信道');
  30. }
  31. currentTunnel = this;
  32. // 等确认微信小程序全面支持 ES6 就不用那么麻烦了
  33. var me = this;
  34. //=========================================================================
  35. // 暴露实例状态以及方法
  36. //=========================================================================
  37. this.serviceUrl = serviceUrl;
  38. this.socketUrl = null;
  39. this.status = null;
  40. this.open = openConnect;
  41. this.on = registerEventHandler;
  42. this.emit = emitMessagePacket;
  43. this.close = close;
  44. this.isClosed = isClosed;
  45. this.isConnecting = isConnecting;
  46. this.isActive = isActive;
  47. this.isReconnecting = isReconnecting;
  48. //=========================================================================
  49. // 信道状态处理,状态说明:
  50. // closed - 已关闭
  51. // connecting - 首次连接
  52. // active - 当前信道已经在工作
  53. // reconnecting - 断线重连中
  54. //=========================================================================
  55. function isClosed() { return me.status === STATUS_CLOSED; }
  56. function isConnecting() { return me.status === STATUS_CONNECTING; }
  57. function isActive() { return me.status === STATUS_ACTIVE; }
  58. function isReconnecting() { return me.status === STATUS_RECONNECTING; }
  59. function setStatus(status) {
  60. var lastStatus = me.status;
  61. if (lastStatus !== status) {
  62. me.status = status;
  63. }
  64. }
  65. // 初始为关闭状态
  66. setStatus(STATUS_CLOSED);
  67. //=========================================================================
  68. // 信道事件处理机制
  69. // 信道事件包括:
  70. // connect - 连接已建立
  71. // close - 连接被关闭(包括主动关闭和被动关闭)
  72. // reconnecting - 开始重连
  73. // reconnect - 重连成功
  74. // error - 发生错误,其中包括连接失败、重连失败、解包失败等等
  75. // [message] - 信道服务器发送过来的其它事件类型,如果事件类型和上面内置的事件类型冲突,将在事件类型前面添加前缀 `@`
  76. //=========================================================================
  77. var preservedEventTypes = 'connect,close,reconnecting,reconnect,error'.split(',');
  78. var eventHandlers = [];
  79. /**
  80. * 注册消息处理函数
  81. * @param {string} messageType 支持内置消息类型("connect"|"close"|"reconnecting"|"reconnect"|"error")以及业务消息类型
  82. */
  83. function registerEventHandler(eventType, eventHandler) {
  84. if (typeof eventHandler === 'function') {
  85. eventHandlers.push([eventType, eventHandler]);
  86. }
  87. }
  88. /**
  89. * 派发事件,通知所有处理函数进行处理
  90. */
  91. function dispatchEvent(eventType, eventPayload) {
  92. eventHandlers.forEach(function (handler) {
  93. var handleType = handler[0];
  94. var handleFn = handler[1];
  95. if (handleType === '*') {
  96. handleFn(eventType, eventPayload);
  97. } else if (handleType === eventType) {
  98. handleFn(eventPayload);
  99. }
  100. });
  101. }
  102. /**
  103. * 派发事件,事件类型和系统保留冲突的,事件名会自动加上 '@' 前缀
  104. */
  105. function dispatchEscapedEvent(eventType, eventPayload) {
  106. if (preservedEventTypes.indexOf(eventType) > -1) {
  107. eventType = '@' + eventType;
  108. }
  109. dispatchEvent(eventType, eventPayload);
  110. }
  111. //=========================================================================
  112. // 信道连接控制
  113. //=========================================================================
  114. var isFirstConnection = true;
  115. var isOpening = false;
  116. /**
  117. * 连接信道服务器,获取 WebSocket 连接地址,获取地址成功后,开始进行 WebSocket 连接
  118. */
  119. function openConnect() {
  120. if (isOpening) return;
  121. isOpening = true;
  122. // 只有关闭状态才会重新进入准备中
  123. setStatus(isFirstConnection ? STATUS_CONNECTING : STATUS_RECONNECTING);
  124. requestLib.request({
  125. url: serviceUrl,
  126. method: 'GET',
  127. success: function (response) {
  128. if (+response.statusCode === 200 && response.data && response.data.data.connectUrl) {
  129. console.log('通知服务端获准备开始连接,并成功取信道通讯地址', response.data.data.connectUrl)
  130. openSocket(me.socketUrl = response.data.data.connectUrl);
  131. } else {
  132. dispatchConnectServiceError(response);
  133. }
  134. },
  135. fail: dispatchConnectServiceError,
  136. complete: () => isOpening = false,
  137. });
  138. function dispatchConnectServiceError(detail) {
  139. if (isFirstConnection) {
  140. setStatus(STATUS_CLOSED);
  141. dispatchEvent('error', {
  142. code: ERR_CONNECT_SERVICE,
  143. message: '连接信道服务失败,网络错误或者信道服务没有正确响应',
  144. detail: detail || null,
  145. });
  146. } else {
  147. startReconnect(detail);
  148. }
  149. }
  150. }
  151. /**
  152. * 打开 WebSocket 连接,打开后,注册微信的 Socket 处理方法
  153. */
  154. function openSocket(url) {
  155. wxTunnel.listen({
  156. onOpen: handleSocketOpen,
  157. onMessage: handleSocketMessage,
  158. onClose: handleSocketClose,
  159. onError: handleSocketError,
  160. });
  161. //jacksplwxy:
  162. //wx.connectSocket({ url: url });
  163. wx.connectSocket({
  164. url: url,
  165. success(){
  166. console.log('开始尝试信道连接')
  167. }
  168. });
  169. isFirstConnection = false;
  170. }
  171. //=========================================================================
  172. // 处理消息通讯
  173. //
  174. // packet - 数据包,序列化形式为 `${type}` 或者 `${type}:${content}`
  175. // packet.type - 包类型,包括 message, ping, pong, close
  176. // packet.content? - 当包类型为 message 的时候,会附带 message 数据
  177. //
  178. // message - 消息体,会使用 JSON 序列化后作为 packet.content
  179. // message.type - 消息类型,表示业务消息类型
  180. // message.content? - 消息实体,可以为任意类型,表示消息的附带数据,也可以为空
  181. //
  182. // 数据包示例:
  183. // - 'ping' 表示 Ping 数据包
  184. // - 'message:{"type":"speak","content":"hello"}' 表示一个打招呼的数据包
  185. //=========================================================================
  186. // 连接还没成功建立的时候,需要发送的包会先存放到队列里
  187. var queuedPackets = [];
  188. /**
  189. * WebSocket 打开之后,更新状态,同时发送所有遗留的数据包
  190. */
  191. function handleSocketOpen() {
  192. /* istanbul ignore else */
  193. if (isConnecting()) {
  194. dispatchEvent('connect');
  195. console.log('监听到信道连接成功')
  196. }
  197. else if (isReconnecting()) {
  198. dispatchEvent('reconnect');
  199. resetReconnectionContext();
  200. }
  201. setStatus(STATUS_ACTIVE);
  202. emitQueuedPackets();
  203. nextPing();
  204. }
  205. /**
  206. * 收到 WebSocket 数据包,交给处理函数
  207. */
  208. function handleSocketMessage(message) {
  209. resolvePacket(message.data);
  210. }
  211. /**
  212. * 发送数据包,如果信道没有激活,将先存放队列
  213. */
  214. function emitPacket(packet) {
  215. if (isActive()) {
  216. sendPacket(packet);
  217. } else {
  218. queuedPackets.push(packet);
  219. }
  220. }
  221. /**
  222. * 数据包推送到信道
  223. */
  224. function sendPacket(packet) {
  225. var encodedPacket = [packet.type];
  226. if (packet.content) {
  227. encodedPacket.push(JSON.stringify(packet.content));
  228. }
  229. wx.sendSocketMessage({
  230. data: encodedPacket.join(':'),
  231. fail: handleSocketError,
  232. });
  233. }
  234. function emitQueuedPackets() {
  235. queuedPackets.forEach(emitPacket);
  236. // empty queued packets
  237. queuedPackets.length = 0;
  238. }
  239. /**
  240. * 发送消息包
  241. */
  242. function emitMessagePacket(messageType, messageContent) {
  243. var packet = {
  244. type: PACKET_TYPE_MESSAGE,
  245. content: {
  246. type: messageType,
  247. content: messageContent,
  248. },
  249. };
  250. emitPacket(packet);
  251. }
  252. /**
  253. * 发送 Ping 包
  254. */
  255. function emitPingPacket() {
  256. emitPacket({ type: PACKET_TYPE_PING });
  257. }
  258. /**
  259. * 发送关闭包
  260. */
  261. function emitClosePacket() {
  262. emitPacket({ type: PACKET_TYPE_CLOSE });
  263. }
  264. /**
  265. * 解析并处理从信道接收到的包
  266. */
  267. function resolvePacket(raw) {
  268. var packetParts = raw.split(':');
  269. var packetType = packetParts.shift();
  270. var packetContent = packetParts.join(':') || null;
  271. var packet = { type: packetType };
  272. if (packetContent) {
  273. try {
  274. packet.content = JSON.parse(packetContent);
  275. } catch (e) { }
  276. }
  277. switch (packet.type) {
  278. case PACKET_TYPE_MESSAGE:
  279. handleMessagePacket(packet);
  280. break;
  281. case PACKET_TYPE_PONG:
  282. handlePongPacket(packet);
  283. break;
  284. case PACKET_TYPE_TIMEOUT:
  285. handleTimeoutPacket(packet);
  286. break;
  287. case PACKET_TYPE_CLOSE:
  288. handleClosePacket(packet);
  289. break;
  290. default:
  291. handleUnknownPacket(packet);
  292. break;
  293. }
  294. }
  295. /**
  296. * 收到消息包,直接 dispatch 给处理函数
  297. */
  298. function handleMessagePacket(packet) {
  299. var message = packet.content;
  300. dispatchEscapedEvent(message.type, message.content);
  301. }
  302. //=========================================================================
  303. // 心跳、断开与重连处理
  304. //=========================================================================
  305. /**
  306. * Ping-Pong 心跳检测超时控制,这个值有两个作用:
  307. * 1. 表示收到服务器的 Pong 相应之后,过多久再发下一次 Ping
  308. * 2. 如果 Ping 发送之后,超过这个时间还没收到 Pong,断开与服务器的连接
  309. * 该值将在与信道服务器建立连接后被更新
  310. */
  311. let pingPongTimeout = 15000;
  312. let pingTimer = 0;
  313. let pongTimer = 0;
  314. /**
  315. * 信道服务器返回 Ping-Pong 控制超时时间
  316. */
  317. function handleTimeoutPacket(packet) {
  318. var timeout = packet.content * 1000;
  319. /* istanbul ignore else */
  320. if (!isNaN(timeout)) {
  321. pingPongTimeout = timeout;
  322. ping();
  323. }
  324. }
  325. /**
  326. * 收到服务器 Pong 响应,定时发送下一个 Ping
  327. */
  328. function handlePongPacket(packet) {
  329. nextPing();
  330. }
  331. /**
  332. * 发送下一个 Ping 包
  333. */
  334. function nextPing() {
  335. clearTimeout(pingTimer);
  336. clearTimeout(pongTimer);
  337. pingTimer = setTimeout(ping, pingPongTimeout);
  338. }
  339. /**
  340. * 发送 Ping,等待 Pong
  341. */
  342. function ping() {
  343. /* istanbul ignore else */
  344. if (isActive()) {
  345. emitPingPacket();
  346. // 超时没有响应,关闭信道
  347. pongTimer = setTimeout(handlePongTimeout, pingPongTimeout);
  348. }
  349. }
  350. /**
  351. * Pong 超时没有响应,信道可能已经不可用,需要断开重连
  352. */
  353. function handlePongTimeout() {
  354. startReconnect('服务器已失去响应');
  355. }
  356. // 已经重连失败的次数
  357. var reconnectTryTimes = 0;
  358. // 最多允许失败次数
  359. var maxReconnectTryTimes = Tunnel.MAX_RECONNECT_TRY_TIMES || DEFAULT_MAX_RECONNECT_TRY_TIMES;
  360. // 重连前等待的时间
  361. var waitBeforeReconnect = 0;
  362. // 重连前等待时间增量
  363. var reconnectTimeIncrease = Tunnel.RECONNECT_TIME_INCREASE || DEFAULT_RECONNECT_TIME_INCREASE;
  364. var reconnectTimer = 0;
  365. function startReconnect(lastError) {
  366. if (reconnectTryTimes >= maxReconnectTryTimes) {
  367. close();
  368. dispatchEvent('error', {
  369. code: ERR_RECONNECT,
  370. message: '重连失败',
  371. detail: lastError,
  372. });
  373. }
  374. else {
  375. wx.closeSocket();
  376. waitBeforeReconnect += reconnectTimeIncrease;
  377. setStatus(STATUS_RECONNECTING);
  378. reconnectTimer = setTimeout(doReconnect, waitBeforeReconnect);
  379. }
  380. if (reconnectTryTimes === 0) {
  381. dispatchEvent('reconnecting');
  382. }
  383. reconnectTryTimes += 1;
  384. }
  385. function doReconnect() {
  386. openConnect();
  387. }
  388. function resetReconnectionContext() {
  389. reconnectTryTimes = 0;
  390. waitBeforeReconnect = 0;
  391. }
  392. /**
  393. * 收到服务器的关闭请求
  394. */
  395. function handleClosePacket(packet) {
  396. close();
  397. }
  398. function handleUnknownPacket(packet) {
  399. // throw away
  400. }
  401. var isClosing = false;
  402. /**
  403. * 收到 WebSocket 断开的消息,处理断开逻辑
  404. */
  405. function handleSocketClose() {
  406. /* istanbul ignore if */
  407. if (isClosing) return;
  408. /* istanbul ignore else */
  409. if (isActive()) {
  410. // 意外断开的情况,进行重连
  411. startReconnect('链接已断开');
  412. }
  413. }
  414. function close() {
  415. isClosing = true;
  416. closeSocket();
  417. setStatus(STATUS_CLOSED);
  418. resetReconnectionContext();
  419. isFirstConnection = false;
  420. clearTimeout(pingTimer);
  421. clearTimeout(pongTimer);
  422. clearTimeout(reconnectTimer);
  423. dispatchEvent('close');
  424. isClosing = false;
  425. }
  426. function closeSocket(emitClose) {
  427. if (isActive() && emitClose !== false) {
  428. emitClosePacket();
  429. }
  430. wx.closeSocket();
  431. }
  432. //=========================================================================
  433. // 错误处理
  434. //=========================================================================
  435. /**
  436. * 错误处理
  437. */
  438. function handleSocketError(detail) {
  439. switch (me.status) {
  440. case Tunnel.STATUS_CONNECTING:
  441. dispatchEvent('error', {
  442. code: ERR_SOCKET_ERROR,
  443. message: '连接信道失败,网络错误或者信道服务不可用',
  444. detail: detail,
  445. });
  446. break;
  447. }
  448. }
  449. }
  450. module.exports = Tunnel;