

请求视角 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 65535bashWireshark 分析技巧:
# 过滤TCP握手包
tcp.flags.syn==1
# 查看特定连接的完整流程
tcp.stream eq 0plaintext为什么关闭连接往往比建立连接更容易答乱#
因为建立连接是三次,关闭连接常常会说到四次挥手、半关闭、TIME_WAIT。
这里最重要的是明白:
- TCP 是全双工;
- 一端不再发送,不代表另一端也立刻没数据可发;
- 所以关闭两端发送方向,常常需要分开确认。
这也是为什么“四次挥手”通常比“三次握手”更容易被追问细节。
TIME_WAIT 到底在干嘛
它不是单纯“浪费端口”。保留一段等待时间,主要是为了让可能迟到的旧报文彻底消散,并确保最后的确认报文如果丢了,还有机会重发。它的存在,本质上是在为连接生命周期收尾。
TCP 真正厉害的地方,在于它把“可靠”做成了一整套机制#
“TCP 可靠,UDP 不可靠”是最常见的总结,但这句话太容易把细节全抹平。
TCP 的可靠,是靠几组机制一起堆出来的:
- 序列号;
- 确认应答;
- 超时重传;
- 滑动窗口;
- 流量控制;
- 拥塞控制;
- 乱序重组。
流量控制和拥塞控制不是一个东西#
这也是面试里特别爱混的两个点。
- 流量控制 更像接收端在说:你别发太快,我这边来不及收;
- 拥塞控制 更像网络在说:别把中间链路打爆了。
一个关注接收方处理能力,一个关注整个网络承压情况。
TCP 流量控制详解#
滑动窗口机制:
发送方窗口:
[已发送已确认][已发送未确认][可发送][不可发送]
↑ ↑
发送窗口左边界 发送窗口右边界
接收方窗口:
[已接收已确认][可接收][不可接收]
↑ ↑
接收窗口左边界 接收窗口右边界plaintext窗口大小动态调整:
- 接收方通过TCP头部的Window字段告知发送方自己的接收窗口大小
- 发送方根据接收窗口调整发送速率
- 当接收窗口为0时,发送方停止发送(除了窗口探测包)
零窗口问题:
接收方: Window=0 (缓冲区满)
↓
发送方: 停止发送,启动持续计时器
↓
发送方: 定期发送窗口探测包(1字节)
↓
接收方: Window=1024 (缓冲区有空间)
↓
发送方: 恢复发送plaintextTCP 拥塞控制详解#
TCP使用四种算法控制拥塞:
1. 慢启动(Slow Start):
初始: cwnd = 1 MSS
每收到一个ACK: cwnd = cwnd * 2 (指数增长)
cwnd变化: 1 → 2 → 4 → 8 → 16 → ...
直到达到慢启动阈值(ssthresh)plaintext2. 拥塞避免(Congestion Avoidance):
当 cwnd >= ssthresh 时:
每收到一个ACK: cwnd = cwnd + 1/cwnd (线性增长)
cwnd变化: 16 → 17 → 18 → 19 → ...plaintext3. 快速重传(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)
↓
立即重传数据包3plaintext4. 快速恢复(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=bbrbash为什么 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.ciphersuitebash使用 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.180sbash性能优化建议:
- 使用TLS 1.3(减少握手往返)
- 启用TLS会话复用
- 使用OCSP Stapling减少证书验证开销
- 考虑使用HTTP/2或HTTP/3
HTTPS 主要解决哪几类问题#
- 防窃听:别人看到报文也难直接读懂内容;
- 防篡改:中途改包更容易被发现;
- 防冒充:通过证书体系确认你连的是谁。
它把通信的身份和完整性也一起管了。
HTTP 层负责的是语义,不是底层可靠性#
这个边界一定要分清。
HTTP 不负责重传、丢包恢复、流量控制,这些是 TCP 或 QUIC 那一层在处理。
HTTP 负责的,是请求与响应的表达:
- 方法;
- 状态码;
- 头部;
- 资源语义;
- 缓存协商;
- 内容协商;
- 连接复用方式。
真正高频的 HTTP 面试点,其实就这几组#
GET和POST的语义差异;- 常见状态码;
- 长连接与短连接;
- 缓存控制;
- Cookie、Session、Token;
- 各版本之间的演进。
HTTP 1.1、2、3 应该怎么对比#
- 长连接普及后,请求不必每次都重建 TCP。
- 但同一连接上的并发能力有限,队头阻塞问题明显。
- 时代很长,生态成熟。
队头阻塞问题:
请求1 ━━━━━━━━━━━━━━━━━━━━ (慢)
请求2 等待请求1完成...
请求3 等待请求1完成...plaintext解决方案:
- 域名分片(多个域名并行请求)
- 资源合并(CSS Sprites, JS打包)
- 内联资源(Base64图片)
- 重点是二进制分帧、多路复用、头部压缩。
- 一个连接里可并发多路请求,减少连接数量。
- 但底层如果还是 TCP,传输层队头阻塞并没有完全消失。
多路复用:
单个TCP连接:
Stream 1: ━━ ━━ ━━ ━━
Stream 2: ━━ ━━ ━━
Stream 3: ━━ ━━ ━━
(帧交错发送,无需等待)plaintext头部压缩(HPACK):
请求1:
:method: GET
:path: /index.html
user-agent: Mozilla/5.0...
accept: text/html...
请求2: (只发送差异)
:path: /style.css
(其他头部引用索引表)plaintext服务器推送:
客户端请求: GET /index.html
服务器响应:
- 返回 index.html
- 主动推送 style.css
- 主动推送 script.js
(减少往返次数)plaintext实战配置(Nginx):
server {
listen 443 ssl http2;
# 启用HTTP/2服务器推送
location = /index.html {
http2_push /style.css;
http2_push /script.js;
}
}nginx- 跑在 QUIC 之上,不再直接依赖 TCP。
- 试图把传输层阻塞问题进一步缓解。
- 更适合高丢包、高延迟、移动网络等更复杂场景。
QUIC 的优势:
TCP + TLS 1.2:
TCP握手(1 RTT) + TLS握手(2 RTT) = 3 RTT
QUIC + TLS 1.3:
合并握手(1 RTT) 或 0-RTT(恢复连接)plaintext连接迁移:
TCP: 四元组(源IP,源端口,目标IP,目标端口)
切换网络(WiFi→4G)需要重新建连
QUIC: 使用Connection ID
切换网络后连接保持,无需重连plaintext流级别的拥塞控制:
TCP: 一个包丢失,整个连接阻塞
QUIC: 一个流丢包,只阻塞该流,其他流继续plaintextHTTP 性能优化实战#
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...” />plaintext2. 启用压缩:
# Nginx配置
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
gzip_comp_level 6;nginx3. 使用缓存:
# 强缓存
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 (使用缓存)plaintext4. 使用CDN:
原始: 用户 → 源服务器(延迟200ms)
CDN: 用户 → 边缘节点(延迟20ms) → 源服务器plaintext5. HTTP/2优化:
# 不再需要域名分片
# 不再需要资源合并
# 利用服务器推送
# 但仍需注意:
# - 关键资源优先级
# - 避免推送不需要的资源plaintext6. 预连接优化:
<!-- 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 到页面显示”#
这题基本算计网总复习题。
- 先说解析 URL,确认协议、域名、端口和资源路径。
- 再说 DNS,把域名翻译成 IP。
- 接着说 TCP 建连,如果是 HTTPS 再补 TLS 握手。
- 然后说 HTTP 请求与响应语义,以及各版本差异。
- 最后补传输层的可靠性、拥塞控制,以及浏览器继续拉静态资源和渲染页面。
这套答法的好处是:
- 能把 DNS、TCP、TLS、HTTP 放在一条线上;
- 既能说协议职责,也能说性能影响;
- 对方怎么追问,你都能顺着往下一层展开。
总结#
它更像一条分层协作的请求旅程:
- DNS 先告诉你人在哪;
- TCP 或 QUIC 负责把通道搭起来;
- TLS 负责让通道可信且保密;
- HTTP 负责表达请求与响应的语义;
- 流量控制、拥塞控制、重传等机制负责让这条旅程别轻易跑散。
所以真正学会计网之后,你再看一个请求,就不会只看到“浏览器发了个 HTTP”。
你看到的会是一整套协议,在为同一件事接力:把一次通信尽量正确、尽量高效、尽量安全地送到对面去。