TCP协议
# 格式

# 序号和应答确认序号
- 序号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来标识数据段本身,解决网络包乱序问题。
为什么要随机化序列号? 主要为了防止历史报文被下一个相同四元组的连接接收,其生成算法:ISN = M + F(localhost, localport, remotehost, remoteport),其中 M 是计时器,每 4 微秒加 1,F 是哈希算法。
- 确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。
序号和确认序号是确认应答机制的数据化表示,确认应答机制就是由序号和确认序号来保证的。
# 窗口大小
传输层 TCP 协议中,发送端和接收端都有发送缓冲区和接收缓冲区。当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中。但缓冲区是有大小的,如果接收端处理数据的速度小于发送端发送数据的速度,那么接收端的接收缓冲区会满,这时发送端再发送数据过来就会造成数据丢包,进而引起丢包重传等一系列的连锁反应。所以发送端怎么知道发送数据量是合适的?我们知道对方接收缓冲区剩余空间大小即可。
所以,16 位窗口大小当中填的是自身接收缓冲区中剩余空间的大小。接收端在对发送端发来的数据进行响应时,就可以通过 16 位窗口大小告知发送端自己当前接收缓冲区剩余空间的大小,此时发送端就可以根据这个窗口大小字段来调整自己发送数据的速度。
# 标志位
- SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。只有在连接建立阶段,SYN 才被设置,正常通信时 SYN 不会被设置
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段
- ACK:该位为 1 时,确认应答的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1,因为发送出去的数据本身就对对方发送过来的数据具有一定的确认能力
- PSH:该位为 1 时,告知接收端尽快将接收缓冲区当中的数据交付给上层
- URG:该位为 1 时,表示该 TCP 报文段中包含了紧急数据(发送方应用程序向接收方应用程序紧急地发送一些重要的、需要优先处理的控制信息),配合紧急指针使用,其中
紧急数据末尾序号 = 本报文段序号 + 紧急指针值 - RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接重新建立连接
# 载荷分离和交付上层
如何报头和有效载荷分离?
tcp 协议标准长度为 20 字节,先读取 20 字节,转换为结构化数据提取标准报头中的 4 位首部长度 x,后续报头的剩余大小 = 4 * x - 20,将报头读取完毕后,剩下的就是有效载荷了。如何交付上层?
应用层的每一个网络进程都必须绑定一个端口号(服务端进程必须显示绑定一个端口号,客户端进程由系统分配一个端口号)。 而 TCP 的报头中涵盖了目的端口号,因此 TCP 可以提取出报头中的目的端口号,找到对应的应用层进程(哈希的方式维护了端口号与进程 ID 之间的映射关系),进而将有效载荷交给对应的应用层进程进行处理。
# 特点
- 有连接:建立连接,间接地保证可靠性,一定是一对一的。
- 可靠:确认应答机制、超时重传机制、三次握手四次挥手等保证可靠性。
- 面向字节流:
TCP 在内核中创建一个发送缓冲区和一个接收缓冲区; 调用 write 时, 数据会先写入发送缓冲区中(如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出; 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出 去);调用 read 从接收缓冲区拿数据。
TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 既可以读数据, 也可以写数据,叫做全双工。
# TCP 连接
TCP 连接就是维护一些状态信息,保证可靠性,包括 socket,序列号和窗口大小,通过 IP 头部的源地址和目的地址的字段(32 位)和 TCP 头部的源端口和目的端口的字段(16 位)来确认哪一台主机的哪一个进程。
# 确认应答机制
TCP 实现可靠传输的方式之一,是通过序列号与确认应答。接收方在成功收到数据后,必须向发送方返回一个确认(ACK)信号。发送方只有在收到这个确认信号后,才会认为数据已经成功送达,从而发送下一个数据。确认应答机制不是保证双方通信的全部消息的可靠性,而是通过收到对方的应答消息,来保证自己历史发送的消息被对方可靠的收到了。
# 超时重传机制
但在错综复杂的网络,并不一定能顺利和正常地数据传输,万一数据在传输过程中丢失了呢?比如:主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B;如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答等。
TCP 针对数据包丢失使用重传机制解决,有以下四种:
- 超时重传:在发送数据时,设定一个定时器,超过指定时间没有收到对方的 ACK 确认应答报文,会重新发送。特定的这段时间叫做 RTO(Retransmission Timeout),过大(丢半天才重发,性能差)或过小(丢一会就重发,增加网络阻塞)都不行,一般略大于 RTT(Round-Trip Time 往返时延),因为网络波动,RTT 是一个动态的值,RTO 也是动态变化的。如果超时重传时,又一次超时的话,那么就将 RTO 设为上一次的 2 倍。可见超时重传问题是超时周期可能相对较长。
- 快速重传:例如。发送端依次发送 Se1~Seq6,结果数据 1 成功,收到 ACK2,但是数据 2 失败,重复收到 ACK2。这样的话,当收到三个相同的 ACK 报文时,触发重传,重新发送丢掉的数据报文。存在的问题就是,尽管收到了 ACK2,只能保证 Seq1 发送成功,Seq3~Seq6 呢?是否发送成功不能保证,所以重传哪几个报文呢?都有一些问题。
- SACK 方法:SACK(Selective Ack)选择性确认在 TCP 头部选项中加入 SACK,可以将将已收到的数据的信息发送给发送方。
- Duplicate SACK(D-SACK):主要是通过 SACK 告诉发送方有哪些数据被重复接收了,可以让发送方知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了。
# 连接管理机制
# 三次握手
其中,第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
三次握手的主要目的是:同步序列号、分配资源、建立可靠的双向通信通道。连接建立是由操作系统管理起来,先描述后组织的,需要成本的;三次握手这个过程,本质上就是一个双方共同确认、达成共识的协议过程。
# 为什么是三次
原因有以下几点:
- 首要原因是为了防止旧的重复连接初始化造成混乱和资源浪费
- 同步双方初始序列号
为什么不是两次或者四次?
- 两次握手: 两次握手时,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费,也无法可靠的同步双方序列号。
- 四次握手: 三次握手已经能确保双方收发能力可靠,四次握手会增加额外延迟且无必要。
# 丢了怎么办
第一次握手丢了怎么办?
若 SYN 最大重传次数 tcp_syn_retries 为 3 的话,由于发送的 SYN 没有收到 ACK,触发超时重传。当客户端超时重传 3 次 SYN 报文后,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
第二次握手丢了怎么办?
第二次握手(SYN-ACK 报文)丢失,若 SYN 最大重传次数 tcp_syn_retries 为 3,SYN-ACK 最大重传次数 tcp_synack_retries 为 2 的话,当客户端超时重传 3 次 SYN 报文后,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接;当服务端超时重传 2 次 SYN-ACK 报文后,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
第三次握手丢了怎么办?
当服务端超时重传 tcp_synack_retries 次 SYN-ACK 报文后,于是再等待一段时间,如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
# 四次挥手
其中主动断开的一方会进入TIME_WAIT状态。
四次挥手的核心目的是:双方共同确认,安全可靠地关闭一个双向通信通道。断开连接要征得双方的同意,发送FIN表示不给对方发数据(用户的数据)了,要断开连接了。
主动断开的一方的 TIME_WAIT 状态
维持的时间一般是2 * MSL(Maximum Segment Lifetime,报文最大生存时间),再进入 CLOSED 状态。
原因在于如果第四次挥手的报文丢包了,主动方在一段时间内仍然能够接收被动方重发的FIN报文并对其进行响应,能够较大概率保证最后一个ACK被被动方收到;另外,主动方四次挥手后进入TIME_WAIT状态,还可以保证滞留在网络中的报文消散,再出现的数据包一定都是新建立连接所产生的。
例如:当服务器先断开,即主动断开,再重启该端口时会bind err,因为有TIME_WAIT维持时间,可以使用setsockopt()设置 socket 描述符的选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符(端口复用)
int setsockopt(int sockFd, int level, int optname, const void *optval, socklen_t optlen);
# 为什么是四次
为什么是四次?
由于 TCP 是全双工的,建立连接的时候需要建立双方的连接,断开连接时也同样如此。在断开连接时不仅要断开从客户端到服务器方向的通信信道,也要断开从服务器到客户端的通信信道,其中每两次挥手对应就是关闭一个方向的通信信道,并且通常需要等待完成数据的发送和处理,因此断开连接时需要进行四次挥手。
为什么不是三次或五次?
- 三次挥手:当客户端发 FIN 后,服务器可能仍有数据要发送,需先回复 ACK(第二次挥手),处理完数据再发 FIN(第三次挥手)。若合并第二次和第三次挥手(直接发 FIN + ACK),可能迫使服务器立即关闭,导致数据丢失。
- 五次挥手:客户端和服务器各发一次 FIN 和 ACK,能明确关闭双向连接。五次挥手会增加冗余步骤(例如重复确认),但不会提升可靠性。
# 丢了怎么办
第一次挥手丢了怎么办?
同样地会触发超时重传,FIN 最大重传次数受 tcp_orphan_retries 限制,再等待一段时间,如果还是没能收到服务端的第二次挥手,那么客户端就会断开连接。
第二次挥手丢了怎么办?
和上面一样。在这里区分一下close和shutdown,当收到服务端的第二次挥手,close函数的关闭, tcp_fin_timeout 控制了FIN_WAIT_2的持续时长,默认为 60 秒,之后关闭;shutdown函数的关闭,指定了只关闭发送方向,而接收方向并没有关闭,若一直没能收到服务端的第三次挥手,就一直处于FIN_WAIT_2(死等)。
第三次挥手丢了怎么办?
由上可知,服务端重传 FIN,等待之后还没有收到第四次挥手,服务端断开;客户端调用 close 函数,tcp_fin_timeout 时间之后断开。
第四次挥手丢了怎么办?
服务端重传 FIN,等待之后还没有收到第四次挥手,服务端断开;客户端在收到第三次挥手后,进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手后,重置定时器,当等待 2MSL 时长后,客户端断开连接。
# 滑动窗口
在 TCP 早期版本,数据包是串行地发送和确认,这样的话 RTT 越长,效率越低。为此,TCP 引入了滑动窗口的概念,它允许发送方在等待确认的同时发送多个数据段,窗口大小就是无需等待 ACK,而可以继续发送数据的最大值。所以,窗口的大小是由接收方的窗口大小来决定的,表示还有多少缓冲区可以接收数据。
发送端发送数据之后在没有接收到确认确认之前,会保存在发送缓存区中滑动窗口部分,此时发送端的发送缓冲区分为了四部分:

滑动窗口最开始的大小怎么设定?
滑动窗口大小要小于等于对方的接受能力大小,即win_start = ack_seq, win_end = win_start + tcp_win
接收应答时,如果不是左侧报文的确认,而是中间和结尾的怎么办?
有以下两种情况:
- 数据没丢,应答确认没丢:因为应答确认的定义就是应答序号之前的丢被接收到,所以没有影响
- 数据丢了:根据快速重传算法,如果发送方连续收到 3 个或 3 个以上对同一个序列号的重复 ACK,就会推断这个期望的数据包已经丢失,发送方会立即超时重传认为丢失的报文段,从而大大提高效率。
# 流量控制

TCP 支持根据接收端的接收数据的能力来决定发送端发送数据的速度,这个机制叫做流量控制机制。
具体的操作就是接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "16 位窗口大小" 字段, 通过 ACK 端通知发送端;如果接收端缓冲区满了, 就会将窗口置为 0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端。
糊涂窗口综合症
如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这样会为了几字节而增加开销,这就是糊涂窗口综合症。要解决的话可以让接收方不通告小窗口让发送方不发送小数据。
解决方案如下:
- 当接收方窗口大小小于
min(MSS(Maximum Segment Size 最大报文段大小),缓存空间 / 2)时,直接通告窗口大小为 0 - 采用 Nagle 算法,满足收到之前发送数据的 ACK 确认应答包或者窗口大小大于等于 MSS 且数据大小大于等于 MSS 时才能发送数据
# 拥塞控制
TCP 不仅考虑了主机的问题,而且还考虑了网络的问题,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,从而恶循环。拥塞控制的目的就是避免发送方的数据填满整个网络。
拥塞控制维护了拥塞窗口 cwnd,cwnd 是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的,swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

- 初始化:当 TCP 连接建立时,发送方的拥塞窗口 cwnd 被初始化为一个很小的值,通常是 1 个最大段大小(MSS)。
- 慢启动:每经过一个往返时间(RTT),拥塞窗口的大小会加倍,即 cwnd = cwnd × 2。这样,发送速率会逐渐增加,直到达到一个阈值,这个阈值称为慢启动阈值 ssthresh。
- 拥塞避免:当拥塞窗口的大小达到慢启动阈值时,TCP 会进入拥塞避免阶段。在这个阶段,拥塞窗口的大小不再以指数方式增长,而是以线性方式增加,即 cwnd = cwnd + 1。
- 拥塞检测:如果网络出现拥塞,例如,由于发送方发送的数据量过大导致网络堵塞,网络设备可能会丢弃部分数据包。当发送方检测到有数据包丢失时,它会将拥塞窗口的大小减少到慢启动阈值,并重新开始慢启动过程。
- 重传:如果发送方在一定时间内没有收到确认(ACK)应答,它可能会认为数据包丢失,并触发重传机制。重传有超时重传和快速重传。
- 超时重传:sstresh 设为 cwnd/2,cwnd 重置为初始值,进入慢启动算法
- 快速重传:cwnd 设为 cwnd/2, sstresh 重置为 cwnd,进入快速恢复算法
- 快速恢复:快速重传和快速恢复算法一般同时使用,其动作如下:
cwnd = ssthresh + 3(加 3 是因为已收到 3 个 dup ACK);重传 Duplicated ACKs 指定的数据包,如果再收到 duplicated Acks,那么 cwnd = cwnd +1;如果收到了新的 Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法。
# 延迟应答&捎带应答
- 延迟应答:在发送方和接收方进行通信时,接收方的接收缓冲区收到了来自发送方的一批报文(滑动窗口机制),接收方收到第一个数据时,不会立刻进行 ACK,会延迟一会再进行发送!这个延迟时间不会超过超时重传的时间。延迟一会可能是更大的接收窗口,从而提高效率。
- 捎带应答:TCP 可以将确认信息(ACK)和有效数据合并到同一个报文中一起发送,而不是单独发送一个纯 ACK 包。
# TCP 和 UDP 区别
- 连接:TCP 传输数据前需要建立连接;UDP 不需要连接,即可传输数据。
- 可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达;UDP 是不可靠的传输协议,不保证可靠交付数据。
- 传输方式:TCP 是流式传输,没有边界,但保证顺序和可靠;UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
- 分片:TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片;UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
- 应用场景:TCP 常用于文件传输和 HTTP/HTTPS;UDP 用于包总量较少的通信,如 DNS 、SNMP 等或视频、音频等多媒体通信。
# 抓包验证
使用 tcpdump 工具进行抓包,WireShark 工具进行可视化,对三次握手&四次挥手进行抓包验证:


其中,Seq 从 0 开始是计算的相对值,而非真实的值;三次挥手的出现是因为服务端没有数据要发送并且开启了 TCP 延迟确认机制,所以第二和第三次挥手就会合并传输,这样就出现了三次挥手。
# 基于 TCP 的应用层协议
HTTP、HTTPS、SSH、Telnet、SMTP 等