TCP与UDP:传输层协议深度解析

目录

[一. UDP 协议](#一. UDP 协议)

[1.1 UDP协议端格式](#1.1 UDP协议端格式)

[1.2 UDP的特点](#1.2 UDP的特点)

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

[二. TCP协议](#二. TCP协议)

[2.1 TCP协议端格式](#2.1 TCP协议端格式)

[2.2 TCP核心机制](#2.2 TCP核心机制)

[2.2.1 核心机制1:确认应答](#2.2.1 核心机制1:确认应答)

[2.2.2 核心机制2:超时重传](#2.2.2 核心机制2:超时重传)

[2.2.3 核心机制3:连接管理](#2.2.3 核心机制3:连接管理)

[2.2.4 核心机制4:滑动窗口](#2.2.4 核心机制4:滑动窗口)

[2.2.5 核心机制5:流量控制](#2.2.5 核心机制5:流量控制)

[2.2.6 核心机制6:拥塞控制](#2.2.6 核心机制6:拥塞控制)

[2.2.7 核心机制7:延时应答](#2.2.7 核心机制7:延时应答)

[2.2.8 核心机制8:捎带应答](#2.2.8 核心机制8:捎带应答)

[2.2.9 核心机制9:面向字节流](#2.2.9 核心机制9:面向字节流)

[2.2.10 核心机制10:异常处理](#2.2.10 核心机制10:异常处理)

总结:适用场景


UDP和TCP都是传输层协议,下面我们来重点讲解一下UDP与TCP(重点!!!)

一. UDP 协议

1.1 UDP协议端格式

简图:

  • 16位源端口号用于标识发送 UDP 数据包的应用程序或进程的编号,其核心作用是让接收方在回复数据时,能准确找到发送方对应的"通信入口"。--16位指的是16个比特位,即由16个二进制数字组成,对应2个字节(一个字节对应8个bit)--范围0-65535(2^16=65535,其中0-1023是知名端口号)
  • 16位目的端口号标识接收UDP数据包的目标设备上的具体应用程序或进程。
  • 16位UDP长度记录了整个报文的长度(整个报文的总占字节数),包含了报头和载荷(报头是固定8个字节,知道总体,也就知道载荷长度)---对应的范围0-65535(单位:字节)--也就对应了64KB--即一个UDP数据报的最大长度是64KB
  • 16位UDP校验和验证传输的数据是否出错(网络传输可能会出错--原因--像光/电信号这样的物理信号是可能会收到外界环境干扰的)--最简单的校验和方案:循环冗余校验算法(CRC)--在数据发送之前,把数据的每一个字节带入到 "公式" 进行计算,得到一个校验和(check1),发送时把数据和得到的校验和一起发送给接收方,接受方接受到后,把传来的数据根据相同的"公式" 重新计算得到校验和(check2),再将check1和check2进行比较--如果相同,则数据大概率是对的(有小概率不对)--如果不同,则数据一定不对

补:

  • 0-1023 知名端口号中的常见端口号:

ssh服务器,使⽤22端口号

ftp服务器,使⽤21端⼝号

telnet服务器,使⽤23端⼝号

http服务器,使⽤80端⼝号

https服务器,使⽤443端口号

问1:进程是否可以绑定多个端口号?一个端口号是否能被多个进程绑定?

答:一个端口号只能关联一个进程,而一个进程能绑定多个端口号(例如:一个服务器可以有多个ServerSocket对象,一个 ServerSocket 对应一个监听端口,多个 ServerSocket 就对应多个不同的监听端口,这就是进程能绑定多个端口的底层逻辑。而每个 ServerSocket 对象(监听端口用)在接收客户端连接后,会生成多个对应的 Socket 对象(用于与客户端通信),这些通信 Socket 与对应的 ServerSocket 端口号完全相同。)

问2:我们知道一个UDP数据报的最大长度是64KB,如果我们尝试给UDP数据包携带一个超过64KB的数据报就会发生"截断",那么我们该如何解决种情况呢?

答:方案1:在应用程序中拆包组包(实现成本高)方案2:换成TCP(最终使用)--TCP对于单个数据报长度没有限制

1.2 UDP的特点

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

⽆连接:知道对端的IP和端⼝号就直接进⾏传输,不需要建⽴连接;

不可靠:没有确认机制,没有重传机制;如果因为⽹络故障该段⽆法发到对⽅,UDP协议层也不会给应 ⽤层返回任何错误信息;

⾯向数据报:不能够灵活的控制读写数据的次数和数量;

1.3 基于UDP的应用层协议

• NFS:⽹络⽂件系统

• TFTP:简单⽂件传输协议

• DHCP:动态主机配置协议

• BOOTP:启动协议(⽤于⽆盘设备启动)

• DNS:域名解析协议 当然,也包括你⾃⼰写UDP程序时⾃定义的应⽤层协议;

补:UDP适用的场景:1. 本身不易丢包 2. 对于传输效率要求较高

二. TCP协议

2.1 TCP协议端格式

  • 4位首部长度:TCP报头长度(注意不是整个数据报的长度,选项的长度是可变的);

4位--4个bit--表示范围1-15(2^4-1),单位4个字节(和UDP不同)--因此选项的长度为4字节得倍数--所以TCP头部最⼤⻓度是15* 4=60

  • 源/⽬的端⼝号:表⽰数据是从哪个进程来,到哪个进程去;
  • 32位序号/32位确认号:后⾯详细讲
  • 6位标志位:(每个标志位占一个bit)

◦ URG:紧急指针是否有效

◦ ACK:确认号是否有效

◦ PSH:提⽰接收端应⽤程序⽴刻从TCP缓冲区把数据读⾛

◦ RST:对⽅要求重新建⽴连接;我们把携带RST标识的称为复位报⽂段

◦ SYN:请求建⽴连接;我们把携带SYN标识的称为同步报⽂段

◦ FIN:通知对⽅,本端要关闭了,我们称携带FIN标识的为结束报⽂段

  • 16位窗⼝⼤⼩: 后⾯再说
  • 16位校验和:发送端填充,CRC校验.接收端校验不通过,则认为数据有问题.此处的检验和不光包含 TCP⾸部,也包含TCP数据部分.
  • 16位紧急指针:标识哪部分数据是紧急数据;
  • 40字节头部选项:暂时忽略;

2.2 TCP核心机制

2.2.1 核心机制1:确认应答

确认应答机制是TCP实现可靠传输机制的核心机制

网络通信中可能会出现后发先至的情况(TCP只需要保证不同数据报之间的整体有序的,而单个数据报内的字节是连续的 "数据段",不会乱序)--传输数据的顺序错了,就表示的含义可能就错了

如何解决:可以对传输的数据进行编号,在接收方收到的数据时,按照编号重新排队---但是由于TCP是面向字节流,并不存在 "第一条" "第二条" 这样的概念 --- 直接针对字节进行编号--但是不是对每个字节都进行编号,是只对一个数据报的有效数据的第一个字节进行编号---序号的所在的位置:TCP报文的32位序号中---约定:给字节编号,从某个数字开始,连续递增(编号是为了防止乱序)

当接受方接收到数据报后会自动推算出所接收数据报有效数据字节流的范围,然后会给发送方发送一个ACK确认应答报文--ACK报文是TCP内置的特殊报文由操作系统自动回应,它一般没有载荷(有时会有),并且ACK标志位被设为1,ACK报文中32位确认序号中的数字是推算出有效数据范围的最后一个字节编号再+1

确认序号的含义由两种理解:(假如发送方发送来1-1000的数据)

1. 告诉从1001之前的数据,对方已经收到

2. 对方向你索要1001之后的数据

注意:应答 != 响应

应答: 是操作系统自动反馈的

响应:是应用程序,在业务逻辑中计算得到的返回结果

简单总结:
TCP 确认机制的核心是:"用 ACK 保证'数据发送方'和'数据接收方'互相确认,确保数据可靠传输";实际交互中,还会通过"ACK 与响应数据合并发送"等优化,提升传输效率。

2.2.2 核心机制2:超时重传

超时重传机制也是TCP实现可靠传输机制的核心机制(超时--等待ACK的最长时间上限)

超时重传主要是解决丢包问题--丢包是概率性事件,一般概率不大,但是一旦发生后果很严重

丢包问题主要有两种情况:1. 数据包丢了 2. ACK丢了

  • 数据包丢了:由于数据包丢失,接收方没有收到发送方传来的数据,此时就不会返回对应的ACK应答报文,又由于发送方长时间(超时)没有收到ACK确认应答报文,就会重传该数据
  • ACK丢了:发送方成功将数据发送给接收方,但是在接收方返回对应ACK的过程中ACK丢了,由于发送方长时间(超时)没有收到ACK确认应答报文,也会触发重发动作,但是我们重发的数据不会和之前成功发送的数据不会重复(操作系统内核会进行去重),下面是原理图:

2.2.3 核心机制3:连接管理

TCP核心机制:连接管理(重点)主要分为两部分:1. 建立连接 2.断开连接---这两个都是操作系统内核完成的过程,对于应用程序来说感知不到,下面是详图图解:

1. 建立连接---流程---"三次握手"

注:在网络中的"握手"(是在"打招呼",没有实际的含义),通信时会发送一个"不包含业务数据" 的数据包--即没有载荷,光有报头(在三次握手的中会用到SYN报文---只有报头且SYN标志位设为1的TCP数据报)

而三次握手就是通信双方需要三次这样的握手数据才能完成连接的建立。

为了更简单的理解三次握手的过程,我们着重关注理解下面的简图即可:

ACK报文是收到SYN报文时操作系统内核自动立即返回的

那么就有人要问了,不是三次握手吗,这里明明有四步,不应该是四次握手吗?

答:第2和第3步可以合并为一次--(TCP这样设计的)原因--网络传输需要一系列的开销,而ACK和SYN都是由操作系统内核控制完成的,并且是可以同时发送的,将他们合并在一起(一个TCP数据报,并且报头中ACK标志位和SYN标志位均设为1)就可以减少网络往返的次数,提升连接建立效率,下面是合并后的图解:

问1:为什么要三次握手,有什么意义?

1. 三次握手起到"投石问路"的效果--先初步验证通信链路是否通畅(后续进行可靠传输的前提)

2. 三次握手,也是在验证通信双方的发送能力和接受能力是否正常(双方都要知道自己的和对端的发送能力和接受能力是否正常),下面来用一个实例来解释为什么要三次才能知道双方的情况,而不是两次就可以:

3.三次握手过程中还会进行一些必要的"协商工作"---通信过程中,有一些参数是通过"商量"来确定的,TCP建立连接时,最关键的是协商通信过程中32位序号的起始数值( 32位序号不是从1开始

每次连接,起始序号都是是不一样的,而且序号起始值会差别很大--原因--为了避免出现"前朝的剑,斩本朝的官"的情况出现--下面是图解:

问2:TCP的三次握手必须是三次吗?

答:可以是四次 (中间一次拆为两次),但是没必要--增加了开销;不可以是二次,因为对端无法完全知道对方的通信与接收是否正常

简单总结:连接建立是双方确认后保存双方对端的信息

2. 断开连接---流程---"四次挥手"

断开连接需要用到的FIN报文--也是只有报头没有载荷的TCP数据包,且报头中FIN标志位设被为1

对于断开连接来说:客户端和服务器都有可能先发起

下面是图解:(客服端发起断开连接)

问1:对于"四次挥手"来说第2次和第3次能合并成一次吗?

答:不一定,但是一般不能合并 ---原因---首先ACK的返回时机是操作系统内核控制的,即收到FIN之后立即触发的,FIN的返回时机是应用程序控制的,即当应用程序调用Socket.close方法才会触发这里的FIN,然而当收到FIN和程序执行到Socket.close方法之间的时间间隔可能会很长(比如几百个ms),也就是说它们的触发时间都是不同的,因此合并不了 ;;那什么情况下能合并呢--当应用程序调用.close的时机与内核返回ACK的时机几乎同步时,就可以合并发送ACK+FIN

问2:一般什么情况下会触发FIN的发送:

  • 进程退出/手动关闭main方法
  • 调用close()

问3:在客户端发起断开的情况下,服务器是在什么时候发出FIN报文的?

答:当客户端想要断开连接发出FIN后,客户端的发送流关闭(接收流并没有关闭)---意思是"我这边没有要发送的数据了",但是要注意的是发送流关闭仅禁止发送应用数据,ACK属于TCP控制报文,不受此限制。服务器中的Scanner.next() /hasNext()等 会感知到客户端的发送流关闭而返回false,然后经过一系列代码后执行到.close()方法从而触发FIN的发送--- 总之:服务端会先处理完客户端发来的所有请求,再发送 FIN

问3:什么时间节点,客户端与服务端完全断开连接?

答:发起断连端---TLME_WAIT超时后完全断开

对端----收到发起断连端发送的最后一个ACK并且完成资源回收后完全断开

简单总结:连接断开是双方确认后删除双方对端的信息

TCP常用状态(下面图中用红框的就是我们需要重点掌握的)

  • LISTEN -- 服务器特有的状态---服务器启动,ServerSocket关联好端口号之后,此时TCP的状态就是LISTEN(此时服务器准备就绪,随时可以有客服端连上)(注意:windows中是LISTENING,Linux中是LISTEN)
  • ESTABLISHED -- 客户端和服务端都会涉及--- 这个状态标识连接已经建立,接下来可以进行网络通信了
  • CLOSE_WAIT-- 被动关闭连接的一方才会出现的状态--- 断开连接时,收到FIN的一方会进入的状态(一收到FIN立即进入这个状态,直到这一方主动发出FIN,该状态消失)-----CLOSE_WAIT就是在等待调用close方法
  • TIME_WAIT -- 主动断开连接的一方会出现的状态 --- 作用是等待一段时间--其中等待是为了处理"最后一个ACK丢包"这样的特殊情况---如何处理:通过在等待的时间中有没有收到重传的FIN来判断--如果没有收到重传的FIN说明最后一个ACK没有丢包;如果收到了重传的FIN说明最后一个ACK丢包,这是主动断连接的一方就要重新发送ACK(TIME_WAIT的等待时间为2MSL--MSL是操作系统中定义的一个"常量"表示网络传输过程中,从一端到另一端的"最大理论时间"一般默认1mim

2.2.4 核心机制4:滑动窗口

刚才我们讨论了确认应答策略,对每⼀个发送的数据段,都要给⼀个ACK确认应答.收到ACK后再发送下 ⼀个数据段.这样做有⼀个⽐较⼤的缺点,就是性能较差.尤其是数据往返的时间较⻓的时候.既然这样⼀发⼀收的⽅式性能较低,那么我们⼀次发送多条数据,就可以⼤ 的提⾼性能(其实是将多个 段的等待时间重叠在⼀起了).---如何优化--滑动窗口

滑动窗口是在要发送大量数据时提高传输效率

滑动窗口--批量发送,批量等待ACK

滑动窗口本质上是在"亡羊补牢"--它是TCP已经在保证传输的可靠性而损失传输效率的基础上去减少损失,而不是提升到比不带可靠性的协议还要快---即滑动窗口不会破坏可靠传输

一次批量传输数据的大小称为 "窗口大小"--滑动窗口的大小约束的是发送方在未收到接收方ACK确认以前,最多可连续发送的未确认数据量

而滑动窗口一次滑动的字节数是由接收方返回的ACK确认序号(本次新确认收到的连接有序字节数)决定的;例如若滑动窗口大小为 5,发送方一次可发送 5 条未确认数据。当接收方连续收到前 3 条数据且无差错时,会发送一个累积 ACK 确认这 3 条数据已收到(ACK 序号指向第 4 条数据,表明期待接收第 4 条)。此时发送方的滑动窗口会从初始的「1-5」区间,滑动为「4-8」区间,完成 3 格的滑动;

滑动窗口中的累计确认---不是对每一条数据都发送对应的ACK,而是接收方在确认累积收到多个连续数据包时才会统一发送一个ACK(接收方自己维护了一个接收窗口,其中就记录了它接收到的哪些数据的序号,因此它能快速识别失序/丢包),用来确认之前连续发送的数据包接收方已经收到

滑动窗口中的"丢包"如何应对呢?

情况一:ACK丢了

ACK丢失的情况不需要做任何处理---原因---ACK的确认序号,后一个ACK能够包含前一个

那如果丢的是最后一个ACK呢---交给超时重传(等待时间触发超时重传)

情况二:数据丢了

如果是数据丢了--解决--快速重传--接收方察觉到有数据丢包,通过发送三个重复的ACK(确认序号指向丢失的那条数据)给发送方,发送方收到三个重复的ACK后会触发快速重传,重新发送丢失的数据

如果丢了两条数据呢?---缺谁补谁

滑动窗口中的两种重传机制--快速重传 和 超时重传

快速重传是滑动窗口默认优先使用的重传机制 (核心是"早发现、快处理",无需等待超时计时器(RTO)到期。接收方检测到数据失序后,发送3个重复ACK触发发送方立即重传丢失数据),而 超时重传 是兜底的重传机制,是作为快速重传失效后的"最后保障"(当网络极端拥堵(如数据和ACK都丢失),发送方收不到3个重复ACK,此时超时计时器超时后,发送方会重传窗口内未被确认的所有数据,作为快速重传失效后的"最后保障")

快速重传和超时重传是否"冲突"?

是在不同情况下的处理场景:

如果数据发的很慢---无法构成滑动窗口---按照超时重传的机制

如果数据发的很开---构成滑动窗口---优先按照快速重传的机制(特殊情况下也会超时重传--如发送的三个ACK其中的一个或几个丢了--超时重传兜底)

2.2.5 核心机制5:流量控制

我们知道滑动窗口的窗口越大,传输的效率就越高,但是窗口特别大,可行吗---显然是不行的--比如一下子发送太多的数据,接收方可能处理不过来,就有可能出现丢包的情况------->解决方案:流量控制

流量控制实际上就是对滑动窗口的大小进行限制(流量控制会根据接收方的接受能力来反向限制发送方的发送速度)

那么如何取衡量接收方的接收能力--- 看接收方的接受缓冲区的剩余空间大小

衡量了后如何通知发送方做出限制:接收方会把接收缓冲区的剩余空间大小的值,通过ACK通知给发送方--接收方将接收缓冲区的剩余空间大小的值填入TCP报头中的16位窗口大小中,发送方就可以根据这个值来决定下一轮窗口大小是多少(注意:此处返回的窗口大小是否最大只有64KB呢--不是的--TCP报头中的选项中还有窗口扩展因子,16位窗口大小<<窗口扩展因子256KB)

特殊情况:如果接收方的接收缓冲区已经满了,此时就会发送一个 窗口大小值为0的ACK,发送方接收到了后就会暂停发送数据,但是如果暂停了就没法触发ACK,后面如果缓存区有空间了也没法通知给发送方--解决方案:发送方会每隔一段时间就发送一个窗口探测包(无载荷,只是为了触发ACK,得到新的窗口大小的值),目的是为了查询接收方的接收缓冲区是否有空闲

2.2.6 核心机制6:拥塞控制

拥塞控制和流量控制类似,都是对滑动窗口的大小做出限制

流量控制:依据据接收方的接受能力

拥塞控制:依据通信路径的处理能力

拥塞控制过程:刚开始按照一个比较慢的速度(比较小的窗口)发送数据,如果没有丢包->加大速度(增加窗口大小),如果丢包->减小速度(减小窗口大小

注意:拥塞窗口大小是由发送方单独维护的,而流量控制中的窗口大小是由接收方维护并告知给发送方的----实际发送窗口的大小是取两个值的最小值

下图是拥塞窗口大小的变化规律:

注意:

  • 拥塞控制和流量控制都是基于滑动窗口的 ,像TCP三次握手以及以很慢的速度发送并接收数据这种不构成滑动窗口的情况下是不会涉及拥塞控制和流量控制的
  • 拥塞控制和流量控制是同时生效,共同协作的

2.2.7 核心机制7:延时应答

能否在有限的机制下,尽可能把窗口变大?------延时应答(提升速率)

延时应答就是把返回ACK的时间稍微拖晚一点,得到窗口大小数值可能就会更大一些(在推迟的一段时间内应用程序就有可能消耗一部分数据,这时接收方的接受缓冲区的剩余空间就可能会大一些)---注意并不是延迟的这一段时间内接受缓冲区的剩余空间一定会变大,也可能变小或者不变,但是在大多数的情况下是会变大的

在延时应答这样的机制下,返回的ACK的数目可以减少

2.2.8 核心机制8:捎带应答

捎带应答是配合延时应答的机制

我们知道从发送ACK到返回响应是需要时间的 ,但是由于有延时应答的机制,如果响应赶上了ACK延时应答的时间就可以捎带应答---将响应和ACK合并为一个TCP数据包(ACK关键只是报头,不需要载荷,而响应数据关键是载荷) 一并发送出去

2.2.9 核心机制9:面向字节流

TCP是面向字节流传输的,而在传输的过程可能会出现粘包的情况

什么是粘包?:粘包粘的是应用层数据包,而粘包的本质上是因为包与包之间的边界不明确(UDP不会出现粘包的情况,因为UDP传输的最小单位是数据报,每个数据报都有独立的头和完整的数据),而TCP会出现粘包的情况正是因为它是面向字节流传输数据的

如何解决粘包问题:

方案一:引入分隔符,以特殊字符作为包的开头或结尾(在我们写的echoserver就是用的 \n 作为结束标志的,但是我们数据包的正文就不能包含 \n ,所以如果觉得 \n 可能重复,就可以使用其它的奇怪字符作为分隔符)

方案二:在数据开头的地方,添加一个固定的属性来表示数据包的长度----如[4]aaaa[5]bbbbb(约定前两个字节表示长度)

注意:在实际的开发中,一般不涉及粘包的问题(除非我们修改框架/基于TCP原生开发 ),进行网络开发的时候,大多数使用的是现成框架--框架里已经帮我i们把粘包问题都处理好了

2.2.10 核心机制10:异常处理

异常处理主要分为以下四种情况:

1. 进程崩溃

和四次挥手完全相同 (正常四次挥手通过socket.close触发)

进程退出了(正常退出/崩溃退出)--->操作系统自动对文件资源进行清理(操作系统清理PCB,PCB中的文件描述符表)------>等同于调用socket.close

2. 主机关机(正常流程关机)

**正常关机会先强制结束你的所有程序再关机---相当于进程崩溃--->也会触发FIN进一步的进行四次挥手。**而正常的关机需要时间,在这一段时间内可以让四次挥手挥完,也有可能挥不完(挥慢了,主机已经关机了)

下面图解是挥不完会发生的情况:

3. 主机关机(掉电)

3.1 接收方掉电

3.2 发送方掉电

4.网线断开

和第三种情况相似


总结:适用场景

TCP:适合大部分的网络通信场景

UDP:适合对性能要求较高,并且丢包概率不大的情况

相关推荐
devlei2 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
&&Citrus3 小时前
【CPN学习笔记(二)】Chap2 非分层颜色 Petri 网——从一个简单协议开始读懂 CPN
笔记·学习·php·cpn·petri网
HXQ_晴天4 小时前
Linux 磁盘清理 & 查看常用指令笔记
笔记
努力的小郑4 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3565 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3565 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁5 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp5 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴7 小时前
COBOL语言的云计算
开发语言·后端·golang
小陈phd7 小时前
多模态大模型学习笔记(三十)—— 基于YOLO26 Pose实现车牌检测
笔记·学习