微信小程序长连接实战:WebSocket连接、心跳重连与二进制协议设计
2026/9/16 4:56:13 网站建设 项目流程

不少做物联网、智能硬件、工业数据看板的朋友,迟早都会碰到一个需求:小程序里要跟设备保持实时双向通信。HTTP轮询太笨,延迟高、流量浪费大,消息稍微密集一点就卡。于是大家盯上了TCP长连接。但微信小程序并不是浏览器,它虽然有wx.connectSocket这类接口,可它跟后端真正建立起来的、能被开发者直接操控的通道,其实是 WebSocket 协议。换句话说,小程序里的“TCP通信”,99% 的落地形态都是 WebSocket。这篇内容我就从实际项目出发,把小程序里做 TCP/WebSocket 长连接通信的整套流程、坑点和协议设计思路都梳理一遍。不管你是做设备控制、订单推送、IM 聊天,还是要对接自研的 TCP 网关,这篇文章基本能帮你从零跑通,并且避开我当年踩过的那些坑。

1. 为什么要在小程序里做“TCP通信”

1.1 别被名字带偏:小程序里的TCP其实是指WebSocket

先把这个概念掰扯清楚:微信小程序没有直接暴露new Socket()这种原生 TCP 接口,你不能在小程序里像在 Node.js 里那样net.connect(port, host)。小程序提供给开发者的长连接 API 是wx.connectSocket,底层走的是 WebSocket 协议。WebSocket 本身是建立在 TCP 之上的,所以很多人叫它“小程序TCP通信”,本质上是“TCP长连接 + 应用层 WebSocket 握手”。

为什么微信不直接开放裸 TCP?原因很简单,安全管控。小程序跑在微信这个宿主环境里,如果每个小程序都能随意向任意 IP 端口发起 TCP 连接,那恶意连接、内网探测、数据泄露这些问题全都堵不住。所以微信做了一层协议约束:必须走 wss 或 ws(正式环境下必须 wss),必须把域名配置到白名单里。理解了这个大前提,你后面遇到“连不上、真机不通”这类问题时,排查方向就不会跑偏。

那么普通开发者怎么感知到 TCP 的存在?主要靠两个地方:一是连接 URL,wss://yourdomain.com:port/path,这里域名背后解析到的服务器 IP 和端口,其实就是 TCP 层的目标;二是消息的可靠性,TCP 的确认重传、有序传输等能力,在 WebSocket 层基本都被保留了下来。换句话说,你不需要关心 TCP 的滑动窗口和拥塞控制,但可以享受它的可靠性。

1.2 哪些业务必须走Socket长连接而不是HTTP轮询

我经常被问到:“我这个需求明明用 HTTP 也能做,为什么要上长连接?”这里我给出一个比较实用的判断标准。如果你的业务满足下面任意两条,长连接基本就是刚需:

  • 服务端需要主动推送消息,比如有人下单了、设备报警了、新版本发布了。
  • 端上需要实时状态同步,比如大屏数据、在线设备列表、游戏对局状态。
  • 交互频率高,HTTP 轮询会造成大量无效请求,比如聊天、协作编辑。
  • 单条消息延迟要求高,比如遥控设备、抢单,轮询的延迟不可控。

举个例子,我之前做的一个设备远程控制项目:用户在小程序里点一下“开门”,指令要立刻到达设备端。如果走 HTTP 轮询,设备端每隔 3 秒拉一次指令,用户按下按钮后最坏要等 3 秒门才开,体验很糟糕。改用长连接之后,指令从发出到设备收到基本在 500 毫秒以内,而且设备的状态变化也能实时回传,用户体验完全不一样。

另外还有一类典型场景:服务器端做数据汇聚。比如你有一批传感器,通过网关把数据发到服务端,用户在微信小程序里查看实时曲线。如果每次都靠小程序主动拉,服务端就得为每个小程序用户维护一个轮询状态,纯属浪费资源。用长连接,服务端主动把数据“推”给小程序,小程序只负责渲染,架构清晰很多。

1.3 微信给的限制清单,提前知道心里有底

在小程序里玩长连接,有几个硬性限制是绕不过去的,提前知道能省很多排查时间:

  • 并发 Socket 数量:同一个小程序同时最多只能有 5 个 WebSocket 连接。注意是“同时”,超过的会被微信直接拒绝,或者最老的那个被踢掉。
  • 域名白名单:wx.connectSocket的 URL 必须匹配“socket合法域名”,否则开发者工具里能过,真机上一律连不上。正式环境强制要求 wss,也就是需要在服务器上配置 SSL 证书。
  • 前后台状态:小程序切到后台,Socket 连接并不会立刻断开,但长时间在后台,系统随时可能把它回收。回到前台时,不能假设连接一定还在,必须主动检查并重连。
  • 数据大小:单次通过wx.sendSocketMessage发送的数据长度有限制,实际上比较稳妥的做法是控制单包体积,二进制数据尤其要注意分包。
  • 开发者工具与真机差异:开发者工具里模拟的是桌面浏览器的网络环境,真机走的是手机网络,运营商可能对长连接有 NAT 超时、空闲断开等策略,所以“工具里好好的,真机一测就断”这种情况太常见了。

这些限制在项目启动之前就得排进架构里。比如连接数只有 5 个,如果业务里既要 IM,又要设备控制,还要订阅服务,那就要考虑是不是复用同一条连接,用业务层协议来区分消息类型,而不是傻傻地开多个 Socket。

2. 动手前的准备工作:域名、工具与项目结构

2.1 socket合法域名配置,这一步最容易卡住新人

很多新手第一次碰小程序长连接,写完代码在开发者工具里跑得好好的,一上真机就报url not in domain list。这个错的具体意思就是:你wx.connectSocket里的地址,不在微信后台配置的合法域名里。

配置入口在微信公众平台的后台,路径是:开发 → 开发管理 → 开发设置 → 服务器域名 → socket合法域名。在这里添加你的域名,注意几个细节:

  • 域名必须已经完成 ICP 备案,否则填不进去。
  • 域名不能是 IP,必须是一个真正的域名,比如wss://socket.example.com,不能写wss://120.25.x.x
  • 端口可以带,比如wss://socket.example.com:8088,微信会把它整体作为一个合法域名来校验。
  • 配置生效不是即时的,通常需要几分钟到十几分钟,别刚改完就去真机试,容易误判。

这里有个容易混淆的点:很多人以为在开发者工具里勾选了“不校验合法域名”就万事大吉,但这只是本地调试用的,真机完全不认这个开关。所以项目上线前,一定要确认域名已经加到白名单,而且服务器上的证书是可信的、没有过期。

2.2 开发者工具里的调试选项与“本地调试”技巧

开发阶段做联调,最烦的就是域名还没备案、服务器还没上公网。这时候有两个临时方案:

  • 在开发者工具的“详情 → 本地设置”里,勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。这个选项打开后,本地调试时你可以连接任意 ws:// 地址,比如ws://192.168.1.10:9090,方便跟内网服务器联调。
  • 但这个只对开发者工具有效。如果你要用真机预览,也得在预览时打开“开发版”调试模式,在真机上同样可以绕过域名校验。具体操作是:右上角菜单 → 打开调试,会在小程序界面上出现一个 vConsole 的悬浮按钮,这时候网络请求和 Socket 都不会强制走域名白名单。

注意,真机调试下的 Socket 连接不会被 Charles 这类 PC 端抓包工具直接看到,因为小程序真机流量是加密且由微信客户端管理的,这点我在后面的排查部分会专门提到。

2.3 开发框架选型:原生、uni-app、Taro 的差异

小程序开发框架现在主要有三条路线:微信原生、uni-app、Taro。如果你只是做一个微信小程序,我建议直接用原生,API 最直接,也不存在编译层带来的调试差异。但如果你有跨端需求,比如同一套代码要跑支付宝小程序、H5、App,那 uni-app 或 Taro 会是更现实的选择。

以 uni-app 为例,它其实是对wx.connectSocket做了一层封装,对应的接口是uni.connectSocket,回调风格也做成了类似浏览器 WebSocket 的写法。要注意的是,uni-app 在 H5 端和 App 端会走浏览器或原生 WebSocket,微信小程序端会转成小程序的wx.connectSocket,所以有些细节在两端的表现会不一样,比如ArrayBuffer的处理方式、消息事件的字段名差异等。

不管用哪种框架,核心的 TCP/WebSocket 逻辑我建议单独封装成一个模块,不要和页面组件耦合。后面后端改协议、增加心跳、调整重连策略,都只需要改一个文件,页面层完全无感。这个习惯在项目变大之后会非常省事。

3. 核心实现:从连接建立到双向通信

3.1 建立连接:wx.connectSocket 的完整参数说明

下面这段代码是原生小程序里建立 Socket 连接的基本写法,我加上了详细注释。

function connectSocket() { const socketTask = wx.connectSocket({ url: 'wss://socket.example.com:8088/ws', header: { 'X-Token': getToken() // 自定义鉴权头,服务端可以校验 }, protocols: ['my-protocol'], // 可选,子协议,服务端需要对应支持 tcpNoDelay: true, // 这个参数在部分平台生效,控制是否禁用 Nagle 算法 success() { console.log('连接发起成功,注意这不是连接建立成功'); }, fail(err) { console.error('连接发起失败,一般是域名/配置问题', err); } }); socketTask.onOpen(() => { console.log('WebSocket 连接真正建立完成'); // 这里可以发送登录鉴权消息、启动心跳等 }); socketTask.onMessage((res) => { // res.data 可能是字符串,也可能是 ArrayBuffer handleMessage(res.data); }); socketTask.onError((err) => { console.error('Socket 发生错误', err); // 这里不要直接重连,容易造成重连风暴 }); socketTask.onClose((res) => { console.log('连接关闭,code:', res.code, 'reason:', res.reason); // 在这里触发重连逻辑 }); return socketTask; }

wx.connectSocket返回的是一个SocketTask对象,后续的收发、关闭操作都通过它来调用。特别强调一下,success回调只代表“发起连接”这个动作成功了,并不代表连接已经建立。真正建立成功是onOpen触发的时候。新手在这里容易写错,在success里就直接发消息,结果发现服务端收不到。

参数里的header可以用来带鉴权信息,但注意,这个 Header 不是 HTTP Header,而是 WebSocket 握手阶段携带的额外字段。服务端在 WebSocket 握手时(比如 Node.js 的ws库)可以通过request.headers拿到。如果你希望更安全,可以把令牌放在 URL 的 query 里,比如wss://xxx/ws?token=abc,但这样 token 会被记到服务器日志里,建议有效期设短一点。

3.2 消息收发与 ArrayBuffer 字节处理

微信小程序的 Socket 消息有两种数据类型:stringArrayBuffer。默认情况下,onMessage返回的res.data是字符串。如果你的服务端发过来的是二进制数据,比如自定义协议包、图片、音频流,那么在小程序里需要在onMessage里手动转二进制,或者在wx.connectSocket时通过header或其他方式告知服务端只发文本。

一个更规范的做法是:在消息事件里判断数据类型。微信小程序的onMessage拿到的res.data,如果是二进制,它实际上就是一个ArrayBuffer。你可以这样判断:

socketTask.onMessage((res) => { const data = res.data; if (typeof data === 'string') { // 文本消息,一般是 JSON handleTextMessage(JSON.parse(data)); } else { // ArrayBuffer,需要按协议解析 const bytes = new Uint8Array(data); handleBinaryMessage(bytes); } });

发送二进制的时候,你可以直接传ArrayBuffersendSocketMessage

const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04]); socketTask.send({ data: bytes.buffer, success() { console.log('二进制消息发送成功'); }, fail(err) { console.error('发送失败', err); } });

这里有个容易忽略的点:Uint8Arraybuffer才是ArrayBuffer,很多新手直接把Uint8Array传进去,导致发送数据异常或者只发了第一个字节。还有,如果你要拼接多个ArrayBuffer,建议先用Uint8Array操作,最后统一取.buffer,不要直接在ArrayBuffer层面做拼接,API 不方便,也容易出错。

实际项目中,文本和二进制可以混合使用。比如控制指令用一段紧凑的二进制协议,状态上报用 JSON 文本。这样做的理由是:JSON 可读性强、便于调试,但体积大、解析慢;二进制协议紧凑、性能高,但可读性差、需要严格定义字段。协议设计的选择,我会在第四部分详细讲。

3.3 心跳保活与断线重连(含代码)

做过长连接的人都知道一句话:长连接不是连上就完事了,重点在保活和断线重连。尤其是移动端,网络环境极不稳定,Wi-Fi 切 4G、进电梯、地铁过隧道,都可能导致连接被静默断开。更麻烦的是,这种断开有时候 TCP 层根本感知不到,因为运营商路由器可能在空闲超时后把连接杀掉,但两端都没有收到 FIN 包,连接看起来还“活着”,实际上已经死了。

解决这个问题只有一招:主动探测——心跳包。小程序作为客户端,每隔一段时间给服务端发一条心跳消息,服务端收到后回一条确认。如果在规定时间内没收到任何数据,就可以认为连接已经异常。心跳不只为了保活,还能顺便检测链路质量。

下面是我在项目里用的一套比较成熟的心跳和重连策略。

class SocketClient { constructor(options) { this.url = options.url; this.heartbeatInterval = options.heartbeatInterval || 15000; // 心跳间隔:15秒 this.reconnectMaxTimes = options.reconnectMaxTimes || 10; // 最大重连次数 this.reconnectDelay = options.reconnectDelay || 3000; // 重连基础延迟:3秒 this.task = null; this.heartbeatTimer = null; this.reconnectTimer = null; this.reconnectTimes = 0; this.isManualClose = false; } connect() { this.isManualClose = false; this.task = wx.connectSocket({ url: this.url }); this.task.onOpen(() => { console.log('连接建立成功'); this.reconnectTimes = 0; // 重连成功后重置重连次数 this.startHeartbeat(); // 这里可以发送业务登录消息,比如鉴权 token }); this.task.onMessage((res) => { // 收到任何消息,说明链路是通的,可以重置心跳的计时基准 this.resetHeartbeat(); // 业务处理 }); this.task.onClose(() => { console.log('连接关闭'); this.stopHeartbeat(); if (!this.isManualClose) { this.scheduleReconnect(); } }); this.task.onError((err) => { console.error('连接错误', err); // onError 有时候不会自动触发 onClose,最好手动关闭连接再走重连流程 this.closeSocket(); }); } send(data) { return new Promise((resolve, reject) => { if (!this.task) { reject(new Error('Socket 未连接')); return; } this.task.send({ data, success: resolve, fail: reject }); }); } startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer = setInterval(() => { this.sendHeartbeat(); }, this.heartbeatInterval); } sendHeartbeat() { const heartbeatMsg = JSON.stringify({ type: 'ping', ts: Date.now() }); this.task.send({ data: heartbeatMsg, fail: (err) => { console.warn('心跳发送失败', err); // 连续发送失败几次可以考虑主动断开连接 } }); } resetHeartbeat() { // 简单实现:收到任意消息都清掉计时器,重新开始 this.stopHeartbeat(); this.heartbeatTimer = setTimeout(() => { this.sendHeartbeat(); }, this.heartbeatInterval); } stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } scheduleReconnect() { if (this.reconnectTimes >= this.reconnectMaxTimes) { console.error('重连次数达到上限,停止重连'); return; } const delay = this.reconnectDelay * Math.pow(2, this.reconnectTimes); // 指数退避 this.reconnectTimes += 1; console.log(`将在 ${delay}ms 后进行第 ${this.reconnectTimes} 次重连`); this.reconnectTimer = setTimeout(() => { this.connect(); }, delay); } close() { this.isManualClose = true; this.stopHeartbeat(); this.closeSocket(); } closeSocket() { if (this.task) { try { this.task.close({}); } catch (e) { // 忽略 } this.task = null; } } } module.exports = SocketClient;

心跳间隔怎么选?太短浪费流量和电量,太长会导致故障发现不及时。我的经验值是:业务消息频率高的时候,15 秒一次心跳足够;如果业务本身就是秒级高频交互,甚至可以不单独发心跳,靠业务消息就能判断链路状态。但如果你做的是低频设备控制,比如半小时才操作一次,那么心跳间隔最好设置在 10 到 30 秒之间。

断线重连为什么要用指数退避?因为你如果每隔固定 3 秒重连一次,一旦服务端出故障,大量客户端会同时发起连接,形成“重连风暴”,把服务端彻底打垮。指数退避的意思是:第一次失败等 3 秒,第二次等 6 秒,第三次等 12 秒,逐步加大间隔,让重连请求错峰。最多重连 10 次后如果还没成功,说明网络或服务端有大问题,这时候就不该无脑重连了,应该提示用户手动刷新。

4. 传输协议设计:从裸 Socket 到可靠消息

4.1 粘包和半包问题

做 TCP 编程的人都知道粘包和半包,WebSocket 协议本身就解决了这个问题,因为它基于帧传输,一条消息就是一个完整的 frame。所以你在小程序里用 WebSocket 收发消息,不会遇到传统 TCP 的粘包问题。

但有个前提:你必须在“一条 WebSocket 消息”里放完一个完整的业务包。如果你习惯性地把多条业务消息拼在一个大字符串里一次性 send,或者把一条业务消息拆成多个 send,那么服务端(或小程序端)接收时就会出现类似粘包、半包的问题。很多从裸 TCP 转过来的后端同学容易在这一点上栽跟头:在小程序里一次性 send 了一长串 JSON,结果服务端收到的不是一次完整数据,而是被拆成了多帧;或者在服务端把多个业务消息塞进一个 WebSocket 帧里发过来,小程序端onMessage一次性拿到一大坨,需要自己再拆分。所以我给的建议是:一条 WebSocket 消息只承载一个完整的业务消息,发送前先做好数据的序列化,接收时直接按消息粒度解析。

4.2 自定义二进制协议设计举例

文本协议(JSON)开发调试方便,但体积大、解析慢。如果你的业务场景是高频控制指令,或者对带宽和速度敏感,我建议定义一套紧凑的二进制协议。下面是一个我实际用过的协议格式示例:

消息头(5字节): - magic (1字节): 固定值 0x5A,用来校验帧头 - version (1字节): 协议版本号 - type (1字节): 消息类型,比如 0x01 表示心跳,0x02 表示控制指令 - length (2字节): 消息体长度,大端模式 消息体(length字节): - 具体业务数据,比如设备ID、控制码、参数

对应的生成和解析代码可以这样写:

function encodePacket(type, payloadBuffer) { const header = new ArrayBuffer(5); const headerView = new DataView(header); headerView.setUint8(0, 0x5A); headerView.setUint8(1, 0x01); headerView.setUint8(2, type); headerView.setUint16(3, payloadBuffer.byteLength, false); // false 表示大端模式 const packet = new Uint8Array(5 + payloadBuffer.byteLength); packet.set(new Uint8Array(header), 0); packet.set(new Uint8Array(payloadBuffer), 5); return packet.buffer; } function parsePacket(buffer) { const bytes = new Uint8Array(buffer); if (bytes.length < 5) { return null; } const view = new DataView(buffer); const magic = view.getUint8(0); if (magic !== 0x5A) { throw new Error('错误的帧头'); } const version = view.getUint8(1); const type = view.getUint8(2); const length = view.getUint16(3, false); if (bytes.length !== 5 + length) { throw new Error('消息不完整,可能还有后续分片'); } const payload = bytes.slice(5, 5 + length); return { version, type, payload }; }

设计协议时有几个小建议:

  • 大端还是小端要固定下来,推荐大端,也就是网络字节序,通用性最好。
  • 尽量用无符号整数存长度字段,避免符号位带来的负数问题。
  • 协议里一定带版本号,方便以后升级,不至于因为一个字段调整导致新老版本直接在公司群里互相扯皮。
  • magic 字段别省,它能挡住大部分“连错服务器、收到脏数据”的情况。

4.3 超时与错误处理

长连接除了断线,还可能遇到“消息发出去了,但一直没收到响应”的情况。这在控制类业务里特别致命:用户点了“开门”,界面一直转圈,谁都不知道到底开没开。所以应用层一定要有超时机制。

最简单的做法是:发送请求时生成一个唯一的msgId,把msgId和发送时间记在一个 Map 里,同时启动一个定时器。收到响应时,根据响应里的msgId找到对应条目,清除定时器。如果定时器到点还没收到响应,就认为这次请求超时,提示用户重试。

const pendingMap = new Map(); let nextMsgId = 1; function sendWithAck(type, payload, timeout = 5000) { const msgId = nextMsgId++; const packet = encodePacket(type, packWithMsgId(msgId, payload)); return new Promise((resolve, reject) => { const timer = setTimeout(() => { pendingMap.delete(msgId); reject(new Error('请求超时')); }, timeout); pendingMap.set(msgId, { resolve, reject, timer }); socketTask.send({ data: packet }); }); } function onMessageReceived(buffer) { const { type, payload } = parsePacket(buffer); const msgId = extractMsgId(payload); if (pendingMap.has(msgId)) { const pending = pendingMap.get(msgId); clearTimeout(pending.timer); pendingMap.delete(msgId); pending.resolve(payload); } }

这套机制写起来不难,但能解决大量实际问题。尤其是“看起来连接还活着,但服务端已经没在处理”的情况,超时能帮你及时发现问题,而不是让用户一直干等。

5. 真机调试与抓包排查实录

5.1 常见的连不上、收不到数据案例

第一类问题:真机上报“url not in domain list”。前面提过,这是域名白名单问题。解决办法是后台配置 socket 合法域名,然后等几分钟再试。要注意的是,如果你连接的是ws://(不带 s),真机上是连不上的,正式环境必须wss://

第二类问题:开发者工具一切正常,真机连不上。这种情况大概率是服务器防火墙没有放行 wss 对应的端口,或者 SSL 证书链不完整。用手机浏览器直接访问https://你的域名:端口,如果能正常打开且浏览器地址栏显示锁形图标,说明证书没问题。如果浏览器都报证书错误,那就要先修证书。

第三类问题:连接能建立,但收不到消息。我排查过几次,原因出在服务端发送的是二进制数据,小程序端按字符串解析,导致乱码或直接忽略。这时候在onMessage里先console.log打印一下res.data的类型,确定是字符串还是ArrayBuffer,再决定解析逻辑。还有一种情况是服务端发消息的时候用了sendfin字段或混淆了 WebSocket 的 close 和 send,这类问题要用抓包工具对比才能发现。

第四类问题:连接建好之后过几分钟就被断开。大多数是 NAT 超时或服务端主动踢人。解决方法就是用心跳,让连接一直有数据流动,运营商的 NAT 表项就不会因为空闲被删掉。如果心跳都发了还是被断,检查一下服务端有没有按来源 IP 做连接数限流,或者对空闲连接设置了最大生存时间。

现象可能原因排查优先级
真机url not in domain listsocket 合法域名未配置或未生效
开发者工具正常,真机连不上端口未放行 / 证书错误 / 域名未备案
连接成功但收不到数据数据类型不一致 / 服务端发送异常
连接过几分钟被断开NAT 超时 / 服务端空闲踢人 / 心跳缺失
发送消息失败Socket 已断开未感知 / 发送数据格式错误
重连失败次数过多服务端恢复慢 / 重连策略太激进

5.2 charles 等工具抓包要点与局限

调试小程序网络请求,很多人习惯开 Charles,但要注意,PC 端 Charles 默认只能抓到 HTTP/HTTPS 的请求,对 WebSocket 连接只能看到升级握手那一下,后面的数据帧在 Charles 里看不到明文的 WS 帧。而且真机小程序走的是微信客户端自身管理的网络栈,你在 PC 上通过代理抓包,经常连握手都看不到。

如果你用开发者工具做调试,可以打开 console 面板,给socketTask挂上onMessageonSend日志,这是最直接的方式。或者接一个 vConsole,在真机页面上直接看 Log。真机上如果开启“调试”模式,vConsole 会浮在小程序页面上,console.log的信息都能看到。

要抓 WebSocket 帧内容,我比较推荐用服务端日志来配合:在小程序端打日志,同时在服务端把每条收到的原始帧打印出来,两边时间戳一对比,就能快速定位是“没发出去”、“发出去了服务端没收到”还是“服务端回了但小程序没解析”。

5.3 上线前必做的连接检查清单

这里列一个我在每次发版前都会过一遍的检查清单,照着做基本能防住大部分线上事故:

  • socket 合法域名已配置且证书在有效期内,用手机浏览器验证过直连没问题。
  • 小程序前后台切换时能正确处理连接状态,后台挂起时间长了回前台能自动重连。
  • 心跳间隔与业务消息频率不冲突,心跳发送失败连续 3 次会主动断开并触发重连。
  • 重连有最大次数限制,避免无限重连造成服务器压力。
  • 所有发送给服务端的数据都做了长度校验,单帧数据控制在合理范围内。
  • 消息里带msgId,超时未响应有明确提示,不会让用户等死。
  • 服务端有连接数监控,能对异常断连进行告警。
  • 小程序版本更新时,旧版本的长连接不会被新版本顶掉或造成冲突(服务端需要做连接的账号维度管理)。

另外一个容易被忽视的点:小程序的wx.connectSocket在 Android 和 iOS 上的底层行为有一些差异。iOS 上 WebSocket 掉线后重连的触发时机通常更及时,Android 部分定制 ROM 对网络权限控制更严格,后台息屏后 Socket 大概率会被杀掉。所以 Android 机的用户更容易感知到“过一会儿就掉线、再点进来要等重连”,这是正常的,不要以为是自己代码出了问题。

6. 从项目实战再往后的一点心得

我个人的习惯是:只要小程序里用了 WebSocket,服务端就一定配一套连接状态监控。连接数、消息量、心跳成功率、重连次数这几个指标,比什么都重要。有一次我们线上出现大面积掉线,就是靠监控发现半小时内重连请求突然翻倍,排查后确定是服务端一台机器负载过高导致 accept 变慢。如果没有监控,这种问题可能得等用户投诉才会暴露。

另外,协议设计上尽量不要把逻辑写在 frame 层。也就是说,WebSocket 帧里承载的是业务消息,业务消息里再用 JSON 或者二进制自定义协议去区分具体动作。这样将来就算把 WebSocket 换成 MQTT 或者其他传输层,业务逻辑也还能复用。

最后分享一个做设备控制类项目的小技巧:把“连接状态”做成全局可观察的数据。页面进入时订阅连接状态,一旦断线,界面上立刻显示一个“连接已断开,正在重连”的提示条,重连成功再自动消失。这比用户点了按钮没反应才意识到掉线要友好得多。这个状态同步可以通过全局 store(比如 mobx、vuex)来实现,原生小程序也可以用getApp().globalData加事件订阅来做。状态可视化,是长连接项目里最能提升体验的一个小细节,强烈建议加上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询