Linux 系统编程 15:UDP 编程

前言:

承接上一篇 Reactor 事件驱动架构,至此我们已经完整掌握了 TCP 协议的编程模型、并发架构与高性能优化。传输层两大核心协议中,TCP 负责可靠有序传输,UDP 则主打低延迟、高效率,在音视频直播、实时游戏、DNS 解析、物联网等场景下有着不可替代的地位。本篇完整讲解 UDP Socket 的编程模型、核心 API 与实战实现,拓展广播、组播两种一对多通信方式,并分析 UDP 可靠性增强的工程实现思路,补全传输层编程的最后一块核心拼图。


一、UDP 协议核心特性

1. UDP 的本质

UDP(用户数据报协议)是传输层的无连接协议,它不提供可靠传输保证,只负责将数据报尽可能发送出去。和 TCP 面向字节流的设计完全不同,UDP 是面向报文的协议,每个数据报都是独立的,有明确的消息边界。

2. 五大核心特性

  1. 无连接:不需要三次握手建立连接,创建 Socket 后可以直接向任意目标发送数据,启动速度快
  2. 不可靠传输:不保证数据一定送达,不保证顺序,不做重传,出错直接丢弃
  3. 面向报文:一次发送对应一个数据报,接收方一次完整接收一个报文,不存在粘包问题
  4. 无拥塞控制:不会根据网络状况调整发送速率,始终以恒定速率发送,适合实时场景
  5. 头部开销极小:UDP 头部只有 8 字节,远小于 TCP 的 20 字节以上头部,传输效率高

3. 为什么需要「不可靠」的 UDP

可靠性和实时性是一对矛盾体:TCP 为了保证可靠,引入了握手、重传、拥塞控制等机制,带来了延迟和开销;而 UDP 舍弃了可靠性,换来了极低的延迟和极高的传输效率。

在直播、语音通话、在线游戏等场景中,宁可丢失少量数据包,也不能容忍高延迟,UDP 就是这类场景的最优选择。


二、UDP Socket 编程流程

UDP 没有连接的概念,编程流程比 TCP 简单得多,服务端和客户端的差异非常小。

1. 服务端编程流程

  1. 创建套接字:调用socket()指定SOCK_DGRAM类型
  2. 绑定地址:调用bind()绑定固定的 IP 和端口
  3. 循环收发:调用recvfrom()接收数据,调用sendto()发送数据
  4. 关闭套接字:通信结束调用close()

2. 客户端编程流程

  1. 创建套接字:调用socket()创建 UDP 套接字
  2. 直接收发:无需绑定,直接调用sendto()向服务端发送数据,调用recvfrom()接收回复
  3. 关闭套接字

核心差异:UDP 没有listen、accept、connect这三步连接相关操作,没有监听套接字和通信套接字的区分,一个 Socket 可以和多个对端通信。


三、核心 API 函数详解

1. 创建 UDP 套接字

复制代码
#include <sys/socket.h>

int socket(AF_INET, SOCK_DGRAM, 0);
  • 第二个参数固定为SOCK_DGRAM,表示数据报套接字,对应 UDP 协议
  • 返回值为 Socket 文件描述符,失败返回 - 1

2. 绑定地址

复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 用法和 TCP 完全一致,服务端必须绑定固定端口才能接收数据
  • 客户端通常不需要绑定,系统会自动分配临时端口

3. 接收数据:recvfrom

复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • buf:接收数据的缓冲区
  • flags:接收标志,填 0 为常规阻塞接收
  • src_addr:传出参数,保存发送方的 IP 和端口信息
  • addrlen:值 - 结果参数,传入时为地址结构体大小,传出时为实际地址长度
  • 返回值:成功返回实际接收的字节数,失败返回 - 1,对端关闭返回 0(UDP 一般不会收到 0)

易错点:addrlen必须先初始化为地址结构体的大小,否则会导致地址信息读取错误。

4. 发送数据:sendto

复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • dest_addr:目标地址结构体,指定要发送给谁
  • addrlen:目标地址结构体的长度
  • 返回值:成功返回实际发送的字节数,失败返回 - 1

注意:UDP 的 sendto 成功返回只代表数据成功拷贝到内核发送缓冲区,不代表数据已经成功发出,更不代表对方已经收到。

5. UDP 的 connect 操作

UDP 虽然是无连接协议,但也可以调用connect函数,作用是绑定一个固定的对端地址:

  • 绑定后可以直接用read/write/send/recv收发数据,不需要每次都传地址参数
  • 内核会自动过滤掉其他地址发来的数据包,只接收绑定地址的数据
  • 这种模式称为「已连接 UDP」,比普通 UDP 效率略高,适合一对一固定通信的场景

四、实战:UDP 回显服务

实现 UDP 版的回显服务:客户端发送字符串,服务端收到后原样返回。

1. UDP 服务端代码

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUF_SIZE 1024

int main(void) {
    // 创建UDP套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd == -1) {
        perror("socket failed");
        return 1;
    }

    // 绑定地址端口
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("bind failed");
        return 1;
    }
    printf("UDP服务端启动,监听端口%d\n", PORT);

    char buf[BUF_SIZE];
    struct sockaddr_in cli_addr;
    socklen_t cli_len;

    while (1) {
        cli_len = sizeof(cli_addr); // 每次接收前必须重置长度
        memset(buf, 0, sizeof(buf));

        // 接收数据,同时获取客户端地址
        ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
                             (struct sockaddr *)&cli_addr, &cli_len);
        if (n == -1) {
            perror("recvfrom failed");
            break;
        }

        char cli_ip[16];
        inet_ntop(AF_INET, &cli_addr.sin_addr, cli_ip, sizeof(cli_ip));
        printf("收到 %s:%d 数据:%.*s\n", cli_ip, ntohs(cli_addr.sin_port), (int)n, buf);

        // 原样回写给客户端
        sendto(fd, buf, n, 0, (struct sockaddr *)&cli_addr, cli_len);
    }

    close(fd);
    return 0;
}

2. UDP 客户端代码

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUF_SIZE 1024

int main(void) {
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd == -1) {
        perror("socket failed");
        return 1;
    }

    // 构造服务端地址
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

    char buf[BUF_SIZE];
    socklen_t serv_len = sizeof(serv_addr);

    while (1) {
        printf("请输入要发送的内容:");
        fgets(buf, sizeof(buf), stdin);
        buf[strcspn(buf, "\n")] = 0;

        if (strcmp(buf, "quit") == 0) break;

        // 发送数据到服务端
        sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&serv_addr, serv_len);

        // 接收回显
        memset(buf, 0, sizeof(buf));
        ssize_t n = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        if (n > 0) {
            printf("收到回显:%.*s\n", (int)n, buf);
        }
    }

    close(fd);
    return 0;
}

五、UDP 广播通信

1. 什么是广播

广播是一对多的通信方式,发送方发送一个广播包,同一个局域网内的所有主机都能收到这个数据包。广播只能在局域网内使用,路由器不会转发广播数据包。

  • 受限广播地址 :255.255.255.255,向所有本地网络广播,不被路由器转发
  • 直接广播地址 :网段的最后一个地址,如192.168.1.255,向指定网段广播

2. 核心配置选项

发送广播前必须开启 Socket 的SO_BROADCAST选项,否则发送会失败:

复制代码
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));

3. 广播适用场景

  • 局域网设备发现:如打印机发现、设备在线探测
  • 局域网游戏、局域网文件传输
  • 服务发现协议:如 DHCP 就是基于 UDP 广播实现的

4. 广播的局限性

  • 只能在局域网内使用,无法跨网段
  • 所有主机都会收到广播包,即使不需要处理,浪费带宽和主机资源
  • 安全性差,容易被滥用造成广播风暴

六、UDP 组播(多播)通信

1. 什么是组播

组播也叫多播,是介于单播和广播之间的一对多通信方式:主机可以加入一个指定的组播组,只有加入该组的主机才能收到对应组播数据。组播可以跨网段路由,是广域网一对多传输的标准方案。

2. 组播地址

组播使用 D 类 IP 地址,范围是224.0.0.0 ~ 239.255.255.255,分为三类:

  • 局部链路组播:224.0.0.0 ~ 224.0.0.255,只在本地网段有效,路由器不转发
  • 全局组播:224.0.1.0 ~ 238.255.255.255,可在全网范围内使用
  • 管理权限组播:239.0.0.0 ~ 239.255.255.255,私有组播地址,类似内网 IP

3. 核心操作:加入 / 离开组播组

接收方必须加入指定组播组,才能收到对应组播地址的数据,通过setsockopt实现:

复制代码
#include <netinet/in.h>

struct ip_mreq mreq;
// 组播地址
inet_pton(AF_INET, "239.0.0.1", &mreq.imr_multiaddr);
// 本机加入组播的网卡地址,INADDR_ANY表示任意网卡
mreq.imr_interface.s_addr = htonl(INADDR_ANY);

// 加入组播组
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

// 离开组播组
setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));

4. 组播的优势

  • 比广播更高效,只有需要的主机会收到数据,带宽利用率高
  • 支持跨网段、跨广域网传输
  • 可同时支持大量接收者,适合直播、视频会议等大规模一对多场景

七、UDP 可靠性增强方案

UDP 本身不可靠,但在很多场景中,我们既需要 UDP 的低延迟,又需要一定的传输可靠性,这时就需要在应用层自己实现可靠传输机制。

1. 四大核心可靠机制

机制 作用 实现原理
序号机制 保证数据顺序、去重 每个数据包分配唯一序号,接收方按序号排序,重复包丢弃
确认机制 感知数据是否送达 接收方收到数据包后,回复 ACK 确认包
超时重传 丢失的数据包重发 发送方启动定时器,超时未收到 ACK 则重传对应数据包
滑动窗口 流量控制 + 提升吞吐 维护发送窗口,连续发送多个包再等待确认,提升传输效率

在此基础上还可以扩展拥塞控制、快速重传、选择确认等进阶机制,逐步逼近 TCP 的可靠性,同时保留 UDP 的低延迟特性。

2. 工业级可靠 UDP 实现

实际开发中不需要重复造轮子,主流的可靠 UDP 实现有:

  • KCP:轻量级可靠 UDP 协议,专注于低延迟,广泛用于游戏、物联网
  • QUIC:Google 推出的基于 UDP 的可靠传输协议,已成为 HTTP/3 的底层标准
  • RTP/RTCP:音视频传输标准协议,RTP 传数据,RTCP 做质量监控
  • ENet:游戏开发常用的可靠 UDP 网络库

3. 选型原则

如果业务对可靠性要求高、延迟不敏感,直接用 TCP;如果对延迟要求极高、可以容忍少量丢包,用原生 UDP;如果需要低延迟 + 基本可靠,选择成熟的可靠 UDP 方案。


八、TCP 与 UDP 选型指南

对比维度 TCP UDP
连接特性 面向连接 无连接
可靠性 可靠,保证不丢、不乱、不重复 不可靠,不保证送达和顺序
传输形式 字节流,无消息边界 数据报,有明确消息边界
头部开销 20~60 字节 8 字节
延迟 高,握手 + 重传 + 拥塞控制 极低,无额外开销
流量 / 拥塞控制 有 无
编程复杂度 高,连接管理、粘包处理 低,简单直接
适用场景 文件传输、网页、支付等可靠优先场景 直播、语音、游戏、DNS 等实时优先场景

九、面试高频考点与易错坑点

1. 经典面试问答

Q1:UDP 有没有粘包问题?为什么?

答: UDP 没有粘包问题。因为 UDP 是面向数据报的协议,每个数据报是独立的,有明确的消息边界。内核一次交付一个完整的数据报,发送方一次 sendto 对应接收方一次 recvfrom,不会出现多个包合并或者拆分的情况。

Q2:UDP 调用 connect 有什么作用?和 TCP 的 connect 有什么区别?

答: UDP 的 connect 不是建立连接,只是绑定一个固定的对端地址。绑定后可以用 read/write 收发数据,不需要每次传地址,同时内核会过滤其他地址的数据包。 和 TCP 的区别:TCP 的 connect 会发起三次握手建立真实连接;UDP 的 connect 只是在内核记录对端地址,没有任何握手过程。

Q3:广播和组播有什么核心区别?

答: 广播是向网段内所有主机发送,所有主机无论是否需要都会收到,只能在局域网使用,资源浪费大。 组播是向加入组的主机发送,只有需要的主机会收到,支持跨网段传输,带宽利用率高,适合大规模一对多场景。

Q4:怎么实现可靠的 UDP 传输?

答: 在应用层实现可靠机制:给数据包加序号实现排序去重,接收方回复 ACK 确认,发送方超时重传丢失的包,通过滑动窗口提升吞吐量。 工业界常用 KCP、QUIC 等成熟方案,不需要自行从零实现。

Q5:UDP 的 sendto 会阻塞吗?

答: 默认阻塞模式下会阻塞。当内核发送缓冲区满了的时候,sendto 会阻塞等待缓冲区有空位,不是完全不会阻塞。设置非阻塞模式后,缓冲区满会直接返回 - 1,errno 设为 EAGAIN。

2. 常见易错坑点

  1. 忘记初始化recvfrom的 addrlen 参数,导致获取不到正确的对端地址
  2. 误以为 UDP 不会丢包,业务逻辑没有做容错处理
  3. 发送广播包前忘记开启SO_BROADCAST选项,导致发送失败
  4. 发送的数据包超过 MTU 大小,导致 IP 分片,只要一个分片丢失整个包就作废,性能大幅下降
  5. 接收缓冲区设置太小,数据包超过缓冲区大小被内核直接截断丢弃
  6. 混淆 TCP 和 UDP 的编程模型,在 UDP 中调用 listen/accept 导致错误
  7. 组播接收端没有加入对应组播组,导致收不到数据

以上就是 UDP Socket 编程的全部核心内容,至此传输层两大协议的编程实践已经完整覆盖。下一篇我们将讲解 Unix 域套接字,这是本机最高效的进程间通信方式,基于 Socket 接口实现,性能远超管道、消息队列等传统 IPC,是本地服务通信的首选方案。


制作不易,如果对你有用,希望能点赞收藏支持一下。

相关推荐
且从容.3 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落4 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨4 小时前
WSL2的安装与使用
linux·wsl
微小的鱼4 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng5 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮6 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky17 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默17 小时前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu
skywalk816318 小时前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd
爱莉希雅&&&20 小时前
GitLab 完整学习笔记
linux·运维·服务器