news 2026/10/1 10:50:23

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 默认是 Unicode,inet_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__sendto@24,八成就是库没链上。手动加的方法是在项目属性 → 链接器 → 输入 → 附加依赖项里填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 和端口。如果你先跑 Client,sendto不会报错,但数据发出去没人收,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_INET;sin_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看错误码。常见错误是WSAECONNRESET(10054),现象是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.h(Winsock 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返回 10054(WSAECONNRESET)

原因:对方主机返回了 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行为诡异。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:50:21

杭电网安复试编程:从“能跑”到“能打”的蜕变与考点拆解

杭电网安复试编程 Day19&#xff1a;从“能跑”到“能打”的蜕变记录我连续备考杭电网安方向的研究生复试&#xff0c;到今天就整整第十九天了。先说实话&#xff0c;前两周我已经把常见算法题滚了两三遍&#xff0c;可在前天拿到一套杭电风格的复试模拟题时&#xff0c;还是被…

作者头像 李华
网站建设 2026/10/1 10:50:03

30岁转行网络安全晚吗?护城河与实战路线全解析

1. 30岁不是问题&#xff0c;问题是你的护城河在哪里 先给结论&#xff1a;30岁转行网络安全&#xff0c;完全来得及&#xff0c;而且我见过太多比这更晚入行、现在混得很好的案例。我不是给你灌鸡汤&#xff0c;而是基于对行业用人逻辑的观察。 很多人一上来就问"30岁学…

作者头像 李华
网站建设 2026/10/1 10:48:53

基于SpringCloud微服务的程序员薪资分析平台设计与实现

这套系统是用 SpringBoot、Vue、SpringCloud 微服务架构组合出的一个程序员薪资分析平台&#xff0c;从公开招聘信息里采集岗位数据&#xff0c;清洗入库后再用 ECharts 输出可视化大屏。我大概花了三周多的时间把这个全链路项目从零抡出来&#xff0c;中间踩了不少坑&#xff…

作者头像 李华
网站建设 2026/10/1 10:47:23

DDoS攻击类型全解析与防御思路(实战笔记):从入门到实战完整指南

本文深入探讨DDoS攻击类型全解析与防御思路&#xff08;实战笔记&#xff09;&#xff0c;涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。很多团队在DDoS与CC防护场景中都会遇到与DDoS攻击类型全解析与防御思路&#xff08;实战笔记&#xff09;相关的挑战…

作者头像 李华
网站建设 2026/10/1 10:47:06

WPF + C#构建高性能视频编辑器:核心架构与踩坑实录

前阵子做了一个可视化视频编辑器&#xff0c;技术栈就选了 WPF C#。怎么说呢&#xff0c;做之前我甚至犹豫过要不要上 .NET MAUI&#xff0c;后来冷静想了想&#xff0c;Windows 桌面端做视频编辑这种重渲染、重交互、重状态管理的产品形态&#xff0c;WPF 依然是最后的赢家。…

作者头像 李华
网站建设 2026/10/1 10:46:18

Mac快捷键底层逻辑与实战大全:从系统操作到开发工具一次讲透

Mac 笔记本用久了你会发现一个规律&#xff1a;真正让你效率翻倍的&#xff0c;不是翻了多少设置菜单&#xff0c;而是记住了几个关键快捷键。尤其是从 Windows 转到 Mac 的朋友&#xff0c;最初多半被 Command 键和 Control 键天天打架折磨到怀疑人生——CtrlC 复制变成了关闭…

作者头像 李华