tcp协议
2026年4月6日 · 3690 字 · 更新 2026年4月7日
TCP协议的特性
TCP是构建现代互联网稳定性的基石,是一个极度严谨、面向状态的协议,实现了在不可靠的互联网络上提供可靠的端到端字节流传输。它的核心特性可以概括为以下五个关键维度:
- 面向连接
- 可靠传输
- 流量控制
- 拥塞控制
- 全双工通信
面向连接
在数据传输之前,TCP必须通过经典的三次握手建立逻辑连接。这意味着TCP有状态,每个连接都占用服务器的内存资源(文件句柄、缓冲区)。三次握手流程图如下:

-
首先由客户端发起连接,请求报文中包含SYN和seq
-
SYN是TCP报文中的一个标志位,表示发起连接。它的物理值只有0或1,1表示这是一个同步序列号的请求,通常出现在连接建立阶段(三次握手的前两次);0表示连接已经建立,目前处于普通的数据传输阶段
-
seq是TCP报文中的序列号,表示该报文段中数据的第一个字节在整个字节流中的编号,用来保证TCP的可靠、有序传输。初始序列号
ISN是在三次握手中随机生成的,这样可以避免旧连接干扰新连接。另外,不要把它理解为包的编号,因为TCP的设计是面向字节流的,而包是操作系统帮你拆的,接收方也只看字节的具体位置
-
-
服务端如果接受客户端的请求,会返回一个确认报文
-
ACK也是TCP报文中的一个标志位,表示该报文是确认报文
-
ack是TCP报文中的确认号,表示服务端期望收到客户端下一个报文段数据的第一个字节的序号。由于客户端SYN标志为1,因此SYN也会占据一个序列号,故x本身也被确认了,下一次期望拿到的序列号是x+1
-
SYN表示服务端收到了请求,也想建立连接,而服务端这边的初始序列号为y
-
-
客户端收到服务端的确认请求后,最后再发起确认报文
-
因为发起连接请求时SYN已经消耗了一个序号,因此回复seq=x+1
-
由于服务端的SYN标志为1,也会占据一个序列号,故y本身也被确认了,下一次期望拿到的序列号是y+1
-
连接建立后的数据传输阶段,SYN为0,它将不再占据一个序列号。如果客户端发送seq=100、数据长度为50,那实际占用是100~149,服务端会回复ack=150,表示已经收到100~149这些字节,下一步期望的是150,不需要再加1了
状态机
底层网络(IP层)是无连接、不可靠的,中间路由器只管转发IP包,并不记得你的连接。因此号称有连接的TCP协议本质上也是无连接的。为了确保无论网络环境多复杂(丢包、乱序),收发双方都能同步,且能根据不同类型的报文做出不同的响应,TCP在两端主机的内存中维护了一个状态机。根据RFC标准,TCP有如下11个核心状态,它们支撑了连接的完整生命周期
- LISTEN:服务端等待客户端请求
- SYN_SENT:客户端已发送SYN,等待同步确认
- SYN_RCVD:服务端已收到SYN并回发了SYN+ACK,处于半连接状态
- ESTABLISHED:连接正式建立,双方可以自由交换字节流
- FIN_WAIT_1:主动关闭(通常客户端)发送FIN,等待对方确认
- FIN_WAIT_2:主动关闭方收到对方确认ACK,但还未收到对方FIN
- CLOSE_WAIT:被动关闭方收到对方FIN,等待应用程序关闭连接
- LAST_ACK:被动关闭方发送FIN后,等待对方ACK
- CLOSING:双方同时关闭,发送FIN,等待对方ACK
- TIME_WAIT:等待超时,确保对方收到最后ACK
- CLOSED:初始状态或者连接已经关闭
在三次握手中,客户端的状态机变化为CLOSED -> SYN_SENT -> ESTABLISHED,服务端的状态机变化为LISTEN->SYN_RCVD->ESTABLISHED
为什么不能是两次握手?
两次握手的流程是这样的:
- 客户端:发起连接请求
SYN=1,seq=x - 服务端:确认后也发起连接
seq=y、Ack=x+1、SYN=1、ACK=1
客户端并没有收到确认,服务端却单方面的认为连接确认了,这就会导致半开连接或假连接,无法保证双向通信的可靠性。那为什么不能是四次握手呢?因为第四次握手没有实际意义,只是重复确认客户端的ACK,既增加了延迟,效率又低
可靠传输
这是TCP最核心的价值,它通过以下机制确保数据不丢、不乱、不重
- 序列号:给每个字节分配编号,接收方根据编号重组乱序到达的数据包
- 确认应答:接收方收到包后必须回复发送方
- 校验和:检测数据在传输过程中是否损坏
- 超时重传:如果发送方在特定时间内没收到ACK,会认定丢包并自动重发
队头阻塞问题
TCP通过序列号和确认应答保证数据有序传输,但是有序的数据一定能够按照顺序被处理吗?接收方操作系统内核在将数据交给应用层之前,必须要确保所有字节按seq顺序排好。假设客户端发送了三个包:包1(seq:1-500)、包2(seq:501-1000)、包3(seq:1001-1500)
- 包1和包3都顺利到达了,但包2在底层网络中丢了
- 接收方内核虽然拿到了包3,但因为它不是连续的,内核必须把包3存在缓冲区里,不能交给应用层
- 应用层即便急需包3的数据,也只能在那里干等
这就导致了一个包的丢失导致后续所有到齐的包都被阻塞
HTTP/2虽然通过多路复用技术能够处理单个连接的多个请求,但由于TCP的队头阻塞问题,一个TCP包丢失,会阻塞整个TCP字节流的交付,从而影响该连接上的所有HTTP/2流。这也是HTTP/3转向QUIC协议的根本原因
丢包重传机制
为了处理丢包,TCP设计了四种主要的重传机制
- 超时重传:发送方发完包后启动定时器。如果在一定时间内(RTO)没收到ack,就认为包丢了,重新发;RTO是基于采样的RTT动态计算得出的,如果RTO设置太短会造成不必要的重传;太长则反应太慢
- 快速重传:不等待定时器超时。当发送方连续收到三个相同的冗余ACK(比如都在要seq:501),说明501肯定丢了,需要立即重传。这种机制比RTO快得多,能迅速填补字节流的空洞
- SACK:在TCP头部选项里增加了一个“地图”,能够精确重传缺失的片段。如明确告诉发送方 - “我收到了1-500和1001-1500,中间缺了501-1000”,这样就可以不用傻傻地把501之后的所有包全重发一遍
- D-SACK:利用SACK告诉发送方“这个包我收到重复的了”。这能帮发送方判断,是包丢了,还是ack丢了,或者是网络把包发重了
RTT:是请求发送到目的地并返回响应的总时间。在三次握手中,从连接逻辑上看,是1.5个RTT;从首字节传输的性能看,是1个RTT
流量控制
TCP流量控制是确保发送方不会发送太快、淹没接收方的关键机制。其核心原理基于字节的滑动窗口协议来实现,主要由接收方通过通告窗口大小(rwnd)控制发送方的发送上限。有三种场景:
- 如果接收方处理数据很快,有大量空闲缓冲区。接收方会通告一个更大的rwnd,发送方根据这个大窗口持续发送数据
- 如果接收方处理速度很慢,导致接收缓冲区逐渐被填满。接收方则减小rwnd,并在ACK中通告给发送方
- 如果接收缓冲区完全满了,接收方会通告接收窗口为0。发送方收到0窗口后,必须停止发送数据。一旦接收方的应用读取了一些数据,缓冲区腾出一定空间时,接收方就会发送一个Window Update报文段,通告新的rwnd大小,发送方才能恢复发送
rwnd体现在TCP报文的Window Size字段中
拥塞控制
TCP拥塞控制,关注的是整个网络传输路径上的所有设备和链路的负载情况,主要是怕把网络塞满,解决的是网络自身的拥塞问题。本质也是通过调整窗口的大小来控制,但这个窗口由发送方维护。有以下四个核心算法:
- 慢启动:刚开始发包时,TCP并不知道网络带宽是多少,先发1个MSS(最大报文段),每收到一个ACK,拥塞窗口 (cwnd) 就翻倍,呈指数级增长。这样可以迅速探测出网络的承载上限
- 拥塞避免:当cwnd达到一个阈值(ssthresh,慢启动门限)时,cwnd会呈线性增长。每经过一个RTT,cwnd只增加1个MSS,直到发生丢包。实现在接近网络上限时,缓慢逼近最大带宽,尽量延长不丢包的时间
- 快速重传:当网络真的塞满了,出现了丢包时,使用重传机制补数据
- 如果是RTO重传机制:将ssthresh降为当前cwnd的一半,cwnd则直接重置为1,重新进入慢启动。这是一个惩罚阶段,会导致网络吞吐量剧降(“锯齿波”低谷)
- 如果是快速重传:TCP 认为网络还没彻底瘫痪,只是有点挤(进入快速恢复)
- 快速恢复:不会将cwnd降为1,而是降为原来的一半,然后直接进入“拥塞避免”阶段。这样可以避免网络吞吐量断崖式下跌,维持高效率传输
以上拥塞控制存在缺陷,其一是丢包不等于阻塞,比如在无线网络中,丢包可能是因为信号干扰而非由于路由器排队塞满,如果这是盲目减半cwnd,导致在带宽充足但链路不稳的情况下,传输速度极其低下;其二是缓冲区膨胀冲突,中间设备为了不丢包,设置了巨大的缓存,TCP傻傻地一直发,导致RTT飙升,虽然没丢包,但交互极其卡顿
# 使用以下命令可以查看系统当前默认的算法
sysctl net.ipv4.tcp_allowed_congestion_controlQUIC协议是一个基于UDP的用户态传输协议,它把协议栈从内核移到了应用层,可以更容易更换拥塞控制算法,不需要更新内核
全双工通信
TCP是全双工协议,一个TCP连接是两条独立的字节流,允许数据在两个方向同时传输。这是因为每个方向都有独立的seq/ack,收发都是并行的。带来的好处是能够提高效率、支持实时通信,并且支持流式数据。但需要注意的是全双工不等于不会互相影响,即使是独立流,但由于底层还是一个TCP连接,所以丢包、拥塞控制仍然会影响两个方向
四次挥手
TCP连接关闭的过程使用四个步骤来终止连接,因为是全双工通信,所以双方的连接必须分别关闭,因此需要两次“请求关闭” + 两次“确认”,也就是四次。假设客户端主动关闭,四次挥手的全过程如下图:
流程简述如下:
- 客户端:我这边不再发送数据了
- 服务端:知道了,但我可能还有数据没发完
- 服务端:我也发完了,可以关闭了
- 客户端:确认关闭
为什么不能是三次挥手呢?因为服务端可能还有数据没发完,必须等它把数据发完,再FIN,防止数据丢失 为什么有TIME_WAIT?这是为了确保最后的ACK能够送达。如果四次挥手中的最后一个ACK丢包了,客户端可以在TIME_WAIT期间收到服务端重发的FIN,然后再次补发一个ACK,从而保护服务端能正常进入CLOSED。而且有了TIME_WAIT,还能防止“迷途的报文”干扰新连接,2MSL的时间足够让网络中所有属于该连接的旧报文(无论是发出的还是收到的)都因超时而消失。当TIME_WAIT结束时,网络里已经干干净净,新连接可以放心使用
在Linux源码中,2MSL通常被硬编码为60秒
TCP包头

- 源端口:标识发送方进程使用的端口号,用于区分同一台主机上的多个进程(如HTTP的80)
- 目的端口:标识接收方进程监听的端口号
- 序列号:表示该报文段中数据的第一个字节在整个字节流中的编号
- 确认号:表示服务端期望收到客户端下一个报文段数据的第一个字节的序号
- 首部长度:表示TCP头有多长
- 保留位:供将来使用
- 标志位:每个比特位表示一个控制功能
- SYN:建立连接
- ACK:确认有效
- FIN:关闭连接
- RST:重置连接
- PSH:立即推送
- URG:紧急数据
- 窗口大小:表示接收端可接收的字节数,用于流量控制
- 校验和:用来检验TCP段在传输过程中是否出错
- 紧急指针:表示紧急数据位置
- 选项:长度可变,包含一些可选功能。如MSS、SACK、时间戳等
- 填充:用于TCP首部字节对齐
四元组标识(源IP+源端口+目的IP+目的端口)可以确定一条唯一的TCP连接