41.传输层协议TCP(2)
文章目录
传送门:【Linux笔记】网络部分------传输层协议TCP(1)

连接管理机制
TCP的链接管理机制,涉及三次握手和四次挥手。在讲解这些机制之前,需要先介绍TCP报头中的三个标志位。标志位的存在是为了区分不同类型的TCP报文,因为服务器端可能同时收到来自多个客户端的请求,包括建立连接、数据传输和断开连接等不同操作。服务器需要根据报文类型采取不同的处理策略,例如建立稳定连接、流量控制或释放资源。
ACK标志位
ACK标志位用于表明当前报文是对历史报文的确认,即它是一个确认报文。设置ACK标志位并不代表报文正文部分不包含数据,而是表示该报文具备确认功能。接收方在收到ACK标志位为1的报文时,应关注确认序号字段。
SYN和FIN标志位
-
SYN标志位用于TCP连接的建立过程,具体体现在三次握手中。当客户端希望与服务器建立连接时,会发送一个SYN标志位置1的报文,表示请求建立连接。服务器收到后,会回复一个SYN+ACK报文,表示同意建立连接。最后,客户端再发送一个ACK报文确认,完成三次握手。
-
FIN标志位用于断开连接,表示通信结束时需要终止链接。
三次握手和四次挥手的过程

服务端状态变化:
-
CLOSE-\>LISTEN\]服务器调用`listen`进入LISTEN状态,等待客户端连接
-
SYN_RCVD -\> ESTABLISHED\] 服务端⼀旦收到客⼾端的确认报⽂, 就进⼊ESTABLISHED状态, 可以进⾏读写数据了
-
CLOSE_WAIT -\> LAST_ACK\] 进⼊CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调⽤close关闭连接时, 会向客⼾端发送FIN, 此时服务器进⼊LAST_ACK状态, 等待最后⼀个ACK到来(这个ACK是客⼾端确认收到了FIN)
客户端状态变化:
-
CLOSED -\> SYN_SENT\] 客⼾端调⽤connect, 发送同步报⽂段;
-
ESTABLISHED -\> FIN_WAIT_1\] 客⼾端主动调⽤close时, 向服务器发送结束报⽂段, 同时进⼊FIN_WAIT_1;
-
FIN_WAIT_2 -\> TIME_WAIT\] 客⼾端收到服务器发来的结束报⽂段, 进⼊TIME_WAIT, 并发出LAST_ACK;
TCP状态转换汇总

理解
- 在TCP三次握手过程中,客户端和服务器对链接建立的时间认知存在差异。客户端通常在发送最后一个ACK后即认为链接已建立,而服务器需要等待收到该ACK才能确认链接建立,这导致服务器建立链接的时间比客户端稍晚。
- ACK在网络传输中需要一定时间,虽然最后一个ACK不需要确认,但它确实被发送给对方。如果最后一个ACK丢失,客户端认为链接已建立,而服务器认为未建立,这将导致客户端尝试发送数据时,服务器因三次握手未完成而拒绝接收,并可能返回重置标志位RST。
- TCP是全双工的,因此断开链接需要双方各自确认。例如,客户端可能希望断开链接,但服务器仍希望继续发送数据,此时需要协商。四次挥手必须完成,因为TCP的全双工特性要求双方在双向通信中都确认断开。在某些情况下,四次挥手可能简化为三次,如果双方同时希望断开链接,FIN和ACK报文可以合并发送。但大多数情况下,由于双方断开链接的意愿不一定同步,四次挥手更为常见。
- 三次握手的本质可以理解为四次握手的简化,服务器在建立链接时将ACK和建立链接的请求合并为一个报文,从而实现捎带应答。这种合并使得四次握手简化为三次握手。三次握手确保了双方在建立链接时的双向可靠性,即从左向右和从右向左的通信均被确认。建立链接需要双方同意,因为TCP是全双工的,必须在两个方向上建立链接。断开链接时的四次挥手与建立链接时的三次握手形成对比,因为断开链接时双方的意愿不一定同步,因为一个方向上的通信终止,但另一个方向可能仍在通信,难以合并报文。但在某些情况下,如果双方同时希望断开链接,四次挥手也可能简化为三次。
- TCP链接在操作系统中表现为文件描述符,背后对应着特定的内核数据结构。服务器可以同时维护多个链接,每个链接都有独立的文件描述符。操作系统需要管理这些链接的整个生命周期,包括创建、维护和销毁。这一点我们在之前的学习中已经了解了,下面是之前的图:

-
当客户端关闭链接但服务器仍需发送数据时,可以使用
shutdown系统调用实现半关闭,单独关闭写端而保持读端开放。shutdown函数参数指定关闭方式(读、写或读写),允许选择性关闭通信方向。文件描述符的读写模式信息存储在struct file结构中,shutdown通过修改这些标志位实现半关闭功能。半关闭状态下,被关闭方向的IO操作会被系统阻止,但另一方向仍可正常通信。HTTP短链接场景通常直接使用close完全关闭链接,而需要保持单向通信的场景则使用shutdown实现半关闭。一般在实践中我们不会经常用到,具体用法可以用man去查c#include <sys/socket.h> int shutdown(int sockfd, int how); -
在TCP连接建立阶段,通信双方通过三次握手过程不仅完成连接建立,还会进行初始窗口大小协商。这种设计解决了首次数据传输时发送方不确定接收方缓冲区大小的问题。具体流程是:在SYN和ACK报文交换过程中,双方会携带各自的接收窗口信息。这样在正式传输数据前,发送方就已获知接收方的处理能力,避免了首次发送就可能超出接收方缓冲区容量的风险。
链接异常的情况
TCP三次握手异常与链接重置机制
TCP通信过程中,客户端和服务器在三次握手时可能出现不一致的情况。客户端第一次发送SYN,第二次收到SYN-ACK后发送ACK,此时客户端认为链接已建立,但服务器只有在收到ACK后才确认链接建立。如果ACK丢失,客户端会认为链接已建立而服务器认为未建立。此时客户端可能直接发送数据报文,服务器收到后会发送带有RST标志位的ACK报文进行应答。客户端识别到RST标志位后会意识到链接异常,立即释放当前链接并重新进行三次握手。这种机制称为链接重置(reset),可以解决因丢包导致的链接建立不一致问题。
在实际网络环境中,当网站压力过大或出现丢包时,客户端可能会收到链接重置提示。此外,当服务器突然崩溃并重启时,客户端可能仍保持原有链接状态,此时服务器会对客户端发送RST报文来终止异常链接。
TCP四次挥手异常导致的CLOSE_WAIT状态的堆积
在TCP连接关闭过程中,当客户端主动关闭连接时,客户端发送FIN后,服务器操作系统自动发送ACK,客户端进入FIN_WAIT_2状态,服务器端会进入CLOSE_WAIT状态。但如果服务器未关闭文件描述符(也就是这个链接的sockfd),导致连接无法完成四次挥手。
服务器长时间不关闭文件描述符会导致CLOSE_WAIT状态的连接堆积,造成资源泄露和服务器性能下降。这也是之前我们写socket实现服务器时要及时把用不到的文件描述符关闭的原因。
客户端在FIN_WAIT_2状态等待一段时间后,如果未收到服务器的FIN报文,会自动关闭连接。当服务器进程被强制终止时,CLOSE_WAIT状态的连接会立即进入LAST_ACK状态,但由于客户端早已断开,无法完成最后的ACK确认。服务器最终会超时自动关闭连接。
进程异常导致的TCP链接异常情况
当通信一方的进程异常终止时,操作系统会自动回收进程资源并关闭文件描述符,这会触发正常的TCP四次挥手过程,确保连接被正确关闭。在机器重启的情况下,操作系统会在关机前终止所有运行中的进程,这些进程的终止同样会触发TCP连接的正常关闭流程。这也是为什么在系统负载较重时,关机过程会较慢的原因------系统需要等待所有网络连接完成关闭流程。
对于突然掉电或网络断开这类极端情况,TCP连接将无法完成正常的关闭流程。当网线被物理断开或设备突然断电时,连接双方可能无法及时检测到对方状态的变化,导致连接处于异常状态。这种情况下,TCP协议依赖于保活机制来检测失效连接,经过多次重试失败后才会最终放弃连接。
TCP链接保活机制
当客户端网络状态发生变化时,服务器端无法立即感知链接异常。客户端突然断电或拔掉网线会导致操作系统直接释放链接,但服务器端仍认为链接有效。服务器会通过TCP内置的保活机制定期向客户端发送询问报文,若多次未收到响应则会关闭链接。TCP保活时间通常为分钟级或小时级,可在操作系统配置。实际工程中更多采用应用层保活机制,如QQ客户端定期向服务器发送请求。若服务器向已断开的客户端发送数据,客户端会返回reset报文通知服务器关闭链接。应用层协议如HTTP长链接和QQ断线重连都实现了自己的保活机制。
需要注意的是:在TCP通信中通信双方地位相同,因此三次握手和四次挥手的主动发起方可以反过来,流程不变
TIME_WAIT状态详解
在TCP协议中,主动断开连接的一方会经历四次挥手过程。第一次挥手由主动方发起FIN请求,第二次挥手由被动方回复ACK确认,第三次挥手由被动方发送FIN请求,第四次挥手由主动方发送ACK确认。
完成四次挥手后,主动方不会立即进入CLOSED状态,而是进入TIME_WAIT状态。TIME_WAIT状态会持续两个MSL(Maximum Segment Lifetime)时间,MSL表示报文在网络中存活的最长时间。操作系统会动态计算这个时间,通常是从发送端到接收端的往返时间。
进入TIME_WAIT状态的主要原因:
- 可能存在历史游离报文在网络中滞留。这些报文可能已经被判定为超时但实际上仍在网络中传输。如果主动方立即关闭连接并重新建立相同端口的连接,这些滞留报文可能会被误认为是新连接的数据。等待两个MSL时间可以确保这些游离报文在网络中完全消散。
- 确保最后一个ACK能够被对方收到。如果ACK丢失,被动方会重传FIN,处于TIME_WAIT状态的主动方可以再次发送ACK。这种机制虽然不能完全避免问题,但能显著降低异常情况发生的概率。在网络通信量巨大的情况下,即使小概率事件也会频繁发生,因此TIME_WAIT状态是必要的保护机制。
等待两个MSL时间而不是一个MSL的原因:
- 考虑到报文往返的双向传输路径,确保两个方向上的游离报文都能完全消散。
TIME_WAIT引起的bind失败问题
在实际应用中,服务器作为主动关闭方时经常会遇到端口被占用的情况,就是因为处于TIME_WAIT状态的连接尚未完全释放。服务器主动关闭连接后会进入TIME_WAIT状态,此时立即重启服务会报端口绑定错误,等待足够时间后服务才能正常启动。
虽然TCP协议通过这种机制在各种网络异常情况下提供最大程度的可靠性保障,但是在某些情况下可能是不合理的,比如:服务器需要处理⾮常⼤量的客⼾端的连接(每个连接的⽣存时间可能很短, 但是每秒都有很⼤数量的客⼾端来请求).
使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socjet描述符

滑动窗口
-
TCP滑动窗口机制通过将发送缓冲区划分为三个区域来实现高效的数据传输:已发送并确认的区域、可立即发送的窗口区域(滑动窗口)以及待发送区域。
-
滑动窗口本质是发送缓冲区中由起始(start)和结束(end)下标界定的一个连续字节范围,这个范围内的数据可以被立即发送。缓冲区采用字节流模型,每个字节都有唯一序号,便于构建TCP报文。当数据从用户空间拷贝到发送缓冲区时,就被赋予相应序号。滑动窗口的大小会根据接收方的窗口通告动态调整,确保发送量不超过接收方处理能力。随着数据不断被发送和确认,滑动窗口会在缓冲区中向前"滑动",新的数据随之进入可发送范围。
-
滑动窗口的滑动是通过接收方的确认应答(ACK)实现的。ACK中包含确认序号和窗口大小信息,发送方根据确认序号更新滑动窗口的起始位置(start),根据窗口大小更新结束位置(end)。
-
滑动窗口的滑动方向只能是向右,因为数据序号是单调递增的,且已发送并确认的数据不会被重新发送。可以把发送缓冲区想象成一个环形数组,滑动窗口在这个数组中滑动。滑动窗口的实现类似于基于环形队列的生产者消费者模型,生产者是用户,消费者是操作系统。
-
滑动窗口的大小可以动态变化,取决于接收方缓冲区的剩余空间,也就是与TCP报头中16位窗口大小有关。如果接收方缓冲区空间增加,窗口会变大;如果接收方缓冲区空间减少,窗口会变小。滑动窗口的机制实现了TCP的流量控制功能。

批量转发
这种机制允许发送方批量发送多个报文后再等待批量确认,显著提高了网络利用率。刚才我们讨论了确认应答策略, 对每⼀个发送的数据段, 都要给⼀个ACK确认应答. 收到ACK后再发送下⼀个数据段. 这样做有⼀个⽐较⼤的缺点, 就是性能较差. 尤其是数据往返的时间较⻓的时候. 既然这样⼀发⼀收的⽅式性能较低, 那么我们⼀次发送多条数据, 就可以⼤⼤的提⾼性能(其实是将多个段的等待时间重叠在⼀起了)

窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值. 上图的窗⼝⼤⼩就是4000个字节(四个段)
发送前四个段的时候, 不需要等待任何ACK, 直接发送
收到第⼀个ACK后, 滑动窗⼝向后移动, 继续发送第五个段的数据; 依次类推
操作系统内核为了维护这个滑动窗⼝, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉
窗⼝越⼤, 则⽹络的吞吐率就越⾼;
尽管滑动窗口内的数据可以直接发送,但TCP协议在实际发送时会将数据分成多个小段发送,而不是一次性发送一个大报文。这是因为数据链路层对帧的大小有限制,通常最大传输单元(MTU)为1500字节。如果滑动窗口内的数据超过MTU限制,发送方必须将数据分成多个符合MTU大小的段发送。数据链路层不允许发送过大的报文,如果发送的报文超过MTU限制,链路层会直接丢弃该报文。
丢包问题
滑动窗口的异常丢包问题包括数据丢包和应答丢包,对发送方来说效果相同,即收不到应答。在讨论丢包问题时,不考虑请求丢包还是应答丢包,只考虑接收方收不到应答的情况。丢包问题可以分为三种情况:最左侧丢包、中间丢包和最右侧丢包。最左侧丢包时,滑动窗口不会右移,发送方会根据确认序号判断丢包并进行补发。中间或右侧丢包时滑动窗口移动之后会将问题转化为最左侧丢包问题。
快重传机制
快重传是TCP的一种高速重发机制,当发送方连续收到三个相同的确认应答时,会立即对丢失的报文进行补发。在并发发送场景下,如果主机a连续收到三个同样的确认序号,会立即对当前被丢失的报文进行补发。补发成功后,会根据新的ack决定下一步该补发哪个报文。快重传的前提是必须收到三个同样的确认信息。如果发送的报文数量不足三个,无法触发快重传,此时只能依靠超时重传。超时重传是TCP的兜底机制,确保报文不会永久丢失。快重传和超时重传在TCP中同时使用,快重传提高效率,超时重传保证可靠性。

拥塞控制
虽然滑动窗口和流量控制能够高效可靠的发送大量数据。但在网络通信中,当出现大规模数据包丢失时,直接重传会加剧网络拥塞。此时应避免立即重传,转而采用等待策略。网络拥塞是全局性问题,涉及多台主机同时通信。若所有主机因丢包而同时重传,会导致网络流量激增,进一步恶化拥塞。因此,拥塞控制策略需考虑全网状态,而非单台主机的行为。
慢启动
TCP协议通过慢启动机制应对拥塞:发送方逐步增加数据量(如1、2、4、8个报文),动态探测网络状况。
慢启动通过指数增长(1→2→4→8...)快速探测网络容量上限,同时避免初期过量发送引发雪崩效应。

拥塞窗口
拥塞窗口(cwnd)是发送端维护的关键参数,用于限制单次发送量。像上⾯这样的拥塞窗⼝增⻓速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增⻓速度⾮常快。为了不增⻓的那么快, 因此不能使拥塞窗⼝单纯的加倍。此处引⼊⼀个叫做慢启动的阈值,当拥塞窗⼝超过这个阈值的时候, 不再按照指数⽅式增⻓, ⽽是按照线性⽅式增⻓。

当TCP开始启动的时候, 慢启动阈值等于窗⼝最⼤值; 在每次超时重发的时候, 慢启动阈值会变成原来的⼀半, 同时拥塞窗⼝置回1;
拥塞窗口与滑动窗口的协同机制
拥塞窗口的调整独立于滑动窗口,但两者共同决定实际发送量。拥塞窗口(cwnd)是发送端内部维护的动态变量,反映当前网络健康状态。实际发送窗口取拥塞窗口与接收端通告窗口的较小值,确保数据量不超过网络和接收端的综合承载能力。这种设计解决了滑动窗口的局限性------后者仅关注接收端缓冲区,忽略网络状况。二者协同工作:拥塞窗口约束网络层面的发送激进程度,滑动窗口确保接收端不溢出。
实际发送窗口大小 = min(拥塞窗口, 接收方通告窗口)
拥塞窗口:发送方根据网络状况估计的窗口
接收方通告窗口:接收方根据自身缓冲区大小通告的窗口
拥塞控制总结
拥塞控制的实际应用体现在TCP协议中,通过动态调整拥塞窗口来优化数据发送效率。拥塞窗口的值反映网络健康状态:值越大,网络越健康;值越小,网络越拥堵。发送方通过不断更改拥塞窗口来评估网络状况:只要发送的数据能收到应答,窗口可以继续增长;如果网络状况恶化,窗口会迅速减小。
延迟应答
当接收方收到数据时,可以选择不立即应答,而是等待一小段时间。在此期间,上层用户可能从接收缓冲区读取数据,从而增加缓冲区剩余空间。延迟应答后,接收方可以通告更大的窗口,发送方可能因此调整滑动窗口,提高数据传输效率。延迟应答的原理在于通过等待上层处理数据,动态调整窗口大小。然而,延迟应答并非总是有效,其效果取决于上层读取速度和网络拥塞情况。延迟应答的策略包括数量限制(每隔N个包应答一次)和时间限制(等待固定时间后应答),具体实现因操作系统而异。
延迟应答也可能会带来一些问题。如果接收方等待期间上层未读取数据,延迟应答可能无法调整窗口大小。此外,滑动窗口不仅受接收缓冲区空间影响,还受拥塞窗口限制,因此延迟应答不一定能提高效率。延迟应答是一种概率性优化策略,其效果取决于通信频率和数据量。因此对延迟应答的正确理解:它并非绝对有效,但一旦生效,可以提升吞吐量。具体延迟时间(如200毫秒)或包数(如隔2个包)的设定需远小于报文超时时间,以避免重传问题。
粘包问题
TCP协议中的粘包问题是指接收方可能读取到不完整的应用层报文,或者将多个报文的部分内容合并读取的情况。这种现象源于TCP面向字节流的特性,协议本身不维护应用层报文边界。UDP协议由于采用面向数据报的方式,每个数据包都包含完整的长度信息,因此不存在粘包问题。
解决TCP粘包问题的核心在于应用层明确报文边界,常见方法包括:固定长度报文、特殊分隔符、固定长度报头加变长内容等策略。以日常生活为例,粘包问题类似于蒸笼中粘连的包子,需要通过物理分离来明确每个包子的边界。在网络编程实践中,网络版计算器就采用了先读取长度字段,再根据长度读取内容的方法来处理粘包问题。
总结
为什么TCP这么复杂? 因为要保证可靠性, 同时⼜尽可能的提⾼性能
-
为保证可靠性,TCP拥有:校验和、序列号、确认应答、超时重发、连接管理、流量控制、拥塞控制等功能;
-
为提高性能,TCP拥有:滑动窗口、快速重传、延迟应答、捎带应答等机制
TCP为保证可靠性需要实现复杂机制,包括链接管理、流量控制、拥塞控制等,而UDP则简单高效但不保证可靠性。TCP适用于文件传输、重要状态更新、交易转账等需要可靠性的场景;UDP更适合视频传输、直播等对实时性要求高的场景。早期QQ使用UDP协议。选择TCP或UDP应根据具体应用需求决定,可靠性不是绝对的优缺点而是协议特性。基于TCP的应用层协议包括HTTP、HTTPS、SSH等。