简介:面向WinCE嵌入式网络开发初学者及需要打通TCP通信链路的开发者,这份基于Visual Studio 2005的SampleTCP工程样例,围绕Winsock API演示套接字创建、与服务器建立连接、数据收发循环以及异常关闭等关键环节,可用于理解WinCE设备上的客户端/服务器通信流程,并为后续移植到实际嵌入式项目提供参照。压缩包为zip格式,共29个文件、约4.45MB,包含C++源码与头文件、VS2005工程及解决方案、资源脚本、清单文件和调试输出等,工程结构相对完整,便于直接打开对照学习,也能观察编译产物与项目配置之间的关系。已有315人学习,适合作为嵌入式网络编程的入门样本。读者可从中获得可运行的TCP通信样例、ws2_32.lib链接与_WIN32_WCE预处理器等工程配置线索,以及连接失败、收发异常时关闭套接字并清理资源的排错思路,进而理解从套接字创建到数据收发的完整链路。
1. WinCE 上跑 TCP 通信:先别急着 connect,连接生命周期才是主战场
WinCE TCP 通信最容易被低估的地方,不是 connect 能不能返回 0,而是连接跑上几小时后突然假死。某产线手持终端通过 Wi-Fi 上报数据,界面显示在线,服务端却收不到心跳,抓包看到设备端不再发 ACK——这类血泪经验我踩过不止一次。这份 WinCE TCP 通信示例源码把服务端监听、客户端连接、收发线程、心跳与断线重连拆成了可独立替换的模块,适合做设备端数据上报、上位机联调、老设备网络化改造。它不追求大而全,重点是把 socket 生命周期、线程退出和弱网重连讲清楚。适合从桌面 Windows 迁到 WinCE 的嵌入式开发者,也适合手里有老设备、需要快速搭一条稳定 TCP 链路的现场工程师。
2. WinCE 的 TCP 通信底座:Winsock 初始化、socket 选型与线程模型
2.1 WinCE 网络栈与 Winsock 版本边界
WinCE 不是桌面 Windows 的简单缩小版。网络栈从 NDIS 驱动、TCP/IP 协议栈到 Winsock API,任何一层在平台镜像里被裁掉,socket 都可能在创建阶段就失败。我一般先确认目标设备能正常获取 IP,再用上位机 ping 一下设备地址;如果 ping 不通,先查网卡驱动和 IP 配置,别急着怀疑代码。WSAStartup 返回 0 也不代表协议栈完整,它只说明 Winsock 库加载成功。真正要看的是 socket 创建失败时的 WSAGetLastError:WSAEAFNOSUPPORT 多半是地址族不支持,WSAEPROTONOSUPPORT 多半是 TCP 协议组件没进镜像,WSAENOBUFS 则要查内存和连接数。
工程链接也要对齐。WinCE 工程通常链接目标 SDK 自带的 ws2.lib,不要拿桌面工程的 ws2_32.lib 硬套,否则编译能过、运行时报错,排查起来很玄学。Winsock 版本按目标 SDK 头文件支持来,常见做法是先试 MAKEWORD(2,2),失败再降级到 1,1 并记录日志;不要在不确认平台能力的情况下把版本写死。WSAStartup 和 WSACleanup 要配对,建议在主线程初始化一次,工作线程只使用 socket,不要在每次收发前反复 WSAStartup。
| 检查项 | 常见现象 | 处理方式 |
|---|---|---|
| 平台组件 | socket 创建返回 INVALID_SOCKET | 查镜像是否包含 TCP/IP 与 Winsock 组件 |
| 链接库 | 编译通过,运行时报找不到入口 | 链接目标 SDK 的 ws2.lib,清理旧中间文件 |
| 版本协商 | WSAStartup 返回非 0 | 记录错误码,按 SDK 支持版本降级 |
| 多线程初始化 | 偶发 WSAStartup 失败 | 主线程统一初始化,避免并发清理 |
提示:WSAStartup 失败时不要继续创建 socket,先把 WSAGetLastError 写进设备日志,否则后面所有错误都会变成“连接失败”,很难定位。
2.2 客户端与服务端的 socket 选型及线程模型
在 WinCE 上,我优先用原生 Winsock,不用 MFC 的 CSocket。CSocket 依赖消息泵,放到没有消息循环的工作线程里容易阻塞,出问题时还多一层黑匣子。原生 socket 虽然代码多一点,但阻塞、非阻塞、select 超时都可控。客户端侧常见做法是“一个收线程 + 发送加锁”:收线程阻塞在 recv 上,发送方在临界区里调用 sendAll,避免两个线程同时写同一个 socket 导致数据交错。服务端侧如果连接数少,可以 accept 线程加每连接一个收线程;连接数一多就要限制最大连接数,或者改成 select 单线程轮询,WinCE 设备内存和线程资源都有限,别开口就是每连接一线程。
阻塞还是非阻塞,要看场景。设备端主循环要定时发心跳,纯阻塞 recv 会拖住状态机,所以我一般把收数据放在独立线程,主线程只做状态判断和心跳调度。connect 阶段用非阻塞加 select 等超时,避免网络不通时卡住 UI 或看门狗。TCP_NODELAY 对少量控制命令有用,能减少小包延迟;心跳包本身不追求低延迟,别为了省事把所有 socket 都设成一有数据就发。SO_KEEPALIVE 在 WinCE 上支持程度不一,不能把它当应用层心跳的替代品,弱网下还是自己发心跳、自己判超时更稳。
2.3 初始化、绑定与工程链接的落地步骤
先跑最小闭环:WSAStartup、创建 socket、绑定地址、监听或连接。下面这段是服务端初始化骨架,客户端把 bind/listen 换成 connect 即可。代码里保留了错误码记录,现场排查时比只看“失败”有用。
#include <winsock2.h> // 工程里链接目标 SDK 的 ws2.lib;桌面 MSVC 才常见 ws2_32.lib bool InitServer(unsigned short port, SOCKET& listenSock) { WSADATA wsa; int ret = WSAStartup(MAKEWORD(2, 2), &wsa); if (ret != 0) { // 记录 ret,按 SDK 支持情况决定是否降级到 1,1 return false; } listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock == INVALID_SOCKET) { // 重点看 WSAGetLastError:组件裁剪、库不匹配都会在这里暴露 WSACleanup(); return false; } BOOL reuse = TRUE; setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)); sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); // 端口必须转网络字节序 addr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端监听所有网卡 if (bind(listenSock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { closesocket(listenSock); listenSock = INVALID_SOCKET; WSACleanup(); return false; } if (listen(listenSock, 4) == SOCKET_ERROR) { // backlog 按设备能力给,别盲目调大 closesocket(listenSock); listenSock = INVALID_SOCKET; WSACleanup(); return false; } return true; }这段代码的逻辑很直白:先初始化 Winsock,再创建 TCP socket,设置地址复用,绑定端口并进入监听。参数上,port 用业务约定端口,示例里可以是 9000;backlog 给 4 是设备端保守值,连接数多时再按内存调。SO_REUSEADDR 主要解决重启后端口占用,不能解决协议错误。bind 的地址用 INADDR_ANY 表示监听所有网卡,客户端连接时则要填服务端具体 IP,不能照抄。初始化失败时先关 socket 再 WSACleanup,顺序反了会增加排查噪音。
3. 把 TCP 通信跑起来:服务端监听、客户端连接与收发闭环
3.1 服务端:bind/listen/accept 的 WinCE 写法
服务端最容易翻车的地方不是 accept 本身,而是 accept 返回错误后直接退出循环。WinCE 设备上网络会抖动,accept 可能被信号或关闭动作打断,正确做法是判断错误码,能继续就继续。下面这个 accept 循环带了退出标志和连接数限制,实际工程里把 ClientThread 换成自己的收线程即可。
volatile bool g_exit = false; const int MAX_CONN = 8; volatile LONG g_connCount = 0; DWORD WINAPI ClientThread(LPVOID param) { SOCKET client = (SOCKET)(INT_PTR)param; char buf[1024]; while (!g_exit) { int n = recv(client, buf, sizeof(buf), 0); if (n > 0) { // 这里交给协议解析器,不要直接在收线程里做耗时业务 continue; } if (n == 0) break; // 对端正常关闭 int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK) continue; break; } shutdown(client, SD_BOTH); closesocket(client); InterlockedDecrement(&g_connCount); return 0; } void AcceptLoop(SOCKET listenSock) { while (!g_exit) { SOCKET client = accept(listenSock, NULL, NULL); if (client == INVALID_SOCKET) { int err = WSAGetLastError(); if (err == WSAEINTR) break; // 主动关闭监听 socket Sleep(50); // 避免错误状态下空转 continue; } if (InterlockedIncrement(&g_connCount) > MAX_CONN) { InterlockedDecrement(&g_connCount); shutdown(client, SD_BOTH); closesocket(client); continue; } HANDLE h = CreateThread(NULL, 0, ClientThread, (LPVOID)(INT_PTR)client, 0, NULL); if (h) CloseHandle(h); // 线程句柄不用时及时关,线程本身继续跑 } }这里 accept 返回 INVALID_SOCKET 时没有一律 break,而是区分 WSAEINTR 和普通错误。MAX_CONN 给 8 是示例值,按设备内存和业务并发调;连接数超限时先 shutdown 再 closesocket,避免对端还挂在半开连接上。ClientThread 里只做 recv 和转交解析,耗时业务放到队列或单独线程,否则一个慢客户端就能拖住整个收线程。CreateThread 返回的句柄用完就 CloseHandle,不等于杀线程,只是释放句柄资源。
3.2 客户端:非阻塞 connect 与超时控制
客户端 connect 在弱网下可能卡很久,阻塞调用会把主线程拖死。常见做法是临时把 socket 设成非阻塞,connect 返回 WSAEWOULDBLOCK 后用 select 等可写,再用 SO_ERROR 判断真实结果。连接成功后恢复阻塞模式,收线程继续用阻塞 recv。
SOCKET ConnectWithTimeout(const char* ip, unsigned short port, int timeoutSec) { SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (s == INVALID_SOCKET) return INVALID_SOCKET; u_long nonblock = 1; ioctlsocket(s, FIONBIO, &nonblock); sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(ip); // 现场建议先校验返回值 int r = connect(s, (sockaddr*)&addr, sizeof(addr)); if (r == SOCKET_ERROR) { int err = WSAGetLastError(); if (err != WSAEWOULDBLOCK && err != WSAEINPROGRESS) { closesocket(s); return INVALID_SOCKET; } fd_set wset; FD_ZERO(&wset); FD_SET(s, &wset); timeval tv; tv.tv_sec = timeoutSec; tv.tv_usec = 0; r = select(0, NULL, &wset, NULL, &tv); // WinCE 下第一个参数忽略 if (r <= 0) { closesocket(s); // 0 是超时,SOCKET_ERROR 是 select 失败 return INVALID_SOCKET; } int soErr = 0; int len = sizeof(soErr); getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&soErr, &len); if (soErr != 0) { closesocket(s); return INVALID_SOCKET; } } nonblock = 0; ioctlsocket(s, FIONBIO, &nonblock); // 恢复阻塞,后续 recv 逻辑更简单 return s; }timeoutSec 建议 3 到 5 秒,现场网络差可以放到 8 秒,但不要无限等。select 返回 0 表示超时,返回 SOCKET_ERROR 要看 WSAGetLastError,不能一律当超时。getsockopt 的 SO_ERROR 才是 connect 的最终结果,select 可写只说明“有结果了”,不代表连接成功。inet_addr 失败会返回 INADDR_NONE,正式代码里要先校验 IP 字符串,别把错误地址带进 connect。
3.3 收发循环:粘包、半包与 sendAll 的实现
TCP 是字节流,没有消息边界。设备端发一个结构体,上位机可能一次收到半个,也可能两次粘在一起。我一般定固定包头:magic、type、length,length 表示 payload 长度,所有多字节字段走网络字节序。接收侧维护一个缓冲区,循环 recv 后按长度切包,长度非法就断开,别让越界解析把整个进程带崩。
#pragma pack(push, 1) struct PacketHeader { unsigned short magic; // 网络字节序,例如 0x55AA unsigned char type; unsigned short length; // payload 长度,网络字节序 }; #pragma pack(pop) bool SendAll(SOCKET s, const char* data, int len) { int sent = 0; while (sent < len) { int n = send(s, data + sent, len - sent, 0); if (n == SOCKET_ERROR) { int err = WSAGetLastError(); if (err == WSAEWOULDBLOCK) { // 非阻塞发送时要等可写;阻塞 socket 一般不会走到这里 continue; } return false; } sent += n; } return true; } // 收线程里的解析骨架:g_recvBuf 是每连接独立缓冲区 void OnRecvBytes(const char* data, int len) { g_recvBuf.append(data, len); while (g_recvBuf.size() >= sizeof(PacketHeader)) { PacketHeader hdr; memcpy(&hdr, g_recvBuf.data(), sizeof(hdr)); unsigned short payloadLen = ntohs(hdr.length); if (payloadLen > 4096) { // 最大包长按业务定,超限直接断开 CloseConnSafely(); return; } int total = sizeof(PacketHeader) + payloadLen; if ((int)g_recvBuf.size() < total) break; // 半包,等下次 recv std::string payload = g_recvBuf.substr(sizeof(PacketHeader), payloadLen); DispatchPacket(hdr.type, payload); g_recvBuf.erase(0, total); } }参数上,magic 用固定值做二次校验,type 区分心跳和业务,length 限制 4096 是保守值,视频或文件传输要另开通道。SendAll 必须循环 send,不能假设一次 send 把 len 全发完;非阻塞模式下遇到 WSAEWOULDBLOCK 要配合 select 等可写,不能原地死循环。接收缓冲区按连接独立,解析完立即 erase,避免长时间运行后内存只涨不降。结构体加#pragma pack(1)只解决对齐,不解决大小端,length 和 magic 仍要 htons/ntohs。
4. 断线重连与心跳:弱网下把 WinCE 长连接保住
4.1 心跳包格式与超时参数怎么定
应用层心跳比 TCP keepalive 更适合 WinCE 设备,因为超时时间、包内容、日志都能自己控制。心跳包不用带业务数据,type 设为 0x01,payload 放一个 4 字节时间戳或递增序号即可。发送间隔太短会浪费流量和唤醒次数,太长则发现断线慢。我一般给 10 秒发送、30 秒判超时,弱网现场可以调到 15 秒和 45 秒,但别把超时设得比重连退避还短,否则状态机会来回抖。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 心跳发送间隔 | 10 秒 | 设备端定时器触发,别用 Sleep 阻塞主循环 |
| 心跳超时 | 30 秒 | 连续 3 个周期没收到任何数据再判死 |
| 重连初始退避 | 1 秒 | 第一次断开快速重试 |
| 重连最大退避 | 30 秒 | 避免无网时疯狂重连 |
| 单包最大长度 | 4096 字节 | 按业务调,超限直接断开 |
| 接收缓冲区上限 | 8192 字节 | 至少容纳两个最大包,防止半包堆积 |
判断超时不要只看“最后一次发心跳”,要看“最后一次收到对端任何数据”。服务端可能不回心跳,但会回业务 ACK;如果协议里服务端完全不回包,那心跳机制要改成请求-响应式,否则设备端无法区分“链路正常”和“服务端收了但不理你”。这一点在联调时经常被忽略,最后日志里全是“心跳已发”,实际上连接早就半死了。
4.2 重连状态机:指数退避与连接风暴
重连不要写成 while 循环里疯狂 connect。设备断网后每秒重连一次,会把 CPU 和网络栈拖垮,服务端也会看到大量半开连接。我一般用四态状态机:IDLE、CONNECTING、CONNECTED、CLOSING,失败后按 1、2、4、8、16、30 秒退避,连接成功后重置回 1 秒。状态机放在主循环里,每次只推进一步,方便看门狗喂狗和响应退出标志。
enum ConnState { ST_IDLE, ST_CONNECTING, ST_CONNECTED, ST_CLOSING }; ConnState g_state = ST_IDLE; int g_backoffMs = 1000; DWORD g_lastRecvTick = 0; DWORD g_lastSendTick = 0; void ConnStateMachine() { DWORD now = GetTickCount(); switch (g_state) { case ST_IDLE: g_state = ST_CONNECTING; break; case ST_CONNECTING: { SOCKET s = ConnectWithTimeout("192.168.1.10", 9000, 5); if (s != INVALID_SOCKET) { g_sock = s; g_lastRecvTick = now; g_lastSendTick = now; g_backoffMs = 1000; // 成功后退避归零 g_state = ST_CONNECTED; } else { Sleep(g_backoffMs); g_backoffMs = (g_backoffMs * 2 > 30000) ? 30000 : g_backoffMs * 2; g_state = ST_IDLE; } break; } case ST_CONNECTED: if (now - g_lastRecvTick > 30000) { g_state = ST_CLOSING; // 30 秒没收到任何数据,判超时 } else if (now - g_lastSendTick > 10000) { SendHeartbeat(g_sock); g_lastSendTick = now; } break; case ST_CLOSING: CloseConnSafely(); g_state = ST_IDLE; break; } }这段状态机里,Sleep 只出现在连接失败后的退避,不阻塞已建立连接的收发。GetTickCount 会回绕,判断差值时用无符号减法,不要直接比较大小。ConnectWithTimeout 里的 IP 和端口是示例,现场要换成配置项,别写死在代码里。退避上限 30 秒是经验值,设备对实时性要求高可以降到 10 秒,但要有上限,不能无限翻倍。
4.3 多线程退出与 socket 资源回收顺序
关闭连接是最容易出死锁的地方。一个线程在 recv 上阻塞,另一个线程直接 closesocket,收线程可能返回错误,也可能卡在驱动里;如果还持有临界区,主线程再等锁,整个设备就像死机。我的做法是统一关闭入口:先置退出标志,再 shutdown(SD_BOTH) 唤醒 recv,然后 closesocket,最后 WaitForSingleObject 等收线程退出。不要在持锁状态下做 shutdown 和 closesocket,也不要用 TerminateThread 硬杀。
CRITICAL_SECTION g_cs; SOCKET g_sock = INVALID_SOCKET; HANDLE g_recvThread = NULL; volatile bool g_threadExit = false; void CloseConnSafely() { g_threadExit = true; EnterCriticalSection(&g_cs); SOCKET s = g_sock; g_sock = INVALID_SOCKET; LeaveCriticalSection(&g_cs); if (s != INVALID_SOCKET) { shutdown(s, SD_BOTH); // 先唤醒阻塞 recv closesocket(s); // 再释放句柄 } if (g_recvThread) { DWORD wr = WaitForSingleObject(g_recvThread, 3000); if (wr == WAIT_TIMEOUT) { // 记录日志并检查线程是否卡在驱动或业务回调里 // 不要在这里 TerminateThread } CloseHandle(g_recvThread); g_recvThread = NULL; } }关闭顺序的核心是“先让阻塞调用返回,再释放资源”。shutdown 只关闭收发方向,不释放 socket;closesocket 才释放句柄。WaitForSingleObject 给 3 秒是保守值,超时后先记日志,排查收线程是不是卡在业务回调或驱动里,而不是直接杀线程。g_sock 被临界区保护,其他线程发数据前先取本地句柄,发现 INVALID_SOCKET 就放弃本次发送,避免关闭后继续写。
5. 避坑与排查:WinCE TCP 通信最常见的 5 个翻车点
5.1 现象:WSAStartup 成功但 socket 创建失败
现象是 WSAStartup 返回 0,日志里也能看到初始化成功,但 socket 返回 INVALID_SOCKET,后面 connect 根本走不到。原因通常不是代码写错,而是目标镜像裁剪了 TCP/IP 或 Winsock 组件,或者工程链接了不匹配的库。桌面工程迁到 WinCE 时,链接器里还留着桌面库路径,编译能过,运行就报协议不支持。解决方式是先看 WSAGetLastError:WSAEPROTONOSUPPORT 查平台组件,WSAEAFNOSUPPORT 查地址族支持,WSAENOBUFS 查内存和句柄泄漏。然后清理中间文件,确认链接的是目标 SDK 的 ws2.lib,不要混用桌面库。最后用最小 demo 只做 WSAStartup 和 socket 创建,把业务代码全去掉,能快速判断是环境问题还是工程配置问题。
5.2 现象:connect 成功,收第一包就断
现象是客户端日志显示 connect 成功,服务端也 accept 了,但第一包数据一到,连接立刻被 RST 或 recv 返回错误。原因常见有三种:一是发送方把结构体内存直接 send,接收方按字符串或固定长度解析,长度字段错位;二是接收缓冲区太小,粘包后越界,程序自己调用 closesocket;三是中间网络设备有空闲超时,连接看似建立,实际路径已经断。解决方式是先抓包看 RST 是谁发的,如果是设备端发的,查解析代码的边界检查;如果是服务端发的,查服务端是否因为包长度非法主动断开。协议上固定 magic 和 length,length 超过上限直接断开并记日志,不要尝试“容错解析”。接收缓冲区按最大包两倍以上分配,半包时等待下一次 recv,不要用一次 recv 的结果当完整包。
5.3 现象:关闭连接后收线程卡死
现象是界面点了断开,主线程等收线程退出,等了几秒没反应,最后看门狗复位。原因多是另一个线程直接 closesocket,而收线程还阻塞在 recv 上;或者关闭时持有临界区,收线程返回后又要拿同一把锁,形成死锁。WinCE 的网络驱动在 socket 被关闭时的行为不一定和桌面一致,不能假设 closesocket 一定立刻唤醒 recv。解决方式是统一关闭入口:先置退出标志,再 shutdown(SD_BOTH),再 closesocket,最后 WaitForSingleObject。临界区只保护句柄赋值,不保护 shutdown 和 closesocket。等待超时后记录线程状态,不要用 TerminateThread,硬杀线程可能留下锁和驱动资源,后面更难查。
5.4 现象:结构体发出去,上位机解析错位
现象是设备端明明按结构体填了字段,上位机收到的数值却大得离谱,或者字符串后面带乱码。原因通常不是 TCP 丢包,而是大小端、结构体对齐和类型长度不一致。WinCE 设备多是小端,上位机如果按网络字节序解析,ushort 和 uint 必须先转换;结构体默认对齐可能插入填充字节,sizeof(struct)和字段实际长度对不上;long 在不同编译器下长度也可能不同。解决方式是不要直接发结构体内存,逐字段序列化成字节流,多字节字段统一 htons/htonl,接收端再 ntohs/ntohl。结构体定义加#pragma pack(1)只解决对齐,不解决字节序。协议文档里把字段偏移和长度写清楚,联调时先发固定测试包,用抓包工具对照十六进制,比猜代码快。
5.5 现象:日志全是超时,抓包却有回包
现象是设备日志不断打印“心跳超时”,但抓包能看到服务端回了 ACK,甚至回了业务包。原因可能是收线程被 UI 或慢业务阻塞,回包到了 socket 缓冲区但没人 recv;也可能是时间判断用了 GetTickCount 直接比较大小,回绕后差值变成负数;还可能是 select 的 timeval 单位写错,把毫秒当微秒,超时时间严重缩短。解决方式是把收数据和业务处理分开,收线程只做 recv 和入队,业务线程慢慢处理;时间比较统一用无符号差值;select 的 tv_sec 是秒,tv_usec 是微秒,别混。抓包时同时记录设备日志时间戳,对照回包到达时间和日志打印时间,差几百毫秒是正常的,差几秒就要查线程调度和锁竞争。
6. 进阶验证:用抓包与压测把 WinCE TCP 通信盯死
6.1 三个验证动作:抓包、时间戳、压测
联调时不要只看设备界面显示“在线”。我一般做三个验证动作。第一,在 PC 端抓包,过滤ip.addr == 设备IP and tcp.port == 9000,看三次握手、心跳间隔、FIN/RST 是谁发的;如果心跳间隔和代码里的 10 秒对不上,先查定时器是不是被慢业务拖了。第二,设备日志加两个时间戳:GetTickCount 和收到数据的时刻,排查超时误判。第三,用压测脚本模拟多连接和随机断开,观察设备线程数、内存和句柄数是否稳定。通过标准不是“能连上”,而是连续跑 30 分钟,心跳间隔稳定、无异常 RST、内存不持续上涨。
6.2 一个 Python 压测脚本片段
下面脚本只做连接、发心跳、随机断开重连,用来压设备端的 accept 和关闭逻辑。地址换成联调环境地址,不要用真实生产地址。
import socket, threading, time, random HOST, PORT = "192.168.1.10", 9000 # 换成联调环境地址 HEARTBEAT = b"\x55\xaa\x01\x00\x00" # magic + type + length 示例 def client(cid): while True: try: s = socket.create_connection((HOST, PORT), timeout=3) s.settimeout(5) for _ in range(30): s.sendall(HEARTBEAT) s.recv(1024) # 设备端应回心跳 ACK time.sleep(10) # 心跳间隔与设备端保持一致 s.close() except Exception as e: print(cid, "reconnect", e) time.sleep(random.randint(1, 5)) for i in range(50): # 50 并发,按设备能力调 threading.Thread(target=client, args=(i,), daemon=True).start() time.sleep(300)脚本里连接超时 3 秒、读超时 5 秒、心跳 10 秒、并发 50,都是压测起点,不是固定值。设备端如果限制 MAX_CONN=8,多出来的连接会被服务端主动关闭,这时脚本报 reconnect 是预期行为,重点看设备端有没有句柄泄漏。抓包和压测一起跑,才能把“连接建立”和“连接生命周期”分开看。
从那以后我每次在 WinCE 上接 TCP,都先把关闭顺序、心跳超时和抓包三点写成检查表,再谈业务协议。如果你手上正好有 WinCE 设备要联调,这份源码包适合直接拆开改;先跑通最小闭环,再把心跳和重连接进去,比从零搭架子省时间。希望帮到你。
本文还有配套的精品资源,点击获取