---
description: TCP
---

# ch18: TCP Connection Establishment and Termination



![18.TCP Connection Establishment and Termination](https://tva1.sinaimg.cn/large/007S8ZIlly1ggyn31un2ij31e50u0k58.jpg)

## 1. TCP Connection Establishment and Termination

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

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

### 1.1 tcpdump Output

下面是tcpdump抓包的结果：

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggtz5ofnlmj31ay0m2dlm.jpg)

每行就是一个单独的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

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

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggtzxwip7uj30vj0u0gpe.jpg)

### 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。

连接关闭，流程如下：

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggu3ldqimij30ri0l2gnn.jpg)

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

#### 1.4.1 Normal tcpdump Output

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

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggu3oulzmaj31ao0j8wjh.jpg)

## 2. Timeout of Connection Establishment

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

### 2.1 The Server is Down

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

比如：

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggu41fjbeyj31bc0dytc1.jpg)

client进行了两次重试。

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

### 2.2 First Timeout Period

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

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

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

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggu4dyf0e3j315e0ds76m.jpg)

但是其余的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。

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

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggxoplh6vlj30t60segol.jpg)

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

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

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

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

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

## 5. TCP State Transition Diagram

### 5.1 Diagram

TCP连接的状态转移图：

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggxot09mnnj30u014zwra.jpg)

* 一共有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。

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

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggxp3laygdj30u00wnwhb.jpg)

### 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）。

流程如图：

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggykusrpeuj314u0fsjsw.jpg)

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

## 8. Simultaneous Close

同样，两个主机也可以同时发起连接关闭（active close）。

状态转移图：

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggykulwc3xj314g0fsjsu.jpg)

## 9. TCP Options

TCP header中的可选项：

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggyl1h3xpjj314i0u077p.jpg)

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

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

## 10. TCP Server Design

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

这里关注的问题是：

* 当一个服务器进程接收一个来自客户端的请求时是如何处理端口的？
* 如果多个连接请求几乎同时到达会发生什么？

### 10.1 Restricting Foreign IP Address

![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggylsrduscj315k086go4.jpg)

### 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端就会超时然后重发。
