TCP协议
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。其主要特点包括:
- 面向连接。在传输数据前必须先建立连接,数据传输结束后释放连接。
- 可靠交付。TCP通过序号、确认、校验、重传、流量控制等机制确保数据正确可靠地传输。
- 字节流服务。TCP把应用层传下来的报文看成是无结构的字节流。
- 拥塞控制。TCP使用滑动窗口、慢开始、拥塞避免等算法控制网络拥塞。
- 全双工通信。TCP允许通信双方的应用程序在任何时候都能发送数据。
TCP工作在IP协议之上,为上层应用程序提供可靠的网络数据传输服务。应用层常见的协议如HTTP、SMTP等都需要依赖TCP协议进行数据交换。
TCP是互联网的基础,也是最重要、最核心的网络传输层协议之一。其可靠、稳定、高效的传输服务对互联网的发展起到了至关重要的作用。
TCP协议报文格式
TCP报文由报头和数据两部分组成。
TCP报头主要包含以下字段:
- 源端口(16比特)和目标端口(16比特):标识发送端和接收端的应用程序。
- 序号(32比特):该报文中第一个字节的序列号。
- 确认号(32比特):期望收到的下一个报文的序列号。
- 头长度(4比特):TCP报头长度,以32位字为单位,最小20字节。
- 标志位(12比特):SYN/ACK等控制位。
- 窗口大小(16比特):指定接收端缓冲区剩余大小。
- 校验和(16比特):检验报文损坏情况。
- 紧急指针(16比特):指示紧急数据的偏移量。
- 选项(0或40字节):最大40字节,设置MSS等参数。
通过这些关键字段,TCP报头在流量控制、拥塞控制、重传控制等方面发挥着重要作用。报头后是实际传输的数据。
三次握手
三次握手的过程
为了确保客户端和服务端都能正常发送和接收数据。三次握手是从客户端开始。
握手前状态:客户端和服务端都是处于连接关闭状态。服务端首先处于 listen 状态,等待客户端连接,然后就进入三次握手。
1. 客户端向服务端发送一个SYN(synchronize)包,随后客户端进入SYN-SENT 阶段。
- 标志位为 SYN:表示请求建立连接;
- 序列号为 Seq = x(x 一般为随机数);
2. 服务端收到客户端发送SYN包后,对该包进行确认后结束 LISTEN 阶段,并返回一段 TCP 报文,随后服务器端进入 SYN-RECV(同步接收) 阶段。
- 标志位为 SYN 和 ACK:表示确认客户端的报文 Seq 序号有效,服务器能正常接收客户端发送的数据,并同意创建新连接;
- 序号为 Seq = y,将自己的初始序列号同步给客户端。
- 确认号为 Ack = x + 1,表示收到客户端的序号 Seq 并将其值加 1 作为自己确认号 Ack 的值,告诉客户端自己接收的Seq没错;
3. 客户端接收到发送的 SYN + ACK 包后,明确了从客户端到服务器的数据传输是正常的,从而结束 SYN-SENT 阶段。并返回最后一段报文,随后客户端进入 ESTABLISHED状态。
- 标志位为 ACK,表示确认收到服务器端同意连接的信号;
- 序号为 Seq = x + 1,表示收到服务器端的确认号 Ack,并将其值作为自己的序号值;
- 确认号为 Ack= y + 1,表示收到服务器端序号 seq,并将其值加 1 作为自己的确认号 Ack 的值。
4. 当服务器端收到来自客户端确认收到服务器数据的报文后,得知从服务器到客户端的数据传输是正常的,从而结束 SYN-RECV 阶段,进入 ESTABLISHED 阶段,从而完成三次握手。
为什么需要三次握手
确认客户端和服务端都可以正常发送接收数据。
- 第一次握手:确认客户端可以正常发送数据。
- 第二次握手:确认客户端可以正常发送数据,确认服务端可以正常接收数据。
- 第三次握手:确认客户端可以正常发送数据,确认服务端可以正常接收数据,确认服务端可以正常发送数据,服务可以正常接收数据。
握手失败
- 第一次握手失败
- 客户端
- 客户端认为发送成功,进入SYNC-SENT状态,等待第二次握手
- 客户端一段时间内无法收到第二次握手,会认为第一次握手丢失,触发超时重传机制,重新发送SYN报文
- 如果收到第二次握手,则进行下面的操作
- 如果握手还是丢失,继续重新发送报文,发送5次后(可自定义),停止发送
- 服务器
- 服务器没有任何感知,一直处于LISTEN状态
- 客户端
- 第二次握手失败
- 客户端
- 同第一次握手相同,采用超时重传机制
- 服务器
- 服务器收到第一次握手后进入SYN_RCVD状态
- 一段时间内无法收到第三次握手,触发超时重传机制,重新发送第二次握手请求
- 重试5次后,停止发送
- 客户端
- 第三次握手失败
- 客户端
- 客户端处于ESTABLISHED状态,客户端无感知,等待服务端发送数据
- 服务器
- 服务端触发超时重传机制,再次发送第二次握手请求,直到连接成功或达到最大重传批次
- 客户端
四次挥手
四次挥手的过程
为了告知客户端和服务端将未发送的数据发送完毕,并且关闭连接,四次挥手是从客户端开始的。
挥手前状态:客户端和服务端都处于连接状态(ESTAB-LISHEN),进入四次挥手。
1. 首先客户端向服务器发送一段 TCP 报文表明其想要释放 TCP 连接,随后客户端进入 FIN-WAIT-1 阶段,即半关闭阶段,并且停止向服务端发送通信数据。
- 标记位为 FIN,表示请求释放连接;
- 序号为 Seq = u;
2. 服务器接收到客户端请求断开连接的 FIN 报文后,结束 ESTABLISHED 阶段,进入 CLOSE-WAIT 阶段并返回一段 TCP 报文,随后服务器开始准备释放服务器端到客户端方向上的连接。客户端收到服务器发送过来的 TCP 报文后,确认服务器已经收到了客户端连接释放的请求,随后客户端结束 FIN-WAIT-1 阶段,进入 FIN-WAIT-2 阶段
- 标记位为 ACK,表示接收到客户端释放连接的请求;
- 序号为 Seq = v;
- 确认号为 Ack = u + 1,表示是在收到客户端报文的基础上,将其序号值加 1 作为本段报文确认号 Ack 的值。
3. 服务器端在发出 ACK 确认报文后,服务器端会将遗留的待传数据传送给客户端,待传输完成后即经过 CLOSE-WAIT 阶段,便做好了释放服务器端到客户端的连接准备,再次向客户端发出一段 TCP 报文,随后服务器端结束 CLOSE-WAIT 阶段,进入 LAST-ACK 阶段。并且停止向客户端发送数据。
- 标记位为 FIN 和 ACK,表示已经准备好释放连接了;
- 序号为 Seq = w;
- 确认号 Ack = u + 1,表示是在收到客户端报文的基础上,将其序号 Seq 的值加 1 作为本段报文确认号 Ack 的值。
4. 客户端收到从服务器发来的 TCP 报文,确认了服务器已经做好释放连接的准备,于是结束 FIN-WAIT-2 阶段,进入 TIME-WAIT 阶段,并向服务器发送一段报文。
- 标记位为 ACK,表示接收到服务器准备好释放连接的信号;
- 序号为 Seq= u + 1,表示是在已收到服务器报文的基础上,将其确认号 Ack 值作为本段序号的值;
- 确认号为 Ack= w + 1,表示是在收到了服务器报文的基础上,将其序号 Seq 的值作为本段报文确认号的值。
5. 随后客户端开始在 TIME-WAIT 阶段等待 2 MSL。服务器端收到从客户端发出的 TCP 报文之后结束 LAST-ACK 阶段,进入 CLOSED 阶段。由此正式确认关闭服务器端到客户端方向上的连接。客户端等待完 2 MSL 之后,结束 TIME-WAIT 阶段,进入 CLOSED 阶段,由此完成「四次挥手」。
挥手失败
- 第一次挥手失败
- 客户端
- 第一次挥手失败,客户端触发超时重传机制,再次发送请求,直到发送成功或达到上限,如果发送次数达到上限仍然失败,直接关闭连接
- 服务端
- 无任何感知
- 客户端
- 第二次挥手失败
- 客户端
- 同第一次挥手失败,客户端感知是相同的
- 服务端
- 无任何感知
- 客户端
- 第三次挥手失败
- 客户端
- 客户端处于FIN-WAIT2状态一段时间后,关闭连接
- 服务端
- 服务端处于LAST-ACK一段时间,时间过了,断开连接
- 客户端
- 第四次挥手失败
- 客户端
- 无感知
- 服务端
- 同第三次挥手失败
- 客户端
四次挥手的问题
- 可以看到,如果四次挥手的过程中出现问题,客户端和服务端都可能在一段时间内处于半连接状态,此时无法进行开启新的连接。如果大量连接都处于这种状态,将会浪费大量连接资源。可以根据具体情况,修改系统内核参数,减少处于这种状态的时间。
- 四次挥手后,客户端要等待2MSL(Maximum Segment Lifetime,指一段 TCP 报文在传输过程中的最大生命周期)时间,因为要确认服务端收到了第四次挥手,如果服务端没有收到第四次挥手,就会重新发送第三次挥手,此时客户端再次发送第四次挥手,等待2MSL时间,如果2MSL 时间没收到,则认为服务端收到了请求。
流量控制
滑动窗口
滑动窗口是TCP流量控制的核心机制,用于动态调节发送方的数据发送速率,以匹配网络拥塞程度和接收方的处理能力。
滑动窗口的工作原理是:
- 发送方和接收方在连接建立时协商确定一个最大窗口大小(rwnd),即接收方可以接受的数据量。
- 接收方会给发送方返回一个窗口值(cwnd),表示当前可以发送的数据量。这就是"滑动窗口"。
- 发送方可以发送 cwnd 指定的字节数到网络中,等待确认。收到确认后,会相应地向右滑动窗口,再发送新的数据。
- 如果网络拥塞或接收方处理能力跟不上,cwnd 会变小,发送方就会减慢发送速率。如果网络空闲,cwnd 会增大,发送方可以增大发送速率。
- 窗口大小变化时,需要遵守缓慢启动和拥塞避免算法,逐渐才改变窗口大小。
避免死锁:
如果B向A发送了0窗口的报文段后,B的缓存区有了闲置空间后,B向A发送了rwnd = 400的报文段,但是这个报文段在传输的过程丢失了。A一直等不到B发送的非0窗口通知,B也在等待A发送数据。这会造成互相等待的死锁局面。 为了解决这个问题,TCP它为每一个连接都设有一个持续计时器。这个计时器什么时候会启动呢?在TCP连接的某一方收到对方的0窗口通知,就会启动持续计时器,若时间到期了,就会发送一个0窗口探测报文段(包含有1字节的数据),对面会在确认这个探测报文段的时候给出现在的窗口值,如果窗口依然是0,那么收到这个报文段的一方就会重新设置持续计时器。如果不是0,那么就可以开始传输数据了。
综上,滑动窗口机制通过动态调整 cwnd 大小来控制发送方的发送速率,从而实现TCP的流量控制。它既可以避免网络拥塞,又可以充分利用网络资源。
拥塞控制
慢启动
TCP慢启动(Slow Start)是TCP拥塞控制算法的一部分,用于控制发送端的数据发送速率,以避免网络拥塞。
慢启动的主要特征有:
- 拥塞窗口cwnd的初始值通常设置为1个最大报文段(MSS)。
- 每当收到确认ACK时,cwnd加1,即每轮都将cwnd加倍。这实现了指数增长。
- 当cwnd达到慢启动阈值ssthresh时,模式切换为拥塞避免算法。
- 如果发生超时,则cwnd重置为1,重新进入慢启动阶段。
- 如果发生丢包,则cwnd减半,进入快速恢复。
综上,慢启动通过指数增长cwnd来快速探测网络带宽,但有拥塞风险。当达到阈值后,会切换到线性增长,网络稳定后再慢慢探测。慢启动算法既能快速启动,又能防止网络拥塞。
快重传
TCP快重传(Fast Retransmit)是TCP拥塞控制中的一种机制,用于更快地恢复丢失的数据包。其基本原理是:
- 接收方收到一个失序的报文段时,它会发送一个重复确认ACK(dup ACK),ACK号码是期待收到的下一个报文段的序号。
- 如果发送方收到三个重复确认(即三个dup ACK),可以确认有一个报文段在网络中丢失了。
- 此时发送方不等待重传超时,直接重传丢失的报文段。
- 发送方每收到一个dup ACK,就把拥塞窗口cwnd减小一半。
- 收到重传的报文段后,接收方就可以把所有报文段顺序交付应用层,并发送ACK。
- 发送方收到ACK后,可以退出快重传,恢复到拥塞避免模式。
综上,快重传既避免了不必要的重传超时,又快速恢复了传输,改善了传输效率,是TCP拥塞控制的重要组成部分。
快恢复
TCP快恢复(Fast Recovery)通常与快重传配合使用,其目的是在发生丢包后快速恢复TCP的拥塞窗口cwnd。其基本原理是:
- 发生丢包,接受端发送重复ACK,触发发送端的快重传。
- 发送端重传丢失报文段的同时,进入快恢复阶段。将cwnd减半并设置为慢启动阈值ssthresh。
- 此后,每收到一个重复ACK,cwnd加1,即线性增长cwnd。
- 当收到 ACK 确认数据传送结束,转入拥塞避免算法。
- 如果再次超时,则完全重置 cwnd,重新进入慢启动。
综上,快恢复通过线性增长拥塞窗口,避免了慢启动的指数增长带来的网络拥塞风险,既实现了快速恢复,又保证了网络稳定性。它与快重传配合使用,组成了TCP拥塞控制的快速算法。
拥塞避免
TCP拥塞避免(Congestion Avoidance)是TCP拥塞控制中的一种算法,用于长期平稳地提高网络吞吐量,避免网络拥塞。其基本原理是:
- 当慢启动阶段结束后,如果没有网络拥塞发生,则进入拥塞避免阶段。
- 设置拥塞窗口cwnd的慢启动阈值ssthresh。此时cwnd恰好等于ssthresh。
- 每当接收到一个ACK时, cwnd加1/cwnd,即每轮增大一个MSS的cwnd值。
- 这将使cwnd按线性增长,避免拥塞窗口的猛增。
- 如果发生超时,则cwnd设为1,重新进入慢启动阶段。
- 如果只是丢包,则cwnd减半,进入快速重传和恢复。
综上,拥塞避免算法通过线性增长拥塞窗口,保持了TCP流量的平稳增长,最大限度地提升网络吞吐量,又避免了网络拥塞,是TCP最重要的拥塞控制算法之一。
重传控制
TCP的重传控制主要通过设置重传超时(RTO)来实现可靠的数据传输。其基本机制是:
-
发送端设置重传计时器,等待接收端的确认ACK。
-
如果在RTO时间内没收到ACK,则重传该报文段。
-
计算RTO的方法:
- 每段报文都记录发送时间
- 接收端确认时带回送达时间
- 计算往返时间RTT=(接收时间-发送时间)
- 计算平均RTT和平滑偏差,设定RTO稍大于平均RTT
-
指数补偿回退机制
每次重传时,将RTO乘以2,直到达到阈值,避免网络拥塞。
-
Karn算法
只对未重传的报文计算RTT,避免被重传报文影响RTT计算。
综上,TCP通过动态计算RTO和设定重传计时器来实现可靠重传控制,是TCP可靠传输的关键机制。
TCP粘包
-
为什么会发生TCP粘包和拆包?
- 发送方写入的数据大于套接字缓冲区的大小,此时将发生拆包。
- 发送方写入的数据小于套接字缓冲区大小,由于 TCP 默认使用 Nagle 算法,只有当收到一个确认后,才将分组发送给对端,当发送方收集了多个较小的分组,就会一起发送给对端,这将会发生粘包。
- 进行 MSS (最大报文长度)大小的 TCP 分段,当 TCP 报文的数据部分大于 MSS 的时候将发生拆包。
- 发送方发送的数据太快,接收方处理数据的速度赶不上发送端的速度,将发生粘包。
-
常见解决方法
- 在消息的头部添加消息长度字段,服务端获取消息头的时候解析消息长度,然后向后读取相应长度的内容。
- 固定消息数据的长度,服务端每次读取既定长度的内容作为一条完整消息,当消息不够长时,空位补上固定字符。但是该方法会浪费网络资源。
- 设置消息边界,也可以理解为分隔符,服务端从数据流中按消息边界分离出消息内容,一般使用换行符。
-
什么时候需要处理粘包问题?
- 当接收端同时收到多个分组,并且这些分组之间毫无关系时,需要处理粘包;而当多个分组属于同一数据的不同部分时,并不需要处理粘包问题