Bootstrap

Android C++系列:Linux网络(四)TCP详解

1. tcp状态转换图

这个图N多人都知道,它排除和定位网络或系统故障时大有帮助,但是怎样牢牢地将这 张图刻在脑中呢?那么你就一定要对这张图的每一个状态,及转换的过程有深刻 的认识, 不能只停留在一知半解之中。下面对这张图的11种状态详细解析一下,以便加强记忆!不过在这之前,先回顾一下TCP建立连接的三次握手过程,以及关闭连接的四次握手过程。

1.1建立连接协议(三次握手)

1. 2连接终止协议(四次握手)

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这 一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方 将执行主动关闭,而另一方执行被动关闭。

1.3 11种状态

  • CLOSED: 这个没什么好说的了,表示初始状态。

  • LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。

  • SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次 握手会话过程中的一个中间状态,很短暂,基本 上用netstat你是很难看到这种状态的,除非你特意写了一个客户 端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态 时,当收到客户端的ACK报文 后,它会进入到ESTABLISHED状态。

  • SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即 它会进入到了SYN_SENT状 态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN 报文。

  • ESTABLISHED:这个容易理解了,表示连接已经建立了。

  • 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连接。

  • CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自 己,你系统毫无疑问地会回应一个ACK报文给对 方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要 考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文 给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。

  • LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收 到ACK报文后,也即可以进入到CLOSED可用状态了。

2. TCP流量控制(滑动窗口)

介绍UDP时我们描述了这样的问题:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过’滑动窗口 (Sliding Window)’机制解决这一问题。看下图的通讯过程。

上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序 提走数据,虚线框是向右滑动的,因此称为滑动窗口。

从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两 K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就 是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数 据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此 TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必 须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

3. TCP半链接状态

当TCP链接中A发送FIN请求关闭,另一段B回应ACK后,B没有立即发送FIN给A时,A方处在半链接状态,此时A可以接收B发送的数据,但是A已不能再向B发送数据。

#include 
int shutdown(int sockfd, int how);

  • sockfd: 需要关闭的socket的描述符

  • how:允许为shutdown操作选择以下几种方式:

  • SHUT_RD:关闭连接的读端。也就是该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。 进程将不能对该套接字发出任何读操作。对 TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。

  • SHUT_WR:关闭连接的写端,进程不能在对此套接字发出写操作

  • SHUT_RDWR:相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR

使用close中止一个连接,但它只是减少描述符的参考数,并不直接关闭连接,只有当描述符的参考数为0时才关闭连接。 shutdown可直接关闭描述符,不考虑描述 符的参考 数,可选择中止一个方向的连接。

注意:

4. 2MSL

MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,2MSL即两倍的MSLTCPTIME_WAIT状态也称为2MSL等待状态。

2MSL TIME_WAIT状态存在的理由:

TIME_WAIT状态的存在有两个理由:

该状态为什么设计在主动关闭这一方:

RFC要求socket pair在处于TIME_WAIT时,不能再起一个incarnation connection。但绝大部分TCP实现,强加了更为严格的限制。在2MSL等待期间,socket中使用的本地端口在默认情况下不能再被使用。若A 10.234.5.5:1234 和B 10.55.55.60:6666建立了连接,A主动关闭,那么在A端只要port为1234,无论对方的port和ip是什么,都不允许再起服务。显而易见这是比RFC更为严格的限制,RFC仅仅是要求socket pair不一致,而实现当中只要这个 port处于TIME_WAIT,就不允许起连接。这个限制对主动打开方来说是无所谓的,因为一般用的是临时端口;但对于被动打开方,一般是server,就悲剧了,因为server一般是熟知端口。比如http,一般端口是80,不可能允许这个服务在2MSL内不能起来。解决方案是给服务器的socket设置SO_REUSEADDR选项,这样的话就算熟知端口处于 TIME_WAIT状态,在这个端口上依旧可以将服务启动。当然,虽然有了SO_REUSEADDR选项,但sockt pair这个限制依旧存在。比如上面的例子,A通过SO_REUSEADDR选项依旧在1234端口上起了监听,但这时我们若是从B通过6666端 口去连它,TCP协议会告诉我们连接失败,原因为Address already in use.

5. 总结

本文介绍了TCP的三次握手、四次挥手、11种状态、TCP滑动窗口流量控制、TCP半连接状态以及TCP TIME_WAIT两倍报文最大生存时长。