最近复习《计算机网络》,被 TCP 的三次握手和四次挥手虐了一个晚上。索性一咬牙,把传输层这块从头到尾捋了一遍,整理成这篇笔记。文章尽量用"说人话"的方式来讲,配了不少简图,希望和我一样正在被 TCP 折磨的同学,看完能少掉几根头发。
一、TCP 是个啥?先搞清楚它在哪一层
讲协议之前先定位。我们熟知的 TCP/IP 体系分为四层,TCP 工作在传输层:
应用层 HTTP、FTP、SMTP、DNS…… (数据) ↓ 传输层 TCP / UDP (TCP 报文段) ↓ 网络层 IP (IP 数据报) ↓ 网络接口层 网卡、驱动程序 (帧)
一句话总结:TCP 是传输层的协议,为应用层提供面向连接的、可靠的字节流服务。
这里先拉一个对照组——UDP:无连接、不可靠、但简单高效。TCP 的"可靠"不是白来的,是靠后面要讲的一整套机制(序号、确认应答、超时重传、连接管理)换来的。这篇文章就围绕"TCP 是怎么做到可靠的"这条主线展开。
二、TCP 报文段格式(简单版)
TCP 是面向字节流的,但在网络层看来,它传输的数据是一个个报文段(Segment)。可以把报文段理解成一张"快递单 + 货物":前面是固定格式的头部(快递单),后面跟着数据(货物)。
报文段头部字段一览(挑重点的讲):
字段 | 位数 | 作用 |
源端口 | 16 | 数据从哪个进程来 |
目的端口 | 16 | 数据到哪个进程去 |
序号 seq | 32 | 本报文段第一个字节的编号 |
确认号 ack | 32 | 期望收到对方下一个字节的编号 |
4 位首部长度 | 4 | 头部有多长(单位是 4 字节,最大 60 字节) |
保留位 | 6 | 目前全为 0 |
控制位 | 6 | URG / ACK / PSH / RST / SYN / FIN |
窗口大小 | 16 | 告诉对方"我这边还能收多少"(流量控制用) |
校验和 | 16 | 检验报文段是否损坏 |
紧急指针 | 16 | 配合 URG 使用 |
用一张图看整体结构:
0 3 4 7 8 15 31 位 +----------+----------+----------+----------+----------+----------+ | 源端口(16 bit) | 目的端口(16 bit) | +----------+----------+----------+----------+----------+----------+ | 序号 seq(32 bit) | +----------+----------+----------+----------+----------+----------+ | 确认号 ack(32 bit) | +----------+----------+----------+----------+----------+----------+ | 4位首部长度 | 保留6位 |U|A|P|R|S|F| 窗口大小(16 bit) | +----------+----------+----------+----------+----------+----------+ | 校验和(16 bit) | 紧急指针(16 bit) | +----------+----------+----------+----------+----------+----------+ | 选项(长度可变,如 MSS) | +----------+----------+----------+----------+----------+----------+ | 数据(可以没有) | +----------+----------+----------+----------+----------+----------+
控制位:只重点记这四个
6 个控制位里,面试和学习中最常用的是下面四个,先记住它们:
标志位 | 全称 | 含义 |
ACK | Acknowledgment | 确认标志位。为 1 时,确认号字段才有效 |
SYN | Synchronize | 请求建立连接的标志位 |
FIN | Finish | 请求释放连接的标志位("我说完了") |
RST | Reset | 重置连接。连接出现严重错误时,强制断开,重新来 |
另外两个一笔带过:URG(紧急指针有效)、PSH(推送,尽快把数据交给应用层),了解即可。
敲黑板:报文格式不用死记硬背,记住"端口 + 序号 + 确认号 + 控制位"这条主干,剩下的字段用到再查。
三、确认应答:你发货,我回执
TCP 可靠传输的第一块基石是确认应答机制。
思想非常朴素:发送方每发一段数据,接收方收到后要回一个"我收到了"(ACK),发送方确认对方收到了,才继续放心发后面的数据。
这里就要用到头部里的序号 seq和确认号 ack了,它们的含义是:
- seq:本报文段中数据的第一个字节在整个字节流中的编号;
- ack:期望收到对方下一个字节的编号,同时隐含"ack 之前的字节我都正确收到了"。
看个例子:
客户端 服务端 seq = 1,发送 100 字节数据 ─────────▶ 收到数据,校验无误 ◀───────── ACK = 1,ack = 101 (含义:101 之前的我都收到啦) seq = 101,继续发下一段……
注意上图中 ack = 101 的含义:不是"我收到了编号 101",而是"我已经收到 100 号为止的数据,下次请从 101 开始发"。这个细节刚开始学特别容易搞混,多品两遍。
有了确认应答,发送方就能知道"哪段数据对方确实收到了"。但问题来了:如果数据包在路上丢了呢?
四、超时重传:发货丢了怎么办
网络是不靠谱的,数据包可能丢失。TCP 的对策是:发送方发出数据后启动一个超时计时器,如果在规定时间内没收到对应的 ACK,就重新发送这段数据。这就是超时重传。
丢包分两种情况:
情况一:数据包本身丢了。
客户端 服务端 发送 seq = 1 ~ 100 ──── ╳ ────▶ (数据包在路上丢失) (启动超时计时器) 等待中…… ⏰ 超时!没等到 ack = 101 重发 seq = 1 ~ 100 ─────────▶ ACK = 1,ack = 101
情况二:数据到了,但 ACK 回执丢了。
这种情况下接收方其实已经收到了数据。当发送方超时重发时,接收方会发现"这段数据我见过"(通过序号判断),于是丢弃重复的数据,并重新发一次 ACK。所以接收方也有一套去重机制,重传不会导致数据重复堆积。
敲黑板:超时时间不是固定值,TCP 会持续测量数据包的往返时间(RTT),动态调整超时阈值——网络快就少等会儿,网络慢就多等会儿。具体算法(如 Jacobson 算法)了解思路即可,不用记公式。
到这一步,"可靠传输"的两个核心手段——确认应答和超时重传——就都登场了。但只有这两样还不够:传输之前总得先确认对方在不在、通不通。这就引出了 TCP 最经典的环节——连接管理。
五、连接管理(重点)
TCP 是面向连接的协议:正式传数据之前,要先"打电话"确认双方都在(建立连接);传完了,要"说再见"挂电话(释放连接)。前者叫三次握手,后者叫四次挥手。
5.1 三次握手
过程图解
假设客户端主动发起连接,服务端在 80 端口监听:
客户端(CLOSED) 服务端(LISTEN) ① SYN = 1,seq = x ─────────▶ 客户端状态:CLOSED → SYN_SENT ◀───────── ② SYN = 1,ACK = 1, seq = y,ack = x + 1 服务端状态:LISTEN → SYN_RCVD ③ ACK = 1,seq = x + 1 ─────────▶ ack = y + 1 客户端状态:SYN_SENT → ESTABLISHED 服务端状态:SYN_RCVD → ESTABLISHED
三次报文走完,双方进入ESTABLISHED(已建立连接)状态,可以开始互相传数据了。
每一步在干嘛
- 第一次握手:客户端把
SYN置 1、随机生成一个初始序号seq = x发给服务端,相当于说"我想跟你建立连接,我的数据从 x 开始编号"。发完进入SYN_SENT状态。
- 第二次握手:服务端收到后,如果同意建立连接,就回一个
SYN = 1, ACK = 1的报文:SYN表示"我也想跟你连接,我的序号是 y",ACK = x + 1表示"你的请求我收到了"。之后服务端进入SYN_RCVD状态(半连接状态)。
- 第三次握手:客户端回一个
ACK = 1, ack = y + 1,表示"你的建立请求我也收到了"。双方正式进入ESTABLISHED。
这里有两个常见小疑问,顺手解答:
Q1:seq = x 为什么是随机的?初始序号(ISN)随机生成,是为了防止网络中滞留的、上一个旧连接的报文段被新连接误收。如果序号从固定值开始,历史残留报文就可能"混进"新连接造成数据错乱。
Q2:为什么握手是三次,不是两次?
这是面试高频题,分两个角度回答:
- 两次无法可靠确认双方的收发能力。TCP 是全双工的,连接建立后双方都要能收能发。三次是确认"双方收发能力都正常"的最小次数:
- 第一次:服务端确认"客户端能发、我能收";
- 第二次:客户端确认"服务端能收能发、我也能发";
- 第三次:服务端确认"客户端能收"。 只有三次,双方才能都确认彼此的收发通道是通的。
- 两次无法防止"历史重复连接"造成错误。设想:客户端发的第一个 SYN 在网络里滞留了,客户端超时后重发 SYN 并正常完成传输、关闭了连接;这时那个滞留的旧 SYN 才到达服务端。如果是两次握手,服务端收到 SYN 就直接建立连接、干等客户端发数据——但客户端早就走了,这个连接就白占着资源。而三次握手中,服务端必须再等客户端回最后一次 ACK 才算建立,滞留在历史洪流里的旧 SYN 就不会被"将错就错"地接受。
敲黑板:记住这个比喻——两次握手就像你打电话喊了一句"喂",没听到对方回应就开始自说自话;三次握手中,双方都确认了对方既在听也能说,通话才算真正建立。
三次握手状态迁移图
客户端 服务端 ┌───────┐ ┌─────────┐ │CLOSED │ │ CLOSED │ └───┬───┘ ① SYN=1, seq=x └────┬────┘ │ ─────────────────────────────────────────────▶ │ 服务端 listen ▼ ▼ ┌──────────┐ ┌──────────┐ │SYN_SENT │ │ LISTEN │ └────┬─────┘ ◀───────────────────────────── └────┬─────┘ │ ② SYN=1, ACK=1, ack=x+1 │ │ ▼ │ ┌──────────┐ │ │SYN_RCVD │ │ ③ ACK=1, ack=y+1 └────┬─────┘ │ ───────────────────────────────────────────▶ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ESTABLISHED │ ◀────────────────────────▶ │ESTABLISHED │ └──────────────┘ 双方开始正常传输数据 └────────────────┘
5.2 四次挥手
数据传完了,要断开连接。TCP 是全双工的:两个方向的数据通道要各自关闭,所以断开需要四次报文,俗称"四次挥手"。
以客户端主动关闭为例:
过程图解
客户端(ESTABLISHED) 服务端(ESTABLISHED) ① FIN = 1,seq = u ─────────▶ 客户端状态:ESTABLISHED → FIN_WAIT_1 ◀───────── ② ACK = 1,ack = u + 1 服务端状态:ESTABLISHED → CLOSE_WAIT 客户端状态:FIN_WAIT_1 → FIN_WAIT_2 (服务端可能还有数据没发完,继续发……发完了才能告别) ④ ACK = 1,ack = w + 1 ─────────▶ ③ FIN = 1,ACK = 1, 客户端状态:TIME_WAIT(等待 2MSL) seq = w,ack = u + 1 之后 → CLOSED 服务端状态:CLOSE_WAIT → LAST_ACK 收到④后 → CLOSED
每一步在干嘛
- 第一次挥手:客户端数据发完了,发一个
FIN = 1的报文("我没话说了"),进入FIN_WAIT_1状态。注意此时客户端只是不再发数据,还能收。
- 第二次挥手:服务端收到 FIN,回一个 ACK("知道了"),进入
CLOSE_WAIT状态。此时客户端进入FIN_WAIT_2。这个状态很关键:服务端进入 CLOSE_WAIT 说明"它还有数据要发"——如果服务端一直停在 CLOSE_WAIT 不往前走,通常意味着服务端还有数据没发完或者代码没正常关闭连接。
- 第三次挥手:服务端把剩下的数据发完,也发一个
FIN = 1("我也说完了"),进入LAST_ACK状态。
- 第四次挥手:客户端回最后一个 ACK,然后进入
TIME_WAIT状态,等2MSL时间后才进入CLOSED。服务端收到这个 ACK 后直接CLOSED。
为什么挥手是四次,握手却是三次?
因为握手时,服务端对"请求建立连接"的回应(SYN)和"确认收到你的 SYN"(ACK)可以合并成一个报文(SYN + ACK 一起发),所以三次就够。
而挥手时,被动关闭方收到 FIN 后,往往还有数据要发,只能先回一个 ACK("我知道了,但我还没说完"),等数据发完才能发自己的 FIN。ACK 和 FIN 没法合并,所以是四次。
TIME_WAIT 为什么要等 2MSL?
MSL(Maximum Segment Lifetime)是报文在网络中的最大存活时间。TIME_WAIT 等 2MSL 有两个原因:
- 保证最后一个 ACK 能被对方收到。如果这最后一个 ACK 丢了,服务端会重发自己的 FIN;客户端在 TIME_WAIT 期间还能收到并重新回 ACK。如果客户端发完 ACK 立刻 CLOSED,服务端就会一直重发 FIN 无法正常关闭。
- 让旧连接的报文彻底消散。等待 2MSL 后,本次连接产生的所有报文都从网络中消失了,防止这些"迟到"的报文被下一个使用相同端口的新连接误收。
敲黑板:面试常追问"服务器出现大量 TIME_WAIT 怎么办?"——TIME_WAIT 出现在主动关闭连接的一方。对主动关闭的服务器(如短连接 HTTP 服务),可以用长连接(keep-alive)减少关闭次数,或调整内核参数(如
tcp_tw_reuse)复用 TIME_WAIT 连接。点到为止,原理理解了,参数用到再查。
四次挥手状态迁移图
客户端 服务端
┌──────────────┐ ┌──────────────┐
│ESTABLISHED │ │ESTABLISHED │
└──────┬───────┘ └──────┬───────┘
│ ① FIN=1, seq=u │
▼ ▼
┌──────────────┐ ② ACK=1, ack=u+1 ┌──────────────┐
│ FIN_WAIT_1 │ ◀────────────────────── │ CLOSE_WAIT │
└──────┬───────┘ └──────┬───────┘
│ │ ③ FIN=1, ACK=1
▼ ▼ (数据发完后)
┌──────────────┐ ┌──────────────┐
│ FIN_WAIT_2 │ ④ ACK=1, ack=w+1 ──▶ │ LAST_ACK │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ 收到④ ┌──────────────┐
│ TIME_WAIT │ ────────────────────────▶ │ CLOSED │
│ (等2MSL) │ └──────────────┘
│
▼
┌──────────────┐
│ CLOSED │
└──────────────┘
六、TCP 的特点总结
最后把 TCP 的各个特点集中梳理一遍,方便收藏复习:
特点 | 说明 |
面向连接 | 传输数据前先三次握手建立连接,传完四次挥手释放连接 |
可靠传输 | 确认应答 + 超时重传 + 序号去重 + 校验和,保证数据不丢、不重、不错 |
字节流 | 数据像水流一样传输,没有消息边界;发送方写几次、接收方读几次不一定对应,需要应用层自己处理粘包 |
全双工通信 | 连接双方可同时收发数据,两个方向独立关闭 |
点对点 | 一对一通信,不支持广播和多播(UDP 可以) |
流量控制 | 通过滑动窗口,接收方用"窗口大小"字段告诉发送方自己还能收多少,防止发太快把接收方"淹没" |
拥塞控制 | 感知网络拥堵程度,动态调整发送速率:慢启动、拥塞避免、快重传、快恢复 |
流量控制和拥控制粗看很像,区别在于:
- 流量控制解决的是"接收方收不过来"——点对点,照顾你一个人;
- 拥塞控制解决的是"网络堵车了"——全局视角,别把互联网搞崩。
七、写在最后
学 TCP 最大的感受是:它的每一个机制都不是凭空设计的,而是在回答一个具体的问题——数据会丢?(确认应答 + 超时重传);连接会乱?(三次握手 + 随机序号);发太快?(流量控制 + 拥塞控制)。带着"它在解决什么问题"的角度去学,比死记硬背状态图舒服得多。
最后给个实操建议:下载一个 Wireshark,随便访问一个 HTTP 网站,过滤tcp,亲眼看一次三次握手和四次挥手的报文。当你在屏幕上指着说"这个是 SYN""这个是 FIN"的时候,TCP 就算真正入门了。
如果这篇文章对你有帮助,欢迎点赞收藏评论交流,咱们一起进步!