《Linux 网络编程》深入传输层 UDP 协议原理:端口、报文、缓冲区与内核源码解析

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、传输层基础:端口号深度解析

[1.1 端口号承担的核心职责](#1.1 端口号承担的核心职责)

[1.2 五元组:网络通信的唯一识别标识](#1.2 五元组:网络通信的唯一识别标识)

[1.3 端口号范围划分](#1.3 端口号范围划分)

[1.4 端口号和进程之间的关联](#1.4 端口号和进程之间的关联)

[1.5 两个问题:端口号与进程绑定规则](#1.5 两个问题:端口号与进程绑定规则)

[二、UDP 协议核心原理](#二、UDP 协议核心原理)

[2.1 协议本质](#2.1 协议本质)

[2.2 UDP 协议格式(定长 8 字节报头)](#2.2 UDP 协议格式(定长 8 字节报头))

[2.3 UDP 报头结构体 (C 语言内核定义)](#2.3 UDP 报头结构体 (C 语言内核定义))

[2.4 UDP 的封装与解包过程](#2.4 UDP 的封装与解包过程)

[2.5 UDP 的三大核心特点](#2.5 UDP 的三大核心特点)

[2.5.1 无连接](#2.5.1 无连接)

[2.5.2 不可靠](#2.5.2 不可靠)

[2.5.3 面向数据报](#2.5.3 面向数据报)

[2.6 UDP 的缓冲区机制](#2.6 UDP 的缓冲区机制)

[2.7 UDP 使用的关键注意事项](#2.7 UDP 使用的关键注意事项)

[2.8 基于 UDP 的应用层协议](#2.8 基于 UDP 的应用层协议)

三、内核源码解读

结束语


前言

在 Linux 网络编程的学习路径中,UDP 作为传输层两大核心协议之一,经常被大家简单贴上 "无连接、不可靠" 的标签就一笔带过。但很多人对端口五元组的本质、UDP 报文封装解包逻辑、内核缓冲区行为,以及底层内核实现细节理解依旧模糊,遇到丢包、接收异常等线上问题时无从下手。

本文会从传输层端口基础概念出发,先讲清端口号划分、五元组通信标识这些底层基石,再逐层拆解 UDP 协议报文结构、核心特性、缓冲区工作机制与开发注意事项。同时结合上层应用场景,下沉到 Linux 内核源码角度,分析 UDP 报头处理、套接字缓冲区、端口绑定的内核逻辑,把应用层开发行为和内核内部实现串联起来,帮助我们既会写 UDP 业务代码,也能理解底层发生了什么,为后续网络开发、问题排查与面试打下扎实基础。

一、传输层基础:端口号深度解析

1.1 端口号承担的核心职责

IP 地址的作用,仅仅是定位到网络当中的某一台主机。但一台主机内部,会同时运行大量应用进程,网卡收到数据包之后,还需要确定该把数据交给本机哪一个进程处理。

端口号(Port)就是用于区分主机内不同通信进程16 位无符号整数,可以类比成主机内部的房间号。网卡接收报文,经过 IP 层处理完毕之后,操作系统依靠端口号,把网络报文精准交付给对应的应用套接字进程。

1.2 五元组:网络通信的唯一识别标识

在 TCP/IP 协议体系中,一条完整通信链路,依靠 5 个要素做唯一区分,也就是我们常说的五元组:

bash 复制代码
源IP地址 + 源端口号 + 目的IP地址 + 目的端口号 + 协议号

举个实例:客户端 A 172.20.100.34:2001 访问服务器 172.20.100.32:80,TCP 协议号为 6,这条连接完整五元组如下:

bash 复制代码
172.20.100.34:2001  ->  172.20.100.32:80  (TCP,协议号6)

即使同一台机器,打开多个浏览器标签页访问同一个网站,目标 IP 与目标端口完全一致。操作系统会为每一个会话分配不同的源端口,借助五元组,内核就可以把多条会话数据流完全隔离开,区分每一条独立通信流。

协议号用来区分传输层协议,TCP 协议号为 6,UDP 协议号为 17。就算源 IP、源端口、目的 IP、目的端口全部相同,只要协议号不一样,也代表两条完全独立的通信。

1.3 端口号范围划分

端口号是 16 位无符号整数,取值区间为0 ~ 65535,IANA 对全部端口做了分类规划,Linux 系统也沿用这套划分规则,整体分为知名端口与动态端口两大区间。

  • 知名端口号 (0-1023) :由 IANA 统一分配,与标准应用层协议一一绑定,普通用户进程无权直接绑定。常见的知名端口号包括:
    • SSH:22 端口
    • FTP:21 端口
    • HTTP:80 端口
    • HTTPS:443 端口
    • DNS:53 端口

0 号端口属于特殊知名端口 ,客户端 bind 绑定端口填 0 时,代表交给内核自动随机分配可用端口 ,一般不用于服务端监听

  • 动态端口号 (1024-65535):该区间又被叫做临时端口、私有端口。客户端发起网络连接时,操作系统会从此区间随机挑选一个端口作为源端口使用。 开发自定义服务端程序时,业务代码优先选用该区间端口,不需要 root 权限,是业务开发的首选范围。

Linux 可以通过**/proc/sys/net/ipv4/ip_local_port_range** 配置文件,修改系统自动分配客户端临时端口的起止区间

Linux 系统保存了知名端口与协议名称的映射表,可以执行下面命令查看完整映射:

bash 复制代码
cat /etc/services

1.4 端口号和进程之间的关联

1.5 两个问题:端口号与进程绑定规则

问题一:一个进程是否可以 bind 多个端口号?

**答案:可以。**一个进程内部可以创建多个套接字(socket),每个套接字分别绑定不同的端口号,从而同时监听多个端口的请求。

  • 每个 socket 是独立的文件描述符,内核为每个套接字维护独立的接收缓冲区和发送缓冲区,互不干扰。
  • 典型应用场景:Nginx 可以同时监听 80 端口(HTTP)和 443 端口(HTTPS),就是同一个进程内创建多个 socket 分别绑定不同端口。
  • 一个进程能创建的 socket 数量受限于进程最大文件描述符数(ulimit -n),理论上可以绑定大量端口。

问题二:一个端口号是否可以被多个进程 bind?

答案:默认情况下不可以(不考虑 SO_REUSEPORT 等特殊选项)。

端口号是操作系统区分本机不同通信进程的核心标识。如果多个进程绑定同一个端口,网卡收到数据包后,内核无法判断应该把数据交付给哪一个进程,会产生歧义。

  • 内核维护一张端口绑定表,记录哪个端口被哪个 socket 占用,重复 bind 会返回EADDRINUSE错误。
  • SO_REUSEADDR 选项 :允许端口处于**TIME_WAIT 状态(后续讲解TCP原理时会详细介绍)**时被快速复用,但同一时刻依旧只能有一个进程监听该端口,主要解决服务器重启时端口被占用的问题。
  • SO_REUSEPORT 选项:Linux 3.9 之后引入,允许多个进程或线程同时绑定同一个端口,内核会把到来的连接在多个 socket 之间做负载均衡分发。Nginx、Redis 等高并发服务常用该选项提升多核利用率。注意这是特殊场景,默认行为依旧是端口独占。
  • 父子进程场景:父进程先 bind 监听端口,再 fork 子进程,子进程会继承已绑定的 socket 文件描述符,此时父子进程共享同一个监听 socket,这不属于 "多个进程分别 bind 同一端口",而是共享同一个已绑定的 socket。

二、UDP 协议核心原理

2.1 协议本质

UDP操作系统之间约定的报文结构体,通信双方按统一格式解析数据,无需建立连接即可传输。

2.2 UDP 协议格式(定长 8 字节报头)

UDP 协议的设计极其简洁,报文格式只有固定的 8 字节报头,后面紧跟应用层数据,没有 TCP 那样复杂的序号、确认号、窗口大小等字段。

bash 复制代码
0      15 16     31
┌────────┬────────┐
│源端口号 │目的端口号│ 16位
├────────┼────────┤
│UDP长度  │校验和   │ 16位
└────────┴────────┘
        数据部分
字段 长度(位) 描述
源端口号 16 发送端的端口号
目的端口号 16 接收端的端口号
UDP 长度 16 UDP 数据报总长度(首部 + 数据)
UDP 检验和 16 覆盖 UDP 首部和数据的检验和
应用层数据 可变 实际传输的应用层数据

2.3 UDP 报头结构体 (C 语言内核定义)

在 Linux 内核中,UDP 报头 被定义为一个 C 语言结构体 ,这体现了 "协议本质是双方约定的结构体类型" 的核心思想:

cpp 复制代码
// Linux内核中UDP报头的定义(简化版)
struct udphdr {
    __be16 source;   // 源端口号(16位)
    __be16 dest;     // 目的端口号(16位)
    __be16 len;      // UDP报文总长度(报头+数据, 16位)
    __sum16 check;   // 校验和(16位)
};
  • 源端口号:发送端应用进程的端口号,属于可选字段,若不使用则填 0。源端口填 0 的场景常见于某些不需要回复的单向广播报文。
  • 目的端口号:接收端应用进程的端口号,必填字段。内核依靠目的端口把报文交付给对应套接字。
  • UDP 长度:整个 UDP 报文的总长度,包括 8 字节报头,最大值为 65535 字节(64KB)。这是因为长度字段只有 16 位,理论上限就是 2^16-1。
  • 校验和:用于检测报文在传输过程中是否出错。若校验和错误,接收端会直接丢弃该报文,且不通知发送端 ------ 这也体现了 UDP 不可靠的特性,出错了就直接丢,不做重传。

注意:UDP 校验和是可选的,IPv4 中允许校验和字段填 0 表示不计算校验和;但在 IPv6 中校验和是强制必填的。实际工程中一般都会开启校验和,避免传输差错导致数据错乱。

2.4 UDP 的封装与解包过程

发送端:封装过程

应用层调用sendto()发起数据发送后,内核借助sk_buff套接字缓冲区完成 UDP 报文的逐层封装,完整流程如下:

  1. 内核创建sk_buff结构体,作为承载整份网络报文的统一缓冲区。
  2. 将应用层传入的业务数据,拷贝到sk_buff的数据存储区域。
  3. sk_buff内部的data指针向前偏移sizeof(struct udphdr)个字节,预留出 8 字节的 UDP 报头空间。
  4. 在预留空间内填充udphdr结构体的全部字段,依次写入源端口、目的端口、报文总长度以及校验和。
  5. 封装完成的 UDP 报文向上交付给网络层,IP 层继续追加 IP 首部,再向下交给数据链路层完成最终发送。

整个封装过程本质就是 C 语言指针移动配合结构体字段填充,没有 TCP 那样复杂的状态机和序号管理,这也是 UDP 协议开销小、转发效率高的根本原因。

接收端:解包过程

网络层剥掉 IP 首部后,把 UDP 数据报向上递交到传输层,UDP 协议执行逆向解包:

  1. 报头与数据分离 :UDP 报头固定占 8 字节,直接截取报文最前面的 8 字节解析为udphdr结构体,剩余部分即为上层应用数据。
  2. 校验和:重新计算整份报文的校验和,与报头中存储的校验和做比对;校验失败直接丢弃该报文,不会向发送方返回任何差错通知。
  3. 报文分用交付 :提取报文中的目的端口号,查询内核哈希表匹配对应的套接字,将有效数据存入该套接字的接收缓冲区,等待用户态调用recvfrom()读取。

解包过程同样简洁高效,固定报头长度使得解析逻辑非常直接,不需要像 TCP 那样处理序号重组、乱序重排等复杂逻辑。

2.5 UDP 的三大核心特点

UDP 通信可以类比邮寄信件:写好内容、填写收件地址,直接投入邮筒,无需提前和接收方建立沟通,同时也无法确保信件一定能够送达目的地。

2.5.1 无连接

  • **无连接特性:**UDP 不需要执行 TCP 的三次握手来完成连接建立。只要知晓对端 IP 与端口信息,便可直接向外发送报文。省去连接协商的开销,带来更低的通信延迟,多用于对实时性要求严苛的业务场景。

2.5.2 不可靠

  • **传输不可靠:**UDP 没有设计应答确认、失败重传、拥塞控制相关逻辑。当报文在网络中出现丢包、损坏、乱序等情况时,传输层不会向应用程序反馈错误信息,发送方完全无法感知报文是否投递失败。

注意:"不可靠" 不能简单理解为 UDP 的缺点,这是它的设计特性。正是放弃了可靠性保障,UDP 实现了极简的协议逻辑与高性能,报头仅有 8 字节;而 TCP 协议头部最小就占用 20 字节。

2.5.3 面向数据报

  • 面向数据报: UDP 严格保留应用层递交的数据边界,上层递交多大的报文,UDP 就原样发送,不会对数据做拆分,也不会自动合并多条消息。由此产生两条重要行为:
    • 发送方调用一次sendto送出 100 字节,接收方必须通过一次recvfrom读取全部 100 字节,不可以分成 10 次、每次读取 10 字节。
    • UDP 不会出现 TCP 的粘包问题,每一个 UDP 数据报都具备独立边界,报文之间相互隔离。

2.6 UDP 的缓冲区机制

UDP 的缓冲区实现逻辑和 TCP 存在明显差异,分为发送缓冲区、接收缓冲区两部分,同时套接字支持全双工通信。

  • 不存在真正意义的发送缓冲区: 调用sendto发送数据之后,应用数据会直接交付内核,内核立刻交给网络层完成后续处理。UDP 不需要缓存报文来做超时重传,因此发送缓冲区没有存在的必要性,内核不会留存待发送的数据副本。
  • 具备接收缓冲区: 内核会为每一个 UDP 套接字维护接收缓冲区,用来存放网络上抵达的 UDP 报文。该缓冲区底层依靠sk_buff链表来管理报文,同时存在两条关键限制:
    • 不保障报文顺序,接收缓冲区里报文顺序不一定和发送方发送顺序保持一致。
    • 缓冲区存满之后,后续新抵达的 UDP 报文会直接被内核丢弃,不会做任何通知。
  • **全双工特性:**UDP 套接字天然支持全双工,同一个 socket 文件描述符可以同时执行读、写操作,在同一个套接字上既可以发送数据,也能够接收远端报文。

小结:UDP 发送侧无缓存,数据递交内核后应用层就不再保留副本;接收侧有缓存队列,但无序、队列溢出直接丢包;单套接字可读可写,实现全双工通信。

2.7 UDP 使用的关键注意事项

UDP 头部的 16 位长度字段,限定了单条 UDP 报文最大上限为 64KB,该数值已经包含 8 字节 UDP 报头。在互联网实际业务场景下,64KB 的报文上限并不算大。

当业务需要传输超过 64KB 的数据时,内核不会自动完成分片重组,需要我们在应用层手动处理分包与拼装逻辑:

  • 发送端:把大数据切分成多份小于 64KB 的 UDP 报文,同时为每一个报文携带序号标记,方便接收端识别顺序。
  • 接收端:依靠报文中携带的序号信息,将多段报文重新拼接,还原出完整原始数据。

2.8 基于 UDP 的应用层协议

UDP 本身不具备可靠传输能力,但凭借实现简单、时延低、支持广播的特性,大量上层应用协议选择基于 UDP 开发。

  • DNS(域名解析协议):域名解析大多是一问一答的短报文交互,UDP 低延迟的优势可以充分发挥。
  • DHCP(动态主机配置协议):实现 IP 地址自动分配,依赖广播通信,UDP 原生支持广播,而 TCP 无法做到。
  • TFTP(简单文件传输协议):面向小文件传输,协议逻辑轻量,广泛用于嵌入式设备。
  • NFS(网络文件系统):用于局域网文件共享,看重 UDP 带来的传输速率优势。
  • BOOTP(启动协议):服务于无盘设备,完成网络启动。

三、内核源码解读

在 Linux 内核当中,全部网络报文都依靠sk_buff结构体完成管理。UDP 报文封装的核心逻辑,本质就是对sk_buff内部的data指针做偏移操作。

cpp 复制代码
// 简化后的 sk_buff 核心成员
struct sk_buff {
    unsigned char *head;   // 数据区起始地址
    unsigned char *data;   // 当前数据指针
    unsigned char *tail;   // 数据区结束地址
    unsigned char *end;    // 缓冲区结束地址
    // ...其余内核字段
};

// UDP报文封装核心逻辑
struct udphdr *uh;
// 向前偏移,预留UDP头部存储空间
skb->data -= sizeof(struct udphdr);
// 强转得到UDP头部结构体指针
uh = (struct udphdr *)skb->data;
// 填充源端口、目的端口,注意转换为网络字节序
uh->source = htons(source_port);
uh->dest = htons(dest_port);
uh->len = htons(skb->len);
uh->check = 0; // 校验和先置零,后续再完成计算
// 校验和计算逻辑此处省略

实现思路:通过移动data指针预留头部位置,之后直接以结构体指针的方式对内存赋值,完成 UDP 报头填充,整套流程以内存操作为主。

结束语

本篇我们从传输层端口的基础概念入手,梳理了端口作用、五元组、端口号分配规则,完整学习了 UDP 协议的报文结构、封装解包流程、核心特性与缓冲区机制,同时梳理了 UDP 开发中容易踩坑的实践要点。最后下沉到 Linux 内核层面,简单解读 UDP 报文处理、套接字缓冲区以及端口绑定的内核逻辑。

很多初学者只记住 UDP 无连接、不可靠这几个简单结论,但实际开发中遇到丢包、缓冲区溢出、端口绑定冲突等问题时,依旧很难定位根因。只有把应用层接口行为和内核内部实现结合起来理解,才能真正看懂 UDP 的运行逻辑。UDP 虽然不提供可靠传输能力,但凭借开销小、时延低的优势,在很多业务场景不可替代。理解 UDP 的底层原理,也能为后续对比学习 TCP 协议,深入学习传输层完整知识体系做好铺垫。

相关推荐
好评1242 小时前
【Linux】数据链路层
linux·运维·网络
天衍四九-2 小时前
第一章:从 LLM 到 Agent —— DeepSeek Harness 入门
网络·数据库·人工智能·python
Jae den2 小时前
网络丢包是什么?
网络
CC城子3 小时前
EtherNet/IP I/O 丢包排查(linux系统)
linux·单片机·tcp/ip·ethernetip
新时代牛马3 小时前
Linux 信号处理完整篇:从sigaction、掩码到内核发送与可重入排障
linux·运维·信号处理
程序员梅雨3 小时前
Linux & Shell 实用干货
linux·运维·服务器·后端·面试·php
吴声子夜歌3 小时前
编写Shell脚本——自顶向下设计
linux·运维·shell
M78佐菲3 小时前
HTML学习笔记
linux·笔记·学习·tcp/ip·html
XR1234567883 小时前
医院无线整网高密覆盖选型深度解析
运维·网络