上三层,放在一起也可以统称为应用层,我们已经介绍过了,下面我们就来一层一层往下介绍,这篇我们就来介绍传输层最重要的两个协议之一UDP,下一篇介绍TCP
文章目录
- [1. 传输层](#1. 传输层)
-
- [1.1 再谈端口号](#1.1 再谈端口号)
- [1.2 端口号范围划分](#1.2 端口号范围划分)
- [1.3 认识知名端口号(Well-Know Port Number)](#1.3 认识知名端口号(Well-Know Port Number))
- [1.4 两个问题](#1.4 两个问题)
- [2. UDP协议](#2. UDP协议)
-
- [2.1 UDP协议端格式](#2.1 UDP协议端格式)
- [2.2 UDP的特点](#2.2 UDP的特点)
- [2.3 UDP的缓冲区](#2.3 UDP的缓冲区)
- [2.4 UDP使用注意事项](#2.4 UDP使用注意事项)
- [2.5 基于UDP的应用层协议](#2.5 基于UDP的应用层协议)
1. 传输层
"负责数据能够从发送端传输到接收端" 是传输层最核心、最根本的任务。
-
网络层 负责的是 "主机到主机" 的通信(比如,你的电脑到一台遥远的服务器)。它只关心把数据包送到目标IP地址。
-
传输层 则更进一步,负责 "进程到进程" 或 "应用到应用" 的通信。你的电脑上可能同时运行着浏览器、微信、音乐播放器等多个程序,它们都在通过网络收发数据。传输层要确保浏览器的数据交给服务器的Web服务,而不是别的。
如何实现?------ 通过端口号
1.1 再谈端口号
传输层使用 端口号 来标识主机上的不同应用程序。
-
发送端:知道目标服务器的IP地址和目标应用程序的目标端口号(如Web服务通常是80/443)。
-
接收端:通过数据包中的目标端口号,就知道该把这个数据交给哪个应用程序处理。

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

1.2 端口号范围划分
- 0 - 1023:知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议,他们的端口号都是固定的.
- 1024 - 65535:操作系统动态分配的端口号,客户端程序的端口号,就是由操作系统从这个范围分配的.
1.3 认识知名端口号(Well-Know Port Number)
有些服务器是非常常用的,为了使用方便,人们约定一些常用的服务器,都是用以下这些固定的端口号:
- ssh服务器,使用22端口
- ftp服务器,使用21端口
- telnet服务器,使用23端口
- http服务器,使用80端口
- https服务器,使用443端口
执行下面的命令,可以看到知名端口号
bash
cat /etc/services
我们自己写一个程序使用端口号时,要避开这些知名端口号
1.4 两个问题
下面两个问题其实我们在之前的文章中就已经提过,下面我们再来提一下
问题一:一个进程是否可以bind多个端口号?
答案:可以。
一个进程可以创建多个网络套接字,并将每个套接字绑定到不同的端口号上。这是非常常见的技术。
工作原理:
-
每个 socket 是一个独立的通信端点。
-
一个进程可以调用多次 socket() 系统调用,创建多个套接字描述符。
-
然后,对每个套接字描述符调用 bind(),并指定不同的端口号。
应用场景举例:
-
FTP 服务器:通常使用两个端口。
-
端口 21 用于控制连接(传输命令)。
-
另一个随机或指定的端口(如 20)用于数据连接(传输文件内容)。
-
-
自定义服务:一个后台进程可能同时提供管理接口(绑定端口 9000)和用户数据接口(绑定端口 8080)。
-
负载监听:一个进程可以同时监听多个端口,以处理不同类型的请求。
问题二:一个端口号是否可以被多个进程bind?
答案:通常情况下不行,但在特定条件下可以。
这是一个更复杂的问题,我们需要分情况讨论。
情况 A:默认情况 ------ 不允许
在绝大多数情况下,操作系统不允许两个进程绑定到同一个端口号。如果你尝试这样做,第二个进程在调用 bind() 时会失败,并得到一个类似 "Address already in use" 的错误。
原因:
-
当数据包到达时,操作系统需要通过 IP地址 + 端口号 + 协议(TCP/UDP) 这个三元组来唯一确定应该将数据交付给哪个进程的哪个套接字。
-
如果两个进程绑定了同一个端口,操作系统将无法做出唯一决策,导致数据混乱。
情况 B:特殊情况 ------ 允许
在某些特定技术手段下,可以实现多个进程绑定同一个端口。
-
SO_REUSEADDR / SO_REUSEPORT 套接字选项
这是最常见的实现方式。通过设置套接字选项,可以允许多个套接字绑定到相同的地址和端口。
-
SO_REUSEADDR:主要用于解决"TIME_WAIT"状态下的端口快速重用问题。在某些系统(如 Windows)和特定条件下,它也能允许多个绑定。
-
SO_REUSEPORT(Linux 3.9+ 引入):明确设计用于允许多个进程(或线程)绑定到完全相同的 IP 地址和端口号。操作系统内核会在内核层面进行负载均衡,将传入的连接均匀地分配给这些监听了同一端口的进程。
-
应用场景:多进程网络服务器(如 Nginx)可以使用 SO_REUSEPORT 来实现,让多个 worker 进程同时监听 80 端口,提高性能并避免"惊群"问题。
-
-
多播 / 组播
在组播中,多个进程可以加入同一个组播组,并绑定到同一个端口来接收发送到该组播地址的数据包。这是一种一对多的通信模式。
-
不同的传输协议
一个端口号可以同时被一个 TCP 进程和一个 UDP 进程绑定。因为 (IP, Port, TCP) 和 (IP, Port, UDP) 被视为两个完全不同的端点。
- 例如,一个 DNS 服务器进程可以同时在 TCP 53 端口和 UDP 53 端口上提供服务。
- 绑定到不同的 IP 地址
如果一台机器有多个 IP 地址(例如,多个网卡或配置了多个虚拟 IP),那么:
-
进程 A 可以绑定到 (IP地址1, 端口80)
-
进程 B 可以绑定到 (IP地址2, 端口80)
这是完全允许的,因为它们仍然是两个不同的端点。
2. UDP协议
2.1 UDP协议端格式

UDP首部长度:固定为8字节,包含4个16位字段。
各字段详细作用解析
- 16位源端口号
-
作用:标识发送数据包的应用程序或进程。
-
详解:这个字段告诉接收方,数据是来自发送方设备的哪个"门牌号"(端口)。接收方在回复数据时,就可以将数据发送到这个源端口号,从而确保回复能准确送达最初发送数据的那个应用程序。此字段是可选的,如果发送方不需要接收回复,可以将其置为0。
- 16位目的端口号
-
作用:标识接收数据包的目标应用程序或服务。
-
详解:这是最关键字段之一,告诉网络设备(如路由器、交换机)和操作系统,这个数据包最终应该交给哪个正在"监听"的应用程序。例如,目的端口号是53,设备就知道这个包是发给DNS服务的;是123,则是发给NTP(时间同步)服务的。
- 16位UDP长度
-
作用:指示整个UDP数据包的总长度。
-
详解:这个长度包括了UDP首部(8字节)和数据部分。因为是16位(2字节),所以最大可以表示 2^16 - 1 = 65535字节。这意味着一个UDP数据包的最大长度是64KB。接收方通过这个字段可以知道应该从网络层接收多少数据。
- 16位UDP检验和
-
作用:用于检测UDP首部和数据在传输过程中是否发生错误(如比特翻转)。
-
详解:
-
发送方会根据UDP伪首部(一个包含IP地址等信息的结构)、UDP首部和数据计算出一个校验值,并填入此字段。
-
接收方会进行同样的计算,如果计算结果与接收到的检验和不匹配,则表明数据在传输中已损坏,接收方会静默地丢弃这个包,而不会要求重传。
-
注意:这个字段在IPv4中是可选的(如果不用可置为0),但在IPv6中是强制使用的。它为UDP提供了一层最基本的可靠性保障。
-
- 数据
-
作用:承载实际要传输的应用层信息。
-
详解:这是UDP协议最终要运送的"货物",比如DNS查询内容、语音通话的音频数据、视频流数据等。数据字段的长度是可变的,最大为 65535 - 8 = 65527字节(减去8字节的首部)。
所以udp报文的报头和有效载荷怎么分离呢?
具体分离步骤:
-
定位报头的开始:
UDP报文是从网络层(IP层)交付上来的。你拿到的是一个完整的UDP报文(即 IP 数据包中的数据部分)。这个报文的起始位置就是UDP报头的开始。
-
截取固定长度的报头:
UDP报头的长度是固定不变的 8个字节(64位)。所以,你只需要:
UDP报头 = UDP报文的前8个字节 -
解析报头字段以获取信息:
将这8个字节的报头按图片所示的结构进行解析,可以获得关键信息:
-
前2个字节:16位源端口号。
-
紧接着的2个字节:16位目的端口号。
-
再接着的2个字节:16位UDP长度。这个字段至关重要,它指明了整个UDP报文(报头+数据)的总长度。
-
-
分离有效载荷:
知道了总长度(假设为 L),又知道报头固定长度为 8字节,那么有效载荷的长度就是 L - 8字节。
因此,有效载荷的起始位置是第9个字节,结束位置是第 L个字节。
有效载荷 = UDP报文的第9个字节 到 第L个字节
举例说明
-
假设你收到一个UDP报文,其"16位UDP长度"字段的值经解析后为 1028(单位是字节)。
-
分离报头:直接取前8个字节。这8个字节就包含了所有的控制信息(端口号、长度、校验和)。
-
计算数据长度:有效载荷长度 = 1028(总长度) - 8(报头长度) = 1020字节。
-
分离有效载荷:从第9个字节开始,连续取1020个字节,这部分就是发送方应用程序真正要发送的数据。
2.2 UDP的特点
UDP 核心特点详解
-
无连接
就像寄信,不需要先打电话确认
-
没有握手过程:不需要像 TCP 那样进行三次握手建立连接
-
直接发送:知道目标地址(IP)和门牌号(端口)就直接发送数据
-
无状态:服务器不会维护与客户端的连接状态信息
-
优势:开销小,速度快,适合短平快的通信
-
-
不可靠
就像寄平信,不保证对方一定能收到
-
无确认机制:发送后不知道对方是否成功接收
-
无重传机制:如果数据丢失,不会自动重新发送
-
无错误通知:网络故障导致发送失败,应用层不会收到任何错误报告
-
不保证顺序:后发送的数据包可能先到达
-
影响:应用层需要自己处理可靠性问题(如果需要的话)
-
-
面向数据报
就像寄送包裹,每个都是独立完整的
-
数据有明确边界:每个 UDP 数据报都是一个完整的消息单元
-
一次性读写:应用层每次 sendto() 发送一个完整的数据报,每次 recvfrom() 接收一个完整的数据报
-
大小限制:每个 UDP 数据报最大约 64KB(包含头部)
-
对比 TCP 的流式传输:
TCP 像水管流水,没有明确边界
UDP 像邮寄包裹,每个包裹都是独立的
2.3 UDP的缓冲区
UDP缓冲区详解
- 发送缓冲区
-
无真正的发送缓冲区:UDP协议本身并不维护一个发送缓冲区。当应用程序调用sendto发送数据时,数据会被直接封装成UDP数据报,然后交给网络层(IP层)处理。
-
可能因网络层拥堵而阻塞:虽然UDP本身没有发送缓冲区,但在数据交给网络层后,如果网络层(例如,由于出口队列已满)无法立即发送,则可能会阻塞后续的发送操作。但是,UDP不会像TCP那样进行重传或拥塞控制,它只是尽可能快地发送。
- 接收缓冲区
-
存在接收缓冲区:UDP协议在内核中维护了一个接收缓冲区,用于存储接收到的UDP数据报。每个UDP socket都有自己独立的接收缓冲区。
-
不保证顺序:由于UDP数据报是独立传输的,可能因为网络路径不同而导致到达顺序与发送顺序不一致。接收缓冲区内数据报的顺序是不确定的,应用程序必须能够处理乱序到达的数据。
-
缓冲区大小有限:接收缓冲区的大小是有限的。当缓冲区满时,新到达的UDP数据报会被丢弃,并且不会通知发送方。因此,如果应用程序不能及时读取数据,就会导致丢包。
- 全双工
- UDP socket是全双工的:一个UDP socket可以同时进行读和写操作。这意味着同一个socket既可以接收数据也可以发送数据,而不需要像TCP那样建立连接后才能通信。
UDP数据报的丢失
由于UDP的不可靠性,数据报可能会因为以下原因丢失:
-
接收缓冲区满:如果应用程序读取速度跟不上接收速度,导致缓冲区满,新到的数据报被丢弃。
-
网络拥堵:路由器或交换机的队列满,数据报被丢弃。
-
校验和错误:如果UDP数据报在传输过程中发生错误,校验和不匹配,数据报会被丢弃。
应用层处理
由于UDP不提供可靠性,如果应用需要可靠性,则必须在应用层实现:
-
序列号:用于检测丢失和乱序。
-
确认和重传:确保数据到达。
-
流量控制:防止发送方发送过快导致接收方无法处理。
为什么udp不需要发送缓冲区呢?
我们可以从UDP的工作方式来理解。
- UDP的发送过程:
-
当应用程序调用sendto发送UDP数据报时,UDP不会对数据进行缓存,而是立即将数据封装成IP数据报并发送到网络。
-
如果应用程序发送数据的速度超过了网络接口的处理能力,UDP不会像TCP那样将数据缓存在发送缓冲区中,而是直接丢弃数据并返回错误(或者取决于具体实现,可能不会返回错误,但不会保留数据)。
- UDP的无连接和不可靠特性:
-
由于UDP不需要保证可靠性,因此它不需要维护发送缓冲区来存储已发送但未确认的数据(因为不需要重传)。
-
同时,UDP是无连接的,所以它不需要维护连接状态,包括发送缓冲区。
- TCP的对比:
-
TCP是面向连接的、可靠的协议。它使用发送缓冲区来存储已发送但尚未被确认的数据,以便在超时或收到重复确认时进行重传。
-
TCP还需要实现流量控制和拥塞控制,这些都需要缓冲区来配合。
- UDP的简单性:
- UDP的设计目标就是简单高效。省略发送缓冲区减少了UDP的实现复杂性和内存开销。
- 实际影响:
- 由于没有发送缓冲区,应用程序调用sendto时,数据会立即被送出(或者因为某些原因被丢弃),而不会在UDP层被缓存。这意味着如果网络条件不好,数据包可能会被大量丢弃,而应用程序可能无法及时得知。
因此,UDP不需要发送缓冲区是因为其无连接和不可靠的特性所决定的。它不需要重传,也不需要流量控制,所以没有必要缓存发送的数据。
2.4 UDP使用注意事项
我们注意到,UDP协议首部中有一个16位的最大长度。也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。
然而64K在当今的互联网环境下,是一个非常小的数字。
如果我们需要传输的数据超过64K,就需要在应用层手动分包,多次发送,并在接收端手动拼装。
但是,我们也要注意,网络环境是复杂的,我们并不能保证发送的多个包都会按照顺序到达,甚至可能丢包。
因此,在应用层实现分包和组装时,需要考虑以下问题:
-
如何分组?每个分组应该包含哪些信息?
-
如何确保接收端能够正确组装?(例如,需要包含序列号、总包数、当前包序号等)
-
如何处理丢包和乱序?
-
是否需要进行重传?如何重传?
下面是一个简单的示例,说明如何在应用层实现分包和组装:
假设我们要传输一个大型数据,我们将其分成多个包,每个包包含:
-
序列号(用于标识同一个大数据的传输,接收端通过序列号来区分不同的大数据)
-
总包数
-
当前包序号(从0开始)
-
数据
注意:由于UDP包最大为64K,我们每个包的实际数据部分需要减去我们添加的头部信息(序列号、总包数、当前包序号等)。
但是,实际上我们通常不会让UDP包接近64K,因为超过MTU(通常1500字节)会导致IP分片,而IP分片会降低传输效率和增加丢包率。
因此,我们通常会在应用层将数据分成更小的包,比如每个包1KB左右,避免IP分片。
步骤:
发送端:
-
将数据分成多个小块,每个小块加上我们自定义的协议头(序列号、总包数、当前包序号)。
-
依次发送这些包。
接收端:
-
接收包,根据序列号区分不同的大数据,将同一个序列号的包收集起来。
-
根据当前包序号和总包数,判断是否接收完整。
-
如果接收完整,则按照包序号排序,然后组合成原始数据。
但是,这种方法可能会遇到丢包和乱序的问题。我们可以通过以下方式改进:
-
接收端收到包后,可以发送一个确认包(ACK)给发送端,告知收到了哪些包。发送端可以根据ACK决定重传哪些包。
-
或者,我们可以使用前向纠错(FEC)的方式,发送一些冗余包,使得接收端在丢失部分包的情况下也能恢复数据。
由于UDP本身不保证可靠性,所以我们需要在应用层实现这些机制。这就是为什么在需要可靠传输时,我们通常使用TCP,或者使用基于UDP的可靠传输协议(如QUIC)的原因。
2.5 基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
当然, 也包括你自己写UDP程序时自定义的应用层协议;