本节我们讨论建立和关闭tcp连接的过程。
首先从ernest-laptop上执行telnet命令登录kongming20的80端口,然后抓取这一过程中客户端和服务器交换的tcp报文段。具体操作过程如下:
当执行telnet命令并在两台通信主机之间建立tcp连接后(telnet输出“connected to 192.168.1.109”),输入ctrl+]以调出telnet程序的命令提示符,然后在telnet命令提示符后输入quit以退出telnet客户端程序,从而结束tcp连接。整个过程中(从连接建立到结束),tcpdump输出的内容如代码清单3-2所示。
因为整个过程并没有发生应用层数据的交换,所以tcp报文段的数据部分的长度(length)总是0。为了更清楚地表示建立和关闭tcp连接的整个过程,我们将tcpdump输出的内容绘制成图3-6所示的时序图。
第1个tcp报文段包含syn标志,因此它是一个同步报文段,即ernest-laptop(客户端)向kongming20(服务器)发起连接请求。同时,该同步报文段包含一个isn值为535734930的序号。第2个tcp报文段也是同步报文段,表示kongming20同意与ernest-laptop建立连接。同时它发送自己的isn值为2159701207的序号,并对第1个同步报文段进行确认。确认值是535734931,即第1个同步报文段的序号值加1。前文说过,序号值是用来标识tcp数据流中的每一字节的。但同步报文段比较特殊,即使它并没有携带任何应用程序数据,它也要占用一个序号值。第3个tcp报文段是ernest-laptop对第2个同步报文段的确认。至此,tcp连接就建立起来了。建立tcp连接的这3个步骤被称为tcp三次握手。
从第3个tcp报文段开始,tcpdump输出的序号值和确认值都是相对初始isn值的偏移。当然,我们可以开启tcpdump的-s选项来选择打印序号的绝对值。
后面4个tcp报文段是关闭连接的过程。第4个tcp报文段包含fin标志,因此它是一个结束报文段,即ernest-laptop要求关闭连接。结束报文段和同步报文段一样,也要占用一个序号值。kongming20用tcp报文段5来确认该结束报文段。紧接着kongming20发送自己的结束报文段6,ernest-laptop则用tcp报文段7给予确认。实际上,仅用于确认目的的确认报文段5是可以省略的,因为结束报文段6也携带了该确认信息。确认报文段5是否出现在连接断开的过程中,取决于tcp的延迟确认特性。延迟确认将在后面讨论。
在连接的关闭过程中,因为ernest-laptop先发送结束报文段(telnet客户端程序主动退出),故称ernest-laptop执行主动关闭,而称kongming20执行被动关闭。
一般而言,tcp连接是由客户端发起,并通过三次握手建立(特殊情况是所谓同时打开[1])的。tcp连接的关闭过程相对复杂一些。可能是客户端执行主动关闭,比如前面的例子;也可能是服务器执行主动关闭,比如服务器程序被中断而强制关闭连接;还可能是同时关闭(和同时打开一样,非常少见)。
tcp连接是全双工的,所以它允许两个方向的数据传输被独立关闭。换言之,通信的一端可以发送结束报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送结束报文段以关闭连接。tcp连接的这种状态称为半关闭(half close)状态,如图3-7所示。
请注意,在图3-7中,服务器和客户端应用程序判断对方是否已经关闭连接的方法是:read系统调用返回0(收到结束报文段)。当然,linux还提供其他检测连接是否被对方关闭的方法,这将在后续章节讨论。
socket网络编程接口通过shutdown函数提供了对半关闭的支持,我们将在后续章节讨论它。这里强调一下,虽然我们介绍了半关闭状态,但是使用半关闭的应用程序很少见。
前面我们讨论的是很快建立连接的情况。如果客户端访问一个距离它很远的服务器,或者由于网络繁忙,导致服务器对于客户端发送出的同步报文段没有应答,此时客户端程序将产生什么样的行为呢?显然,对于提供可靠服务的tcp来说,它必然是先进行重连(可能执行多次),如果重连仍然无效,则通知应用程序连接超时。
为了观察连接超时,我们模拟一个繁忙的服务器环境,在ernest-laptop上执行下面的操作:
iptable命令用于过滤数据包,这里我们利用它来丢弃所有接收到的连接请求(丢弃所有同步报文段,这样客户端就无法得到任何确认报文段)。
接下来从kongming20上执行telnet命令登录到ernest-laptop,并用tcpdump抓取这个过程中双方交换的tcp报文段。具体操作如下:
从两次date命令的输出来看,kongming20建立tcp连接的超时时间是63?s。本次tcpdump的输出如代码清单3-3所示。
这次抓包我们保留了tcpdump输出的时间戳(不使用其-t选项),以便推理linux的超时重连策略。
我们一共抓取到6个tcp报文段,它们都是同步报文段,并且具有相同的序号值,这说明后面5个同步报文段都是超时重连报文段。观察这些tcp报文段被发送的时间间隔,它们分别为1?s、2?s、4?s、8?s和16?s(由于定时器精度的问题,这些时间间隔都有一定偏差),可以推断最后一个tcp报文段的超时时间是32?s(63?s-16?s-8?s-4?s-2?s-1?s)。因此,tcp模块一共执行了5次重连操作,这是由/proc/sys/net/ipv4/tcp_syn_retries内核变量所定义的。每次重连的超时时间都增加一倍。在5次重连均失败的情况下,tcp模块放弃连接并通知应用程序。
在应用程序中,我们可以修改连接超时时间,具体方法将在本书后续章节中进行介绍。