说明:
内容主要参照自《UNIX网络编程卷1:套接字联网API(第3版)》的第二章节。
理解connect、accept和close这3个函数并使用netstat程序调试TCP应用,必须了解TCP连接如何建立和终止,并掌握TCP状态转换图。
一、三路握手:
建立一个TCP连接时会发生下述情形:
1、服务器必须准备好接受外来的连接。这通常通过调用socket、bind和listen这3个函数来完成,我们称之为被动打开(passive open)。
2、客户通过调用connect发起主动打开(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项。
3、服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。
4、客户必须确认服务器的SYN。
这种交换至少需要3个分组,因此称之为TCP的三路握手(three-way handshake)。如下图所示:
图2-2给出的客户的初始序列号为J,服务器的初始序列号为K。ACK中的确认号是发送这个ACK的一端所期所期待的下一个序列号。因为SYN占据一个字节的序列号空间,所以每一个SYN的ACK中的确认号就是该SYN的初始序列号加1。类似地,每一个FIN(表示结束)的ACK中的确认号为该FIN的序列号加1。
注:
建立TCP连接就好比一个电话系统。socket函数等同于有电话可用。bind函数是在告诉别人你的电话号码,这样他们可以呼叫你。listen函数是打开电话振铃,这样当有一个外来呼叫到达时,你就可以听到。connect函数要求我们知道对方的电话号码并拔打它。accept函数发生在被呼叫的人应答电话之时。由accept返回客户的标识(即客户的IP地址和端口号)类似于让电话机的呼叫者ID功能部件显示呼叫者的电话号码。然而两者的不同之处在于accept只在连接建立之后返回客户的标识,而呼叫者ID功能部件却在我们选择应答或不应答电话之前显示呼叫者的电话号码。如果使用域名系统DNS,它就提供了一种类似于电话簿的服务。getaddrinfo类似于在电话簿中查找某个人的电话号码,getnameinfo则类似于有一本按照电话号码而不是按照用户名排序的电话簿。
二、TCP选项:
每一个SYN可以含有多个TCP选项,常用的TCP选项如下:
1、MSS(maximum segment size),最大分节大小。
2、窗口规模选项。
3、时间戳选项。
高带宽或长延迟的网络被称为“长胖管道”(long fat pipe),2、3选项也称为“长胖管道选项”。
三、TCP连接终止:
1、某个应用进程首先调用close,称该端执行为主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
2、接收到这个FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
3、一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
4、接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。我们使限定词“通常”是因为:某些情形下步骤1的FIN随数据一起发送;另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能合并成一个分节。
类似SYN,一个FIN也占据1个字节的序列号空间。因此,每个FIN的ACK确认号就是这个FIN的序列号加1。
在步骤2和步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的。这称为半关闭(half-close),如下图18-10(来自《TCP/IP详解卷1》)所示:
当套接字被关闭时,其所在端TCP和自发送一个FIN。由图2-3中指出,这是由应用进程调用close而发生的,不过需认识到,当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接也发出一个FIN。
图2-3展示了客户执行主动关闭的情形,不过我们指出,无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是客户执行主动关闭,但是某些协议(譬如值得注意的HTTP/1.0)却由服务器执行主动关闭。
四、TCP状态转换图:
TCP涉及连接建立和连接终止的操作可以用状态转换图(state transition diagram)来说明。
TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。
这些状态可以使有netstat显示,它是一个在调试客户/服务器应用时很有用的工具。
五、TCP观察分组:
如果该连接的整个目的仅仅是发送一个单分节的请求和接收一个单分节的应答,那么使用TCP有8个分节的开销。如果改用UDP,那么只需要交换两个分组:一个承载请求,一个承载应答。然而从TCP切换到UDP将丧失TCP提供应用进程的全部可靠性,迫使可靠服务的一大堆细节从传输层(TCP)转移到UDP应用进程。TCP提供的另一个重要特性即拥塞控制也必须由UDP应用进程来处理。尽管如此,我们仍然需要知道许多网络应用是使用UDP构建的,因为它们交换的数据量很少,而UDP避免了TCP连接建立和终止所需的开销。
六、TIME_WAIT状态:
1、TCP有关网络编程最不容易理解的是它的TIME_WAIT状态。图2-5中看到执行主动关闭的那端经历了这个状态。该端点停留在这个状态的持续时间是最长分节生命期(maximum segment lifetime,MSL)的两倍,有时候称之为2MSL。
2、TIME_WAIT状态有两个存在的理由:
1)、可靠地实现TCP全双工连接的终止;
2)、允许老的重复分节在网络中消逝。
3、第一个理由,可以通过查看图2-5并假设最终的ACK丢失了来解释。服务器将重发送它的最终那个FIN,因此客户必须维护状态信息,以允许它重新发送最终那个ACK。要是客户不维护状态信息,它将影响以一个RST(另外一种类的TCP分节),该分节将被服务器解释成一个错误。
4、第二个理由,假设在12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口之间建立另一个连接。后一个连接称为前一个连接的化身(incamation),因为它们的IP地址和端口号都相同。TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一连接的某个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的连接发起新的化身。既然TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存话MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。通过实施这个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了。