FXJ Wiki

Back

从浏览器到服务端:一条请求经过了哪些网络层次Blur image

请求视角 TCP / HTTP HTTPS

计算机网络最容易学成“协议名仓库”。

HTTP、HTTPS、TCP、UDP、DNS、IP、MAC、ARP、拥塞控制、流量控制,听起来都重要,做题时也确实都能单独问。可一旦把它们拆开太久,人脑就会开始遗忘一个最关键的事实:这些东西本来就是为了让一次真实通信能成功发生

所以计网这章我越来越喜欢从一条请求开始学。

先问:当我在浏览器里输入一个 URL,到底有哪些层在接力

先把一条请求走完整#

  • 解析 URL

    浏览器先拆出协议、域名、端口、路径,准备好要访问的对象。

  • DNS 查询

    把域名换成 IP,缓存、递归解析器、权威 DNS 依次接力。

  • 建立连接

    如果走 TCP,就要先握手;如果是 HTTPS,还要在上面完成 TLS 协商。

  • 发送 HTTP 请求

    请求方法、头部、路径、状态语义开始发挥作用。

  • 传输与控制

    分段、确认、重传、流量控制、拥塞控制一起保证数据尽量稳定送达。

  • 返回与渲染

    浏览器接收响应,再继续拉取 CSS、JS、图片,页面才真正显示完整。

只要把这条时间线放在脑子里,后面每一层的职责就好理解很多。

第一步先找到对方#

很多人说“浏览器发送请求”,听起来像第一步就发 HTTP。

其实不是。

在大多数场景里,浏览器首先得知道:这个域名到底对应哪台机器。

DNS 做的,是把名字换成地址#

域名好记,但机器真正转发靠的是 IP。

所以 DNS 这层最核心的职责,就是把:

  • www.example.com

翻译成:

  • 某个可以路由到的 IP 地址。

这一步常见会经过这些缓存或节点:

  • 浏览器缓存;
  • 操作系统缓存;
  • 本地递归解析器;
  • 根、顶级域、权威 DNS 服务器。

你不一定要每次都把整套递归过程背全,但最好知道:DNS 不是一张单点电话簿,它本身就是一套分层协作系统

为什么 DNS 会直接影响访问体验#

因为它是请求真正开始前的必要环节。

  • 解析慢,整体首包就慢;
  • 解析策略不同,用户可能被导到不同机房;
  • CDN 调度很多时候首先依赖的就是域名解析结果。

所以当你说“网站慢”,问题可能都还没到业务服务那里,先慢在了解析路径上。

DNS 把“人类友好的名字”和“机器可路由的地址”解耦了,同时还能借此做缓存和流量调度。这就比背书强很多。

第二步:连接不是一句“连上了”就完事#

拿到 IP 之后,请求也还没真正开始。

如果走的是基于 TCP 的通信,客户端和服务端先得把这条通道建立起来。

三次握手到底在确认什么#

很多人会说”三次握手是为了确认双方收发正常”,这没错,但略显抽象。

更完整一点可以这样理解:

  • 客户端先发起连接意愿;
  • 服务端确认自己能收、也愿意发;
  • 客户端再确认自己收到了对方回应;
  • 双方顺便完成初始序列号同步,为后面的可靠传输打地基。

所以握手是给后续可靠字节流建立共同上下文

TCP 三次握手完整流程#

第一次握手:

  • 客户端发送: SYN=1, seq=x
  • 状态转换: CLOSED → SYN_SENT
  • 含义: 客户端请求建立连接,初始序列号为x

第二次握手:

  • 服务端发送: SYN=1, ACK=1, seq=y, ack=x+1
  • 状态转换: LISTEN → SYN_RCVD
  • 含义: 服务端确认收到请求,同时发送自己的初始序列号y

第三次握手:

  • 客户端发送: ACK=1, seq=x+1, ack=y+1
  • 状态转换: SYN_SENT → ESTABLISHED (客户端), SYN_RCVD → ESTABLISHED (服务端)
  • 含义: 客户端确认收到服务端响应,连接建立完成

为什么需要三次握手:

  • 两次不够: 服务端无法确认客户端能收到自己的响应
  • 四次太多: 第二、三次可以合并(服务端的ACK和SYN可以一起发)
  • 防止旧连接: 如果客户端的旧SYN包延迟到达,三次握手可以识别并拒绝

使用 tcpdump 观察三次握手#

# 抓取本机80端口的TCP包
sudo tcpdump -i any port 80 -nn -S

# 输出示例:
# 第一次握手
# IP 192.168.1.100.54321 > 192.168.1.200.80: Flags [S], seq 1000, win 65535
# 第二次握手
# IP 192.168.1.200.80 > 192.168.1.100.54321: Flags [S.], seq 2000, ack 1001, win 65535
# 第三次握手
# IP 192.168.1.100.54321 > 192.168.1.200.80: Flags [.], ack 2001, win 65535
bash

Wireshark 分析技巧:

# 过滤TCP握手包
tcp.flags.syn==1

# 查看特定连接的完整流程
tcp.stream eq 0
plaintext

为什么关闭连接往往比建立连接更容易答乱#

因为建立连接是三次,关闭连接常常会说到四次挥手、半关闭、TIME_WAIT

这里最重要的是明白:

  • TCP 是全双工;
  • 一端不再发送,不代表另一端也立刻没数据可发;
  • 所以关闭两端发送方向,常常需要分开确认。

这也是为什么“四次挥手”通常比“三次握手”更容易被追问细节。

TIME_WAIT 到底在干嘛

它不是单纯“浪费端口”。保留一段等待时间,主要是为了让可能迟到的旧报文彻底消散,并确保最后的确认报文如果丢了,还有机会重发。它的存在,本质上是在为连接生命周期收尾。

TCP 真正厉害的地方,在于它把“可靠”做成了一整套机制#

“TCP 可靠,UDP 不可靠”是最常见的总结,但这句话太容易把细节全抹平。

TCP 的可靠,是靠几组机制一起堆出来的:

  • 序列号;
  • 确认应答;
  • 超时重传;
  • 滑动窗口;
  • 流量控制;
  • 拥塞控制;
  • 乱序重组。

流量控制和拥塞控制不是一个东西#

这也是面试里特别爱混的两个点。

  • 流量控制 更像接收端在说:你别发太快,我这边来不及收;
  • 拥塞控制 更像网络在说:别把中间链路打爆了。

一个关注接收方处理能力,一个关注整个网络承压情况。

TCP 流量控制详解#

滑动窗口机制:

发送方窗口:
[已发送已确认][已发送未确认][可发送][不可发送]
              ↑           ↑
              发送窗口左边界  发送窗口右边界

接收方窗口:
[已接收已确认][可接收][不可接收]
              ↑     ↑
              接收窗口左边界  接收窗口右边界
plaintext

窗口大小动态调整:

  • 接收方通过TCP头部的Window字段告知发送方自己的接收窗口大小
  • 发送方根据接收窗口调整发送速率
  • 当接收窗口为0时,发送方停止发送(除了窗口探测包)

零窗口问题:

接收方: Window=0 (缓冲区满)

发送方: 停止发送,启动持续计时器

发送方: 定期发送窗口探测包(1字节)

接收方: Window=1024 (缓冲区有空间)

发送方: 恢复发送
plaintext

TCP 拥塞控制详解#

TCP使用四种算法控制拥塞:

1. 慢启动(Slow Start):

初始: cwnd = 1 MSS
每收到一个ACK: cwnd = cwnd * 2 (指数增长)

cwnd变化: 1 → 2 → 4 → 8 → 16 → ...
直到达到慢启动阈值(ssthresh)
plaintext

2. 拥塞避免(Congestion Avoidance):

当 cwnd >= ssthresh 时:
每收到一个ACK: cwnd = cwnd + 1/cwnd (线性增长)

cwnd变化: 16 → 17 → 18 → 19 → ...
plaintext

3. 快速重传(Fast Retransmit):

发送方收到3个重复ACK:
立即重传丢失的数据包,不等超时

正常: 发送1,2,3,4,5 → 收到ACK 1,2,3,4,5
丢包: 发送1,2,3,4,5 → 收到ACK 1,2,2,2,2 (3个重复ACK)

      立即重传数据包3
plaintext

4. 快速恢复(Fast Recovery):

快速重传后:
ssthresh = cwnd / 2
cwnd = ssthresh + 3
进入拥塞避免阶段
plaintext

拥塞控制状态转换:

                慢启动

            cwnd >= ssthresh

               拥塞避免

            检测到丢包(3个重复ACK)

               快速重传

               快速恢复

            回到拥塞避免
plaintext

超时重传的处理:

超时发生:
ssthresh = cwnd / 2
cwnd = 1
重新进入慢启动
plaintext

实战: 使用 ss 命令查看TCP状态#

# 查看TCP连接的拥塞窗口信息
ss -tin

# 输出示例:
State    Recv-Q Send-Q Local Address:Port  Peer Address:Port
ESTAB    0      0      192.168.1.100:22    192.168.1.200:54321
         cubic wscale:7,7 rto:204 rtt:3.5/1.5 ato:40 mss:1448
         pmtu:1500 rcvmss:1448 advmss:1448
         cwnd:10 ssthresh:7 bytes_acked:12345 bytes_received:67890

         拥塞窗口  慢启动阈值

# 参数说明:
# cubic: 拥塞控制算法
# rto: 重传超时时间(ms)
# rtt: 往返时间(ms)
# cwnd: 拥塞窗口大小
# ssthresh: 慢启动阈值
bash

不同拥塞控制算法对比#

Linux支持多种拥塞控制算法:

Reno(经典算法):

  • 使用快速重传和快速恢复
  • 丢包时cwnd减半
  • 适合低延迟网络

Cubic(Linux默认):

  • 窗口增长函数为三次函数
  • 更激进的窗口增长
  • 适合高带宽长延迟网络

BBR(Google开发):

  • 基于带宽和RTT建模
  • 不依赖丢包作为拥塞信号
  • 适合高丢包率网络
# 查看当前使用的拥塞控制算法
sysctl net.ipv4.tcp_congestion_control

# 查看可用的算法
sysctl net.ipv4.tcp_available_congestion_control

# 修改拥塞控制算法
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
bash

为什么 TCP 适合通用应用层协议#

因为上层应用通常不想自己重新发明一遍这些机制。

HTTP、MySQL 协议、绝大多数面向正确性交互的后端服务,都更愿意直接站在可靠字节流上说话。

这也是 TCP 在传统互联网应用里长期稳坐主力的原因。

HTTPS: HTTP 叠了一层 TLS#

这句话很简单,但特别值得记住。

因为它一下就把职责拆清楚了:

  • HTTP 负责表达“我要什么、你给我什么”;
  • TLS 负责保密性、完整性和身份认证;
  • TCP 负责把下层可靠传输通道准备好。

TLS 握手到底在干嘛#

如果只用一句人话来概括,那就是:

  • 协商算法;
  • 验证服务端身份;
  • 建立这次会话要用的密钥材料。

所以 HTTPS 慢一点,常见不只是”加密更复杂”,而是:

  • 前面先有 TCP 握手;
  • 上面还要走 TLS 协商;
  • 证书校验、密钥交换、握手往返都会带来额外成本。

但换来的,是明文 HTTP 根本不具备的安全边界。

TLS 1.2 握手完整流程#

1. ClientHello:

  • 客户端发送:
    • 支持的TLS版本
    • 支持的加密套件列表
    • 客户端随机数(Client Random)
    • 支持的压缩方法

2. ServerHello:

  • 服务端发送:
    • 选定的TLS版本
    • 选定的加密套件
    • 服务端随机数(Server Random)
    • 服务端证书(Certificate)
    • ServerHelloDone

3. 客户端验证证书:

  • 检查证书是否由可信CA签发
  • 验证证书链的完整性
  • 检查证书是否过期
  • 验证域名是否匹配

4. 密钥交换(以ECDHE为例):

  • 客户端生成预主密钥(Pre-Master Secret)
  • 使用服务端公钥加密
  • 发送ClientKeyExchange消息

5. 生成会话密钥:

  • 双方使用: Client Random + Server Random + Pre-Master Secret
  • 通过PRF(伪随机函数)生成会话密钥
  • 用于后续对称加密通信

6. 完成握手:

  • 客户端发送: ChangeCipherSpec + Finished
  • 服务端发送: ChangeCipherSpec + Finished
  • 握手完成,开始加密通信

使用 Wireshark 分析 TLS 握手#

# 抓取HTTPS流量
sudo tcpdump -i any port 443 -w tls.pcap

# 在Wireshark中查看:
# 1. 过滤TLS握手包
ssl.handshake

# 2. 查看证书信息
ssl.handshake.certificate

# 3. 查看加密套件协商
ssl.handshake.ciphersuite
bash

使用 curl 分析 HTTPS 请求耗时#

# 详细显示各阶段耗时
curl -w\n\
DNS解析: %{time_namelookup}s\n\
TCP连接: %{time_connect}s\n\
TLS握手: %{time_appconnect}s\n\
开始传输: %{time_starttransfer}s\n\
总耗时: %{time_total}s\n \
-o /dev/null -s https://example.com

# 输出示例:
# DNS解析: 0.015s
# TCP连接: 0.045s
# TLS握手: 0.120s  ← TLS握手额外耗时
# 开始传输: 0.150s
# 总耗时: 0.180s
bash

性能优化建议:

  • 使用TLS 1.3(减少握手往返)
  • 启用TLS会话复用
  • 使用OCSP Stapling减少证书验证开销
  • 考虑使用HTTP/2或HTTP/3

HTTPS 主要解决哪几类问题#

  • 防窃听:别人看到报文也难直接读懂内容;
  • 防篡改:中途改包更容易被发现;
  • 防冒充:通过证书体系确认你连的是谁。

它把通信的身份和完整性也一起管了。

HTTP 层负责的是语义,不是底层可靠性#

这个边界一定要分清。

HTTP 不负责重传、丢包恢复、流量控制,这些是 TCP 或 QUIC 那一层在处理。

HTTP 负责的,是请求与响应的表达:

  • 方法;
  • 状态码;
  • 头部;
  • 资源语义;
  • 缓存协商;
  • 内容协商;
  • 连接复用方式。

真正高频的 HTTP 面试点,其实就这几组#

  • GETPOST 的语义差异;
  • 常见状态码;
  • 长连接与短连接;
  • 缓存控制;
  • Cookie、Session、Token;
  • 各版本之间的演进。

HTTP 1.1、2、3 应该怎么对比#

  • 长连接普及后,请求不必每次都重建 TCP。
  • 但同一连接上的并发能力有限,队头阻塞问题明显。
  • 时代很长,生态成熟。

队头阻塞问题:

请求1 ━━━━━━━━━━━━━━━━━━━━ (慢)
请求2 等待请求1完成...
请求3 等待请求1完成...
plaintext

解决方案:

  • 域名分片(多个域名并行请求)
  • 资源合并(CSS Sprites, JS打包)
  • 内联资源(Base64图片)

HTTP 性能优化实战#

1. 减少请求数:

# 资源合并
cat file1.css file2.css > bundle.css

# 使用CSS Sprites
.icon1 { background-position: 0 0; }
.icon2 { background-position: -20px 0; }

# 内联小资源
<img src=”data:image/png;base64,iVBORw0KG...” />
plaintext

2. 启用压缩:

# Nginx配置
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
gzip_comp_level 6;
nginx

3. 使用缓存:

# 强缓存
Cache-Control: max-age=31536000, immutable

# 协商缓存
ETag: “33a64df551425fcc55e4d42a148795d9f25f89d4”
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT

# 客户端再次请求
If-None-Match: “33a64df551425fcc55e4d42a148795d9f25f89d4”
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

# 服务器响应
304 Not Modified (使用缓存)
plaintext

4. 使用CDN:

原始: 用户 → 源服务器(延迟200ms)
CDN:  用户 → 边缘节点(延迟20ms) → 源服务器
plaintext

5. HTTP/2优化:

# 不再需要域名分片
# 不再需要资源合并
# 利用服务器推送

# 但仍需注意:
# - 关键资源优先级
# - 避免推送不需要的资源
plaintext

6. 预连接优化:

<!-- DNS预解析 -->
<link rel=”dns-prefetch” href=”//example.com”>

<!-- 预连接(DNS+TCP+TLS) -->
<link rel=”preconnect” href=”https://example.com”>

<!-- 预加载关键资源 -->
<link rel=”preload” href=”style.css” as=”style”>

<!-- 预取下一页资源 -->
<link rel=”prefetch” href=”next-page.html”>
html

这里最好别只答”版本越高越快”。

更稳的说法是:每一代都在努力减少额外开销、提高并发传输效率、降低阻塞带来的连锁影响。

一次请求真的慢,问题可能在很多地方#

这也是计网学习最有价值的地方:它会逼你从分层角度看性能。

一个网页打开慢,可能是:

  • DNS 解析慢;
  • TCP 握手慢;
  • TLS 协商慢;
  • 应用层缓存没命中;
  • 服务端处理慢;
  • 网络丢包导致重传;
  • 拥塞控制把发送速率压下来了;
  • 浏览器还在继续拉 CSS、JS、图片。

所以“网络慢”从来不是一个单点答案。

UDP 为什么依然重要#

讲计网不能只讲 TCP,不然会误以为“可靠 = 万能”。

UDP 的特点很鲜明:

  • 头部小;
  • 连接负担轻;
  • 实时性更友好;
  • 上层自己决定要不要补可靠语义。

所以像音视频、实时交互、某些游戏场景,会更愿意站在 UDP 或基于 UDP 的协议之上重新设计传输策略。

这也是后来看 QUIC 会很有意思的原因:它把很多传统上依赖 TCP 的传输能力,搬到了用户态协议层重新组织。

TCP 和 UDP 这题怎么答才不空

别只说“一个可靠一个不可靠”。更好的答法是:TCP 把顺序、确认、重传、窗口、拥塞控制这套机制内建了,适合通用可靠传输;UDP 保持轻量,把更多选择权留给应用层,更适合实时性优先或自定义传输语义的场景。

如果面试官让你“讲一下从输入 URL 到页面显示”#

这题基本算计网总复习题。

  1. 先说解析 URL,确认协议、域名、端口和资源路径。
  2. 再说 DNS,把域名翻译成 IP。
  3. 接着说 TCP 建连,如果是 HTTPS 再补 TLS 握手。
  4. 然后说 HTTP 请求与响应语义,以及各版本差异。
  5. 最后补传输层的可靠性、拥塞控制,以及浏览器继续拉静态资源和渲染页面。

这套答法的好处是:

  • 能把 DNS、TCP、TLS、HTTP 放在一条线上;
  • 既能说协议职责,也能说性能影响;
  • 对方怎么追问,你都能顺着往下一层展开。

总结#

它更像一条分层协作的请求旅程:

  • DNS 先告诉你人在哪;
  • TCP 或 QUIC 负责把通道搭起来;
  • TLS 负责让通道可信且保密;
  • HTTP 负责表达请求与响应的语义;
  • 流量控制、拥塞控制、重传等机制负责让这条旅程别轻易跑散。

所以真正学会计网之后,你再看一个请求,就不会只看到“浏览器发了个 HTTP”。

你看到的会是一整套协议,在为同一件事接力:把一次通信尽量正确、尽量高效、尽量安全地送到对面去。

从浏览器到服务端:一条请求经过了哪些网络层次
https://fxj.wiki/blog/interview-computer-network
Author 玛卡巴卡
Published at 2025年3月21日
Comment seems to stuck. Try to refresh?✨