易栈 · 一盏

塞外秋来,衡阳雁去

运输层:TCP

一、TCP:传输控制协议

TCP提供面向连接的、可靠的字节流服务。

TCP很重要的一个方面是它的确认机制。接收方通过回复确认来表示自己已经收到数据。同样,发送数据后也期望得到对方的确认,根据有没有收到确认来决定是否重发。建立连接和断开连接时,也需要得到对方的确认,又由于TCP是全双工的,两个方向的连接要单独建立和断开,所以就有了三次握手四次挥手。

"Can I tell you a TCP joke?"  "Please tell me a TCP joke."  "OK, I'll tell you a TCP joke."

TCP报文段格式如下:

源端口|目的端口|序号|确认序号|首部长度|标志|窗口大小|校验和|紧急指针|选项部分|数据部分

  • 端口号:一个IP地址和一个端口号也称为一个插口(socket)。一个插口对(socket pair,也就是客户IP地址、客户端口、服务器IP地址和服务器端口号的四元组)可以唯一地标识一个TCP连接。
  • 序号:序号对传输的字节进行计数,每个序号标识数据字节流中的一个字节。序号是32bit无符号数,随着字节流递增,溢出后又从0开始。每个连接的初始序号ISN(Initial Sequence Number)一般是根据时间戳来取值,不同系统的实现有差别。注意由于TCP是全双工的,所以通信的两个方向上的序号是独立的
  • 确认序号:确认序号是本端期望收到的下一个序号(即对方发来的下一个报文段中的序号字段),它的值等于已经成功收到的数据的字节序号加1,作用是告诉对方之前发来的数据已经收到。注意数据报文段和确认报文段不是一一对应的,经常是收到多个数据报文段,才返回一个确认报文段,确认报文段中的确认序号是数据报文段的序号累积的结果。TCP确认序号的缺点是不能对数据流中选定部分进行确认:假如发送方发了3个报文段,分别为#1、#2、#3。可是#2丢失了,接收方只收到#1和#3。这时接收方只能一直发确认序号为#2起始序号的报文段,表示期望收到#2,而无法表达出自己也收到了#3(#3会被接收方缓存下来,不需要重传)。
  • 首部长度:这个长度是必要的,因为选项部分的长度是可变的。该字段占4位,所以首部最长为60字节。没有选项部分时,TCP首部长度为20字节(谨记!这个值在估算数据长度时经常要用到)。注意!TCP没有表示报文总长度的字段,所以TCP数据部分的长度是通过IP数据报长度减去TCP首部长度算出。
  • 标志:有6种标志。
    • URG:紧急指针有效。详见后面紧急指针的解释。
    • ACK:确认序号有效。一旦一个连接建立起来,这个标志总是被设置的,也就是确认序号总是有效。
    • SYN:同步序号。用来发起一个连接。
    • FIN:用于关闭本端的发送连接。由于TCP是全双工的,所以每一端要各自负责关闭自己的发送连接。当两个方向的连接都已经关闭,这个连接才算关闭。
    • PSH:告诉接收方应该尽快将这个报文段交给应用层。不需要应用程显式设置这个标志,而是由协议栈自行决定什么时候需要置位PSH标志(伯克利实现中,一般在发送操作会清空发送缓存时设置)。
    • RST:复位连接。有时候发现出问题,要重置连接,或者快速关闭连接。
注意SYN和FIN虽然是标志位,但是也占一个序号。也就是这两个标志任意一个被置位时,看作数据段中有额外一个字节的数据。
  • 窗口大小:表示还能继续接收的数据长度(根据当前的缓冲区剩余的空间)。用来做流量控制:发送方发现接收方的窗口太小,就会暂停发送,等待窗口扩大。
  • 检验和:覆盖整个TCP报文。
  • 紧急指针:指向紧急数据的最后一个字节。如果报文段的URG位被设置,表示数据中有紧急数据,紧急指针标记字节流中紧急数据结束的位置。
  • 选项部分:最常见的可选字段是最长报文大小,即MSS(Maximum Segment Size)。用来指明本端能够接收的最大长度报文段,这样对端在发数据的时候,会把数据长度控制成小于MSS的长度。这个字段可以防止数据被分片,是提高可靠性的很重要的一个方式。MSS通常在建立连接的报文段(SYN)中交换。
  • 数据:数据部分长度可以为0。不带任何数据的报文段是有意义的,例如建立连接的报文段(SYN)和关闭连接的报文段(FIN)。

二、TCP连接的建立与终止

建立连接的过程,也称为三次握手:

         客户端                     服务器(初始化为LISTEN状态)
   SYN_SENT| ->       SYN J        ->| SYN_RCVD
ESTABLISHED| <-   SYN K, ACK J+1   <-|
           | ->      ACK K+1       ->|ESTABLISHED

  1. 客户端给服务器发送一个SYN报文段(初始序号为J),来向服务器请求建立连接。
  2. 服务器给客户端回复一个ACK确认,表示已经收到SYN;并携带一个SYN,来向客户端请求建立连接。SYN虽然是标志位,但是也占一个序号,客户端SYN的序号为J,所以这里ACK确认序号为J+1。
  3. 客户端回复一个ACK,表示已经收到服务器的SYN。

建立连接时可能出现的问题如下:

  • 连接建立超时:无法建立连接有很多种情况,一种是服务器掉线。这种情况下客户端会持续重传SYN,直到75s。
  • 连接请求的目的端口不存在:也就是当连接到达时,目的端口没有任何进程在监听,这是TCP会返回一个RST复位报文段。(注意UDP是返回ICMP端口不可达差错)
  • 连接请求队列:三次握手是由协议栈自动完成的,连接建立才之后放到队列中,等待应用程序取用。当队列满的时候,TCP不会理会收到的SYN(即不返回任何报文段),任由对方超时重传SYN(队列满是个暂时的状态,重发几次可能就可以建立成功了)。
  • 同时打开:即两端同时彼此执行主动打开。TCP是特别设计为了可以处理同时打开,对于同时打开它只建立一条连接而不是两条连接。过程为双方几乎同时发送SYN,导致一方在发完SYN后,马上接到了对方的SYN。然后双方都返回ACK确认,连接建立。整个过程需要4个报文段

断开连接的过程,也称为四次挥手:

        客户端                    服务器
FIN_WAIT_1| ->       FIN J       ->|CLOSE_WAIT
FIN_WAIT_2| <-      ACK J+1      <-|
 TIME_WAIT| <-       FIN K       <-|LAST_ACK
          | ->      ACK K+1      ->|CLOSED

关闭连接用的FIN报文段。注意TCP是全双工的,所以两个方向连接必须单独关闭。本端发起关闭表示本端不会再发送数据,但是对端还可以继续发送数据,本端也可以继续接收处理数据,这就是TCP的半关闭状态

断开连接时可能出现的问题如下:

  • 2MSL等待状态:TIME_WAIT状态也称为2MSL等待状态。从上图可以看到,在准备发最后一个ACK时,主动关闭方进入TIME_WAIT状态。TIME_WAIT状态需要持续2倍MSL时间,为了(1)保证对方能接收到这个ACK(丢失时对方会重发FIN,然后本端要重传ACK),(2)保证上一个连接的报文段已经在网络中消逝,不会被使用相同插口对新连接收到以至造成错误。MSL即报文段的最大生存时间(Maximum Segment Lifetime),是报文段被丢弃前在网络内的最长时间,实现中常用的值是30秒。这里有一个隐含的问题是:在2MSL等待期间,这个连接对应的四元组是不能被复用的。大多数TCP实现做了更严格的限制,即插口使用的本地端口也不能使用(设置SO_REUSEADDR选项可以避开这个限制)。
  • 平静时间:上面讲到2MSL时间可以防止新连接接收到旧连接的迟到报文,但是如果处于2MSL等待状态的主机意外重启,并立即使用之前的插口对建立一个新连接,这时收到旧连接的报文就有可能会出错。所以TCP在系统启动后MSL秒内不应允许建立任何连接,这就是平静时间。大多数系统不遵循这个原则,因为主机的启动时间一般比MSL长。
  • 异常终止:通过RST复位报文而不是FIN来终止连接,称为异常终止。收到RST报文的一方会马上终止连接,不返回任何响应。
  • 半打开连接:如果一方异常退出了另一方还不知道,这种TCP连接为半打开连接。为了防止半打开连接一直占用资源,大部分实现提供保活定时器,定时监测对端的存活状态(这个在后面会讲到)。
  • 同时关闭:参见前面介绍的同时打开,同时关闭也需要4个报文段

本文链接:易栈 - 运输层:TCP