一、UDP 代码的整体结构#define_WINSOCK_DEPRECATED_NO_WARNINGS#define_CRT_SECURE_NO_WARNINGS#includewinsock2.h#includecstdio#includecstring#pragmacomment(lib,ws2_32.lib)constintPORT8888;constchar*SERVER_IP127.0.0.1;voidrunServer();voidrunClient();intmain(){WSAStartup(MAKEWORD(2,2),wsa);选择角色;role1?runServer():runClient();WSACleanup();}生命周期与 TCP 对照项目上期 TCP 示例本 UDP 示例套接字类型SOCK_STREAMSOCK_DGRAM协议IPPROTO_TCP0由SOCK_DGRAM AF_INET推断为 UDP服务器流程socket → bind → listen → accept → recv/sendsocket → bind → recvfrom/sendto客户端流程socket → connect → send/recvsocket → sendto/recvfrom连接有三次握手、四次挥手无连接每个数据报自带目标/来源地址接收recv字节流recvfrom一次收一个数据报并拿到来源地址发送send已连接套接字sendto每次指定目标地址服务器并发模型accept返回新套接字一个 UDP 套接字服务所有客户端靠来源地址区分———接口典型场景说明send/recvTCP、已 connect() 的 UDP套接字已绑定对端读写不传地址sendto/recvfrom未连接 UDP、原始套接字等发送指定目的地址接收获取源地址UDP 全景服务器WSAStartup → socket(SOCK_DGRAM) → bind → recvfrom/sendto 循环 → closesocket → WSACleanup 客户端WSAStartup → socket(SOCK_DGRAM) → sendto/recvfrom 循环 → closesocket → WSACleanup没有listen、accept、connect也没有“连接套接字”和“监听套接字”的区分。服务器的sock既收又发客户端的sock也既发又收。二、头文件、宏与链接2.1 两个宏#define_WINSOCK_DEPRECATED_NO_WARNINGS#define_CRT_SECURE_NO_WARNINGS_WINSOCK_DEPRECATED_NO_WARNINGSMSVC 下关闭inet_ntoa、inet_addr等“已弃用”警告。本代码用了这两个旧 API所以需要它。_CRT_SECURE_NO_WARNINGS关闭 CRT 安全警告例如scanf等。这两个宏必须写在#include之前否则头文件已经处理过宏不生效。2.2 头文件#includewinsock2.h#includecstdio#includecstring这里只包含winsock2.h没有包含ws2tcpip.h。因为代码用的是旧接口inet_addr字符串 IPv4 → 二进制旧接口inet_ntoa二进制 IPv4 → 字符串旧接口没有用inet_pton/inet_ntop所以不需要ws2tcpip.h。如果改成现代写法#includewinsock2.h#includews2tcpip.hinet_pton(AF_INET,SERVER_IP,srv.sin_addr);inet_ntop(AF_INET,cli.sin_addr,ipbuf,sizeof(ipbuf));就需要加ws2tcpip.h。本代码没有定义WIN32_LEAN_AND_MEAN但也没有直接包含windows.h所以暂时不会触发旧winsock.h冲突。若后续加入windows.h#defineWIN32_LEAN_AND_MEAN#includewinsock2.h#includews2tcpip.h2.3 链接#pragmacomment(lib,ws2_32.lib)MSVC 自动链接 Winsock 导入库。MinGW/GCC 不支持这行需要命令行加-lws2_32。编译命令cl /EHsc /W4 udp_echo.cpp /link ws2_32.libg -Wall -Wextra -o udp_echo.exe udp_echo.cpp -lws2_32三、Winsock 初始化本代码放在main统一做intmain(){WSADATA wsa;if(WSAStartup(MAKEWORD(2,2),wsa)!0){printf(WSAStartup 失败\n);return1;}printf(1 - 服务器\n2 - 客户端\n请选择: );introle2;scanf(%d,role);getchar();if(role1)runServer();elserunClient();WSACleanup();return0;}要点WSAStartup必须在任何socket之前否则socket可能返回INVALID_SOCKET错误码WSANOTINITIALISED10093。MAKEWORD(2,2)请求 Winsock 2.2。WSACleanup与WSAStartup成对。这里在main末尾统一清理适合单进程单角色小 demo。scanf(%d, role); getchar();中getchar()用来吃掉回车换行避免后面fgets第一次读到空行。但注意runServer()是死循环正常路径永远走不到closesocket(sock)和WSACleanup()。按CtrlC结束进程时操作系统会回收套接字和 Winsock 资源。四、字节序与地址结构和 TCP 完全一致UDP 也使用sockaddr_in端口和地址同样要网络字节序。服务器sockaddr_in addr{};addr.sin_familyAF_INET;addr.sin_porthtons(PORT);addr.sin_addr.s_addrINADDR_ANY;客户端sockaddr_in srv{};srv.sin_familyAF_INET;srv.sin_porthtons(PORT);srv.sin_addr.s_addrinet_addr(SERVER_IP);字段赋值字节序sin_familyAF_INET主机序sin_porthtons(PORT)网络序sin_addr.s_addrINADDR_ANY或inet_addr()结果网络序sin_zero[8]{}零初始化—INADDR_ANY表示服务器监听本机所有网卡。inet_addr(127.0.0.1)返回网络序 IPv4 地址。打印客户端端口时要ntohs(cli.sin_port)转回主机序。但inet_addr有两个问题返回值INADDR_NONE既可能表示255.255.255.255也可能表示格式错误无法区分不支持 IPv6。可改用inet_pton(AF_INET,SERVER_IP,srv.sin_addr);并检查返回值1成功0格式非法-1系统错误。同样inet_ntoa(cli.sin_addr)返回的是静态缓冲区多线程不安全并且一次printf中不要多次调用它。本代码只调用一次问题不大可改为inet_ntop。五、UDP 与 TCP 的核心差异无连接、数据报TCP 是字节流recv一次不保证一个完整消息存在粘包/半包。UDP 是数据报一次sendto对应一个数据报recvfrom一次取一个数据报保留消息边界。特性TCPUDP连接面向连接无连接可靠性可靠、有序、自动重传不保证到达、不保证顺序、可能重复消息边界无字节流有一次一数据报服务器 APIlisten/accept无客户端 APIconnect通常无直接sendto发送sendsendto接收recvrecvfrom服务器区分客户端accept返回新套接字每个数据报的来源地址UDP 的“无连接”不等于不能connect。客户端也可以connect一个 UDP 套接字之后用send/recv内核会固定对端并过滤来源但本代码没有这样做。六、服务器端逐段详解6.1 创建 UDP 套接字SOCKET socksocket(AF_INET,SOCK_DGRAM,0);if(sockINVALID_SOCKET){printf(socket 失败\n);return;}参数值含义afAF_INETIPv4typeSOCK_DGRAM数据报套接字对应 UDPprotocol0让系统根据AF_INET SOCK_DGRAM推断为 UDP也可以显式写IPPROTO_UDP可读性更好socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);失败返回INVALID_SOCKET。这里只打印“socket 失败”没有打印WSAGetLastError()。6.2bind绑定本地端口sockaddr_in addr{};addr.sin_familyAF_INET;addr.sin_porthtons(PORT);addr.sin_addr.s_addrINADDR_ANY;if(bind(sock,(sockaddr*)addr,sizeof(addr))SOCKET_ERROR){printf(bind 失败: %d\n,WSAGetLastError());closesocket(sock);return;}UDP 服务器也必须bind否则系统不知道它该在哪个端口收数据报。INADDR_ANY表示所有网卡。htons(PORT)不能漏。失败常见原因错误码常量原因10048WSAEADDRINUSE端口已被占用10013WSAEACCES权限不足低端口在部分系统需要管理员10049WSAEADDRNOTAVAIL指定地址不可用6.3 接收循环recvfromcharbuf[1024];sockaddr_in cli{};intcliLensizeof(cli);while(true){cliLensizeof(cli);intnrecvfrom(sock,buf,sizeof(buf)-1,0,(sockaddr*)cli,cliLen);if(n0)continue;buf[n]\0;printf(收到 [%s:%d]: %s\n,inet_ntoa(cli.sin_addr),ntohs(cli.sin_port),buf);constchar*replyhello from server;sendto(sock,reply,(int)strlen(reply),0,(sockaddr*)cli,cliLen);}这是 UDP 服务器核心。recvfrom参数参数方向说明sock输入已绑定端口的 UDP 套接字buf输出接收缓冲区sizeof(buf)-1输入最多收多少字节留一个给\00输入flags(sockaddr*)cli输出内核填入发送方地址cliLen输入/输出输入缓冲区大小输出实际地址结构大小关键点recvfrom是阻塞的没有数据报就一直等。一次recvfrom取一个数据报。UDP 保留消息边界不像 TCP 字节流会粘包。cli被填入来源地址服务器靠它知道回复给谁。sendto又用同一个cli和cliLen把回复发回去。cliLen每轮循环都要重置为sizeof(cli)。如果忘记重置下一轮recvfrom可能因为长度被改小而出错。buf[n] \0是文本处理需要UDP 本身不补字符串结束符。sizeof(buf)-1留一个字节给\0。如果数据报大于缓冲区Windows 下recvfrom可能返回SOCKET_ERROR错误码WSAEMSGSIZE10040。代码n 0会直接continue把错误和零长度数据报都忽略了。recvfrom返回值语义返回值含义本代码处理 0收到n字节正常处理0合法的零长度 UDP 数据报被n 0忽略SOCKET_ERROR出错被忽略可能忙循环按返回值区分if(nSOCKET_ERROR){interrWSAGetLastError();if(errWSAEMSGSIZE){/* 数据报被截断 */}else{/* 记录错误必要时退出 */}continue;}if(n0){/* 零长度数据报可选择处理 */}6.4sendto向来源地址回复constchar*replyhello from server;sendto(sock,reply,(int)strlen(reply),0,(sockaddr*)cli,cliLen);sendto参数参数说明sockUDP 套接字reply数据缓冲区strlen(reply)发送长度不含\00flags(sockaddr*)cli目标地址这里就是刚才的来源地址cliLen目标地址结构长度sendto成功返回实际交给内核的字节数失败返回SOCKET_ERROR。但注意sendto成功只表示数据报交给了本地内核不保证到达对端。UDP 没有重传、没有 ACK、没有顺序保证。flags是sendto调用时的“附加选项”一个位掩码每一位代表一种特殊行为可以用|组合常见 flags 含义flags作用备注0默认行为最常用MSG_DONTWAIT本次发送非阻塞Linux 常用即使 socket 是阻塞的这次也不等MSG_NOSIGNAL出错时不产生SIGPIPE信号Linux主要对 TCP/流式套接字有意义UDP 基本无关MSG_DONTROUTE不经过正常路由直接发本地网络特殊场景MSG_OOB发送带外数据TCP 用UDP 通常不支持MSG_MORE告诉内核“后面还有数据”可延迟/合并发送Linux 上 UDP 也有特殊语义普通代码很少用MSG_CONFIRM告诉内核邻居地址仍然有效Linux底层网络细节多个标志可以组合sendto(sock,buf,len,MSG_DONTWAIT|MSG_NOSIGNAL,...);为什么普通 UDP 代码一般写0因为绝大多数 UDP 发送就是把这个数据报发给这个地址能发就发发不出去就返回错误。不需要非阻塞、带外、路由控制这些特殊行为所以写0最简单、可移植。注意flags和 socket 本身的非阻塞是两回事socket(..., SOCK_DGRAM, 0)创建的是普通阻塞 socket。如果 socket 设了O_NONBLOCK那所有发送默认非阻塞。MSG_DONTWAIT是只让本次调用非阻塞不改 socket 状态。例如sendto(sock,buf,len,MSG_DONTWAIT,...);如果发送缓冲区满了立刻返回-1错误码EAGAIN/EWOULDBLOCK而不是卡住等。七、客户端逐段详解7.1 创建 UDP 套接字SOCKET socksocket(AF_INET,SOCK_DGRAM,0);if(sockINVALID_SOCKET){printf(socket 失败\n);return;}客户端不需要bind。第一次sendto时内核会自动为它分配一个临时端口ephemeral port服务器回复时就发到这个临时端口。7.2 填写服务器地址sockaddr_in srv{};srv.sin_familyAF_INET;srv.sin_porthtons(PORT);srv.sin_addr.s_addrinet_addr(SERVER_IP);SERVER_IP默认127.0.0.1。换机器测试时改成服务器实际 IP。inet_addr没有检查返回值。如果SERVER_IP写错inet_addr返回INADDR_NONE后续sendto可能失败。现代写法if(inet_pton(AF_INET,SERVER_IP,srv.sin_addr)!1){printf(invalid ip: %s\n,SERVER_IP);closesocket(sock);return;}7.3 输入、发送、接收回复charbuf[1024];while(true){printf(输入要发送的消息(q 退出): );if(!fgets(buf,sizeof(buf),stdin))break;buf[strcspn(buf,\r\n)]\0;if(strcmp(buf,q)0)break;sendto(sock,buf,(int)strlen(buf),0,(sockaddr*)srv,sizeof(srv));intsrvLensizeof(srv);intnrecvfrom(sock,buf,sizeof(buf)-1,0,(sockaddr*)srv,srvLen);if(n0){buf[n]\0;printf(回复: %s\n,buf);}}closesocket(sock);细节fgets读一行保留换行buf[strcspn(buf, \r\n)] \0去掉\r和\n。输入q退出。sendto每次指定目标srv发送strlen(buf)字节不含\0。发送后阻塞等待recvfrom。如果服务器没开、回复丢包、网络不通客户端会一直卡在recvfrom。可设置接收超时inttimeout_ms5000;setsockopt(sock,SOL_SOCKET,SO_RCVTIMEO,(constchar*)timeout_ms,sizeof(timeout_ms));超时后recvfrom返回SOCKET_ERRORWSAGetLastError()为WSAETIMEDOUT10060。recvfrom的srv和srvLen是输出参数。调用后srv会被回复来源地址覆盖。正常情况下就是服务器地址但 UDP 未connect时recvfrom可以接收任何来源的数据报存在伪造/欺骗风险。若要限制只收服务器回复可以客户端先connect(sock, (sockaddr*)srv, sizeof(srv))之后用send/recv或recvfrom后比较来源地址。n 0才打印。n 0是零长度数据报SOCKET_ERROR是错误本代码都静默忽略。客户端closesocket(sock)可达然后main执行WSACleanup()资源清理正确。八、UDP 关键 API 速查API作用本代码成功失败WSAStartup初始化 Winsockmain0非 0WSACleanup清理 Winsockmain0SOCKET_ERRORsocket创建套接字服务器/客户端SOCKETINVALID_SOCKETbind绑定本地 IP:端口仅服务器0SOCKET_ERRORsendto向指定地址发数据报两者发送字节数SOCKET_ERRORrecvfrom收数据报并取来源地址两者字节数0 零长度数据报SOCKET_ERRORclosesocket关闭套接字两者0SOCKET_ERRORWSAGetLastError取错误码部分使用——htons/ntohs端口字节序转换两者——inet_addr字符串 IPv4 → 网络序客户端网络序地址INADDR_NONE有歧义inet_ntoa网络序 IPv4 → 字符串服务器打印静态字符串NULL线程不安全sendto/recvfrom和 TCP 博客里的send/recv对应关系TCP: send(已连接套接字, buf, len, 0) UDP: sendto(套接字, buf, len, 0, 目标地址, 地址长度) TCP: recv(已连接套接字, buf, len, 0) UDP: recvfrom(套接字, buf, len, 0, 来源地址输出, 地址长度输入/输出)
阅读完成 · 觉得有帮助?