
简介面向微信小程序 TCP/IP 长连接开发者的完整源码包覆盖小程序客户端与 Go 语言服务端两端工程适合需要在小程序中实现稳定长连接通信、研究网络协议与数据交互的初中级开发者。资源共包含 35 个文件以 Go 服务端源码为主18 个 .go 文件配合小程序前端页面所需 js、wxml、wxss、json 等文件另有 html、md 文档及 license 说明整体压缩包仅 39KB结构紧凑便于阅读。已有 2017 人学习使用可作为从零搭建长连接 demo 的参考模板。包内目录划分为服务端与客户端两大模块服务端提供 TCP/IP 长连接处理逻辑和基础协议实现客户端展示小程序如何建立连接、收发数据及处理状态变化同时附带 README 帮助快速理解工程结构与启动方式。对刚接触小程序长连接、或想在项目里快速集成通信能力的开发者是一份轻量且可直接改写的实用源码。1. 微信小程序做 TCP/IP 长连接一次真实可落地的选型与排错复盘很多人一听到「微信小程序 TCP/IP 长连接」第一反应是拿 wx.request 去怼一个 TCP 服务端结果连上就断、收到消息乱码、一退后台就掉线。实际上微信小程序里能建立 TCP 长连接的只有 wx.connectSocket 这条 WebSocket 通道而真正的 TCP 裸连接在 iOS 端被系统限制得死死的Android 上行为也不一致。这篇文章不绕弯子直接把「小程序端怎么连 TCP 服务、IP 和端口怎么配、心跳和重连怎么做、源码怎么组织」讲透适合正在做智能硬件控制、IM、实时数据推送的开发者照着改。2. 先厘清小程序里的 TCP/IP 长连接WebSocket 与 TCP 的关系与边界2.1 为什么小程序不开放裸 TCP而是用 WebSocket 封装微信小程序没有提供类似 Node.js 里 net.connect 那种直接建立 TCP socket 的 API。小程序运行在微信客户端提供的 JS 运行时里网络层被微信统一管控开发者能碰到的只有 wx.requestHTTP 短连接、wx.connectSocketWebSocket、wx.sendSocketMessage、wx.onSocketMessage 这一套。WebSocket 协议本身是建立在 TCP 之上的握手阶段走 HTTP Upgrade之后就是全双工的帧数据传输。所以你要在小程序里维持一个到服务器的长连接实际做的是 WebSocket 长连接底层依然是 TCP/IP 协议栈在跑。这里有个关键认知WebSocket 不改变 TCP 的连接语义但多了一层帧封装服务端必须实现 WebSocket 协议才能和小程序对接。如果你手里的服务端是纯 TCP 的比如用 Node.js 的 net 模块写的、用 Python socket 写的、或者 Java ServerSocket 写的那小程序不能直接连。你需要在这类 TCP 服务前面加一层 WebSocket 网关让网关把 WebSocket 帧解出来再通过内部 TCP 转发给业务服务端。常见做法是部署一个独立的网关进程专门做协议转换。提示判断一个服务端能不能直接接小程序最简单的标准是——它能不能响应 HTTP Upgrade 请求。能就能走 WebSocket不能就需要做协议转换层。2.2 IP 地址、域名与端口在小程序侧的配置规则微信小程序对网络请求有严格的域名白名单限制。对于 WebSocket 连接你必须在微信公众平台的后台「开发管理 - 服务器域名」里配置 socket 合法域名而且这个域名必须是 HTTPS/WSS 开头的合法域名不能直接填 IP 地址加端口。也就是说小程序端你只能写 wss://your-domain.com/path 这样的地址不能写 ws://192.168.1.10:8080。那标题里的 TCP/IP 里的 IP 体现在哪体现在服务端。常见做法是小程序连 WebSocket 网关域名网关内部通过 TCP/IP 连接后端的真实业务服务业务服务的地址配置就是 IP 加端口。比如智能硬件项目里网关拿到小程序下发的指令后用 TCP 连到设备内网 IP9100把指令转发过去。所以 IP 配置出现在服务端或网关侧不直接出现在小程序代码里。如果你在本地开发调试微信开发者工具里可以勾选「不校验合法域名」这时你就能直接用 ws://192.168.x.x:8080 连本地 WebSocket 服务。这个选项只在开发者工具里有效真机预览时必须走合法域名。很多人在这步翻车开发工具里通、真机一测就连接失败十有八九是域名没配或者没走 wss。2.3 长连接状态机连接、心跳、重连、关闭的完整拓扑小程序里的 WebSocket 生命周期比普通 TCP 连接要复杂因为它多了小程序前后台切换、微信客户端网络切换这些干扰因素。实际跑一个长连接项目你需要维护的状态至少包括初始态、连接中、已连接、连接失败、掉线重连中、主动关闭。我一般用一个小对象专门管状态不用 if 散落在各个回调里。每次 wx.connectSocket 成功之后微信会触发 wx.onSocketOpen这时候才能发消息。但注意onSocketOpen 不代表服务器已经处理完你的业务握手对于需要鉴权的系统你还要在 onSocketOpen 后再发一条业务层登录消息等 onSocketMessage 返回登录成功才算真正可用。这个「连接建立」和「业务就绪」之间有个时间差很多新手把登录消息放在 open 之前发结果必然丢消息。后台切换是另一个影响状态机的因素。小程序退到后台超过一定时间微信可能直接断开 WebSocket切回前台时如果发现连接已关闭就应该触发重连逻辑。不要赌微信一定会保持连接重连逻辑是长连接的保命符。下一章给出可直接复制的状态机源码。3. 小程序端长连接源码从建立连接到心跳重连的完整实现3.1 连接管理器封装 wx.connectSocket 与消息分发我通常把整个长连接逻辑封装成一个 socketManager 单例页面或组件只调用它的 connect、send、onMessage、close 方法不直接碰 wx 的 socket API。这样做的好处是多个页面共享同一条连接不会出现每个页面各建一条连接导致服务端连接泛滥的问题消息回调统一分发页面按需订阅不会漏消息。const socketManager (function () { let socketTask null; let isConnected false; let isManualClose false; let reconnectCount 0; const messageHandlers new Map(); function connect(url, options {}) { isManualClose false; socketTask wx.connectSocket({ url: url, protocols: [], success: () { console.log(socket connecting); }, fail: (err) { console.error(socket connect fail, err); options.onFail options.onFail(err); scheduleReconnect(url, options); } }); socketTask.onOpen(() { isConnected true; reconnectCount 0; console.log(socket opened); options.onOpen options.onOpen(); }); socketTask.onMessage((res) { handleIncomingMessage(res.data); }); socketTask.onClose((res) { isConnected false; console.log(socket closed, res.code, res.reason); if (!isManualClose) { scheduleReconnect(url, options); } }); socketTask.onError((err) { console.error(socket error, err); isConnected false; if (!isManualClose) { scheduleReconnect(url, options); } }); } function handleIncomingMessage(data) { // 这里做统一的消息解析比如 JSON.parse let parsed; try { parsed JSON.parse(data); } catch (e) { parsed { raw: data }; } messageHandlers.forEach((handler) { handler(parsed); }); } function onMessage(callback) { const key Date.now() Math.random().toString(36).slice(2); messageHandlers.set(key, callback); return key; } function offMessage(key) { messageHandlers.delete(key); } function send(data) { if (!isConnected || !socketTask) { console.warn(socket not connected, message dropped:, data); return false; } const payload typeof data string ? data : JSON.stringify(data); socketTask.send({ data: payload, success: () {}, fail: (err) { console.error(send fail, err); } }); return true; } function close() { isManualClose true; if (socketTask) { socketTask.close({ code: 1000, reason: manual close }); socketTask null; } } return { connect: connect, send: send, onMessage: onMessage, offMessage: offMessage, close: close, isConnected: () isConnected }; })();这段代码的核心逻辑有三个第一把 wx.connectSocket 的返回值 socketTask 保存下来后续的 onOpen、onMessage、onClose、onError 都挂在这个 task 上第二用 Message 发布订阅模式做消息分发避免页面之间耦合第三用 isManualClose 区分主动关闭和被动掉线主动关闭时不触发重连。参数方面url 是 wss 地址protocols 留空数组即可如果服务端自定义了子协议这里要对应填。这里有个容易被忽略的细节wx.connectSocket 返回的 socketTask 的 onMessage 回调里res.data 可能是字符串也可能是 ArrayBuffer取决于你在 send 时用的是 string 还是 ArrayBuffer。如果做二进制协议比如和硬件设备的 TCP 通信我建议统一用 ArrayBuffer 收发避免微信底层做字符串编码转换导致数据错乱。3.2 心跳机制心跳包格式、间隔与超时判定TCP 长连接的本质是维持一条不关闭的 TCP 连接但中间任何一层网络设备都可能因为空闲回收连接。尤其是 NAT 超时一般移动网络下的 NAT 映射在 30 秒到几分钟内无数据就会失效服务器感知不到但数据已经发不过来了。心跳包的作用就是周期性产生数据流量让 NAT 映射和 TCP 连接保持活性。class Heartbeat { constructor(socketManager, options) { this.socket socketManager; this.heartbeatInterval options.heartbeatInterval || 30000; this.timeout options.timeout || 10000; this.maxMiss options.maxMiss || 2; this.timer null; this.timeoutTimer null; this.missCount 0; } start() { this.stop(); this.timer setInterval(() { const sent this.socket.send({ type: heartbeat, ts: Date.now() }); if (!sent) { this.missCount; if (this.missCount this.maxMiss) { this.onDead(); return; } } this.startTimeout(); }, this.heartbeatInterval); } startTimeout() { this.clearTimeout(); this.timeoutTimer setTimeout(() { this.missCount; if (this.missCount this.maxMiss) { this.onDead(); } }, this.timeout); } reset() { this.missCount 0; this.clearTimeout(); } onDead(callback) { // 心跳连续超时判定连接已死触发重连 this.stop(); socketManager.close(); socketManager.connect(socketManager.url, socketManager.options); } clearTimeout() { if (this.timeoutTimer) { clearTimeout(this.timeoutTimer); this.timeoutTimer null; } } stop() { if (this.timer) { clearInterval(this.timer); this.timer null; } this.clearTimeout(); } }心跳间隔的选型要结合服务端配置和服务端的心跳超时策略。常见做法是客户端每 30 秒发一个心跳包服务端如果 60 秒内没收到任何数据就踢掉连接。这两个数字联动客户端心跳间隔要小于服务端超时时间而且最好留出 2 倍余量避免网络抖动导致服务端误杀。如果服务端在你发心跳后没有响应客户端连续两个心跳周期没收到任何消息就应该判定连接已死。注意这里判定的依据是「没收到任何消息」而不是「没收到心跳响应」因为服务端可能不单独回心跳包。3.3 指数退避重连重连间隔策略与最大次数控制无脑立即重连在服务端看来就是连接风暴。尤其在小程序切后台再切回前台的瞬间如果服务端还没把旧连接释放完新连接又打进来很容易把服务端连接数打满。重连策略我习惯用指数退避加抖动第一次失败等 1 秒第二次 2 秒第三次 4 秒上限 30 秒再加上随机 0 到 300 毫秒的抖动避免多个客户端同时重连导致服务端崩溃。function scheduleReconnect(url, options) { if (reconnectCount 10) { console.error(reconnect exceed max count, stop); // 通知 UI 层连接彻底失败 options.onReconnectFail options.onReconnectFail(); return; } const baseDelay Math.min(1000 * Math.pow(2, reconnectCount), 30000); const jitter Math.floor(Math.random() * 300); const delay baseDelay jitter; reconnectCount; setTimeout(() { console.log(reconnecting, attempt: reconnectCount); if (!isManualClose) { connect(url, options); } }, delay); }重连次数达到上限后不要再无限重连而是把状态抛给 UI 层让用户决定是手动重试还是退出。小程序和传统 App 不一样用户不会一直在前台盯着无限重连会白白消耗用户流量和电量。另外reconnectCount 要在连接成功时清零否则每次成功前失败的次数会一直累积导致后续重连间隔越来越大。这段逻辑其实应该挂在 3.1 的 onOpen 里但为了保持代码独立可读性单独拆出来更清楚。注意小程序退到后台时定时器会被挂起setInterval 不保证准时触发。心跳逻辑在切回前台后要重新校准否则可能因为定时器堆积导致突然触发多次心跳。4. 小程序端与服务端的协议对接TCP/IP 服务怎么和小程序 WebSocket 网关互通4.1 服务端已有的 TCP 协议如何映射到 WebSocket 消息如果你的后端已经有一套成熟的 TCP 私有协议比如 4 字节包头加 JSON 包体的二进制协议小程序走的 WebSocket 通道需要把这套协议原样搬到 WebSocket 消息里。区别只在传输层TCP 里你收到的是字节流需要自己处理粘包拆包WebSocket 帮你在每个消息帧里划好了边界应用层收到一条消息就是一个完整的业务包。这个差异直接决定服务端网关的解析逻辑怎么写。常见做法是在网关里维护一张「连接映射表」键是 WebSocket 连接的唯一标识值是对应后端 TCP 连接的对象。小程序发来的 WebSocket 消息网关原样写入后端 TCP socket后端 TCP 返回的数据网关按后端 TCP 协议拆包后再封装成 WebSocket 帧推回小程序。这一层协议转换是整个方案的咽喉扩容时网关会成为瓶颈所以网关要设计成无状态的连接映射尽量放在 Redis 里多实例都能读写。# 伪代码WebSocket 网关转发到后端 TCP import asyncio import websockets import socket backend_host 192.168.1.10 # 后端真实 TCP 服务 IP backend_port 9100 # 后端真实 TCP 服务端口 async def handle_ws(ws, path): backend_sock socket.create_connection((backend_host, backend_port), timeout5) loop asyncio.get_event_loop() try: async for message in ws: # 小程序发来的消息直接写入后端 TCP await loop.sock_sendall(backend_sock, message.encode(utf-8)) # 同步读取后端返回这里做简化实际要异步轮询 data await loop.sock_recv(backend_sock, 4096) if data: await ws.send(data.decode(utf-8)) except Exception as e: print(connection broken:, e) finally: backend_sock.close()这是最简实现实际生产环境要处理的核心问题包括后端 TCP 响应的异步读取不能阻塞 WebSocket 消息接收所以后端到小程序的推送要单独用一个 asyncio.create_task 去读 socketTCP 粘包问题要自己实现缓冲区拆包4 字节头记录包体长度读满一个包再发给小程序后端 TCP 连接断开时要把错误码映射成 WebSocket close code 推给小程序端让客户端能区分是业务错误还是网络错误。4.2 小程序端收到的消息是字符串还是二进制格式统一策略微信小程序的 wx.sendSocketMessage 和 wx.onSocketMessage 支持 string 和 ArrayBuffer 两种格式。如果不指定默认走字符串WebSocket 帧里的 payload 会被按 UTF-8 解码。如果你的业务数据是纯 JSON字符串格式方便调试但如果你要下发给设备的是二进制指令比如 0xAA 0x55 开头的硬件帧字符串格式会把二进制数据破坏掉。我一般统一走 ArrayBuffer小程序端发送前用 DataView 写入字节接收后用 DataView 按协议逐字节解析。这样做的好处是协议层完全透明网关后端也可以直接透传二进制流不用关心编码问题。// 发送二进制协议帧 function sendBinaryFrame(cmd, payload) { const headerLen 4; const totalLen headerLen payload.length; const buffer new ArrayBuffer(totalLen); const view new DataView(buffer); view.setUint8(0, 0xAA); // 帧头标志 view.setUint8(1, cmd); // 命令字 view.setUint16(2, totalLen, false); // 包体长度大端序 const payloadView new Uint8Array(buffer, headerLen); payloadView.set(payload); socketManager.send(buffer); }注意 setUint16 的第三个参数false 表示大端序。TCP/IP 协议栈默认网络字节序是大端很多嵌入式服务端的协议也按大端来两边对齐了就不会出现数据高低字节颠倒的玄学问题。如果你发现设备端解析出来的长度字段变成 0x5000 这种翻了倍的值九成就是端序不一致。4.3 局域网场景怎么处理真机调试、域名白名单与 IP 地址的取舍很多智能硬件项目其实跑在局域网里设备 IP 是 192.168.1.100服务端就在同一个路由器下面。这种情况小程序能不能直连设备 IP严格来说不行小程序真机环境下 socket 合法域名必须是公网可解析的 HTTPS 域名不能是 IP。但有几种绕过或者说绕行的方案。方案一局域网内部署一台 WebSocket 网关域名解析走内网 DNS 或者做 host 替换小程序端仍然走 wss 连接这个内网网关网关再去连设备 IP。这个方案最规范但要求开发机或硬件环境里有能跑网关的机器。方案二用微信开发者工具调试时勾选不校验合法域名直接连 ws://192.168.x.x:8080这只能解决开发阶段的问题。方案三如果你的设备本身实现了 WebSocket 服务端那小程序可以直接连设备但设备端实现 WebSocket 协议栈的成本不低一般嵌入式设备不会干这事。选择排列上优先推荐方案一。只要在局域网里加一个树莓派或一台旧电脑跑网关设备和网关走 TCP小程序走 WebSocket所有域名白名单问题都绕开了。开发阶段用方案二保底。真机预览想快速测也可以把开发者工具的「不校验合法域名」关掉然后重新编译但正式发布版必须按方案一。提示工具的「不校验合法域名」选项勾选后只对当前项目有效而且真机预览时不生效。别想着靠这个选项上生产环境。5. 排错避坑微信小程序长连接最常见的 7 个坑与排查顺序5.1 开发工具能连真机一测就失败现象在微信开发者工具里连 ws://192.168.x.x:8080 一切正常消息收发都通用手机预览同一个项目连接瞬间失败报 errCode 或直接 onClose。原因真机环境强制要求 socket 合法域名且必须是 HTTPS 开头的 wss 地址另外手机和开发机不在同一网络环境时局域网 IP 根本不可达。解决把服务端部署到有公网域名的机器上同时确保微信公众平台配置了对应的 socket 合法域名。在开发者工具里把地址改为 wss://yourdomain.com去掉「不校验合法域名」选项后再测一遍。确认域名能解析、443 端口能访问再用 telnet 检查端口通不通。注意telnet ip 端口命令怎么看通不通——输入 telnet 域名 443回车后如果黑窗口光标停在空白处就是通了如果提示 Connection refused 或 timed out说明域名解析或端口有问题。5.2 连接能建立但消息发不出去或收不到现象onOpen 已经触发了socketManager.send 也返回 true服务端却收不到或者服务端明明发了消息小程序端 onMessage 一直不触发。原因第一发送的时机不对onOpen 触发不代表底层 socket 已经完全就绪某些 Android 机型在 onOpen 后立即 send 会丢包第二服务端收到的消息被你自己的协议解析丢弃了不是没收到而是解析失败第三触发了微信客户端的并发消息限制send 太密集被限流。解决onOpen 后延迟 50 到 100 毫秒再发第一条业务消息发送前先打印 socketTask 状态在 onMessage 里加一个原始数据日志先用字符串打出来看看服务端到底推了什么。如果服务端收到的字节和自己发的字节不一致重点查编码wx.sendSocketMessage 默认按 UTF-8 编码如果数据里有非 UTF-8 字符就会被替换成 EF BF BD。5.3 心跳正常但连接还是被服务端断开现象小程序端每 30 秒发一次心跳日志显示 send 成功但服务端仍然在 60 秒后主动断开连接。原因服务端判断活性不只依赖你发了多少数据还依赖 TCP 层面的包是否真的到达。局域网或移动网络下客户端发出去的数据可能被 NAT 设备丢弃但本地 send 回调依然显示成功因为 send 成功只代表数据进了系统发送缓冲区不代表服务端收到。解决服务端在收到心跳包后必须回一个心跳响应客户端在连续两次心跳周期内没收到任何服务端数据才判定掉线。把「发送成功」和「服务端可达」区分开前者是本地行为后者是网络行为。另外检查服务端的心跳超时设置是否和客户端心跳间隔匹配比如服务端设了 30 秒超时客户端心跳间隔却是 30 秒一次抖动就超时了。标准做法是客户端心跳间隔 20 秒服务端超时 60 秒留足 3 倍余量。5.4 小程序切后台再回来连接没了现象用户把小程序切到后台刷了一会儿微信回来之后页面还在但发送消息失败onClose 或者 onError 已经触发过。原因微信客户端在后台会挂起小程序的 JS 执行定时器停止网络 socket 也可能被系统回收。这是平台行为不是你的代码问题。解决在小程序切回前台的 onShow 事件里检查 socketManager.isConnected()如果为 false直接重新 connect。注意要在 app.js 的 onShow 里做全局检查而不是在某个页面的 onShow 里做否则用户从一个页面切到另一个页面也会重复重连。重连前的旧连接要确认已经 close避免新旧连接同时在服务端存活。5.5 服务端连接数持续上涨回收不掉现象服务端日志显示客户端连接数每分钟都在涨但没有对应的断开记录很快就把端口或文件描述符耗尽。原因小程序端主动关闭连接时没有调用 wx.closeSocket或者 close 之后没有在 onClose 里做资源清理客户端掉线时服务端依赖 TCP 超时才发现而 TCP 超时可能长达十几分钟。解决客户端在页面卸载时调用 socketManager.close()服务端把 TCP keepalive 打开并设置较短的探测间隔比如 netty 里设置 keepAliveTime 为 60 秒。同时服务端定期扫描空闲连接超过 120 秒无任何数据收发就主动关闭。不要依赖客户端主动发 close 包移动网络下 close 包可能根本到不了服务端。5.6 Android 和 iOS 行为不一致现象同一套代码iOS 上连接稳定Android 上频繁掉线重连或反过来Android 正常iOS 发不出消息。原因微信客户端在不同平台上的网络栈实现有差异尤其是 socket 空闲回收策略和前后台切换时的行为。iOS 对后台网络限制更严格Android 对 WiFi 和移动网络切换更敏感。解决不要针对平台写死逻辑而是把重连策略做强保证任何平台下都能在 2 到 3 次重连内恢复。网络切换时主动监听 wx.onNetworkStatusChange发现网络类型变化立即重连而不是等心跳超时。这个事件在两端都会触发但触发时机略有不同统一处理即可。5.7 开发者工具看日志正常真机上一片空白现象代码逻辑没错开发者工具里连接、心跳、消息都正常真机预览时控制台没有任何输出甚至没有报错。原因可能是真机预览的调试功能没开或者代码里用了 console.log 但没打开 vConsole。更隐蔽的原因是真机上 wx.connectSocket 的 url 里如果带了路径参数某些 Android WebView 内核会导致连接被拒。解决在 app.js 的 onLaunch 里打开 vConsolewx.setEnableDebug({ enable: true })。如果开启后依然没有日志检查 ws 地址是否真的能被手机访问用手机浏览器访问 http://域名:端口 测试可达性如果手机浏览器都打不开那问题不在小程序在网络或服务端配置。6. 进阶自己用 Node.js 写一个 WebSocket 网关把 TCP 服务完整暴露给小程序6.1 网关架构与代码骨架刚才的排错内容里反复提到网关这一节直接给一个能跑起来的最小网关。场景是你有一个老 TCP 服务监听 127.0.0.1:9100协议是 JSON 行协议每行一个 JSON 对象换行符作为包边界。现在要让它能被小程序通过 WebSocket 访问。Node.js 里最省事的组合是 ws 库加 net 模块前者处理 WebSocket 连接后者连接后端 TCP。const WebSocket require(ws); const net require(net); const WS_PORT 8080; const TCP_HOST 127.0.0.1; const TCP_PORT 9100; const wss new WebSocket.Server({ port: WS_PORT }); wss.on(connection, (ws) { console.log(ws client connected); const tcp net.createConnection({ host: TCP_HOST, port: TCP_PORT }, () { console.log(tcp connected to backend); }); tcp.on(data, (data) { // 后端 TCP 数据原样推给小程序 if (ws.readyState WebSocket.OPEN) { ws.send(data.toString(utf-8)); } }); tcp.on(close, () { ws.close(); }); tcp.on(error, (err) { console.error(tcp error:, err.message); ws.close(); }); ws.on(message, (message) { // 小程序消息写进 TCP tcp.write(message.toString()); }); ws.on(close, () { tcp.destroy(); }); });这个网关的核心逻辑在于它的流转方向小程序到 TCP 是单向透传TCP 到小程序也是单向透传网关不做业务解析。参数上TCP_HOST 和 TCP_PORT 对应你后端真实服务的 IP 和端口WS_PORT 是网关对外暴露的 WebSocket 监听端口。ws.on(message) 里收到的 message 在 ws 库默认是 Buffer 或字符串取决于参数这里统一 toString。注意 sync 和 async 的问题如果后端 TCP 服务发的数据没有换行或者一条消息被拆成多个 TCP 包上面代码会出现小程序收到多个半包的情况。改进的思路是维护一个 buffer每次 data 事件到了先用换行符切分完整的一行才推送不完整的挂起。这个简版网关只适合协议本来就是一行一条的业务如果你的 TCP 协议是自定义二进制帧需要自己实现拆包。6.2 生产环境网关注意的四个点第一wss 证书问题。小程序正式环境必须走 wss网关前面要挂 Nginx 做 TLS 终止Nginx 把 wss 流量反向代理到 Node.js 网关的 8080 端口。Nginx 配置里 proxy_set_header Upgrade 和 Connection 要正确设置。第二网关的连接数管理。小程序端每次重连都会建立一条新的 WebSocket 连接如果旧连接没有及时关闭网关侧会堆积一堆 TIME_WAIT 状态连接。在 ws.on(close) 里必须销毁对应的 TCP 连接不要留引用。第三心跳在网关侧的透传。小程序发的心跳包是 JSON网关原样写入 TCP 后端后端如果没按「一行一条」分帧心跳响应可能会被延迟。最稳妥的做法是网关单独解析心跳包自己回一个心跳响应不转发给后端 TCP 服务。这样后端专心处理业务。第四IP 冲突排查技巧。如果网关部署在局域网内多个设备 IP 配置不正确会导致 TCP 连接被路由到错误的设备上。排查方法是在网关上用 arp -a 查看对应 IP 的 MAC 地址和真实设备的 MAC 对比不一致就是 IP 冲突。6.3 验证长连接是否还活着的三种手段第一种看 TCP 连接状态。在服务端或网关上执行 ss -tn | grep 9100如果看到 ESTABLISHED 状态的连接一直在说明 TCP 层还是通的。如果变成 CLOSE_WAIT 或 TIME_WAIT说明连接已经开始回收。这个命令比在小程序端看日志准确因为它反映的是真实网络栈状态。第二种用双重心跳验证。小程序端每 20 秒发一次心跳网关注册自己的心跳日志如果超过 60 秒没收到任何连接上的心跳就把这个 WebSocket 连接主动断开。断开后小程序端必然触发 onClose自然走进重连逻辑。这比让小程序端自己去发现问题更快。第三种端到端链路测试。这个方案适用于硬件控制场景小程序发一条指令设备端执行后返回一条响应链路里任何一环断了都能立即反映出来。我一般会写一个简单的 ping 指令服务端收到就返回 pong 加时间戳小程序端计算耗时超过 2 秒就告警。这个耗时不仅反映链路通断还能侧面看出网络质量。另外一个我自己踩过的坑是TCP 连接建立了但应用层超时。现象是 ss 看连接是 ESTABLISHED但小程序发消息服务端没响应。原因是后端服务线程阻塞或者数据库连接池耗尽TCP 层只是传输通道不代表应用活着。所以长连接监控不能只看 TCP 状态必须配合应用层心跳做最终判断。做这类系统我现在习惯把所有连接状态和重连记录写到一份日志里文件名按天切分每次用户反馈连接不稳定先翻这份日志排查顺序是先看域名解析和端口连通再看证书过期再看服务端连接数最后才怀疑代码逻辑。这份日志救了我很多次比任何调试工具都实在。希望上面这些经验能帮你在做微信小程序 TCP/IP 长连接时少走弯路。核心记住一句话小程序里没有裸 TCP只有 WebSocket但你的 IP 和端口功力全用在网关到后端这段路上。把状态机、心跳、重连这三个基本功做扎实长连接这个问题基本就翻不出什么大浪了。希望帮到你。本文还有配套的精品资源点击获取