传输层协议TCP
TCP协议是传输层的另一个知名协议,它是面向连接、可靠的、面向字节流的一种传输层协议。TCP协议在操作系统内部,通过确认应答、超时重传、流量控制、三次握手、四次挥手等机制,解决了在网络通信时的可靠性、效率等问题。
一.TCP报头
协议必谈的两个话题:1.如何解包; 2.如何分用
分用很好理解,与UDP协议一样,TCP协议报头也包含了16位的目的端口号,可以将有效载荷交给应用层的具体一个进程。
解包,就是将报头和有效载荷分离,UDP那里,UDP报头的长度是固定的,且报头内部还有一个16位报头长度,总长度减去报头长度,就可以拿到有效载荷。而在TCP这里,也有相对应的字段来使报头和有效载荷分离——4位首部长度。
1.4位首部长度
4位首部长度,也就意味着该字段只有4个比特位,范围就是[0,15]。这显然是不合理,因为不含选项,报头就有20字节的固定长度。所以,这里的4位首部长度是有单位的,以4字节作为基本单位。所以,4位首部长度所能表示的范围就是[0,60],这些都是tcp报头的长度,不含有效载荷。
但因为报头基本长度就为20字节,所以真正的范围应该从20~60,也就是说,4字节首部长度5~15.这里也就间接表明,选项这个可选字段的长度范围是0~40字节。
需要注意的是,tcp报头长度必须是4的整数倍。
综上,我们有了4位首部长度,就可以读取整个tcp报头,然后tcp要进行有效载荷的分离,还需要IP协议的帮助:IP协议报头中包含IP数据报的总长度,以及IP报头长度,两者相减即得tcp报文长度。再根据4位首部长度,即可得到有效载荷。
tcp报文有效载荷 = IP数据报总长度 - IP报头长度 - tcp报头长度
2.序号与确认序号
tcp协议是如何保证可靠性的?其实是通过确认应答机制实现的。
以客户端向服务端发送数据为例:client向server发送数据,client无法知道对方是否收到了数据。所以,server收到数据之后,给client发送一个应答,表示我收到了你的数据。当client收到该应答时,就能100%保证,自己上一次发送的数据被对方收到了。
所以,具有应答,可以保证对历史消息的可靠性(只要收到应答,就可以保证对应的数据对方收到了)。
在通信历程中,最新一条报文是没有的应答的,这也就导致最新的报文可靠性无法保证。
所以,为了保证可靠性,我们只需要让报文不是最新的即可,即确认应答机制。所以,保证tcp可靠性的最核心机制:确认应答机制。
在一般情况下,client给server发,server也要给client发,所以方法都要对对方发送的报文进行应答。这样就能够保证双方的可靠性。但是不能对应答做应答。如果对方发送的应答,你也要做出应答的话,这不就套娃了。这个应答只是表明我收到了你的报文。
在更一般的情况下,client可能同时向server发送多条报文,为了确保报文的可靠性,那么就要对所有的报文做出应答。但如果此时出现了丢包,我们怎么确认这些应答对应的是那个报文呢?我们怎么确认要重发那个报文呢?
我们要有一个明确的观念在脑海中,那就是发送给对方的都是一个tcp报文,可能没有有效载荷,但最少都是一个tcp报头!!! 所以,报头中就应该有字段来解决以上问题——序号和确认序号。
在我们发送报文的时候,要在tcp报头中添加序号,这样就能区分开发送出去的报文。而确认应答时也要带上确认序号,确认序号 = 序号 + 1。这样就能知道该应答对应的是哪个报文了。
确认序号除了表明确认序号 - 1报文对方收到了,同时还表明该序号之间的所有报文对方都收到了。例如:client发送4个报文序列号分别为100、200、300、400,如果4个报文都收到了,那么client就会收到101、201、301、401这四个确认应答,只要收到401就表明401之间所有的报文都收到了。如果100、400对方收到了,200、300没收到,此时发送的确认应答报文中的确认序号就是两个101.
server接收到多个报文,这些报文到达的顺序可能与发出的顺序不一致,如果不处理,就会导致应用层拿到的数据是乱的,这也是不可靠的一种表现。所以,这些乱序的报文会暂存在接收缓冲区中,tcp协议根据报文中的序号可以对收到的报文进行排序,来使接收到底报文顺序与发送报文的顺序一致。
那按照上面的来说,感觉只使用一个序列号就可以了啊。为什么有一个序列号,还得有一个确认序号呢?
那我问你,我发送应答的时候可不可以携带着自己的数据呢?回答我!很显然,这是可以的。这种机制叫做捎带应答。
那么此时的应答表明我收到了对方的消息,那么我发送的数据对方是否要应答呢?肯定是要的。既然要应答,那就得知道数据对应的序列号是多少。
所以,结果很显然了,对于捎带应答来说,既要对对方发送的数据做出应答,又要保证自己的报文也要有序列号,以便对方做出应答。所以,序列号和确认序列号都是必不可少的。
3.16位窗口大小
tcp发送数据的本质就是将发送缓冲区中的内容拷贝到对方的接收缓冲区中。那么如果对方的介绍缓冲区已经满了,可我此时却不知道,仍然发送了大量的报文。此时对方收到了报文,但介于接收缓冲区已经满了,无法接收报文,所以,此时到的报文只能被丢弃。
这个过程中,数据的发送消耗了电力、网络资源等,并且因为没有收到,所以发送端会一直重传,导致新数据迟迟不能发送出去。但最后报文却因为没有空间而导致丢弃,这无疑是浪费资源的,低效的。
所以,我们在发送报文的时候得知道对方接收缓冲区的剩余容量,以此来做到按需发送,这个过程叫做流量控制!!!
我们既然得知道对方接收缓冲区的剩余空间,那么在发送报文的时候,就得将自己的接收缓冲区的剩余大小带上,发送出去。
我们需要理解,流量控制是为了使发送和接收报文变得合理,不是说只能让发送变慢,如果此时缓冲区大小还很多,此时发送的速率就会变快。
4.6个标志位
客户端和服务端再使用tcp协议进行通信的时候,互相发送的报文是有类型的。有的报文是用来建立连接的,有的是用来发送数据的,有的是用来应答的,有的则是用来关闭连接的,并且同一时间可能存在多个不同类型的报文,所以在操作系统内部就要对这些报文进行管理——先描述再组织。其实就是借助tcp报头结构体来表示的。
struct tcphdr {__be16 source; // 源端口号__be16 dest; // 目的端口号__be32 seq; // 序列号__be32 ack_seq; // 确认号
#if defined(__LITTLE_ENDIAN_BITFIELD)__u16 res1:4, // 保留字段doff:4, // 数据偏移,即 TCP 首部长度fin:1, // FIN 标志位syn:1, // SYN 标志位rst:1, // RST 标志位psh:1, // PSH 标志位ack:1, // ACK 标志位urg:1, // URG 标志位res2:2; // 保留字段
#elif defined(__BIG_ENDIAN_BITFIELD)__u16 doff:4, // 数据偏移,即 TCP 首部长度res1:4, // 保留字段fin:1, // FIN 标志位syn:1, // SYN 标志位rst:1, // RST 标志位psh:1, // PSH 标志位ack:1, // ACK 标志位urg:1, // URG 标志位res2:2; // 保留字段
#else__u16 res1, // 保留字段res2; // 保留字段__u16 doff; // 数据偏移,即 TCP 首部长度__u16 fin:1, // FIN 标志位syn:1, // SYN 标志位rst:1, // RST 标志位psh:1, // PSH 标志位ack:1, // ACK 标志位urg:1, // URG 标志位;res2:2; // 保留字段
在tcp结构体中,有6个标志位,分别用来表示不同的报文类型,而它们是用位段来实现的。位段就是一种可能将比特位细化使用的一种方式。我们可以看到,这6个表示位每个都只占了一个bit位,所以,要表示不同类型的报文,只需要将对应的bit位置为1即可。
0x1.SYN、ACK
客户端和服务端在进行通信之前,首先要进行连接。而tcp协议中,获取连接使用的是三次握手机制。
当客户端使用connect向服务端发起连接时,此时发送给服务端的就是SYN(Synchronize,同步)报文;当服务端收到SYN报文之后,就知道是客户端想要和我连接了,此时服务端要对该报文进行应答,同时也要和客户端进行连接,所以服务端就要想客户端发送SYN + ACK报文;客户端收到其实是服务端发送的捎带应答,所以也要对其做出应答。当服务端收到应答之后,表明三次握手成功。
所以,SYN标志位就表示这是一个发起连接的报文,ACK标志位就表示这是一个应答报文。
在三次握手中,客户端形成三次握手和服务端形成三次握手的时机是不同。客户端在发送ack报文就形成了,服务端则要在收到ack后。。。
所以这也就导致,前两次握手是不可以携带数据的,因为三次握手并没有完成,连接并没有成功。但客户端在发送ack的时候是可以捎带应答的,因为当客户端发送ack时就已经完成了握手。
同时,在流量控制那里,我们说可以通过16位窗口大小来得知对方的缓冲区剩余空间,但如果在刚连接上就发送大量数据呢?此时并不知道对方的窗口大小啊?其实有了三次握手,这个问题也就解决了。在前两次握手的时候,其实就是交换双方缓冲区大小的过程,因为前两次无法携带数据,也就不会有发送大量数据的情况了。就算客户端在第三次做了捎带应答,也没事,因为已经知道窗口大小了。
0x2.FIN
通信前需要三次握手建立连接,而当CS不想通信的时候,需要采取四次挥手来关闭连接。
FIN标志位就意味着:通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段。
0x3.RST
建立连接一定会成功么?三次握手本质上是确认双方的通信条件于意愿。如果有一方不满足条件,连接就会建立失败。
我们上面说过,客户端和服务端完成三次握手的时机是不同的。如果服务端没有收到客户端发送的ack,就会导致,服务端->客户端的连接没有建立成功。但客户端并不知情,此时如果直接给服务端发送报文,服务端就会有一个大大的问号? 你给我发报文干嘛?咱两没有建立连接啊!!
此时,服务端就会向客户端发送RST,来重置连接。
RST:对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段;通信过程中,遇到任何异常都可以进行连接重置。
0x4.URG
URG标志位用来表明紧急指针是否有效。该标志位是和16位紧急指针搭配使用的。
那么16位紧急指针是干嘛的?紧急指针其实是一个偏移量,用来表示有效载荷中紧急数据的结束位置。即该报文的序号+紧急指针值就可以知道该紧急数据结束位置的序号。紧急数据通常只有1个字节,通常是一个状态码。
紧急指针可以理解为优先级快递。当TCP报头设置了URG标志位时,接收方需立即处理紧急数据,将紧急数据从正常数据流中分离,立即传递给应用层。
应用场景:比如说我们在上传文件,当我们需要紧急让所有的上传终止,此时就可以使用紧急指针,因为这个操作需要立即被执行。如果没有紧急指针,该紧急数据与正常数据一样,被正常处理,那只能等上传结束了,才能处理!!!
带外数据(out of band data):通过紧急指针机制传输的少量特殊数据,独立于正常数据流。正常数据按序存入接收缓冲区,而带外数据可 “插队” 直接送达应用层
// 发送1字节紧急数据(推荐)
char urgent_byte = 0x01;
send(sockfd, &urgent_byte, 1, MSG_OOB);MSG_OOBSends out-of-band data on sockets that support this notion (e.g., of type SOCK_STREAM); the underlying protocol must also support out-of-band data.
0x5.PSH
PSH:提示接收端应用程序立刻从 TCP 缓冲区把数据读走
当PSH标志位设为1之后,接收端收到该报文后,要执行以下操作:
- 将该段数据及之前缓存的所有数据立即递交给应用层。
- 清空接收缓冲区中已递交的数据。
需要注意的是,PSH 标志仅影响接收方的交付时机,不强制发送方立即发送。
二.确认应答机制
确认应答机制是用来维护tcp协议的可靠性的。通过确认应答,我们可以保证历史消息的可靠性。而在确认应答机制里面最重要的就是序号和确认序号了。
序号与确认序号:
我们可以将tcp缓冲区看作一个巨大的一维char类型的数组:char buffer[ N ];而序号其实就是对应位置的下标。当我们发送报文的时候,该报文的序号就是这段报文的第一元素的下标。而确认序号就是这端区间中最后一个元素的下标+1.
例:要发送的报文序号为1000,报文长度为200,所以对方收到的报文中的序号就为1000,表明对方已经接收了1000~1199这200个字节,此时对方返回给我的确认序号就是最后一个字节+1 = 1200.表示我希望接收从这个序号开始的数据。
在通信之前,进行三次握手的时候,两台主机会进行协商,随机生成开始序号。连接建立时随机生成(RFC 6528 建议使用 4 字节随机数),防止历史数据干扰。
(注意:tcp缓冲区中的数据是没有报头的!!!)
三.超时重传机制
理解超时重传机制,我们就得先理解何为丢包?
发送方没有收到ACK能代表丢包么? 不能!没有收到ACK有两中情况:1.数据真的丢了;2.ACK丢了。
当发送方没有收到ACK时,并不能直接表明丢包了,因为有可能只是应答丢了,如果此时立刻重传的化,就会导致对方收到重复的报文。所以在没有收到ACK时,我们不应该直接重传,而是等待一段时间,确认应答确实没有来,此时再进行重传。这样就能大大避免发送重复报文的几率。
当然,我们这里不用担心重复报文的问题,因为每一份报文都是有序号的,如果对方收到了报文,我们此时再发送重复的报文,对方检测到报文序号已经出现过了,此时就会直接丢弃。
所以,超时重传的触发机制必须满足:未收到应答 && 超时了,才会触发重传。
那么这个等待特定的时间间隔是多少呢?
最理想的情况下,找到一个最小的时间,保证“确认应答一定能在这个时间内返回”,这个时间的长短,随着网络环境的不同,是由差异的。如果超时时间太长,会影响整体的重传效率;如果超时时间太短,有可能导致频繁发送重复的报文。
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大的超时时间:
- Linux中(BSD Unix 和 Windows 也是如此),超时时间以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
- 如果重发一次之后,仍然得不到应答,等地2*500ms后再进行重传
- 如果仍然得不到应答,等待4*500ms进行重传,以此类推。
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
四.连接管理机制
对于一台主机来说,在同一时间内可能存在了多条连接,每条连接都指向不同的服务端。所以在操作系统内部,就要对所有的连接管理起来——先描述再组织。所以,在三次握手建立连接的时候需要在操作系统内核创建内核数据结构,这个过程是需要花费时间和空间的。那么也就是说TCP建立连接是有成本的。
我们注意上图,每一个阶段服务器和客户端间的状态变化。而这个状态其实就是整数。客户端调用connect来向目的ip和目的端口发起三次握手,服务端调用accept来获取一个已经处于连接状态的连接。总之,connect和accept并不参加三次握手,三次握手是由客户端和服务器的操作系统自动协商完成的。
1.三次握手
三次握手是两台主机要进行通信之前的准备工作,即建立连接。那为什么要进行三次握手呢?
三次握手本质上是借助最短路径来验证双方的通信意愿,并且验证双方全双工是否准备就绪(网络是否满足,是否通畅)。
三次握手本质上其实是4次握手?因为第二次握手其实上是一次捎带应答!!!因为服务器一般要对客户端发来的连接请求无脑同意,所以可以将SYN和ACK做捎带应答。
如果服务器先发送 ACK 再发送 SYN,可能在中间状态(已确认客户端 SYN 但未发送自身 SYN)创建临时连接,增加服务器负担。
TCP 三次握手的本质是通过最小化的通信次数完成双向连接的建立。第二次握手的 “捎带应答” 是协议设计的关键优化,而非 “可以选择不合并” 的选项。如果强行拆分,不仅会降低效率,还会引入额外的复杂性。
理解这种设计取舍,有助于我们把握网络协议设计的核心原则:在可靠性和效率之间寻找平衡点。
2.四次挥手
三次握手需要建立双方通信意愿的共识,而四次挥手就是要建立双方断开连接的共识。
- 第一次挥手:客户端向服务器发送FIN报文,表明自己要断开连接,此时客户端处于FIN_WAIT_1状态。这里的 FIN 报文表示客户端已经没有数据要发送给服务器了,但此时客户端仍可以接收服务器发送的数据。
- 第二次挥手:服务端收到客户端发送的FIN报文,知道了对方要关闭连接,此时发送ACK告知客户端我收到了。服务器端进入 CLOSE_WAIT 状态 。这表明服务器已经收到客户端的关闭请求,但服务器可能还有数据未发送完,所以先不急于关闭连接,而是先确认收到客户端的请求。
- 第三次挥手:服务端收到了FIN报文,并且已发送ACK告知对方。自己的数据也已经发送完毕了,此时也向客户端发送FIN报文,表示我也要和你断开连接,此时服务端处于LAST_ACK状态。
- 第四次挥手:客户端收到服务端的FIN后,要进行确认应答,告知对方我收到了。此时客户端进入TIME_WAIT状态。当服务端收到ACK报文后,进入CLOSE状态,完成四次挥手。客户端在 TIME_WAIT 状态等待 2 倍的最长报文段寿命(2MSL)后也进入 CLOSED 状态,至此连接彻底断开。
客户端调用close关闭连接时,此时就会进入四次挥手,在前两次挥手,客户端已经将自己的文件描述符关闭了,他怎么还能收到服务端发送的FIN报文呢?
客户端调用close关闭文件描述符并不等于TCP连接直接断开。
在应用层:
- 客户端调用
close(sockfd)
,通知操作系统「我不再发送数据」。- 文件描述符被标记为「已关闭」,应用层无法再通过该描述符发送或接收数据。
在tcp协议栈:
- 内核发送 FIN 包给服务器,表示「客户端到服务器的发送方向已关闭」(半关闭)。
- 接收方向仍保持打开,内核继续接收服务器发送的数据,并将其缓存到接收缓冲区。
NAMEshutdown - shut down part of a full-duplex connectionSYNOPSIS#include <sys/socket.h>int shutdown(int sockfd, int how);DESCRIPTIONThe shutdown() call causes all or part of a full-duplex connection on the socket associated with sockfd to be shut down. If how is SHUT_RD, further receptions will be disal‐lowed. If how is SHUT_WR, further transmissions will be disallowed. If how is SHUT_RDWR, further receptions and transmissions will be disallowed.
当服务端收到FIN后,发送ack之后,处于close_wait状态缺不主动调用close()会发生什么?
CLOSE_WAIT状态的含义:表示服务端已收到客户端的关闭请求,但尚未完成自身的数据发送任务,需要等待应用层处理完毕后主动调用
close()
发送 FIN 包(第三次挥手)。如果服务端已经将数据发送完毕了,但仍没有调用close,就会导致文件描述符没有释放,导致文件描述符泄露。
内核资源占用:
- 服务端的 TCP 协议栈会持续维护该连接的状态(如接收缓冲区、发送缓冲区、定时器等),占用内核内存、文件描述符等资源。
- 若大量连接堆积在 CLOSE_WAIT 状态,可能导致:
- 文件描述符耗尽(Linux 默认单个进程最大文件描述符数通常为 1024 或更高,但高并发场景下易耗尽)。
- 内存占用过高,影响其他服务运行
3.TIME_WAIT状态
主动关闭连接的一方,在收到对方的FIN报文 并 发送ack报文之后,会进入TIME_WAIT状态。虽然此时客户端发出ack后已经完成了四次挥手,但仍不能处于close状态,要保持TIME_WAIT状态一段时间。这个时间一般都是2个MSL时间(Maximum Segment Lifetime,报文最大生存时间)Linux默认msl = 60s。
为什么要有TIME_WAIT状态呢?
1.确保最后一个ACK包被对方收到。
若主动方发送的最后一个 ACK 包丢失,被动方(服务器)会重发 FIN 包。
TIME_WAIT 状态允许主动方接收这个重发的 FIN 包并再次发送 ACK,避免被动方因未收到 ACK 而无法关闭连接。2.防止旧连接的延迟数据包干扰新连接
因为数据在网络中选择的路由路径不同,网络中可能会存在一些延迟数据包,但这些数据包是属于之间旧连接的。若这些数据包在新连接建立后到达,可能被误认为是新连接的数据。TIME_WAIT 的 2MSL 等待时间确保所有旧连接的数据包都已在网络中消亡,不会影响后续相同四元组(源 IP、源端口、目的 IP、目的端口)的新连接。
设计 TIME_WAIT 的核心目的是确保 TCP 连接可靠关闭,并避免新旧连接混淆。
4.TIME_WAIT状态导致bind失败
当一个连接进入TIME_WAIT状态,操作系统会在 2MSL 时间内保留该连接的四元组(源 IP、源端口、目的 IP、目的端口),以防止旧连接的延迟数据包干扰新连接。
在这期间,相同四元组的新连接无法立即建立,因为系统认为该端口仍处于 “使用中”。
但如果就是想要利用原四元组立即启动,可以设置端口复用,来绕过TIME_WAIT.
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int optval = 1;
// 设置SO_REUSEADDR选项
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
五.滑动窗口
滑动窗口其实是发送缓冲区的一部分,用来指定可以发送的并且无需立即应答的数据的最大值。当我们将发送缓冲区当作一个一维数组,那么该滑动窗口其实是用两个指针strat ,end来维护起来。
滑动窗口左侧是已发送,并且已经确认的数据,也就是说这块空间是可以直接被覆盖使用的。
滑动窗口右侧是未发送/待发送的数据,或者是没有使用的空间。
滑动窗口可以理解为对方可以直接接受的最大的数据大小。未来当滑动窗口有数据被发送并且被对方收到时,滑动窗口就要向右滑动。本质上就是start和end++。
滑动窗口的的数据是可以直接发送的,最大就是对方当前的接收能力,所以滑动窗口 = 对方的接受能力(对方报文中的窗口大小)。因为在一维数组中每一个元素都有其下标,也就是序号。所以,start的更新原则就是对方的确认序号,而end = start + 窗口大小。
滑动窗口实际上是流量控制机制的一个具体实现方案。
首先,滑动窗口是不会向左滑动的,因为确认序号是一致递增的。
其次,滑动窗口是可以动态变化的,根据对方的窗口大小,进行灵活变化。
滑动窗口出现了丢包——最左侧丢包,中间丢包,最右侧丢包。
如果是最左侧丢包:
1.如果最左侧报文真的丢了,此时其余报文的ACK的确认序号都是左侧报文的序号。即4个报文分别为1001,2001,3001,4001。左侧报文丢失,2001、3001、4001的ACK报文的确认序号都是1001,即滑动窗口不会移动。当对方收到三个重复的确认序号后,就会发觉,至少确认序号的下一个报文丢失了,此时就会进行快重传,重发1001报文。
2.如果只是ACK丢了,这并不要紧,因为可以根据其他的ACK来确认收到了多少个报文。
而我们的中间丢失和最右侧丢失都可以转化为最左侧丢失。
快重传 VS 超时重传:快重传是提高效率的表现,超时重传是为快重传做兜底的。如果只发送了两个报文,就算第一个报文丢了,也不会收到三个重复的ACK,此时只能够超时重传。
有了上面丢包的理解,如果tcp报文发出,但还没有收到应答,此时就应该保存在滑动窗口中,来等待超时重传/快重传。所以,再来理解滑动窗口,发送了的,暂时不需要应答的数据。
滑动窗口一直向右移动,会不会溢出呢?答案是不会的,它会进行回绕。我们可以将发送缓冲区当作一个环形结构,滑动窗口一直在里面循环。
六.流量控制
接收缓冲区可以将自己的剩余空间大小通过16为窗口大小交给对方。对方根据窗口大小来控制发送数据的快/满,多/少。
也就是说窗口的总大小就是65535字节,我们可以设置选项中的扩大因子M,来使窗口大小左移M位。
在三次握手的时候,会进行第一次互换窗口大小,不至于第三次握手发送大量数据。
当接收方的接收缓冲区已满时,会向发送方返回一个窗口大小为 0 的 ACK 报文,告知发送方暂停发送数据(称为流量控制)。此时发送方进入窗口关闭状态,停止发送数据并启动坚持计时器(Persist Timer)。
窗口探测:
当坚持计时器超时,发送端会发送窗口探测报文,并按指数退避规则延长计时器(如 5 秒→6 秒→12 秒→24 秒…,最大值通常为 60 秒)。若连续多次探测失败,则发送方会断开连接。
窗口更新通知:
当接收方的接收缓冲区有空闲空间(窗口打开)时,主动向发送方发送窗口更新报文(ACK 报文,窗口大小 > 0),告知发送方可以继续发送数据。TCP 协议规定,只有当窗口打开的大小超过一个阈值(通常为 MSS,最大段大小)时,接收方才会主动发送窗口更新通知,避免因微小窗口变化导致频繁通知(称为窗口扩大避免机制)。
七.拥塞控制
如果在某个时间,突然出现大面积的丢包情况,首先是在硬件健康的情况下,tcp协议就会认为此时出现了网络拥塞。如果此刻立即重发,可能会加重网络拥塞的情况。
TCP引入了慢启动,先发送少量数据来探探路,看看网络是否恢复,再决定按多大速度发送数据。
这里引入一个拥塞窗口的概念,表示当前网络拥塞的阈值,小于拥塞窗口网络正常,大于该拥塞窗口网络会拥塞。我们看到拥塞窗口并不是一直按照指数级增长的,增长一段时间后(拥塞窗口大于阈值),就会变为线性增长。
指数级增长阶段:慢慢试探,为了快速恢复网络状态;
线性增长阶段:探索新的网络拥塞阈值。
所以,我们就得更新一组概念了,滑动窗口 = min(对方窗口,拥塞窗口)。
因为我们不仅要考虑网络因素,还要考虑对方的接收能力。
注意,当对方窗口大小,小于拥塞窗口时,此时发送的数据大小就是对方窗口,但此时拥塞窗口依旧在增大,在试探网络拥塞的最大值。当试探到新的网络拥塞值时,新的阈值 = 新的网络拥塞值/2.
八.延迟应答
延迟应答(Delayed ACK)是 TCP 协议中的一种优化机制,用于减少 ACK 报文的发送数量,从而提高网络效率。其核心思想是:接收方不必立即对每个数据段发送 ACK,而是延迟一段时间,等待可能到来的更多数据段,然后一次性确认多个数据段。
延迟应答,有可能会让接收方更新出一个更大的窗口,提高发送方的发送效率。同时也可能让接受发缩小窗口,避免让发送方发送过多的数据。
延迟应答是有要求的:要么每个2个包就应答一次。要么超过超时时间就应答一次,一般超时时间取200ms。
九.TCP异常情况
当进程终止或者机器重启,此时都相当于进程结束,而文件描述符的生命周期随进程,进程结束,系统就会关闭文件描述符,此时就会在底层发送FIN报文,进行四次挥手断开连接。
当客户端的机器突然掉电,或者突然断网了,此时客户端到服务器的连接就已经断开了。但是服务端到客户端方向的连接来还没有断开,此时 服务端依旧可以向客户端发送数据。
如果客户端长时间没有应答,服务端重传几次后,就会任务对方已经掉线了,主动断开连接。如果客户端断网又重连了,此时服务端发送数据,客户端收到之后就会进行RST,重置连接,因为客户端此时还没有进行三次握手。
所以,我们并不用担心,旧的连接无论如何都是会被释放掉的!!!
另外,tcp协议自带了保活机制,如果客户端与服务器建立号连接之后,一直处于不活跃状态,没有数据的交互,此时无法tcp无法得知连接是否有效,所以,tcp自带的保活机制,通常是大几十分钟的,来判断连接是否有效。