6.4 TCP套接字的生存周期

新的Socket实例创建后(无论是通过公有的构造函数,或是通过调用ServerSocket类的accept()方法)立即就能用于发送和接收数据。也就是说,当Socket实例返回时,它已经连接到了一个远程终端,并通过协议的底层实现完成了TCP消息或握手信息的交换。

下面,让我们进一步详细考虑底层的数据结构如何到达已连接或“已建立(Established)”状态。后面你将看到,这些细节会对可靠性的定义和创建一个绑定到特定端口的Socket或ServerSocket的能力产生影响。

6.4.1 连接

Socket构造函数的调用与客户端连接建立时所关联的协议事件之间的关系如图6-6所示。在本节所有示意图中,大箭头都表示导致底层套接字数据结构发生状态改变的外部事件。在应用程序中发生的事件(即方法调用和返回)显示在图个上部;如消息到达等事件显示在图的下部。所有图的时间顺序都是从左到右的;客户端的互联网地址表示为A.B.C.D,服务器端的互联网地址表示为W.X.Y.Z;服务器的端口号是Q。(我们描述的是IPv4地址,不过这里介绍的内容都适用于IPv4和IPv6。)

figure_0171_0241

图 6-6 客户端连接建立

当客户端以服务器端的互联网地址W.X.Y.Z和端口号Q作为参数,调用Socket的构造函数时,底层实现将创建一个套接字实例,该实例的初始状态是关闭状态(Closed)。如果在调用构造函数时客户端没有指定本地地址或端口号,底层实现将选择一个没有被其他TCP套接字使用的本地端口号(P)。同时还要指定本地的互联网地址,如果没有显式地指定,则将向服务器发送数据报文的网络接口地址作为本地地址。底层实现将本地和远程地址和端口复制到底层套接字的数据结构,并初始化TCP连接建立时的握手消息。

TCP的开放握手也称为3次握手(3-way handshake),因为这通常包括3条消息:一条从客户端到服务器端的连接请求,一条从服务器端到客户端的确认消息,以及另一条从客户端到服务器端的确认消息。客户端一收到服务器端发来的确认消息,就立即认为连接已经成功建立。通常情况这个过程发生得很快。然而,互联网是一种尽力而为(best-effort)的网络,客户端的起始消息或服务器端的回复消息都可能在传输过程中丢失。出于这个原因,TCP协议实现将以递增的时间间隔重复发送几次握手消息。如果TCP客户端在一段时间后还没有收到服务器的回复消息,则发生超时并放弃连接。这种情况下,构造函数将抛出IOException异常。连接的超时通常比较长,因此要经过几分种的时间Socket的构造函数才会失败。

在初始的握手消息发送之后,并在接收到服务器端的回复消息之前(即图6-6的中间部分),客户端主机上netstat的输出将类似于以下内容:

figure_0172_0242

其中,“SYN_SENT”是在第一条和第二条握手消息之间,客户端状态的专业名称。

如果服务器并没有接收连接(比如,目标地址的给定端口上没有关联任何程序),服务器端的TCP将发送一条拒绝消息而不是确认消息,并且构造函数几乎立即会抛出一个IOException异常。否则,在客户端收到了服务器端的肯定回复后,其netstat的输出将类似于以下内容:

figure_0172_0243

服务器端的事件序列则有所不同,我们在图6-7,图6-8和图6-9中对其进行描述。服务器首先创建一个ServerSocket实例,并将其与已知端口相关联(在此为Q)。套接字实现为新的ServerSocket实例创建了一个底层数据结构,并将Q赋给本地端口,将特定的通配符地址(图中的“*”)赋给本地IP地址。(服务器也可能会在构造函数中指定一个本地IP地址,但是通常不这样做。对于服务器主机有多个IP地址的情况,不指定本地地址使套接能够接受发送到该服务器主机任何地址的连接请求。)套接字的状态设置为“LISTENING”,指示该套接已经准备好接受传入该端口的连接请求。图6-7描述了这个过程。服务器端netstat的输出中会包含类似于如下行的内容:

figure_0173_0244

figure_0173_0245

图 6-7 服务器端的套接字设置

现在服务器可以调用ServerSocket的accept()方法,该方法将阻塞等待,直到与某个客户端完成了开放握手信息交换,并成功建立了新的连接。因此我们关注于(见图6-8)当客户端连接请求到来时,TCP实现中发生的事件。注意,该图中描述的内容全都隐蔽地发生在TCP底层实现中。

当客户端的连接请求到来时,将为该连接创建一个新的套接字数据结构。新套接字的地址根据到来的分组报文设置:分组报文的目标互联网地址和端口号(分别为W.X.Y.Z和Q)成为该套接字的本地互联网地址和端口号;而分组报文的源地址和端口号(分别为A.B.C.D和P)则成为该套接字的远程互联网地址和端口号。注意,新套接字的本地端口号总是与ServerSocket的端口号一致。新套接字的状态设置为指示“正在连接(Connecting)”(在服务器方,专业术语称其为SYN_RCVD),并将其添加到ServerSocket套接字数据结构所关联的一个未完全连接的套接字列表中。注意,ServerSocket自己并不改变状态,其地址信息也不会有任何改变。此时,netstat的输出内容应该包括原始的侦听套接字和新创建的套接字:

figure_0174_0246

除了要创建一个新的底层套接字数据结构外,服务器方的TCP实现还要向客户端发回一个TCP握手确认消息。

figure_0174_0247

图 6-8 处理传入的连接请求

然而,在接收到客户端发来的3次握手的第3条消息之前,服务器端TCP并不会认为握手消息已经完成。第3条握手消息到来后,新数据结构的状态则设置为“ESTABLISHED”,并将其移动到ServerSocket数据结构关联的另一个套接字数据结构列表中,该列表代表了能够通过ServerSocket的accept()方法进行接收的已成功建立的连接。(如果第3条握手消息接收失败,最终会将“Connecting”状态的数据结构删除。)此时netstat的输出将包含:

figure_0175_0248

现在,我们来考虑(见图6-9)服务器程序调用了ServerSocket的accept()方法后发生的事情。只要其关联的套接字数据结构列表中有新的连接到来,该方法调用就立即停止阻塞。(注意,在调用accept()方法时,这个列表可能已经是非空状态。)此时,一个新的连接数据结构将从列表中移除,并为其创建一个Socket实例,作为accept()方法的返回值。

这里有非常重要的一点需要注意,在ServerSocket关联的列表中的每个数据结构,都代表了一个与另一端的客户端已经完成建立的TCP连接。实际上,客户端只要接收到了开放握手的第2条消息,就可以立即发送数据—这可能比服务器调用accept()方法为其获取一个Socket实例要早很长时间。

figure_0175_0249

图 6-9 accept()处理