ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序TCP长连接实战:WebSocket心跳重连与鉴权避坑指南

微信小程序TCP长连接实战:WebSocket心跳重连与鉴权避坑指南 简介这份资源是一套微信小程序 TCP/IP 长连接实战源码面向具备一定小程序开发基础、希望掌握长连接通信的开发者与学习者。它围绕小程序端与后端服务之间的稳定长连接展开可用于即时通讯、消息推送、设备状态同步等需要持续通信的场景帮助读者理解连接建立、心跳维持与数据收发的完整链路。压缩包共 35 个文件约 39KB以 18 个 Go 文件构成服务端核心逻辑7 个 JavaScript 文件负责小程序端交互另有 wxss、wxml、json 等页面与配置文件以及 README 说明文档和 LICENSE 授权文件整体结构清晰、便于按模块阅读。目前已有 397 人学习下载。源码附带运行截图读者可对照界面快速定位关键代码梳理客户端与服务端的分工并在此基础上改造出符合自身业务的长连接方案。1. 微信小程序 TCP 长连接为什么 HTTP 轮询撑不住实时场景做过微信小程序即时通讯、设备控制、协同白板的人大概率都经历过这个阶段先用wx.request定时轮询2 秒一次接口压力大、消息延迟高、手机发烫用户还嫌消息怎么半天才到。这时候你会开始搜微信小程序 TCP 长连接微信小程序源码 含截图想找一个能直接跑起来的方案。问题在于小程序运行在微信的 JS 引擎里没有 Node.js 的net模块也没有浏览器原生的WebSocket之外的裸 TCP 能力所以TCP 长连接在小程序里其实是一个被封装过的概念——底层是 TCP上层暴露给你的是wx.connectSocket这套 WebSocket API。这篇笔记要讲清楚的就是在小程序里怎么用 WebSocket 把一条长连接真正跑稳服务端怎么配合心跳、重连、鉴权、消息可靠这些坑怎么填。适合已经写过小程序页面、懂一点后端、准备把轮询换成推送的开发者。整套方案我会按能复现的标准写服务端用 Node.js 的ws库做示例因为它是目前最省事、最容易和微信生态对齐的选择换成 Go、Java、PHP 的 Swoole 思路是一样的。先给一个反直觉的结论小程序长连接翻车八成不是 TCP 本身的问题而是心跳设计、重连风暴和鉴权时序这三件事没处理好。TCP 三次握手四次挥手这些底层机制微信已经帮你兜住了你要操心的是应用层的活着和断了怎么办。2. 小程序长连接的底层链路从 wx.connectSocket 到服务端握手2.1 小程序为什么不能裸用 TCPWebSocket 到底封装了什么微信小程序的网络能力被限制在几个白名单 API 里wx.requestHTTP、wx.uploadFile、wx.downloadFile、wx.connectSocketWebSocket。你没法在小程序里new Socket()去连一个裸 TCP 端口这是微信出于安全和管控的设计。所以标题里的TCP/IP 长连接落到小程序这一端实际就是 WebSocket 长连接——WebSocket 在握手阶段用 HTTP Upgrade握手成功后底层就是一条持久的 TCP 连接双向收发。理解这一点很关键因为它决定了你的服务端选型。你不需要自己实现 TCP 粘包拆包、不需要处理 IP 包头WebSocket 协议已经帮你做了帧边界。你要做的是在 WebSocket 之上定义自己的消息格式通常是 JSON 或 Protobuf然后处理连接生命周期。小程序端建立连接的核心 API 就一个// pages/chat/chat.js const socketUrl wss://your-domain.com/ws; // 必须是 wss且域名要在小程序后台配置 Page({ onLoad() { // 建立连接注意小程序同时最多维持的 socket 连接数有限 this.socketTask wx.connectSocket({ url: socketUrl, header: { content-type: application/json, // 鉴权 token 建议放 header握手阶段就能校验 Authorization: Bearer wx.getStorageSync(token) }, // 注意protocols 参数用于子协议协商一般不用 }); // 监听连接打开 this.socketTask.onOpen(() { console.log(socket 已连接); this.startHeartbeat(); // 连接成功后立刻启动心跳 }); // 监听收到消息 this.socketTask.onMessage((res) { const msg JSON.parse(res.data); this.handleMessage(msg); }); // 监听连接关闭 this.socketTask.onClose((res) { console.log(socket 关闭, res.code, res.reason); this.stopHeartbeat(); this.scheduleReconnect(); // 触发重连 }); // 监听错误 this.socketTask.onError((err) { console.error(socket 错误, err); }); } });这段代码的逻辑说明wx.connectSocket返回一个SocketTask对象所有后续操作都基于它。onOpen是连接真正建立WebSocket 握手完成后触发这是启动心跳的正确时机不要在connectSocket调用后立刻发心跳那时连接还没通。onMessage收到的res.data是字符串需要自己解析。onClose和onError是重连的触发点。参数说明url必须是wss://生产环境且域名要在微信公众平台开发设置-服务器域名-socket 合法域名里配置否则真机上直接连不上开发者工具里可能因为不校验合法域名选项而蒙混过关这是新手最常见的翻车点。header里的鉴权信息会在握手请求里带上服务端可以在 upgrade 阶段读取。2.2 服务端握手与连接管理用 ws 库搭一个最小可用网关服务端我用 Node.js 的ws库因为它轻、和微信生态的 JSON 消息天然契合。核心要做三件事握手时校验 token、维护在线连接表、处理消息路由。// server.js const WebSocket require(ws); const jwt require(jsonwebtoken); const wss new WebSocket.Server({ port: 8080 }); // 在线连接表userId - socket const clients new Map(); wss.on(connection, (ws, req) { // 从握手请求的 header 里取 token const token (req.headers[authorization] || ).replace(Bearer , ); let userId; try { const payload jwt.verify(token, process.env.JWT_SECRET); userId payload.uid; } catch (e) { // 鉴权失败直接关闭code 4001 自定义 ws.close(4001, auth failed); return; } // 同一用户重复登录踢掉旧连接 if (clients.has(userId)) { clients.get(userId).close(4002, duplicate login); } clients.set(userId, ws); console.log(user ${userId} online, total ${clients.size}); // 心跳超时标记 ws.isAlive true; ws.on(pong, () { ws.isAlive true; }); ws.on(message, (raw) { let msg; try { msg JSON.parse(raw); } catch (e) { return; // 非法 JSON 直接丢弃 } routeMessage(userId, msg); }); ws.on(close, () { // 只有当前连接还是这个用户的活跃连接时才删除避免踢人时误删新连接 if (clients.get(userId) ws) { clients.delete(userId); } }); }); // 服务端主动心跳探测30 秒一轮 setInterval(() { wss.clients.forEach((ws) { if (ws.isAlive false) return ws.terminate(); ws.isAlive false; ws.ping(); // 发 ping 帧客户端会自动回 pong }); }, 30000); function routeMessage(fromUid, msg) { // 简化示例点对点转发 const target clients.get(msg.to); if (target target.readyState WebSocket.OPEN) { target.send(JSON.stringify({ from: fromUid, data: msg.data })); } }逻辑说明wss.on(connection)在 WebSocket 握手完成后触发此时req里还保留着 HTTP upgrade 请求的 header可以拿到鉴权信息。鉴权失败用自定义 close code4000 以上是应用自定义区间关闭客户端能据此判断是被踢还是网络断。clients这个 Map 是在线连接表是所有消息路由的基础。服务端用ws.ping()发协议级 ping 帧客户端包括小程序会自动回 pong这是比应用层心跳更省流量的做法但小程序端对协议级 ping 的响应行为不完全可控所以应用层心跳还是要做下面会讲。参数说明port按你的部署环境改生产环境一般挂在 Nginx 后面走 443。JWT_SECRET从环境变量读别硬编码。心跳间隔 30 秒是个经验值太短浪费流量太长超过 60 秒容易被中间的反向代理或运营商 NAT 超时切断。2.3 消息格式设计JSON 够不够什么时候该上 Protobuf小程序的 JS 引擎解析 JSON 很快绝大多数场景 JSON 足够。但如果你的消息量大、字段多或者要做二进制透传比如设备控制指令可以考虑 Protobuf。不过 Protobuf 在小程序里需要引入额外的库包体积会增加我一般建议先用 JSON等真的遇到性能瓶颈再换。一个实用的 JSON 消息格式约定字段类型说明typestring消息类型chat / heartbeat / ack / systemseqnumber客户端自增序号用于 ack 确认和去重tostring目标用户 ID服务端路由用dataobject业务负载tsnumber客户端时间戳毫秒seq这个字段很多人省掉结果消息丢了不知道、重复了不知道。加上它配合服务端的 ack 回执才能做到至少一次投递。这是从轮询切到长连接后最容易忽略的一环——轮询天然有重试长连接断了就是断了。3. 心跳、重连与鉴权长连接稳定的三个命门3.1 应用层心跳怎么写才不会误判断线小程序端没法像浏览器那样方便地监听协议级 ping/pong所以应用层心跳是必须的。但心跳写不好会出现两种翻车一是心跳太频繁流量和电量扛不住二是心跳判断逻辑有 bug明明连着却以为断了疯狂重连。// 心跳管理挂在 Page 上 const HEARTBEAT_INTERVAL 25000; // 25 秒发一次 const HEARTBEAT_TIMEOUT 10000; // 10 秒没收到 pong 就认为断线 Page({ startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer setInterval(() { if (!this.socketTask) return; this.socketTask.send({ data: JSON.stringify({ type: heartbeat, ts: Date.now() }) }); // 发完启动超时检测 this.pongTimer setTimeout(() { console.warn(心跳超时主动关闭触发重连); this.socketTask.close({ code: 3000, reason: heartbeat timeout }); }, HEARTBEAT_TIMEOUT); }, HEARTBEAT_INTERVAL); }, stopHeartbeat() { if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); if (this.pongTimer) clearTimeout(this.pongTimer); this.heartbeatTimer null; this.pongTimer null; }, handleMessage(msg) { if (msg.type pong) { // 收到服务端心跳回应清掉超时定时器 if (this.pongTimer) clearTimeout(this.pongTimer); return; } // ... 其他消息处理 } });逻辑说明心跳间隔 25 秒比服务端 30 秒的探测稍短保证服务端探测前客户端已经活跃。发完心跳启动一个 10 秒的超时定时器如果这期间收到服务端的pong消息就清掉它否则认为连接已死主动close触发重连。注意这里用的是应用层的pong消息服务端收到heartbeat后回一个{type:pong}不是协议级 pong因为小程序端拿不到协议级 pong 事件。参数说明HEARTBEAT_INTERVAL建议 20~30 秒HEARTBEAT_TIMEOUT建议是间隔的 1/3 到 1/2。这两个值要和 Nginx 的proxy_read_timeout默认 60 秒配合心跳间隔必须小于它否则连接会被 Nginx 先掐断。3.2 重连为什么要退避直连重试会引发什么新手写重连最常见的就是onClose里直接connectSocket一秒重试一次。本地测试没问题一上线遇到服务端重启几千个客户端同时重连服务端刚起来就被打挂然后客户端继续重连形成雪崩。这就是重连风暴。正确做法是指数退避加随机抖动Page({ scheduleReconnect() { if (this.reconnectTimer) return; // 已经在等待重连不重复排 this.reconnectAttempts (this.reconnectAttempts || 0) 1; // 指数退避1s, 2s, 4s, 8s... 上限 30s const base Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000); // 加 0~1000ms 随机抖动打散重连时间 const delay base Math.random() * 1000; console.log(第 ${this.reconnectAttempts} 次重连${Math.round(delay)}ms 后); this.reconnectTimer setTimeout(() { this.reconnectTimer null; this.initSocket(); // 重新走一遍连接流程 }, delay); }, // 连接成功后重置计数 onSocketOpen() { this.reconnectAttempts 0; this.startHeartbeat(); } });逻辑说明reconnectAttempts记录连续失败次数每次翻倍上限 30 秒。随机抖动是关键它让成千上万个客户端不会在同一毫秒一起重连。连接成功后必须把计数清零否则下次断线会从很大的延迟开始。参数说明上限 30 秒是平衡恢复速度和服务端压力的经验值。如果你的业务对实时性要求极高比如交易可以降到 10 秒但要配合服务端的限流。抖动范围一般取 base 的 0~1 倍。3.3 鉴权时序token 过期了长连接怎么办长连接和 HTTP 不一样HTTP 每次请求都带 token过期了下次请求就 401。长连接建立时校验一次之后这条连接就一直活着token 过期了连接还在。这带来两个问题一是安全上token 失效后连接不该继续用二是体验上不能让用户 token 过期就掉线。常见做法是服务端在 token 快过期时主动下发一个token_expiring消息客户端收到后用 refresh token 换新 token然后通过一条reauth消息在现有连接上更新鉴权状态不用断开重连。// 客户端处理 token 过期 handleMessage(msg) { if (msg.type token_expiring) { wx.request({ url: https://your-domain.com/api/refresh, method: POST, data: { refreshToken: wx.getStorageSync(refreshToken) }, success: (res) { const newToken res.data.token; wx.setStorageSync(token, newToken); // 在现有连接上重新鉴权 this.socketTask.send({ data: JSON.stringify({ type: reauth, token: newToken }) }); } }); } }服务端在message处理里加一个reauth分支重新jwt.verify并更新该连接对应的用户信息。这样用户全程无感连接不断。这个设计很多人不做结果就是 token 一过期全员掉线重连又是一次重连风暴。4. 避坑与排查长连接上线后最常翻车的五个点4.1 开发者工具能连真机连不上现象在微信开发者工具里一切正常扫码到真机上onError直接报错连接建立不了。原因开发者工具默认可以勾选不校验合法域名真机没有这个选项。你的wss域名没在小程序后台配置或者配置了但没生效配置后需要重新编译、有时要等几分钟。解决登录微信公众平台进入开发-开发管理-开发设置-服务器域名把wss://your-domain.com加到 socket 合法域名里。注意必须是wssws不行域名不能带端口除非是 443不能是 IP。配置完在开发者工具里点详情-域名信息确认已同步。4.2 连接几分钟后必断日志显示 1006现象连接稳定运行几分钟后固定断开onClose的 code 是 1006异常关闭没有 close 帧。原因1006 通常意味着连接被中间设备静默切断最常见的是 Nginx 的proxy_read_timeout默认 60 秒或者云厂商负载均衡的空闲超时。你的心跳间隔如果大于这个值连接就会被掐。解决把心跳间隔调到小于所有中间设备的超时时间。Nginx 配置里显式设置proxy_read_timeout 300s;和proxy_send_timeout 300s;同时proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection upgrade;这两行必须有否则 WebSocket 升级不了。心跳间隔我一般设 25 秒留足余量。4.3 消息偶发丢失用户说我明明发了现象发送方显示已发送接收方没收到也没有报错。原因WebSocket 的send只保证把数据交给内核缓冲区不保证对方收到。连接在传输过程中断了缓冲区里的消息就丢了。轮询时代每次请求都有响应切到长连接后这个保证没了。解决应用层做 ack。发送方给每条消息带seq服务端收到后回{type:ack, seq}发送方收到 ack 才把消息标记为已送达超时没收到就重发。接收方也要去重用seq或消息 ID 判断。这套机制不复杂但必须有否则消息可靠性无从谈起。4.4 切后台再回来连接状态错乱现象小程序切到后台一段时间再切回来页面显示已连接但发不出消息或者重复触发重连。原因小程序切后台后微信会在一定时间后挂起 JS 执行定时器停摆心跳发不出去连接被服务端判定超时关闭。切回来时onClose才触发但页面状态可能还停留在已连接。解决监听onShow和onHide。onHide时主动关闭连接或停止心跳onShow时检查连接状态如果readyState不是 OPEN 就重连。不要依赖定时器在后台继续跑它不会。onShow() { if (!this.socketTask || this.socketTask.readyState ! 1) { this.initSocket(); } }, onHide() { // 可选切后台主动断开省电省流量 // this.socketTask this.socketTask.close({ code: 1000 }); }4.5 同一账号多端登录消息乱窜现象用户在手机和 iPad 同时登录A 端发的消息 B 端也收到或者旧设备的连接没断消息发到了旧设备。原因服务端的clients表用 userId 做 key新连接覆盖旧连接但旧连接没被关闭还在接收消息。解决新连接建立时检查clients里是否已有该 userId 的连接有就主动close掉旧的用自定义 code 4002 标识被踢。客户端收到 4002 时不要重连而是提示用户账号在其他设备登录。这个逻辑在 2.2 的代码里已经体现关键是客户端要区分 close code别一律重连。5. 进阶把长连接做成可观测、可压测的工程件前面讲的都是能跑这一章讲跑得放心。长连接最大的问题是它是黑匣子——连接断了、消息慢了你从业务日志里看不出来。我一般会加三个东西连接指标、消息链路追踪、压测脚本。连接指标最直接服务端定时打点当前在线连接数、每分钟新建连接数、每分钟异常断开数、平均连接存活时长。这四个指标能覆盖 90% 的线上问题。在线数突然掉一半多半是服务端或中间设备出问题新建连接数飙升多半是客户端在重连风暴平均存活时长变短多半是心跳或超时配置不对。// 每分钟打一次指标 setInterval(() { const now Date.now(); let alive 0; wss.clients.forEach((ws) { if (ws.readyState WebSocket.OPEN) alive; }); console.log(JSON.stringify({ metric: ws_stats, online: alive, total: wss.clients.size, ts: now })); }, 60000);消息链路追踪给每条消息在入口生成一个 traceId服务端转发时带上客户端收到后如果发现异常可以把这个 traceId 报上来你就能在日志里追这条消息从进到出的全过程。这个在排查消息到底丢在哪一环时特别有用。压测脚本用 Node.js 写就行模拟 N 个客户端同时连接、发心跳、互发消息观察服务端的 CPU、内存、连接数。我一般从 1000 连接起步逐步加到目标量级。压测时重点看两件事一是服务端文件描述符够不够ulimit -n二是内存有没有随连接数线性增长有泄漏的话会。ws库单机撑几千到上万连接没问题再往上就要考虑多进程加 Redis 做连接路由了。最后说个我自己的习惯任何长连接项目上线前我一定会做一次断网演练——用工具把服务端网络切断 30 秒再恢复观察客户端多久能全部重连回来、期间有没有消息丢失、服务端有没有被打挂。这个演练能暴露 80% 的重连和心跳问题比看代码管用。长连接这东西玄学的地方不多但细节密度高配置差一点、时序错一步表现就天差地别。把心跳、退避、ack 这三件事做扎实剩下的就是耐心调参了。希望帮到你。本文还有配套的精品资源点击获取
返回列表