☰
UDP和TCP原理
2026/10/20 18:51:21 网站建设 项目流程

UDP 传输层

传输层

负责数据能够从发送端传输接收端.

再谈端口号

端口号(Port)标识了⼀个主机上进行通信的不同的应用程序

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

UDP协议端格式

含义:

字段大小作用
源端口号16bit (2 字节)发送方端口
目的端口号16bit (2 字节)接收方端口,最重要!交给上层哪个程序
UDP 长度16bit (2 字节)整个 UDP 报文总长度(头部 8 字节 + 数据)
UDP 校验和16bit (2 字节)校验数据是否损坏,可校验

分离:怎么把 UDP 报文区分开报头和正文?

UDP 头部有16 位 UDP 长度,记录整个 UDP 报文(8 字节头部 + 数据)总字节数。读到了长度后只需再减去8bit的报头,那剩下的就是正文部分了

UDP 为什么叫用户数据报”

  • 数据报 (datagram):自带边界、独立完整的一包数据。每个报文独立。
  • 用户:应用层用户直接发送一份完整数据,内核不拆分、不合并。 你 sendto 给多少数据,就封装成一个 UDP 数据报发出去。

struct udphdr 结构体(内核里面描述 UDP 头部的 C 结构体)

struct udphdr { __u16 source; //源端口 __u16 dest; //目的端口 __u16 len; //udp总长度 __u16 check; //校验和 };

注意:这个结构体只在本机操作系统内核内存内部使用。 内核内部:同一台机器,大小端、对齐规则一致,直接操作结构体成员没问题。不能直接把这个结构体二进制原样发到网络上!跨机器传输必须转成网络大端字节流(序列化),处理大小端。

UDP 的特点

UDP 传输的过程类似于寄信。

  • 无连接:知道对端的 IP 和端口号就直接进行传输,不需要建立连接;
  • 不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP 协议层也不会给应用层返回任何错误信息;
  • 面向数据报:不能够灵活的控制读写数据的次数和数量;

UDP 缓冲区

• UDP没有真正意义上的发送缓冲区.调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;

• UDP具有接收缓冲区.但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序⼀ 致;如果缓冲区满了,再到达的UDP数据就会被丢弃;

怎么理解:

1.UDP没有真正意义上的发送缓冲区调用sendto(),直接把数据拷贝交给内核。内核简单封装 UDP 头部,直接交给网络层发出去。 sendto 调用成功,只代表数据拷贝到内核,不代表对方收到。 没有缓存起来等待重传的区域,所以不需要发送缓冲区。 TCP 才有发送缓冲区,用来存数据,做超时重传(就是说传数据的时候如果失败了,因为不会删除缓冲区里的数据,这样就可以重传了呀)。UDP 没有重传机制,所以不需要。udp不可靠的原因之一

2.

  1. 不保证顺序网络上 UDP 报文会乱序。缓冲区里报文顺序,不一定和发送顺序一样。先发的包可能后到。
  2. 缓冲区满 → 新报文直接丢弃接收缓冲区存满了,再有新 UDP 报文到达内核,直接丢掉,不会通知应用层。应用程序完全不知道丢包了,不可靠

UDP 使用注意事项

我们注意到,UDP 协议首部中有一个 16 位的最大长度。也就是说一个 UDP 能传输的数据最大长度是 64K (包含 UDP 首部).

然而 64K 在当今的互联网环境下,是一个非常小的数字.

如果我们需要传输的数据超过 64K, 就需要在应用层手动的分包,多次发送,并在接收端手动拼装;

基于 UDP 的应用层协议

  • NFS: 网络文件系统
  • TFTP: 简单文件传输协议
  • DHCP: 动态主机配置协议
  • BOOTP: 启动协议 (用于无盘设备启动)
  • DNS: 域名解析协议

报文的理解

在OS内部存在大量的报文,要管理这些报文,酒就得通过先描述再组织的方式

sk_buff 内核缓冲区(Linux 网络核心)

sk_buff是 Linux 内核用来存放一个网络报文的结构体。 网卡收到每一个数据包,内核都会生成一个struct sk_buff。所有 IP/UDP/TCP 报文在内核都靠它管理。

sk_buff 关键指针(图中标出来 head /data/tail /end)

  • head:这块内存的起始地址
  • end:这块内存的结束地址

head‑end 是整个申请出来的内存总区间。

  • data:当前协议头部的起始位置,可以前后移动。
  • tail:当前有效数据的末尾位置。

拆包、封包本质:移动 data、tail 这两个指针!不是拷贝内存!

拆包(向下封装)

从应用层往下发: 一开始data指向应用数据。 封装 UDP 头:data指针往前挪 ,留出位置填写 UDP 头部; 封装 IP 头:data再往前挪,填写 IP 头部; 封装以太网头:继续往前挪指针。

封包过程就与其相反

  1. 向上收包:不断后移data,剥掉一层一层头部。
  2. 向下发包:不断前移data,留出空间填充各层协议头部。

sk_buff 链表

OS 内核里面会有很多收到的数据包,每个包一个 sk_buff,用链表串起来。UDP 接收缓冲区,本质就是一条 sk_buff 链表,每一个节点存完整一个 UDP 报文。 当你recvfrom:取出链表中一个 sk_buff,拷贝数据给用户态,然后释放这个 sk_buff。

如果应用层正在解析报文,会不会影响 OS 从网络中读取报文?为什么?

不会。

  • 内核接收报文放在内核的 sk_buff 队列(内核缓冲区)。
  • 应用层调用recvfrom/recv只是把内核 sk_buff 里面的数据拷贝一份到用户内存。
  • 用户层拿着拷贝出来的数据慢慢解析,不影响内核继续接收新报文放入 sk_buff 链表。 内核和应用层内存隔离。

TCP协议段格式

  1. 16 位源端口号
  2. 16 位目的端口号
  3. 32 位序号 seq:本报文段数据第一个字节的编号,可靠传输、重排序靠它。
  4. 32 位确认号 ack:期望收到对方下一个字节序号,确认收到。
  5. 4 位首部长度(数据偏移)

单位:4 字节!! 取值范围:5‑15

  • 5 ×4 =20 字节:无选项的情况,TCP 最小头部 20 字节
  • 最大 15 ×4 =60 字节,头部最大 60 字节。

靠这个字段,内核知道:头部到哪里结束,后面哪里开始是应用数据。

  1. 保留 6 位 + 6 个标志位:URG、ACK、PSH、RST、SYN、FIN(具体含义后面说)
  2. 16 位窗口大小:滑动窗口,流量控制。告诉对方我方接收缓冲区还能收多少字节。
  3. 16 位校验和:校验头部 + 数据
  4. 16 位紧急指针:URG 标志生效才有用,标记紧急数据位置。
  5. 选项部分:0~40 字节。MSS、窗口扩大因子、时间戳等。补齐到 4 字节对齐。
  6. 后面:应用层数据载荷

怎么没有整个 TCP 报文总长度字段?

UDP 头部有udp‑len记录整个报文长度。TCP 头部没有总长度字段!因为TCP 是字节流协议,报文边界不重要(每一个报文之间边界不明确)。

报头和有效载荷怎么分离?

靠4 位首部长度(数据偏移)头部字节数 = 首部长度 ×4data 指针向后移动这么多字节,就跨过 TCP 头部,到达应用数据。

TCP通信过程

可靠性本质 & 确认应答机制

客户端一次可以向服务端发送多个报文,服务端再做应答,不用发一次应答一次,提高效率

画图箭头传递的内容:如果单单只是应答的话就只有报头,如果带有数据(有效载荷)就是一个完整的报文

怎么理解 TCP 的可靠性

  1. 可以保证历史消息的可靠性:已经收到应答的报文,100% 确认对方收到。
  2. 最新发出去的报文,还没有收到应答,可靠性无法保证(可能丢包、延迟)。

TCP不能做到 100% 实时可靠,只有收到应答的数据,才代表可靠交付。TCP 最重要可靠性策略:确认应答机制(ACK)

确认应答机制(ACK)

  • 发送方发送数据。
  • 接收方收到数据,返回应答
  • 发送方收到应答:代表这部分数据对方已经成功收到,这份历史数据可靠。
  • 没有收到应答,就不确定对方是否收到。

TCP 序号 (seq) & 确认号 (ack)、捎带应答

1.序号 seq(32 位)

TCP将每个字节的数据都进行了编号,即为序号.(后面再进一步理解)。

  • seq:当前报文数据第一个字节的编号。

作用:①解决乱序(TCP 根据序号 seq 把乱序到达的报文重新排序,整理成有序字节放进缓冲区);②去重;③实现确认应答。

2. 确认号 ack(32 位,核心)

ack = 期望收到对方下一个字节的序号

含义:确认号之前所有字节,全部已经完整收到。

举例: 对方发来报文:seq=1000,携带 200 字节数据(字节 1000‑1199) 接收方回复ack = 1200

意思:1000~1199 全部收到,下一次希望收到序号 1200 开始的数

捎带应答

接收方不一定专门发一个纯 ACK 报文。即除了单单发送应答(报头)表示成功收到了这条报文外,还可以发送数据(有效载荷)呀,这时就可以把 ACK 确认号直接填在自己要发送的数据 TCP 头部里面,跟着业务数据一起发出去,这叫捎带应答。这样ACK 确认可以跟随自己的数据报文一起发送,不用单独发确认报文。

为什么 TCP 报文要有 seq 和 ack 两个字段?

TCP 是全双工,双方都可以同时发数据。

  • seq:我发给你的数据的字节编号
  • ack:我对你发给我的数据的确认

一条报文既要标记「我发出去的数据序号 seq」,又要标记「我确认收到了你多少数据 ack」,所以两个字段都必须存在

再谈TCP流量控制

接收方有接收缓冲区,大小有限。 如果发送方疯狂发数据包,接收缓冲区满了:

  1. 新的数据装不下 → 只能丢弃报文
  2. 发送方白发,网络浪费,低效。

问题:发送端怎么知道对方接收缓冲区还剩多少空间?TCP 引入窗口(window),也就是接收窗口 rwnd (receive window)。

流量控制原理:滑动窗口,靠 ACK 报文中的窗口字段

  1. 接收方每次回复 ACK 的时候,会把自己接收缓冲区剩余可用空间,填到 TCP 头部的Window(窗口)字段,告诉发送方。
  2. rwnd = 接收缓冲区剩余字节数。
  3. 发送方严格遵守:不能一次性发送超过 rwnd 字节的数据。

发送方的发送窗口大小,就等于对方通告过来的 rwnd。 这就是流量控制:控制发送速率,适配接收方处理能力,防止接收缓冲区溢出。

TCP 标志位

为什么要有标志位

TCP 报文有多种类型:建立连接、断开连接、普通数据、应答、重置连接、紧急数据。 接收方拿到报文,需要知道这是什么类型报文,做不同处理。 于是 TCP 头部用多个 1bit 标志位(位段 bit‑field)来标记报文类型

struct tcphdr:

6 个核心 TCP 标志位(每个占 1bit,1 置位代表生效)

标志名字作用
SYN同步建立连接;三次握手。
FIN结束断开连接;四次挥手。
ACK确认应答,ack_seq字段有效(即表明是一个应答报文);绝大多数报文 ACK=1
RST重置异常断开连接,拒绝连接,报错复位
PSH推送接收方立刻把缓冲区数据上交应用层,不要缓存
URG紧急urg_ptr紧急指针有效;传输紧急数据
SYN和FIN

三次握手:

  1. 第一次握手:客户端 → 服务端SYN=1,申请建立连接
  2. 第二次握手:服务端 → 客户端SYN=1,ACK=1,服务端同意连接;
  3. 第三次握手:客户端 → 服务端ACK=1

四次挥手:

  1. 第一次挥手:客户端 → 服务端FIN=1客户端:我这边不再发送新数据(关闭发送方向),但还可以接收对方数据。
  2. 第二次挥手:服务端 → 客户端ACK=1确认收到 FIN。此时服务端还可以继续向客户端发送剩下的数据。

这一步只是应答,服务端自己的发送通道还没关闭,所以不能和 FIN 合并,因此需要四次。

3.第三次挥手:服务端 → 客户端FIN=1,ACK=1服务端把自己剩余数据全部发送完毕后,发送 FIN,代表服务端也不再发送数据。

4.第四次挥手:客户端 → 服务端ACK=1确认服务端 FIN。

RST

建立连接,一定会成功吗?

不一定。网络丢包、端口没监听、双方对连接状态认知不一致,就会出现RST(Reset,重置报文),直接暴力断开连接,对应浏览器报错ERR_CONNECTION_RESET连接已重置。

图中的场景:三次握手异常 —— 双方连接状态认知不一致

对客户端来说在1位置处,发送了ACK报文后就代表3次握手已经完成,而服务端要在2位置处收到了这个ACK才算3次握手成功,即双方对连接状态的认知不一样

所以如果在1位置处,客户端只要发送了ACK后,就会认为已经成功建立了连接,向服务端发送数据,但如果发送的ACK丢失了,服务端在2位置处没有收到,那么等到服务端收到客户端发送的数据(报文)后,发现根本就没有这个连接记录。

服务端不知道这个 ACK 属于谁,直接回复RST 报文,重置。

RST 常见触发场景

(1)访问一个没有进程监听的端口

(2)迟到的旧报文到达,连接已经消失

(3)通信中途异常,一方进程崩溃退出

(4)报文序号不合法

(5)防火墙拦截

核心:上面所有这些问题,都可以进行重置来解决!

URG 紧急标志 & 紧急指针 urg_ptr
  • URG 标志位 = 1:代表紧急指针 (urg_ptr) 字段有效。
  • urg_ptr(16 位紧急指针):当前报文的有效载荷中特定偏移量处,有紧急数据

指向:紧急数据的最后一个字节。

TCP 是字节流,默认按顺序处理接收缓冲区数据;紧急数据可以跳过普通排队,优先交给应用程序处理,不用等前面全部普通数据读完。

工作过程

  1. 发送方:URG=1,填写urg_ptr

经典:紧急数据通常 1 字节,一般用作状态信号(中断信号,比如 ctrl+c 取消上传),不是传输大块业务数据。

  1. 接收方:
  • 看到URG=1,根据seq + urg_ptr定位到紧急数据末尾(先处理)。

⚠️重点:紧急指针指向紧急数据的最后一字节,不是起始位置

例子:取消上传

场景:客户端正在上传大文件,想立刻中断本次传输。 不需要把前面一大堆上传数据全部读完,直接发1 字节紧急数据(当作中断命令),接收方优先拿到这个紧急字节,执行取消上传,丢弃剩余普通数据流。

再理解序列号,确认号

TCP不给报文编号,给每一个字节编号。。

我们可以把TCP发送缓冲区当成一个巨大的字节数组(char send_buf[]),缓冲区里每一字节都分配唯一序列号 (seq)。

序列号就是数组下标,每个字节对应一个序列号。

seq=这个报文中第一个字节的编号。

eg:

字节内容abcdef
序列号100110021003100410051006

现在把abc放在第一个报文发送

报文 seq = 1001,载荷:abc代表这个报文携带字节:1001、1002、1003。

报文只是 “打包箱子”,箱子本身没有编号,箱子里面的每一个字节才有编号。

ACK 确认号 ack_seq 怎么理解呢?

对方回复ack_seq = 1004含义:

1004 之前所有字节(1001、1002、1003)我全部收到了,下一个我想要字节编号 1004。

不是确认报文!是确认字节编号

超时重传机制

先来理解一下什么时候要超时重传,什么是丢包

ACK 报文(ACK)或者应答都是同一回事:只有 TCP 头部,没有载荷

两种情况

  1. A 发出去的数据报文在网络丢了,B 根本没收到。---数据丢了
  2. B 已经成功收到数据,并且返回了 ACK=1001,但是ACK 应答报文在路上丢了。---ACK丢了

所以丢包分为数据丢包和ACK丢包

  • 从网络设备角度:两种都是网络丢包事件
  • 从 TCP 发送方A的角度:丢包 = 我发出去的数据报文,B 没有收到。

在A的角度只有图1的那种情况(数据没有到对方)才算丢包,图2对A来说不算丢包(数据已经成功到B了),所以对A来说没有收到ACK,不意味着丢包,即不意味着我的数据没有到B(也有可能是数据已经到了但发生图2的情况),但对网络来说没有收到ACK就是丢包了(不管哪一种情况都是丢包)

超时重传

设置一个超时计时器(RTO,重传超时时间)

规则:发送数据的时候启动计时器;

  • 如果计时器到期之前收到对应 ACK:计时器撤销,万事大吉。
  • 如果收不到 ACK && 计时器超时:发送方就判定报文丢失,重传这份数据(1‑1000)(“判定报文丢失”不是说 TCP 确定 “数据一定丢了”。 是:时间窗口已经耗尽,继续等没有意义,只能假定丢失,执行重传兜底。)

问题来了:如果B 早就收到 1‑1000 字节。现在 A 又重传一遍 1‑1000,B 收到重复数据。解决方案:依靠序列号去重。B 根据序列号识别:1‑1000 已经接收过,直接丢弃重复数据但是依然再次回复 ACK=1001。

超时时间 RTO 应该设置多长?为什么?

不能写死固定值!

  • 如果设置太小:马上就判定超时了,明明再等一会就可以传回来了,却定位超时,导致大量不必要的重传,浪费网络。
  • 如果设置太大:真丢包的时候,要等很久才重传,传输延迟高。

TCP 为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间.

  • Linux 中 (BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍.
  • 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传.
  • 如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增.
  • 累计到一定的重传次数,TCP 认为网络或者对端主机出现异常,强制关闭连接.

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询