目录
[1. 传输层](#1. 传输层)
[1.1 再谈端口号](#1.1 再谈端口号)
[1.1.1 端口号范围划分](#1.1.1 端口号范围划分)
[1.1.2 认识知名端口号(Well-Know Port Number)](#1.1.2 认识知名端口号(Well-Know Port Number))
[1.1.3 两个问题](#1.1.3 两个问题)
[1.2 UDP协议](#1.2 UDP协议)
[1.2.1 UDP协议端格式](#1.2.1 UDP协议端格式)
[1.2.2 UDP的特点](#1.2.2 UDP的特点)
[1.2.3 面向数据报](#1.2.3 面向数据报)
[1.2.4 UDP的缓冲区](#1.2.4 UDP的缓冲区)
[1.2.5 UDP使用注意事项](#1.2.5 UDP使用注意事项)
[1.2.6 基于UDP的应用层协议](#1.2.6 基于UDP的应用层协议)
1. 传输层
负责数据能够从发送端传输接收端。
UDP协议主要负责将数据从发送端转发到接收端,而传输层不考虑所对应的面向连接的,只要套接字创建好,UDP客户端直接向服务器UDP发送消息,而且UDP是不需要考虑粘包问题和字节流问题的,UDP再内核当中,报文的长度操作系统是知道的。
1.1 再谈端口号
端口号(Port)标识一个主机上进行通信的不同应用程序。

在TCP/IP协议中,用"源IP","源端口号","目的IP","目的端口号","协议号"这样一个五元组来标识一个通信(可以通过netstat-n查看);


1.1.1 端口号范围划分
- 0-1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的.
- 1024-65535:操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的.
1.1.2 认识知名端口号(Well-Know Port Number)
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
- ssh 服务器, 使用 22 端口
- ftp 服务器, 使用 21 端口
- telnet 服务器, 使用 23 端口
- http 服务器, 使用 80 端口
- https 服务器, 使用 443
执行下面的命令, 可以看到知名端口号
bash
cat /etc/services
我们自己写一个程序使用端口号时,要避开这些知名端口号。
1.1.3 两个问题
- 一个进程是否可以bind多个端口号?可以
- 一个端口号是否可以被多个进程bind?不可以
1.2 UDP协议
1.2.1 UDP协议端格式

- 16位UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度;
- 如果校验和出错,就会直接丢弃;
协议本质上是一种约定,协议本质就是通信双方约定好的格式化字段,协议就是struct或者class。
TCP/UDP是属于传输层的,传输层往下包括传输层以及属于操作系统了,操作系统当中我们对应的协议和我们之前谈的概念是不是契合的呢?
首先,操作系统是用C语言写的,我们所说的协议,再操作系统内核当中,本质上都是结构体,这个也非常符合我们之前说的协议其实就是结构化字段,在应用层写C++的时候都是class,在C语言都是struct,而Linux内核包括windows内核都是C语言写的,所以它的协议就是纯C语言的结构体。
操作系统和应用层有点不一样,操作系统有重组的效率考量,在自己的代码中双方直接传输我们的二进制结构体对象,可以认为它没有明显的做序列和反序列化,也可以认为它直接以二进制方式做序列和反序列化,在应用层不建议收发二进制结构体,但在操作系统中就是这么干的,因为这样做效率高,我们对应的协议在内核当中就是struct,将来我们所谓的添加报头,封装报头,就是定义结构体对象,把应用层交给上层对应的数据,在上层交给我的报头前带上一个报头,(struct对象,然后把结构体对象和我们的数据拷贝合并起来)。
也就是说你应用层不管是http还是网络版本计算器,从上面拷贝下来到传输层,传输层添加报头就是在前面做二进制拷贝,把我们每种协议的报头信息往里面拷贝,这就叫做封装。
- UDP是如何做到解包的?
直接读取UDP报文的前八个字节。- UDP是如何做到分用的?
收到一个报文,从报头当中发现是16位端口号,然后UDP服务器应用层曾经我们绑定过端口号,所以操作系统就会拿着16位目的端口号在系统当中去查找我们的进程,看哪一个进程是和16位端口号关联的,进而把报文转给特定进程。
我们曾经写代码时端口号是两字节。2的16次方,为什么?
因为UDP协议即操作系统,它的报头协议当中规定的端口号是16位的。所以我们在应用层,我们使用端口号的时候,端口号就是16位。这就是为什么我们曾经在应用层,我们的IP地址四字节,而端口号是是两字节,因为端口号在这其实就是协议源代码,在协议当中它就是两个字节的。
UDP协议是面向数据报的,怎么保证报文的完整性,怎么做到把报头取完,剩下的就是有效载荷了。如果接收多个报文,黏在一块,接收方怎么保证把一个报文全部读上来呢?
其实就是固定报头+16位UDP长度,我们解析UDP的时候,UDP字节可以读取前8个字节,然后直接读取16位长度,16位长度表面有效载荷的长度,所以操作系统就可以根据报头去掉,从剩余内容当中连续16位的UDP,长度-8,这个长度是包含8的,然后对应的就是数据的长度,所以它可以把正常的数据报文完整的交给上层。
16位UDP长度,自己的报头中会描述自己有效载荷的情况的这种特性我们叫做协议当中的自描述字段。

UDP协议的报头。
核心概念回顾:UDP (User Datagram Protocol - 用户数据报协议)
-
无连接: 发送前无需建立连接,直接发送。
-
不可靠: 不保证数据报一定到达、不保证顺序、不保证不重复、不提供拥塞控制或流量控制。
-
面向报文: 应用层交给 UDP 多长的报文,UDP 就原样发送(不拆分、不合并),接收端也一次交付一个完整的报文给应用层。
-
轻量级: 协议头部小(仅 8 字节),处理开销低。
-
关键字段:
-
源端口 (Source Port - 16 bits): 发送方应用程序的端口号(可选,为 0 时表示发送方不需要回复)。
-
目的端口 (Destination Port - 16 bits): 接收方应用程序的端口号。
-
长度 (Length - 16 bits): UDP 数据报的总长度(字节数),包括 UDP 头部 (8 字节) + UDP 数据 (Payload)。最小值是 8(只有头部)。
-
校验和 (Checksum - 16 bits): 用于检测 UDP 头部、数据和 IP 伪头部 (Pseudo-Header) 在传输中是否出错。此字段可选,为 0 表示未计算校验和(不推荐)。计算时使用二进制反码求和再取反。
-
过程详解:
阶段一:发送端 - 封装 (Encapsulation)
-
应用层生成数据 (Application Layer):
-
应用程序(如 DNS 客户端、TFTP 客户端、视频流应用、某些游戏)决定发送数据。
-
应用程序调用类似
sendto()
的 Socket API。 -
应用程序提供:
-
要发送的数据 (Payload)。
-
目标 IP 地址。
-
目标 端口号。
-
源端口号(通常由操作系统自动分配,也可由应用指定)。
-
-
-
传输层处理 - UDP 封装 (Transport Layer - UDP):
-
操作系统内核的 UDP 模块接收到应用层的数据和要求。
-
构建 UDP 头部 (Header):
-
将
源端口
值填入相应字段。 -
将
目的端口
值填入相应字段。 -
计算
长度
:UDP 头部长度 (8字节)
+Payload 长度
。将此值填入长度字段。 -
计算校验和 (Checksum): (如果启用)
-
构造 IP 伪头部 (Pseudo-Header): 这是一个虚拟结构,仅用于校验和计算,不会被实际传输。它包含:
-
源 IP 地址 (32 bits)
-
目的 IP 地址 (32 bits)
-
协议字段 (8 bits, 值固定为
17
表示 UDP) -
UDP 长度 (16 bits, 与 UDP 头部的 Length 字段值相同)
-
-
将
IP 伪头部
、UDP 头部
和Payload 数据
拼接起来。 -
如果 Payload 数据的长度是奇数字节,则在其后填充一个值为
0
的字节(仅用于计算校验和,不实际发送填充字节),使总长度为 16 位的整数倍。 -
对这个拼接后的整个数据块(伪头部 + UDP 头部 + Payload + 可能的填充字节)进行 二进制反码求和 (One's Complement Sum) 运算。
-
将求和结果的 反码 (One's Complement) 作为
校验和
填入 UDP 头部的校验和字段。
-
-
如果未启用校验和,则将校验和字段置为
0
。
-
-
封装 UDP 数据报 (Datagram): 将
UDP 头部
添加到Payload
数据前面,形成一个完整的 UDP 数据报。现在结构是:[UDP 头部 (8字节)] + [Payload (应用数据)]
。
-
-
网络层处理 - IP 封装 (Network Layer - IP):
-
UDP 模块将封装好的 UDP 数据报传递给 IP 层。
-
IP 层负责路由和寻址。
-
构建 IP 头部 (IP Header):
-
设置源 IP 地址。
-
设置目的 IP 地址。
-
设置协议字段为
17
(表示载荷是 UDP)。 -
计算整个 IP 数据包的长度(
IP 头部长度
+UDP 数据报长度
)。 -
设置其他 IP 头部字段(如 TTL、标识、标志、片偏移、头部校验和等)。
-
-
封装 IP 数据包 (Packet): 将
IP 头部
添加到UDP 数据报
前面,形成一个完整的 IP 数据包。现在结构是:[IP 头部 (通常 20 字节)] + [UDP 头部 (8 字节)] + [Payload (应用数据)]
。
-
-
链路层处理 - 帧封装 (Data Link Layer - e.g., Ethernet):
-
IP 层将 IP 数据包传递给数据链路层(如以太网驱动)。
-
数据链路层负责在物理链路上传输帧。
-
构建帧头部 (Frame Header) 和帧尾部 (Frame Trailer):
-
帧头: 通常包含目标 MAC 地址(下一跳路由器的 MAC 或最终目标的 MAC)、源 MAC 地址、类型字段(如
0x0800
表示 IPv4)。 -
帧尾: 通常包含帧校验序列 (FCS, Frame Check Sequence),用于检测帧在物理链路上的传输错误(如 CRC)。
-
-
封装帧 (Frame): 将
帧头
、IP 数据包
、帧尾
组合成一个 帧。现在结构是:[帧头] + [IP 头部] + [UDP 头部] + [Payload] + [帧尾 (FCS)]
。
-
-
物理层传输 (Physical Layer):
- 数据链路层将帧转换成物理信号(电信号、光信号、无线电波等),通过物理介质(网线、光纤、空气)发送出去。
阶段二:接收端 - 解包 (Decapsulation)
-
物理层接收 (Physical Layer):
-
网卡接收到物理信号。
-
将物理信号转换回二进制数据(比特流)。
-
-
链路层处理 - 帧解封装 & 校验 (Data Link Layer - e.g., Ethernet):
-
数据链路层(网卡驱动)识别帧的开始和结束。
-
检查帧尾 (FCS): 计算接收到的帧(去除帧尾之前)的 CRC 校验值,与帧尾中的 FCS 值比较。如果不匹配,说明帧在传输中损坏,该帧被静默丢弃。不产生任何错误报告给上层(UDP/IP)。
-
解封装帧: 如果 FCS 校验通过:
-
剥离帧头和帧尾。
-
检查帧头中的
类型字段
(e.g.,0x0800
)。识别出载荷是一个 IP 数据包。 -
将剥离出来的
IP 数据包
([IP 头部] + [UDP 头部] + [Payload]
) 传递给网络层 (IP 层)。
-
-
-
网络层处理 - IP 解封装 & 路由 (Network Layer - IP):
-
IP 模块接收到链路层传递上来的 IP 数据包。
-
检查 IP 头部校验和: 计算 IP 头部的校验和,验证其是否有效。如果无效,丢弃该数据包。可能发送 ICMP 错误消息给源端(取决于错误类型和配置)。
-
检查目的 IP 地址: 判断该数据包是否是发给本机的(比较目的 IP 和本机接口 IP)。
-
如果不是发给本机的(如本机是路由器),则进行 转发 (Routing):查找路由表,决定下一跳,然后重新封装(修改 TTL,重新计算 IP 头部校验和)并发送到相应接口的链路层。
-
如果是发给本机的:
-
检查
协议字段
(e.g.,17
)。 -
剥离
IP 头部
。 -
将剥离出来的
载荷
([UDP 头部] + [Payload]
) 传递给协议字段
指定的上层协议模块,即 UDP 模块。
-
-
-
-
传输层处理 - UDP 解封装 & 校验 (Transport Layer - UDP):
-
UDP 模块接收到 IP 层传递上来的数据 (
[UDP 头部] + [Payload]
)。 -
检查 UDP 长度: 将 UDP 头部中的
长度
字段值与实际接收到的 UDP 数据部分长度 + 8 进行比较。如果不匹配(通常意味着数据包在传输中被截断),丢弃该数据报。无错误报告。 -
检查校验和 (如果启用且不为 0):
-
重新构造 IP 伪头部: 使用接收到的 IP 数据包的源 IP、目的 IP、协议字段 (
17
)、UDP 长度(来自 UDP 头部的 Length 字段)构建伪头部。 -
拼接数据块: 将
伪头部
+ 接收到的UDP 头部
+Payload 数据
拼接起来。如果 Payload 数据长度是奇数,同样填充一个0
字节(仅用于计算)。 -
计算校验和: 对整个拼接数据块进行 二进制反码求和。
-
验证: 如果计算结果是
0xFFFF
(全 1,因为校验和是反码),表示数据在传输过程中没有检测到错误。如果计算结果不是全 1,说明 UDP 头部或数据部分在传输中出错,该 UDP 数据报被静默丢弃。UDP 本身不发送任何错误通知给源端或应用层。
-
-
解封装 UDP 数据报: 如果所有检查通过:
-
剥离
UDP 头部
。 -
提取
源端口
和目的端口
。 -
将剥离出来的
Payload
(应用数据) 以及源 IP 地址
和源端口
信息一起传递给 绑定到目的端口号 的 应用程序。
-
-
-
应用层接收 (Application Layer):
-
应用程序(如 DNS 服务器、TFTP 服务器)通过其 Socket (调用类似
recvfrom()
的 API) 接收到:-
原始的应用数据 (
Payload
)。 -
发送方的
源 IP 地址
。 -
发送方的
源端口号
。
-
-
应用程序根据这些信息进行处理(如解析 DNS 查询、存储 TFTP 文件块、处理游戏动作)。
-
关键点总结:
-
封装是加头 (加尾) 的过程: 从应用层到物理层,每一层都在来自上层的协议数据单元 (PDU) 前面添加自己的头部(链路层还会加尾),形成本层的 PDU (Segment/Datagram, Packet, Frame)。
-
解包是去头 (去尾) 的过程: 从物理层到应用层,每一层在验证和处理本层头部后,剥离该头部(链路层还剥离尾部),将剩余部分(上层 PDU)传递给对应的上层协议。
-
UDP 的核心职责:
-
发送端: 添加源/目的端口、长度、校验和(可选),形成 UDP 数据报交给 IP。
-
接收端: 验证长度和校验和(如果启用),根据目的端口找到对应应用,将数据和源地址/端口交给应用。
-
-
UDP 的"不可靠"体现:
-
链路层 FCS 错误 -> 帧丢弃 -> IP/UDP/数据丢失 (无通知)。
-
IP 头部错误、TTL 超时、路由失败 -> IP 包丢弃 -> UDP/数据丢失 (可能有 ICMP 错误报告给源端,但 UDP 层和应用层通常不知道)。
-
UDP 长度不匹配/校验和错误 -> UDP 数据报丢弃 (无通知)。
-
网络拥塞 -> 路由器丢弃 IP 包 -> UDP/数据丢失 (无通知)。
-
不保证顺序和防重。
-
-
端口号是解复用的关键: 接收端 UDP 模块根据
目的端口号
决定将数据交给哪个应用程序。源端口号
和源 IP
则告知接收方应用数据是谁发来的,以便回复。 -
校验和依赖伪头部: UDP 校验和的计算包含了 IP 层的关键信息(源/目的 IP),这在一定程度上提供了比仅校验 UDP 头部和载荷更强的端到端错误检测能力(防止 IP 层信息被篡改导致数据投递错误)。
理解 UDP 的封装和解包过程,有助于深入掌握网络通信的基础原理,特别是无连接、轻量级协议是如何工作的,以及其可靠性与 TCP 的本质区别在哪里。

1.2.2 UDP的特点
UDP传输的过程类似于寄信。
- 无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接;
- 不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;
- 面向数据报:不能够灵活的控制读写数据的次数和数量;
1.2.3 面向数据报
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并;
用UDP传输100个字节的数据:
- 如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节;而不能循环调用10次recvfrom,每次接收10个字节;
1.2.4 UDP的缓冲区
- UDP没有真正意义上的发送缓冲区.调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;
- UDP具有接收缓冲区.但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃;
UDP的socket既能读,也能写,这个概念叫做全双工。
1.2.5 UDP使用注意事项
我们注意到,UDP协议首部中有一个16位的最大长度.也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)
然而64K在当今的互联网环境下,是一个非常小的数字.
如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装;
1.2.6 基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
当然,也包括你自己写UDP程序时自定义的应用层协议;