ch18: TCP Connection Establishment and Termination

TCP

1. TCP Connection Establishment and Termination

通过一个简单的例子,来看看TCP连接的建立和终止过程中发生了什么。

通过Telnet来发起一个请求,建立连接后就退出。

1.1 tcpdump Output

下面是tcpdump抓包的结果:

每行就是一个单独的TCP segment。

每行开头的格式是:

source > destination: flags

所以第一行的例子是说,svr4的1037上发起一个请求,目的是bsdi的discard进程,flag是S,代表了SYN。

1.1.1 Flags

在tcpdump中,flag的含义如下:

Flag

Abbr

S

SYN

F

FIN

R

RST

P

PSH

.

none of above flour flags is on

不过在TCP header中,一共有六个标志位,除了上面的四个还有ACK和URG,这两个标志位在tcpdump中会以特殊的形式打印出来。

在上面的例子中所有的segment中的flag只有一个,其实还可能会有多个一起的情况。

1.1.2 Sequence Number

第一行中的1415531521:1415531521 (0),表示这个segment的序号是1415531521,冒号后面的数字就是这个segment数据的最后一个字节的序号,括号里的数字表示这个segment包中的数据大小,这里是0,就是说这个segment没有数据。

1.1.3 Acknowledgedment Number

第三行中的ack 1415531522是一个确认号,说明bsdi收到了svr4的第一个segment。

只有ACK打开的时候才会打印这个字段。

1.1.4 Window Size

每一行都有一个win 4096,这个是发送方通知接收方的窗口大小,由于没有额外的数据发送,所以这些窗口大小没有变化。

1.1.5 MSS

第一行和第二行还有,这个是发送方发送给接收方的mss值。

这就是说发送方不希望另一方给它一次发送超过1024个字节的数据。

这个值是为了防止数据分片的。

1.2 Timeline

将上面的结果通过一个时间线的形式来展示出来,能更好地看到两只之间的往来:

1.3 Connection Establishment Protocol

1.3.1 Three-way Handshake

为了建立连接,需要三次握手:

  • 发送方(叫做client)发起连接请求,一个SYN segment,附带对方的IP和端口,还有一个client选取的初始序号(ISN);

  • 接收方(叫做server)接收到client的SYN之后,发送一个SYN,对client的ISN进行ack,然后选择一个自己的ISN,每个SYN都要消耗一个序号;

  • client对server的SYN进行ack,确认号是server的序号加1。

这样连接就建立了。

第一个发送SYN的叫做主动打开(active open),另一个SYN就叫被动打开(passive open)。

也有可能两方都发起一个主动打开。

1.3.2 ISN

ISN每次应该不同,如何选择不同系统有不同的实现。

为什么应该不同呢?

这是为了防止网络中被延迟的segment在以后又被传送,导致连接的一方对它作出错误的解释。

怎么选择ISN呢?

在BSD的实现中,系统启动的时候ISN是1,然后然后每隔0.5秒增加64000,大概每隔9.5小时就会循环一次。

同时,每次连接建立的时候,也会加64000。

这样的方式有一个现象就是,所有的ISN都是一个奇数。

1.4 Connection Termination Protocol

建立连接需要三次握手,而断开连接需要四次。

这是因为TCP是双全工(full-duplex)的,每个方向上都可以传送数据,每个方向都可以单独关闭,叫做half-close。

收到一个FIN意味着对方不会发送数据了,但是仍然可以接收数据。

先发送FIN的叫做主动关闭(active close),先接收到FIN的叫做被动关闭(passive close),不过也有两者同时发起一个主动关闭的情况。

在时间线这个图中:

  • client先发送一个FIN(segment 4),一个FIN也消耗一个序号。这时client不会向server发送数据了,但还可以接收server的数据;

  • server收到这个FIN之后发送一个ack,这时server知道不回接收到client的数据了(除了ack);

  • 如果server仍有数据发送的话,继续发送,然后client收到后继续ack;

  • 当server发送完数据之后,关闭连接,发送一个FIN给client;

  • 最后client对server的FIN进行ack。

连接关闭,流程如下:

大多数情况下都是client主动关闭一个连接,不过server也可以主动关闭连接。

1.4.1 Normal tcpdump Output

为了简洁,可以只在建立连接的时候打印序号与确认号,后序可以使用相对值。比如:

2. Timeout of Connection Establishment

可能可有几种情况导致连接建立不成功。

2.1 The Server is Down

如果服务器down掉了,那么client发送的SYN不会收到ack。

比如:

client进行了两次重试。

第一次间隔5.8秒,第二次间隔24秒。

2.2 First Timeout Period

为啥第一个重试间隔不是6秒呢?

这是因为BSD版的TCP实现使用了一种500ms的定时器,当发起一个建立连接请求时,TCP就会建立一个6秒的定时器。

但是第一个tick不一定刚好对齐,导致第一个tick的时间可能在0到500ms之间:

但是其余的11个tick都是500ms,最后一个到了之后,就对齐了,所以第二次重试就是24秒了。

2.3 TOS: Type of Service

上面的输出还有[tos 0x10],这个是IP datagram中的TOS,type of service,这个0x10的含义就是最小时延。

3. Maximum Segment Size

最大报文长度(MSS)是TCP传往另一端的最大块数据的长度。

建立连接的时候,每一端都可以向对方通知自己的MSS。

MSS不是协商的结果,而是一个可选项,MSS这个可选项只能在SYN中出现。

如果没有收到对方的MSS的话,那么就默认是536。

MSS越大,那么IP header和TCP header的比例就越低,传输效率就越高。

对于发送SYN的机器来说,MSS的值可以是出口接口的MTU减去IP和TCP header的大小。

BSD实现要求MSS是512的整数倍。

不过,只有当两个机器是直接相连的,才能根据两个MSS值选择一个较小的值。

当两个机器需要通过路由器来连接的时候,就不能根据两个MSS的值来选择较小的了,因为这个时候还要考虑Path MTU

4. TCP Half-Close

TCP连接在一端结束发送数据后还可以接收数据,这就是半关闭。

这需要连接的双方都向对方发送一个FIN。

下图是一个场景的半关闭场景:

客户端发起主动关闭,服务器对这个FIN进行ack。

此时服务器还有数据没有发送完,没有关系,客户端还可以接收数据。

服务器数据发送完了就发送一个FIN。

客户端进行ack,连接完全关闭。

如果没有半关闭,那么为了达到关闭发送连接而还能接收数据的话,需要使用两个连接。

5. TCP State Transition Diagram

5.1 Diagram

TCP连接的状态转移图:

  • 一共有11个状态,四个建立连接(CLOSED、LISTEN、SYN_SENT和SYN_RCVD),一个连接建立ESTABLISHED),和六个关闭连接(FIN_WAIT_1、FIN_WAIT_2、CLOSING、TIME_WAIT、CLOSE_WAIT和LAST_ACK);

  • ESTABLISHED状态是两个方向都可以发送数据的状态;

  • 主动关闭一般是client来完成,有四个状态:FIN_WAIT_1、FIN_WAIT_2、CLOSING和TIME_WAIT;

  • 被动关闭一般是server来完成,有两个状态:CLOSE_WAIT、LAST_ACK。

下面是带上状态的时间序列图:

5.2 2MSL Wait State

TIME_WAIT状态也叫2MSL状态,每个实现都应该确定自己的MSL:maximum segment lifetime(报文段最大生存时间)。

这个值是每个segment在被丢弃前在网络中的最大存活时间。

这个值也是有最大值的,因为TCP segment是包装在IP datagram中的,IP datagram有一个TTL(基于跳的而不是时间)。

一般的实现有30秒,1分钟和2分钟。

给定一个MSL,规则就是:当TCP发起一个主动关闭并发送了最后一个ACK之后,这个连接必须在TIME_WAIT状态停留2个MSL时间

这是为了防止如果最后一个ACK丢失后可以再次发送一个ACK,不然另一端就会超时并重发最后的FIN。

另一个影响就是,处于2MSL状态时,这个TCP连接不可以复用

由于一般服务器是被动关闭,所以不会经历这个2MSL状态,但整个socket都将处于2MSL状态。当关闭一个client之后,立即启动一个client,那么这个client不能使用和刚才一样的端口,这对客户端来说没什么问题。

但是对于使用一个固定端口的服务器来说就有问题了。关闭一个服务器程序再重启,可能会失败,因为这个端口还在被一个处于2MSL状态的连接占用。

大概要经过1到4分钟才能重新启动。

不过也可以复用这个socket,加上一个SO_REUSEADDR的标志即可。

5.3 Quiet Time Concept

由于处于2MSL状态的连接不可用,那么超过2MSL状态的那个连接就不会受到前一个连接中延迟的segment的影响了。

不过这需要处于2MSL状态的主机工作正常。

如果处于2MSL状态的主机出现故障,然后在MSL时间内重启,紧接着建立了一个和之前连接一样的连接,会怎么样呢?

这样的话,故障前的segment会被新的连接接收并使用,这会导致问题。

因此,TCP在重启动之后的MSL秒内不能建立任何连接,这就是平静时间(quiet time)。

不过大多数主机的重启时都比MSL时间长,所以不一定要遵守这个规定。

5.4 FIN_WAIT_2状态

在FIN_WAIT_2状态,我们已经发了一个FIN,并且另一端也进行了ack。如果我们发起的不是半关闭,那么我们就期待另一端的应用层意识到连接已关闭,然后发送一个FIN。

只有另一端完成这个关闭,才能从FIN_WAIT_2转移到TIME_WAIT状态。

这意味着我们这一端一直处于FIN_WAIT_2状态,另一端一直处于CLOSE_WAIT状态。

Berkeley实现通过加一个超时时间来避免这个问题。

6. Reset Segments

当一个segment发送到一个TCP产出错误的时候,就会发送一个RST。

6.1 Connection Request to Nonexistent Port

一个常见的例子就是向一个没有程序监听的端口发起一个连接请求。

在UDP中会返回一个端口不可达的ICMP报文,在TCP中,发送一个RST。

返回的RST中,序号的值是0。

6.2 Aborting a Connection

正常关闭一个连接叫做orderly release,还可以异常关闭,叫做abortive release。

异常关闭有两个优点:

  • 可以立即释放所有待发送的数据,然后发送一个RST;

  • 接收RST的一端可以知道另一端是异常关闭,而不是正常关闭。

为了能够异常关闭,API需要提供一个SO_LINGER标志。

6.3 Detecting Half-Open Connections

半打开(Half-open),就是一方已经关闭或异常终止连接但是另一方却不知道。

任何一端的主机异常都可能会导致这种情况发生,只要不在半打开的连接上传输数据,那么处于连接状态的一方就不知道另一方已经出现异常。

当检测到半打开的连接时,TCP会发送一个RST。

可以通过keepalive选项来检测半打开。

7. Simultaneous Open

也有可能连接的两端同时发起一个主动打开请求,这要求两端都使用一个对方知道的端口,比如A使用自己的8888端口连接B的7777端口,同时B使用自己的7777端口连接A的8888端口。这才是一个同时打开(simultaneous open)。

流程如图:

如果不能正确实现同时打开的话,可以会出现一个两个方向上无限地发送SYN和ACK的情况。

8. Simultaneous Close

同样,两个主机也可以同时发起连接关闭(active close)。

状态转移图:

9. TCP Options

TCP header中的可选项:

len包含选项中的所有数据。

NOP是为了header填充到4字节的整数倍。

10. TCP Server Design

TCP服务器设计问题,基本上是来一个连接就开启一个进程或线程进行处理。

这里关注的问题是:

  • 当一个服务器进程接收一个来自客户端的请求时是如何处理端口的?

  • 如果多个连接请求几乎同时到达会发生什么?

10.1 Restricting Foreign IP Address

10.2 Incoming Connection Request Queue

有时候连接请求到了之后但是不能马上响应,比如操作系统正在处理优先级更高的任务。

这个时候对于进来的请求该如何处理呢?

  1. TCP有一个固定长度的队列(一般实现都是5),用来存被TCP接受的请求但是还没有被应用层接受的请求。TCP接受一个请求是将其放入队列,而应用层接受请求是从队列中取出;

  2. 这个队列的最大长度,也叫作积压值(backlog),一般是5;

  3. 当一个连接请求SYN到达后,TCP判断是否接受,如果接受的话就入队列,对其SYN进行ack并发送自己的SYN,这样连接就可以到达ESTABLISHED状态了,只不过这个时候连接还没有被应用层接受,但是client认为连接已经建立,可以发送数据了,这些数据会被放在TCP的缓冲区中;

  4. 如果没有空间了,TCP就不会对新的SYN进行响应,啥也不做,这样client端就会超时然后重发。

Last updated