☰
WinCE平台TCP通信实战:Winsock客户端与服务端完整实现
2026/10/13 19:48:02 网站建设 项目流程

简介:这份资源面向在Windows CE(WinCE)嵌入式平台上进行网络开发的程序员,提供基于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的返回值要检查是否等于len,TCP 是字节流协议,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循环发这三条,基本能挡掉八成低级问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询