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_control

QUIC协议是一个基于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连接