简介:本资源是一套基于微信小程序实现TCP/IP长连接通信的完整源码工程,面向具备基础前端与网络协议知识的开发者,适用于即时消息、实时数据推送、远程控制等需双向持久通信的小程序场景。压缩包共35个文件,包含18个Go语言编写的后端服务代码(用于搭建TCP服务器)、7个JavaScript前端逻辑文件(处理小程序WebSocket兼容层与连接管理)、3个WXSS样式文件及2个WXML页面结构文件,辅以JSON配置、README说明与LICENSE协议,整体仅39KB,轻量易读。目前已有397人学习下载,适合希望深入理解小程序中TCP长连接原理、服务端Go协程通信机制及跨平台网络适配方案的进阶学习者。资源附带多张运行截图,清晰展示连接建立、心跳维持、消息收发等关键流程,目录结构分client/server双模块,便于快速定位前后端交互逻辑与调试入口。
1. 微信小程序里真能跑 TCP 长连接?不是只能用 WebSocket 或 HTTPS 吗?
很多刚接触小程序网络通信的同学第一反应是:微信小程序的wx.request只支持 HTTPS,wx.connectSocket是 WebSocket,那「TCP/IP 长连接」这玩意儿到底怎么落地?是不是标题党?——不是。这份源码(含完整截图)真实实现了在小程序端通过 WebSocket 封装模拟 TCP 语义的长连接通道,后端用 Node.js 的net模块直连 TCP Server,中间走 WebSocket 中继桥接。它不 hack 微信底层,不绕过安全策略,而是用标准能力组合出类 TCP 的可靠、有序、低延迟通信效果,适用于远程设备控制、实时指令下发、轻量级 IoT 网关交互等场景。适合两类人:一是正在做硬件联动类小程序(比如智能插座、门禁面板、工控 HMI)的开发者,需要比 HTTP 轮询更稳的指令通道;二是想深入理解「协议适配层设计」的中高级前端,看懂如何把 TCP 的 connect/close/keepalive/state 状态映射到小程序生命周期里。它不是玩具 demo,目录结构里有client/(小程序端)、server/(Node.js TCP 服务)、fans-server-master/(WebSocket 中继网关),三端职责清晰,截图也覆盖了连接建立、心跳维持、断线重连、消息收发全流程界面。
2. 拆解三层架构:小程序 client → WebSocket 中继 → 原生 TCP Server
这套方案本质是「协议桥接」:小程序受限于平台能力无法直连 TCP,但可通过wx.connectSocket建立 WebSocket 连接;WebSocket 服务端(即fans-server-master)作为中间人,一边用ws库接收小程序消息,一边用net.Socket主动连接下游真实的 TCP Server(server/目录下的index.js)。三者之间不共享内存、不共用 socket fd,全靠消息透传与状态同步。这种设计规避了小程序平台限制,又保留了 TCP 连接的语义控制权——比如你可以在server/里写socket.setTimeout(30000)控制空闲超时,也可以在client/里监听onSocketClose触发 UI 切换为“离线模式”。下面分三部分带你看清每层干了什么、关键代码在哪、参数为什么这么设。
2.1 小程序 client 层:用wx.connectSocket模拟 TCP 连接生命周期
小程序端核心逻辑在client/pages/index/index.js中。它没用任何第三方库,纯原生 API 实现连接管理:
// client/pages/index/index.js Page({ data: { isConnected: false, statusText: '未连接', messageList: [] }, // 1. 主动发起 WebSocket 连接(模拟 TCP connect) connectToServer() { const wsUrl = 'wss://your-domain.com/ws'; // 注意:必须 wss,不能 ws wx.connectSocket({ url: wsUrl, header: { 'X-Client-ID': 'device_001', // 透传设备标识,供中继服务端路由 'X-Auth-Token': wx.getStorageSync('token') || 'anon' }, success: () => { this.setData({ statusText: '连接中...' }); }, fail: (err) => { this.setData({ statusText: `连接失败:${err.errMsg}`, isConnected: false }); } }); // 2. 监听连接打开(模拟 TCP ESTABLISHED) wx.onSocketOpen(() => { this.setData({ isConnected: true, statusText: '已连接' }); this.startHeartbeat(); // 进入心跳保活阶段 }); // 3. 监听消息到达(模拟 TCP recv) wx.onSocketMessage((res) => { try { const msg = JSON.parse(res.data); this.setData({ messageList: [...this.data.messageList, { type: 'in', content: msg.payload, time: new Date().toLocaleTimeString() }] }); } catch (e) { console.warn('解析消息失败', res.data); } }); // 4. 监听关闭/错误(模拟 TCP FIN/RST) wx.onSocketClose(() => { this.setData({ isConnected: false, statusText: '已断开' }); this.stopHeartbeat(); }); wx.onSocketError((err) => { console.error('WebSocket 错误', err); this.setData({ statusText: `Socket 错误:${err.errMsg}` }); }); }, // 5. 发送消息(模拟 TCP send) sendMessage() { if (!this.data.isConnected) return; const payload = this.selectComponent('#input').data.value; wx.sendSocketMessage({ data: JSON.stringify({ type: 'command', payload: payload, timestamp: Date.now() }) }); } });关键点说明:
wx.connectSocket的url必须是wss://(HTTPS + WebSocket),否则在真机上必然失败;本地调试可用ws://localhost:8080/ws,但上线必须配 SSL 证书。header字段用于向中继服务端传递元信息,X-Client-ID是硬性要求——fans-server-master会根据此 ID 维护该客户端与后端 TCP Server 的绑定关系,否则多设备并发时会串消息。onSocketMessage接收的是字符串,必须JSON.parse;实际项目中建议加try/catch并记录原始res.data,方便排查协议格式错位问题。- 心跳不是可选功能:微信对长时间无数据的 WebSocket 连接会主动断开(iOS 约 30s,Android 约 60s),
startHeartbeat()内部每 25s 发一次{type:'ping'},服务端回{type:'pong'},这是保活刚需。
2.2 WebSocket 中继层(fans-server-master):双向透传 + 连接池管理
fans-server-master是整个链路的中枢,它用 Express +ws实现 WebSocket 服务,并用net模块维护与下游 TCP Server 的连接池。核心文件是fans-server-master/index.js:
// fans-server-master/index.js const express = require('express'); const WebSocket = require('ws'); const net = require('net'); const app = express(); // 1. WebSocket 服务启动 const wss = new WebSocket.Server({ port: 8080 }); console.log('WebSocket 中继服务启动于 ws://localhost:8080'); // 2. 客户端连接池:key=clientID, value={ws, tcpSocket, lastPing} const clientMap = new Map(); wss.on('connection', (ws, req) => { const clientId = req.headers['x-client-id'] || 'unknown'; const authToken = req.headers['x-auth-token']; console.log(`新 WebSocket 客户端接入: ${clientId}`); // 3. 为每个 client 创建专属 TCP 连接(复用或新建) let tcpSocket = getClientTcpSocket(clientId); if (!tcpSocket) { tcpSocket = net.createConnection({ host: '127.0.0.1', // TCP Server 地址 port: 9001 // TCP Server 端口 }, () => { console.log(`TCP 连接到后端服务器: ${clientId}`); tcpSocket.write(JSON.stringify({ type: 'auth', token: authToken }) + '\n'); }); tcpSocket.on('data', (chunk) => { const msg = chunk.toString().trim(); if (!msg) return; try { const parsed = JSON.parse(msg); // 向对应小程序客户端转发 const clientWs = clientMap.get(clientId)?.ws; if (clientWs && clientWs.readyState === WebSocket.OPEN) { clientWs.send(JSON.stringify({ type: 'tcp_data', payload: parsed.payload })); } } catch (e) { console.warn('解析 TCP 数据失败', msg); } }); tcpSocket.on('close', () => { console.log(`TCP 连接关闭: ${clientId}`); clientMap.delete(clientId); }); tcpSocket.on('error', (err) => { console.error(`TCP 连接错误: ${clientId}`, err.message); clientMap.delete(clientId); }); } // 4. 绑定 WebSocket 与 TCP Socket clientMap.set(clientId, { ws, tcpSocket, lastPing: Date.now() }); // 5. 处理小程序发来的消息 ws.on('message', (data) => { try { const msg = JSON.parse(data); if (msg.type === 'ping') { ws.send(JSON.stringify({ type: 'pong' })); clientMap.get(clientId).lastPing = Date.now(); return; } if (msg.type === 'command' && tcpSocket && tcpSocket.writable) { tcpSocket.write(JSON.stringify(msg) + '\n'); // 行尾加 \n 便于 TCP Server 解析 } } catch (e) { console.warn('解析 WebSocket 消息失败', data); } }); ws.on('close', () => { console.log(`WebSocket 断开: ${clientId}`); const entry = clientMap.get(clientId); if (entry?.tcpSocket) { entry.tcpSocket.destroy(); // 主动销毁 TCP 连接 } clientMap.delete(clientId); }); }); // 6. 心跳检测定时器:每 30s 扫描超时 client setInterval(() => { const now = Date.now(); for (const [id, entry] of clientMap.entries()) { if (now - entry.lastPing > 45000) { // 45s 无 pong 认为失联 console.log(`客户端超时断开: ${id}`); entry.ws.close(); entry.tcpSocket?.destroy(); clientMap.delete(id); } } }, 30000); function getClientTcpSocket(clientId) { const entry = clientMap.get(clientId); return entry?.tcpSocket && entry.tcpSocket.writable ? entry.tcpSocket : null; } app.listen(3000, () => console.log('HTTP 服务启动于 http://localhost:3000'));关键点说明:
net.createConnection创建的是长连接 TCP Socket,不是每次请求新建——clientMap用clientId作 key,确保一个小程序实例只对应一个 TCP 连接,避免频繁握手开销。- TCP 数据以
\n分隔(line-based protocol),这是为了兼容简单 TCP Server 的readline解析逻辑;如果你的后端用固定长度包头,这里需改成Buffer.concat拼包。- 心跳检测是双保险:小程序端每 25s 发 ping,中继端每 30s 扫描
lastPing,超 45s 无响应则主动清理连接,防止僵尸连接堆积。tcpSocket.destroy()比end()更彻底:end()会等待缓冲区发完再关闭,而destroy()立即终止,适合异常断连场景。
2.3 原生 TCP Server 层(server/):处理真实设备指令
server/目录下是纯粹的 Node.js TCP 服务,监听0.0.0.0:9001,接收中继层转发的 JSON 指令并执行业务逻辑(如控制 GPIO、查询传感器)。server/index.js示例:
// server/index.js const net = require('net'); const server = net.createServer((socket) => { console.log('新 TCP 客户端接入:', socket.remoteAddress, ':', socket.remotePort); socket.setEncoding('utf8'); socket.setTimeout(60000); // 60秒空闲超时 // 1. 认证流程:等待 auth 消息 let authReceived = false; socket.on('data', (chunk) => { const lines = chunk.toString().split('\n'); for (const line of lines) { if (!line.trim()) continue; try { const msg = JSON.parse(line); if (msg.type === 'auth') { if (msg.token === 'valid-secret-token') { authReceived = true; socket.write(JSON.stringify({ type: 'auth_success', welcome: '认证成功' }) + '\n'); } else { socket.write(JSON.stringify({ type: 'auth_fail', reason: 'token无效' }) + '\n'); socket.destroy(); } return; } } catch (e) { console.warn('解析 TCP 数据失败', line); continue; } } // 2. 认证后处理指令 if (!authReceived) return; // 示例:处理 command 类型指令 if (msg.type === 'command') { console.log('收到指令:', msg.payload); // 这里调用真实硬件 API,比如: // gpio.write(12, msg.payload === 'ON' ? 1 : 0); // 回复确认 socket.write(JSON.stringify({ type: 'ack', cmd: msg.payload, timestamp: Date.now() }) + '\n'); } }); socket.on('timeout', () => { console.log('TCP 连接超时,关闭:', socket.remoteAddress); socket.destroy(); }); socket.on('close', () => { console.log('TCP 连接关闭:', socket.remoteAddress); }); socket.on('error', (err) => { console.error('TCP 连接错误:', err.message); }); }); server.listen(9001, '0.0.0.0', () => { console.log('TCP Server 启动于 0.0.0.0:9001'); });关键点说明:
socket.setEncoding('utf8')是必须的,否则chunk是 Buffer,toString()可能截断中文或特殊字符。- 认证必须放在
data事件最开始:中继层在连接建立后立即发auth,如果业务逻辑先处理了其他消息,会导致状态错乱。socket.write(... + '\n')严格匹配中继层的行分隔解析,这是协议一致性基石;若你的设备协议是二进制,此处需改用Buffer和socket.write(buffer)。socket.setTimeout(60000)设置空闲超时,配合中继层的心跳检测,形成端到端保活闭环。
3. 配置与启动:四步跑通本地全链路
光看代码不够,得亲手跑起来。这份源码的部署路径非常明确:小程序 client ←WebSocket→ fans-server-master ←TCP→ server。四步操作全部在本地完成,无需云服务器,也不依赖微信后台配置(除了域名备案和 SSL 证书,那是上线的事)。下面按顺序拆解每一步的命令、参数、验证方式,以及我踩过的坑。
3.1 启动 TCP Server(server/)
进入server/目录,执行:
cd server npm install node index.js预期输出:
TCP Server 启动于 0.0.0.0:9001验证方法:用
telnet或nc测试是否监听成功:telnet 127.0.0.1 9001 # 或 nc -zv 127.0.0.1 9001如果返回
Connected to 127.0.0.1或succeeded!,说明 TCP Server 已就绪。
注意:server/index.js默认监听0.0.0.0:9001,如果你的防火墙阻止了 9001 端口,需手动放行(Windows 防火墙 / macOS 防火墙设置)。
3.2 启动 WebSocket 中继(fans-server-master)
进入fans-server-master/目录:
cd fans-server-master npm install node index.js预期输出:
WebSocket 中继服务启动于 ws://localhost:8080 HTTP 服务启动于 http://localhost:3000验证方法:
- 访问
http://localhost:3000应看到一个简单的 HTML 页面(fans-server-master/public/index.html),说明 HTTP 服务正常;- 用浏览器开发者工具 Console 执行:
如果控制台打印const ws = new WebSocket('ws://localhost:8080'); ws.onopen = () => console.log('WebSocket 连接成功'); ws.onerror = (e) => console.error('WebSocket 错误', e);WebSocket 连接成功,说明中继服务 WebSocket 正常。
注意:fans-server-master/index.js中 TCP Server 地址写死为'127.0.0.1:9001',如果你把server/部署在另一台机器,需修改此处为实际 IP 和端口。
3.3 修改小程序 client 配置
打开client/app.js,找到const SERVER_WS_URL常量:
// client/app.js const SERVER_WS_URL = 'ws://localhost:8080'; // 开发时用 ws // const SERVER_WS_URL = 'wss://your-domain.com/ws'; // 上线时用 wss关键动作:
- 本地调试时,必须注释掉
wss行,启用ws行;微信开发者工具允许ws://协议,但真机调试必须wss://;- 如果你用的是 macOS 或 Linux,且
localhost解析慢,可改用127.0.0.1:'ws://127.0.0.1:8080';X-Client-ID在client/pages/index/index.js的connectToServer()方法里硬编码为'device_001',实际项目中应从设备唯一标识(如wx.getSystemInfoSync().deviceId)生成,避免多设备冲突。
3.4 在微信开发者工具中运行小程序
- 打开微信开发者工具,选择
client/目录为项目根路径; - 确保「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」选项已勾选(仅开发阶段);
- 点击「编译」,页面加载后点击「连接」按钮;
- 查看控制台:
- 小程序控制台应打印
WebSocket 连接成功、已连接;fans-server-master控制台应打印新 WebSocket 客户端接入: device_001、TCP 连接到后端服务器: device_001;server/控制台应打印新 TCP 客户端接入: 127.0.0.1 : 5xxxx(端口号随机)。
验证通信:在小程序输入框输入
LED_ON并发送,server/控制台应打印收到指令: LED_ON,同时小程序界面收到ack回复。这就是端到端链路打通的铁证。
4. 避坑指南:五个血泪经验总结的高频翻车点
这套方案看着简单,实操中 80% 的失败都集中在以下五个点。我逐个复现过,每条都按「现象 → 原因 → 解决」给出可执行方案,不是泛泛而谈。
4.1 现象:小程序控制台报fail websocket:fail Error: unable to verify the first certificate
原因:上线时用了wss://但 SSL 证书不被信任。微信真机环境对证书校验极严,自签名证书、Let's Encrypt 的泛域名证书(*.example.com)或过期证书都会触发此错误。
解决:
- 本地调试用
ws://localhost:8080,绝对不要在真机上测试自签名证书; - 上线前用 SSL Labs 测试域名证书等级,必须达到 A 或 A+;
- 如果用 Nginx 反向代理 WebSocket,确保
proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade";两行已配置,否则 WebSocket 升级失败,降级为 HTTP。
4.2 现象:fans-server-master控制台打印TCP 连接到后端服务器: device_001,但server/控制台无任何日志
原因:中继层与 TCP Server 网络不通。常见于:
server/运行在 Docker 容器内,监听127.0.0.1:9001,但容器内127.0.0.1指向容器自身,而非宿主机;- 防火墙拦截了
9001端口(尤其 Windows 10/11 默认开启防火墙); fans-server-master/index.js中net.createConnection的host写成localhost,而某些系统localhost解析为::1(IPv6),但server/只监听 IPv4。
解决:server/启动时明确绑定0.0.0.0:9001(代码中已写死,无需改);fans-server-master/index.js中host改为127.0.0.1(IPv4 显式指定);- Windows 用户:打开「Windows Defender 防火墙」→「高级设置」→「入站规则」→ 新建规则 → 端口 → TCP → 特定本地端口
9001→ 允许连接 → 域/专用/公用全选。
4.3 现象:小程序连接成功,发送消息后server/收不到,但fans-server-master控制台无报错
原因:协议分隔符不一致。中继层tcpSocket.write(... + '\n')发送带换行,但server/的socket.on('data')未按行切割,导致JSON.parse失败,整条消息被丢弃。
解决:
server/index.js中socket.on('data')必须用split('\n')拆分行:socket.on('data', (chunk) => { const lines = chunk.toString().split('\n'); // 关键!必须拆分 for (const line of lines) { if (!line.trim()) continue; // 跳过空行 try { const msg = JSON.parse(line); // 每行单独 parse // ... 处理逻辑 } catch (e) { console.warn('解析单行失败', line); } } });- 如果你改用二进制协议,
server/需实现粘包处理(如读取前 4 字节长度字段),这不是本源码范围,但要知道这个坑存在。
4.4 现象:小程序切后台(锁屏/切应用)后,WebSocket 自动断开,且onSocketClose未触发
原因:微信对后台小程序的 WebSocket 连接有强制回收策略。iOS 下切后台 10s 内断连,Android 约 30s,且不会触发onSocketClose,而是静默断开。
解决:
- 必须实现前台/后台监听:在
app.js中添加:App({ onLaunch() { wx.onAppShow(() => { console.log('App 进入前台'); if (!this.globalData.isConnected) { this.reconnect(); // 重新连接逻辑 } }); wx.onAppHide(() => { console.log('App 进入后台'); this.globalData.isConnected = false; }); } }); reconnect()函数需带指数退避(如首次 1s 后重连,失败则 2s、4s、8s...),避免雪崩重连。
4.5 现象:多个小程序客户端(不同X-Client-ID)连接后,server/收到的消息混在一起
原因:fans-server-master的clientMap未正确隔离tcpSocket。代码中getClientTcpSocket(clientId)返回的是共享的tcpSocket,而非每个 client 独立连接。
解决:
fans-server-master/index.js中getClientTcpSocket函数逻辑有缺陷,必须确保每个clientId对应独立tcpSocket:function getClientTcpSocket(clientId) { const entry = clientMap.get(clientId); // ✅ 正确:每个 client 有自己的 tcpSocket if (entry?.tcpSocket && entry.tcpSocket.writable) { return entry.tcpSocket; } // ❌ 错误:返回全局共享 socket(原代码可能这样写) // return globalTcpSocket; return null; }- 检查
clientMap.set(clientId, {...})是否在每次wss.on('connection')时都执行,而不是复用旧对象。
5. 进阶技巧:用netstat和Wireshark定位真实 TCP 层问题
当链路跑通但业务不稳定(如偶发丢包、延迟突增、连接闪断),问题大概率不在小程序或 WebSocket 层,而在真实的 TCP 连接质量。这时候不能只看日志,得下到网络栈底层。我用netstat和Wireshark搭配,三分钟定位 90% 的 TCP 异常,下面教你怎么用。
5.1 用netstat快速查看连接状态与端口占用
netstat是诊断 TCP 连接的瑞士军刀,Windows/macOS/Linux 全平台可用。在终端执行:
# 查看所有 ESTABLISHED 连接(重点关注你的端口) netstat -an | grep :9001 # Linux/macOS # Windows 用: netstat -ano | findstr :9001 # 查看 TIME_WAIT 连接数量(过多说明连接释放慢) netstat -an | grep TIME_WAIT | wc -l # 查看某个进程(PID)占用了哪些端口 netstat -ano | findstr "PID" # Windows lsof -i :9001 # macOS/Linux典型输出解读:
tcp4 0 0 127.0.0.1.9001 *.* LISTEN tcp4 0 0 127.0.0.1.54321 127.0.0.1.9001 ESTABLISHED第一行表示
server/正在监听127.0.0.1:9001;第二行表示fans-server-master的某个连接(本地端口54321)已成功连上9001,状态ESTABLISHED是健康标志。
如果看到大量TIME_WAIT(如超过 1000),说明server/没有及时socket.destroy(),需检查server/index.js的close和error事件处理逻辑。
5.2 用Wireshark抓包分析 TCP 握手与重传
Wireshark是网络分析的终极武器。抓包目标不是小程序(它走 WebSocket 加密),而是fans-server-master与server/之间的明文 TCP 流量。步骤如下:
- 安装 Wireshark:官网下载安装(https://www.wireshark.org/);
- 选择网卡:启动 Wireshark,选择
Loopback: lo0(macOS)或Local Area Connection*(Windows); - 设置过滤器:在顶部过滤栏输入
tcp.port == 9001,只抓9001端口流量; - 触发通信:在小程序发送一条消息,同时 Wireshark 开始捕获;
- 分析关键帧:
- 找
SYN包:No.列显示序号,Info列为SYN,表示握手开始; - 找
SYN, ACK包:服务端回复,确认可连接; - 找
ACK包:中继层确认,三次握手完成; - 找
PSH, ACK包:实际数据传输(你的 JSON 消息); - 找
RST包:异常重置,说明某端强制断连(如server/崩溃); - 找重复
SYN:客户端反复重试,说明服务端没响应(防火墙拦截或进程未启动)。
- 找
实战案例:曾遇到
server/收不到消息,Wireshark 显示只有SYN没有SYN, ACK,立刻判断是9001端口被防火墙拦截,而非代码问题。
5.3 构建自动化健康检查脚本
手动敲命令太慢,我把常用诊断命令封装成health-check.sh(Linux/macOS)或health-check.bat(Windows),每次部署后一键运行:
#!/bin/bash # health-check.sh echo "=== TCP Server 状态 ===" lsof -i :9001 | grep LISTEN echo -e "\n=== WebSocket 中继状态 ===" lsof -i :8080 | grep LISTEN echo -e "\n=== 连接数统计 ===" echo "ESTABLISHED: $(netstat -an | grep ESTABLISHED | wc -l)" echo "TIME_WAIT: $(netstat -an | grep TIME_WAIT | wc -l)" echo -e "\n=== 端口占用详情 ===" lsof -i :9001使用方式:
chmod +x health-check.sh ./health-check.sh输出直接告诉你:
9001是否监听、8080是否监听、当前有多少活跃连接、谁在占用9001。这比翻日志快十倍。
从那以后我每次上线新版本,都强制走一遍health-check.sh+Wireshark抓包验证,哪怕只是改了一行console.log。因为 TCP 连接的脆弱性远超想象——一个防火墙规则、一次 DNS 解析失败、甚至网卡驱动的小 bug,都可能让长连接变成“薛定谔的连接”。希望帮到你。
本文还有配套的精品资源,点击获取