ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VC++ UDP 通信示例包解析:从工程结构到 Winsock API 实战

VC++ UDP 通信示例包解析:从工程结构到 Winsock API 实战 简介这是一份面向VC初学者与网络编程入门者的UDP通信演示工程围绕Windows平台Winsock套接字展开帮助读者理解无连接传输协议的基本用法。资源以客户端与服务器双端示例为主线涵盖套接字库初始化、UDP套接字创建、sockaddr_in地址结构填充、bind绑定、sendto发送与recvfrom接收等核心环节并涉及错误码捕获与缓冲区管理等常见问题。压缩包共15个文件约11KB包含6个cpp源文件、3个头文件、3个dsp工程文件、2个dsw工作区文件及1个positions文件分别对应客户端与服务器两套可编译工程便于直接加载调试。目前已有131人学习下载。通过对照双端代码读者可快速掌握UDP数据收发流程理解客户端与服务器在绑定与应答上的差异并在此基础上扩展多线程或异步处理为后续网络编程实践打下基础。1. 从一份 VC UDP 示例包说起它到底能跑通什么很多人第一次在 Windows 上写网络程序都是从一份UDP.rar这样的示例包开始的。解压之后你会看到Example.dsw、Client.dsp、Server.dsp这些老式 VC 工程文件还有成对的Client.cpp、Server.cpp和StdAfx.cpp/h。这不是什么新潮框架而是一份典型的 VC 6.0 时代的 UDP 通信 demo客户端发、服务端收或者反过来核心就是 Winsock 的sendto和recvfrom。它解决的问题很具体让你在一个能直接编译的工程里看清 UDP 套接字从WSAStartup到WSACleanup的完整生命周期而不是对着文档空想。适合谁刚接触 Windows 网络编程、需要一份能改能跑的底稿的人也适合想快速验证 UDP 收发逻辑、不想从零搭工程的老手。这份包里的代码量不大但把sockaddr_in、bind、htons这些关键点都串起来了属于那种“跑一遍就懂一半”的素材。2. 拆开工程看结构VC UDP demo 的骨架与编译链路2.1 工程文件与源码的对应关系拿到UDP.rar之后别急着双击Example.dsw。先看清楚目录里有什么Example.dsw是工作区文件它下面挂着Example.dsp、Client.dsp、Server.dsp三个工程文件。Client和Server各自有独立的StdAfx.cpp、StdAfx.h这是 VC 6.0 预编译头的老规矩。Example.cpp通常是主入口或者一个汇总示例具体要看工程配置里哪个文件被设为编译目标。这种结构在今天的 Visual Studio 里打开会提示升级升级之后.dsp会变成.vcxproj但源码逻辑不变。我一般会先确认三件事第一Client.cpp和Server.cpp里是否都包含了winsock2.h第二工程设置里有没有链接ws2_32.lib第三StdAfx.h里有没有把winsock2.h放在windows.h之前。这三点任何一条出问题编译期就是一堆重定义或者链接错误。尤其是winsock2.h和windows.h的顺序老 VC 工程里经常翻车现象是fd_set重定义或者timeval冲突血泪经验就是永远把winsock2.h放最前面。2.2 编译环境准备与工程升级如果你用的是 VS2017 或更高版本直接打开.dsw会触发升级向导。升级本身没问题但要注意平台工具集和字符集。老工程默认是 MBCS新 VS 默认是 Unicodeinet_addr这类函数在 Unicode 下不会报错但MessageBox之类的显示会乱。稳妥做法是升级后把字符集改成“使用多字节字符集”或者把代码里的char缓冲区统一改成TCHAR并配合_tcslen。不过对于这份 demo我建议先保持 MBCS减少变量。编译前还要确认ws2_32.lib的链接方式。老工程常用#pragma comment(lib, ws2_32.lib)写在StdAfx.h或Client.cpp顶部也有在工程设置里手动加的。如果你升级后发现链接错误unresolved external symbol __imp__sendto24八成就是库没链上。手动加的方法是在项目属性 → 链接器 → 输入 → 附加依赖项里填ws2_32.lib。// StdAfx.h 里常见的顺序别搞反 #include winsock2.h #include ws2tcpip.h #include windows.h #pragma comment(lib, ws2_32.lib)这段顺序说明winsock2.h必须在windows.h之前否则windows.h会引入winsock.h导致winsock2.h里的定义冲突。ws2tcpip.h是为了用inet_pton这类新函数老 demo 可能没包含但加上没坏处。#pragma comment是给链接器看的省去手动配置。2.3 客户端与服务端的启动顺序这份 demo 的典型用法是先跑 Server再跑 Client。Server 端会bind到一个固定端口比如 12345然后阻塞在recvfrom上等数据。Client 端不需要bind直接sendto到 Server 的 IP 和端口。如果你先跑 Clientsendto不会报错但数据发出去没人收Server 启动后也收不到之前发的包因为 UDP 不缓存。所以顺序错了现象就是“客户端显示发送成功服务端没反应”不是代码问题是时序问题。启动 Server 时如果bind失败常见原因是端口被占用。用netstat -ano | findstr 12345查一下如果有进程占着换个端口或者结束那个进程。另外如果 Server 绑定的是INADDR_ANY那本机所有网卡都能收如果绑定的是具体 IP比如192.168.1.100那只有发往这个 IP 的包能收到。调试阶段建议先用INADDR_ANY减少变量。3. 核心 API 逐个拆从 WSAStartup 到 recvfrom 的参数怎么设3.1 WSAStartup 与 WSACleanup 的配对WSAStartup是 Winsock 的入口不调用它后面所有套接字函数都会返回WSANOTINITIALISED。参数是版本号和WSADATA结构体指针。版本号用MAKEWORD(2, 2)表示 Winsock 2.2这是目前最通用的。WSADATA用来接收系统返回的实际版本信息一般不用细看但可以打印出来确认。WSADATA wsaData; int ret WSAStartup(MAKEWORD(2, 2), wsaData); if (ret ! 0) { std::cerr WSAStartup failed: ret std::endl; return 1; } // ... 网络操作 ... WSACleanup();逻辑说明WSAStartup成功返回 0失败返回错误码。WSACleanup必须和WSAStartup配对每次WSAStartup成功都要对应一次WSACleanup。在 demo 里通常放在main开头和结尾。如果程序中途return记得先WSACleanup再退出否则资源泄漏。参数MAKEWORD(2, 2)的高字节是主版本低字节是次版本写成MAKEWORD(2, 0)也能跑但用 2.2 更稳。3.2 socket 创建与 sockaddr_in 填充socket函数的三个参数地址族AF_INET、套接字类型SOCK_DGRAM、协议IPPROTO_UDP或 0。返回SOCKET类型失败返回INVALID_SOCKET。创建之后要检查别直接往下用。sockaddr_in结构体填充是新手最容易错的地方。sin_family必须设为AF_INETsin_port必须用htons转成网络字节序直接写12345在 x86 上会变成端口 14640 之类的怪值sin_addr.s_addr用inet_addr转字符串 IP或者用inet_pton更安全。SOCKET udpSocket socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udpSocket INVALID_SOCKET) { std::cerr socket failed: WSAGetLastError() std::endl; WSACleanup(); return 1; } sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_port htons(12345); // 端口必须转网络字节序 serverAddr.sin_addr.s_addr inet_addr(127.0.0.1); // 本机测试用回环参数说明htons把主机字节序的端口转成网络字节序htonl用于 32 位地址但inet_addr已经返回网络字节序所以不用再转。inet_addr失败返回INADDR_NONE如果 IP 是255.255.255.255会误判所以更推荐inet_pton。在 demo 里用127.0.0.1做本机回环测试最方便不用改网卡配置。3.3 bind 与 recvfrom 的阻塞行为Server 端必须bind否则系统会随机分配端口Client 不知道往哪发。bind的参数是套接字和sockaddr指针长度用sizeof(sockaddr_in)。绑定INADDR_ANY表示监听所有本地地址。recvfrom是阻塞调用没有数据时线程会挂起。它的参数包括接收缓冲区、缓冲区长度、标志位通常 0、发送者地址结构体指针、地址长度指针。返回值是收到的字节数失败返回SOCKET_ERROR。注意recvfrom的第 5 个参数是int*传入sizeof(sockaddr_in)的地址函数返回时会更新为实际地址长度。sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_port htons(12345); serverAddr.sin_addr.s_addr INADDR_ANY; if (bind(udpSocket, (sockaddr*)serverAddr, sizeof(serverAddr)) SOCKET_ERROR) { std::cerr bind failed: WSAGetLastError() std::endl; closesocket(udpSocket); WSACleanup(); return 1; } char recvBuf[1024]; sockaddr_in clientAddr; int clientAddrLen sizeof(clientAddr); int bytesReceived recvfrom(udpSocket, recvBuf, sizeof(recvBuf), 0, (sockaddr*)clientAddr, clientAddrLen); if (bytesReceived SOCKET_ERROR) { std::cerr recvfrom failed: WSAGetLastError() std::endl; } else { recvBuf[bytesReceived] \0; std::cout Received from inet_ntoa(clientAddr.sin_addr) : ntohs(clientAddr.sin_port) - recvBuf std::endl; }逻辑说明bind失败常见于端口被占用或权限不足小于 1024 的端口需要管理员。recvfrom返回后clientAddr里就是发送方的地址和端口可以用inet_ntoa和ntohs转回可读格式。注意inet_ntoa返回的是静态缓冲区多线程下不安全demo 里单线程无所谓。recvBuf[bytesReceived] \0是为了当字符串打印如果数据本身是二进制别这么干。3.4 sendto 的目标地址与返回值Client 端用sendto发送。参数和recvfrom类似只是缓冲区是发送内容地址是目标地址。返回值是实际发送的字节数UDP 下要么全发要么失败不会部分发送。如果返回SOCKET_ERROR用WSAGetLastError看错误码。常见错误是WSAECONNRESET10054现象是recvfrom或sendto报“Connection reset by peer”。这是因为之前发的包对方没收到ICMP 端口不可达消息返回后Winsock 在下次调用时报告。解决办法是用WSAIoctl关掉SIO_UDP_CONNRESET或者忽略这个错误继续发。char sendBuf[] Hello, UDP!; int bytesSent sendto(udpSocket, sendBuf, strlen(sendBuf), 0, (sockaddr*)serverAddr, sizeof(serverAddr)); if (bytesSent SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAECONNRESET) { std::cerr Got ICMP port unreachable, ignore and continue std::endl; } else { std::cerr sendto failed: err std::endl; } }参数说明strlen(sendBuf)不带1因为 UDP 不要求发字符串结束符。如果发二进制用实际长度。sendto的第 5 个参数是目标地址第 6 个是地址长度。注意serverAddr在 Client 端要填成 Server 的 IP 和端口别填成自己的。4. 避坑与排查UDP demo 跑不通时先看这几条4.1 现象编译报错winsock2.h与windows.h冲突原因windows.h默认包含winsock.hWinsock 1.1而winsock2.h是 2.x 版本两者定义冲突。解决把#include winsock2.h放在#include windows.h之前并且定义WIN32_LEAN_AND_MEAN减少windows.h的包含范围。如果还不行在项目属性 → C/C → 预处理器 → 预处理器定义里加WIN32_LEAN_AND_MEAN。4.2 现象bind返回 10048地址已在使用原因端口被其他进程占用或者上一次运行的程序没完全退出套接字还处于TIME_WAIT状态UDP 没有TIME_WAIT但如果是 TCP 会有。解决换端口或者用SO_REUSEADDR选项。在bind之前调用setsockopt设置SO_REUSEADDR为TRUE。注意 UDP 下SO_REUSEADDR的行为和 TCP 不同多个套接字绑同一端口可能都能收到包看系统实现。BOOL optVal TRUE; setsockopt(udpSocket, SOL_SOCKET, SO_REUSEADDR, (char*)optVal, sizeof(optVal));4.3 现象recvfrom一直阻塞收不到数据原因可能是 Client 发到了错误的 IP 或端口或者 Server 绑定的是127.0.0.1而 Client 发到了局域网 IP。解决先用netstat -ano | findstr 12345确认 Server 确实在监听。然后在 Client 端确认serverAddr的 IP 和端口与 Server 一致。本机测试统一用127.0.0.1。如果跨机测试关掉 Windows 防火墙或者放行对应端口。防火墙拦截 UDP 时不会有明显提示现象就是包发出去没回音。4.4 现象sendto返回 10054WSAECONNRESET原因对方主机返回了 ICMP 端口不可达Winsock 在下次sendto或recvfrom时报告。解决用WSAIoctl关闭SIO_UDP_CONNRESET或者直接忽略这个错误码继续发送。关闭的代码在 MSDN 有示例核心是DWORD dwBytesReturned 0; BOOL bNewBehavior FALSE; WSAIoctl(udpSocket, SIO_UDP_CONNRESET, bNewBehavior, sizeof(bNewBehavior), NULL, 0, dwBytesReturned, NULL, NULL);。注意这个选项在 Windows 2000 之后才有老系统可能不支持。4.5 现象中文数据乱码原因发送端和接收端的字符编码不一致或者strlen对中文计算错误。解决统一用 UTF-8 或者 GBK发送时用strlen对char数组没问题但如果用wchar_t就要用wcslen并乘以sizeof(wchar_t)。接收端打印时确认控制台代码页。更稳妥的做法是发送二进制长度接收端按长度截断不依赖字符串结束符。5. 进阶用法把 demo 改成可测试的 UDP 工具5.1 加一个命令行参数解析原始 demo 的 IP 和端口是硬编码的改起来麻烦。我一般会加一个简单的参数解析让程序支持-s服务端模式、-c客户端模式、-p端口、-a地址。这样同一份代码编译出来就能当工具用不用每次改代码重编译。int main(int argc, char* argv[]) { bool isServer false; int port 12345; std::string ip 127.0.0.1; for (int i 1; i argc; i) { if (strcmp(argv[i], -s) 0) isServer true; else if (strcmp(argv[i], -c) 0) isServer false; else if (strcmp(argv[i], -p) 0 i 1 argc) port atoi(argv[i]); else if (strcmp(argv[i], -a) 0 i 1 argc) ip argv[i]; } // 后续根据 isServer 走不同分支 }逻辑说明-s和-c互斥默认客户端。-p和-a后面跟一个值用i跳过。atoi不检查错误生产环境可以用strtol。这个解析很简单但足够让 demo 变成可复用的小工具。5.2 用 select 实现非阻塞接收recvfrom阻塞会导致程序没法同时做别的事。加一个select可以设置超时超时后继续循环顺便检查退出条件。select的第一个参数在 Windows 下忽略填 0 即可。fd_set用FD_ZERO和FD_SET初始化timeval设置超时时间。fd_set readSet; FD_ZERO(readSet); FD_SET(udpSocket, readSet); timeval timeout; timeout.tv_sec 1; timeout.tv_usec 0; int selRet select(0, readSet, NULL, NULL, timeout); if (selRet SOCKET_ERROR) { std::cerr select failed: WSAGetLastError() std::endl; } else if (selRet 0) { // 超时没有数据 } else { if (FD_ISSET(udpSocket, readSet)) { // 有数据可收调用 recvfrom } }参数说明select返回 0 表示超时返回SOCKET_ERROR表示出错返回正数表示有事件。FD_ISSET检查套接字是否在集合中。timeout每次调用select后会被修改所以如果循环里要重复用每次都要重新赋值。这个模式适合需要定时发送心跳或者同时处理多个套接字的场景。5.3 用 iperf3 做 UDP 打流验证写完 demo 之后怎么确认收发逻辑没问题我习惯用iperf3做 UDP 打流。服务端跑iperf3 -s客户端跑iperf3 -c 127.0.0.1 -u -b 10M -t 10表示 UDP 模式、带宽 10Mbps、持续 10 秒。然后看丢包率和抖动。如果 iperf3 能跑通说明系统网络栈没问题demo 的问题就在代码里。反过来如果 demo 收不到包先用 iperf3 确认端口和防火墙能省很多排查时间。iperf3 的 UDP 测试结果里Lost/Total Datagrams是丢包统计Jitter是抖动。局域网内丢包应该接近 0如果丢包严重检查网卡、交换机或者带宽限制。注意 iperf3 的 UDP 默认带宽是 1Mbps要手动用-b指定否则测不出压力。5.4 一个我常犯的错误早期我写 UDP 接收时总忘了recvfrom的第 5 个参数是int*直接传sizeof(sockaddr_in)进去编译能过但运行结果不对clientAddr里全是垃圾。后来每次写recvfrom都强制检查这个参数是不是取了地址。从那以后我每次初始化sockaddr_in都先memset清零再填字段避免残留数据导致bind或sendto行为诡异。希望帮到你。本文还有配套的精品资源点击获取
返回列表