
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[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 报文的逐层封装,完整流程如下:
- 内核创建
sk_buff结构体,作为承载整份网络报文的统一缓冲区。 - 将应用层传入的业务数据,拷贝到
sk_buff的数据存储区域。 - 把
sk_buff内部的data指针向前偏移sizeof(struct udphdr)个字节,预留出 8 字节的 UDP 报头空间。 - 在预留空间内填充
udphdr结构体的全部字段,依次写入源端口、目的端口、报文总长度以及校验和。 - 封装完成的 UDP 报文向上交付给网络层,IP 层继续追加 IP 首部,再向下交给数据链路层完成最终发送。
整个封装过程本质就是 C 语言指针移动配合结构体字段填充,没有 TCP 那样复杂的状态机和序号管理,这也是 UDP 协议开销小、转发效率高的根本原因。
接收端:解包过程
网络层剥掉 IP 首部后,把 UDP 数据报向上递交到传输层,UDP 协议执行逆向解包:
- 报头与数据分离 :UDP 报头固定占 8 字节,直接截取报文最前面的 8 字节解析为
udphdr结构体,剩余部分即为上层应用数据。 - 校验和:重新计算整份报文的校验和,与报头中存储的校验和做比对;校验失败直接丢弃该报文,不会向发送方返回任何差错通知。
- 报文分用交付 :提取报文中的目的端口号,查询内核哈希表匹配对应的套接字,将有效数据存入该套接字的接收缓冲区,等待用户态调用
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 协议,深入学习传输层完整知识体系做好铺垫。