Linux C/C++ 学习日记(24):UDP协议的介绍:广播、多播的实现

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

一、UDP 协议是什么

UDP 的全称是用户数据报协议(User Datagram Protocol),是 OSI 模型中传输层的重要协议,核心特性可概括为三点:

  1. 无连接:通信前不需要像 TCP 那样建立 "三次握手",直接发送数据。
  2. 不可靠:不保证数据能送达,也不保证送达顺序,丢包后不会自动重传。
  3. 轻量:头部仅 8 字节(远少于 TCP 的 20 字节),协议本身几乎不占用额外资源。

二、为什么会有 UDP

UDP 的存在是为了弥补 TCP 的 "短板",因为 TCP 的 "可靠性" 是有代价的,无法满足所有场景需求:

  1. TCP 需要建立连接、确认数据、重传丢包,会产生连接开销传输延迟
  2. 部分场景对 "实时性" 的需求远高于 "可靠性",TCP 的延迟会直接影响体验。
  3. 网络中需要一种 "极简" 的传输方式,用于传递小数据量、对丢失不敏感的信息。

三、它解决了什么问题

UDP 的核心价值是 "放弃部分可靠性,换取速度和效率",主要解决三类问题:

  1. 降低传输延迟:无连接、无重传机制,数据能以最快速度从发送方到接收方,适合实时场景。
  2. 减少资源开销:极简的头部设计和无连接管理,对发送端、接收端的 CPU 和内存占用极低。
  3. 支持特殊通信 :原生支持广播(一对多)多播(一组多),而 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 字节数据块0x11 0x22 0x33 0x44 0x55补 0x00 后为 6 字节,拆分为0x11220x33440x5500
步骤 4:反码求和(核心) 1. 逐字相加:所有 16 位字按二进制逐位相加,保留进位; 2. 处理进位:若结果为 17 位,将最高位进位(1)循环加到最低位; 3. 累加所有字,最终得到 16 位 "总和"。
步骤 5:计算检验和 对步骤 4 的 16 位总和取反码(0 变 1、1 变 0),结果即为 16 位 UDP 检验和: 例:总和 0x0269(0000 0010 0110 1001)的反码为 0xFD96(1111 1101 1001 0110)。
步骤 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/sendtorecv/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. 套接字需开启SO_BROADCAST选项 1. D 类多播地址(范围:224.0.0.0~239.255.255.255,如 239.0.0.1) 2. 依赖 IGMP 协议管理组成员(加入 / 退出)
关键实现步骤 1. 创建 UDP 套接字 2. 开启广播权限(setsockoptSO_BROADCAST=1) 3. 目标 IP 设为广播地址,指定端口发送 4. 接收方绑定对应端口监听 1. 创建 UDP 套接字 2. 接收方通过setsockopt加入多播组(指定组地址 + 本地网卡 IP) 3. 发送方目标 IP 设为多播组地址,指定端口发送 4. 路由器通过 IGMP 转发给组内设备
跨子网能力 不支持,路由器默认丢弃广播包,仅局限于本地局域网 支持,只要路由器支持 IGMP 协议,可转发多播包到其他子网的多播组设备
网络带宽占用 高,所有局域网设备都会接收数据包,可能造成带宽浪费 低,仅多播组内设备接收,无关设备不占用带宽
关键注意事项 1. 仅适用于小数据量传输,避免广播风暴 2. 套接字默认禁止广播,需手动开启权限 1. 多播组动态管理,设备可随时加入 / 退出 2. 发送方无需加入多播组,仅接收方需加入
典型应用场景 1. 局域网设备发现(如打印机搜索)2. 本地游戏房间创建通知3. 局域网内简单数据同步 1. IPTV / 网络直播(仅订阅用户接收) 2. 实时数据推送(如股票行情、气象数据)

代码实现:

1.广播

发送端

cpp 复制代码
#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;                  // IPv4
    broadcast_addr.sin_port = htons(BROADCAST_PORT);      // 端口转换为网络字节序
    // 将广播地址字符串转换为网络字节序IP
    if (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;
}

接收端

cpp 复制代码
#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 1024

int 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;  // 监听所有本地IP
    local_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';  // 手动添加字符串结束符

        // 打印接收的消息和发送方IP
        printf("Received from %s:%d: %s\n",
               inet_ntoa(sender_addr.sin_addr),  // 网络字节序IP转字符串
               ntohs(sender_addr.sin_port),      // 网络字节序端口转主机序
               buffer);
    }

    close(sockfd);
    return 0;
}

2. 多播

发送端

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

接收端

cpp 复制代码
#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 1024

int 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;  // 监听所有本地IP
    local_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;
}
相关推荐
卓码软件测评5 小时前
第三方软件质量检测:RTSP协议和HLS协议哪个更好用来做视频站?
网络·网络协议·http·音视频·web
爱吃甜品的糯米团子5 小时前
Linux 学习笔记之 VI 编辑器与文件查找技巧
linux·笔记·学习
琦琦琦baby5 小时前
RIP路由协议总结
网络·rip
琦琦琦baby5 小时前
VRRP技术重点总结
运维·网络·智能路由器·vrrp
im_AMBER5 小时前
数据结构 03 栈和队列
数据结构·学习·算法
筑梦之路6 小时前
深入linux的审计服务auditd —— 筑梦之路
linux·运维·服务器
hi_link6 小时前
centos系统将/home分区的空间分配给/
linux·运维·centos
我先去打把游戏先6 小时前
VSCode通过SSH连接到Ubuntu虚拟机失败“找不到ssh安装”问题解决
笔记·vscode·单片机·嵌入式硬件·学习·ubuntu·ssh
CSND7406 小时前
linux离线环境局域网远程ssh连接vscode
linux·vscode·ssh