资讯动态

WinCE平台TCP通信实战:Winsock客户端与服务端完整实现

发布时间:2026/10/13 19:48:09 来源:尧图企业网站定制
简介这份资源面向在Windows CEWinCE嵌入式平台上进行网络开发的程序员提供基于Visual Studio 2005环境的TCP通信样例工程帮助理解并掌握Winsock API的套接字创建、连接建立与数据收发流程。压缩包共29个文件约4.45MB包含cpp与h源码文件、vcproj工程配置、rc资源脚本、sln解决方案以及exe可执行文件等覆盖从工程配置到编译运行的完整环节。资源中给出客户端与服务器示例演示socket()、connect()、send()、recv()及closesocket()等关键函数的调用方式并涉及ws2_32.lib链接依赖与_WIN32_WCE预处理器定义等WinCE特有配置。目前已有313人学习下载适合需要快速上手WinCE网络编程、对照样例调试并迁移到实际嵌入式项目的开发者参考。1. 从一台老工控屏说起WinCE 上的 TCP 通信到底难在哪手里还有几块跑 Windows CE 的工控板子的人大概率都遇到过这个场景设备本身带网口屏幕也能点但就是不知道怎么让它跟 PC 或者另一台设备对上话。搜“wince实现TCP通信”出来的结果要么是十几年前的论坛残帖要么是直接甩一段 MFC 代码没有上下文。这个资源的定位很明确——它是一套在 WinCE 平台上用 Winsock 实现 TCP 客户端与服务端通信的完整代码包覆盖 socket 创建、绑定、监听、连接、收发和关闭的全流程。适合两类人一是手上真有 WinCE 设备、需要快速把网络通信跑通的嵌入式工程师二是想理解早期 Windows 网络编程模型、拿 WinCE 当练手平台的开发者。它不解决驱动层问题也不涉及加密传输就是最朴素的 TCP 字节流通信。2. WinCE 的 Winsock 跟桌面 Windows 差在哪选型与接口约束2.1 为什么 WinCE 上不能照抄桌面端的 socket 代码很多人第一次在 WinCE 上写 TCP习惯性把桌面 Windows 的代码复制过来结果编译就报错。核心原因是 WinCE 用的是Winsock 1.1 的一个子集虽然接口名字跟桌面端一样socket、bind、listen、accept、connect、send、recv、closesocket但可用特性被砍了不少。桌面端常见的WSAAsyncSelect、WSAEventSelect在部分 WinCE 版本里支持不完整select模型反而是最稳的。另一个差异是 WinCE 的 CRT 库精简errno和WSAGetLastError的配合要格外注意出错时优先看WSAGetLastError的返回值别只盯着errno。选型上如果你只是做设备跟 PC 之间的命令下发和数据回传阻塞式 socket 单线程就够了代码量最小调试最直观。如果设备要同时处理多个连接比如既接受 PC 的连接又要主动上报数据那就得上select做多路复用或者开独立线程。我一般建议先在阻塞模型上把链路跑通确认硬件和网络没问题再考虑改成非阻塞或select否则一上来就搞异步出问题你连是网络的事还是代码的事都分不清。2.2 开发环境与头文件依赖WinCE 的 socket 编程需要链接ws2.lib注意不是桌面端的ws2_32.lib头文件是winsock.h或winsock2.h具体用哪个取决于你的 SDK 版本。在 VS2005/VS2008 的 WinCE 工程里通常要在项目属性里手动加上ws2.lib的链接。初始化时必须调用WSAStartup退出时调用WSACleanup这两个调用在 WinCE 上不能省否则 socket 创建会直接失败。// WinCE 下 Winsock 初始化的标准写法 #include winsock.h #pragma comment(lib, ws2.lib) // WinCE 用 ws2.lib不是 ws2_32.lib BOOL InitWinsock() { WSADATA wsaData; // 请求 Winsock 1.1 版本WinCE 对 2.x 支持有限 if (WSAStartup(MAKEWORD(1, 1), wsaData) ! 0) { return FALSE; // 初始化失败后续所有 socket 操作都不可用 } // 可以在这里检查 wsaData.wVersion 确认实际协商到的版本 return TRUE; } void DeinitWinsock() { WSACleanup(); // 与 WSAStartup 配对程序退出前调用 }MAKEWORD(1, 1)表示请求 Winsock 1.1这是 WinCE 上兼容性最好的选择。如果你写MAKEWORD(2, 2)在某些老版本 WinCE 上会直接返回失败。WSAStartup返回非零就不要再往下走了后面每一步都会失败而且报错信息会误导你以为是 socket 参数问题。3. 服务端与客户端代码落地从 bind 到 recv 的完整链路3.1 服务端监听、接受连接、收数据服务端的流程是socket → bind → listen → accept → recv/send → closesocket。WinCE 上bind的地址结构跟桌面端一样用sockaddr_in但要注意sin_addr.s_addr建议直接填INADDR_ANY别去解析具体网卡地址WinCE 的网络接口枚举比桌面端麻烦得多。// WinCE TCP 服务端核心流程 SOCKET StartTcpServer(unsigned short port) { SOCKET listenSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock INVALID_SOCKET) return INVALID_SOCKET; sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(port); // 端口必须转网络字节序 addr.sin_addr.s_addr INADDR_ANY; // 监听所有本地接口 if (bind(listenSock, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { closesocket(listenSock); return INVALID_SOCKET; // 常见原因端口被占用 } if (listen(listenSock, SOMAXCONN) SOCKET_ERROR) { closesocket(listenSock); return INVALID_SOCKET; } return listenSock; // 返回监听 socket后续用 accept 接受连接 } // 接受一个客户端连接并接收数据 void AcceptAndRecv(SOCKET listenSock) { sockaddr_in clientAddr; int addrLen sizeof(clientAddr); SOCKET clientSock accept(listenSock, (sockaddr*)clientAddr, addrLen); if (clientSock INVALID_SOCKET) return; char buf[512]; int ret recv(clientSock, buf, sizeof(buf) - 1, 0); if (ret 0) { buf[ret] \0; // 手动补结束符recv 不保证 // 处理收到的数据 } else if (ret 0) { // 对端正常关闭连接 } else { // ret SOCKET_ERROR用 WSAGetLastError() 查原因 } closesocket(clientSock); }htons(port)这一步新手最容易漏漏了之后端口号会变成字节翻转的值比如想监听 8000 结果实际监听的是另一个端口PC 端怎么连都连不上。recv返回 0 表示对端关闭了连接不是出错这个要跟SOCKET_ERROR区分开。buf收到数据后手动补\0是因为recv只负责搬字节不负责给你加字符串结束符。3.2 客户端连接服务端并发送数据客户端流程是socket → connect → send/recv → closesocket。WinCE 上connect是阻塞的如果服务端不在线会卡住一段时间才返回失败。实际项目里我一般会先把设备跟 PC 用网线直连或者接同一台交换机确认链路通了再调代码。// WinCE TCP 客户端连接与发送 SOCKET ConnectToServer(const char* ip, unsigned short port) { SOCKET sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock INVALID_SOCKET) return INVALID_SOCKET; 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); // 把点分十进制 IP 转成网络字节序 if (connect(sock, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { closesocket(sock); return INVALID_SOCKET; // 服务端未启动或网络不通 } return sock; } // 发送一段数据 BOOL SendData(SOCKET sock, const char* data, int len) { int sent send(sock, data, len, 0); if (sent SOCKET_ERROR || sent ! len) { // 部分发送的情况在 WinCE 上也可能出现严格场景要循环发 return FALSE; } return TRUE; }inet_addr在 WinCE 上可用但它不支持域名解析。如果你需要填域名得用gethostbyname不过 WinCE 的 DNS 解析依赖系统网络配置工控场景下我建议直接用 IP省掉一层不确定性。send的返回值要检查是否等于lenTCP 是字节流协议send不保证一次把你要发的全发出去虽然小数据量下很少出问题但严谨的代码应该循环发送剩余部分。3.3 用 select 做多路复用让一个线程管多个 socket当设备需要同时处理多个连接时阻塞式单 socket 就不够了。WinCE 上select是可用的而且比线程方案更省资源。select的核心是三个fd_set读集合、写集合、异常集合。每次调用前要重新用FD_ZERO和FD_SET填充因为select返回后会修改这些集合。// 用 select 同时监听监听 socket 和已连接 socket void SelectLoop(SOCKET listenSock) { fd_set readSet; SOCKET clientSock INVALID_SOCKET; while (TRUE) { FD_ZERO(readSet); FD_SET(listenSock, readSet); // 监听新连接 if (clientSock ! INVALID_SOCKET) FD_SET(clientSock, readSet); // 监听已有连接的数据 // WinCE 上 select 第一个参数被忽略填 0 即可 int ret select(0, readSet, NULL, NULL, NULL); if (ret SOCKET_ERROR) break; if (FD_ISSET(listenSock, readSet)) { // 有新连接进来 clientSock accept(listenSock, NULL, NULL); } if (clientSock ! INVALID_SOCKET FD_ISSET(clientSock, readSet)) { char buf[256]; int n recv(clientSock, buf, sizeof(buf), 0); if (n 0) { closesocket(clientSock); clientSock INVALID_SOCKET; // 连接断开从集合中移除 } // n 0 时处理数据 } } }select的第一个参数在 WinCE 上被忽略填 0 就行这跟桌面端必须填最大 socket 值加一不一样。每次循环都要重新FD_ZERO再FD_SET因为上一轮select返回时已经把没就绪的 socket 从集合里清掉了。连接断开后要把clientSock置为INVALID_SOCKET否则下一轮FD_SET会塞一个已经关闭的句柄进去行为不可预期。4. 避坑与排查WinCE TCP 通信里最容易翻车的五个点4.1 连不上但 ping 得通现象PC 能 ping 通 WinCE 设备但 TCP 客户端就是连不上服务端。原因服务端bind时绑定了具体 IP 而不是INADDR_ANY或者绑错了网卡也可能是防火墙层面拦了端口但 WinCE 本身一般没有软件防火墙更多是绑错地址。解决把sin_addr.s_addr改成INADDR_ANY确认listen返回值正常。在 PC 上用telnet 设备IP 端口测一下端口是否真的在监听。4.2 recv 返回 -1 但不知道原因现象recv返回SOCKET_ERROR代码里只打印了“接收失败”没有任何有用信息。原因没有调用WSAGetLastError()或者调用的时机不对——中间插了别的系统调用错误码被覆盖了。解决recv返回SOCKET_ERROR后立刻调WSAGetLastError()把返回值打出来。WSAEWOULDBLOCK表示非阻塞模式下暂时没数据不是真错误WSAECONNRESET表示对端强制关闭了连接。4.3 中文数据发过去变乱码现象发送英文字符串正常发中文对方收到的是乱码。原因WinCE 默认可能是 Unicode 编译环境char字符串和send的字节数对不上或者两端编码不一致。解决统一用 UTF-8 编码发送发送前把宽字符转成 UTF-8 字节流接收端也按 UTF-8 解析。send的第三个参数用实际字节数不要用strlen去算宽字符串。4.4 程序退出后端口还被占用现象服务端程序关闭后重新启动bind报错说地址已在使用。原因TCP 的TIME_WAIT状态导致端口在关闭后一段时间内仍被占用。解决在bind之前设置SO_REUSEADDR选项允许重用本地地址。WinCE 上这个选项是支持的。int opt 1; setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (char*)opt, sizeof(opt));4.5 大数据量发送时丢包或截断现象发送几 KB 的数据接收端只收到一部分。原因send返回的发送字节数小于请求的字节数代码没有处理部分发送的情况。解决把send包在一个循环里记录已发送字节数直到全部发完或出错。接收端同理recv也可能只收到一部分需要根据协议约定判断数据是否完整。5. 进阶技巧用非阻塞 socket 加超时控制让通信更可控阻塞式 socket 在 WinCE 上有个让人头疼的问题connect和recv没有超时网络异常时线程会卡死。我一般会给 socket 设置非阻塞模式配合select做超时控制。ioctlsocket的FIONBIO命令可以把 socket 切成非阻塞之后connect会立刻返回WSAEWOULDBLOCK再用select的写集合等待连接真正建立。// 设置 socket 为非阻塞并带超时连接 BOOL ConnectWithTimeout(SOCKET sock, const char* ip, unsigned short port, int timeoutMs) { // 先切成非阻塞 unsigned long nonBlock 1; ioctlsocket(sock, 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); connect(sock, (sockaddr*)addr, sizeof(addr)); // 非阻塞 connect 返回 WSAEWOULDBLOCK 是正常的 fd_set writeSet, errSet; FD_ZERO(writeSet); FD_ZERO(errSet); FD_SET(sock, writeSet); FD_SET(sock, errSet); timeval tv; tv.tv_sec timeoutMs / 1000; tv.tv_usec (timeoutMs % 1000) * 1000; int ret select(0, NULL, writeSet, errSet, tv); if (ret 0) return FALSE; // 超时或出错 if (FD_ISSET(sock, errSet)) return FALSE; // 连接失败 // 连接成功切回阻塞模式方便后续收发 nonBlock 0; ioctlsocket(sock, FIONBIO, nonBlock); return TRUE; }ioctlsocket的FIONBIO参数在 WinCE 上行为跟桌面端一致传 1 是非阻塞传 0 是阻塞。select的超时用timeval结构WinCE 上精度到毫秒级没问题。连接成功后我习惯切回阻塞模式因为后续收发逻辑用阻塞写起来更简单只在连接阶段做超时控制就够了。这个模式我在好几个工控项目里用过比纯阻塞方案稳得多尤其是设备上电后服务端还没起来的情况不会把整个线程卡死。从那以后我每次在 WinCE 上写 TCP都强制先跑一遍WSAStartup返回值检查、bind地址用INADDR_ANY、send循环发这三条基本能挡掉八成低级问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑