今天在书中找到了比较详细的解释,记录一下
三次握手
在可以使用TCP链路之前,必须在客户端和主机之间显式建立连接。如上所述,在主动(active)和被动(passive)连接的建立方式是有区别的。
内核(即连接所涉及的两台机器的内核)在连接建立之前,会看到下述情形:客户端进程的套接字状态为CLOSED,而服务器套接字的状态是LISTEN。
建立TCP连接的过程需要交换3个TCP分组,因而称为三次握手(three-way handshake),会发生下列操作。
客户端通过向服务器发送SYN来发出连接请求。客户端的套接字状态由CLOSED变为SYN_SENT。
服务器在一个监听套接字上接收到连接请求,并返回SYN和ACK。服务器套接字的状态由LISTEN变为SYN_RECV。
客户端套接字接收到SYN/ACK分组后,切换到ESTABLISHED状态,表明连接已经建立。一个ACK分组被发送到服务器。
服务器接收到ACK分组,也切换到ESTABLISHED状态。这就完成了两端的连接建立工作,可以开始数据交换。
原则上,可以仅使用一个或两个分组建立连接。但这可能带来一种风险,由于与同一地址(IP地址和端口号)之间的旧连接的延期分组的存在,可能导致建立有缺陷的连接。三次握手的目的就是要防止这种情况。
在连接建立后,TCP链路的特点就很清楚了。每个分组发送时都指定一个序列号,而接收方的TCP协议实例在接收到每个分组之后,都必须确认。我们考察一下向Web服务器发出的连接请求的记录:
1 192.168.0.143 192.168.1.10 TCP 1025 > http [SYN] Seq=2895263889 Ack=0
2 192.168.1.10 192.168.0.143 TCP http > 1025 [SYN, ACK] Seq=2882478813 Ack=2895263890
3 192.168.0.143 192.168.1.10 TCP 1025 > http [ACK] Seq=2895263890 Ack=2882478814
客户端对第一个分组生成随机的序列号2895263889,保存在在TCP首部的SEQ字段。服务器对该分组的到达,响应一个组合的SYN/ACK分组,序列号是新的(在本例中是2882478813)。
我们在这里感兴趣的是SEQ/ACK字段的内容(数值字段,不是标志位)。服务器填充该字段时,将接收到的字节数目加1,再加到接收的序列号上(底层的原理,在下文讨论)。
分组还需要设置ACK标志,这用于向客户端表示已经接收到第一个分组。无须产生额外的分组来确认收到第一个分组。
确认可以在任何分组中给出,只要该分组设置了ACK标志并填充ack字段即可。为建立连接而发送的分组不包含数据,只有TCP首部是有意义的。首部中len字段存储的长度总是0。
这里描述的机制不是特定于Linux内核的,对所有希望通过TCP通信的操作系统来说,都是必须实现的。下面几节将更多阐述上述操作特定于Linux内核的实现。
四次挥手
类似于连接建立,TCP连接的关闭也是通过一系列分组交换完成的,连接可以采用下列两种方法关闭。
- 在参与传输的某一方(偶尔也会两个系统同时发出请求的情况)显式请求关闭连接时,连接会以优雅关闭(graceful close)的方式终止。
- 高层协议有可能导致连接终止或异常中止(例如,可能因为程序崩溃)。幸运的是,因为第一种情况通常更为常见,这里只讨论这种情况并忽略第二种情况。为了优雅地关闭连接,TCP连接的参与方必须交换4个分组。各个步骤的顺序描述如下:
计算机A调用标准库函数close,发出一个TCP分组,首部中的FIN标志置位。A的套接字切换到FIN_WAIT_1状态。
B收到FIN分组并返回一个ACK分组。其套接字状态从ESTABLISHED改变为CLOSE_WAIT。收到FIN后,以“文件结束”的方式通知套接字。
在收到ACK分组之后,计算机A的套接字状态从FIN_WAIT_1变为FIN_WAIT_2。
计算机B上与对应套接字相关的应用程序也执行close,从B向A发送FIN分组。计算机B的套接字状态变为LAST_ACK。
计算机A用一个ACK分组确认B发送的FIN,然后首先进入TIME_WAIT状态,接下来在一定时间后自动切换到CLOSED状态。
计算机B收到ACK分组,其套接字也切换到CLOSED状态。
解释:
- FIN_WAIT_1:这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
- FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
- TIME_WAIT:表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
- CLOSING:这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也就会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
- LAST_ACK:这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
状态迁移在中枢的分配器函数(tcp_rcv_state_process)中进行,可能的代码路径包括处理现存连接的tcp_rcv_established,以及尚未讨论的tcp_close函数。
在用户进程决定调用库函数close关闭连接时,会调用tcp_close。如果套接字的状态为LISTEN(即没有到另一台计算机的连接),因为不需要通知其他参与方连接的结束。在过程开始时会检查这种情况,如果确实如此,则将套接字的状态改为CLOSED。否则,在通过tcp_close_state并tcp_set_state调用链将套接字状态设置为FIN_WAIT_1之后,tcp_send_fin向另一方发送一个FIN分组。②
从FIN_WAIT_1到FIN_WAIT_2状态的迁移通过中枢的分配器函数tcp_rcv_state_process进行,因为不再需要采取快速路径处理现存连接。我们熟悉的一种情况是,收到的带有ACK标志的分组触发到FIN_WAIT_2状态的迁移,具体的状态迁移通过tcp_set_state进行。现在只需要从另一方发送过来的一个FIN分组,即可将TCP连接置为TIME_WAIT状态(然后会自动切换到CLOSED状态)。
在收到第一个FIN分组因而需要被动关闭连接的另一方,状态迁移的过程是类似的。因为收到第一个FIN分组是套接字状态为ESTABLISHED,处理由tcp_rcv_established的低速路径进行,涉及向另一方发送一个ACK分组,并将套接字状态改为TCP_CLOSING。
下一个状态转移(到LAST_ACK)是通过调用close库函数(进而调用了内核的tcp_close_state函数)进行的。此时,只需要另一方再发送一个ACK分组,即可终止连接。该分组也是通过tcp_rcv_state_process函数处理,该函数将套接字状态改为CLOSED(通过tcp_done),释放套接字占用的内存空间,并最终终止连接。