
简介面向微信小程序开发者的TCP/IP长连接通信示例源码包适配需要在微信小程序中实现实时消息推送、在线状态维持等场景的初中级开发者。资源包含微信小程序前端页面与配套服务端代码服务端以Go语言实现核心逻辑覆盖连接建立、心跳保活、消息收发等长连接常见环节便于快速理解小程序端与服务端的交互流程。包内共35个文件以Go服务端文件为主同时包含小程序前端所需的js逻辑、wxml页面结构、wxss样式以及json配置等压缩后仅39KB体积小巧、结构清晰适合直接导入工程学习或二次改造。目前已有397人学习下载资源配有运行截图可对照检查界面效果与服务端响应状态。通过该源码开发者能掌握微信小程序建立长连接、处理数据帧及异常断线等实用方法并积累简单可运行的全栈通信方案。1. 微信小程序里的 TCP/IP 长连接先分清 WebSocket 与原始 socket 的边界做即时通讯、设备指令下发、行情推送这类业务时最怕的问题是“明明在线消息却延迟”。HTTP 的请求-响应模型在小程序里还叠加了“切后台就挂”“wx.request 有并发限制”这些约束所以实时链路基本都走长连接。微信小程序源码里常见的 TCP/IP 长连接方案通常有两种载体一是用 wx.connectSocket 建立的 WebSocket二是用 wx.createTCPSocket 建立的原始 TCP 连接。前者是微信生态里的默认路径后者是当你遇到自定义协议、二进制帧、私有设备服务端时不得不选的路。这篇文章把两条路的选型依据、最小实现、保活参数和真机抓包验证讲透面向已经写过页面级业务、要去啃连接层稳定性的开发者。2. 微信小程序 TCP 长连接选型wx.connectSocket 与 wx.createTCPSocket 的取舍先澄清一个容易误判的点微信小程序并不直接暴露浏览器里那种new WebSocket()但wx.connectSocket的语义和它几乎一致走的是标准的 ws/wss 协议而wx.createTCPSocket是基础库 2.25.0 之后加入的原始套接字能力可以直接连到任意 IP 和端口自行定义应用层协议。选错载体后续的域名配置、心跳设计、拆包逻辑全部会跟着错。2.1 WebSocket 在小程序里的成熟度与隐藏成本wx.connectSocket的成熟度远高于原始 TCP。它的三大优势是协议被主流后端网关Nginx、云负载均衡原生支持、自带帧格式和 masking、微信开发者工具里可以直接模拟。对绝大多数聊天室、客服、行情推送场景WebSocket 是默认答案因为接入成本最低。但 WebSocket 并非没有隐藏成本。生产环境里小程序端必须把wss://域名配置到小程序后台的 socket 合法域名清单中且要求域名已完成备案。另一个容易被忽略的是WebSocket 的分帧并不等同于业务协议的分包你依然要自己在 onMessage 里处理粘包和半包这个问题在第三章里详细展开。也就是说选 WebSocket 只省了 TCP 层的握手省不了应用层的内存管理。2.2 wx.createTCPSocket 的能力边界与启用条件当你的服务端协议不是 HTTP 之上的 WS 握手而是自定义的 TCP 协议——比如物联网设备网关、老旧 C/S 架构服务端或者直接内置了私有帧格式——就必须用wx.createTCPSocketconst tcpSocket wx.createTCPSocket() tcpSocket.connect({ address: 172.16.10.23, port: 9501 })这段代码创建一个 TCP 套接字并连接指定 IP 和端口。注意createTCPSocket不要求在后台配置 wss 域名它直接面向 IP 和端口发起连接这是和 WebSocket 在运维层面最大的差别。代价是没有自动的握手协议所有字节都要自己拼没有内置的心跳帧保活必须自己做也没有 masking 和数据帧约束粘包问题需要从第一个字节开始设计。iOS 和 Android 的收发行为与断线感知也存在差异需要各端回归。2.3 选型决策别让“源码里有 TCP”绑架你的架构拿到一份“微信小程序源码含截图TCPIP长连接”的参考实现时先判断它用的是哪套 API。如果服务端并不存在非原始 TCP 不可的理由我一般会建议团队重新评估一次 WebSocket。下面这张表可以直接作为评审时的核对清单判断维度WebSocket (wx.connectSocket)原始 TCP (wx.createTCPSocket)协议标准RFC 6455帧格式固定无帧纯字节流后端接入Nginx / SLB 可直接代理需要四层代理或直连域名白名单需要 wss:// 合法域名连 IP不依赖后台域名配置心跳可复用 WS 消息通道应用层从零设计二进制流支持 ArrayBuffer但包一层帧原生字节流解析更直接调试工具开发者工具可直接模拟必须真机 抓包辅助选型的最终判据是服务端有没有现成的 WS 网关。有就优先 WebSocket协议锁死在 TCP 私有格式上再用原始 TCP。别为了“用上源码里的 TCP 代码”而刻意给业务增加无谓的复杂度。3. 微信小程序 TCP 长连接源码落地createTCPSocket 的最小可运行方案这一章直接给一套能跑的最小代码。假设服务端监听在192.168.2.11:9501协议是 4 字节长度头 JSON 体的自定义帧。我们聚焦连接的建立、消息收发、状态日志和基础拆包心跳与重连放到第四章。3.1 建立连接connect、onConnect、onError 三件事// 下面的代码放在 Page 的一个 method 中 // 回调统一使用箭头函数确保 this 指向页面实例 this.tcpSocket wx.createTCPSocket() this.tcpSocket.onConnect(() { // 连接建立成功此时才允许 write console.log(TCP connected) this.setData({ linkState: connected }) }) this.tcpSocket.onError((err) { // err.errMsg 会给出失败原因如 connect fail console.error(TCP error, err) this.setData({ linkState: error }) }) this.tcpSocket.onClose(() { console.log(TCP closed) this.setData({ linkState: closed }) }) this.tcpSocket.connect({ address: 192.168.2.11, port: 9501 })这里的关键点是onConnect回调。TCP 是流式协议连接建立后才能调用write在onConnect之前写入会触发错误。onError里最常见的错误是socket connect fail大概率是 IP 不可达或端口没放通排查时先在本机执行nc -vz 192.168.2.11 9501确认服务端可连再回来查小程序端代码。3.2 发送与接收数据的正确姿势发送数据前必须把业务对象编码成 ArrayBuffer。直接传字符串会抛类型错误而且 JSON 里的中文必须走 UTF-8 编码const body JSON.stringify({ type: ping, ts: Date.now() }) const bodyBuffer new TextEncoder().encode(body) const header new ArrayBuffer(4) const headerView new DataView(header) headerView.setUint32(0, bodyBuffer.byteLength, false) // false 表示大端序 // 组装4 字节长度头 消息体 const packet new Uint8Array(4 bodyBuffer.byteLength) packet.set(new Uint8Array(header), 0) packet.set(bodyBuffer, 4) this.tcpSocket.write(packet.buffer)说明setUint32(0, length, false)的第三个参数是字节序标识false表示大端序网络序这是大多数 TCP 服务端约定的字段序不要随意改必须与服务端定义保持一致。write接收 ArrayBuffer内部负责排队发送但不会替你分片超过 MTU 的长包依然由系统栈处理。接收方向onMessage回调拿到的是 ArrayBuffer先收集到一个缓冲区等待后续拆包器处理this.tcpSocket.onMessage((res) { const chunk new Uint8Array(res.message) this.pushToRecvBuffer(chunk) })3.3 先把字节流拆成业务包4 字节头的解析TCP 是流协议一次 onMessage 可能收到半个业务包也可能收到好几个业务包所以拆包是长连接代码里必须有的基础设施。按“4 字节大端长度 JSON 体”的约定拆包循环如下handleFrame(buffer) { if (!this.recvBuffer) { this.recvBuffer buffer } else { this.recvBuffer this.concat(this.recvBuffer, buffer) } while (this.recvBuffer.byteLength 4) { const view new DataView(this.recvBuffer) const len view.getUint32(0, false) if (this.recvBuffer.byteLength 4 len) { break // 半包继续等后面的数据 } const payload this.recvBuffer.slice(4, 4 len) const json JSON.parse(new TextDecoder().decode(payload)) this.onBusinessMessage(json) this.recvBuffer this.recvBuffer.slice(4 len) } } concat(a, b) { const tmp new Uint8Array(a.byteLength b.byteLength) tmp.set(new Uint8Array(a), 0) tmp.set(new Uint8Array(b), a.byteLength) return tmp.buffer }这段代码的关键是while循环必须反复拆直到缓冲区里剩余字节不足一个包头否则一次收到多个包时会漏包。每次拆完用slice(4 len)把已消费部分丢弃而不是重新分配一个新的大对象。这里concat用Uint8Array.set做字节拷贝避免频繁使用push导致的性能抖动。3.4 状态日志与源码截图对应的可观测性带截图的参考源码通常会把linkState和最近一条消息渲染到页面上截图里能看到 connected / waiting / closed 三种状态以及收到的报文。复现时我会在工程里再加一条日志队列把每次收发耗时、字节数、时间戳追加到data.logs页面直接滚动展示this.setData({ logs: [...this.data.logs, [${Date.now()}] send ${packet.byteLength} bytes] })这段日志不是为了好看而是为了跟第五章的抓包结果对齐。真机上报的收发时刻要与 Wireshark 里的包时间戳一一对应差超过 500ms 就说明你的队列处理里有隐性延迟。相关 API 参数汇总如下API/回调关键参数或字段注意事项tcpSocket.connectaddress: string、port: number必须在onConnect前调用且只调用一次tcpSocket.writeArrayBuffer单次写入尽量控制在 32KB 内超长报文自行切片tcpSocket.onMessageres.message为ArrayBuffer不保证一次回调包含一个完整业务包tcpSocket.onClose无参数主动 close 和被动断开都会触发注意wx.createTCPSocket依赖基础库 2.25.0 及以上且需要在真机环境下验证。开发者工具的网络模拟不会覆盖 MTU 与系统断线行为结论以真机为准。4. TCP 长连接保活三件套心跳、断线重连与 IP 地址变化处理移动网络下的 TCP 长连接非常脆弱。Wi-Fi 切蜂窝、电梯里信号中断、后台运行被系统回收都会让连接进入假死状态。所谓假死就是两端 TCP 栈都认为连接没断但中间路由器已经把它丢弃了。要对抗假死心跳、断线重连、网络切换感知三件事缺一不可。4.1 心跳机制应用层 ping 比 TCP keepalive 更可控wx.createTCPSocket底层虽然是系统 socket但小程序没有开放设置 SO_KEEPALIVE 的接口而且系统 keepalive 默认探测周期太长对移动端毫无意义。因此应用层心跳必须自己实现。常见做法是每 30 秒发一个心跳包服务端 90 秒内收不到任何包就主动断开。客户端用 setInterval 发送同时启动一个超时计时器若在 10 秒内没收到任何服务端数据判定链路失效并触发重连startHeartbeat() { this.heartbeatTimer setInterval(() { if (!this.isConnected) return // sendFrame 是第三章里封装好的编解码入口 this.sendFrame({ type: ping }) this.pingTimeout setTimeout(() { console.warn(heartbeat timeout, reconnect...) this.reconnect() }, 10000) }, 30000) }逻辑说明30 秒发送间隔、10 秒本地超时是微信小程序里比较保守的参数。间隔太短会增加服务端负载和手机功耗太长则对假死链路不敏感。调参时先量线上请求包大小心跳包压缩到 10 字节以内时30 秒间隔对电量和流量的影响可以忽略。4.2 断线重连指数退避别写成死循环重连最容易犯的错是“断开后立刻重连”在移动网络恢复瞬间会造成连接风暴。指数退避是标准解法第一次失败等 1 秒第二次 2 秒第三次 4 秒封顶 60 秒直到成功reconnect() { if (this.reconnecting) return this.reconnecting true const delay Math.min(1000 * Math.pow(2, this.retryCount), 60000) setTimeout(() { this.closeSocket() // 先清理旧句柄避免回调风暴 this.initTCPSocket() // 重新 createTCPSocket connect this.retryCount this.reconnecting false }, delay) }参数说明this.retryCount每次连接成功时归零这样网络稳定后就不会一直按 60 秒的间隔重试。代码里的this.reconnecting是一个互斥锁防止重复重连。最关键的是closeSocket()必须先执行否则基础库里会保留旧连接句柄导致 onClose 回调被反复触发。4.3 IP 地址变化监听网络状态不是可选项服务端 IP 是固定的但客户端 IP 会随网络切换变化。wx.onNetworkStatusChange用来感知网络切换正确用法是在回调里主动断开并重置重试计数而不是等服务端超时。注意这个监听应该全局注册一次建议放到App.onLaunch里wx.onNetworkStatusChange((res) { if (res.isConnected) { console.log(network back, force reconnect) this.closeSocket() this.retryCount 0 this.initTCPSocket() } else { this.closeSocket() } })这里有个容易被忽略的细节res.networkType从 wifi 变成 4g 时旧 TCP 连接基本不可用主动closeSocket()能让 onClose 状态回调尽快收尾避免 UI 停留在“连接中”。只做心跳不做网络监听切网后会有一段长达数秒的假在线状态这期间用户发的消息全部进入待发送队列体验非常差。4.4 保活参数速查表下表给出我在生产环境里常用的初始参数可以直接抄作业参数推荐值调整方向心跳发送间隔30s弱网环境调小到 10-15s省电省流量调大到 60s心跳超时判死2-3 个心跳周期调小加快假死感知但误判率升高重连初始间隔1s服务端承载强可降到 0.5s重连最大间隔60s超过后仍失败建议提示用户检查网络单包数据上限32KB超过需应用层切片避免系统缓冲压力线上监控发现重连次数激增时先看是心跳超时触发还是 onClose 触发再按表反推是间隔问题还是链路问题。如果 onClose 频繁说明对端主动断开去查服务端的连接清理策略如果心跳超时频繁说明中间链路在丢包优先调小心跳间隔而不是调大重连次数。5. 真机抓包验证把小程序 TCP 长连接的收发流程完整看一遍5.1 开发者工具能做的事逻辑验证不等于网络验证开发者工具里能模拟无网络和 Wi-Fi 切换但注意wx.createTCPSocket在工具中的行为并不一致具体表现为不校验真实 MTU、不模拟系统级的断线感知。所以它能验证业务逻辑不能验证网络边界。所有连接层问题最后都要回真机验证。5.2 真机抓包用 Wireshark 看三次握手和心跳时序最可靠的抓包姿势是电脑开一个热点手机连这个热点Wireshark 选择对应的热点网卡接口过滤器填tcp.port 9501 ip.addr 192.168.2.11然后去小程序页面触发一次连接和心跳观察 TCP 三次握手是否完成、心跳包是否按 30 秒间隔到达。一个有效技巧是专门写一个“诊断页”页面上只放两个按钮——“连接”和“发一帧”每次发帧时打印字节数和时间戳。这样抓包时不用在复杂页面里来回找能把每条 TCP segment 和应用日志一一对上。服务端侧可以同步用 tcpdump 确认收包情况tcpdump -i any tcp port 9501 -nn -A-A参数把包内容以 ASCII 打印出来适合验证 JSON 体编码是否正确。若发现 payload 里中文被编码成问号说明客户端没有在TextEncoder里指定 UTF-8回第三章检查编码链路。5.3 最后一个技巧返回前台时主动重建连接小程序切后台时JS 定时器会被系统冻结但 TCP 连接本身通常不会立刻断开。当你回到小程序时onShow 里检查连接状态和最后心跳时间若超过两个心跳周期直接断开重建比等服务端超时快得多onShow() { const gap Date.now() - this.lastHeartbeatTs if (gap 60000 this.isConnected) { this.closeSocket() this.initTCPSocket() } }这个技巧能把“回到小程序后消息卡 30 秒”的用户体感问题压缩到一次重连的手感之内。本文还有配套的精品资源点击获取