【Linux网络篇】:TCP协议全解析(一)——从数据段格式到可靠传输的三大基石
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客
文章目录
- TCP协议(第一部分)
- 一.TCP协议段格式
- 1.回答两个问题
- 2.窗口大小的作用
- 3.序号和确认序号的作用
- 4.6个标志位的作用
- 二.确认应答(ACK)机制
- 三.超时重传机制
- 四.连接管理机制
TCP协议(第一部分)
TCP全称为”传输控制协议“,是一个具有接受和发送缓冲区的,全双工通信的,数据传输控制的一种协议。
一.TCP协议段格式
首先,同一份数据在网络协议栈中的不同层次有不同的名称,比如在应用层一般叫做请求和响应数据;在传输层中一般叫做数据段;在网络层一般叫做数据报;在数据链路层一般叫做数据帧。所以同样都是二进制数据,在不同层有不同的名称。
TCP协议对应的数据段格式:
1.回答两个问题
1.TCP如何把数据段交付到上层应用层的?
- 当一台主机收到一个TCP数据段时,操作系统会检查数据报头中的”目的端口号“字段。
- 这个端口号对应着本地运行的某个应用程序(比如HTTP服务器的端口号为80)。
- 操作系统根据端口号,把数据交给对应的应用程序。
2.TCP如何把报头和有效载荷正确分离?
- 在报头中有一个”4位首部长度“字段,这个字段占4位,数字范围
0~15
(0000~1111
),单位是4字节(32位字),长度范围是0~60
字节,表示TCP头部的长度(标准报头+选项)。 - 例如,如果首部长度字段的值是5,表示TCP头部长度为
5*4=20
字节(这是没有选项的最小长度);如果首部长度的值是15,表示TCP头部长度为15*4=60
字节,其中标准报头的长度是20字节,选项长度是40字节。 - 操作系统读取这个字段,就知道从第几个字节开始是有效载荷(数据部分),从而把报头和有效载荷分开。
举例说明
- 首部长度字段是5(即20字节)
- 那么前20字节就是TCP头部,后面的就是应用层数据(比如HTTP请求内容)
2.窗口大小的作用
- TCP报头
客户端和服务器在基于TCP协议通信时,每次发送的数据包都会带有TCP报头。TCP报头中包含了如源端口号,目的端口,序列号,窗口大小等关键信息。
- 流量控制
流量控制是TCP协议中的一种机制,用来防止发送方发送数据过快,导致接收方来不及处理,最终造成数据丢失。
简单来说,就是让发送方的发送速度适应接收方的接收能力。
- 确认应答机制
TCP是面向连接的,可靠的传输协议。每当接收方收到数据后,会发送一个应答包,告诉发送方”我已经收到了你发来的数据“。
明白了上面的三点,就能理解TCP是如何实现流量控制的:
- 当发送方给接收方发送一个数据后,根据确认应答机制,接收方需要给发送方发送一个应答包;
- 因为双方是基于TCP协议通信的,所以接收方发送的应答包中也包含了TCP报头,其中就有一个16位窗口大小;
- 该窗口大小表示接收方当前还能接受多少字节的数据,也就是接收缓冲区的剩余空间;
- 发送方收到应答包后,根据报头中的窗口大小来调整自己的发送速率,从而避免发送过快导致接收方的缓冲区溢出;
- 而通信是双向的,所以双方在通信时,都会在窗口大小字段中填写自己接收缓冲区的剩余空间大小,已告知对方从而控制发送速率。
总结:
1.窗口大小字段就是用来做流量控制的,确保双方都不会因为对方发送过快而导致缓冲区溢出。
2.双向通信时,双方都要动态的告知对方自己的接受能力,这就是TCP流量控制的核心机制。
3.序号和确认序号的作用
1.TCP是面向字节流的协议
- 发送方把要发送的数据看作一个连续的字节流,每个字节都有一个唯一的序号。
- 发送方和接收方都以”字节“为单位进行编号和确认。
2.数据的发送方式
- 发送方在未收到确认的情况下,连续发送多个数据包(而不是”发一个等一个“)提高了传输效率。
- 发送方会为每个数据包分配一个”序号“,这个序号是本包第一个字节在整个字节流中的位置。
3.数据包可能会乱序到达
- 由于网络原因,数据包可能不是按顺序到达接收方;例如,先发送的包可能后到,后发的包可能先到。
4.接收方根据序号重排
- 接收方收到数据包后,会根据每个包的序号,把数据重新排列,保证最终交付给应用层的数据是有序的。
5.接收方发送应答包
- 接收方收到数据后,会发送一个应答包给发送方,告知”我已收到哪些数据,期待下一个字节的序号是多少“。
- 确认序号:表示”下一个期望收到的自己序号“,即”我已经收到你发来的所有数据,知道这个序号减一为止“。
6.累计确认
- TCP的应答包通常都是累计确认,即”我已经连续收到从起始序号到某个序号的所有数据“。
- 如果有数据丢失或乱序,应答包会停在缺失的哪个字节序号,直到缺失的数据被补齐。
7.超时重传机制
- 如果发送方在一定时间内没有收到某个数据的应答包,会自动重传未被确认的数据包,直到收到确认。
- 只要连接没有异常断开,所有的数据最终都会被确认和重传,保证可靠性。
举个例子:
假设发送发A,接收方B:
- A发送了序号为1000的数据包,长度为599字节(即1000~1499)
- B收到后,发送ACK应答包,确认序号为1500,表示我已经收到1000~1499,期待下一个是1500
- 如果A再发送1500~1999的数据包,B收到后再发ACK应答包为2000.
- 如果中间某个数据包丢了,B的ACK会停在缺失的那个序号,A没有收到ACK后,会重新发送,直到B收到并确认为止。
总结:
- 序号:标识每个数据包中的第一个字节在整个字节流中的位置,让接收方能够识别数据包的顺序,进行乱序重排。
- 确认序号:告诉发送方”我已经收到哪些数据,期待下一个字节的序号是多少“;让发送方知道哪些数据已经被可靠接收,那些还需要重传。
两者配合,共同实现了TCP的可靠,有序传输!
4.6个标志位的作用
先来理解什么是标志位?
在基于TCP协议通信时,发送的TCP报文有不同的用途,比如:建立连接,数据传输,断开连接等。可以理解为,TCP报文是有”类型“的,不同的类型,决定了该报文不同的用途。
但TCP协议本身没有专门的“类型字段”,而是通过报头中的6个标志位来区分报文的作用。
不同的标志位或他们的组合,决定了报文的具体用途;当接收方收到报文后,通过检查这些标志位,判断该报文应该如何处理。
所以标志位的存在,就是为了让接收方能够区分报文的不同作用,并据此做出相应的处理动作。
接下来就是详细介绍6个标志位的具体作用:
1.ACK
作用:
ACK
标志位用于指示该报文段中的确认序号字段是否是有效的。
- 确认收到数据:
当ACK标志位被置为1时,表示该报文段携带了一个有效的确认序号,用于告诉对方“我已经收到了你发来的数据,到某个字节为止”;(为0那就是无效)
- 保证数据可靠传输:
通过ACK机制,TCP实现了可靠的数据传输,发送方只有在收到对方的ACK确认后,才认为数据已经被可靠送达。
典型场景:
- 建立连接,三次握手中的第二次和第三次;
- 正常的数据传输,接收方需要发送带ACK标志位的报文(也就是应答包),告知对方哪些数据已经收到;
- 断开连接,四次挥手的过程中,通过互相发送ACK标志位的报文,确认双方的断开请求;
2.SYN
作用:
SYN
标志位用于TCP建立连接,发起和响应连接请求,并同步双方的初始序号,是三次握手的关键,带有SYN标志位的称为同步报文段。
- 第一次握手:
客户端发送SYN=1
的报文,表示请求建立连接,并告知自己的初始序号。
- 第二次握手:
服务端收到后,回复SYN=1
,ACK=1
的报文,表示同意连接,并告知自己的初始序号,同时确认客户端的序号。
- 第三次握手:
客户端收到后,回复ACK=1
的报文,连接建立完成。
SYN报文的特点:
SYN=1
时,通常不携带数据,只用于建立连接;- 只有在三次握手阶段,SYN标志位才会被置为1;
- SYN报文段的序号字段用于告诉对方本端的初始序号;
3.FIN
作用:
FIN
标志位用于TCP断开连接,表示发送方已经没有数据要发送了,请求关闭连接。
- 当一方发送带有FIN标志的报文时,表示“我已经没有数据要发送了,请求断开本次的连接”;
- 对方收到带有FIN标志的报文后,会发送ACK进行确认,并进入关闭流程,完成TCP的四次挥手断开连接。
4.PSH
作用:
PSH
标志位用于提示接收方“立即将数据交付给应用层”,而不是在缓冲区中等待更多数据。
- 加快数据传递速度:
让接收方“马上”把数据交给应用层,而不是等缓冲区满了再交付;
- 提升实时性和交互体验:
适用于对时效性要求高的应用,比如聊天软件,即时通讯,金融交易,在线游戏等,确保数据能够被接收方应用层第一时间处理。
5.RST
作用:
RST
标志位用于TCP重新建立连接,通常把携带RST标志位的报文称为“复位报文段。
应用场景分析:
- 在客户端与服务器三次握手建立连接时,当第三次握手客户端发送完ACK后,客户端就认为连接已经建立,可以开始发送数据了。
- 但是如果第三次ACK在网络中丢失或者延迟,服务器就会还停留在第二次握手完成阶段,认为此时连接还并没有建立。
- 这时,服务器如果先收到了客户端发来的数据包(不是ACK,而是应用数据),
- 服务器就会发现:当前并没有与该客户端建立连接完成,却受到了数据包。
- 按照TCP协议,服务器就会发送一个带有RST标志位的报文,告诉客户端”连接不存在或为建立,请重连“。
在这个应用场景中:
- 客户端误以为连接已经建立,提前发送数据,但服务器还没准备好。
- 服务器收到”无主“的数据包,只能用RST强制告知客户端”连接无效“。
6.URG
作用:
URG
标志位用于表示紧急指针是否有效。
紧急指针的作用:
- 紧急指针是TCP报头中的一个字段,只有当URG标志位为1时才有效。
- 用来指明报文中紧急数据地结束位置,让接收方知道哪些数据是”紧急的“,需要优先处理。
紧急指针的含义:
-
紧急指针不是紧急数据的字节序好,而是从当前序号开始,往后数多少字节是紧急数据。紧急指针的值+当前序号=紧急数据最后一个字节的序号+1。
-
例子:
当前TCP报文的序号是1000,紧急指针是5;那么这表示从1000开始的5个字节(1000~1004)是紧急数据,接收方需要优先处理这5个字节。
二.确认应答(ACK)机制
在讲解确认序号和ACK标志位时已经提到过确认应答机制是什么了,这里就不过多讲述。
三.超时重传机制
如果主机A发送数据给主机B之后,可能因为网络拥堵等原因,数据无法到达主机B;如果主机A在一个特定的数据间隔内没有收到哦B发来的确认应答,就会进行重发;
但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了;
因此主机B会受到很多重复数据,那么TCP协议需要能够识别出哪些数据包是重复的,并且把重复的丢弃掉,这时候就可以利用前面提到的序列号,来达到去重的效果。
那么,超时的时间如何确定?
- 最理想的情况下,找到一个最小的时间,保证”确认应答一定能在这个时间内返回“;
- 但是这个时间的长短,随着网络环境的不同,是有差异的;
- 如果时间设得太长,会影响整体的重传效率;
- 如果超时时间设得太短,会可能频繁发送重复的包。
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个超时时间。
- Linux中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
- 如果重发一次后,仍然得不到应答,等待2*500ms后再进行重传。
- 如果仍然得不到应答,等待4*500ms进行重传,以此类推,以指数形式递增;
- 累积到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
四.连接管理机制
在正常的情况下,TCP要经过三次握手建立连接,四次挥手断开连接。
服务端状态转化:
CLOSED->LISTEN
:服务端调用listen函数后进入LISTEN
状态,开始监听,等待客户端连接;LISTEN->SYN_RCVD
:服务端收到客户端发送的连接请求(SYN报文)后,就将该连接放入到内核半连接队列中,SYN_RCVD->ESTABLISHED
:服务端一旦收到客户端的确认报文后,就进入ESTABLISHED状态,连接从半连接队列转移到全连接队列,然后等待应用层调用accep函数从全连接队列中获取连接,进行后续读写数据。ESTABLISHED->CLOSE_WAIT
:当客户端主动关闭连接(调用close),服务器返回确认报文并进入CLOSE_WAIT状态;CLOSE_wAIT->LAST_ACK
:进入CLOSE_WAIT后说明服务器关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LSAT_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN);LAST_ACK->CLOSED
:服务器收到了对FIN的ACK,彻底关闭连接。
客户端状态变化:
CLOSED->SYN_SENT
:客户端调用connect,发送同步报文段;SYN_SEN->ESTABLISHED
:客户端收到服务器的确认和连接请求,发送确认报文段;connect调用成功,进入ESTABLISHED状态,开始读写数据;ESTABLISHED->FIN_WAIT_1
:客户端主动调用close时,向服务器发送结束报文段FIN,同时进入FIN_WAIT1;FIN_WAIT_1->FIN_WAIT_2
:客户端收到服务器对结束报文段的确认ACK,则进入FIN_WAIT_2,开始等待服务器的结束报文段;FIN_WAIT_2->TIME_WAIT
:客户端收到服务器发来的结束报文段FIN,进入TIME_WAIT,并发出最后一个确认报文段ACK;TIME_WAIT->CLOSED
:客户端要等待2MSL
的时间,才会进入CLOSED状态。
建立连接时的细节点:
- 建立连接三次握手是由操作系统自动完成的,不依赖于应用层是否调用
accept
函数!!! - 服务端调用
listen
后,内核会维护一个半连接队列(SYN队列
)和全连接队列(accept队列
)。- 半连接队列存放那些已经收到客户端SYN,服务端回复了SYN+ACK,但还没有收到客户端最后ACK的连接(即服务端处于SYN_RECV状态)。
- 全连接队列存放那些三次握手已经全部成功(服务端收到客户端ACK,状态变为ESTABLISHED),等待应用层调用
accept
函数从该队列中获取连接。 - 连接建立时,先进入半连接队列,收到ACK后转到全连接队列。
- 全连接队列的长度和listen函数的第二个参数backlog有关。
backlog+1
指定了全连接队列的最大长度(也就是最多允许存放的连接个数)。- 如果全连接队列已满,新的已完成三次握手的连接会被丢弃或拒绝,客户端可能收到RST或重试。
- 实际上最大长度可能还受操作系统内核参数限制。
- 连接的超时与清理
- 如果服务端长时间收不到客户端的ACK(比如客户端网络异常,恶意攻击等),这个连接会一直停留在半连接队列。
- 操作系统会有超时机制;如果超时还没收到ACK,内核会自动清理这个半连接,释放资源,防止服务端资源被无效连接占用。
经典问题1:为什么全连接队列不能设置太长,也不能设置太短?
设置太长的后果:
资源占用增加:
队列太长,内核需要为每个连接分配内存和管理结构,会消耗更过系统资源(内存,CPU);
延迟增加:
如果应用层处理不过来,连接在队列中等待时间就会变长,客户端感知到相应变慢,影响体验;
隐藏问题:
队列太长可能掩盖了应用层处理能力不足的问题,导致问题积压,最终可能引发更严重的故障(如内存耗尽,进程崩溃);
被攻击风险增加:
队列过长,攻击者可以更容易发起SYN洪水等攻击,占满队列,拖垮服务器。
设置太短的后果:
易导致连接丢失:
如果队列太短,短时间内有大量客户端同时连接,队列很快就会被占满。此时新完成三次握手的连接会被丢弃或拒绝,客户端可能收到RST或连接超时,用于体验变差;
抗突发能力差:
在高并发场景下,服务端无法承受瞬时的连接高峰,容易出现“假死”或拒绝服务;
资源利用不充分:
服务器明明还有处理能力,但因为队列太短,连接被提前拒绝,浪费了服务器资源。
最后结论:全连接队列的长度不能太长也不能太短,要结合实际业务和服务器能力,动态调优。
断开连接时的细节点:
- TIME_WAIT状态的本质
- TIME_WAIT状态是主动关闭连接(即最后一个发送ACK的一方)在完成四次挥手后进入的状态。
- 在TIME_WATI期间,该连接的IP地址和端口号(四元组)依然被占用,不能被新的相同四元组的连接复用。
- TIME_WATI的持续时间通常是2倍的
MSL
(报文在网络中的最大生存时间),在Linux下默认是60秒。
- 服务器主动断开带来的影响
- 如果服务器主动关闭连接,就会进入TIME_WAIT状态。
- 在等待的期间,相同的IP地址和端口号不能被立即复用,这会导致服务器崩溃后立即重启,绑定同样的IP和端口号会失败,提示
Address already in use
。 - 解决方法:可以通过
setsockopt
函数设置套接字端口复用选项SO_REUSEADDR
int opt=1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
经典问题2:为什么断开连接时需要TIME_WAIT状态,并且时间是2倍的MSL?
TIME_WAIT状态的主要作用有两个:
1.保证”被动关闭方“能收到最后的ACK
- 在TCP四次挥手中,主动关闭方发送最后一个ACK后,连接就进入TIME_WAIT状态;
- 如果这个ACK在网络中丢失,被动关闭方(即最后发送FIN的一方)会重发FIN;
- 如果没有TIME_WAIT,主动关闭方已经释放资源,无法再响应这个FIN,导致被动关闭方无法正常关闭;
- 有了TIME_WAIT,主动关闭方可以在这段时间内重发ACK,确保双方能正常关闭;
2.防止”旧连接的数据包“影响新连接
- 如果刚关闭连接,马上又建立了相同四元组的新连接,网络中延迟的旧数据包可能会被新连接接收到,造成数据混乱;
- TIME_WAIT保证这段时间内,所有属于旧连接的报文都从网络中消失,避免影响新连接;
为什么是2倍的MSL?
- MSL是一个TCP报文在网络中可能存在的最长时间。
- 第一个MSL确保本方发送的最后一个ACK能在网络中“活够”一圈,即使对方没收到ACK重发FIN,本方还能重发ACK;
- 第二个MSL确保网络中所有属于这个连接的旧报文都消失,避免影响后续新连接;
举例说明:
- 假设主动关闭发送ACK后,ACK在网络中丢失,被动关闭方重发FIN,主动关闭方还在TIME_WAIT,可以再次发送ACK;
- 如果只等1倍的MSL,可能有些报文还在网络中漂流,2倍MSL可以确保通信双方历史数据都得以消散;
总结:
- TIME_WAIT的存在是为了保证TCP连接的可靠性和安全性;
- 2倍的MSL即保证了最后的ACK的可靠连接,也防止了旧数据包影响新连接;
经典问题3:为什么建立连接时要三次握手,断开连接时要四次挥手?
1.三次握手的根本目的:
- 验证双方的收发能力,并同步初始序号,确保客户端和服务端都能正常”发“和”收“,实现TCP的全双工特性。
- 防止服务端资源被无效连接占用,提升安全性和健壮性。
2.不能一次或两次握手的原因:
- 一次握手:
只能保证客户端能发,服务端能收,无法确认服务端能发,客户端能收。
- 两次握手:
1.”半连接队列“问题
- 如果只用两次握手,服务端收到SYN后立即分配资源并进入”已连接“状态,等待客户端数据;
- 但客户端可能因为网络异常,恶意攻击等原因,根本不会正常发送数据,导致服务端白白维护了一个”无效连接“。
- 服务端通常要同时服务大量客户端,如果有大量这样的”半连接“,会极大消耗服务器资源,甚至被恶意利用。
2.三次握手让”连接建立的最终确认权“交给客户端
- 三次握手的第三步,客户端收到服务端的SYN+ACK后,只有客户端再发出ACK,服务端才能认为连接真正建立。
- 这样,如果客户端不发最后的ACK,服务端会在超时后自动释放资源,不会一直维护无效连接。
- 这实际上把”维护错误连接的成本“转移给了客户端,保护了服务器。
总结:
三次握手的设计是基于”双方能力确认+资源分配安全“这两个核心目标,三次是最小且足够的次数。
三次握手既保证了双方收发能力的验证,也把维护错误连接的成本交给了客户端,保护了服务器资源,是最小且合理的设计。
1.四次挥手的根本目的:
- 确保双方都能把剩余数据传输完,并且双方都同意断开。
2.为什么不能只用两次:
- TCP是全双工工的,双方都可以独立的发送和接收数据。
- 关闭连接时,每个方向都要单独关闭,即A->B和B->A都要各自确认。
- 如果只使用两次挥手,可能会导致一方的数据还没发完,连接就关闭了,造成数据丢失。
以上就是关于传输层TCP协议第一部分的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!