【Java TCP/IP Socket编程】----深入剖析----TCP套接字生命周期

目录

 

简介

TCP连接

关闭TCP连接

解调多路复用


--------笔记来自于书籍《Java TCP/IP Socket编程》

简介

    新的Socket实例创建后(无论是通过公有的构造函数,或通过调用ServerSocket类的accept()方法)立即就能用于发送和接收数据。也就是说,当Socket实例返回时,它已经连接到一个远程终端,并通过协议的底层实现完成了TCP消息或握手信息的交换,本节主要对TCP的连接和关闭TCP连接具体实现细节,以及解调多路复用进行了描述。

TCP连接

客户端的连接事件

Socket构造函数的调用与客户端连接建立时所关联的协议事件之间的关系如下图所示,其中大箭头表示导致底层套接字数据结构发生状态改变的外部事件,客户端的互联网地址表示为A.B.C.D,服务器端的互联网地址表示为W.X.Y.Z;服务器的端口号是Q。

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

    TCP的开放握手也称为3次握手,包含一条从客户端到服务器端的连接请求,一条从服务器端到客户端的确认消息,以及另外一条从客户端到服务器端的确认消息。客户端一收到服务器端发来的确认消息,就立即认为连接已经成功建立。但客户端的起始消息或服务器端的回复消息都可能在传输过程中丢失。出于这个原因,TCP协议实现将以递增的时间间隔重复发送几次握手消息,如果TCP客户端在这一段时间后还没有收到服务器的回复消息,则发生超时并放弃连接。

服务器端的事件序列

    服务器首先创建一个关联已知端口号(在此为Q)的ServerSocket实例,套接字实现为新的ServerSocket实例创建了一个底层数据结构。并将Q赋给本地端口,将特定的通配符地址(*)赋给本地IP地址。此时套接字处于“LISTENING”状态,表示准备好接受传入该端口的连接请求。现在服务器端可以调用ServerSocket的accept()方法,该方法将阻塞等待,直到与某个客户端完成开放握手信息交换,并成功建立了新的连接。

                     

    当客户端的连接请求到来时,将为该连接创建一个新的套接字数据结构。新套接字的地址根据到来的分组报文设置:分组报文的目标互联网地址和端口号(分别是W.X.Y.Z和Q)称为该套接字的本地互联网地址和端口号;而分组报文的源地址和端口号(分别为A.B.C.D和P)则称为该套接字的远程互联网地址和端口号。新套接字的本地端口号总是与ServerSocket的端口号是一致的。新套接字的状态为正在连接(Connectioning)。除了要创建一个新的底层套接字数据结构外,服务器方的TCP实现还要向客户端发回一个TCP握手确认消息。

                   

    在接收到客户端发来的3次握手的第3条消息之前,服务器端TCP并不会认为握手消息已经完成。第三条握手消息到来后,新数据结构状态为“ESTABLISHED”,并将其移动到ServerSocket数据结构关联的另一个套接字数据结构列表中,该列表代表了能够通过ServerSocket的accept()方法进行接收的已成功建立的连接。服务器在调用ServerSocket的accept()方法后,只要其关联的套接字数据结构列表中有新的连接到来,该方法就立即停止阻塞,此时一个新的连接数据结构将从列表中移除,并为其创建一 个Socket实例,作为accept()方法返回值。

                 

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

关闭TCP连接

    TCP协议有一个优雅的关闭机制,以保证应用从程序在关闭连接时不必担心正在传输的数据会丢失。这个机制还设计为允许两个方向的护具相互独立地终止。关闭机制的工作流程:应用程序通过调用连接套接字的close()方法或者shutdownOutput()方法表明数据已经发送完毕。底层的TCP实现首先将留存在SendQ队列中的数据传输出去(要依赖于另一端的RecvQ队列的剩余空间),然后向另一端发送一个关闭TCP连接的握手消息。该关闭握手消息可以看作是流终止标志:它告诉接收端TCP不会再有新的数据传入RecvQ队列了。(注意,关闭握手消息本身并没有传递给接收端应用程序,而是通过read()方法返回-1来指示其在字节流中的位置。)正在关闭的TCP将等待其关闭握手消息的确认消息,该确认消息表明在连接上传输所有的数据已经完全地传输到RecvQ中,只要收到了确认消息,该连接就变成了半关闭状态,直到连接的另一个方向上收到了对称的握手消息后,连接才完全关闭。即连接的两端都表明它们再没有数据要发送了。

    TPC连接的关闭事件序列可能以两种方式发生:一种方式是先由一个应用程序调用close()方法(或shutdownOutput()方法),并在另一端调用close()方法之前完成关闭握手消息,另一种方式是两端同时调用close()方法,它们的关闭握手消息在网络上交叉传输。如下是第一种方式关闭连接时,底层实现中事件序列。

                   

    需要注意的是如果连接处于半关闭状态时,远程终端已经离开,那么本地底层数据结构则将无限期地保持在该状态。当另一端关闭握手消息到达后,则发回一条确认消息并将状态改变成“Time-Wait”。虽然应用程序中相应Socket实例可能早已消息,与之关联的底层数据结构还将在底层实现中存留几分钟。

    下图展示了没有首先发起关闭的终端上的事件序列,关闭握手消息到达后,它立即发回一个消息,并将状态改变为“Close-Wait”.此时只需要等待应用程序调用Socket的close()方法。调用该方法后,将发起最终的关闭握手消息,并释放底层套接字数据结构。需要注意的一点是close()或者shutdownOutput()方法都没有等待关闭握手的完成,而是调用后立即返回。所以可能还有数据留存在SendQ队列中,如果连接的任何一端在数据传输到RecvQ队列之前崩溃,数据将丢失,而发送端应用程序不会知道。解决方案就是最好一种应用程序协议,以使调用close()方法的一方在接收到应用程序层的数据已接收保证后,才真正执行关闭操作。

                                     

    关闭TCP连接的最后微妙之处在于对Time—Wait状态的需要。TCP规范要求在终止连接时,两端的关闭握手都完成后,至少要有一个套接字在Time—Wait状态保持一段时间。这个要求的提出是由于消息在网络中传输时可能延迟。如果在连接两端都完成了关闭握手后,它们都移除了其底层数据结构,而此时在同样一对套接字地址之间又建立了新的连接,那么前一个连接在网络上传输时延迟的消息就可能在新建立的连接后到达。由于包含了相同的源地址和目的地址,旧消息就会被错误地认为是属于新连接的,其包含的数据就可能被错误地分配到应用程序中。虽然这种情况很少发生,TCP还是使用了包括Time—Write状态在内的多种机制对其进行防范。

     Time—Wait状态最重要的作用是:只要底层套接字数据结构还存在,就不允许在相同的本地端口上关联其他套接字,尤其试图使用该端口创建新的Socket实例时,将抛出IOException异常

解调多路复用

    服务器端存在这样一个问题:同一个机器上的不同套接字可以有相同的本地地址和端口号。如:在只有一个IP地址的机器上,每个通过ServerSocket的accept()方法接收到的新Socket实例都将使用与ServerSocket相同的本地端口号。显然要确定传入的分组报文应该分配到那个套接字(即解调多路复用)不仅仅是查看分组报文的目的地址和端口。方对于TCP和UDP来说,将传入的分组报文匹配到某个套接字过程是一样的。可以归纳为以下几点:

    1)套接字数据结构中的本地端口号必须与传入的分组报文的目的端口号相匹配。

    2)在套接字数据结构中,任何包含了统配符(*)的字段可以匹配分组报文中相应字段的任何值。

    3)如果有一个以上的套接字数据结构与传入分组报文地址的4个字段匹配,那么谁使用的统配符少,谁就获得该分组报文。

猜你喜欢

转载自blog.csdn.net/lili13897741554/article/details/83116969