6.5 解调多路复用揭秘

在前面的讨论中已经隐含表明一个事实,即同一个机器上的不同套接字可以有相同的本地地址和端口号。例如,在只有一个IP地址的机器上,每个通过ServerSocket的accept()方法接收的新Socket实例都将使用与ServerSocket相同的本地端口号。显然,要确定传入的分组报文应该分配到那个套接字(即,解调多路复用)不仅仅是查看分组报文的目的地址和端口。

figure_0179_0255

图 6-12 匹配了多个套接字的解调多路复用

否则传入的分组报文应该分配给哪个套接字就会含糊不清。对于TCP和UDP来说,将传入的分组报文匹配到某个套接字的过程是一样的,可以归纳为以下几点:

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

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

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

例如,考虑一个主机有两个IP地址的情况,10.1.2.3和192.168.3.2,还有如图6-12所示的活跃的TCP套接字数据结构子集。标记为0的数据结构与一个ServerSocket关联,有一个通配符本地地址,端口号为99。标记为1的套接字数据结构也关联了同一个端口号的ServerSocket,但其本地地址指定为10.1.2.3(因此它只接收发向这个地址的连接请求)。数据结构2代表了通过ServerSocket为数据结构0接收的一个连接,因此有相同的本地端口号,但也填入了本地和远程互联网地址。其他套接字则属于其他活跃的连接。现在考虑一个分组报文,其源IP地址是172.16.1.10,源端口号是56789,目的IP地址是10.1.2.3,目的端口号是99。该报文将分配到与数据结构1相关联的套接字上,因为该套接字匹配的通配符最少。

当程序试图使用特定的本地端口号创建套接字时,要检查已有的套接字以确保没有其他套接字已经使用了那个本地端口。如果已经有套接字与构造函数中指定的本地端口和本地IP地址(如果有的话)相匹配,Socket的构造函数将抛出一个异常。这在如下情形中将导致一些问题:

1.客户端程序用特定的本地端口号P创建了一个Socket实例,并通过它与服务器进行通信。

2.客户端关闭了Socket,底层数据结构进入了Time-Wait状态。

3.客户端程序终止后又立即重新启动。

如果新的客户端化身试图使用同样的本地端口号,而由于其他数据结构正处于Time-Wait状态,Socket构造函数将抛出IOException异常。在写本书期间,解决这个问题的唯一途径是等待底层数据结构离开Time-Wait状态。

那么怎么确定本地或远程的地址和端口号呢?对于ServerSocket,所有构造函数都要求传入本地端口号。本地地址可能会在构造函数中指定,否则,就使用通配符(*)地址。ServerSocket的远程地址和端口号始终是通配符。对于Socket,所有构造函数都要求传入特定的远程地址和端口号。本地地址或端口号可能会在构造函数中指定,否则,本地地址就使用用来建立到服务器的连接的网络接口地址,本地端口号就随机选择一个大于1023的未使用端口号。对于accept()方法返回的Socket实例,本地地址是从客户端发起的初始握手消息的目的地址,本地端口号是SeverSocket的本地端口,远程地址和端口号则是客户端的本地地址和端口号。对于DatagramSocket,本地地址和端口可能会在构造函数中指定,否则,本地地址将使用通配符地址,本地端口则随机选择一个大于1023的未使用端口号,远程地址和端口号都初始化为通配符并一直保持下去,除非调用connect()方法指定了特定的值。