TCP协议

TCP的设计目标

  1. TCP与UDP的重要区别在于:
    TCP充分实现了数据传输时的各种控制功能: 可以在丢包时控制重发, 可以对顺序乱掉的分包进行顺序控制
    TCP是面向连接的协议: 只有在确认对方存在的情况下才会发送数据, 从而控制流量的浪费

  2. TCP的设计目标
    设计的目的是, 保证数据报的可靠性传输. 因此需要考虑很多事情: 比如数据报的破坏, 丢包, 重复, 乱序到达等问题. 针对这些问题, TCP使用检验和(数据破坏), 序列号(乱序), 确认应答(可靠性), 重发控制, 连接管理和窗口移动等机制实现可靠性传输

1. 通过序列号和确认应答确保可靠性

什么叫确认应答
当发送端的数据到达接收端后, 接收端返回一个"已收到"的通知, 这个通知称为ACK.
如果一段时间内, 发送端仍没有接收到应答, 则认为数据报丢失并进行重发. 因此, 即使网络中发生丢包, TCP仍能保证数据最终能到达对端, 实现可靠传输

什么是序号
(1) 序号是按顺序给发送数据的每一个字节打上的编号, 序号会写在TCP首部中.
(2) 接收端查询收到的数据报中TCP首部的序列号和数据长度, 计算出自己下一步需要接受的序号, 并将这个序号作为ACK的一部分发送出去
(3) TCP的数据长度并未写入TCP首部, 而是通过计算得来的: I P 首 部 中 的 数 据 包 长 度 − I P 数 据 包 首 部 的 长 度 − T C P 首 部 的 长 度 IP首部中的数据包长度 - IP数据包首部的长度 - TCP首部的长度 IP首部中的数据包长度−IP数据包首部的长度−TCP首部的长度

2. 重发的超时时间如何确定

重发超时时间之所以不能写死成一个固定值, 是因为不同网络环境中, 数据报在发送端和接收端之间往返的时间可能相差很大, 即使是相同的链路, 不同时段下数据报的往返时间也可能相差较大. 此时:

  • 如果超时时间设置的太小, 会导致反复重发相同的数据报, 而这些数据报在一定时间后是可以被接收端正确接收的, 造成网络流量的浪费
  • 若果设置的很大, 会导致网络通信能力下降

TCP要求无论在何种网络环境下都要提供高性能的通信.因此, 发送方在每次发包时都会计算数据报的往返时间 (RTT:Rounf Trip Time) 及其偏差. 将 RTT 和偏差相加, 重发时间就是比这个和大一点的值.
Unix 和 Windows 中, 超时时间都是0.5秒的倍数. 不过由于最初不知道往返时间, 所以重发时间一般设置为6秒. 数据重发后如果在超时时间内接收不到应答, 则会再次重发该数据报, 且超时时间会以2倍, 4倍的指数函数延长; 此外, 如果重发次数到达一个阈值后还没有收到任何应答, 就会判断为网络或对端主机发生异常, 强制关闭连接并通知通信行为异常.

我们常说的三次握手和四次挥手, 不知要关注客户端和服务端发送的数据包类型, 还要关注发送数据包后出浴的状态

三次握手

我们经常将三次握手,描述成「客户端请求 → 服务端应答 → 客户端应答之应答」。
TCP协议

数据包发送以及状态演进:

  • 在初始时,双端处于 CLOSE 状态,服务端为了提供服务,会主动监听某个端口,进入 LISTEN 状态。
  • 客户端主动发送连接的[SYN]包,之后进入 SYN-SENT 状态,服务端在收到客户端发来的[SYN]包后,回复[SYN,ACK]包,之后进入 SYN-RCVD 状态。
  • 客户端收到服务端发来的[SYN,ACK]包后,可以确认对方存在,此时回复[ACK]包,并进入 ESTABLISHED 状态。
  • 服务端收到最后一个[ACK] 包后,也进入 ESTABLISHED 状态。

三次握手过程中可能发生的异常状态

  1. 客户端第一个[SYN] 包丢了。
    如果客户端发送的第一个[SYN]包丢了,那服务端根本不知道客户端曾经发过包,所以处理流程主要在客户端。而在 TCP 协议中,某端的一组[请求-应答]中,在一定时间范围内,只要没有收到对应的[ACK]包,无论是请求包对方没收到,还是对方的应答包自己没有收到,均认为是丢包了,会触发超时重传机制。
    所以此时客户端重传[SYN]包。根据《TCP/IP详解卷Ⅰ:协议》中的描述,此时会尝试三次,间隔时间分别是 5.8s、24s、48s,三次时间大约是 76s 左右,而大多数伯克利系统将建立一个新连接的最长时间,限制为 75s。
    也就是说三次握手第一个[SYN]包丢了,会重传,总的尝试时间是 75s。

  2. 服务端收到[SYN]后回复的[SYN,ACK]包丢了。
    此时服务端已经收到了数据包并回复,如果这个回复的[SYN,ACK]包丢了,

    • 站在客户端的角度,会认为是最开始的那个[SYN]丢了,那么就继续重传,就是我们前面说的错误 1 的流程。
    • 对服务端而言,如果发送的[SYN,ACK]包丢了,在超时时间内没有收到客户端发来的[ACK]包,也会触发重传,此时服务端处于 SYN_RCVD 状态,这个[SYN,ACK]包的重传次数,不同的操作系统下有不同的配置,例如在 Linux 下可以通过 tcp_synack_retries 进行配置,默认值为 5。如果这个重试次数内,仍未收到[ACK]应答包,那么服务端会自动关闭这个连接。
      同时由于客户端在没有收到[SYN,ACK]时,也会进行重传,当客户端重传的[SYN]被收到后,服务端会立即重新发送[SYN,ACK]包。
  3. 客户端最后一次回复的 [ACK] 包丢了。
    客户端发送完 [ACK] 后, 就进入 ESTABLISHED 状态. 如果这个[ACK] 包丢了,服务端因为收不到[ACK] 会走重传机制. 多数情况下,客户端进入 ESTABLISHED 状态后,则认为连接已建立,会立即发送业务数据。但是服务端因为没有收到最后一个[ACK]包,依然处于 SYN-RCVD 状态, 此时接收到客户端发送来的业务数据包中, 会包含下一次期望的 [ACK] 确认序号,服务端发现这个序号大于对第2步 [SYN,ACK] 包的确认序号, 会认为连接已建立,自动进入 ESTABLISHED 状态。

  4. 客户端恶意不发最后一次[SYN]包。
    如果客户端是恶意的,在发送[SYN]包,并收到服务端回复的 [SYN,ACK] 后就不响应了,那么服务端此时处于一种半连接的状态,虽然服务端会通过 tcp_synack_retries 配置重试的次数,不会无限等待下去,但是这也是有一个时间周期的。如果短时间内存在大量的这种恶意连接,对服务端来说压力就会很大,这就是所谓的 SYN FLOOD 攻击。

四次挥手

我们经常将三次握手,描述成「客户端请求 → 服务端应答 → 服务端请求 → 客户端应答」。
TCP协议

数据包发送以及状态演进:

  • 初始时双端还都处于 ESTABLISHED 状态并传输数据,某端可以主动发起[FIN]包准备断开连接,在这里的场景下,是客户端发起[FIN]请求, 然后进入 FIN-WAIT-1 状态。
  • 服务端收到[FIN]消息后,回复[ACK]表示知道了,并从 ESTABLISHED 状态进入 CLOSED-WAIT 状态,开始做一些断开连接前的准备工作。
  • 客户端收到之前[FIN]的回复[ACK]消息后,进入 FIN-WAIT-2 状态。而当服务端做好断开前的准备工作后,也会发送一个[FIN,ACK] 的消息給客户端,表示我也好了,请求断开连接,并在发送消息后,服务端进入 LAST-ACK 状态。
  • 客户端在收到[FIN,ACK]消息后,会立即回复[ACK],表示知道了,并进入 TIME_WAIT 状态,为了稳定和安全考虑,客户端会在 TIME-WAIT 状态等待 2MSL 的时长,最终进入 CLOSED 状态。
    (1) 为什么 TIME_WAIT 等待的时间是 2MSL
    MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
    (2) MSL 与 TTL 的区别:
    MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
    (3) TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是:
    网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。比如如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
    在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
    其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:
  • 服务端收到客户端回复的[ACK]消息后,直接从 LAST-ACK 状态进入 CLOSED 状态。

TCP 挥手的异常情况

  1. 断开连接的 FIN 包丢了。
    我们前面一直强调过,如果一个包发出去,在一定时间内,只要没有收到对端的[ACK]回复,均认为这个包丢了,会触发超时重传机制。而不会关心到底是自己发的包丢了,还是对方的[ACK]丢了。所以这里, 如果客户端率先发的[FIN]包丢了,或者没有收到对端的[ACK]回复,则会触发超时重传,直到触发重传的次数,直接关闭连接

  2. 服务端第一次回复的 [ACK] 丢了。
    此时因为客户端没有收到[ACK]应答,会尝试重传之前的[FIN]请求,服务端收到后,又会立即再重传[ACK]。因为此时服务端已经进入 CLOSED-WAIT 状态,开始做断开连接前的准备工作。当准备好之后,会回复 [FIN,ACK], 注意这个消息是携带了之前[ACK]的响应序号的。只要这个消息没丢,客户端可以凭借[FIN,ACK]包中的响应序号,直接从 FIN-WAIT-1 状态,进入 TIME-WAIT 状态,开始长达 2MSL 的等待。

  3. 服务端发送的 [FIN,ACK] 丢了。
    服务端在超时后会重传,此时客户端有两种情况,要么处于 FIN-WAIT-2 状态(之前的 [ACK] 也丢了),会一直等待; 要么处于 TIME-WAIT 状态,会等待2MSL 时间。也就是说,在一小段时间内客户端还在,客户端在收到服务端发来的[FIN,ACK]包后,也会回复一个[ACK]应答,并做好自己的状态切换。

  4. 客户端最后回复的 [ACK] 丢了。
    客户端在回复[ACK]后,会进入 TIME-WAIT 状态,开始长达 2MSL 的等待,服务端因为没有收到[ACK]的回复,会重试一段时间,直到服务端重试超时后主动断开。或者等待新的客户端接入后,收到服务端重试的[FIN]消息后,回复[RST]消息,在收到[RST]消息后,复位服务端的状态。

  5. 客户端收到 [ACK] 后,服务端跑路了。
    客户端在收到[ACK]后,进入了 FIN-WAIT-2 状态,等待服务端发来的[FIN]包,而如果服务端跑路了,这个包永远都等不到。
    在 TCP 协议中,是没有对这个状态的处理机制的。但是协议不管,系统来凑,操作系统会接管这个状态,例如在 Linux 下,就可以通过 tcp_fin_timeout 参数,来对这个状态设定一个超时时间。当超过 tcp_fin_timeout 的限制后,状态并不是切换到 TIME_WAIT,而是直接进入 CLOSED 状态。

  6. 客户端收到 [ACK] 后,客户端自己跑路了。
    客户端收到[ACK]后直接跑路,服务端后续在发送的[FIN,ACK]就没有接收端,也就不会得到回复,会不断的走 TCP 的超时重试的机制,此时服务端处于 LAST-ACK 状态。那就要分 2 种情况分析:

    • (1) 在超过一定时间后,服务端主动断开。
    • (2) 收到[RST]后,主动断开连接。
      [RST]消息是一种重置消息,表示当前错误了,应该回到初始的状态。如果客户端跑路后有新的客户端接入,会在此发送[SYN]以期望建立连接,服务端收到这个[SYN]后将被忽略,并直接回复[FIN,ACK]消息,新客户端在收到[FIN]消息后会回复一个[RST]消息。

三次握手与四次挥手的问题

1. 为什么关闭连接时, 服务端需要返回ACK后再发送FIN请求? 而不是像建立连接时一样, 把ACK和SYN一同发送.

因为关闭连接时, 客户端发出FIN时, 只说明客户端没有数据要传输给服务端了, 而服务端可能还有数据发送给客户端. 因此服务端先发送一个ACK应答表示自己受到了客户端的断开连接请求, 在发送完剩余数据后, 服务端也发送FIN请求给客户端请求关闭

2. 建立连接时, SYN包的作用? (数据报大小确认)

建立TCP连接时, 要确定以后数据报发送的大小, 这个大小要是在IP中不会再分片的最大长度. 这主要是考虑到避免数据报到网络层L3时, 由于过大被L3层的设备切片后传输. 由于L3是不可靠传输, 一旦发生切片, 如果某个切片发生丢失, L3层设备会重发所有分片的数据, 造成网络流量浪费. 而L3层分片的大小, 主要是由L2层设备决定的. 比如生活中使用最广泛的以太网(Ethernet, IEEE 802.3)的帧大小是1518字节, 根据Ethernet Frame的定义, L2 Frame由14字节Header和4字节Trailer组成, 所以L3层(也就是 IP 层)最多只能填充1500字节大小,这就是 MTU 的由来.当在MTU=1500的L3网络上传输时, MSS为1460(即1500-20字节IP头-20字节TCP头). 因此, 客户端在发送SYN时, 会在TCP首部中写入自己的MSS(maximum segment size), 告知对方自己的接口能够适应的MSS大小, 服务端返回的SYN中也会包含服务端的MSS大小. 然后再二者中选择一个较小的值投入使用. 因此TCP数据报的大小是以段(segment)为单位的

4. 利用滑动窗口发送segment-提高传输效率

(1) 发送一个应答一个的方式:
    如果发送端1次发送一个段, 等待这个段被接收端应答后再发送第二个段, 这种传输方式的缺点在于, 数据报往返时间越长, 通信性能越低

(2) 滑动窗口的方式:
    发送方主机发送了一个段后, 无需阻塞的等待这个段的应答, 而是继续发送段. 窗口大小: 指无需等待确认应答就可以继续发送段的个数. 等整个窗口的数据发送完毕后, 就要进入等待接收端ACK, 如果其中有部分段出现丢包, 那么发送端仍然要负责重传, 因此, 发送端要设置缓存保留这些待重传的段, 直到收到他们的确认应答
    在收到ACK后, 窗口将滑动到ACK中的序号位置, 这样可以顺序的把多个段同时发送提高通信性能.
TCP协议

(3) 窗口中的数据段出现丢失怎么办?

  • 首先, 考虑ACK应答丢失的情况
    在没有使用滑动窗口时, 未收到ACK应答的数据需要被重传;
    当时用滑动窗口后, 因为接收端只有在正确接收某个段后才会对该段返回ACK, 所以ACK确认的段之前的所有段都是以正确顺序被接收端接收的. 所以, 如果对第n段的ACK丢失, 而第n+1段的ACK正确抵达发送方, 则说明前面n段也已经正确到达接收方而不用重传

  • 然后, 考虑窗口中某个数据段丢失的问题
        由于接收方需要按顺序接收数据段, 一但中间某个序号的数据段丢失, 接收端在接收到后面序号的数据段后, 先保存这个数据段, 然后对该段返回ACK, 但ACK中的"下一个期望序号"写的是丢失的那个数据段的序号. 当发送端收到3个相同的ACK, 则证明"下一个期望序号"的数据段丢失, 会立刻重传. 这种重传机制往往比超时重传更加高效
    TCP协议

5. 流量控制

如果发送端发送数据报的速率, 超过了接收端缓冲区的大小, 则接收端只能抛弃这些发送来的数据, 而这又会出发重传机制, 造成网络流量的大量浪费. 因此, TCP提供了一种由接收方控制发送方发送速率的办法 - 通过接收方控制发送方发送窗口的大小

  • 发送方会每个一段时间发送一个窗口探测包给接收方, 接收方将自己当前缓冲区的大小放在TCP首部中, 然后组成"窗口更新通知"返回给发送方, 发送方根据"窗口更新通知"调整自己的发送窗口大小
    TCP协议

6. 拥塞控制

计算机网络处于一个共享环境下, 网络出现拥堵时, 入股突然发送个较大的数据, 极有可能导致整个网络瘫痪. 因此, TCP通过一个慢启动的算法对发送数据量进行控制:

  • 首先定义一个拥塞窗口, 初始值为1. 每收到一个ACK, 拥塞窗口大小+1. 发送方发送数据时, 取拥塞窗口和接收端主机通知的窗口大小二者中较小的值作为滑动窗口大小.
  • 不过, 随着数据报的往返, 为了提高传输效率, 拥塞窗口也会以1,2,4…的指数函数增长, 这会导致拥塞状况激增. 为了防止这些, TCP定义了拥塞窗口阈值的概念. 因此:
    • 在未发生超时重传时, 拥塞窗口大小每次以2倍的速度递增
    • 当发生超时重传后, 慢启动阈值定义为当前拥塞窗口大小的一半, 且新的拥塞窗口大小改为1. 此后, 若未发生超时重传, 拥塞窗口仍以2倍的速度递增, 递增到慢启动阈值后, 拥塞窗口大小每次+1;
    • 当发生重复应答(3个相同的ACK确认)触发高速重发时, TCP认为这种情况下的拥塞情况比超时重传下的拥塞情况轻许多, 因此, 慢启动阈值改为当前拥塞窗口的一半, 并且将拥塞窗口的大小改为慢启动阈值的大小.
      TCP协议

7. 几个提高网络利用率的规范

  • 延迟ACK应答
    因为TCP采用滑动窗口机制, 因此某几个ACK确认的丢失并不会造成这几个段的重传, 因此可以从接收方哪里减少ACK的发送, 只对当前正确收到的最大数据报序号做应答. 这样做的另一个好处是: 如果收到1个数据报就立刻应答, 由于接收方还没来的急处理数据, 返回的ACK中控制流量的窗口就会很小, 从而降低通信效率. 因此, 接收方往往采用延迟应答的机制 :

    • 收到2个数据报后再返回ACK应答
    • 其他情况下, 延迟0.2秒发送ACK应答
  • Nagle算法
    Nagle算法是一种发送方延迟发送数据报的策略. 它仅当下列2个条件至少有一个满足的情况下才能发送数据报:

    • 已发送的数据报已经全部收到ACK
    • 数据报达到最大可发送长度MSS

    Nagle的本意是提高网络利用率, 因为当数据很小时就被组成数据报发送的话, tcp头部所占的字节比例太高, 降低通信效率; 但如果接收方开启了延迟ACK应答的话, 由于ACK的延迟, 使得数据报小于MSS时, Nagle要等待全部ACK到达后再发送数据报, 使得发送延迟进一步增大. 因此, 对于低延迟的要求, 往往要关闭Nagle(例如:Xwindow).

java.net.Socket.setTcpNoDelay(True)  // java中使用这个API关闭Nagle算法
上一篇:计算机课内知识总结


下一篇:详解HTTP、TCP和UDP之间的区别(一)