
简介这是一份微信小程序 TCP/IP 长连接完整源码面向需要在小程序端实现稳定 socket 通信的开发者适用于即时通讯、远程设备控制、消息推送等场景也适合有一定前端基础的学习者对照理解小程序网络层写法。压缩包共35个文件约39KB以18个 Go 服务端文件、7个 JS 逻辑文件为主配合 WXSS/WXML 页面、JSON 配置与说明文档前后端目录划分明确可直接导入工具运行并二次修改。已有2017人学习/下载可作为正式项目起步模板。读者能拿到一套完整的服务端与小程序客户端实现从 TCP 建连、数据发送接收、连接状态管理到心跳保活均有对应代码配合 README 与 LICENSE可快速梳理长连接整体链路节省自行排查网络 API 的时间适合需要在微信生态内做长连接能力验证或课程设计的开发者。1. 微信小程序里的TCP长连接绕不开的“网络黑匣子”当产品经理把“做一个TCP/IP长连接收发数据”的需求甩过来的时候我第一反应是翻微信官方文档找wx.createTCPSocket。结果大家应该都猜得到——文档里没有社区里有人声称写过但最后都指向“请改用WebSocket”。这不是微信刁难你而是小程序宿主环境根本不把原始socket能力暴露给业务层TCP三次握手只能在系统内核里静默完成业务代码能拿到的最高层抽象就是wx.connectSocket。所以这篇笔记想讲清楚微信小程序里的TCP长连接源码实际上长什么样、为什么那么写、连接断开后怎么办、以及哪几步最容易被坑到返工。适合正在做IoT控制、消息推送、视频下载进度上报或者被服务端强制要求“用TCP协议”却在小程序里找不到出口的开发者。2. 小程序侧的网络协议边界socket能力、IP配置与选型理由2.1 底层网络协议栈定位TCP的三次握手发生在哪个环节要理解微信小程序里的长连接先得把协议栈的位置摆正。手机上跑着一个微信宿主App小程序代码跑在JavaScript引擎里业务JS不能直接创建socket描述符不能调用connect()系统调用更不能自己构造SYN包。TCP三次握手、IP层路由、端口分配全部发生在微信网络模块和操作系统协议栈之间业务层看不到握手过程甚至看不到五元组源IP、源端口、目的IP、目的端口、协议号。你能感知到的只是onOpen回调——系统告诉你“连接可用”。这个边界会直接影响你对“IP”的理解。在小程序里写长连接代码你不会写“连接IP地址1.2.3.4的5000端口”因为域名才是真正的连接入口。wx.connectSocket的url强制要求是ws://或wss://开头IP地址可以放进去例如ws://114.114.114.114:8080/ws但生产环境大概率被微信安全策略拦掉原因看第2.2节。所以这里的“IP”更多是指服务器公网IP的归属地、纯净度、以及服务端能看到的小程序出口IP——也就是“IP纯净度”这个热搜词背后真正关心的问题出口IP是否固定、是否被防火墙标记直接影响长连接的稳定性。2.2 为什么原生TCP被限定IPC、IP端口的可用性与降级路径微信不开放原生TCP官方给的理由基本是安全——如果任意插件都能建立raw TCP连接那恶意代码就能绕过HTTPS、私传数据、伪造请求。但真实项目中更现实的限制有三条第一wx.connectSocket必须在小程序后台配置Socket合法域名域名不能是IP地址开发者工具允许IP真机受限旧版本甚至直接失败第二socket的通信协议必须走WebSocket握手也就是先发HTTP Upgrade请求再升到TCP长连接服务端必须实现WebSocket协议而非裸TCP第三同一个时间点对同一个域名最多只能有5个socket连接超出了就直接onError。那如果服务端就是裸TCP协议、只接受自定义二进制帧怎么办常见做法是加一个“协议转换网关”——一台服务器上跑ws ↔ tcp桥接服务小程序连接WebSocket网关网关再与后端TCP服务通信。我一般用Go写这个网关因为它处理TCP并发连接的成本低二进制转发的代码不超过100行。这样小程序端还是走wx.connectSocketIP/端口校验只发生在网关层后端可以继续保留原有的TCP长连接协议不动底层的四层网络架构。这也是为什么你在网上看到“微信小程序 TCP长连接源码”时里面通常不是wx.createTCPSocket而是“WebSocket协议转换方案”。2.3 选型理由ws、wss与明文TCP的取舍选连接方式时把“微信小程序 vs Android/iOS/鸿蒙”之间差异也纳入考虑微信小程序的socket生命周期完全受宿主控制任何版本更新都可能调整底层网络策略所以选型要保守。从稳定度排序wss://最适合生产环境它把WebSocket数据封装在TLS层内既避免运营商对明文长连接的干扰你不想看到连接被重置、被插入RST包又能在证书层面做域名校验。ws://适合内网联调比如你用局域网IP连开发服务器省掉证书配置。裸TCP在小程序里不存在这个选项。还有个现实因素微信小程序里的视频下载之类的功能如果走“短连接 断点续传”方案每下一个片段就重新TCP握手延迟和失败率都会变大而长连接把握手开销摊到所有数据帧里只是小程序端回收socket的问题。所以长连接源码的关键不是“能不能连”而是“断了以后怎么快速恢复”这正是后文心跳和重连要解决的事。注意选 wss 并不代表服务端可以随便配自签名证书微信对证书链校验非常严格局部信任的私有CA证书也可能被判定无效。先准备好合规的域名和证书再往生产上推。3. 用 wx.connectSocket 建立稳定的长连接通道连接流程、端口参数与心跳前置3.1 从新建连接到三次握手完成最小实现代码小程序建立长连接第一步先封装一个SocketManager单例。这个单例要保证全局只有一个socket实例否则页面跳转后旧连接没人清理连接数到了5个上限就发不出新请求。下面是最小可用的连接代码// socketManager.js const app getApp(); class SocketManager { constructor() { this.socket null; this.connected false; this.reconnectAttempts 0; this.heartbeatTimer null; } connect(url, options {}) { if (this.socket this.connected) { console.warn(socket already connected, skip); return; } // url 形如 wss://api.example.com/ws?deviceIdxxx this.socket wx.connectSocket({ url: url, header: options.header || { content-type: application/json, X-Device-ID: options.deviceId || unknown }, protocols: options.protocols || [], timeout: options.timeout || 10000, // 连接超时单位 ms success: (res) { console.log(connectSocket called, readyState, res.errMsg); }, fail: (err) { console.error(connectSocket fail, err); this.scheduleReconnect(url, options); } }); this.bindSocketEvents(url, options); } bindSocketEvents(url, options) { this.socket.onOpen((res) { console.log(TCP handshake done, connection open); this.connected true; this.reconnectAttempts 0; this.startHeartbeat(); if (options.onOpen) options.onOpen(res); }); this.socket.onMessage((frame) { console.log(frame received, len, frame.data.byteLength || frame.data.length); if (options.onMessage) options.onMessage(frame); }); this.socket.onError((err) { console.error(socket error, err); // onError 后 onClose 必定会触发 }); this.socket.onClose((closeInfo) { console.warn(socket closed, code, closeInfo.code, reason, closeInfo.reason); this.connected false; this.stopHeartbeat(); this.scheduleReconnect(url, options); }); } } module.exports new SocketManager();逻辑解释wx.connectSocket本身是异步的它只发起连接不代表连上连接结果在onOpen里给TCP三次握手失败则进入onError。header里的X-Device-ID用于服务端识别设备这在长连接场景是必备的——连接是无状态的服务端必须靠这个字段把socket映射回业务用户。timeout只针对连接阶段的超时一旦连接建立后续数据超时由心跳负责别指望这个参数兜底。这段代码里隐藏着一个关键点wx.connectSocket的success回调只在“API调用成功”时触发不等于“连接成功”。有同事把这个回调误当成连接成功去发业务数据结果发了空包。真正的连接成功只有onOpen服务端也只有在收到TCP握手完成后的HTTP升级请求并返回101时才会触发这个事件。3.2 ws:// 与 wss:// 的IP配置端口、路径与鉴权头连接地址最常踩的坑是“把端口放在域名后面就以为万事大吉了”。真实配置有四个要素缺一不可配置项示例说明协议wss://生产必须 wss开发可以 ws域名api.example.com微信公众平台里登记过的Socket合法域名端口443wss默认如果服务端监听非标准端口域名配置里要写全如api.example.com:8080路径/ws?deviceIdxxx路径可以携带业务参数避免在header里暴露敏感键域名校验的规则很严你在小程序后台配置的是api.example.com连接url写成wss://api.example.com:8080/ws浏览器和微信会直接报“域名不合法”。端口也是域名的一部分配置时要带上。IP地址直接写在url里只有两个场景开发者工具本地联调、或者你的小程序设置了“不校验合法域名”的调试选项。真机预览时IP直连基本不可用这不是技术能力问题是微信的准入策略——确保流量都从可追溯的域名走。路径里的参数还有一个隐藏好处服务端可以在不改动业务协议的前提下通过path区分连接用途比如/ws/push和/ws/video走不同的handler而TCP层无需重新握手。我在网关里就这么设计省了不少维护成本。3.3 把连接状态机写到全局readyState和回调时序长连接源码最忌讳的是“拿网络状态当局部变量藏着”。小程序页面随时会销毁但socket连接属于App级资源必须提升到全局。wx.connectSocket返回的SocketTask里有readyState0表示连接中CONNECTING1表示已连接OPEN2表示正在关闭CLOSING3表示已关闭CLOSED。状态机的作用是防止重复连接和重复发送。// 在发送数据前检查状态 sendMessage(payload) { if (!this.socket || this.socket.readyState ! 1) { console.warn(socket not ready, state, this.socket this.socket.readyState); return false; } try { this.socket.send({ data: payload, success: () console.log(frame sent), fail: (err) console.error(send fail, err) }); return true; } catch (e) { console.error(send exception, e); return false; } }逻辑解释readyState是同步可读的用它可以避免大部分“状态错乱”。收到onClose后不能立即重连因为onClose可能由wx.closeSocket主动触发也可能是异常断开。区分方法是看关闭码1000是正常关闭1006是异常断开。异常断开才进入重连逻辑正常关闭比如用户退出登录要清理定时器和全局引用。回调时序也要心里有数onError之后不一定立刻触发onClose可能间隔几百毫秒而onClose之后socket对象不能再用必须重新wx.connectSocket。所以重连逻辑放在onClose回调里最稳妥放在onError里容易造成“还在关闭流程中又发起连接”的双socket错乱。4. 长连接源码里的核心部件心跳、重连与数据帧的拆分粘包4.1 心跳机制的定时器与ping/pong实测参数长连接建立后服务端和客户端之间可能长时间没有业务数据流动。运营商NAT映射表默认超时时间五花八门有的60秒有的300秒微信宿主在App进入后台时也可能冻结JavaScript定时器。如果连接没有心跳表面上还开着实际上TCP链路早已被中间设备悄悄断开。心跳就是用来“保住这条链”的。心跳的协议设计有两种一是WebSocket协议自带的ping/pong帧在wx.connectSocket里通过protocols: [binary]或专用子协议触发二是业务层自定义JSON心跳包比如{type:ping,ts:1666600000}服务端收到后回{type:pong,ts:...}。我实际项目中更推荐业务层心跳因为它能被服务端直接打到日志里排查问题时有据可查。startHeartbeat(interval 30000) { this.stopHeartbeat(); this.heartbeatTimer setInterval(() { if (!this.socket || this.socket.readyState ! 1) { return; } const ping JSON.stringify({ type: ping, ts: Date.now() }); this.socket.send({ data: ping, fail: (err) { console.error(heartbeat send fail, will close socket, err); this.socket.close({ code: 4000, reason: heartbeat fail }); } }); // 记录上一次心跳发送时间用于超时判断 this.lastPingSentAt Date.now(); }, interval); }参数说明心跳间隔30秒是我常用的起点对应运营商NAT映射60秒超时的“一半原则”居多但要在弱网地铁、电梯场景下测试如果发现还断就缩到20秒。这里有个易忽略点心跳发送失败不能只console必须主动close因为此时连接大概率已半开留着只会让数据发送进入假死。服务端发现3次心跳没收到pong就主动断开小程序端的onClose会触发重连流程形成闭环。4.2 断线重连与“指数退避”的踩坑代码重连不能写死固定间隔。如果服务端因升级短暂不可用客户端每秒重连一次几百台设备同时撞上来服务端直接被打挂而且微信对失败连接请求有隐性惩罚连续报错后API可能短暂被禁用。指数退避是标准解法。scheduleReconnect(url, options) { const baseDelay 2000; // 首次重连延迟 2 秒 const maxDelay 60000; // 最大延迟 60 秒 let delay baseDelay * Math.pow(2, this.reconnectAttempts); delay Math.min(delay, maxDelay); // 加随机抖动防止设备集体重连 delay Math.floor(Math.random() * 1000); this.reconnectAttempts; console.log(reconnect scheduled in, delay, ms, attempt, this.reconnectAttempts); this.reconnectTimer setTimeout(() { this.cleanupSocket(); this.connect(url, options); }, delay); }逻辑说明reconnectAttempts每次onOpen成功后归零确保正常状态下重连延迟一直从2秒起算。抖动区间是01000ms作用是把同一时间断线的设备重连时间散开。这个写法有一个坑setTimeout回调执行时如果页面已经卸载socket对象可能已失效所以要在回调里先cleanupSocket()——清掉旧socket引用、停止心跳再走connect流程。还有一个隐藏坑reconnectAttempts无限上涨会导致重连频率越来越低直到60秒一次这对可用性有影响。常规做法是连续失败10次后停止自动重连弹一个“网络连接异常”的UI提示让用户手动刷新。注意小程序切后台时setTimeout和setInterval都会暂停。回前台后需要重新检查socket状态如果断开了不要等定时器——在wx.onAppShow生命周期里主动触发一次reconnect。4.3 收包粘包/半包的处理用缓冲池与length字段拆包WebSocket协议本身有帧边界不会出现TCP流里的粘包问题。但小程序业务层经常做“多个业务消息塞进一个WebSocket帧”或者服务端发的是自定义二进制协议一帧里含多条消息。拆包逻辑就得自己写。长连接源码里处理二进制帧最干净的方案是“前4字节表示后续数据长度”的Length-Base协议。ArrayBuffer转DataView先攒数据够长度就截出来。class BinaryFrameParser { constructor() { this.buffer Buffer.alloc(0); // 小程序里用 Uint8Array 或 ArrayBuffer } // 假设协议格式: [4字节大端长度][业务数据] pushChunk(chunk) { // chunk 是 Uint8Array const merged new Uint8Array(this.buffer.length chunk.length); merged.set(this.buffer, 0); merged.set(chunk, this.buffer.length); this.buffer merged; const frames []; const view new DataView(this.buffer.buffer); while (this.buffer.length 4) { const bodyLen view.getUint32(0, false); // false 大端序 if (this.buffer.length 4 bodyLen) { break; // 半包继续等 } const body this.buffer.slice(4, 4 bodyLen); frames.push(body); // 消费掉已解析的字节 this.buffer this.buffer.slice(4 bodyLen); } return frames; } }逻辑说明进来的每一段数据都先合并到缓冲池然后循环检查“前4字节的长度字段”是否已经满足。不够就退出等下一个chunk够就切出一个完整帧。这个写法处理半包很稳妥。代码里的buffer.slice在Node里是零拷贝但在小程序JS引擎里会生成新数组如果帧太大、太频繁GC压力会变大所以生产代码建议把合并改成索引偏移而不是反复slice。校验长度字段是否有上限也很关键如果服务端发了一个恶意的4字节长度0xFFFFFFFF小程序会一直攒缓冲直到内存爆掉必须加bodyLen MAX_FRAME_SIZE的判断。同样的逻辑也适用于字符串协议。把字符串按\n拆帧但要注意中文字符在ArrayBuffer里是UTF-8编码不能直接用字符串长度推断帧边界。5. 微信小程序长连接避坑5个真实翻车现场与排查手段5.1 现象开发者工具能连真机上秒断开发者工具里用IP直连没问题airdrop到真机预览socket在onOpen之后不到3秒就触发onClose。原因微信真机强制校验socket合法域名IP地址不是合法域名连接被安全策略拦截。解决去微信公众平台“开发管理—服务器域名—socket合法域名”里添加你的域名必须带端口并且真机调试时在“详情—本地配置”勾选“不校验合法域名”只在开发阶段有效发布体验版后不校验选项不再生效。这个坑所有人都会踩属于微信小程序开发必修课。5.2 现象连接稳定但服务端收不到消息服务端日志显示连接建立、心跳正常但业务数据一直收不到。原因是发送的数据格式与服务端解析规则不一致——比如小程序发送的是字符串服务端按二进制解析或者发送的JSON里字段名大小写对不上。解决在send之前把数据打印出来与服务端约定的报文结构逐字段比对。我遇到过一次是数组被序列化成对象因为JSON.stringify对undefined字段的丢弃服务器端严格校验字段时直接拒绝。建议把协议定义写到共享的JSON Schema里两边同时引用。5.3 现象App切后台再回来连接处于半开状态用户锁屏再解锁页面还在但socket状态已经CLOSED或者状态是CONNECTING但实际连不上。原因iOS微信在App进入后台后会挂起WebSocket任务定时器全部暂停回到前台后旧连接已经失效但onClose没触发。解决在App.onShow生命周期里检查socket.readyState如果等于0或3主动走重连逻辑。这里注意别在onHide里做“暂停”操作——小程序切后台不等于销毁你要做的是记录时间回前台后判断“断开超过多少秒就重连”。阈值我一般设10秒。5.4 现象wss域名证书过期连接报errCode 400/501自己签发的证书或者Lets Encrypt自动续期失败导致证书失效。微信校验不通过时onError返回类似errCode: 400。解决这个没有代码层面的取巧方案只能换证书。注意微信要求的证书链必须在手机系统信任范围内自签名根证书加服务端证书链的“自建CA”方案在iOS和Android上都会意外失败。建议直接上受信任的公共证书部署时用openssl s_client检查完整链。遇到errCode: 501或1400时先检查协议是不是真的wss——有人把端口写成80却用wss结构对不上也会报证书错误。5.5 现象长连接一多页面卡顿且内存增长这在需要同时维持多个socket连接的场景出现比如每打开一个视频页面就建一条连接页面关闭时没有按顺序断开。每多一条连接微信底层就多一套收发缓冲AXJS引擎的内存压力显著上升。解决回到第3.1节说的单例管理整个小程序只维护一个连接业务消息自己封装“类型payload”的协议去区分如果业务场景确实需要多条连接比如同时连控制通道和视频通道页面onUnload里必须closeSocket并清除引用。用wx.getSystemInfo拿手机内存来动态决定是否限制连接数是过度设计先检查自己有没有“关页面忘了关连接”更实际。6. 在真机上验证长连接收包网络面板、分包打印与线上日志埋点6.1 真机调试用vConsole面板观察帧收发开发者工具的Network面板能看socket收发但真机上的行为才有说服力。打开真机调试模式的vConsole在onMessage回调里把帧内容打到控制台。注意二进制帧在vConsole里是一串数字可读性差要先转成字符串或hex再打印onMessage: function(frame) { const raw frame.data; if (typeof raw string) { console.log([socket recv], raw); } else { const bytes new Uint8Array(raw); const hex Array.from(bytes).map(b b.toString(16).padStart(2, 0)).join( ); console.log([socket recv hex], hex); } }这段代码的作用是把底层收到的数据可视化当服务端发来的是一个复杂的二进制帧时hex格式能直接看出长度字段是否正常、字节序是否匹配。我看到过有人在这一层发现服务端发过来的长度字段是字符串ASCII数字而不是int导致拆包逻辑永远进不去。6.2 用日志上报做线上回捞真机vConsole只能看本机线上问题则需要日志。小程序没有原生日志框架常见做法是把socket关键事件连接成功、断开、重连、心跳超时、收帧长度上报到自己的日志服务或者就近的监控平台。注意上报走wx.request短连接不能走同一条长连接——长连接都断了日志自然也发不出去。上报频率要限流每5秒一条聚合日志否则故障时日志上报会和大流量重连打崩服务器。线上回捞时把deviceId、readyState、reconnectAttempts、lastPingSentAt都打进日志能快速定位是网络问题还是服务端问题。6.3 自动化验证在开发者工具里模拟弱网和断线开发者工具工具栏提供“网络”下拉选项切换为“弱网3G/断网”。这是检验心跳和重连逻辑最有效的免费工具。验证方法先正常连接切到3G观察心跳发送间隔再切到“断网”等onClose触发记录重连间隔最后恢复“无限制”确认指数退避能恢复连接。这套手动测试流程我每次改完重连逻辑都要跑一遍。提示真机调试时把手机Wi-Fi关掉再打开比开发者工具模拟更接近真实场景。因为真机上还涉及DNS缓存、网络代理、微信宿主杀后台等开发者工具模拟不出来的变量。测试用例我固定在每条首次连接、切后台回前台、飞行模式恢复、服务端主动断开。四类走完长连接基本可以交付。最后说一个我的个人习惯每次写完长连接源码都会刻意把它跑在“被服务端无理由断开”的场景下观察客户端能不能在用户无感知的情况下恢复。这个方案值不值得做取决于你的业务对实时性的要求如果只是要每分钟轮询一次状态长连接反而增加复杂度。但如果你已经在等推送、等指令下发、等视频进度同步请收好上面这套心跳、重连、拆包的代码它会是你省下最多查半天日志时间的部分。希望帮到你。本文还有配套的精品资源点击获取