TCP, Transmission Control Protocol - tenji/ks GitHub Wiki

TCP协议解析

一、Terminologies

二、TCP/IP

TCP/IP 意味着 TCP 和 IP 在一起协同工作。

TCP 负责应用软件(比如你的浏览器)和网络软件之间的通信。

IP 负责计算机之间的通信。

TCP 负责将数据分割并装入 IP 包,然后在它们到达的时候重新组合它们。

IP 负责将包发送至接受者。

三、TCP报文格式

  • 源端口(16位):源端口中包含初始化通信的端口。源端口和源 IP 地址的作用是标识报文的返回地址。
  • 目的端口(16位):目的端口域定义传输的目的。这个端口指明报文接收计算机上的应用程序地址接口。
  • 保留(6位):这些位必须是 0。为了将来定义新的用途而保留。
  • 标志(6位):标志域。表示为:紧急标志、有意义的应答标志、推、重置连接标志、同步序列号标志、完成发送数据标志。按照顺序排列是:URG、ACK、PSH、RST、SYN、FIN。
  • 窗口大小(16位):用来表示想收到的每个 TCP 数据段的大小。TCP 的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个 16 字节字段,因而窗口大小最大为 65535 字节。
  • 校验和(16位):源机器基于数据内容计算一个数值,收信息机要与源机器数值结果完全一样,从而证明数据的有效性。检验和覆盖了整个的 TCP 报文段:这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证的。
  • 紧急指针(16位):指向后面是优先数据的字节,在URG标志设置了时才有效。如果URG标志没有被设置,紧急域作为填充。加快处理标示为紧急的数据段。
  • 选项:长度不定,但长度必须为1个字节。如果没有选项就表示这个1字节的域等于0。
  • 数据:该TCP协议包负载的数据。

在上述字段中,6 位标志域的各个选项功能如下:

  • URG:紧急标志。紧急标志为 "1" 表明该位有效。
  • ACK:确认标志。表明确认编号栏有效。大多数情况下该标志位是置位的。TCP 报头内的确认编号栏内包含的确认编号(w+1)为下一个预期的序列编号,同时提示远端系统已经成功接收所有数据。
  • PSH:推标志。该标志置位时,接收端不将该数据进行队列处理,而是尽可能快地将数据转由应用处理。在处理 Telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。
  • RST:复位标志。用于复位相应的 TCP 连接。
  • SYN:同步标志。表明同步序列编号栏有效。该标志仅在三次握手建立 TCP 连接时有效。它提示 TCP 连接的服务端检查序列编号,该序列编号为 TCP 连接初始端(一般是客户端)的初始序列编号。在这里,可以把 TCP 序列编号看作是一个范围从 0 到 4,294,967,295 的 32 位计数器。通过 TCP 连接交换的数据中每一个字节都经过序列编号。在 TCP 报头中的序列编号栏包括了 TCP 分段中第一个字节的序列编号。
  • FIN:结束标志。

四、TCP 状态机

五、TCP 三次握手

所谓三次握手(Three-Way Handshake)即建立 TCP 连接,就是指建立一个 TCP 连接时,需要客户端和服务端总共发送 3 个包以确认连接的建立。在 socket 编程中,这一过程由客户端执行 connect 来触发,整个流程如下图所示:

  1. 第一次握手:Client 将标志位 SYN 置为 1,随机产生一个值 seq = J,并将该数据包发送给 Server,Client 进入 SYN_SENT 状态,等待 Server 确认;
  2. 第二次握手:Server 收到数据包后由标志位 SYN = 1 知道 Client 请求建立连接,Server 将标志位 SYN 和 ACK 都置为 1,ack = J + 1,随机产生一个值 seq = K,并将该数据包发送给 Client 以确认连接请求,Server 进入 SYN_RCVD 状态;
  3. 第三次握手:Client 收到确认后,检查 ack 是否为 J + 1,ACK 是否为 1,如果正确则将标志位 ACK 置为 1,ack = K + 1,并将该数据包发送给 Server,Server 检查 ack 是否为 K + 1,ACK 是否为 1,如果正确则连接建立成功,Client 和 Server 进入 ESTABLISHED 状态,完成三次握手,随后 Client 与 Server 之间可以开始传输数据了。

简单来说,就是:

  1. 建立连接时,客户端发送 SYN 包(SYN = i)到服务器,并进入到 SYN-SEND 状态,等待服务器确认;
  2. 服务器收到 SYN 包,必须确认客户的 SYN(ack = i + 1),同时自己也发送一个 SYN 包(SYN = k),即 SYN + ACK 包,此时服务器进入 SYN-RECV 状态;
  3. 客户端收到服务器的 SYN + ACK 包,向服务器发送确认报 ACK(ack = k + 1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手,客户端与服务器开始传送数据。

5.1 SYN 攻击

在三次握手过程中,Server 发送 SYN-ACK 之后,收到 Client 的 ACK 之前的 TCP 连接称为半连接(half-open connect),此时 Server 处于 SYN_RCVD 状态,当收到 ACK 后,Server 转入 ESTABLISHED 状态。SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向 Server 不断地发送 SYN 包,Server 回复确认包,并等待 Client 的确认,由于源地址是不存在的,因此,Server 需要不断重发直至超时,这些伪造的 SYN 包将产时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN 攻击时一种典型的 DDOS 攻击,检测 SYN 攻击的方式非常简单,即当 Server 上有大量半连接状态且源 IP 地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:

netstat -nap | grep SYN_RECV

六、TCP 四次握手

所谓四次挥手(Four-Way Wavehand)即终止 TCP 连接,就是指断开一个 TCP 连接时,需要客户端和服务端总共发送 4 个包以确认连接的断开。在 socket 编程中,这一过程由客户端或服务端任一方执行 close 来触发,整个流程如下图所示:

由于 TCP 连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个 FIN 来终止这一方向的连接,收到一个 FIN 只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个 TCP 连接上仍然能够发送数据,直到这一方向也发送了 FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。

  1. 第一次挥手:Client 发送一个 FIN,用来关闭 Client 到 Server 的数据传送,Client 进入 FIN_WAIT_1 状态。
  2. 第二次挥手:Server 收到 FIN 后,发送一个 ACK 给 Client,确认序号为收到序号 +1(与 SYN 相同,一个 FIN 占用一个序号),Server 进入 CLOSE_WAIT 状态。
  3. 第三次挥手:Server 发送一个 FIN,用来关闭 Server 到 Client 的数据传送,Server 进入 LAST_ACK 状态。
  4. 第四次挥手:Client 收到 FIN 后,Client 进入 TIME_WAIT 状态,接着发送一个 ACK 给 Server,确认序号为收到序号 +1,Server 进入 CLOSED 状态,完成四次挥手。

6.1 出现大量 CLOSE_WAIT 的原因

不要被图中的 client 和 server 所迷惑,你只要记住:主动关闭的一方发出 FIN 包,被动关闭的一方响应 ACK 包,此时,被动关闭的一方就进入了 CLOSE_WAIT 状态。如果一切正常,稍后被动关闭的一方也会发出 FIN 包,然后迁移到 LAST_ACK 状态。

通常,CLOSE_WAIT 状态在服务器停留时间很短,如果你发现大量的 CLOSE_WAIT 状态,那么就意味着被动关闭的一方没有及时发出 FIN 包,一般有如下几种可能:

  • 程序问题:如果代码层面忘记了 close 相应的 socket 连接,那么自然不会发出 FIN 包,从而导致 CLOSE_WAIT 累积;或者代码不严谨,出现死循环之类的问题,导致即便后面写了 close 也永远执行不到。
  • 响应太慢或者超时设置过小:如果连接双方不和谐,一方不耐烦直接 timeout,另一方却还在忙于耗时逻辑,就会导致 close 被延后。响应太慢是首要问题,不过换个角度看,也可能是 timeout 设置过小。
  • BACKLOG 太大:此处的 backlog 不是 syn backlog,而是 accept 的 backlog,如果 backlog 太大的话,设想突然遭遇大访问量的话,即便响应速度不慢,也可能出现来不及消费的情况,导致多余的请求还在队列里就被对方关闭了。

如果被动关闭方(服务端)是我们自己写的一些程序,比如用 HttpClient 自定义的蜘蛛,那么八九不离十是程序问题,如果是一些使用广泛的程序,比如 Tomcat 之类的,那么更可能是响应速度太慢或者 timeout 设置太小或者 BACKLOG 设置过大导致的故障。

6.2 为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

这是因为服务端在 LISTEN 状态下,收到建立连接请求的 SYN 报文后,把 ACK 和 SYN 放在一个报文里发送给客户端。而关闭连接时,当收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即 close,也可以发送一些数据给对方后,再发送 FIN 报文给对方来表示同意现在关闭连接,因此,己方 ACK 和 FIN 一般都会分开发送

6.3 为什么 TIME_WAIT 状态需要经过 2MSL (Maximum Segment Lifetime) 才能返回到 CLOSE 状态?

RFC793 定义了 MSL 为 2 分钟,Linux 设置成了 30s

原因有二:

  1. 保证 TCP 协议的全双工连接能够可靠关闭
  2. 保证这次连接的重复数据段从网络中消失

先说第一点,TIME_WAIT 确保有足够的时间让对端收到了 ACK,如果被动关闭的那方没有收到 Ack,就会触发被动端重发 Fin,一来一去正好2个 MSL。如果 Client 直接 CLOSED 了,那么由于 IP 协议的不可靠性或者是其它网络原因,导致 Server 没有收到 Client 最后回复的 ACK。那么 Server 就会在超时之后继续发送 FIN,此时由于 Client 已经 CLOSED 了,就找不到与重发的 FIN 对应的连接,最后 Server 就会收到 RST 而不是 ACK,Server 就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致 TCP 协议不符合可靠连接的要求。所以,Client 不是直接进入 CLOSED,而是要保持 TIME_WAIT,当再次收到 FIN 的时候,能够保证对方收到 ACK,最后正确的关闭连接。

再说第二点,你要知道,有些自做主张的路由器会缓存 IP 数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起。如果 Client 直接 CLOSED,然后又再向 Server 发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达 Server,由于新连接和老连接的端口号是一样的,又因为 TCP 协议判断不同连接的依据是 socket pair,于是,TCP 协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以 TCP 连接还要在 TIME_WAIT 状态等待 2 倍 MSL,这样可以保证本次连接的所有数据都从网络中消失。

七、TCP 重传机制

TCP 要保证所有的数据包都可以到达,所以,必需要有重传机制。

注意,接收端给发送端的 Ack 确认只会确认最后一个连续的包,比如,发送端发了 1, 2, 3, 4, 5 一共五份数据,接收端收到了 1, 2,于是回 ack 3,然后收到了 4(注意此时 3 没收到),此时的 TCP 会怎么办?我们要知道,因为正如前面所说的,SeqNum 和 Ack 是以字节数为单位,所以 ack 的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

7.1 超时重传机制

一种是不回 ack,一直等待 3,当发送方发现收不到 3 的 ack 超时后,会重传 3。一旦接收方收到 3 后,ack 回传 4 - 意味着 3 和 4 都收到了。

但是,这种方式会有比较严重的问题,那就是因为要死等 3,所以会导致 4 和 5 即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到 Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致 4 和 5 的重传。

对此有两种选择:

  • 一种是仅重传 timeout 的包。也就是第 3 份数据。
  • 另一种是重传 timeout 后所有的数据,也就是第 3, 4, 5 这三份数据。

这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等 timeout,timeout 可能会很长(在下篇会说 TCP 是怎么动态地计算出 timeout 的)

7.2 快速重传机制

于是,TCP 引入了一种叫 Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就 ack 最后那个可能被丢了的包,如果发送方连续收到 3 次相同的 ack,就重传。Fast Retransmit 的好处是不用等 timeout 了再重传。

比如:如果发送方发出了 1,2,3,4,5 份数据,第一份先到送了,于是就 ack 回 2,结果 2 因为某些原因没收到,3 到达了,于是还是 ack 回 2, 后面的 4 和 5 都到了,但是还是 ack 回 2,因为 2 还是没有收到,于是发送端收到了三个 ack = 2 的确认,知道了 2 还没有到,于是就马上重转 2。然后,接收 端收到了 2,此时因为 3,4,5都收到了,于是 ack 回 6。示意图如下:

Fast Retransmit 只解决了一个问题,就是 timeout 的问题,它依然面临一个艰难的选择,就是重转之前的一个还是重装所有的问题。 对于上面的示例来说,是重传 #2 呢还是重传 #2,#3,#4,#5 呢?因为发送端并不清楚这连续的 3 个 ack(2) 是谁传回来的?也许发送端发了 20 份数 据,是 #6,#10,#20 传来的呢。这样,发送端很有可能要重传从 2 到 20 的这堆数据(这就是某些 TCP 的实际的实现)。可见,这是一把双刃剑。

7.3 SACK 方法

另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看 RFC 2018),这种方式需要在 TCP 头里加一个 SACK 的东西,ACK 还是 Fast Retransmit 的 ACK,SACK 则是汇报收到的数据碎版。参看下图:

八、TPC 滑动窗口

需要说明一下,如果你不了解 TCP 的滑动窗口这个事,你等于不了解 TCP 协议。我们都知道,TCP 必需要解决的可靠传输以及包乱序(reordering)的问题,所以,TCP 必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。

所以,TCP 引入了一些技术和设计来做网络流控,Sliding Window 是其中一个技术。 前面我们说过,**TCP 头里有一个字段叫 Window,又叫 Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。**为了说明滑动窗口,我们需要先看一下 TCP 缓冲区的一些数据结构:

上图中,我们可以看到:

  • 接收端 LastByteRead 指向了 TCP 缓冲区中读到的位置,NextByteExpected 指向的地方是收到的连续包的最后一个位置,LastByteRcved 指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。
  • 发送端的 LastByteAcked 指向了被接收端 Ack 过的位置(表示成功发送确认),LastByteSent 表示发出去了,但还没有收到成功确认的 Ack,LastByteWritten 指向的是上层应用正在写的地方。

于是:

  • 接收端在给发送端回ACK中会汇报自己的 AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
  • 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。

上图,我们可以看到一个处理缓慢的 Server(接收端)是怎么把 Client(发送端)的 TCP Sliding Window 给降成 0 的。此时,你一定会问,如果 Window 变成 0 了,TCP 会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想 像成 Window Closed,那你一定还会问,如果发送端不发数据了,接收方一会儿 Window size 可用了,怎么通知发送端呢?

解决这个问题,TCP 使用了 Zero Window Probe 技术,缩写为 ZWP,也就是说,发送端在窗口变成 0 后,会发 ZWP 的包给接收方,让接收方来 ack 他的 Window 尺寸,一般这个值会设置成 3 次,第次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后还是 0 的话,有的 TCP 实现就会发 RST 把链接断了。

注意:只要有等待的地方都可能出现 DDoS攻 击,Zero Window 也不例外,一些攻击者会在和 HTTP 建好链发完 GET 请求后,就把 Window 设置为 0,然后服务端就只能等待进行 ZWP,于是攻击者会并发 大量的这样的请求,把服务器端的资源耗尽。(关于这方面的攻击,大家可以移步看一下 Wikipedia 的 SockStress 词条)

另外,Wireshark 中,你可以使用 tcp.analysis.zero_window 来过滤包,然后使用右键菜单里的 follow TCP stream,你可以看到 ZeroWindowProbe 及 ZeroWindowProbeAck 的包。

8.1 Zero Window

8.2 Silly Window Syndrome

九、TCP 拥塞处理

参考链接