ch18: TCP Connection Establishment and Termination
TCP
Last updated
TCP
Last updated
通过一个简单的例子,来看看TCP连接的建立和终止过程中发生了什么。
通过Telnet来发起一个请求,建立连接后就退出。
下面是tcpdump抓包的结果:
每行就是一个单独的TCP segment。
每行开头的格式是:
source > destination: flags
所以第一行的例子是说,svr4的1037上发起一个请求,目的是bsdi的discard进程,flag是S,代表了SYN。
在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只有一个,其实还可能会有多个一起的情况。
第一行中的1415531521:1415531521 (0),表示这个segment的序号是1415531521,冒号后面的数字就是这个segment数据的最后一个字节的序号,括号里的数字表示这个segment包中的数据大小,这里是0,就是说这个segment没有数据。
第三行中的ack 1415531522是一个确认号,说明bsdi收到了svr4的第一个segment。
只有ACK打开的时候才会打印这个字段。
每一行都有一个win 4096,这个是发送方通知接收方的窗口大小,由于没有额外的数据发送,所以这些窗口大小没有变化。
第一行和第二行还有,这个是发送方发送给接收方的mss值。
这就是说发送方不希望另一方给它一次发送超过1024个字节的数据。
这个值是为了防止数据分片的。
将上面的结果通过一个时间线的形式来展示出来,能更好地看到两只之间的往来:
为了建立连接,需要三次握手:
发送方(叫做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)。
也有可能两方都发起一个主动打开。
ISN每次应该不同,如何选择不同系统有不同的实现。
为什么应该不同呢?
这是为了防止网络中被延迟的segment在以后又被传送,导致连接的一方对它作出错误的解释。
怎么选择ISN呢?
在BSD的实现中,系统启动的时候ISN是1,然后然后每隔0.5秒增加64000,大概每隔9.5小时就会循环一次。
同时,每次连接建立的时候,也会加64000。
这样的方式有一个现象就是,所有的ISN都是一个奇数。
建立连接需要三次握手,而断开连接需要四次。
这是因为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也可以主动关闭连接。
为了简洁,可以只在建立连接的时候打印序号与确认号,后序可以使用相对值。比如:
可能可有几种情况导致连接建立不成功。
如果服务器down掉了,那么client发送的SYN不会收到ack。
比如:
client进行了两次重试。
第一次间隔5.8秒,第二次间隔24秒。
为啥第一个重试间隔不是6秒呢?
这是因为BSD版的TCP实现使用了一种500ms的定时器,当发起一个建立连接请求时,TCP就会建立一个6秒的定时器。
但是第一个tick不一定刚好对齐,导致第一个tick的时间可能在0到500ms之间:
但是其余的11个tick都是500ms,最后一个到了之后,就对齐了,所以第二次重试就是24秒了。
上面的输出还有[tos 0x10],这个是IP datagram中的TOS,type of service,这个0x10的含义就是最小时延。
最大报文长度(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。
TCP连接在一端结束发送数据后还可以接收数据,这就是半关闭。
这需要连接的双方都向对方发送一个FIN。
下图是一个场景的半关闭场景:
客户端发起主动关闭,服务器对这个FIN进行ack。
此时服务器还有数据没有发送完,没有关系,客户端还可以接收数据。
服务器数据发送完了就发送一个FIN。
客户端进行ack,连接完全关闭。
如果没有半关闭,那么为了达到关闭发送连接而还能接收数据的话,需要使用两个连接。
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。
下面是带上状态的时间序列图:
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的标志即可。
由于处于2MSL状态的连接不可用,那么超过2MSL状态的那个连接就不会受到前一个连接中延迟的segment的影响了。
不过这需要处于2MSL状态的主机工作正常。
如果处于2MSL状态的主机出现故障,然后在MSL时间内重启,紧接着建立了一个和之前连接一样的连接,会怎么样呢?
这样的话,故障前的segment会被新的连接接收并使用,这会导致问题。
因此,TCP在重启动之后的MSL秒内不能建立任何连接,这就是平静时间(quiet time)。
不过大多数主机的重启时都比MSL时间长,所以不一定要遵守这个规定。
在FIN_WAIT_2状态,我们已经发了一个FIN,并且另一端也进行了ack。如果我们发起的不是半关闭,那么我们就期待另一端的应用层意识到连接已关闭,然后发送一个FIN。
只有另一端完成这个关闭,才能从FIN_WAIT_2转移到TIME_WAIT状态。
这意味着我们这一端一直处于FIN_WAIT_2状态,另一端一直处于CLOSE_WAIT状态。
Berkeley实现通过加一个超时时间来避免这个问题。
当一个segment发送到一个TCP产出错误的时候,就会发送一个RST。
一个常见的例子就是向一个没有程序监听的端口发起一个连接请求。
在UDP中会返回一个端口不可达的ICMP报文,在TCP中,发送一个RST。
返回的RST中,序号的值是0。
正常关闭一个连接叫做orderly release,还可以异常关闭,叫做abortive release。
异常关闭有两个优点:
可以立即释放所有待发送的数据,然后发送一个RST;
接收RST的一端可以知道另一端是异常关闭,而不是正常关闭。
为了能够异常关闭,API需要提供一个SO_LINGER标志。
半打开(Half-open),就是一方已经关闭或异常终止连接但是另一方却不知道。
任何一端的主机异常都可能会导致这种情况发生,只要不在半打开的连接上传输数据,那么处于连接状态的一方就不知道另一方已经出现异常。
当检测到半打开的连接时,TCP会发送一个RST。
可以通过keepalive选项来检测半打开。
也有可能连接的两端同时发起一个主动打开请求,这要求两端都使用一个对方知道的端口,比如A使用自己的8888端口连接B的7777端口,同时B使用自己的7777端口连接A的8888端口。这才是一个同时打开(simultaneous open)。
流程如图:
如果不能正确实现同时打开的话,可以会出现一个两个方向上无限地发送SYN和ACK的情况。
同样,两个主机也可以同时发起连接关闭(active close)。
状态转移图:
TCP header中的可选项:
len包含选项中的所有数据。
NOP是为了header填充到4字节的整数倍。
TCP服务器设计问题,基本上是来一个连接就开启一个进程或线程进行处理。
这里关注的问题是:
当一个服务器进程接收一个来自客户端的请求时是如何处理端口的?
如果多个连接请求几乎同时到达会发生什么?
有时候连接请求到了之后但是不能马上响应,比如操作系统正在处理优先级更高的任务。
这个时候对于进来的请求该如何处理呢?
TCP有一个固定长度的队列(一般实现都是5),用来存被TCP接受的请求但是还没有被应用层接受的请求。TCP接受一个请求是将其放入队列,而应用层接受请求是从队列中取出;
这个队列的最大长度,也叫作积压值(backlog),一般是5;
当一个连接请求SYN到达后,TCP判断是否接受,如果接受的话就入队列,对其SYN进行ack并发送自己的SYN,这样连接就可以到达ESTABLISHED状态了,只不过这个时候连接还没有被应用层接受,但是client认为连接已经建立,可以发送数据了,这些数据会被放在TCP的缓冲区中;
如果没有空间了,TCP就不会对新的SYN进行响应,啥也不做,这样client端就会超时然后重发。