📖 Day 8 / 15 · 《计算机网络:自顶向下方法》阅读笔记
每天 30 分钟,15 天读懂计算机网络。今天我们来深入互联网最重要的传输层协议——TCP。三次握手、四次挥手、可靠传输、流量控制、拥塞控制,一次讲透。
一、TCP 是什么?为什么这么重要?#
1.1 从 UDP 到 TCP#
前两天我们聊传输层的时候提过 UDP——那个「我发了,收到没?不关我事」的无忧无虑少年。UDP 很简单,但也太不靠谱了。
现实世界中,很多时候我们需要可靠的数据传输。你用 SSH 连你的 Mac mini,敲一个 ls,不可能出现「这个 ls 包丢了,你再来一次」的情况。你希望的是:每一条命令都准确无误地到达,而且按顺序到达。
这就是 TCP(Transmission Control Protocol,传输控制协议)存在的意义。
🎯 一句话理解 TCP
TCP 是一个面向连接的、可靠的、基于字节流的传输层协议。它在不可靠的网络之上,为你搭建了一条看起来靠谱的「逻辑通道」。
1.2 TCP 的三大核心特征#
| 特征 | 含义 | 生活类比 |
|---|---|---|
| 面向连接 | 通信前先「建立连接」(三次握手),通信后「断开连接」(四次挥手) | 打电话先拨号,对方接听后才开始聊 |
| 可靠传输 | 保证数据不丢、不重复、不乱序 | 快递员不仅要送到,还要按顺序送到,丢了就重新派件 |
| 字节流 | 数据被视为连续的字节流,没有消息边界 | 水龙头里流出来的水,不分「第几杯」,就是源源不断的流 |
1.3 TCP 报文段结构#
先来看看 TCP 数据包长什么样:
1 | 0 1 2 3 |
几个关键字段速览:
- 端口号(Port):源端口 + 目的端口,标识「这是谁发给谁的应用」。比如 SSH 是 22,HTTP 是 80
- 序号(Sequence Number):这是 TCP 可靠传输的灵魂——每个字节都有编号,接收方靠它来排序和去重
- 确认号(Acknowledgment Number):「我收到了你序号 N 之前的所有数据,下一个我等 N+1」
- 标志位(Flags):ACK、SYN、FIN、RST 等,控制 TCP 状态机的运转
- 窗口大小(Window):流量控制的核心,告诉对方「我还能接收多少数据」
💡 实战观察
你现在用 SSH 连着 Mac mini?那就有至少一条 TCP 连接活着。我们可以用
ss命令看到它:
1
2 # 查看 established 的 TCP 连接
ss -tn state established输出里你会看到源端口(你的终端,通常是随机的高位端口)和目的端口(22,SSH 默认端口),以及连接状态。
二、三次握手:TCP 连接是怎么建立的?#
2.1 为什么要「握手」?#
想象你给朋友打电话:
- 你拨号,说:「喂,听得到吗?」(试探一下线路通不通)
- 朋友说:「听到了,你能听到我吗?」(确认你能听到,同时反向试探)
- 你说:「能听到,那我们开始聊吧。」(确认双向都通了,开搞)
这就是三次握手的逻辑。TCP 要确保双方都能发送和接收数据,才正式建立连接。
2.2 三次握手详细过程#
1 | 客户端(主动打开) 服务器(被动打开) |
第一步:SYN(同步)
客户端选一个初始序号 x,发送 SYN 报文段给服务器。此时客户端进入 SYN_SENT 状态。
意思是:「嘿,我想跟你建连,我的初始序号是 x」
第二步:SYN+ACK(同步+确认)
服务器收到 SYN 后,自己也选一个初始序号 y,然后回复 SYN+ACK 报文段。确认号是 x+1(表示「x 之前的我都收到了,等你发 x+1」)。服务器进入 SYN_RCVD 状态。
意思是:「收到!我也想跟你建连,我的序号是 y,你发的 x 我收到了」
第三步:ACK(确认)
客户端收到 SYN+ACK 后,再发一个 ACK,确认号是 y+1。双方都进入 ESTABLISHED 状态。
意思是:「你的 y 我也收到了,咱俩正式开聊!」
2.3 为什么是三次,不是两次?#
这是一个经典面试题。
两次握手的问题:假设客户端发了一个 SYN,但因为网络延迟,这个 SYN 过了很久才到服务器。此时客户端早就放弃了这个连接(超时了),但服务器不知道,它会回复 SYN+ACK,然后就傻等。如果只有两次握手,服务器在发出 SYN+ACK 后就认为连接建立了,会一直等数据——白白浪费资源。
三次握手中,服务器发了 SYN+ACK 后还要等客户端的第三个 ACK 才算建立连接。如果客户端没回应,服务器可以超时重发或放弃,不会一直傻等。
🍵 生活类比
两次握手就像你敲别人家的门,里面说「请进」,你就直接进去了——但如果这个「请进」是说给之前敲门的另一个人(延迟的旧敲门),那就尴尬了。三次握手就是你在门外面再确认一句「真的是请我进吗?」,里面说「是的」,你才进去。
2.4 实战:用 tcpdump 抓三次握手#
让我们实际抓一个 TCP 三次握手的包看看:
1 | # 在一个终端窗口开始抓包(抓取 22 端口的包) |
你会看到类似这样的输出:
1 | IP 127.0.0.1.54321 > 127.0.0.1.22: Flags [S], seq 1234567890, win 65535, ... |
- 第一行
Flags [S]就是 SYN - 第二行
Flags [S.]就是 SYN+ACK(那个.代表 ACK 标志位) - 第三行
Flags [.]就是最后的 ACK
三行,三次握手,完美。
三、四次挥手:TCP 连接是怎么断开的?#
3.1 为什么断开要四次?#
建立连接只需要三次握手,断开却要四次挥手。这是因为断开是单向的——A 可以停止发送数据,但 B 可能还有数据要发给 A。
1 | 客户端 服务器 |
第一步:主动方发 FIN,表示「我没数据要发了」
第二步:被动方回 ACK,表示「知道了,你先别发了」。但被动方可能还有数据要发,所以这个阶段叫 CLOSE_WAIT
第三步:被动方数据发完了,也发 FIN,表示「我也没数据了」
第四步:主动方回 ACK,表示「收到,再见」
3.2 为什么要等待 2MSL?#
主动方在最后发完 ACK 后,不会立刻关闭,而是进入 TIME_WAIT 状态,等待 2 倍的 MSL(Maximum Segment Lifetime,报文最大生存时间,通常 2 分钟)。
原因有两个:
- 保证最后的 ACK 能到达对方:如果最后的 ACK 丢了,对方会超时重发 FIN,你还能再回一个 ACK。如果你直接关了,对方就永远等不到确认了。
- 让旧连接的报文消失:防止延迟的旧报文段被错误地接收为下一个新连接的数据。
🍵 生活类比
四次挥手就像两个人挂电话:
A:「我没啥事说了,先挂了?」(FIN)
B:「好的,但我还有两句。」(ACK)
B:「……(说完最后的话)……好了,我也挂了。」(FIN)
A:「好的,拜拜!」(ACK)
A 挂了之后还默默拿着电话等了一会儿(TIME_WAIT),怕 B 没听到「拜拜」又打过来。
3.3 实战:观察 TCP 状态#
1 | # 查看所有 TCP 连接及其状态 |
如果你的服务器跑了很多 Web 服务或者 frp 隧道,你很可能会看到大量的 TIME_WAIT 状态连接——这完全是正常的,是 TCP 在做它该做的事。
四、TCP 可靠传输:怎么保证数据不丢不错不乱?#
4.1 可靠传输的四大法宝#
TCP 的可靠性不是魔法,它靠的是四个核心机制:
① 序号与确认#
每个发送的字节都有序号。接收方通过确认号告诉发送方「我收到了哪些」。如果发送方发出数据后一段时间没收到确认,就认为丢了,重新发。
② 重传#
发送方每发一个数据段就启动一个定时器(重传定时器)。超时了还没收到 ACK?重传!
超时时间(RTO,Retransmission Timeout)不是固定的,TCP 会动态估算往返时间(RTT)来设置合理的超时值。
③ 流水线与滑动窗口#
一个一个发太慢了——发一个等一个 ACK,网络利用率极低。TCP 用滑动窗口机制,允许一次发多个数据段,不用等每个都确认。
④ 冗余 ACK 与快重传#
如果接收方收到一个乱序的段(比如缺了 3 号段),它会发一个冗余 ACK(重复确认之前收到的最后一段)。如果发送方收到 3 个冗余 ACK,就认为那个段丢了,立即重传——这就是快重传,不用等超时。
🍵 生活类比
想象你在网购,快递分 10 箱送过来:
- 序号与确认:每箱都有编号,你签收时告诉卖家「1-5 箱收到了」
- 重传:第 6 箱迟迟没到,你等了 3 天后联系卖家说「重发」
- 滑动窗口:你不用一箱一箱收,可以同时有 5 箱在路上
- 快重传:你收到了 7、8、9 箱但没收到 6 号,你连着催了 3 次「6 号呢?」,卖家立刻补发 6 号,不用等 3 天超时
4.2 TCP 的状态机#
TCP 连接的完整生命周期涉及多个状态。这里画一个简化的状态转移图:
1 | 被动打开 |
不用担心记不住——实际开发中你很少需要手动跟踪这些状态。但理解了它,遇到网络问题时你就知道去哪里查。
五、流量控制:别淹死我!#
5.1 什么是流量控制?#
流量控制(Flow Control)解决的是:发送方发太快,接收方处理不过来怎么办?
接收方有一个接收缓存。如果发送方发得太快,缓存满了,后续的数据就会被丢弃。为了解决这个问题,TCP 让接收方在 ACK 中带上自己的接收窗口大小,告诉发送方「我还能吃多少」。
5.2 滑动窗口机制#
发送方维护一个滑动窗口,窗口大小 = 接收方通告的窗口大小。窗口内的数据可以连续发送,不用等确认。随着 ACK 到达,窗口向前「滑动」,露出新的可发送数据。
1 | 发送窗口(Window = 400 bytes): |
当接收方处理慢、缓存快满时,它会通告一个越来越小的窗口,甚至降到 0(零窗口)。发送方看到零窗口就暂停发送,等接收方缓出空间后再继续。
🍵 生活类比
流量控制就像你往浴缸里放水:
- 水龙头(发送方)控制水流大小
- 浴缸(接收缓存)有固定容量
- 如果水放得太快,浴缸要溢出了,你得关小水龙头
- 等水用了部分后,再把水龙头开大
接收方通告窗口大小,就是浴缸在喊「还能装多少水!」
六、拥塞控制:别把路堵了!#
6.1 流量控制 vs 拥塞控制#
很多人搞混这两个概念:
| 流量控制 | 拥塞控制 | |
|---|---|---|
| 解决什么问题 | 发送方 vs 接收方 | 所有发送方 vs 网络 |
| 类比 | 别淹死浴缸 | 别堵死高速公路 |
| 依据 | 接收方的窗口通告 | 发送方自己的网络感知 |
| 控制者 | 接收方决定 | 发送方自己决定 |
流量控制是「点对点」的——发送方根据接收方的容量调速。
拥塞控制是「全局」的——发送方根据网络的整体拥堵情况调速。
6.2 拥塞控制的四大算法#
TCP 拥塞控制由四个经典算法组成,它们是 TCP 的灵魂。
① 慢启动(Slow Start)#
别被名字骗了——慢启动其实一点都不慢。
连接刚建立时,发送方不知道网络能承受多少数据,所以从 cwnd = 1 MSS(最大报文段长度,通常约 1460 字节)开始。每收到一个 ACK,cwnd 加 1。
看起来每轮只加 1,但注意:每一轮的 ACK 数量等于这一轮发送的段数,所以 cwnd 实际上是指数增长的:
1 | RTT 1: cwnd = 1 (发送 1 段) |
1 → 2 → 4 → 8 → 16 → 32,蹭蹭蹭往上涨。直到达到一个阈值(ssthresh),就切换到拥塞避免模式。
🍵 生活类比
搬进新宿舍,不知道电线能承受多大功率。先开一盏灯(1),没问题?再开两盏(2),还是没事?开四盏(4)……一直翻倍试探,直到快跳闸了就停下来,改成一盏一盏地加。
② 拥塞避免(Congestion Avoidance)#
当 cwnd 达到 ssthresh 后,从指数增长切换为线性增长——每个 RTT 只增加 1 MSS。
这就像从「猛踩油门」切换到「轻踩油门」。因为已经接近网络容量了,要小心试探。
1 | RTT 6: cwnd = 32 (ssthresh = 32) |
③ 快重传(Fast Retransmit)#
之前提到了:如果收到 3 个冗余 ACK,说明那个段大概率丢了。TCP 不等超时,立刻重传丢失的段。
这比等超时(可能要等几秒)快多了。
④ 快恢复(Fast Recovery)#
快重传之后,TCP 不是从头开始慢启动,而是:
- 把
ssthresh设为当前cwnd的一半 - 把
cwnd设为新的ssthresh - 进入拥塞避免模式(线性增长)
也就是说,从一半的位置重新线性增长,而不是从 1 开始指数增长。这更合理,因为收到冗余 ACK 说明网络还在传输数据,只是有点堵,不是完全崩了。
但如果是超时丢包呢?(连冗余 ACK 都没收到,直接超时)
那就严重了:
ssthresh = cwnd / 2cwnd = 1- 回到慢启动模式
🍵 生活类比
你在高速上开车:
- 慢启动:刚上高速,从低速开始,但很快加速(指数增长)
- 拥塞避免:接近限速了,慢慢加(线性增长)
- 快重传 + 快恢复:前面看到有几辆车急刹(冗余 ACK),你减速到一半,但没停车,继续慢慢加速
- 超时:直接堵死了(超时),你只能从一档重新开始
6.3 完整的拥塞控制状态转移#
1 | cwnd = 1 |
6.4 TCP 变体:Reno vs Cubic vs BBR#
教科书上讲的是 TCP Reno(快重传+快恢复的发明者),但实际世界更丰富:
- TCP Reno:经典版本,上面的算法就是它
- TCP Cubic:Linux 默认(2008-至今),窗口增长函数是三次方程,在高带宽延迟网络中表现更好
- TCP BBR:Google 开发(2016),不看丢包,而是根据 RTT 和带宽吞吐量来调整,在弱网环境下表现优异
你可以查看你的 Mac mini 当前用的是什么拥塞控制算法:
1 | # macOS 查看TCP拥塞控制 |
七、实战分析:你身边的 TCP#
7.1 SSH 连接中的 TCP#
你每天用 SSH 连 Mac mini,这就是 TCP 的典型场景:
1 | # 查看 SSH 连接的完整信息 |
SSH 对可靠性要求极高——你不能容忍一个 rm 命令在传输中丢失然后没执行(或者误执行)。所以 SSH 用 TCP 是天经地义的选择。
7.2 frp 隧道中的 TCP#
你的 frp 内网穿透隧道,底层也是 TCP 连接:
- frpc → frps:一条持久的 TCP 连接,所有流量复用这条连接
- 用户 → frps → frpc → 内网服务:每次访问都可能有新的 TCP 连接建立和断开
这就是为什么你经常在 frp 日志里看到大量 TIME_WAIT 状态的连接——每个 HTTP 请求都对应一次 TCP 连接的生命周期。
7.3 实战:观察 TCP 重传#
1 | # macOS 查看 TCP 统计信息 |
7.4 实战:TCP 连接状态统计#
1 | # 统计各状态的 TCP 连接数量 |
典型输出:
1 | 142 TIME_WAIT # 正常,刚关闭的连接还在等 2MSL |
如果你看到大量 CLOSE_WAIT,通常意味着应用程序有 bug——收到对方的 FIN 后没有调用 close(),导致连接一直卡在 CLOSE_WAIT。
八、TCP vs UDP:何时用谁?#
经过前面几天和今天的学习,我们来做个完整对比:
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(重传、确认、序号) | 不可靠(尽力而为) |
| 顺序 | 有序到达 | 可能乱序 |
| 速度 | 较慢(开销大) | 快(开销小) |
| 头部大小 | 20 字节 | 8 字节 |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有(慢启动等) | 无 |
| 适用场景 | SSH、HTTP/1、文件传输 | DNS、视频流、游戏、QUIC |
值得注意的是,现代趋势是 UDP + 应用层可靠性 的组合。比如 HTTP/3(QUIC)就用 UDP 在应用层实现了类似 TCP 的可靠传输,同时避免了 TCP 的队头阻塞问题。这是后话了,等我们学到应用层进阶的时候再聊。
九、关键知识点回顾#
读完今天的内容,确认你掌握了以下要点:
- 三次握手:SYN → SYN+ACK → ACK,确保双向通信能力
- 四次挥手:FIN → ACK → FIN → ACK,允许半关闭(一方先停,另一方继续发)
- TIME_WAIT:等待 2MSL,保证最后的 ACK 到达 + 让旧报文消失
- 可靠传输四机制:序号与确认、超时重传、滑动窗口、冗余 ACK + 快重传
- 流量控制:接收方通过通告窗口大小控制发送方速度(点对点)
- 拥塞控制:发送方感知网络拥堵,调整发送速率(全局)
- 慢启动:
cwnd从 1 开始,指数增长到ssthresh - 拥塞避免:超过
ssthresh后线性增长 - 快重传:3 个冗余 ACK 触发立即重传,不等超时
- 快恢复:快重传后
cwnd减半进入线性增长,不回 1 - 超时事件:
cwnd回 1,ssthresh减半,重新慢启动
🧠 记忆口诀
三握四挥建断连,
序号确认保可靠,
滑动窗口控流量,
慢启拥避快重恢。
十、Day 9 预告:进入网络层!#
今天我们彻底搞定了传输层。从明天开始,我们要往下钻一层,进入计算机网络的核心——网络层。
Day 9 你将学到:
- 🌐 网络层概述:数据平面 vs 控制平面,转发与路由的区别
- 📦 数据平面:路由器是怎么读 IP 地址、查表、转发数据包的?
- 🔧 路由器工作原理:输入端口、交换结构、输出端口, longest prefix matching(最长前缀匹配)
网络层是整个互联网的「骨架」——没有它,数据包就不知道该往哪走。
「TCP 像快递公司的调度系统,保证包裹送达。但包裹走哪条路?这是网络层的事。」
明天见!🚀
本文是《计算机网络:自顶向下方法》第 8 版读书笔记系列的第 8 篇。上一篇我们聊了传输层概述与 UDP,今天深入了 TCP 的方方面面。如果觉得有帮助,欢迎分享给也在学网络的朋友。