目录
[3.1 确认应答](#3.1 确认应答)
[3.2 超时重传](#3.2 超时重传)
[3.3 连接管理](#3.3 连接管理)
[3.4 滑动窗口](#3.4 滑动窗口)
[3.5 流量控制](#3.5 流量控制)
一、TCP协议概念
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,是互联网协议族(TCP/IP)的核心协议之一。
TCP 是面向连接的运输层协议,在无连接的、不可靠的 IP 网络服务基础之上提供可靠交付的服务。为此,在 IP 的数据报服务基础之上,增加了保证可靠性的一系列措施。
二、TCP报头首部字段

注意:数据偏移即为首部长度
TCP 虽然是面向字节流 的,但 TCP 传送的数据单元却是报文段。一个TCP 报文段分为首部和数据 两部分,而TCP 的全部功能都体现在它首部中各字段的作用。因此,只有弄清TCP首部各字段的作用才能掌握TCP的工作原理。下面讨论TCP报文段的首部格式。
TCP报文段首部的前20个字节是固定的,后面有4n字节是根据需要而增加的选项(n是整数)。因此TCP首部的最小长度是20字节。
首部固定部分各字段的意义如下:
1.源端口和目的端口:各占 2 字节。端口是运输层与应用层的服务接口。 运输层的复用和分用功能通过端口实现。
2.序号:占 4 字节(32位)。TCP 连接中传送的数据流中的每一个字节都有一个序号。序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。
3.确认号:占 4 字节(32位),是期望收到对方的下一个报文段的数据的第一个字节的序号。即表示确认号之前的数据已经收到了。
记住:若确认号 = N,则表明:到序号 N -- 1 为止的所有数据都已正确收到。
eg:给客户端返回1001,表示前面的1000我已经收到了,下一次发送"请从确认序号指定的数字开始发送",所以下次客户端就开始从1001开始发
4.数据偏移(即首部长度 ):占 4 位,指出 TCP 报文段的数据起始处 距离 TCP 报文段的起始处有多远。单位是 32 位字(以 4 字节为计算单位)。
**为什么要有首部长度呢?**因为我们还有选项,可能会增加数据,导致首部长度不仅仅是20字节,是变的,但如此则不知道数据从哪里开始了。所以需要标记上首部的长度。
4位首部长度范围为4比特位即->(0,15),单位:4字节(报头的宽度),所以真正的表示范围为0 -- 60个字节,报头长度固定是20,所以选项可以是0 -- 40个字节。
5.保留:占 6 位,保留为今后使用
6.六个标记位
1)紧急 URG:控制位。当 URG = 1 时,表明紧急指针字段有效,告诉系统此报文段中有紧急数据,应尽快传送 (相当于高优先级的数据)。
TCP 确保数据按序到达,接收方会根据序列号对数据包进行排序。但在某些场景下,我们需要让特定数据优先被处理,即"插队"。尽管标准 TCP 不允许真正意义上的数据顺序变更,但可以通过设置 URG 标记位 来指示紧急数据。
当 URG 标志设为 1 时,表示报文中有需要优先处理的数据,此时 16 位紧急指针生效,用于指示紧急数据在有效载荷中的偏移量。若 URG 为 0,则紧急指针无效。这一机制允许接收方提前识别并处理紧急部分,而不必等待所有前置数据到达。
2)确认 ACK:控制位。只有当 ACK =1 时,确认号字段才有效。当 ACK =0 时,确认号无效。
3)推送 PSH (PuSH) :控制位。接收 TCP 收到 PSH = 1 的报文段后,就尽快(即"推送"向前)交付接收应用进程,而不再等到整个缓存都填满后再交付。
因为在客户端发给服务器端数据时,不会直接发送,而是存到服务器端的缓冲区里面。
eg:
正常情况下:你会等水桶快满了(缓冲区有一定数据)或者接水结束(发送完毕)了,才把水倒进水壶(交付给应用)。
PSH=1 的情况:相当于送水的人敲一下桶说"这是最后一点了,赶紧处理吧!"。你一听到这个信号,就会立即把当前桶里已有的水倒进水壶,而不会干等着桶被完全填满
4)复位 RST (ReSeT) :控制位。当 RST=1 时,表明 TCP 连接中出现严重差错(如主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。
由于三次握手的每次握手都有时间差,所以重点不在"握手"而在"三次"上,那么客户端认为链接建立好了,是第三次握手我把ACK发出去就确认了,还是我要确认服务器把消息收到了才确认呢?事实上,最后一个ACK是没有应答的 ,所以客户端只要把第三次报文发出去了就认为链接建立好了,所以三次握手其实是在"赌",因为第三次ACK可能会丢,前面两个都有应答不怕丢,所以Tcp是允许链接建立失败的(失败了重新来就好)
当客户端发送第三次ACK成功,但是服务器没有收到ACK,那么客户端和服务器对与链接是否已经建立好了的认知不一致,导致客户端认为三次握手已经完成直接开始传输,但是服务器还停留在第二次握手上,此时服务器直接收到了来自客户端的数据咋办?
服务器就会推测以上情况可能发生,于是服务器在下一次给客户端应答的报文中,将RST标志位设置为1,告诉客户端刚刚的链接没有建好,就让客户端重新发起三次握手
所以RST标志位的作用就是**"当链接异常情况下,让双方重新建立链接"** ,上面说的"第三次握手报文丢失"只是众多链接异常情况下的一种(比如:浏览器连接被重置,服务器压力过大等)
本段原文链接:https://blog.csdn.net/aaqq800520/article/details/142374338
5)同步 SYN (SYNchronization) :控制位。 同步 SYN = 1 表示这是一个连接请求或连接接受报文。 当 SYN = 1,ACK = 0 时,表明这是一个连接请求报文段 。 当 SYN = 1,ACK = 1 时,表明这是一个连接接受报文段。
只有在三次握手阶段才会设置SYN,正常通信时SYN不会被设置
6)终止 FIN (FINish) :控制位。用来释放一个连接。 FIN=1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。
只有在断开连接阶段才会被设置,正常通信不会设置
7.窗口:(后面详细介绍)占 2 字节。 窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。
8.16位检验和:TCP实现可靠传输的关键检错机制,它通过一个巧妙的数学计算来确保数据在端到端传输中的完整性。发送方在构造TCP报文段时,会基于一个临时添加的"伪首部"(包含源和目的IP地址等关键信息)、真正的TCP首部以及数据载荷,计算出一个16位的反码和作为检验和填入报头。接收方收到报文后,使用相同方法重新计算。如果数据在传输过程中因硬件故障、信号干扰等发生任何比特错误,接收方的计算结果将与报文中的检验和不匹配,此时TCP会静默丢弃该损坏报文,并依赖超时重传机制重新获取正确副本。

9.紧急指针:占 2 字节。在 URG = 1时,指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据),指出了紧急数据的末尾在报文段中的位置
10.填充 :是TCP 设计中的一个重要细节,它的出现是为了让整个 TCP 首部长度满足"4 字节对齐"的规范要求。
具体原因如下:
核心原因:对齐
TCP 首部中的 "数据偏移" 字段(在图中也叫"首部长度")占 4 位,用于指示整个 TCP 首部有多长。它的单位是 "32 位字" ,也就是4 字节。
- 这意味着:TCP 首部长度必须是 4 字节的整数倍。
问题所在:选项字段长度可变
图中可以看到,在固定 20 字节的头部之后,有一个 **"选项(长度可变)"** 字段。
这个选项字段的长度可以是 0 字节、1 字节、4 字节或任意长度,不一定刚好是 4 字节的整数倍。
例如,选项只用了 3 个字节,那么整个头部长度就是 20 + 3 = 23 字节,这不符合 4 字节对齐的要求。
解决方案:用"填充"补齐
为了解决这个问题,"填充" 字段应运而生。它的作用就是用全 0 的字节,将 "选项"字段的末尾到整个首部的结束 之间的空位填满,使得整个 TCP 首部的总长度达到下一个 4 字节的整数倍。
三、TCP有哪些特性?
3.1 确认应答
确认应答是TCP 可靠传输的基础,它给每个字节的数据分配序列号,接收方收到完整有序的数据后,会发回带确认号的 ACK 报文,这个确认号代表 "期望收到的下一个字节序号"。
eg:M1发送的数据是从1-1000 那么ACK1会返回1001代表收到了1001以前的数据

3.2 超时重传
超时重传分为两种情况:
一、 主机A发送数据给B之后, 可能因为**⽹络拥堵**等原因, 数据⽆法到达主机B;
如果主机A在⼀个特定时间间隔内没有收到B发来的确认应答, 就会进⾏重发;

二、 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了

因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出哪些包是重复的包, 并且把重复的丢弃掉。这时候我们可以利⽤前⾯提到的序列号, 就可以很容易做到去重的效果:
因为接受的数据会存放到接收方的缓冲区内,发现有重复的数据传来,就会进行丢弃。
那么, 如果超时的时间如何确定?
最理想的情况下, 找到⼀个最⼩的时间, 保证 "确认应答⼀定能在这个时间内返回".
但是这个时间的⻓短, 随着⽹络环境的不同, 是有差异的.
如果超时时间设的太⻓, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证⽆论在任何环境下都能⽐较⾼性能的通信, 因此会动态计算这个最⼤超时时间.
Linux中(BSD Unix和Windows也是如此), 超时以500ms为⼀个单位进⾏控制, 每次判定超时重发的
超时时间都是500ms的整数倍.
如果重发⼀次之后, 仍然得不到应答, 等待 2*500ms后再进⾏重传.
如果仍然得不到应答, 等待4*500ms进⾏重传. 依次类推, 以指数形式递增.
累计到⼀定的重传次数, TCP认为⽹络或者对端主机出现异常, 强制关闭连接.
3.3 连接管理
TCP需要进行三次握手和四次挥手进行连接

3.3.1三次握手(建立连接)
三次握手的前提:
1.服务端提前启动,处于 监听状态 ,等着客户端来连。
2.TCP 通信靠序列号 保证数据不丢、不乱,双方得先把各自的初始序列号(ISN)告诉对方。TCP的编号是针对载荷的部分按照字节编号,当TCP连接建立好了之后,传输的第一个数据包的第一个字节的编号,不是从0/1开始,而是在三次握手阶段协商出这样的数据,作为起始编号,那为什么不从1开始呢?原因如下:
假设你是网购用户(客户端),快递站是仓库(服务端),你们约定用 快递编号来核对包裹,避免拿错。
如果你每次寄快递都从 编号 1 开始:
- 第一次你寄了 1、2、3 号包裹,仓库收到后正常签收。
- 第二次你又寄 1、2、3 号包裹,但第一次的 2 号包裹因为物流延迟,过了几天才到仓库。
- 仓库看到 "2 号包裹",会误以为是你第二次寄的,直接签收,导致包裹混乱。
如果你每次寄快递都随机选一个起始编号(比如第一次从 100 开始,第二次从 500 开始):
- 第一次寄 100、101、102 → 仓库签收。
- 就算 101 号包裹延迟了,第二次你寄 500、501、502 → 仓库看到 101 号,就知道这是历史失效包裹,直接丢弃。
3.带 SYN=1 的报文不能带数据 ,但会占一个序列号(相当于 "占个位置")。
那万一随机的序号也一样了呢? (前提第2点的第2小点)这种情况理论上存在,但实际中几乎不可能发生,而且就算真的出现重复,TCP 也有机制能规避风险。我们分两层说清楚:
1. 随机 ISN 的取值范围,决定了重复概率极低
TCP 的序列号是 32 位整数 ,取值范围是
0 ~ 2³²-1(也就是 0 到 4294967295),差不多是 43 亿 个可能的数值。现代操作系统生成 ISN 不是简单的 "伪随机",而是遵循一个严格的算法:
- 核心逻辑是 "基于时间戳 + 随机因子" 生成,比如 Linux 系统的 ISN 会和系统启动时间、时钟滴答数、随机种子绑定。
- 每次生成的 ISN 会和上一次的数值拉开极大的差距,不会在小范围内循环。
举个直观的例子:就算你一秒钟建立 10 万个 TCP 连接,要把 43 亿个序号用完,也需要约 430000 秒(差不多 5 天)。而实际中,一个客户端或服务端每秒的连接数远低于这个量级,而且旧连接的报文早就超时失效了。
2. 就算 ISN 重复,TCP 还有 "超时 + 上下文校验" 双保险
TCP 处理报文时,不是只看序列号,还要匹配 "连接上下文":
- 每个 TCP 连接都有唯一的 四元组 :
源IP + 源端口 + 目的IP + 目的端口。只有四元组完全匹配的报文,才会被当成当前连接的有效报文。- 就算两个不同连接的 ISN 碰巧一样,但它们的四元组肯定不同,系统会直接把不匹配的报文丢弃。
另外,TCP 报文有 超时机制:
- 一个报文在网络中存活的最长时间叫 MSL(最大报文段生存时间),通常是 30 秒~2 分钟。
- 超过 MSL 的报文会被路由器丢弃,就算 ISN 重复,旧连接的报文也早就在网络里消失了,不会干扰新连接。
1. 为什么 SYN=1 的报文不能带数据?
SYN报文的唯一作用是 "发起连接 + 协商初始序列号",它是 TCP 建立连接的 "握手信号",不是用来传数据的。
- 就像你打电话时,第一句只会说 "喂,能听到吗?",不会一开口就直接讲正事 ------ 因为对方还没准备好接收,讲了也可能乱。
- TCP 同理:只有等三次握手完成,双方进入
ESTABLISHED状态,才会开始传输真正的数据。- 如果允许
SYN报文带数据,万一连接建立失败,这些数据就会丢失;而且服务端还没确认序列号,收到数据也没法排序,反而增加混乱。2. 为什么明明没数据,却要消耗一个序号?
这是最关键的点:消耗序号 = 让这个 "握手信号" 变成一个 "可被确认的报文段"。
TCP 是可靠协议 ,"可靠" 的前提是 "发出去的报文,必须收到对方的确认,才算成功"。
SYN报文是客户端发的第一个报文,它需要服务端的确认(也就是第二次握手的ack=x+1),才能证明 "服务端收到了我的连接请求"。- 而 TCP 里,只有 "占了序号" 的报文,才需要被确认。
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号。此时客户端处于 SYN_SENT 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号seq=y。同时会把客户端的初始序号加1即为:seq=x+1作为ACK 的值,表示自己已经收到了客户端的 SYN,让客户端下次从x+1开始发数据,此时服务器处于 SYN_RCVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1,ACK报文段可以携带数据,不携带数据则不消耗序号。
建⽴连接的意义:
投⽯问路, 确认当前通信路径是否畅通.
协商参数, 通信双⽅共同确认⼀些通信中的必备参数数值
3.3.2四次挥手(断开连接)

1.第一次挥手 :客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
2.第二次挥手 :服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
3.第三次挥手 :如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1(三次挥手的时候也要再次发送ack确认收到,所以ack和第二次一样)),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
4.第四次挥手 :客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1(上次发送报文的序号是u,因为服务器发送的数据的编号和客户端的不同),ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
本段原文链接:https://blog.csdn.net/hyg0811/article/details/102366854
想⼀想, 为什么是TIME_WAIT的时间是2MSL?
MSL是TCP报⽂的最⼤⽣存时间, 因此TIME_WAIT持续存在2MSL的话
就能保证在两个传输⽅向上的尚未被接收或迟到的报⽂段都已经消失(否则服务器⽴刻重启, 可能会
收到来⾃上⼀个进程的迟到的数据, 但是这种数据很可能是错误的);
同时也是在理论上保证最后⼀个报⽂可靠到达(假设最后⼀个ACK丢失, 那么服务器会再重发⼀个 FIN. 这时虽然客⼾端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
3.3.3TCP协议中的几个关键状态
LISTEN:手机开机,信号良好,随时可以打电话
ESTABLISHED:连接已经建立,电话接听,可以说话了
CLOSE_WAIT:等待代码调用close
TIME_WAIT:等待FIN重传,应对最后一个ACK丢包的情况
3.3.4面试题
1.三次握手建立连接的时候可以变成四次吗?两次呢?
1)可以,但是没有必要,第二次握手的ack和syn合并为一次有利于提高效率
2)不可以,无法完成通信双发送能力和接受能力的验证
2.四次挥手可以合并为三次吗?
想象一下两个人在打电话(A和B),通话即将结束:
A说:"我说完了,要挂电话了。" → 对应 FIN
B说:"好的,我知道了。" → 对应 ACK
(此时,A到B方向的通信通道关闭了,A不会再发送数据,但B可能还有话没说完)
B继续说:"另外,我也说完了。" → 对应 FIN
A说:"好的,那就挂了吧。" → 对应 ACK
(此时,B到A方向的通信通道也关闭了,双方都确认关闭)
**为什么不能是三次?** 因为在第2步B说完"好的"之后,B可能还需要一些时间来组织最后的语言(处理尚未发完的数据)。B不能把"好的"和"我也说完了"合并成一句话说,因为它需要时间准备。在TCP中,ACK是内核立刻回复的,而应用层发出FIN则需要等待数据都处理完毕
在某些特定情况下,挥手看起来像是"三次"!
当服务器在收到客户端的FIN时,恰好也没有任何数据要发送了 ,那么它可以将第二次挥手(ACK)和第三次挥手(自己的FIN)合并成一个报文发送给客户端。
这个合并的报文同时设置了
ACK = 1和FIN = 1。
3.4 滑动窗口
刚才我们讨论了确认应答策略, 对每⼀个发送的数据段, 都要给⼀个ACK确认应答. 收到ACK后再发送下⼀个数据段. 这样做有⼀个⽐较⼤的缺点, 就是性能较差,需要等待对方返回ACK,尤其是数据往返的时间较⻓的时候,那有没有什么办法能让对方返回ACK的同时,我也能发送数据呢?
可以想到,既然这样⼀发⼀收的⽅式性能较低, 那么我们⼀次发送多条数据, 就可以⼤⼤的提⾼性能

窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值,上图的窗⼝⼤⼩就是4000个字节(即从4000字节往后的数据开始等待对方的ACK)(四个段)
发送前四个段的时候, 不需要等待任何ACK, 直接发送;
收到第⼀个ACK后, 滑动窗⼝向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗⼝, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只
有确认应答过的数据, 才能从缓冲区删掉;
窗⼝越⼤, 则⽹络的吞吐率就越⾼;

下图详细解释了一下怎么实现滑动的,不懂的可以自己手动画一下:

那么如果出现了丢包, 如何进⾏重传? 这⾥分两种情况讨论
情况⼀: 数据包已经抵达, ACK被丢了
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进⾏确认,因为在接受缓冲区里的数据都是按顺序应答的,你既然都能发到5000了,那之前的的数据肯定就收到了。

情况⼆: 数据包就直接丢了.
当某⼀段报⽂段丢失之后, 发送端会⼀直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是
1001" ⼀样;
如果发送端主机连续三次收到了同样⼀个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新
发送;
这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就 已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为 "⾼速重发控制"(也叫 "快重传")

3.5 流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送 端继续发送, 就会造成丢包, 继⽽引起丢包重传等等⼀系列连锁反应.
因此TCP⽀持根据接收端的处理能⼒ , 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow
Control);
接收端将**⾃⼰可以接收的缓冲区⼤⼩放⼊ TCP ⾸部中的 "窗⼝⼤⼩" 字段**, 通过ACK端通知发送端;
窗⼝⼤⼩字段越⼤, 说明⽹络的吞吐量越⾼;
接收端⼀旦发现⾃⼰的缓冲区快满了, 就会将窗⼝⼤⼩设置成⼀个更⼩的值通知给发送端;
发送端接受到这个窗⼝之后, 就会减慢⾃⼰的发送速度;
如果接收端缓冲区满了, 就会将窗⼝置为0; 这时发送⽅不再发送数据, 但是需要定期发送⼀个窗⼝探 测数据段, 使接收端把窗⼝⼤⼩告诉发送端

接收端如何把窗⼝⼤⼩告诉发送端呢? 回忆我们的TCP⾸部中, 有⼀个16位窗⼝字段, 就是存放了窗⼝⼤⼩信息;
那么16位数字最⼤表⽰65535, 那么TCP窗⼝最⼤就是65535字节么?
实际上, TCP⾸部40字节选项中还包含了⼀个窗⼝扩⼤因⼦M, 实际窗⼝⼤⼩是 窗⼝字段的值左移 M 位
3.6拥塞控制
虽然TCP滑动窗⼝能够⾼效可靠的发送⼤量的数据,减少由于ACK所占用的时间。但是如果在刚开始阶段就发送⼤量的数据, 仍然可能引发问题
因为⽹络上有很多的计算机, 可能当前的⽹络状态就已经⽐较拥堵. 在不清楚当前⽹络状态下, 贸然发送 ⼤量的数据, 是很有可能引起雪上加霜的
所以TCP引⼊ 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的⽹络拥堵状态, 再决定按照多⼤的速度传输数据

在某段时间,若对网络中某资源的需求超过了该资源所能提供的可用部分, 网络的性能就要明显变坏,整个网络的吞吐量将随输入负荷的增大而下降。这种现象称为拥塞 (congestion)。
那增加资源能解决拥塞吗?
不能,而且还可能使网络的性能更坏。
例如: 增大缓存,但未提高输出链路的容量 和处理机的速度 ,排队等待时间将会大大增加,引起大量超时重传,解决不了网络拥塞; 提高处理机处理的速率会将瓶颈转移到其他地方; 拥塞引起的重传并不会缓解网络的拥塞,反而会加剧网络的拥塞。
此处引⼊⼀个概念称为拥塞窗⼝(cwnd), 下方列举的是TCP Reno版本的:
控制拥塞窗口变化的原则?
只要网络没有出现拥塞,拥塞窗口就可以再增大一些,以便把更多的分组发送出去,提高网络的利用率。 但只要网络出现拥塞或有可能出现拥塞,就必须把拥塞窗口减小一些,以减少注入到网络中的分组数,缓解网络出现的拥塞。
四种拥塞控制算法( RFC 5681) :
慢开始 (slow-start)
拥塞避免 (congestion avoidance)
快重传 (fast retransmit)
快恢复 (fast recovery)
那么如何判断拥塞呢?超时重传计时器超时:网络已经出现了拥塞。
收到 3 个重复的确认ACK:预示网络可能会出现拥塞

下面来解释一下上图的过程
首先,什么是慢开始呢?
先发少量的数据, 探探路, 摸清当前的⽹络拥堵状态, 再决定按照多⼤的速度传输数据,进行指数增长。
例如:
第1轮:发1个 → 收到1个ACK → cwnd=2 (下次能发2个)
第2轮:发2个 → 收到2个ACK → cwnd=4 (下次能发4个)
第3轮:发4个 → 收到4个ACK → cwnd=8
...(1, 2, 4, 8, 16...)
当TCP开始启动的时候, 慢开始阈值等于窗⼝最⼤值;
当达到了这个阈值之后,会进行拥塞避免阶段,让拥塞窗口 cwnd 缓慢地增大,避免出现拥塞当拥(不再按照指数⽅式增⻓, ⽽是按照线性⽅式增⻓)
无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(重传定时器超时):
- ssthresh = cwnd/2
- cwnd = 1
- 执行慢开始算法
需要注意的是:
当 cwnd < ssthresh 时,使用慢开始算法。
当 cwnd > ssthresh 时,停止使用慢开始算法,改用拥塞避免算法。
当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞避免算法。
此外,还有快重传算法:1.慢开始门限 ssthresh = 当前拥塞窗口 cwnd / 2 ;
2.新拥塞窗口 cwnd = 慢开始门限 ssthresh ;
3.执行拥塞避免算法,使拥塞窗口缓慢地线性增大(加法增大 AI)
目的:让发送方尽早知道发生了个别报文段的丢失。
发送方只要连续收到三个重复的确认,就立即进行重传(即"快重传"),这样就不会出现超时。 使用快重传可以使整个网络的吞吐量提高约 20%。 快重传算法要求接收方立即发送确认,即使收到了失序的报文段,也要立即发出对已收到的报文段的重复确认。
这里要进行区分的是:
1.当检测到少量丢包时(收到3个重复ACK),TCP会触发快恢复算法不回到起点,而是直接线性增长(cwnd降低一半后线性增加)
2.当检测到⼤量的丢包, 我们就认为⽹络发生拥塞,回到起点(cwnd=1),然后先指数增长(慢启动阶段),再线性增长
总结下来就是下面这张图:
用生活中的例子帮大家理解一下:
想象TCP是一个开货车的司机,他的任务是以最快的速度送货,但又不能把路搞堵。
1. 起步阶段(慢启动)
司机刚上路,不知道路况。他先发1箱货,送到了。心想:"路挺空嘛!"下次就发2箱,又全送到了。他就4箱、8箱、16箱... 指数级增加发货量,疯狂试探路的极限。
2. 平稳阶段(拥塞避免)
当货量接近他心中的一个"安全阈值"时,他变谨慎了。不再翻倍发,而是一箱一箱地慢慢加(比如16箱 -> 17箱 -> 18箱...),这叫线性增长。他小心翼翼地逼近道路的真正上限。
关键来了!路上出状况了,有两种可能:
状况A:最坏情况------大堵车(超时)
司机发了20箱货出去,结果一箱都没收到回执,完全石沉大海。
司机的判断:路彻底堵死了!完了!
司机的行动 :反应极其激烈!
立刻把"安全阈值"砍到一半(记在心里)。
下一趟只敢发1箱货(回到起点)。
重新开始"慢启动"的疯狂试探过程。
- 这就是Tahoe的做法,也是Reno在这种情况下会做的。
状况B:常见小状况------只丢了一箱货(3个重复ACK)
司机发了编号1、2、3、4、5的货。3号货在半路掉了,但4号和5号都顺利送到了。
收货方每次收到4号、5号都会催:"货收到了,但我还是要3号!"。当司机连续听到3遍"我要3号"时,他明白了。
司机的判断 :哦,只是3号丢了(一个小坑),但4号5号能到,路没全堵,还能走车!
司机的行动(Reno的智慧所在------快速恢复):
快速重传 :不等了,立刻补发那箱丢失的3号货。(不等超时,效率更高)
不矫枉过正 :**他绝不会把货量降到1箱那么夸张。** 他会:
把"安全阈值"设为当前货量的一半(记在心里)。
把当前发货量也降到这个新阈值。
平滑过渡 :补发3号并确认收到后,他不重新开始慢启动 ,而是直接进入平稳阶段(拥塞避免),继续一箱一箱地线性增加。
- "快速恢复"的精髓就是:别为了一个小坑(丢一个包),就把整个车队叫停、退回起点。
3.7延迟应答
如果接收数据的主机⽴刻返回ACK应答, 这时候返回的窗⼝可能⽐较⼩.
假设接收端缓冲区为1M. ⼀次收到了500K的数据; 如果⽴刻应答, 返回的窗⼝就是500K;
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到⾃⼰的极限, 即使窗⼝再放⼤⼀些, 也能处理过来;
如果接收端稍微等⼀会再应答, ⽐如等待200ms再应答, 那么这个时候返回的窗⼝⼤⼩就是1M;
⼀定要记得, 窗⼝越⼤, ⽹络吞吐量就越⼤, 传输效率就越⾼. 我们的⽬标是在保证⽹络不拥塞的情况下尽量提⾼传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
数量限制: 每隔N个包就应答⼀次;
时间限制: 超过最⼤延迟时间就应答⼀次;
具体的数量和超时时间, 依操作系统不同也有差异; ⼀般N取2, 超时时间取200ms;

3.8捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客⼾端服务器在应⽤层也是 "⼀发⼀收" 的. 意味着客⼾端 给服务器说了 "How are you", 服务器也会给客⼾端回⼀个 "Fine, thank you";
那么这个时候ACK就可以搭顺⻛⻋, 和服务器回应的 "Fine, thank you" ⼀起回给客⼾端
3.9面向字节流
创建⼀个TCP的socket, 同时在内核中创建⼀个 发送缓冲区 和⼀个 接收缓冲区;
调⽤write时, 数据会先写⼊发送缓冲区中;
如果发送的字节数太⻓, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区⾥等待, 等到缓冲区⻓度差不多了, 或者其他合适的时机发
送出去;
接收数据的时候, 数据也是从⽹卡驱动程序到达内核的接收缓冲区;
然后应⽤程序可以调⽤read从接收缓冲区拿数据;
另⼀⽅⾯, TCP的⼀个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这⼀个连接, 既可以读数据,
也可以写数据. 这个概念叫做全双⼯
由于缓冲区的存在, TCP程序的读和写不需要⼀⼀匹配, 例如:
写100个字节数据时, 可以调⽤⼀次write写100个字节, 也可以调⽤100次write, 每次写⼀个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以⼀次read 100个字节, 也可以⼀次read⼀个字节, 重复100次;
3.10粘包问题
⾸先要明确, 粘包问题中的 "包" , 是指的应⽤层的数据包.
在TCP的协议头中, 没有如同UDP⼀样的 "报⽂⻓度" 这样的字段, 但是有⼀个序号这样的字段.
- 站在传输层的⻆度, TCP是⼀个⼀个报⽂过来的. 按照序号排好序放在缓冲区中.
- 站在应⽤层的⻆度, 看到的只是⼀串连续的字节数据.
那么应⽤程序看到了这么⼀连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是⼀个完整的应
⽤层数据包.
那么如何避免粘包问题呢?--> 明确两个包之间的边界
对于定⻓的包, 保证每次都按固定⼤⼩读取即可; 例如上⾯的Request结构, 是固定⼤⼩的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变⻓的包, 可以在包头的位置, 约定⼀个包总⻓度的字段, 从⽽就知道了包的结束位置;
- 对于变⻓的包, 还可以在包和包之间使⽤明确的分隔符(只要保证分隔符不和正⽂冲突即可);
3.11异常管理
进程终⽌: 进程终⽌会释放⽂件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终⽌的情况相同
机器掉电/⽹线断开:
当客户端正常访问服务器时,客户端突然掉了,但是服务器在短时间内不知道,所以会维持与客户端的连接,但是不会一直维护,因为Tcp是有保活策略的:
服务器会定期检查客户端的在线情况的,如果连续多次没有ACK应答,服务器会自动关闭连接
此外,客户端也会定期向服务器"报平安",所以如果服务器一段时间内没有收到客户端的消息,服务器也会关闭连接
那这样会发生什么呢?
客户端视角(数据发送方):
假设客户端是发送请求的一方(比如正在上传文件或提交表单)。
客户端突然死亡,它的操作系统没有机会将数据从应用缓冲区发送到网络 ,也没有机会处理最后的ACK。
**结果:客户端内存中未发送的数据会永久丢失。** 这些数据根本没有进入网络,服务器永远不知道它们存在过。
服务器视角(数据接收方):
假设服务器是接收数据的一方。
在客户端死亡之前 已经到达服务器的数据包,服务器会正常接收并放入TCP接收缓冲区,然后发送ACK确认。
只要数据包在网络上成功传输并抵达服务器缓冲区,服务器的数据就是安全的。
服务器感知客户端死亡 的唯一方式是:再也收不到任何数据包,保活探测也失败。
