UDP 协议基础认知
UDP(User Datagram Protocol,用户数据报协议)是传输层核心协议之一,基于 IP 协议实现跨网络主机进程间的无连接数据传输。它面向事务提供简单通信服务,不保证数据交付、有序性和重复防护,也不提供数据包分组与组装功能,适合对实时性要求高、可容忍少量数据丢失的场景。
注:传输层的核心作用是 “端到端”(主机进程间)通信,网络层(IP)仅负责 “点到点”(主机间)数据包转发,因此 UDP 需依赖 IP 完成跨网络路由,同时通过端口号定位主机内的具体进程。
UDP 协议核心特点
- 无连接特性:通信前无需建立连接(如 TCP 的三次握手),发送方直接封装数据报发送,接收方无需发送确认回执,通信流程极简,减少延迟。
- 不可靠传输:
- 不保证数据到达:数据包可能因网络拥堵、链路故障丢失,UDP 无重传机制;
- 不保证有序性:数据包可能因路由不同导致接收顺序错乱,UDP 不处理排序;
- 不防重复:若网络出现数据包重传,UDP 会直接交付进程,不判断重复。
- 轻量高效:报首仅 8 字节(远小于 TCP 的 20 字节最小报首),协议控制字段少,内核处理开销低,数据传输效率高。
- 面向数据报:数据以 “数据报” 为最小传输单位,发送方一次发送一个完整数据报,接收方一次必须读取一个完整数据报(若缓冲区不足,多余数据会被截断并设置
MSG_TRUNC标志)。
UDP 报首结构详解
UDP 数据报由 “报首” 和 “数据” 两部分组成,报首固定 8 字节,无可选字段,结构如下(按字节偏移排序):

| 字段 | 长度(bit) | 长度(字节) | 核心作用 | 补充说明 |
|---|---|---|---|---|
| 源端口号 | 16 | 2 | 标识发送方主机的进程端口 | 可选字段,若不使用(如无需接收回执),填充为 0 |
| 目标端口号 | 16 | 2 | 标识接收方主机的进程端口 | 必选字段,端口号仅 “本地有效”(不同主机的相同端口可能对应不同进程,如主机 A 的 8080 是浏览器,主机 B 的 8080 是服务器) |
| 包总长度 | 16 | 2 | 表示整个 UDP 数据报(报首 + 数据)的总字节数 | 最小值为 8(仅报首,无数据),最大值为 65535(16bit 无符号数的上限) |
| 校验和 | 16 | 2 | 检测数据报在传输过程中是否出错(如比特翻转) | 计算时需包含 “伪报首”(IP 头中的源 IP、目标 IP、协议号、UDP 长度);若设为 0,表示发送方未生成校验和,接收方不校验 |
关键计算:UDP 数据最大长度
UDP 数据部分的最大长度 = UDP 包总长度上限 - UDP 报首长度 - IP 报首默认长度
- UDP 包总长度上限:65535 字节(16bit 字段限制)
- UDP 报首长度:固定 8 字节
- IP 报首默认长度:20 字节(无可选字段时)→ 最终 UDP 数据最大长度 = 65535 - 8 - 20 = 65507 字节
实际应用中,为避免 IP 分片(分片丢失会导致整个 UDP 数据报失效),通常建议 UDP 数据长度 ≤ 1472 字节(推导:MTU 默认 1500 字节 - IP 头 20 字节 - UDP 头 8 字节 = 1472 字节)。
UDP 核心编程接口(Linux 系统)
头文件
#include <sys/socket.h> // 核心套接字函数(socket、bind、sendto等)
#include <netinet/in.h> // 网络地址结构(struct sockaddr_in、in_addr等)
#include <arpa/inet.h> // 字节序转换(htons、ntohl)与IP转换(inet_aton、inet_ntoa)
#include <netdb.h> // 域名解析函数(gethostbyname、getaddrinfo等)
核心函数详解
socket ():创建 UDP 套接字
/*** @brief 创建UDP通信的“端点”(套接字文件),是跨主机进程通信的基础* @param domain 协议族,UDP必选AF_INET(IPv4)或AF_INET6(IPv6)* - AF_INET:使用IPv4地址(32位),对应struct sockaddr_in* - AF_INET6:使用IPv6地址(128位),对应struct sockaddr_in6* @param type 套接字类型,UDP必须设为SOCK_DGRAM(无连接数据报类型)* - SOCK_DGRAM:无连接、不可靠、固定长度数据报,对应UDP* - 对比SOCK_STREAM:面向连接、可靠字节流,对应TCP* @param protocol 具体协议号,UDP设为0即可(系统自动匹配SOCK_DGRAM对应的IPPROTO_UDP)* @return int 成功返回“套接字文件描述符”(非负整数,如3、4),失败返回-1(需通过errno查看错误原因)* @note 套接字是Linux七种文件类型之一(标识符为's'),专门用于不同主机进程间数据传输;* 同一主机的进程通信(如管道、共享内存)无法跨主机,必须通过套接字;* 若创建失败,常见错误:domain无效(如填123)、type与protocol不匹配(如SOCK_STREAM配0会默认TCP)。*/
int socket(int domain, int type, int protocol);// 示例:创建IPv4的UDP套接字
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_fd == -1) {perror("socket create failed"); // 打印错误原因(如"socket create failed: Address family not supported by protocol")return -1;
}
bind ():绑定本地地址与端口
/*** @brief 将UDP套接字与“本地IP+端口”绑定,使套接字能接收发送到该IP和端口的数据* @param sockfd 已创建的UDP套接字文件描述符(socket()的返回值)* @param addr 指向“本地网络地址结构”的指针,需强制转换为struct sockaddr*(通用地址结构)* - IPv4场景下,实际使用struct sockaddr_in(专门存储IPv4地址)* @param addrlen 地址结构的字节长度(通过sizeof(addr)计算)* @return int 成功返回0,失败返回-1(常见错误:端口被占用、IP地址无效)* @note 接收数据必须绑定:若不绑定,系统会随机分配一个临时端口(1024~65535),发送方无法定位;* 端口选择规则:1~1023是“知名端口”(如80是HTTP、53是DNS),普通用户需用1024以上端口;* 地址结构需注意“字节序”:端口和IP必须转换为网络字节序(大端),否则跨平台通信会出错。*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);// 示例:将UDP套接字绑定到本地192.168.1.100的8888端口
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr)); // 初始化地址结构(避免垃圾值)
local_addr.sin_family = AF_INET; // 协议族:IPv4
local_addr.sin_port = htons(8888); // 端口:8888(转换为网络字节序)
local_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // IP:192.168.1.100(转换为网络字节序)int ret = bind(udp_fd, (struct sockaddr*)&local_addr, sizeof(local_addr));
if (ret == -1) {perror("bind failed"); // 常见错误:"bind failed: Address already in use"(端口被占用)close(udp_fd); // 失败时需关闭套接字,避免资源泄漏return -1;
}
sendto ():发送 UDP 数据报
/*** @brief 向指定“目标IP+端口”发送UDP数据报(无连接,每次发送需指定目标地址)* @param sockfd 已创建的UDP套接字文件描述符* @param buf 指向“待发送数据”的缓冲区(如字符串、二进制数据)* @param len 待发送数据的字节长度(需≤65507字节,建议≤1472字节避免IP分片)* @param flags 发送标志,默认设为0(与write()功能一致,阻塞发送)* - 常用标志:MSG_DONTWAIT(非阻塞发送,若无缓冲区则立即返回错误)* @param dest_addr 指向“目标网络地址结构”的指针(存储目标IP和端口)* @param addrlen 目标地址结构的字节长度* @return ssize_t 成功返回“实际发送的字节数”(通常等于len,若网络异常可能小于len),失败返回-1* @note UDP无连接:即使目标主机不存在或端口未监听,sendto()也可能返回成功(数据会在网络中丢失);* 数据截断风险:若数据长度超过UDP包总长度上限(65535),sendto()会返回-1,错误码为EMSGSIZE;* 阻塞特性:默认阻塞发送,直到数据被拷贝到内核发送缓冲区(非直到数据到达目标主机)。*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);// 示例:向192.168.1.200的8888端口发送字符串
char send_data[] = "Hello UDP Server!";
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(8888);
dest_addr.sin_addr.s_addr = inet_addr("192.168.1.200"); // 目标IPssize_t send_len = sendto(udp_fd, send_data, sizeof(send_data), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
if (send_len == -1) {perror("sendto failed"); // 常见错误:"sendto failed: Network is unreachable"(目标网络不可达)return -1;
}
printf("Sent %zd bytes: %s\n", send_len, send_data);
recvfrom ():接收 UDP 数据报
/*** @brief 从UDP套接字接收数据,并获取“发送方的IP+端口”(若需要)* @param sockfd 已绑定的UDP套接字文件描述符(未绑定则无法接收)* @param buf 指向“存储接收数据”的缓冲区(需提前分配空间,避免越界)* @param len 缓冲区的最大字节长度(若接收数据超过len,多余数据会被截断)* @param flags 接收标志,默认设为0(阻塞接收,直到有数据到达)* - 常用标志:MSG_DONTWAIT(非阻塞接收,无数据则返回-1,错误码EAGAIN)* @param src_addr 指向“发送方地址结构”的指针(用于存储发送方IP和端口,可设为NULL表示不关心)* @param addrlen 指向“发送方地址结构长度”的指针(需提前初始化,如设为sizeof(src_addr))* @return ssize_t 成功返回“实际接收的字节数”(0表示接收空数据报,UDP支持空数据报),失败返回-1* @note 阻塞特性:默认阻塞接收,直到有数据到达或发生错误(如套接字被关闭);* 地址长度注意:addrlen是“值-结果”参数,传入前需设为地址结构的初始长度,返回后存储实际地址长度;* 多发送方处理:若多个发送方向同一端口发送数据,recvfrom()会按到达顺序接收,通过src_addr区分发送方。*/
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);// 示例:接收数据并打印发送方IP和端口
char recv_buf[1024] = {0}; // 缓冲区:1024字节
struct sockaddr_in src_addr;
socklen_t src_addr_len = sizeof(src_addr); // 初始长度ssize_t recv_len = recvfrom(udp_fd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&src_addr, &src_addr_len);
if (recv_len == -1) {perror("recvfrom failed");return -1;
}
// 将发送方IP从网络字节序转换为点分十进制字符串
char src_ip[INET_ADDRSTRLEN] = {0};
inet_ntop(AF_INET, &src_addr.sin_addr, src_ip, sizeof(src_ip));
// 将发送方端口从网络字节序转换为主机字节序
uint16_t src_port = ntohs(src_addr.sin_port);printf("Received %zd bytes from %s:%d: %s\n", recv_len, src_ip, src_port, recv_buf);
setsockopt ()/getsockopt ():控制套接字选项(广播 / 组播)
/*** @brief 设置或获取UDP套接字的属性选项(如启用广播、设置接收超时等)* @param sockfd 已创建的UDP套接字文件描述符* @param level 选项的“协议级别”:* - SOL_SOCKET:套接字级选项(通用选项,如SO_BROADCAST、SO_RCVBUF)* - IPPROTO_IP:IP级选项(如IP_ADD_MEMBERSHIP,组播加入)* @param optname 选项名称(需与level匹配):* - SO_BROADCAST:启用/禁用广播功能(SOL_SOCKET级)* - IP_ADD_MEMBERSHIP:加入组播组(IPPROTO_IP级)* @param optval 指向“选项值”的指针(如启用广播则设为非0整数)* @param optlen 选项值的字节长度(如sizeof(int))* @return int 成功返回0,失败返回-1* @note 广播功能必须启用SO_BROADCAST:默认禁用,不启用则无法发送广播数据;* 选项值类型:布尔型选项(如SO_BROADCAST)用int表示(0禁用,非0启用),复杂选项(如IP_ADD_MEMBERSHIP)用结构体;* getsockopt()用法类似,用于获取当前选项值(如检查广播是否已启用)。*/
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);// 示例1:启用UDP广播功能
int broadcast_en = 1; // 1=启用,0=禁用
int ret = setsockopt(udp_fd, SOL_SOCKET, SO_BROADCAST, &broadcast_en, sizeof(broadcast_en));
if (ret == -1) {perror("setsockopt SO_BROADCAST failed");return -1;
}// 示例2:检查广播功能是否已启用
int current_broadcast;
socklen_t opt_len = sizeof(current_broadcast);
ret = getsockopt(udp_fd, SOL_SOCKET, SO_BROADCAST, ¤t_broadcast, &opt_len);
if (ret == -1) {perror("getsockopt SO_BROADCAST failed");return -1;
}
printf("Broadcast enabled: %s\n", current_broadcast ? "Yes" : "No");

IP 地址转换函数
① 点分十进制字符串 → 网络字节序整型(32 位)
/*** @brief 将点分十进制IP字符串(如"192.168.1.100")转换为网络字节序的32位整型* @param cp 待转换的IP字符串(必须是合法的点分十进制,如"256.1.1.1"无效)* @param inp 指向struct in_addr的指针(存储转换后的网络字节序整型IP)* @return int 成功返回非0(真),失败返回0(假)* @note 安全推荐使用:不会将255.255.255.255误判为错误(对比inet_addr());* 转换结果存储在inp->s_addr中(s_addr是32位无符号整型,网络字节序)。*/
int inet_aton(const char *cp, struct in_addr *inp);/*** @brief 将点分十进制IP字符串转换为网络字节序的32位整型(已过时,不推荐)* @param cp 待转换的IP字符串* @return in_addr_t 成功返回网络字节序整型IP,失败返回INADDR_NONE(值为-1,即0xFFFFFFFF)* @note 缺陷:255.255.255.255(合法广播地址)转换后也是0xFFFFFFFF,无法区分“有效地址”和“错误”;* 替代方案:优先使用inet_aton()或inet_pton()(支持IPv6)。*/
in_addr_t inet_addr(const char *cp);/*** @brief 提取IP字符串的“网络部分”(按IP分类),转换为网络字节序整型* @param cp 待转换的IP字符串(如"192.168.1.100",C类地址,网络部分是192.168.1)* @return in_addr_t 成功返回网络部分的整型值(网络字节序),失败返回INADDR_NONE* @note IP分类规则:A类(0.0.0.0~127.255.255.255)网络部分8位,B类(128.0.0.0~191.255.255.255)16位,C类(192.0.0.0~223.255.255.255)24位;* 示例:inet_network("192.168.1")返回0xC0A80100(192.168.1.0的网络字节序)。*/
in_addr_t inet_network(const char *cp);// 示例:inet_aton()使用
struct in_addr ip_addr;
if (inet_aton("192.168.1.100", &ip_addr) == 0) {printf("Invalid IP address\n");return -1;
}
printf("Network byte order IP: 0x%X\n", ip_addr.s_addr); // 输出:0x6401A8C0(192.168.1.100的网络字节序)
② 网络字节序整型 → 点分十进制字符串
/*** @brief 将网络字节序的32位整型IP转换为点分十进制字符串(仅支持IPv4)* @param in struct in_addr类型的IP(存储网络字节序整型IP)* @return char* 成功返回静态缓冲区的字符串指针(如"192.168.1.100"),无失败(但输入无效会返回异常字符串)* @note 静态缓冲区风险:函数内部使用静态数组存储结果,多次调用会覆盖之前的结果;* 线程不安全:多线程同时调用会导致结果错乱,替代方案用inet_ntop()(线程安全,支持IPv6)。*/
char *inet_ntoa(struct in_addr in);/*** @brief 由“网络号”和“主机号”构造网络字节序的IP地址* @param net 网络号(网络字节序,如C类地址的前24位)* @param host 主机号(网络字节序,如C类地址的后8位)* @return struct in_addr 构造后的IP地址结构(s_addr为完整网络字节序IP)* @note 示例:net=0xC0A80100(192.168.1.0),host=0x64(100),构造后IP为192.168.1.100。*/
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);/*** @brief 从IP地址中提取“主机号”(网络字节序)* @param in IP地址结构(网络字节序)* @return in_addr_t 提取的主机号(网络字节序)* @note 示例:IP=192.168.1.100(0x6401A8C0),C类地址主机号8位,返回0x64(100)。*/
in_addr_t inet_lnaof(struct in_addr in);/*** @brief 从IP地址中提取“网络号”(网络字节序)* @param in IP地址结构(网络字节序)* @return in_addr_t 提取的网络号(网络字节序)* @note 示例:IP=192.168.1.100,C类地址网络号24位,返回0xC0A80100(192.168.1.0)。*/
in_addr_t inet_netof(struct in_addr in);// 示例:inet_ntoa()使用
struct in_addr ip_addr;
ip_addr.s_addr = 0x6401A8C0; // 192.168.1.100的网络字节序
char *ip_str = inet_ntoa(ip_addr);
printf("IP string: %s\n", ip_str); // 输出:IP string: 192.168.1.100// 注意:多次调用覆盖问题
struct in_addr ip1, ip2;
inet_aton("192.168.1.100", &ip1);
inet_aton("192.168.1.200", &ip2);
char *str1 = inet_ntoa(ip1);
char *str2 = inet_ntoa(ip2);
printf("str1: %s, str2: %s\n", str1, str2); // 输出:str1: 192.168.1.200, str2: 192.168.1.200(str1被覆盖)
字节序转换函数
不同主机的 “字节序” 不同(存储多字节数据的顺序),网络字节序统一为 “大端序”(高字节存低地址),因此端口和 IP 必须转换:
- 小端序:x86/ARM(默认),低字节存低地址(如 0x12345678 存为 0x78、0x56、0x34、0x12)
- 大端序:网络字节序,高字节存低地址(如 0x12345678 存为 0x12、0x34、0x56、0x78)

#include <arpa/inet.h>
/*** @brief 主机字节序的32位整数 → 网络字节序的32位整数(用于IP地址转换)* @param hostlong 主机字节序的32位无符号整数(如0xC0A80164,192.168.1.100的小端序)* @return uint32_t 网络字节序的32位整数(如0x6401A8C0)* @note 仅在小端主机上会转换,大端主机上直接返回原数(无操作)。*/
uint32_t htonl(uint32_t hostlong);/*** @brief 主机字节序的16位整数 → 网络字节序的16位整数(用于端口转换)* @param hostshort 主机字节序的16位无符号整数(如8888,小端序为0x22B8)* @return uint16_t 网络字节序的16位整数(如0xB822)* @note 端口是16位,必须用htons()转换,否则跨端通信会连接到错误端口(如8888→47282)。*/
uint16_t htons(uint16_t hostshort);/*** @brief 网络字节序的32位整数 → 主机字节序的32位整数(用于IP地址转换)* @param netlong 网络字节序的32位整数(如0x6401A8C0)* @return uint32_t 主机字节序的32位整数(如0xC0A80164)*/
uint32_t ntohl(uint32_t netlong);/*** @brief 网络字节序的16位整数 → 主机字节序的16位整数(用于端口转换)* @param netshort 网络字节序的16位整数(如0xB822)* @return uint16_t 主机字节序的16位整数(如0x22B8,即8888)*/
uint16_t ntohs(uint16_t netshort);// 示例:端口和IP的字节序转换
uint16_t host_port = 8888;
uint16_t net_port = htons(host_port);
printf("Host port: %d → Network port: 0x%X\n", host_port, net_port); // 输出:Host port: 8888 → Network port: 0xB822uint32_t host_ip = 0xC0A80164; // 192.168.1.100(小端序)
uint32_t net_ip = htonl(host_ip);
printf("Host IP: 0x%X → Network IP: 0x%X\n", host_ip, net_ip); // 输出:Host IP: 0xC0A80164 → Network IP: 0x6401A8C0
UDP 高级功能(广播与组播)
广播通信(一对多,局域网内)
核心概念
- 广播地址:局域网内的 “全主机地址”,向该地址发送的数据会被所有主机接收(C 类地址的广播地址是主机位全 1,如 192.168.1.255);
- 实现条件:
- 启用套接字的
SO_BROADCAST选项(默认禁用); - 接收方必须绑定与发送方相同的端口(否则主机收到数据后无法交付进程,直接丢弃);
- 广播仅能在局域网内传播(路由器默认不转发广播包,避免网络风暴)。
- 启用套接字的
广播发送示例
#include <stdio.h> // 标准输入输出(printf、perror)
#include <stdlib.h> // 标准库(EXIT_FAILURE/EXIT_SUCCESS)
#include <string.h> // 内存操作(memset)
#include <unistd.h> // 系统调用(close、sleep)
#include <sys/socket.h> // 套接字核心函数(socket、sendto、setsockopt)
#include <netinet/in.h> // 网络地址结构(sockaddr_in)、字节序转换(htons)
#include <arpa/inet.h> // IP地址转换(inet_addr)int main() {// 创建UDP套接字(IPv4协议族、数据报类型、默认UDP协议)int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);if (udp_fd == -1) {perror("socket create failed"); // 打印错误详情(如资源不足、协议不支持)return EXIT_FAILURE; // 标准错误退出码,比return -1更规范}// 启用广播功能(UDP默认禁用广播,必须显式开启)int broadcast_en = 1; // 1=启用,0=禁用(套接字布尔选项用int存储)if (setsockopt(udp_fd, SOL_SOCKET, SO_BROADCAST, &broadcast_en, sizeof(broadcast_en)) == -1) {perror("enable broadcast failed");close(udp_fd); // 失败时关闭套接字,避免资源泄漏return EXIT_FAILURE;}// 配置广播目标地址(子网:192.168.1.0/24,广播地址:192.168.1.255,端口:8888)struct sockaddr_in broad_addr;memset(&broad_addr, 0, sizeof(broad_addr)); // 初始化地址结构,清除垃圾值broad_addr.sin_family = AF_INET; // 协议族:IPv4(必须与socket()的domain一致)broad_addr.sin_port = htons(8888); // 端口转换为网络字节序(大端),避免跨平台错误broad_addr.sin_addr.s_addr = inet_addr("192.168.1.255"); // 广播地址(主机位全1)// 待发送的广播数据(自动包含字符串结束符'\0',确保接收方完整解析)char broad_data[] = "This is a UDP broadcast message!";printf("UDP broadcast sender started.\n");printf("Broadcast address: 192.168.1.255:%d\n", 8888);printf("Send every 3 seconds, press Ctrl+C to stop.\n\n");// 循环发送广播(死循环,需手动中断)while (1) {// 发送广播数据ssize_t send_len = sendto(udp_fd, // 套接字文件描述符broad_data, // 待发送数据缓冲区sizeof(broad_data), // 数据长度(含'\0',共34字节)0, // 发送标志:0=阻塞发送(默认),与write()行为一致(struct sockaddr*)&broad_addr, // 目标地址(强制转换为通用地址结构)sizeof(broad_addr) // 目标地址结构的字节长度);// 检查发送结果if (send_len == -1) {perror("send broadcast failed");close(udp_fd);return EXIT_FAILURE;}// 打印发送成功信息(send_len为实际发送字节数,正常等于sizeof(broad_data))printf("Sent %zd bytes: %s\n", send_len, broad_data);sleep(3); // 暂停3秒,控制发送频率}// 理论上死循环不会执行到这里,但保留close()避免编译警告close(udp_fd);return EXIT_SUCCESS;
}
广播接收示例
#include <stdio.h> // 标准输入输出(printf、perror)
#include <stdlib.h> // 标准库(EXIT_FAILURE/EXIT_SUCCESS)
#include <string.h> // 内存操作(memset、字符串处理)
#include <unistd.h> // 系统调用(close)
#include <sys/socket.h> // 套接字核心函数(socket、bind、recvfrom)
#include <netinet/in.h> // 网络地址结构(sockaddr_in)、字节序转换(htons、ntohs)、宏定义(INADDR_ANY)
#include <arpa/inet.h> // IP地址转换(inet_ntop)、常量定义(INET_ADDRSTRLEN)int main() {// 创建UDP套接字(IPv4协议族、数据报类型、默认UDP协议)int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);if (udp_fd == -1) {perror("socket create failed");return EXIT_FAILURE;}// 配置本地绑定地址(绑定8888端口,与发送端一致)struct sockaddr_in local_addr;memset(&local_addr, 0, sizeof(local_addr)); // 初始化地址结构,清除垃圾值local_addr.sin_family = AF_INET; // 协议族:IPv4(与socket()的domain一致)local_addr.sin_port = htons(8888); // 绑定端口8888(转换为网络字节序)// 绑定所有本地网卡(INADDR_ANY = 0.0.0.0),接收来自任意网卡的广播/单播数据local_addr.sin_addr.s_addr = htonl(INADDR_ANY);// 绑定套接字与本地地址(UDP接收必须绑定端口,否则系统随机分配端口,无法接收广播)if (bind(udp_fd, (struct sockaddr*)&local_addr, sizeof(local_addr)) == -1) {perror("bind port 8888 failed");close(udp_fd); // 失败时关闭套接字,避免资源泄漏return EXIT_FAILURE;}printf("UDP broadcast receiver started.\n");printf("Bound port: 8888\n");printf("Waiting for broadcast messages (press Ctrl+C to stop)...\n\n");// 接收数据相关变量初始化char recv_buf[1024] = {0}; // 接收缓冲区(1024字节,足够存储大部分广播数据)struct sockaddr_in src_addr; // 存储发送方(广播源)的地址信息socklen_t src_len = sizeof(src_addr); // 发送方地址结构长度(值-结果参数)// 循环接收广播数据(阻塞等待,直到有数据到达或出错)while (1) {// 接收数据:recvfrom会阻塞,直到收到数据或发生错误ssize_t recv_len = recvfrom(udp_fd, // 套接字文件描述符recv_buf, // 接收数据缓冲区sizeof(recv_buf) - 1, // 缓冲区最大可用长度(留1字节存'\0',避免字符串溢出)0, // 接收标志:0=阻塞接收(默认)(struct sockaddr*)&src_addr, // 发送方地址指针(存储广播源IP和端口)&src_len // 发送方地址长度指针(传入初始长度,返回实际长度));// 检查接收结果if (recv_len == -1) {perror("recv broadcast failed");memset(recv_buf, 0, sizeof(recv_buf)); // 清空缓冲区,避免垃圾数据干扰continue; // 忽略错误,继续等待下一条数据}// 给接收的数据添加字符串结束符(确保printf正常打印,避免乱码)recv_buf[recv_len] = '\0';// 将发送方的网络字节序IP转换为点分十进制字符串(线程安全,比inet_ntoa更推荐)char src_ip[INET_ADDRSTRLEN] = {0}; // INET_ADDRSTRLEN=16,足够存储IPv4地址(xxx.xxx.xxx.xxx)inet_ntop(AF_INET, &src_addr.sin_addr, src_ip, sizeof(src_ip));// 将发送方的网络字节序端口转换为主机字节序uint16_t src_port = ntohs(src_addr.sin_port);// 打印接收结果(包含发送方IP、端口和数据)printf("Received from %s:%d (bytes: %zd): %s\n", src_ip, src_port, recv_len, recv_buf);// 清空缓冲区,准备接收下一条数据memset(recv_buf, 0, sizeof(recv_buf));}// 理论上死循环不会执行到这里,保留close()避免编译警告close(udp_fd);return EXIT_SUCCESS;
}
组播通信(一对多,可控范围)

核心概念
- 组播地址:D 类 IP 地址(224.0.0.0~239.255.255.255),仅用于标识组播组,不对应具体主机;
- 组播组:加入同一组播地址的主机集合,发送方向组播地址发送数据,仅组内主机能接收;
- 优势:相比广播,减少网络带宽浪费(仅组内接收),支持跨路由传播(需路由器开启组播路由)。
关键结构体与选项
// 用于加入/离开组播组的结构体(IPPROTO_IP级选项IP_ADD_MEMBERSHIP)
struct ip_mreqn {struct in_addr imr_multiaddr; // 组播组地址(如224.0.0.100)struct in_addr imr_address; // 本地网卡IP(指定从哪个网卡加入组播,设为INADDR_ANY则自动选择)int imr_ifindex; // 网卡索引(0表示任意网卡)
};
组播接收示例(加入组播组)
#include <stdio.h> // 标准输入输出(printf、perror)
#include <stdlib.h> // 标准库(EXIT_FAILURE/EXIT_SUCCESS)
#include <string.h> // 内存操作(memset)
#include <unistd.h> // 系统调用(close)
#include <sys/socket.h> // 套接字核心函数(socket、bind、recvfrom、setsockopt)
#include <netinet/in.h> // 网络地址结构(sockaddr_in、ip_mreqn)、字节序转换(htons/ntohs/htonl)
#include <arpa/inet.h> // IP地址转换(inet_aton、inet_ntop)、常量定义(INET_ADDRSTRLEN)
#include <errno.h> // 错误码处理(可选,增强错误排查)int main() {// 创建UDP套接字(IPv4协议族、数据报类型、默认UDP协议)int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);if (udp_fd == -1) {perror("socket create failed");return EXIT_FAILURE;}// 配置组播参数,加入目标组播组(组播地址:224.0.0.100,D类地址)struct ip_mreqn mreq;memset(&mreq, 0, sizeof(mreq)); // 初始化组播参数结构体,清除垃圾值// 设置组播组地址(必须是D类地址:224.0.0.0 ~ 239.255.255.255)if (inet_aton("224.0.0.100", &mreq.imr_multiaddr) == 0) {perror("invalid multicast address");close(udp_fd);return EXIT_FAILURE;}mreq.imr_address.s_addr = htonl(INADDR_ANY); // 绑定所有本地网卡,自动选择接收网卡mreq.imr_ifindex = 0; // 网卡索引:0表示任意网卡(由系统自动选择)// 启用组播组加入(IPPROTO_IP级别选项,必须在bind前调用)if (setsockopt(udp_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1) {perror("join multicast group failed");close(udp_fd); // 失败时关闭套接字,避免资源泄漏return EXIT_FAILURE;}// 绑定组播端口(8888,必须与发送端目标端口完全一致)struct sockaddr_in local_addr;memset(&local_addr, 0, sizeof(local_addr)); // 初始化本地地址结构local_addr.sin_family = AF_INET; // 协议族:IPv4(与socket()的domain一致)local_addr.sin_port = htons(8888); // 绑定端口8888(转换为网络字节序)local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地网卡,接收任意网卡的组播数据if (bind(udp_fd, (struct sockaddr*)&local_addr, sizeof(local_addr)) == -1) {perror("bind multicast port 8888 failed");// 退出前离开组播组,避免系统组播资源残留setsockopt(udp_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));close(udp_fd);return EXIT_FAILURE;}// 启动成功提示printf("UDP multicast receiver started.\n");printf("Multicast group: 224.0.0.100\n");printf("Bound port: 8888\n");printf("Waiting for multicast messages (press Ctrl+C to stop)...\n\n");// 接收数据相关变量初始化char recv_buf[1024] = {0}; // 接收缓冲区(1024字节,满足大部分场景)struct sockaddr_in src_addr; // 存储组播发送方的地址信息(IP+端口)socklen_t src_len = sizeof(src_addr); // 发送方地址结构长度(值-结果参数)// 循环接收组播数据(阻塞等待,直到有数据到达或出错)while (1) {// 接收组播数据:recvfrom默认阻塞,直到收到数据或发生错误ssize_t recv_len = recvfrom(udp_fd, // 套接字文件描述符recv_buf, // 接收数据缓冲区sizeof(recv_buf) - 1, // 缓冲区最大可用长度(留1字节存'\0',避免字符串溢出)0, // 接收标志:0=阻塞接收(默认)(struct sockaddr*)&src_addr, // 发送方地址指针(存储发送端IP和端口)&src_len // 发送方地址长度指针(传入初始长度,返回实际长度));// 检查接收结果if (recv_len == -1) {perror("recv multicast failed");memset(recv_buf, 0, sizeof(recv_buf)); // 清空缓冲区,避免垃圾数据干扰continue; // 忽略临时错误,继续等待下一条数据}// 给接收的数据添加字符串结束符(确保printf正常打印,避免乱码)recv_buf[recv_len] = '\0';// 将发送方的网络字节序IP转换为点分十进制字符串(线程安全,推荐使用)char src_ip[INET_ADDRSTRLEN] = {0}; // INET_ADDRSTRLEN=16,足够存储IPv4地址inet_ntop(AF_INET, &src_addr.sin_addr, src_ip, sizeof(src_ip));// 将发送方的网络字节序端口转换为主机字节序uint16_t src_port = ntohs(src_addr.sin_port);// 打印接收结果(包含发送方IP、端口、数据长度和内容)printf("Received multicast from %s:%d (bytes: %zd): %s\n", src_ip, src_port, recv_len, recv_buf);// 清空缓冲区,准备接收下一条数据memset(recv_buf, 0, sizeof(recv_buf));}// 退出清理(理论上死循环不会执行到这里,但保留规范流程)// 离开组播组(释放系统组播资源,避免残留)if (setsockopt(udp_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) == -1) {perror("leave multicast group failed");}close(udp_fd); // 关闭套接字,释放文件描述符资源return EXIT_SUCCESS;
}
组播发送示例(无需加入组播组)
#include <stdio.h> // 标准输入输出(printf、perror)
#include <stdlib.h> // 标准库(EXIT_FAILURE/EXIT_SUCCESS)
#include <string.h> // 内存操作(memset)
#include <unistd.h> // 系统调用(close、sleep)
#include <sys/socket.h> // 套接字核心函数(socket、sendto)
#include <netinet/in.h> // 网络地址结构(sockaddr_in)、字节序转换(htons)
#include <arpa/inet.h> // IP地址转换(inet_aton)int main() {// 创建UDP套接字(IPv4协议族、数据报类型、默认UDP协议)int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);if (udp_fd == -1) {perror("socket create failed");return EXIT_FAILURE;}// 配置组播目标地址(组播组:224.0.0.100,端口:8888,与接收端一致)struct sockaddr_in mcast_addr;memset(&mcast_addr, 0, sizeof(mcast_addr)); // 初始化地址结构,清除垃圾值mcast_addr.sin_family = AF_INET; // 协议族:IPv4(与socket()的domain一致)mcast_addr.sin_port = htons(8888); // 目标端口8888(转换为网络字节序)// 设置组播组地址(必须是D类地址:224.0.0.0 ~ 239.255.255.255)if (inet_aton("224.0.0.100", &mcast_addr.sin_addr) == 0) {perror("invalid multicast address"); // 检查组播地址合法性(如输入非D类地址)close(udp_fd);return EXIT_FAILURE;}// 待发送的组播数据(包含字符串结束符'\0',确保接收端完整解析)char mcast_data[] = "This is a UDP multicast message!";printf("UDP multicast sender started.\n");printf("Multicast group: 224.0.0.100:%d\n", 8888);printf("Send every 3 seconds, press Ctrl+C to stop.\n\n");// 循环发送组播数据(死循环,需手动中断)while (1) {// 发送组播数据:sendto默认阻塞发送,无需加入组播组即可发送ssize_t send_len = sendto(udp_fd, // 套接字文件描述符mcast_data, // 待发送数据缓冲区sizeof(mcast_data), // 数据长度(含'\0',共35字节)0, // 发送标志:0=阻塞发送(默认)(struct sockaddr*)&mcast_addr, // 组播目标地址指针(强制转换为通用地址结构)sizeof(mcast_addr) // 目标地址结构长度);// 检查发送结果if (send_len == -1) {perror("send multicast failed"); // 打印错误(如网络不可达、套接字关闭)close(udp_fd);return EXIT_FAILURE;}// 打印发送成功信息(send_len为实际发送字节数,正常等于sizeof(mcast_data))printf("Sent %zd bytes: %s\n", send_len, mcast_data);sleep(3); // 暂停3秒,控制发送频率}// 理论上死循环不会执行到这里,保留close()避免编译警告close(udp_fd);return EXIT_SUCCESS;
}
UDP 协议典型应用场景
音视频流传输(如直播、视频通话)
- 需求:低延迟(实时性优先),少量数据丢失不影响整体体验(如丢 1 帧不影响画面连贯);
- 优势:UDP 无连接、低开销,数据传输延迟远低于 TCP(TCP 的重传和流量控制会增加延迟);
- 实例:抖音直播、Zoom 视频会议、IPTV。
域名解析(DNS)
- 需求:查询请求简短(通常≤512 字节),快速响应,无需可靠传输(查询失败可重试);
- 优势:UDP 一次请求 - 响应即可完成解析,比 TCP 三次握手 + 数据传输更高效;
- 细节:DNS 查询默认用 UDP 53 端口,若响应数据超过 512 字节,会自动切换为 TCP。
域名解析(解析www.baidu.com的 IP)
思路
- 使用
gethostbyname()(已过时,仅作示例)或getaddrinfo()(推荐,支持 IPv6); gethostbyname()返回struct hostent,包含域名对应的所有 IP 地址(一个域名可能对应多个 IP,实现负载均衡)。
/*** 传统域名解析函数,通过主机名获取主机网络信息* @brief 解析域名(如www.example.com)或主机名,返回对应的IPv4地址等信息* @param name 待解析的主机名或域名字符串(如"localhost"、"www.baidu.com"),不支持IPv6地址格式* @return struct hostent* 成功返回指向hostent结构体的指针(包含IP地址列表等信息),失败返回NULL* @note 仅支持IPv4协议,不兼容IPv6,现代网络编程推荐使用getaddrinfo()替代* 错误信息不通过errno返回,需通过h_errno全局变量获取(可配合herror()或hstrerror()打印错误描述)* hostent结构体关键成员:* - h_name: 主机正式名称* - h_aliases: 主机别名列表(以NULL结尾)* - h_addrtype: 地址类型(固定为AF_INET,即IPv4)* - h_length: 地址长度(IPv4为4字节)* - h_addr_list: IPv4地址列表(以NULL结尾,每个元素为in_addr结构体指针,需强转使用)* 返回的结构体由系统分配,无需手动释放,且后续调用可能覆盖该内存*/
struct hostent *gethostbyname(const char *name);/*** 通用网络地址解析函数,支持IPv4/IPv6双栈,兼容域名与服务名解析* @brief 解析主机名/域名、服务名/端口号,返回可直接用于socket连接的地址信息链表* @param node 待解析的主机名、域名或IP地址字符串(如"www.example.com"、"192.168.1.1"、"::1")* 传NULL时表示使用本地主机(回环地址)* @param service 待解析的服务名或端口号字符串(如"http"、"80"、"ssh"、"22")* 传NULL时表示不指定端口,需手动在地址结构体中设置* @param hints 输入参数,指向addrinfo结构体,指定解析规则(如协议族、套接字类型)* 传NULL时使用默认规则(支持所有协议族、所有套接字类型)* 关键成员说明:* - ai_family: 协议族(AF_INET=IPv4,AF_INET6=IPv6,AF_UNSPEC=自动适配)* - ai_socktype: 套接字类型(SOCK_STREAM=TCP,SOCK_DGRAM=UDP,0=不限)* - ai_protocol: 协议类型(IPPROTO_TCP=TCP,IPPROTO_UDP=UDP,0=默认匹配ai_socktype)* - ai_flags: 解析标志(如AI_PASSIVE=用于服务器绑定,AI_CANONNAME=获取主机正式名称)* @param res 输出参数,指向addrinfo结构体链表的头指针,存储解析结果(需通过freeaddrinfo()释放)* @return int 成功返回0,失败返回非0错误码(可通过gai_strerror()函数获取错误描述字符串)* @note 支持IPv4和IPv6双栈,是现代网络编程的首选解析函数,完全替代gethostbyname()* 必须通过freeaddrinfo(res)释放解析结果链表,否则会造成内存泄漏(无论解析成功与否,只要res非NULL)* 解析结果链表需遍历使用,每个addrinfo节点包含:* - ai_family: 地址族(AF_INET/AF_INET6)* - ai_socktype: 套接字类型* - ai_protocol: 协议类型* - ai_addr: 指向sockaddr_in(IPv4)或sockaddr_in6(IPv6)的地址结构体指针* - ai_addrlen: 地址结构体长度* - ai_next: 下一个解析结果节点的指针(链表结束为NULL)* 支持服务名解析(如"http"对应80端口),依赖/etc/services配置文件* hints参数使用前需初始化(建议用memset清零后再设置指定字段),避免随机值导致解析异常*/
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
代码(使用 getaddrinfo (),推荐)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
int main() {const char *domain = "www.baidu.com"; // 待解析域名struct addrinfo hints, *res, *p;int ret;char ip_str[INET6_ADDRSTRLEN] = {0}; // 支持IPv6的IP字符串缓冲区// 初始化hints结构体(指定解析参数)memset(&hints, 0, sizeof(hints));hints.ai_family = AF_UNSPEC; // 不指定IP版本(IPv4/IPv6都解析)hints.ai_socktype = SOCK_DGRAM; // UDP套接字类型(与UDP相关)hints.ai_protocol = IPPROTO_UDP; // UDP协议// 调用getaddrinfo()解析域名ret = getaddrinfo(domain, NULL, &hints, &res);if (ret != 0) {fprintf(stderr, "getaddrinfo failed: %s\n", gai_strerror(ret));return -1;}// 遍历res链表,输出所有IP地址printf("Domain: %s, IP addresses:\n", domain);for (p = res; p != NULL; p = p->ai_next) {void *addr;if (p->ai_family == AF_INET) { // IPv4struct sockaddr_in *ipv4 = (struct sockaddr_in*)p->ai_addr;addr = &(ipv4->sin_addr);} else { // IPv6struct sockaddr_in6 *ipv6 = (struct sockaddr_in6*)p->ai_addr;addr = &(ipv6->sin6_addr);}// 将网络字节序IP转换为字符串inet_ntop(p->ai_family, addr, ip_str, sizeof(ip_str));printf(" %s\n", ip_str);}// 释放res链表(避免内存泄漏)freeaddrinfo(res);return 0;
}
即时通信(如 QQ 消息、微信语音)
- 需求:消息实时送达,可容忍偶尔丢失(如文字消息丢失可重发,语音丢包不影响理解);
- 优势:UDP 轻量化,适合频繁发送短消息,减少服务器资源占用。
物联网(IoT)设备通信
- 需求:设备资源有限(如传感器、智能手环,CPU / 内存小),数据量小(如温度、湿度数据);
- 优势:UDP 协议栈实现简单,设备无需维护连接状态,降低功耗和资源消耗。
UDP 编程实战(含思路)
判断主机字节序(大端 / 小端)
思路
- 利用联合体(所有成员共享内存):定义一个联合体,包含 1 个 int(4 字节)和 1 个 char(1 字节);
- 给 int 赋值 0x12345678,若 char 的值为 0x78,则是小端(低字节存低地址);若为 0x12,则是大端。
代码
#include <stdio.h>union EndianTest {int i;char c;
};int main() {union EndianTest test;test.i = 0x12345678;if (test.c == 0x78) {printf("Host is Little-Endian\n");} else if (test.c == 0x12) {printf("Host is Big-Endian\n");} else {printf("Unknown Endian\n");}return 0;
}
UDP 客户端 - 服务器一对一通信(多线程收发)
需求
- 服务器:绑定端口,同时接收客户端消息(主线程)和向客户端发送消息(子线程);
- 客户端:指定服务器 IP 和端口,同时发送消息(主线程)和接收服务器消息(子线程)。
服务器代码(核心部分)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int udp_fd;
struct sockaddr_in client_addr;
socklen_t client_len;// 子线程:向客户端发送消息
void *send_thread(void *arg) {char send_buf[1024] = {0};while (1) {fgets(send_buf, sizeof(send_buf)-1, stdin);// 移除fgets读取的换行符send_buf[strcspn(send_buf, "\n")] = '\0';sendto(udp_fd, send_buf, strlen(send_buf)+1, 0, (struct sockaddr*)&client_addr, client_len);memset(send_buf, 0, sizeof(send_buf));}return NULL;
}int main() {// 创建UDP套接字udp_fd = socket(AF_INET, SOCK_DGRAM, 0);if (udp_fd == -1) { perror("socket failed"); return -1; }// 绑定端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8888);server_addr.sin_addr.s_addr = htonl(INADDR_ANY);if (bind(udp_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed"); close(udp_fd); return -1;}printf("Server started, wait for client...\n");// 先接收一次客户端消息,获取客户端地址char recv_buf[1024] = {0};client_len = sizeof(client_addr);ssize_t recv_len = recvfrom(udp_fd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&client_addr, &client_len);if (recv_len == -1) { perror("recvfrom failed"); close(udp_fd); return -1; }char client_ip[INET_ADDRSTRLEN] = {0};inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));printf("Client connected: %s:%d, message: %s\n", client_ip, ntohs(client_addr.sin_port), recv_buf);// 创建子线程(发送消息)pthread_t tid;if (pthread_create(&tid, NULL, send_thread, NULL) != 0) {perror("pthread_create failed"); close(udp_fd); return -1;}// 主线程:接收客户端消息memset(recv_buf, 0, sizeof(recv_buf));while (1) {recv_len = recvfrom(udp_fd, recv_buf, sizeof(recv_buf)-1, 0, (struct sockaddr*)&client_addr, &client_len);if (recv_len == -1) { perror("recvfrom failed"); continue; }printf("Client: %s\n", recv_buf);memset(recv_buf, 0, sizeof(recv_buf));}pthread_join(tid, NULL);close(udp_fd);return 0;
}
客户端代码(核心部分)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int udp_fd;
struct sockaddr_in server_addr;
socklen_t server_len;// 子线程:接收服务器消息
void *recv_thread(void *arg) {char recv_buf[1024] = {0};while (1) {ssize_t recv_len = recvfrom(udp_fd, recv_buf, sizeof(recv_buf)-1, 0, NULL, NULL); // 不关心服务器地址,可设为NULLif (recv_len == -1) { perror("recvfrom failed"); continue; }printf("Server: %s\n", recv_buf);memset(recv_buf, 0, sizeof(recv_buf));}return NULL;
}int main() {// 创建UDP套接字udp_fd = socket(AF_INET, SOCK_DGRAM, 0);if (udp_fd == -1) { perror("socket failed"); return -1; }// 配置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8888);if (inet_aton("192.168.1.100", &server_addr.sin_addr) == 0) { // 服务器IPprintf("Invalid server IP\n"); close(udp_fd); return -1;}server_len = sizeof(server_addr);// 创建子线程(接收消息)pthread_t tid;if (pthread_create(&tid, NULL, recv_thread, NULL) != 0) {perror("pthread_create failed"); close(udp_fd); return -1;}// 主线程:向服务器发送消息char send_buf[1024] = {0};printf("Client started, enter message to send (exit to quit):\n");while (1) {fgets(send_buf, sizeof(send_buf)-1, stdin);send_buf[strcspn(send_buf, "\n")] = '\0';// 退出逻辑if (strcmp(send_buf, "exit") == 0) {printf("Client exiting...\n");break;}sendto(udp_fd, send_buf, strlen(send_buf)+1, 0, (struct sockaddr*)&server_addr, server_len);memset(send_buf, 0, sizeof(send_buf));}pthread_cancel(tid);pthread_join(tid, NULL);close(udp_fd);return 0;
}
UDP 编程常见问题与解决方案
| 常见问题 | 原因分析 | 解决方案 |
|---|---|---|
| bind () 失败,错误码 EADDRINUSE | 端口已被其他进程占用(如 8080 端口被浏览器占用) | 更换未被占用的端口(如 8888、9999); 启用 SO_REUSEADDR 选项(允许端口快速重用) |
| sendto () 成功但接收方收不到 | 目标 IP / 端口错误; 接收方未绑定端口; 网络防火墙拦截; 广播 / 组播未启用对应选项 |
检查目标 IP 和端口是否正确; 确保接收方已绑定端口; 关闭防火墙或开放端口; 广播启用 SO_BROADCAST,组播接收方加入组播组 |
| 接收数据被截断 | 接收缓冲区大小小于发送数据的长度 | 增大接收缓冲区(如设为 2048 字节); 发送方控制数据长度≤接收缓冲区大小 |
| 跨平台通信端口错误 | 端口未用 htons () 转换(小端主机的端口发送到大端主机后,数值被解析错误) | 所有端口必须用 htons () 转换为网络字节序, 接收时用 ntohs () 转换为主机字节序 |
| 组播接收不到数据 | 接收方未加入组播组; 发送方组播地址错误; 端口不匹配 |
接收方调用 setsockopt (IP_ADD_MEMBERSHIP) 加入组播组; 检查组播地址是否为 D 类地址; 确保发送方和接收方端口一致 |
UDP 与 TCP 的核心区别
| 对比维度 | UDP(用户数据报协议) | TCP(传输控制协议) |
|---|---|---|
| 连接方式 | 无连接(通信前无需建立连接) | 面向连接(三次握手建立连接,四次挥手关闭连接) |
| 可靠性 | 不可靠(无重传、无确认、无流量控制) | 可靠(重传、确认、流量控制、拥塞控制) |
| 数据传输单位 | 数据报(固定长度,一次发送一个完整数据报) | 字节流(无边界,按字节传输) |
| 传输效率 | 高(报首小、无连接开销、无重传延迟) | 低(报首大、连接开销大、重传和流量控制增加延迟) |
| 适用场景 | 实时性优先(音视频、DNS、即时通信) | 可靠性优先(文件传输、HTTP/HTTPS、数据库通信) |
| 端口占用 | 仅需一个端口(发送和接收用同一个端口) | 需两个端口(客户端随机端口,服务器知名端口) |