欢迎光临
我们一直在努力

《Linux 网络编程》深入理解 TCP 协议(六):TCP 拥塞控制、应答机制、异常处理流程全解析

🔥小叶-duck:个人主页

 ❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》 《Linux系统从入门到实践》《Linux网络从入门到实践》 《Qt 方寸极境》 《MySQL》

未择之路,不须回头 已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、TCP 拥塞控制机制

  1.1 为什么需要拥塞控制

  1.2 引出疑问:慢启动指数增长会不会持续加重网络负载?

   1.3 修正:发送窗口的真正计算方式

  1.4 慢启动与拥塞避免

    1.4.1 慢启动机制

    1.4.2 为什么需要 ssthresh,切换为线性增长?

  1.5 网络拥塞发生时的处理(乘法减小)

  1.6 拥塞控制完整循环流程

  1.7 拥塞控制与流量控制的区分

二、TCP 延迟应答机制

  2.1 问题引出:接收方到底什么时候回复 ACK?

  2.2 为什么需要延迟应答?

  2.3 延迟应答的实现机制:两个限制条件

  2.4 延迟应答与滑动窗口的完整关系(关键补充)

三、TCP 捎带应答机制

  3.1 捎带应答的原理

  3.2 捎带应答的优势

  3.3 延迟应答与捎带应答的配合

四、TCP 的本质特性:面向字节流

  4.1 TCP 缓冲区机制

  4.2 读写不匹配的特性

五、粘包问题:面向字节流带来的衍生问题

  5.1 粘包产生的根本原因

  5.2 为什么 UDP 不存在粘包

  5.3 粘包的三类主流解决方案

六、TCP 异常情况处理

  6.1 进程终止

  6.2 机器重启

  6.3 机器掉电 / 网线断开

七、TCP / UDP对比

核心特性对比

适用场景

八、经典面试题:HTTP 获取网页的完整过程

  8.1 浏览器处理 URL,先确定 “访问目标”

  8.2 先看缓存,能不请求就不请求

    8.2.1 强缓存

    8.2.2 协商缓存

  8.3 DNS 解析:把域名翻译成 IP 地址

  8.4 建立 TCP 连接,必要时再完成 TLS 握手

    8.4.1 HTTP 场景:建立 TCP 连接

    8.4.2 HTTPS 场景:TCP 之后还要进行 TLS 握手

  8.5 浏览器构造并发送 HTTP 请求

    8.5.1 请求行

    8.5.2 请求头

    8.5.3 空行

    8.5.4 请求体

  8.6 服务器处理请求并返回 HTTP 响应

    8.6.1 状态行

    8.6.2 响应头

    8.6.3 空行

    8.6.4 响应体

  8.7 浏览器接收响应,并根据状态码做不同处理

  8.8 TCP 连接如何关闭:短连接与长连接

    8.8.1 短连接

    8.8.2 长连接

  8.9 浏览器解析 HTML,并开始渲染页面

  8.10 完整流程总结

面试中重点答什么?

结束语


前言

      在上一篇文章中,我们学习了滑动窗口、丢包重传与流量控制,理解了 TCP 如何保障收发双方传输速率匹配、实现可靠传输。

      本篇我们继续深入 TCP 协议,将重点讲解拥塞控制、延迟应答、捎带应答这些核心优化机制,剖析 TCP 面向字节流的本质,理清粘包问题产生根源与解决方案,同时分析各类网络异常场景下 TCP 的处理逻辑。文章末尾还会附上经典面试题:HTTP 获取网页的完整流程,串联前面所有 TCP 知识点,打通理论和实际网络访问场景,帮助大家建立完整的 TCP 知识体系。

一、TCP 拥塞控制机制

  1.1 为什么需要拥塞控制

      我们先思考一个问题:流量控制已经通过滑动窗口限制发送方的发送量,保证接收端缓冲区不会溢出,那是不是代表发送方就可以毫无顾虑,尽可能多发报文?       答案是否定的。流量控制只关心接收端的接收上限,却忽略了中间整条网络链路的承载能力。

      丢包要区分两种完全不同的场景:

  • 少量丢包:仅个别报文丢失,大概率只是单次传输异常,网络链路本身没有拥堵;
  • 大量连续丢包:代表链路中路由器报文积压严重,也就是网络拥塞。

      如果网络已经拥塞,发送方依旧持续大量发送报文,路由器队列会持续堆积,触发更多丢包。丢失的报文会触发超时重传,大量重传报文进一步挤占链路资源,形成拥塞→丢包→重传→更加拥塞的恶性循环,严重时会造成整个网络瘫痪。

      TCP 拥塞控制的核心目标:发送方动态探测网络链路负载状态,根据网络实际承载能力调整发送速率,保护整个共享网络。

      小故事类比:一场期末考试,100 名学生参加。如果只有两三个人没及格,是学生自身问题;但如果大量学生挂科,就不是学生个体的问题,而是考试、老师本身这类公共资源出了问题。网络也是多主机共享的公共资源,大面积丢包,代表网络本身拥堵。

      而 TCP 拥塞控制的核心思想是:网络是所有主机共享的公共资源,当网络出现拥塞时,所有主机都必须主动降低发送速率,避免拥塞进一步恶化。如果所有主机在拥塞时都继续重传数据,只会形成 “拥塞→重传→更严重拥塞” 的恶性循环,最终导致整个网络瘫痪。

  1.2 引出疑问:慢启动指数增长会不会持续加重网络负载?

      既然慢启动阶段发送报文的数量是指数增长,我们知道指数增长的速率是非常快的,那这种方式难道不会持续加重网络负担,更容易引发拥塞吗?

      我们可以借助一个小故事理解: 有一个农民要给地主还债,但是地主只要求农民后面每天按指数增长的方式给地主提供足够的米,一个月之后就不需要还债了。从第二天开始,每天给地主送米,第一天 1 粒,第二天 2 粒,第三天 4 粒,按指数增长。前期米粒数量很少,农民很轻松;但指数增长后期数量会爆炸式上涨,远远超出预期。

      慢启动正是利用了指数增长前期增长慢这个特点。当网络刚从拥塞中恢复,指数前期只发送少量报文,给网络留出充足时间消化积压数据;同时指数增长能快速探测网络带宽。 但指数增长后期报文量上涨过快,很容易瞬间超出网络承载上限,所以 TCP 引入慢启动阈值 ssthresh,用来切换增长模式。

   1.3 修正:发送窗口的真正计算方式

      在上篇文章 《Linux 网络编程》深入理解 TCP 协议(五):滑动窗口、丢包重传与流量控制详解 中我们在讲解滑动窗口的时候,我们说滑动窗口的值是由报头16位窗口大小的值(也就是收方通告窗口)决定的,但是当时由于还没有讲解拥塞控制机制,所以只能那样理解滑动窗口。

      但是现在我们讲解了拥塞控制也就清楚了,滑动窗口作为同时发送多个报文的上限值,不只是由对方当前接收缓冲区剩余空间大小直接决定,网络的状态也会影响发送方发送报文的数量。       所以,TCP 引入拥塞窗口 cwnd,它是发送方本地维护的整型变量,作用是衡量当前网络链路能够承载的数据上限。网络状态实时变化,拥塞窗口的值也会持续动态更新。

      发送方实际可发送的未确认数据量公式: 发送窗口 = min (接收方通告窗口,拥塞窗口)

  • 接收方通告窗口:约束条件,保护接收端,防止接收缓冲区溢出(流量控制)
  • 拥塞窗口:约束条件,保护整条网络链路,防止路由器队列溢出(拥塞控制) 两者取最小值,谁更小,谁就是当前阶段的主要矛盾,同时兼顾接收方能力与网络负载。

      那就有一个问题了:这个所谓的拥塞窗口的值我们怎么知道是多少呢?或者说怎么得到这个值?       所以我们就需要讲解 TCP 拥塞控制的慢启动与拥塞避免了。

  1.4 慢启动与拥塞避免

    1.4.1 慢启动机制

慢启动里的 “慢”,指初始发送速率慢,并不是增长速度慢。

  • TCP 连接刚建立,拥塞窗口初始值 = 1 个 MSS(最大报文段大小);
  • 每收到一个 ACK 确认,拥塞窗口 +1 MSS;
  • 每经过一个 RTT 往返时间,拥塞窗口翻倍,呈现指数增长。
  •       这种指数级的增长速度非常快,能够迅速探测网络的可用带宽。但为了避免增长过快导致网络拥塞,TCP 引入了慢启动阈值 (ssthresh)。

        1.4.2 为什么需要 ssthresh,切换为线性增长?

          新的疑问来了:我们能不能全程只用指数增长探测网络带宽? 不行。       指数增长前期平缓,但后期跨度极大。举例:发送 512 个报文时网络正常,下一轮直接跳到 1024 个报文,瞬间触发拥塞。这种大幅度跳跃,无法精细定位网络的真实承载上限,探测结果非常模糊。

          为此引入慢启动阈值 ssthresh,作为增长模式的分界点:

    • cwnd < ssthresh:慢启动阶段,指数增长,快速试探带宽;
    • cwnd ≥ ssthresh:进入拥塞避免阶段,改为线性增长。

          拥塞避免规则:每经过一个 RTT,拥塞窗口只增加 1 个 MSS。 线性增长每次窗口增量很小,可以精细试探网络剩余带宽,更精准找到网络的临界承载值,避免一次性涌入大量报文造成网络拥堵。

      1.5 网络拥塞发生时的处理(乘法减小)

    当检测到网络拥塞、出现丢包时,TCP 会执行下面三步操作:

  • 把慢启动阈值 ssthresh 更新为当前拥塞窗口 cwnd 的一半;
  • 将拥塞窗口 cwnd 重置为 1 个 MSS;
  • 重新回到慢启动阶段,再次探测网络状态。
  •       这个机制叫做乘法减小。它的核心意义不只是单纯降低发送速率缓解当下的网络拥堵,更关键在于保存本次探测得到的网络带宽信息,优化下一轮探测的效率。

          我们结合折线图来理解:当拥塞发生时,此时的拥塞窗口 24,代表网络在 cwnd=24 时达到承载上限。我们将ssthresh更新为 24/2=12。

    • 如果曾经触发拥塞的 cwnd 数值很大,代表之前网络整体质量较好。减半之后得到的 ssthresh 依旧偏大,下一轮慢启动指数增长可以更快抵达阈值,尽早切换到拥塞避免的线性增长阶段,精细探测带宽上限。
    • 如果上次拥塞发生在较小的 cwnd,说明网络本身带宽有限,ssthresh 减半后数值也偏小,避免下一次探测时再次快速冲击网络上限。

          简单来说:ssthresh会记录上一次网络临界容量的一半,相当于给下一轮探测提供一个参考边界。如果没有乘法减小,每次拥塞之后都直接丢弃之前探测到的带宽信息,每次都只能从头盲猜,探测网络上限就会非常低效。

          执行乘法减小之后,cwnd 重置为 1,重新走慢启动流程,再次探测当前网络的健康程度。

      1.6 拥塞控制完整循环流程

          TCP 拥塞控制是持续循环、动态探测的闭环过程:

  • 慢启动阶段:cwnd 指数增长,直到 cwnd 到达 ssthresh;
  • 拥塞避免阶段:cwnd 线性缓慢增长,持续试探网络上限,直到检测到拥塞丢包;
  • 拥塞处理:ssthresh 减半、cwnd 重置为 1MSS,重新进入慢启动; 不断循环往复,动态适配网络实时状态。
  •       补充思考:拥塞窗口在线性增长阶段,会不会一直无限增大? 逻辑上如果网络极其健康无任何丢包,可以持续上涨;但实际会受网卡硬件上限、接收窗口大小约束,不会无限增长。

      1.7 拥塞控制与流量控制的区分

    • 流量控制:保护接收端,防止接收缓冲区溢出,依据接收方返回的通告窗口;
    • 拥塞控制:保护整条网络链路,防止路由器队列溢出,依据发送方探测得到的拥塞窗口; 发送方能够发送的数据上限,取两者之中更小的值。

    二、TCP 延迟应答机制

      2.1 问题引出:接收方到底什么时候回复 ACK?

          回顾前面学的滑动窗口机制:发送窗口的大小 = min (rwnd, cwnd),其中 rwnd(接收窗口)是由接收方在 ACK 报文里通过 16 位窗口字段告知发送方的。

          那么问题来了:接收方收到数据之后,应该在什么时候回复这个 ACK?

    • 如果立刻回复:此时接收缓冲区内还堆积着应用层没来得及取走的数据,rwnd 会比较小;
    • 如果稍微延迟一会再回复:应用层可能已经消费了一部分数据,缓冲区腾出空间,rwnd 会变得更大。

    延迟应答,正是抓住这一点设计出来的。

      2.2 为什么需要延迟应答?

    延迟应答是 TCP 为了最大化网络吞吐量而设计的优化机制。先看一个具体例子:

    场景:接收方 B 的接收缓冲区大小为 1MB,一次收到了 500KB 的数据。

    立刻应答(不延迟):

    • B 收到 500KB 数据 → 缓冲区剩余 500KB → 立刻发 ACK,win = 500KB
    • A 收到 win=500KB → 下次最多只能发 500KB

    延迟应答(等待 200ms):

    • B 收到 500KB 数据 → 暂时不回复 ACK
    • 100ms 后:B 的应用层读取了 400KB → 缓冲区剩余 900KB
    • 200ms 后:B 发送 ACK,win = 900KB
    • A 收到 win=900KB → 下次可能就可以发 900KB

    结论:延迟应答让窗口从 500KB 扩大到 900KB,传输效率更高。

          核心思想一句话:窗口越大,网络吞吐量就越大,传输效率就越高。延迟应答给了接收方应用程序更多时间去读取缓冲区数据,从而让 rwnd 变得更大。       它的目标,就是在保证网络不拥塞的前提下,尽可能增大接收窗口。

      2.3 延迟应答的实现机制:两个限制条件

          不是所有数据包都可以无限期延迟应答,否则发送方可能会因长时间收不到 ACK 而触发超时重传。TCP 通过两个严格的限制条件来保证可靠性,这两个条件是“或” 的关系,满足其中一个就必须立刻应答:

  • 数量限制:每隔 N 个数据包必须应答一次,主流操作系统中 N = 2;
  • 时间限制:超过最大延迟时间必须应答一次,主流操作系统中最大延迟为 200ms。
  •   2.4 延迟应答与滑动窗口的完整关系(关键补充)

          在上面的例子中我们说了对于延迟应答当 A 收到 win=900KB,结果是 下次可能就可以发 900KB。       这里我们并不是肯定的说下次就一定能发900KB,原因就在于延迟应答不一定能保证滑动窗口变大,原因有三点:

  • rwnd 受限于接收方应用层的处理速度:如果应用层读取慢,延迟再久 rwnd 也不会变大;
  • cwnd 可能成为瓶颈:即使 rwnd 变大了,如果网络拥堵(cwnd 很小),实际发送窗口依然是 cwnd;
  • 最终是概率问题:延迟应答只是提高了窗口变大的概率,而不是保证。
  •       一个重要的核心洞察:只要发送的报文数量足够多、样本量足够大,概率问题就会变成必然事件

    • 单次传输:延迟应答可能有效,也可能无效,效率提升是概率性的,甚至可能 “浪费” 200ms;
    • 大量传输:延迟应答在大量样本中平均收益为正,200ms 延迟换来的窗口增大,平均下来收益 > 成本,效率提升是确定性的。

    三、TCP 捎带应答机制

          捎带应答(携带应答)是在延迟应答基础上的进一步优化,它充分利用了 TCP全双工通信的特性。

      3.1 捎带应答的原理

          在很多客户端 – 服务器交互场景中,应用层的通信模式是典型的“一发一收”:客户端发送一个请求,服务器处理后返回一个响应。

          在这种模式下,服务器不需要单独发送一个 ACK 报文来确认客户端的请求,而是可以把 ACK 标志位和确认序号,捎带在服务器的响应数据报文中一起发送给客户端。这样就把 ACK 确认和响应数据合并到了同一个 TCP 报文段中。

      3.2 捎带应答的优势

  • 减少报文数量:一个报文段既可以携带数据,又可以携带 ACK 确认;
  • 降低网络开销:减少了 TCP 报头的重复传输;
  • 提高传输效率:避免了单独发送 ACK 报文的额外开销。
  •       捎带应答是 TCP 中非常主流的内部策略,在实际应用中被广泛使用。

      3.3 延迟应答与捎带应答的配合

          两者目标一致,但分工不同,组合使用效果更好:

    机制作用关系
    捎带数据 把 ACK 和数据合并发送 减少报文数量
    延迟应答 等待应用层读取数据后再回复 ACK 增大 rwnd 窗口

          共同目标:提高网络传输效率。       一个是减报文数,一个是增大单次发送量,两者结合,是 TCP 最核心的两个性能优化机制,组合使用能把 TCP 的传输效率提升数倍。

    四、TCP 的本质特性:面向字节流

          面向字节流是 TCP 最核心的特征,也是 TCP 和 UDP 最根本的区别。面向字节流的含义:TCP 只负责把字节流从一端可靠传输到另一端,本身不维护任何应用层数据包边界。

      4.1 TCP 缓冲区机制

    创建 TCP socket 时,Linux 内核会同步为这个 socket 分配发送缓冲区与接收缓冲区。

    • 调用write()发送数据:数据只是从用户空间拷贝到内核发送缓冲区,函数随即返回。 什么时候发包、一次发送多少字节,由内核 TCP 协议栈自主决定。
      • 数据较短时,内核可能在缓冲区暂存等待,直到缓冲区长度合适或者其他合适的时机;
      • 数据较长时,内核会拆分成多个 TCP 报文段发出。
    • 接收数据:数据包先进入内核接收缓冲区,再由应用程序调用read()从缓冲区读取字节。

      4.2 读写不匹配的特性

          缓冲区的存在,使得 TCP 的读、写操作相互异步,读写次数不需要一一匹配。

    • 发送端:发送 100 字节,可以一次 write 写入 100 字节,也可以分 100 次每次 write 1 字节。
    • 接收端:读取这 100 字节,既可以一次 read 读完 100 字节,也可以循环 100 次每次 read 读 1 字节。

          补充对比:UDP 仅有发送缓冲区,不存在真正意义上的接收缓冲区。如果应用层没有及时读取 UDP 收到的数据,新到达的数据会直接被丢弃。

    五、粘包问题:面向字节流带来的衍生问题

    注意:粘包里所说的 “包”,指代应用层数据包,并不是 TCP 传输层的报文段。

      5.1 粘包产生的根本原因

          TCP 面向字节流,协议本身没有应用层报文边界。传输层虽然是分段发送报文,但应用层拿到的只是一串连续的字节流,程序无法判断一段字节从哪里开始、到哪里结束是一个完整业务数据包。

    举个例子:应用层连续发送两个报文Hello和World,会出现多种情况:

  • 内核合并,一次性发送HelloWorld;
  • 拆分成两段发送,比如Hel、loWorld;
  • 拆分成更多分片。
  •       无论内核如何拆分合并,应用层读取到的只是连续字节流,无法自动区分原始两个数据包的边界,这就是粘包。

      5.2 为什么 UDP 不存在粘包

          UDP 是面向数据报的协议。       UDP 头部自带 16 位长度字段,记录当前报文的总长度,并且 UDP 报头的长度是固定的8字节大小,所以也就是说对于 UDP 而言是完全清楚每个数据报文中有效载荷的具体长度的。       内核会把完整 UDP 报文一次性交付给应用层,要么收到完整报文,要么收不到,永远不会读到半个报文,因此没有粘包。

      5.3 粘包的三类主流解决方案

    TCP 协议不会自动处理报文边界,必须由应用层自定义协议解决,三种常用方案:

  • 定长包:约定所有数据包固定大小。接收端每次读取固定字节,即为完整报文。实现最简单,但灵活性差,存在带宽浪费。
  • 包头增加长度字段(工程最常用):报文头部使用固定字节存储整个数据包总长度。接收方先读取长度字段,再根据长度读取后续业务数据。HTTP 的Content-Length就是典型例子。
  • 特殊分隔符:数据包之间插入专属分隔符,保证分隔符不会出现在报文正文。接收端扫描字节流识别分隔符拆分报文,例如 FTP 的\\r\\n。缺点是需要遍历扫描数据,性能偏低。
  • 六、TCP 异常情况处理

    TCP 内置了完备的异常处理逻辑,应对进程崩溃、机器重启、断网断电等场景。

      6.1 进程终止

          通信一方进程崩溃退出时,操作系统会回收该进程所有文件描述符。TCP 连接对应的 fd 被释放,内核自动向对端发送 FIN 报文,正常启动四次挥手关闭连接。 这个流程和应用层主动调用close()关闭连接完全一致,属于正常的连接关闭。

      6.2 机器重启

          机器正常重启时,操作系统会逐个终止本机所有进程。每个 TCP 连接都会触发 FIN 报文,执行四次挥手释放连接。 这也是关机时系统速度变慢的原因之一,操作系统需要等待大量 TCP 连接完成关闭流程。

      6.3 机器掉电 / 网线断开

          突发断电或者网线拔出时,故障主机没有机会发送 FIN 报文。对端主机无法感知连接失效,依旧认为连接存活,形成半开连接。TCP 提供两种处理机制:

  • 写入操作触发 RST:如果正常主机尝试向这条半开连接写入数据,会收到 ICMP 不可达,内核直接发送 RST 复位,断开连接。
  • TCP 内置保活探测:空闲一段时间后,内核会周期性发送探测报文询问对方状态。多次探测无应答,则判定对方不可达,发送 RST 释放连接。
  •       ⚠️ 工程注意:系统 TCP 保活定时器默认检测周期很长(通常 2 小时),实时性很差。生产环境一般不会依赖内核保活,都会在应用层自行实现心跳保活,例如 HTTP 长连接心跳、游戏断线检测等。

    七、TCP / UDP对比

          TCP 和 UDP 是传输层两大核心协议,代表两种截然不同的设计思路。       很多人会误以为可靠传输的 TCP 一定优于 UDP,但实际上二者没有绝对的优劣,也就是说这里谈的 “可靠” 和 “不可靠” 并不是褒义词和贬义词之分,只有是否适配业务场景。       TCP 把复杂的可靠逻辑封装在内核协议栈,向上层提供稳定可靠的传输;UDP 则把控制权交给应用层,以极简的报文结构换取更低延迟与更高灵活性。

    核心特性对比

    • 连接特性 TCP:面向连接,通信前需要通过三次握手建立专属连接,通信结束执行四次挥手释放连接。 UDP:无连接,不需要预先建立连接,发送报文时直接指定目标地址即可发送。
    • 可靠性 TCP:可靠传输,依靠序号、确认应答、超时重传等机制,保证数据不丢失、不重复,并且按序送达。 UDP:不可靠传输,不保证报文一定到达对端,也不保证报文的接收顺序,存在丢包、乱序的可能性。
    • 协议头部开销 TCP:头部最小 20 字节,携带选项时最长可达 60 字节,头部字段多,额外开销更大。 UDP:固定 8 字节头部,结构简单,协议开销很小。
    • 拥塞与流量控制 TCP:内置流量控制(滑动窗口)和拥塞控制(慢启动、拥塞避免等),可以根据接收方能力和网络状况动态调整发送速率。 UDP:没有流量控制、拥塞控制机制,发送速率完全由应用程序决定。
    • 传输模式 TCP:面向字节流,没有应用层报文边界,会产生粘包问题,报文边界需要应用层自行约定。 UDP:面向数据报,每个报文自带长度信息,内核交付时以完整报文为单位,不存在粘包。
    • 双工特性 TCP:全双工,一条连接上双方可以同时收发数据。 UDP:全双工,双方可以独立收发报文。

    适用场景

    • TCP:文件传输、HTTP/HTTPS、邮件、支付业务等场景,业务优先保证数据完整、准确,对延迟容忍度更高。
    • UDP:视频直播、语音通话、在线游戏、DNS 查询、广播等场景,优先追求低延迟,少量丢包可以接受,可在应用层自行实现可靠性。

          总而言之,TCP 和 UDP 只是两种传输工具。       选择的核心依据不是协议本身好坏,而是业务对可靠性、延迟、带宽的需求。对实时性要求极高的场景,甚至可以基于 UDP 在应用层自主实现部分可靠机制,兼顾延迟与数据完整性。

    八、经典面试题:HTTP 获取网页的完整过程

          当用户在浏览器地址栏输入一个 URL 并按下回车时,整个过程并不是简单的 “浏览器向服务器要一下页面”,而是涉及URL 解析、缓存检查、DNS 解析、TCP 连接、TLS 握手、HTTP 请求响应、连接管理和浏览器渲染等多个阶段。下面按实际发生顺序进行完整分析。

      8.1 浏览器处理 URL,先确定 “访问目标”

          用户输入的 URL 可能是这样的:

    https://www.example.com:443/index.html?name=tom#top

    浏览器首先会对 URL 进行解析,拆分出以下关键部分:

    • 协议:https
    • 域名:www.example.com
    • 端口:443
    • 路径与参数:/index.html?name=tom
    • 锚点:#top

    这里有几个关键点:

    • 协议决定默认端口
      • HTTP 默认端口是 80
      • HTTPS 默认端口是 443
    • 锚点 #top 不会发送给服务器
      • 锚点只用于浏览器内部页面定位
      • 服务器收到的请求路径中不包含锚点
    • 浏览器会先做安全与缓存检查
      • 如果域名在 HSTS 列表中,浏览器会强制使用 HTTPS
      • 同时检查本地是否已有可复用的缓存资源

      8.2 先看缓存,能不请求就不请求

          浏览器在真正发起网络请求前,会先检查本地缓存。如果资源已经缓存且仍然有效,就可以直接使用,不必再向服务器请求。缓存主要分为两种:

        8.2.1 强缓存

          浏览器根据响应头中的缓存控制字段,判断资源是否还在有效期内。

    常见字段包括:

    • Cache-Control
    • Expires
    • Last-Modified
    • ETag

    如果资源没有过期,浏览器直接使用本地缓存,状态码通常是:

    200 OK (from disk cache)

    或者:

    200 OK (from memory cache)

        8.2.2 协商缓存

          如果强缓存过期,浏览器会携带缓存校验信息向服务器确认资源是否更新。

    常用的校验方式有:

    • If-Modified-Since:询问资源是否在某个时间之后被修改过
    • If-None-Match:询问资源的 ETag 是否发生变化

          如果服务器判断资源没有变化,会返回:

    304 Not Modified

          浏览器收到 304 后,继续使用本地缓存,并更新缓存有效期。       如果资源已经更新,则返回新的资源内容,状态码通常是:

    200 OK

          所以,浏览器输入 URL 后,并不一定每次都会建立新的 TCP 连接,也不一定每次都会重新获取完整页面。

      8.3 DNS 解析:把域名翻译成 IP 地址

          如果上述的缓存没有命中,浏览器需要先知道服务器的 IP 地址,才能建立网络连接。

    域名解析的过程大致如下:

  • 浏览器缓存查询:浏览器首先检查自己的 DNS 缓存,看是否有该域名对应的 IP 地址
  • 操作系统缓存查询:如果浏览器缓存中没有,检查操作系统的 DNS 缓存
  • 本地 hosts 文件:如果操作系统缓存也没有命中,操作系统会检查本地的hosts文件(Linux:/etc/hosts,Windows:C:\\Windows\\System32\\drivers\\etc\\hosts),查看是否有手动配置的域名映射。
  • 本地 DNS 服务器:如果以上都没有命中,操作系统会向本地 DNS 服务器(通常由 ISP 提供)发送递归查询请求。本地 DNS 服务器会全权负责后续的查询过程,直到获取到 IP 地址并返回给客户端。
  • 根域名服务器:如果本地 DNS 服务器缓存中没有,向根 DNS 服务器发送迭代查询请求
  • 顶级域名服务器:根 DNS 服务器返回顶级域 DNS 服务器(.com 服务器)的地址
  • 权威域名服务器:顶级域 DNS 服务器返回example.com的权威 DNS 服务器地址
  • 获取 IP 地址:权威 DNS 服务器返回www.example.com对应的 IP 地址
  • DNS 查询通常采用:

    • 递归查询:客户端只需要向本地 DNS 服务器发起一次请求,由本地 DNS 服务器负责查到最终结果
    • 迭代查询:本地 DNS 服务器从根域名服务器开始,一级一级向下查询

          DNS 默认使用 UDP 协议,端口号为 53。如果响应报文较大,也可能使用 TCP。

      8.4 建立 TCP 连接,必要时再完成 TLS 握手

          拿到服务器 IP 后,浏览器会根据协议和端口发起连接。

        8.4.1 HTTP 场景:建立 TCP 连接

          HTTP 基于 TCP 传输,浏览器需要先和服务器建立 TCP 连接。       TCP 建立连接需要三次握手:

  • 客户端发送 SYN
  • 服务器回复 SYN + ACK
  • 客户端回复 ACK
  •       连接建立后,双方状态进入 ESTABLISHED。

        8.4.2 HTTPS 场景:TCP 之后还要进行 TLS 握手

          如果是 HTTPS,TCP 连接建立完成后,还需要进行 TLS 握手。       TLS 握手主要完成以下事情:

    • 协商使用的加密算法
    • 校验服务器证书是否可信
    • 生成会话密钥
    • 建立安全加密通道

          TLS 1.2 通常需要约 2 RTT,TLS 1.3 通过优化可以降低到 1 RTT。

          也就是说,HTTPS 并不是直接比 HTTP 慢,而是因为它在 TCP 之外增加了安全层,但通过协议优化,TLS 1.3 已经明显减少了握手开销。

      8.5 浏览器构造并发送 HTTP 请求

          连接建立完成后,浏览器会按照 HTTP 协议格式构造请求报文,并通过 TCP 发送给服务器。       一个 HTTP 请求报文通常包括:

        8.5.1 请求行

    格式为:

    请求方法 路径 HTTP版本

    例如:

    GET /index.html HTTP/1.1

        8.5.2 请求头

    请求头是一组键值对,用于传递额外信息。

    常见请求头包括:

    • Host:指定服务器域名
    • User-Agent:浏览器类型和版本
    • Accept:客户端可接收的内容类型
    • Cookie:携带本地身份凭证
    • Connection:控制连接是否保持
    • Cache-Control:缓存相关控制

        8.5.3 空行

          请求头结束后,有一个空行,用于分隔请求头和请求体。

        8.5.4 请求体

    请求体不是所有请求都有。

    • GET 请求通常没有请求体
    • POST、PUT 等请求可以携带请求体

          发送时,HTTP 请求会交给 TCP 协议栈处理。TCP 会根据 MSS、窗口大小、网络状况等因素决定如何分片传输,并通过序号、确认应答、超时重传、滑动窗口等机制保证数据可靠到达。

      8.6 服务器处理请求并返回 HTTP 响应

          服务器收到 TCP 报文后,内核会先重组出完整的 HTTP 请求,再交给 Web 服务器程序处理。       服务器的处理过程大致包括:

  • 解析请求行、请求头、请求体
  • 根据域名、端口、路径进行路由匹配
  • 调用对应的应用程序或业务逻辑
  • 读取文件、查询数据库或完成其他计算
  • 构造 HTTP 响应报文
  • HTTP 响应报文通常包括:

        8.6.1 状态行

    格式为:

    HTTP版本 状态码 状态描述

    例如:

    HTTP/1.1 200 OK

    常见状态码分类:

    • 2xx:成功
    • 3xx:重定向或缓存相关
    • 4xx:客户端错误
    • 5xx:服务器错误

        8.6.2 响应头

          响应头用于向客户端传递额外信息。       常见响应头包括:

    • Content-Type:响应内容类型
    • Content-Length:响应体长度
    • Set-Cookie:设置 Cookie
    • Cache-Control:缓存控制
    • ETag:资源标识
    • Last-Modified:资源最后修改时间
    • Location:重定向地址

        8.6.3 空行

          响应头结束后,同样有一个空行。

        8.6.4 响应体

          响应体是服务器返回给浏览器的正文内容,可能是:

    • HTML 文本
    • JSON 数据
    • 图片二进制
    • 文件内容

          浏览器访问网页时,响应体通常就是 HTML 页面内容。

      8.7 浏览器接收响应,并根据状态码做不同处理

          浏览器收到服务器返回的 HTTP 响应后,会先解析状态行、响应头和响应体。

    1. 正常响应

          如果状态码是 200 OK,说明请求成功,浏览器会开始处理响应体中的 HTML 内容。

    2. 重定向响应

          如果状态码是 3xx,浏览器会根据 Location 字段进行跳转。

    例如:

    • 301 Moved Permanently:永久重定向
    • 302 Found:临时重定向
    • 304 Not Modified:资源未修改,使用缓存

    3. 客户端错误

          如果状态码是 4xx,说明请求存在问题。

    例如:

    • 400 Bad Request:请求格式错误
    • 401 Unauthorized:未登录
    • 403 Forbidden:拒绝访问
    • 404 Not Found:资源不存在

    4. 服务器错误

          如果状态码是 5xx,说明服务器处理过程中出现异常。

    例如:

    • 500 Internal Server Error:服务器内部错误
    • 502 Bad Gateway:网关错误
    • 503 Service Unavailable:服务暂时不可用

      8.8 TCP 连接如何关闭:短连接与长连接

    HTTP 并不总是在一次请求结束后就立即关闭 TCP 连接。

        8.8.1 短连接

    在早期 HTTP 中,一次请求响应结束后,TCP 连接就会关闭。       如果是短连接,服务器返回响应后,双方会通过四次挥手释放连接:

  • 主动关闭方发送 FIN:表示自己没有数据要发送了,请求关闭连接。此时主动关闭方处于FIN_WAIT_1状态。
  • 对方回复 ACK:表示确认收到 FIN 报文。此时被动关闭方处于CLOSE_WAIT状态,主动关闭方处于FIN_WAIT_2状态。
  • 对方发送 FIN:被动关闭方可能不会立刻发送 FIN 报文,可能需要处理完所有剩余数据后,再向对方发送一个 FIN 报文,表示自己也没有数据要发送了,请求关闭连接。此时被动关闭方处于LAST_ACK状态。
  • 主动关闭方回复 ACK:表示确认收到 FIN 报文。此时主动关闭方处于TIME_WAIT状态,等待 2MSL(最长报文寿命,通常为 2 分钟)后,进入CLOSED状态。被动关闭方收到 ACK 报文后,立即进入CLOSED状态。
  •       连接关闭后,如果后续还有资源请求,需要重新建立 TCP 连接。

        8.8.2 长连接

    HTTP/1.1 默认支持 Keep-Alive,也就是长连接。       长连接的特点是:

    • 一次 TCP 连接可以多次复用
    • 请求响应结束后,连接不会立即关闭
    • 后续请求可以继续使用这条连接
    • 减少重复建立 TCP 连接的开销

          长连接适合加载多个资源的网页,因为一个页面通常不止一个 HTTP 请求,还可能需要加载 CSS、JS、图片等资源。

      8.9 浏览器解析 HTML,并开始渲染页面

          浏览器拿到 HTML 后,并不是直接显示成页面,而是要经过一系列渲染步骤。

    1. 解析 HTML,生成 DOM 树:HTML 是结构化文本,浏览器会逐行解析标签、属性和文本内容,最终生成一棵文档对象模型树。

    2. 解析 CSS,生成 CSSOM 树:CSS 决定页面的样式。浏览器解析 CSS 后,会生成 CSS 对象模型树。

    3. 合并 DOM 和 CSSOM,生成渲染树:渲染树只包含需要显示的节点和样式信息。

    4. 布局计算:浏览器会计算每个节点的位置、大小、盒模型、排版关系等,确定页面布局。

    5. 绘制页面:布局完成后,浏览器会把页面元素绘制到屏幕上。

    6. 执行 JavaScript:JavaScript 可以修改 DOM 结构和 CSS 样式。

    如果 JS 修改了 DOM,可能触发:

    • 重排:布局发生变化
    • 重绘:外观发生变化,但布局不一定变化

          页面中的图片、CSS、JS 等资源,通常还会触发新的 HTTP 请求,整个网络过程会重复执行。

      8.10 完整流程总结

    可以把 HTTP 获取网页的完整过程总结为:

    用户输入 URL
    → URL 解析
    → 缓存检查
    → DNS 解析
    → 建立 TCP 连接
    → HTTPS 完成 TLS 握手
    → 浏览器发送 HTTP 请求
    → 服务器处理请求
    → 服务器返回 HTTP 响应
    → 浏览器解析响应
    → 浏览器渲染页面
    → 连接关闭或保持复用

    面试中重点答什么?

    如果这个问题在面试中出现,不需要把上面所有细节都背出来,但最好覆盖以下关键点:

  • 浏览器会先解析 URL,并检查本地缓存
  • 缓存命中时直接使用缓存,未命中时继续请求
  • DNS 解析域名对应的 IP 地址
  • HTTP 需要先建立 TCP 连接,HTTPS 还需要 TLS 握手
  • 浏览器构造 HTTP 请求,服务器处理后返回 HTTP 响应
  • 浏览器根据状态码决定正常渲染、跳转还是显示错误
  • HTTP/1.1 支持长连接,减少重复握手开销
  • 最终浏览器会解析 HTML、CSS,并完成页面渲染
  • 结束语

          到这里,本篇关于 TCP 拥塞控制、延迟应答、捎带应答、粘包问题与异常场景的内容就全部讲解完毕。我们区分了拥塞控制与流量控制,理清了慢启动、拥塞避免、拥塞发生后的整套处理流程;同时理解了延迟应答与捎带应答如何减少报文开销,也从 TCP 面向字节流的本质出发,弄懂了粘包问题成因与对应的解决思路,并分析了进程终止、断网、断电等各类异常场景下 TCP 的行为。

          文章最后的 HTTP 网页访问流程,串联了 TCP 连接建立、数据传输、连接释放等全部知识,把 TCP 理论落地到真实的网页访问场景。至此 TCP 核心机制的主体内容已经完成,后续我们可以继续拓展更多网络相关知识。希望本篇内容可以帮大家把 TCP 的各个知识点串联起来,面试或者底层开发时都能形成完整的思路。

    赞(0)
    未经允许不得转载:171主机测评 » 《Linux 网络编程》深入理解 TCP 协议(六):TCP 拥塞控制、应答机制、异常处理流程全解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址