第6章 深入剖析

如果不理解套接字的具体实现所关联的数据结构和底层协议的工作细节,就很难抓住网络编程的精妙之处,对于TCP套接字(即Socket的实例)来说更是如此。本章就对创建和使用Socket或ServerSocket实例时的底层细节进行了介绍。(本章开始的讨论以及第6.5节同样适用于DatagramSocket和MulticastSocket。而且,由于每个SocketChannel都有一个底层的Socket(其他类型的信道也类似),我们讨论的内容也同样适用于它们。然而,本章大部分内容都针对的是TCP套接字,即,Socket和ServerSocket。)请注意,这些内容仅仅涵盖了一些普通的事件序列,略去了很多细节。尽管如此,我们相信即使是这种基础的理解也是有用的。如果希望了解更详尽内容,可以参考TCP规范[1],或关于该主题的其他更全面的著作[2][3]

图6-1是一个Socket实例所关联的一些信息的简化视图。JVM和/或其运行的平台(即,主机操作系统中的“套接字层”)为这些类的支持提供了底层实现。Java对象上的操作则转换成了在这种底层抽象上的操作。在本章中,“Socket”指的是图6-1中的类之一,而“套接字(socket)”指的是底层抽象,这种抽象由操作系统提供或由JVM自己实现(例如在嵌入式系统中)。有一点需要注意,即运行在统一主机上的其他程序可能也会通过底层套接字抽象来使用网络,因此会与Java Socket实例竞争系统资源,如端口等。

在此,“套接字结构”是指底层实现(包括JVM和TCP/IP,但通常是后者)的数据结构集,这些数据结构包含了特定Socket实例所关联的信息。例如,套接字结构除其他信息外还包含:

·该套接字所关联的本地和远程互联网地址和端口号。本地互联网地址(图中标记为“Local IP”)是赋值给本地主机的;本地端口号在Socket实例创建是设置。远程地址和端口号标识了与本地套接字连接的远程套接字(如果有连接的话)。不久,我们将对这些值确定的时间和方式作进一步介绍(第6.5节中有一个简明的概要)。

figure_0163_0230

图 6-1 套接字关联的数据结构

·一个FIFO(先进先出,First In First Out)队列用于存放接收到的等待分配的数据,以及一个用于存放等待传输的数据的队列。

·对于TCP套接字,还包含了与打开和关闭TCP握手相关的额外协议状态信息。图6-1中,状态是“关闭”;所有套接字的起始状态都是关闭的。

一些多用途操作系统为用户提供了获取底层数据结构“快照”的工具,netstat是其中之一,它在UNIX(Linux)和Windows平台上都可用。只要给定适当的选项,netstat就能显示和图6-1中的那些信息:SendQ和RecvQ中的字节数,本地和远程IP地址和端口号,以及连接状态等。netstat的命令行选项有多种,但它的输出看起来是这样的:

figure_0163_0231

figure_0164_0232

前4行和最后一行描述了正在侦听连接的服务器套接字。(最后一行是一个绑定到IPv6地址的侦听套接字。)第5行代表了到一个Web服务器(80端口)的连接,该服务器已经单方面关闭(见第6.4.2节)。倒数第2行是现有的TCP连接。如果系统支持的话,你可能想要尝试一下netstat,来测试下文描述的场景的连接状态。然而要知道,这些图中描述的状态转换过程转瞬即逝,可能很难通过netstat提供的“快照”功能将其捕获。

了解这些数据结构,以及底层协议如何对其进行影响是非常有用的,因为它们控制了各种Socket对象行为的各个方面。例如,由于TCP提供了一种可信赖的字节流服务,任何写入Socket的OutputStream的数据副本都必须保留,直到其在连接的另一端被成功接收。向输出流写数据并不意味着数据实际上已经被发送—它们只是被复制到了本地缓冲区。就算在Socket的OutputStream上进行flush()操作,也不能保证数据能够立即发送到信道。此外,字节流服务的自身属性决定了其无法保留输入流中消息的边界信息。如在第3.3节见到的,这使一些协议的接收和解析过程变得复杂。另一方面,对于DatagramSocket,数据包并没有为重传而进行缓存,任何时候调用send()方法返回后,数据就已经发送给了执行传输任务的网络子系统。如果网络子系统由于某种原因无法处理这些消息,该数据包将毫无提示地被丢弃(不过这种情况很少发生)。

后面3节对使用TCP字节流服务发送和接收数据的一些微妙之处进行了介绍。然后,第6.4节专注于TCP协议连接的建立和终止。最后,第6.5节讨论了匹配传入的数据包到套接字的过程和绑定端口号的一些规则。