Linux C/C++ 学习日记(24):UDP协议的介绍:广播、多播的实现
注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
一、UDP 协议是什么
UDP 的全称是用户数据报协议(User Datagram Protocol),是 OSI 模型中传输层的重要协议,核心特性可概括为三点:
- 无连接:通信前不需要像 TCP 那样建立 “三次握手”,直接发送数据。
- 不可靠:不保证数据能送达,也不保证送达顺序,丢包后不会自动重传。
- 轻量:头部仅 8 字节(远少于 TCP 的 20 字节),协议本身几乎不占用额外资源。
二、为什么会有 UDP
UDP 的存在是为了弥补 TCP 的 “短板”,因为 TCP 的 “可靠性” 是有代价的,无法满足所有场景需求:
- TCP 需要建立连接、确认数据、重传丢包,会产生连接开销和传输延迟。
- 部分场景对 “实时性” 的需求远高于 “可靠性”,TCP 的延迟会直接影响体验。
- 网络中需要一种 “极简” 的传输方式,用于传递小数据量、对丢失不敏感的信息。
三、它解决了什么问题
UDP 的核心价值是 “放弃部分可靠性,换取速度和效率”,主要解决三类问题:
- 降低传输延迟:无连接、无重传机制,数据能以最快速度从发送方到接收方,适合实时场景。
- 减少资源开销:极简的头部设计和无连接管理,对发送端、接收端的 CPU 和内存占用极低。
- 支持特殊通信:原生支持广播(一对多) 和多播(一组多),而 TCP 仅支持点对点通信。
四、它的应用场景是什么
所有选择 UDP 的场景,核心诉求都是 “实时性优先” 或 “轻量优先”,典型场景包括:
- 实时音视频:如视频通话、直播、语音聊天。偶尔丢包只会导致画面轻微卡顿,不会影响整体流畅度,若用 TCP 会因重传产生明显延迟。
- 在线游戏:如 MOBA 类游戏、射击游戏。游戏数据(如位置、操作)需要毫秒级传输,延迟比丢 1-2 个数据包更影响体验。
- DNS 查询:域名解析(如将 “baidu.com” 转成 IP)仅需传递几十字节的小数据,用 UDP 能瞬间完成,没必要建立 TCP 连接。
- 广播 / 多播:如局域网内的设备发现(如打印机搜索)、IPTV 组播,TCP 无法实现这类 “一对多” 通信。
五、UDP协议头(8个字节)
1. 各参数的含义
字段名称 | 长度(位) | 含义说明 |
---|---|---|
16 位源端口号 | 16 | 标识发送方应用程序使用的端口。无需回复时可设为 0;需回复时,接收方通过该端口确定回复目标。 |
16 位目的端口号 | 16 | 标识接收方应用程序的目标端口,是区分不同应用层协议 / 程序的核心依据(如 DNS 用 53 端口,TFTP 用 69 端口),系统据此将数据交付给正确应用。 |
16 位 UDP 长度 | 16 | 表示 UDP 头部 + 数据的总长度(字节)。最小值为 8(仅头部),最大值 65535 字节(受 16 位限制),实际传输受 IP 层 MTU 约束。 |
16 位 UDP 检验和 | 16 | 用于检测传输中的比特错误,计算时包含 UDP 头部、数据及含 IP 源 / 目的地址、协议类型的伪头部。校验不一致则丢弃数据报; 设为 0 表示不校验。 |
2. 16位UDP检验和的使用
环节 / 步骤 | 具体说明 |
---|---|
步骤 1:构造完整数据块 | 需拼接三部分组成临时数据块(仅用于计算): - 伪首部(12 字节):源 IP 地址(32 位)、目的 IP 地址(32 位)、保留字段(8 位,0x00)、协议类型(8 位,UDP 为 0x11)、UDP 总长度(16 位,头部 + 数据总字节数); - UDP 头部(8 字节):源端口、目的端口、UDP 长度、检验和(计算时临时设为 0x0000); - UDP 数据:应用层载荷(可为空)。 |
步骤 2:数据块对齐 | 校验和以 16 位(2 字节)为单位计算,需保证总字节数为偶数:若总长度为奇数,在数据末尾补充 1 个字节的 0x00(仅用于计算,不传输)。 |
步骤 3:拆分 16 位字序列 | 将对齐后的数据块按 “低地址到高地址” 拆分为连续 16 位 “字”: 例:5 字节数据块 |
步骤 4:反码求和(核心) | 1. 逐字相加:所有 16 位字按二进制逐位相加,保留进位; 2. 处理进位:若结果为 17 位,将最高位进位(1)循环加到最低位; 3. 累加所有字,最终得到 16 位 “总和”。 |
步骤 5:计算检验和 | 对步骤 4 的 16 位总和取反码(0 变 1、1 变 0),结果即为 16 位 UDP 检验和: 例:总和 0x0269( |
步骤 6:发送与验证 | - 发送方:将检验和填入 UDP 头部 “检验和字段” 并发送; - 接收方:重构数据块(含接收的检验和), 重复计算(步骤1-5,检验和取发送方传来的:0xFD96): - 结果为 0xFFFF(16 位全 1)→ 校验通过; - 结果非全 1 → 数据错误,丢弃数据报(上层处理错误)。 |
关键特性与注意事项 | 1. 反码求和优势:“进位循环” 特性更易检测单比特 / 多比特连续错误; 2. 协议差异:IPv4 中检验和可选(0x0000 表示不校验),IPv6 强制计算; 3. 伪首部作用:关联 IP 地址和协议类型,确保端到端数据一致性(避免 IP 错误但端口正确的误传)。 |
六、UDP报文传输模式
注意:在UDP报文传输模式中
-
sendto
函数一次调用发送的数据,会被封装为一个独立的 UDP 数据报(报文) 发送。UDP 是面向数据报的协议,不会对数据进行拆分或合并,每次sendto
调用对应一个完整的 UDP 报文。 -
recvfrom
函数一次调用会接收 一个完整的 UDP 报文(去掉封装)(即对应一次sendto
发送的数据)。如果接收缓冲区大小足够,会完整读取该报文;如果缓冲区不足,可能会截断数据(需注意检查返回值确认实际接收长度)。
需要注意的是,这一特性仅适用于 UDP。如果使用 TCP 协议(基于字节流),send
/sendto
和 recv
/recvfrom
没有 “报文” 的概念,数据会被视为连续的字节流,多次发送的数据可能被合并,一次接收也可能获取到多次发送的部分数据(取决于底层缓冲区)。
因此,“一次 sendto
对应一个报文,一次 recvfrom
接收一个报文” 是 UDP 协议的典型行为。
区分帧、IP数据包、UDP 报文、sendto 传入的数据、recvfrom 接收的数据
概念 | 所属协议层 | 组成部分 | 核心特征 | 与其他概念的关系 |
---|---|---|---|---|
sendto 传入的数据 | 应用层 | 仅应用程序生成的原始数据(如字符串、二进制流、结构体等) | 无任何协议封装,是用户真正想传输的 “有效信息” | 是 UDP 报文、IP 数据报、帧的最内层 “有效载荷”,被后续各层协议依次封装 |
recvfrom 接收的数据 | 应用层 | 与发送方 sendto 传入的原始数据完全一致(无传输错误时) | 是经过底层协议栈解封装后,最终交付给应用程序的原始数据 | 是帧、IP 数据报、UDP 报文经过多层解封装后,剥离所有协议头部 / 尾部后的结果 |
UDP 报文 | 传输层(UDP 协议) | UDP 头部(源端口、目的端口、报文长度、校验和) + sendto 传入的应用层数据 | 传输层的封装单位,标记数据的 “发送 / 接收端口”,确保数据交付到正确的应用程序 | 被网络层封装为 IP 数据报的有效载荷;其内部包含 sendto 传入的应用层数据 |
IP 数据报(IP 数据包) | 网络层(IP 协议) | IP 头部(源 IP、目的 IP、TTL、协议类型等) + UDP 报文(或 TCP 段) | 网络层的传输单位,负责跨网络路由,包含 IP 地址等路由标识信息 | 封装 UDP 报文(或 TCP 段),自身被帧封装为有效载荷; 其内部的 UDP 报文(或 TCP 段)包含 sendto(或send) 传入的应用层数据 |
帧 | 数据链路层 | 链路层头部(如 MAC 地址、帧类型) + 链路层尾部(如 CRC 校验码) + IP 数据报 | 链路层的传输单位,负责在物理链路(如网线、无线)上传输,包含物理地址信息 | 在UDP中,其有效载荷是 IP 数据报(包含 IP 头部 + UDP 报文);比 UDP 报文多了 IP 头部、链路层头部和尾部 |
在口语表述UDP传输中,我们有的人说发送一帧或者接收一帧,指的是sendto和recvfrom的数据。(当然实际表达不严谨)
七、广播和多播的概念与实现
对比维度 | UDP 广播(一对所有) | UDP 多播(一对一组) |
---|---|---|
核心定义 | 向同一局域网内所有设备发送数据,无需提前建立连接 | 向预先加入 “多播组” 的设备发送数据,仅组内设备可接收 |
核心要素 | 1. 广播地址( 全局:255.255.255.255; 子网:如 192.168.1.255) 2. 套接字需开启 | 1. D 类多播地址(范围:224.0.0.0~239.255.255.255,如 239.0.0.1) 2. 依赖 IGMP 协议管理组成员(加入 / 退出) |
关键实现步骤 | 1. 创建 UDP 套接字 2. 开启广播权限( 3. 目标 IP 设为广播地址,指定端口发送 4. 接收方绑定对应端口监听 | 1. 创建 UDP 套接字 2. 接收方通过 3. 发送方目标 IP 设为多播组地址,指定端口发送 4. 路由器通过 IGMP 转发给组内设备 |
跨子网能力 | 不支持,路由器默认丢弃广播包,仅局限于本地局域网 | 支持,只要路由器支持 IGMP 协议,可转发多播包到其他子网的多播组设备 |
网络带宽占用 | 高,所有局域网设备都会接收数据包,可能造成带宽浪费 | 低,仅多播组内设备接收,无关设备不占用带宽 |
关键注意事项 | 1. 仅适用于小数据量传输,避免广播风暴 2. 套接字默认禁止广播,需手动开启权限 | 1. 多播组动态管理,设备可随时加入 / 退出 2. 发送方无需加入多播组,仅接收方需加入 |
典型应用场景 | 1. 局域网设备发现(如打印机搜索)2. 本地游戏房间创建通知3. 局域网内简单数据同步 | 1. IPTV / 网络直播(仅订阅用户接收) 2. 实时数据推送(如股票行情、气象数据) |
代码实现:
1.广播
发送端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define BROADCAST_PORT 8888
// 子网广播地址(需根据实际局域网子网修改,例如192.168.1.255)
#define BROADCAST_ADDR "192.168.1.255"int main() {int sockfd;struct sockaddr_in broadcast_addr;char msg[] = "This is a UDP broadcast message!";// 1. 创建UDP套接字(AF_INET:IPv4,SOCK_DGRAM:UDP)if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 开启广播权限(UDP默认禁止广播,需设置SO_BROADCAST选项)int broadcast_enable = 1;if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast_enable, sizeof(broadcast_enable)) < 0) {perror("setsockopt SO_BROADCAST failed");close(sockfd);exit(EXIT_FAILURE);}// 3. 配置广播目标地址(IP+端口)memset(&broadcast_addr, 0, sizeof(broadcast_addr));broadcast_addr.sin_family = AF_INET; // IPv4broadcast_addr.sin_port = htons(BROADCAST_PORT); // 端口转换为网络字节序// 将广播地址字符串转换为网络字节序IPif (inet_pton(AF_INET, BROADCAST_ADDR, &broadcast_addr.sin_addr) <= 0) {perror("invalid broadcast address");close(sockfd);exit(EXIT_FAILURE);}// 4. 循环发送广播消息while (1) {// 发送数据到广播地址ssize_t len = sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&broadcast_addr, sizeof(broadcast_addr));if (len < 0) {perror("sendto failed");close(sockfd);exit(EXIT_FAILURE);}printf("Broadcast sent: %s\n", msg);sleep(2); // 每2秒发送一次}close(sockfd);return 0;
}
接收端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define BROADCAST_PORT 8888
#define BUFFER_SIZE 1024int main() {int sockfd;struct sockaddr_in local_addr;struct sockaddr_in sender_addr;socklen_t sender_len = sizeof(sender_addr);char buffer[BUFFER_SIZE];// 1. 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 配置本地地址(绑定到广播端口,接收所有IP的广播)memset(&local_addr, 0, sizeof(local_addr));local_addr.sin_family = AF_INET;local_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地IPlocal_addr.sin_port = htons(BROADCAST_PORT); // 绑定广播端口// 3. 绑定套接字到本地地址(必须绑定端口才能接收对应端口的广播)if (bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}printf("Waiting for UDP broadcast on port %d...\n", BROADCAST_PORT);// 4. 循环接收广播消息while (1) {// 接收数据(同时获取发送方地址)ssize_t len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,(struct sockaddr*)&sender_addr, &sender_len);if (len < 0) {perror("recvfrom failed");close(sockfd);exit(EXIT_FAILURE);}buffer[len] = '\0'; // 手动添加字符串结束符// 打印接收的消息和发送方IPprintf("Received from %s:%d: %s\n",inet_ntoa(sender_addr.sin_addr), // 网络字节序IP转字符串ntohs(sender_addr.sin_port), // 网络字节序端口转主机序buffer);}close(sockfd);return 0;
}
2. 多播
发送端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define MULTICAST_PORT 9999
// 多播组地址(D类地址,例如239.0.0.1)
#define MULTICAST_GROUP "239.0.0.1"int main() {int sockfd;struct sockaddr_in multicast_addr;char msg[] = "This is a UDP multicast message!";// 1. 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 配置多播目标地址(IP+端口)memset(&multicast_addr, 0, sizeof(multicast_addr));multicast_addr.sin_family = AF_INET;multicast_addr.sin_port = htons(MULTICAST_PORT);// 多播组地址转换为网络字节序if (inet_pton(AF_INET, MULTICAST_GROUP, &multicast_addr.sin_addr) <= 0) {perror("invalid multicast group address");close(sockfd);exit(EXIT_FAILURE);}// 3. 循环发送多播消息(发送方无需加入多播组)while (1) {ssize_t len = sendto(sockfd, msg, strlen(msg), 0,(struct sockaddr*)&multicast_addr, sizeof(multicast_addr));if (len < 0) {perror("sendto failed");close(sockfd);exit(EXIT_FAILURE);}printf("Multicast sent: %s\n", msg);sleep(2); // 每2秒发送一次}close(sockfd);return 0;
}
接收端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define MULTICAST_PORT 9999
#define MULTICAST_GROUP "239.0.0.1"
// 本地网卡IP(需根据实际环境修改,例如192.168.1.100)
#define LOCAL_IP "192.168.1.100"
#define BUFFER_SIZE 1024int main() {int sockfd;struct sockaddr_in local_addr;struct ip_mreq mreq; // 多播组加入请求结构体struct sockaddr_in sender_addr;socklen_t sender_len = sizeof(sender_addr);char buffer[BUFFER_SIZE];// 1. 创建UDP套接字if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 配置本地地址(绑定到多播端口)memset(&local_addr, 0, sizeof(local_addr));local_addr.sin_family = AF_INET;local_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地IPlocal_addr.sin_port = htons(MULTICAST_PORT);// 3. 绑定套接字到本地端口if (bind(sockfd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}// 4. 配置多播组加入信息// 设置要加入的多播组地址if (inet_pton(AF_INET, MULTICAST_GROUP, &mreq.imr_multiaddr.s_addr) <= 0) {perror("invalid multicast group");close(sockfd);exit(EXIT_FAILURE);}// 设置本地接口IP(多网卡时需指定,单网卡可用INADDR_ANY)if (inet_pton(AF_INET, LOCAL_IP, &mreq.imr_interface.s_addr) <= 0) {perror("invalid local IP");close(sockfd);exit(EXIT_FAILURE);}// 5. 加入多播组(通过IP_ADD_MEMBERSHIP选项)if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {perror("setsockopt IP_ADD_MEMBERSHIP failed");close(sockfd);exit(EXIT_FAILURE);}printf("Joined multicast group %s, waiting for messages on port %d...\n",MULTICAST_GROUP, MULTICAST_PORT);// 6. 循环接收多播消息while (1) {ssize_t len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,(struct sockaddr*)&sender_addr, &sender_len);if (len < 0) {perror("recvfrom failed");close(sockfd);exit(EXIT_FAILURE);}buffer[len] = '\0';printf("Received from %s:%d: %s\n",inet_ntoa(sender_addr.sin_addr),ntohs(sender_addr.sin_port),buffer);}// 退出时可离开多播组(实际中程序退出会自动离开)// setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));close(sockfd);return 0;
}