解码UDP

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 系统)

头文件

c 复制代码
#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 套接字

c 复制代码
/**
 * @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 ():绑定本地地址与端口

c 复制代码
/**
 * @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 数据报

c 复制代码
/**
 * @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");  // 目标IP

ssize_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 数据报

c 复制代码
/**
 * @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 ():控制套接字选项(广播 / 组播)

c 复制代码
/**
 * @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, &current_broadcast, &opt_len);
if (ret == -1) {
    perror("getsockopt SO_BROADCAST failed");
    return -1;
}
printf("Broadcast enabled: %s\n", current_broadcast ? "Yes" : "No");

IP 地址转换函数

① 点分十进制字符串 → 网络字节序整型(32 位)

c 复制代码
/**
 * @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的网络字节序)

② 网络字节序整型 → 点分十进制字符串

c 复制代码
/**
 * @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)
c 复制代码
#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: 0xB822

uint32_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选项(默认禁用);
    • 接收方必须绑定与发送方相同的端口(否则主机收到数据后无法交付进程,直接丢弃);
    • 广播仅能在局域网内传播(路由器默认不转发广播包,避免网络风暴)。

广播发送示例

c 复制代码
#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;
}

广播接收示例

c 复制代码
#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),仅用于标识组播组,不对应具体主机;
  • 组播组:加入同一组播地址的主机集合,发送方向组播地址发送数据,仅组内主机能接收;
  • 优势:相比广播,减少网络带宽浪费(仅组内接收),支持跨路由传播(需路由器开启组播路由)。

关键结构体与选项

c 复制代码
// 用于加入/离开组播组的结构体(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表示任意网卡)
};

组播接收示例(加入组播组)

c 复制代码
#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;
}

组播发送示例(无需加入组播组)

c 复制代码
#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,实现负载均衡)。
c 复制代码
/**
 * 传统域名解析函数,通过主机名获取主机网络信息
 * @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 (),推荐)

c 复制代码
#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) {  // IPv4
            struct sockaddr_in *ipv4 = (struct sockaddr_in*)p->ai_addr;
            addr = &(ipv4->sin_addr);
        } else {  // IPv6
            struct 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,则是大端。

代码

c 复制代码
#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 和端口,同时发送消息(主线程)和接收服务器消息(子线程)。

服务器代码(核心部分)

c 复制代码
#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;
}

客户端代码(核心部分)

c 复制代码
#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);  // 不关心服务器地址,可设为NULL
        if (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) {  // 服务器IP
        printf("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、数据库通信)
端口占用 仅需一个端口(发送和接收用同一个端口) 需两个端口(客户端随机端口,服务器知名端口)
相关推荐
A小辣椒21 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言