TCP 全称为 "传输控制协议( Transmission Control Protocol )",人如其名,要对数据的传输进行一个详细的控制;


TCP报头的长度也是定长的——20字节(不包含选项的长度)
TCP头部各字段详细说明
那么TCP报文的报头和有效载荷如何分离呢
TCP 报文头部与有效载荷的分离依赖于头部中的 “数据偏移”(Data Offset) 字段。具体步骤如下:
数据偏移值 × 4) 字节,即得到头部结束处。之后的所有内容即为 有效载荷(应用层数据)。
示例
假设收到一个 TCP 报文,其数据偏移字段为 6,则:
为什么在TCP中,没有包含整个报文大小,只有报头大小?而UDP中就有16位UDP长度
在通信中,如何实现可靠性?是否可能达到100%的可靠?

如果对端没有给我们应答,那我们就不能确定对端是否收到我发送的消息,这其实就说明世界上就没有100%可靠的协议,但是我们能保证历史消息的可靠性
正确理解可靠性
为了确保某个消息的可靠性,我们可以“牺牲”其“最新性”。即:不要求它是最新的报文,只要求它有应答即可。
例如,在一个长连接中,我们可以通过编号或时间戳区分消息的新旧。只要某个消息获得了ACK,我们就知道它“可靠送达”,哪怕它不是最近发出的。
发送方发送一个报文,接收方收到后必须回复一个确认应答。只有收到这个应答,发送方才能确信数据已经成功送达。这其实就是确认应答机制,即确认应答机制:发送方发送数据后,接收方需返回“应答”,以确认收到数据。

对于应答,我们不再需要回应,即不对应答做应答,因为ACK不携带数据,且TCP协议设计中ACK可被捎带,避免无限循环
下面我们以客户端给服务端发送报文为例
实际上,客户端可以一次发送多个消息,服务端则对每个消息做应答

每个TCP报文段都有一个序号(Seq),用于标识数据字节流中的位置。
接收方返回的确认序号(Ack) = 收到的最后一个正确报文的序号 + 1,表示“期待下一个字节的序号”。
举例:
(1)确认应答(ACK)
(2)解决乱序问题
(3)解决丢包问题
例如:
类比:你寄快递给朋友,你的包裹上贴有“序号1”,朋友收货后回复“我已收到序号1,期待序号2”。这里的“序号1”是你发的,“序号2”是他希望你下一次发的。这就是两个独立的序列号系统。
在通信的时候,双方传递的报文就是tcp报文,最少也得是一个报头
发送与接收的速度不匹配
在TCP协议中,流量控制(Flow Control)是为了防止发送方发送数据过快,导致接收方来不及处理或缓冲区溢出。其核心机制是滑动窗口协议。
在TCP报文段的首部中,有一个 16位窗口大小(Window Size)字段,它的作用是:
告知发送方,接收方当前可用的接收缓冲区大小(即接收方还能接收多少字节的数据)。
换句话说,这个“窗口大小”是接收方告诉发送方的,表示的是接收方的接收缓冲区中剩余空间的大小。
具体机制如下:
所以“发送端如何尽早得知对方的接收能力?”
答案是:通过接收方在ACK报文中携带的“窗口大小”字段。
按量按需发送,必须知道对方的接收缓冲区中剩余空间的大小
我们来看一下内核中的tcp报头
// linux kernel include/linux/tcp.h
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};标志位:本质就是报头中的比特位!
为什么需要标志位?
因为网络中传输的TCP报文有不同的类型(如建立连接、传输数据、断开连接),接收方需要根据报文类型做出不同的处理。标志位就是用来明确标识一个TCP报文的类型和目的的。例如:
每个标志位具体有什么作用呢?下面我们展开来讲讲
SYN:用于建立连接 ACK:确认字段有效,表示这是一个确认报文。
其中涉及到三次握手,我们来介绍一下
类比:
✅ 连接建立成功,进入ESTABLISHED状态。

关键点:前两次握手不携带应用数据,是为了防止恶意攻击(如SYN洪泛攻击)。只有在确认连接合法后,第三次握手及之后的报文才可以开始传输数据。
ACK几乎是常被设为1的
FIN:用于断开连接。涉及四次挥手
连接终止需要四个步骤,因为它是一个全双工连接的独立关闭。

PSH (Push): 为1时,要求接收方立即将数据推送给上层应用程序,而不必等待缓冲区填满
没有PSH的情况(默认行为):
这种缓冲机制虽然提高了效率,但对于交互式应用来说,会导致不可接受的延迟。
使用PSH的情况:
PSH标志就像一个“刷新”按钮。当它被设置时:
注意:
RST (Reset): 为1时,表示需要重置连接。通常用于异常终止连接,或拒绝一个非法的连接请求。
与正常的FIN挥手告别不同,RST是一种突然的、强制性的、非优雅的连接终止方式。
核心效果:一旦收到RST,接收方会立即释放连接所有相关资源,连接状态直接回到CLOSED,不经过任何中间状态。
如果说FIN是礼貌的"再见",那么RST就是紧急的"断开!现在!"。它存在的根本目的是处理那些无法通过正常TCP流程解决的错误和异常情况。
RST提供了一种最后的保障,让一方能够单方面宣布连接失效,并强制对方释放资源,防止连接 hanging(挂起)。
RST 的常见触发场景
以下是导致发送RST报文的典型情况:
telnet 192.168.1.1 9999,如果9999端口没有开放,你会看到"Connection refused",这背后就是收到了RST。
URG (Urgent): 为1时,表示报文段中有紧急数据,应优先处理。此时紧急指针字段有效。
核心概念:URG机制允许发送方告诉接收方:“这个报文中有一些数据需要被’紧急’处理,请优先关注这部分数据。”
URG的设计初衷是为了实现一种带外数据的传输机制,即在正常数据流之外传递一些控制信令或紧急通知。
典型应用场景:
Ctrl+C想要中断当前执行的任务时,这个中断信号需要通过URG机制优先传递给服务器。
URG 的工作机制
URG机制涉及两个关键组件:
关键理解:
问题与局限性:

TCP将每个字节的数据都进行了编号,即为序列号.
对于TCP来说,它传输的不是一个个独立的“消息包”,而是一个连续的、无结构的字节流。
序列号就是为这个连续的字节流中的每一个字节都分配一个唯一的编号。
序列号在报文段中的具体体现 虽然每个字节都有编号,但TCP是以报文段为单位来发送数据的。每个TCP报文段的首部中都包含一个序列号字段。
这个字段的值,指明了该报文段所携带的【第一个数据字节】的序列号。

确认应答如何与字节编号对应 接收方的确认同样是基于字节序列号的。
ACK = 1001。
1001 的含义是:“截至到第1000号字节的所有数据我都已收到,我下一个期望收到的字节的序列号是1001”。ACK=1001 后,它就知道前1000个字节已经安全送达,可以继续发送从1001开始的数据。
初始序列号是如何确定的?
在TCP建立连接(三次握手)时,通信双方会各自随机生成一个初始序列号。使用随机值是为了安全,防止被恶意预测和攻击。
SYN 报文中携带自己的初始序列号(例如 seq = x)。
SYN-ACK 报文中携带自己的初始序列号(例如 seq = y),并确认客户端的序列号(ACK = x+1)。
ACK 报文中确认服务端的序列号(ACK = y+1)。
至此,连接建立,双方后续的数据传输都基于这个初始序列号递增。
如何理解丢包,重新理解应答报文
丢包分为两种情况 情况一:主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;

情况二:主机A未收到B发来的确认应答。也可能是因为ACK应答报文丢失了

发送方没有收到应答ACK,意味着什么?意味着丢包吗?
丢包,要么数据丢,要么应答丢,你能确认是数据丢还是应答丢吗?你无法确切地判断究竟是原始数据包在传输途中丢失,还是接收方返回的确认应答(ACK)包丢失了
这只能意味着数据可能丢失,对方可能没有收到。
但是在这两种情况下,发送方会陷入永远的等待,那怎么办呢?
尽管无法区分原因,但TCP协议通过一套统一的机制来保证可靠性,你无需关心具体是哪种丢包:
那么,如果超时的时间如何确定?
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间.
在Linux中,默认的最大重传次数由/proc/sys/net/ipv4/tcp_retries2决定,通常是15次。

所以,序号的作用是什么?
即:按序到达、确认应答、去重
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
但是在建立连接和断开连接时,客户端和服务端的状态会产生变化,这些状态的本质其实就是一个整数。
那服务器一定会存在与多个客户端建立连接的情况,那连接需不需要被管理呢?先描述再组织
struct tcp_sock {
struct inet_connection_sock inet_conn;
// TCP专用字段:序列号、窗口、拥塞控制等
};
struct inet_connection_sock {
/* inet_sock has to be the first member! */
struct inet_sock icsk_inet; // 基础的INET套接字信息
struct request_sock_queue icsk_accept_queue; // 用于管理建立连接前的请求队列(如TCP的SYN队列和accept队列)
unsigned long icsk_timeout; // 当前连接的超时时间
struct timer_list icsk_retransmit_timer; // 数据包重传定时器
struct timer_list icsk_delack_timer; // 延迟ACK定时器
__u32 icsk_rto; // 重传超时时间(根据RTT动态计算)
__u32 icsk_pmtu_cookie; // 记录路径MTU(Path MTU)发现的信息
const struct tcp_congestion_ops *icsk_ca_ops; // 指向拥塞控制算法的操作函数集
const struct inet_connection_sock_af_ops *icsk_af_ops; // 地址族相关的操作(IPv4/IPv6)
__u8 icsk_ca_state:6, // 拥塞控制状态机(如Open, Disorder, CWR, Recovery, Loss)
__u8 icsk_retransmits; // 记录超时重传的次数
__u8 icsk_pending; // 记录挂起的定时器事件(如ICSK_TIME_RETRANS)
__u8 icsk_backoff; // 重传退避指数,用于计算重传超时
__u8 icsk_syn_retries; // SYN重试次数
// ... 可能还有其他字段
};所以建立连接是需要成本的——时间和空间

服务端状态转化:
[CLOSED -> LISTEN] 服务器端调用listen后进入 LISTEN 状态,等待客户端连接;[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文.[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文,就进入 ESTABLISHED 状态,可以进行读写数据了.[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入 CLOSE_WAIT;[CLOSE_WAIT -> LAST_ACK] 进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入 LAST_ACK 状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK,彻底关闭连接.客户端状态转化:
[CLOSED -> SYN_SENT] 客户端调用connect,发送同步报文段;[SYN_SENT -> ESTABLISHED] connect调用成功,则进入 ESTABLISHED 状态,开始读写数据;[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时,向服务器发送结束报文段,同时进入 FIN_WAIT_1;[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认,则进入 FIN_WAIT_2,开始等待服务器的结束报文段;[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段,进入 TIME_WAIT,并发出 LAST_ACK;[TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入 CLOSED 状态客户端connect主动发起连接进行三次握手,这个过程是由操作系统自动完成的
那服务端如果不调用accept,三次握手建立连接能够成功吗?
在TCP协议中,三次握手是由操作系统内核的TCP协议栈完成的,而不是由应用程序控制的。因此,当客户端发送SYN报文段到达服务器时,服务器内核会自动回复SYN-ACK,并在收到客户端的ACK后完成三次握手。此时,连接已经建立,并被放入服务器的监听套接字的连接队列(accept queue)中。
所以,即使服务端应用程序没有调用accept,三次握手仍然可以成功完成。但是,如果服务端一直不调用accept,那么已经建立的连接会一直停留在连接队列中。当连接队列满了之后,服务器内核可能会开始拒绝新的连接请求。
目前我们已经了解了什么是三次握手,三次握手是怎么做的,那么为什么要进行三次握手呢?
理由一:以最小成本,100%确认双方通信意愿 在通信开始前,必须确保客户端和服务器双方都明确同意建立连接。如果只用两次握手,可能会出现以下问题:
三次握手通过以下过程解决此问题:
只有在第3步完成后,服务器才真正确认客户端收到了它的SYN-ACK,从而双向确认了通信意愿。
✅ 这确保了“双方都愿意通信”,避免了一方单方面建立连接的资源浪费。
理由二:以最短的方式,验证全双工通信能力 TCP是面向连接、全双工(Full-duplex)的协议,即双方可以同时发送和接收数据。
三次握手的本质是验证:“我们两个所处的网络是通畅的,能够支持全双工!”
通过三次握手,双方都确认了彼此的发送和接收能力正常,从而确保了全双工通信。
类比 这等价于“男女朋友 → 夫妻”的两个条件:
为什么不是四次?
实际上,如果分开,那么就是:
但是,中间两次(服务器对客户端SYN的确认ACK和服务器发送自己的SYN)可以合并为一次(可以理解为捎带应答),所以就成了三次。
所以三次握手本质上就是四次握手,不过第二次将中间两次合并为一次是以最小成本来验证全双工通信
那断开连接为什么要四次挥手?不能合并为三次挥手吗?
建立连接时,服务器在收到客户端的SYN后,可以将自己的SYN和ACK放在同一个报文中发送,因此可以三次握手。而断开连接时,TCP连接是全双工的,每个方向都必须单独关闭。当一方发送FIN,表示它不再发送数据,但还能接收数据。
四次挥手的过程(假设客户端先发起关闭):
为什么不能合并为三次? 因为服务器在收到客户端的FIN时,可能还有数据要发送,不能立即发送FIN。所以先发送ACK,等数据发送完毕后再发送FIN。 因此,ACK和FIN不能像握手那样合并发送。
但是,如果服务器在收到FIN时,没有数据要发送,那么服务器是否可以将ACK和FIN合并呢? 理论上是可以的,但TCP协议设计时为了可靠,并没有这样做。因为TCP规范要求每个FIN必须单独确认,所以即使没有数据,也要分开两个步骤。 不过,在实际中,有时确实可以看到三次挥手,即服务器的ACK和FIN合并,但这不是标准行为,依赖于实现。
然而,为什么TCP协议不设计成在服务器没有数据要发送时合并ACK和FIN呢? 因为TCP协议设计需要处理一般情况,即服务器可能还有数据要发送。如果为了少数情况而设计合并,会增加复杂性。 而且,分开确认可以保证可靠性。如果合并,那么ACK和FIN的确认机制就会变得复杂。
另外,从状态机角度,断开连接需要四个步骤,确保双方都知道对方已经关闭了连接。
所以,四次挥手是TCP协议设计的标准,以确保可靠地关闭连接。
下图是TCP状态转换的一个汇总:

为什么需要这些状态?
LISTEN:服务器端等待连接请求的状态。这个状态的存在使得服务器能够区分是否正在等待客户端的连接请求。如果没有这个状态,服务器可能无法正确处理 incoming 的连接。
SYN_RCVD:服务器收到SYN报文后,发送SYN-ACK,进入此状态。这个状态的存在是为了处理可能出现的重传和超时。如果服务器发送SYN-ACK后没有收到ACK,它可以重传SYN-ACK。
ESTABLISHED:表示连接已经建立,可以传输数据。这个状态的存在使得两端知道连接已经准备好进行数据传输。
FIN_WAIT_1:当一端(假设为A)发送FIN报文后,进入此状态,表示A没有数据要发送了,等待对方确认。这个状态的存在是为了处理对方可能没有及时确认FIN的情况,如果超时,A可以重传FIN。
FIN_WAIT_2:当A收到对FIN的确认(ACK)后,进入此状态,表示A到B的方向已经关闭,但B到A的方向可能还有数据在传输。这个状态的存在是为了让A能够继续接收B可能还在发送的数据,直到B也发送FIN。
CLOSE_WAIT:当B收到A的FIN后,进入此状态,表示B已经知道A没有数据要发送了,但B可能还有数据要发送给A。这个状态的存在是为了让B能够完成剩余数据的发送,然后再关闭连接。
LAST_ACK:当B发送FIN报文后,进入此状态,等待A对B的FIN的确认。这个状态的存在是为了确保B的FIN能被A确认,如果超时,B可以重传FIN。
TIME_WAIT:当A收到B的FIN并发送ACK后,进入此状态。这个状态的存在有两个主要原因:
TIME_WAIT 状态可以再次发送ACK。
CLOSED:表示连接完全关闭,不再占用资源。
这些状态的存在使得TCP能够以一种可靠的方式管理连接,处理各种网络异常(如丢包、重传、乱序等)。每个状态都有其特定的超时和重传机制,确保连接的正确性。
如果没有这些状态,我们将很难处理连接建立和断开过程中可能出现的各种情况,比如:
SYN_RCVD 状态,服务器在发送SYN-ACK后无法知道连接是否建立成功,也无法处理重传。
FIN_WAIT_1 和 FIN_WAIT_2 状态,主动关闭的一端可能无法知道对方是否已经收到FIN,也无法知道对方是否还有数据要发送。
CLOSE_WAIT 状态,被动关闭的一端可能被迫立即关闭连接,导致数据丢失。
因此,TCP状态机设计这些状态是为了确保连接的可靠性和数据的完整性。
CLOSEING状态是什么情况?
CLOSING状态的触发条件 当TCP连接的两端几乎同时发送FIN报文时,就会进入CLOSING状态。具体来说:
状态转换过程 假设客户端和服务器同时发送FIN:
客户端:
服务器:
但是,注意:在典型的TCP状态机中,CLOSING状态是主动关闭的一方在等待对方确认自己的FIN,同时自己也收到了对方的FIN(即双方都发送了FIN,但都没有收到对应的ACK)时进入的状态。
CLOSING状态的行为 在CLOSING状态下,TCP等待对方对自己发送的FIN的确认(ACK)。实际上,当双方都发送了FIN,那么双方都会收到对方对FIN的ACK(因为收到FIN后必须发送ACK),然后双方都会进入TIME_WAIT状态。
具体过程:
注意:在CLOSING状态,双方都在等待对方对自己FIN的ACK。一旦收到,就会进入TIME_WAIT状态。
状态转换图 在TCP状态转换图中,CLOSING状态的转换路径如下:
为什么需要CLOSING状态? CLOSING状态是为了处理双方同时关闭连接的情况。如果没有这个状态,协议可能无法正确处理这种情况。同时关闭是相对少见的,但TCP协议必须处理所有可能的情况。
在同时关闭的情况下,双方都发送了FIN,并且都收到了对方的FIN,然后双方都发送ACK回应对方的FIN。这样,双方都需要等待对方对自己FIN的确认,这就是CLOSING状态的作用。
与其它状态的区别
实际中的CLOSING状态 在实际网络中,CLOSING状态很少见,因为通常连接关闭是由一方发起的,另一方响应。但是,如果应用程序设计为双方都可以主动关闭,则可能发生同时关闭。
理解TIME_WAIT状态
TIME_ WAIT 状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
想一想,为什么 TIME_WAIT 的时间是 2MSL ?
TIME_WAIT 持续存在 2MSL 的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的);LAST_ACK)解决TIME_WAIT状态引起的bind失败的方法
在 server 的 TCP 连接没有完全断开之前不允许重新监听,某些情况下可能是不合理的
TIME_WAIT 连接.TIME_WAIT 的连接数很多,每个连接都会占用一个通信五元组(源ip、源端口、目的ip、目的端口、协议). 其中服务器的ip和端口和协议是固定的,如果新来的客户端连接的ip和端口号和 TIME_WAIT 占用的连接重复了,就会出现问题.使用 setsockopt ()设置 socket 描述符的 选项 SO_REUSEADDR 为 1 ,表示允许创建端口号相同但IP地址不同的多个 socket 描述符

刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段,这样做有一个比较大的缺点,就是性能较差,尤其是数据往返的时间较长的时候.

既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).
那么,发送方一次发多少数据,由什么决定?
在TCP协议中,发送方一次可以向对方发送的数据量,由当前滑动窗口的大小决定。
滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值
以主机A和主机B收发数据为例:

滑动窗口是发送缓冲区的一部分。
你可以将发送缓冲区当作一个char类型的数组(char buffer[N]),发送方维护一个“发送缓冲区”,其中包含已发送但未确认、可发送和待发送的数据。在该缓冲区内,有一个“滑动窗口”,它表示当前允许发送但尚未发送的数据范围。那么滑动窗口就是这个数组中的两个指针:start和end,窗口大小 = end - start,即窗口右边界减去左边界。

已发送已确认:这部分的空间并不是无效了,而是可以重复利用了,并不需要刻意去清空这部分空间
序号在发送的轮次中,数字是依次增大的,也就意味着滑动窗口是向右滑动的(宏观上)
所以,窗口滑动的本质就是start和end下标增加
那么滑动窗口的大小,由什么决定呢?
窗口大小的决定因素:
窗口移动机制:
滑动窗口的本质:流量控制的具体实现方案

滑动窗口可以向左滑动吗?滑动窗口,可以变大吗?可以变小吗?可以不变吗?可以为0吗?
❌ 不可以向左滑动! 滑动窗口只能向右滑动(即“向前滑动”),不能向左滑动。
原因:
⚠️ 注意: 虽然窗口不能“物理”向左滑动,但在某些情况下(如收到重复ACK或超时重传),发送方可能会回退窗口位置进行重传,但这属于重传机制,不是窗口本身的“向左滑动”。
✅ 可以变大!
条件:
示例:
✅ 可以变小!
条件:
示例:
⚠️ 这种行为称为“窗口缩放”,用于防止接收方溢出。
✅ 可以不变! 当接收方通告的窗口大小没有变化时,滑动窗口大小保持不变。
场景:
✅ 可以为0!
意义:
行为:
如果丢包了怎么办?滑动窗口会不会跳过报文进行应答?
滑动窗口不会“跳过”丢失报文的应答 滑动窗口是基于序列号和确认号工作的,它不会跳过任何应答。接收方必须按顺序确认数据。
关键点:
我们可以把丢包分为以下几种情况: a.最左侧丢失 b.中间报文丢失 c.最右侧丢失。而现实情况是这三种情况的组合。
a.最左侧丢失
这里又分两种情况讨论:
情况1:数据包已经抵达,应答报文ACK被丢了.

这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认。
就比如图中所示:应答报文1001、3001、4001丢失,但是我收到了2001、5001、6001,通过应答报文ACK的确认序号6001,我们就可以知道1~6000字节的数据服务端都收到了,所以部分ACK丢了没有关系
情况2:数据包就直接丢了

这种机制被称为 “高速重发控制”(也叫 “快重传”)
选择性确认(SACK)的优化 标准的快重传有一个小问题:当有多个报文丢失时,发送方收到3个重复ACK后,只知道第一个丢失的报文,不清楚后面是否还有丢失。
SACK(选择性确认) 对此进行了优化。启用SACK后,接收方在ACK报文中可以额外告诉发送方:“虽然我没收到1001-2000,但我已经收到了2001-3000和3001-4000”。这样,发送方就能精确地只重传1001-2000这个丢失的报文段,避免了不必要的重传,进一步提高了效率。
超时重传VS快重传
TCP就是通过超时重传机制和快速重传机制来检测丢包的
滑动窗口行为:
b.中间报文丢失
最左侧报文收到应答,滑动窗口更新,此时就转为了最左侧报文丢失
c.最右侧丢失 最右侧丢失也会转为最左侧丢失的情况
滑动窗口一直向右滑动会不会溢出?
序列号是循环的(Modulo)空间
TCP 为每个方向分配 32 位无符号序列号(0 - 4 294 967 295),在发送方每发送一个字节就递增一次。当序列号达到上限后会 回绕到 0,整个空间采用 模 2³² 运算。
接收端处理数据的速度是有限的,如果发送端发的太快。导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control);

接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中吗,有一个16位窗口字段,就是存放了窗口大小信息;
那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移 M 位;
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题.
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.
TCP引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;

所以滑动窗口的大小 = min(win,拥塞窗口),谁小谁决定(win指接收方窗口大小)
像上面这样的拥塞窗口增长速度,是指数级别的,“慢启动” 只是指初使时慢,但是增长速度非常快

少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞; 当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降; 拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案.
什么是延迟应答?
延迟应答是一种优化技术,它的核心思想是:当接收端收到数据后,不要立即发送ACK,而是等待一段时间(比如200毫秒),看看这段时间内是否会有数据要发送给对方。如果有,那么ACK就可以随着数据一起发送出去,这样就节省了一个单独ACK包的开销。
延迟应答与滑动窗口
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小.
一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高,我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms;

捎带应答 在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 “一发一收” 的,意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”;
那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine, thank you” 一起回给客户端

创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
由于缓冲区的存在,TCP程序的读和写不需要匹配,例如:
粘包问题
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界.
思考: 对于UDP协议来说,是否也存在 “粘包问题” 呢?
TCP异常情况
此外,应用层协议也可能有检测机制,例如HTTP长连接可能会定期发送心跳包,QQ等即时通讯软件也会定期检测连接并尝试重连。
保活机制(Keep-Alive):
RST复位:
总结:TCP通过正常的关闭过程(FIN)、保活机制和RST复位来应对各种异常情况。但是,保活机制默认时间较长,因此在实际应用中,往往依赖应用层的心跳机制来更快地检测连接状态。
TCP小结 为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能.
可靠性:
提高性能:
其他:
基于TCP应用层协议
当然,也包括你自己写TCP程序时自定义的应用层协议;
我们说了TCP是可靠连接,那么是不是TCP一定就优于UDP呢?TCP和UDP之间的优点和缺点,不能简单绝对的进行比较
归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定.
参考TCP的可靠性机制,在应用层实现类似的逻辑; 例如:

在操作系统内部,一定可能同时存在大量的报文,甚至不同层次的报文。操作系统就必须管理这些报文,如何进行管理?先描述,再组织。
struct sk_buff结构体就是用来统一描述报文的结构,类似于struct file的作用。
struct sk_buff(通常简称为SKB)是Linux内核网络子系统中最核心的数据结构,它代表了内核中流通的网络数据包。可以说,整个Linux网络协议栈就是围绕SKB的创建、传递、处理和销毁来构建的。
struct sk_buff {
/* 这两个指针必须放在最前面 */
struct sk_buff *next;
struct sk_buff *prev;
/* 数据缓冲区指针 - 这是SKB的灵魂 */
unsigned char *head; // 分配的数据缓冲区起始位置
unsigned char *data; // 当前协议层有效数据的起始位置
unsigned char *tail; // 当前协议层有效数据的结束位置
unsigned char *end; // 分配的数据缓冲区结束位置
/* 长度信息 */
unsigned int len; // 当前数据长度(data到tail)
unsigned int data_len; // 分片数据的长度
unsigned int truesize; // 整个SKB结构+数据区的总大小
/* 协议头指针 */
union {
struct tcphdr *th; // TCP头指针
struct udphdr *uh; // UDP头指针
struct icmphdr *icmph; // ICMP头指针
struct iphdr *iph; // IPv4头指针
struct ipv6hdr *ipv6h; // IPv6头指针
unsigned char *raw;
} h;
union {
struct iphdr *iph; // IPv4头指针
struct ipv6hdr *ipv6h; // IPv6头指针
struct arphdr *arph;
unsigned char *raw;
} nh;
union {
unsigned char *raw;
} mac;
/* 网络设备信息 */
struct net_device *dev; // 接收或发送该包的网络设备
/* 路由和套接字信息 */
struct sock *sk; // 所属的套接字
struct dst_entry *dst; // 路由缓存结果
/* 控制缓冲区 - 存储各层私有数据 */
char cb[48]; // Control Buffer
/* 其他重要字段 */
__u32 priority; // QoS优先级
__be16 protocol; // 包协议类型
__u16 transport_header; // 传输层头偏移
__u16 network_header; // 网络层头偏移
__u16 mac_header; // MAC层头偏移
/* 引用计数 */
atomic_t users; // 引用计数
};一、接收过程:数据包上山(从网卡到应用层) 这个过程是剥离头部、不断“轻量化”的过程
二、发送过程:数据包下山(从应用到网卡) 这个过程是添加头部、不断“封装”的过程,与接收过程相反
三、sk_buff在协议栈传递中的关键操作
这些操作通过移动sk_buff中的data和tail指针来实现,避免了频繁的内存拷贝,提高了性能。
四、sk_buff的分配和释放 sk_buff结构体和数据缓冲区是分开分配的,但通常通过同一个函数(如alloc_skb)来分配。sk_buff结构体本身位于sk_buff缓存池(slab缓存),而数据缓冲区则通过kmalloc分配。
当sk_buff的引用计数为0时,它会被释放,包括数据缓冲区的释放。
那应该怎么将网络和之前的文件串联起来呢?

下面我们来详细解读图中的每一步:
第1步:进程与文件的关联(task_struct-> files_struct)
第2步:文件描述符的本质(files_struct-> fd_array[])
socket(AF_INET, SOCK_STREAM, 0) 创建一个套接字时,系统调用返回的是一个整数,比如 3。这个 3就是文件描述符,它实际上是 fd_array的索引。
第3步:文件描述符指向文件对象(fd_array[3]-> struct file)
private_data 会指向一个表示磁盘文件的结构(如 inode)。但在这里,关键点来了:对于套接字,struct file的 private_data 指针并没有指向磁盘文件,而是指向了一个 struct socket!
第4步:文件对象连接到网络套接字(struct file-> struct socket)
第5步:套接字关联到具体协议(struct socket-> struct sock)
第6步:协议族的继承关系(struct sock-> 具体协议结构)
总结:一次读写操作的完整路径 当您的应用程序执行 write(fd, data, len)试图通过网络发送数据时,内核的处理流程如下:
读操作 read的路径与此完全对称,只是方向相反。