
2.5 设置Socket的选项
Socket的选项如下所示。
·TCP_NODELAY:表示立即发送数据。
·SO_RESUSEADDR:表示是否允许重用Socket所绑定的本地地址。
·SO_TIMEOUT:表示接收数据时的等待超时时间。
·SO_LINGER:表示当执行Socket的close()方法时,是否立即关闭底层的Socket。
·SO_SNDBUF:表示发送数据的缓冲区的大小。
·SO_RCVBUF:表示接收数据的缓冲区的大小。
·SO_KEEPALIVE:表示对于长时间处于空闲状态的Socket,是否要自动把它关闭。
·OOBINLINE:表示是否支持发送1字节的TCP紧急数据。
2.5.1 TCP_NODELAY选项
·设置该选项:public void setTcpNoDelay(boolean on) throws SocketException
·读取该选项:public boolean getTcpNoDelay() throws SocketException
在默认情况下,发送数据采用Negale算法。Negale算法指发送方发送的数据不会立刻被发出,而是先放在缓冲区内,等缓冲区满了再发出。发送完一批数据后,会等待接收方对这批数据的回应,然后发送下一批数据。Negale算法适用于发送方需要发送大批量数据,并且接收方会及时做出回应的场合,这种算法通过减少传输数据的次数来提高通信效率。
如果发送方持续地发送小批量的数据,并且接收方不一定会立即发送响应数据,那么Negale算法会使发送方运行得很慢。对于GUI程序,比如网络游戏程序(服务器需要实时跟踪客户端鼠标的移动),这个问题尤其突出。客户端鼠标位置改动的信息需要被实时地发送到服务器上,由于Negale算法采用缓冲,大大降低了实时响应速度,导致客户程序运行很慢。
TCP_NODEALY的默认值为false,表示采用Negale算法。如果调用setTcpNoDelay(true)方法,就会关闭Socket的缓冲,确保数据被及时发送。

如果Socket的底层实现不支持TCP_NODELAY选项,那么getTcpNoDelay()和setTcpNoDelay()方法会抛出SocketException。
2.5.2 SO_RESUSEADDR选项
·设置该选项:public void setResuseAddress(boolean on) throws SocketException
·读取该选项:public boolean getResuseAddress() throws SocketException
当接收方通过Socket的close()方法关闭Socket时,如果网络上还有发送到这个Socket的数据,那么底层的Socket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,再释放端口。Socket接收到延迟数据后,不会对这些数据做任何处理。Socket接收延迟数据的目的是,确保这些数据不会被其他碰巧绑定到同样端口的新进程接收到。
客户程序一般采用随机端口,因此出现两个客户程序绑定到同样端口的可能性不大。许多服务器程序都使用固定的端口。当服务器程序被关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一台主机上重启服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,导致启动失败。本书3.5.2节(SO_REUSEADDR选项)对此作了介绍。
为了确保当一个进程关闭了Socket后,即使它还没释放端口,同一台主机上的其他进程也可以立刻重用该端口,可以调用Socket的setResuseAddress(true)方法。

值得注意的是,socket.setResuseAddress(true)方法必须在Socket还没有被绑定到一个本地端口之前调用,否则执行socket.setResuseAddress(true)方法无效。因此必须按照以下方式创建Socket对象,然后连接远程服务器。

或者:

此外,两个共用同一个端口的进程必须都调用socket.setResuseAddress(true)方法,才能使得一个进程关闭Socket后,另一个进程的Socket能够立刻重用相同的端口。
2.5.3 SO_TIMEOUT选项
·设置该选项:public void setSoTimeout(int milliseconds) throws SocketException
·读取该选项:public int getSoTimeOut() throws SocketException
当通过Socket的输入流读数据时,如果还没有数据,就会等待。例如在以下代码中,in.read(buff))方法从输入流中读入1024字节。

如果输入流中没有数据,in.read(buff))就会等待发送方发送数据,直到满足以下情况才结束等待。
(1)输入流中有1024字节,read()方法把这些字节读入buff中,再返回读取的字节数。
(2)已经快接近输入流的末尾,距离末尾还有小于1024字节。read()方法把这些字节读入buff中,再返回读取的字节数。
(3)已经读到输入流的末尾,返回“-1”。
(4)连接已经断开,抛出IOException。
(5)如果通过Socket的setSoTimeout()方法设置了等待超时时间,那么超过这一时间后就抛出SocketTimeoutException。
Socket类的SO_TIMEOUT选项用于设定接收数据的等待超时时间,单位为ms,它的默认值为0,表示会无限等待,永远不会超时。以下代码把接收数据的等待超时时间设为3分钟。

Socket的setTimeout()方法必须在接收数据之前执行才有效。此外,当输入流的read()方法抛出SocketTimeoutException后,Socket仍然是连接的,可以尝试再次读取数据:

例程2-9(ReceiveServer.java)和例程2-10(SendClient.java)是一对简单的服务器/客户程序。SendClient发送字符串“hello everyone”,接着睡眠1分钟,然后关闭Socket。ReceiveServer读取SendClient发送来的数据,直到抵达输入流的末尾,最后打印SendClient发送来的数据。
例程2-9 ReceiveServer.java


例程2-10 SendClient.java

下面分3种情况演示ReceiveServer读取数据的行为。
(1)先运行“java ReceiveServer”,启动ReceiveServer进程,再运行“java SendClient”,启动SendClient进程。当SendClient睡眠时,ReceiveServer在执行in.read(buff)方法,不能读到足够的数据填满buff缓冲区,因此会一直等待SendClient发送数据。等到SendClient睡眠1分钟后,SendClient调用Socket的close()方法关闭Socket,这意味着ReceiveServer读到了输入流的末尾,ReceiveServer立即结束读等待,read()方法返回“-1”。ReceiveServer最后打印接收到的字符串“hello everyone”。
(2)先运行“java ReceiveServer”,启动ReceiveServer进程,再运行“java SendClient”,启动SendClient进程。当SendClient睡眠时,ReceiveServer在执行in.read(buff)方法,不能读到足够的数据填满buff缓冲区,因此会一直等待SendClient发送数据。
趁SendClient睡眠还没结束,还没有执行Socket的close()方法之前,在控制台中按Ctrl-C键,强行中断SendClient程序。正在等待读数据的ReceiveServer发现连接断开,in.read(buff)方法立刻抛出SocketException。ReceiveServer的打印结果如下。

(3)把ReceiveServer类中“s.setSoTimeout(20000)”这行代码前的注释符号去掉,从而把等待接收数据的超时时间设为20s。再次先后运行ReceiveServer和SendClient程序。ReceiveServer在等待读数据时,每当超过20s,就会抛出SocketTimeoutException,ReceiveServer的打印结果如下。

2.5.4 SO_LINGER选项
·设置该选项:public void setSoLinger(boolean on,int seconds) throws SocketException
·读取该选项:public int getSoLinger() throws SocketException
SO_LINGER选项用来控制Socket关闭时的行为。在默认情况下,执行Socket的close()方法,该方法会立即返回,但底层的Socket实际上并不立即关闭,它会延迟一段时间,直到发送完所有剩余的数据,才会真正关闭Socket,断开连接。
如果执行以下方法:

那么执行Socket的close()方法,该方法也会立即返回,而且底层的Socket也会立即关闭,所有未发送完的数据被丢弃。
如果执行以下方法:

那么执行Socket的close()方法,该方法不会立即返回,而进入阻塞状态,同时,底层的Socket会尝试发送剩余的数据。只有满足以下两个条件之一,close()方法才返回。
·底层的Socket已经发送完所有的剩余数据。
·尽管底层的Socket还没有发送完所有的剩余数据,但已经阻塞了3600s。当close()方法的阻塞时间超过3600s时,也会返回,未发送的数据被丢弃。
值得注意的是,在以上两种情况中,当close()方法返回后,底层的Socket会被关闭,断开连接。此外,setSoLinger(boolean on,int seconds)方法中的seconds参数以s为单位,而不是以ms为单位。
提示
当程序通过输出流写数据,仅仅表示程序向网络提交了一批数据,由网络负责输送到接收方。当程序关闭Socket,有可能这批数据还在网络上传输,还未到达接收方。这里所说的“未发送完的数据”就是指这种还在网络上传输,未被接收方接收的数据。
例程2-11(SimpleClient.java)与例程2-12(SimpleServer.java)是一对简单的客户/服务器程序。SimpleClient类发送一万个字符给SimpleServer,然后调用Socket的close()方法关闭Socket。
SimpleServer通过ServerSocket的accept()方法接受了SimpleClient的连接请求后,并不立即接收客户发送的数据,而是睡眠5s后再接收数据。等到SimpleServer开始接收数据时,SimpleClient有可能已经执行了Socket的close()方法,那么SimpleServer还能接收到SimpleClient发送的数据吗?
例程2-11 SimpleClient.java

例程2-12 SimpleServer.java


下面分3种情况演示SimpleClient关闭Socket的行为。
(1)先启动SimpleServer进程,再启动SimpleClient进程。当SimpleClient执行Socket的close()方法时,立即返回,SimpleClient的打印结果如下:

等到SimpleClient结束运行,SimpleServer可能才刚刚结束睡眠,开始接收SimpleClient发送的数据。此时尽管SimpleClient已经执行了Socket的close()方法,并且SimpleClient程序本身也运行结束了,但从SimpleServer的打印结果可以看出,SimpleServer仍然接收到了所有的数据。之所以出现这种情况,是因为当SimpleClient执行了Socket的close()方法后,底层的Socket实际上并没有真正关闭,与SimpleServer的连接依然存在。底层的Socket会存在一段时间,直到发送完所有的数据。
(2)把SimpleClient类中“s.setSoLinger(true,0)”前的注释符去掉。再次先后启动SimpleServer进程和SimpleClient进程。这次当SimpleClient执行Socket的close()方法时,会强行关闭底层的Socket,所有未发送完的数据丢失。SimpleClient的打印结果如下。

从打印结果可以看出,SimpleClient执行Socket的close()方法时,也立即返回。当SimpleServer结束睡眠,开始接收SimpleClient发送的数据时,由于SimpleClient已经关闭底层Socket,断开连接,因此SimpleServer在读数据时会抛出SocketException。

(3)把SimpleClient类中“s.setSoLinger(true,3600)”前的注释符去掉。再次先后启动SimpleServer进程和SimpleClient进程。这次当SimpleClient执行Socket的close()方法时,会进入阻塞状态,直到等待了3600s,或者底层Socket已经把所有剩余数据发送完毕,才会从close()方法返回。SimpleClient的打印结果如下。

当SimpleServer结束了5s的睡眠,开始接收SimpleClient发送的数据时,SimpleClient还在执行Socket的close()方法,并且处于阻塞状态。SimpleClient与SimpleServer之间的连接依然存在,因此SimpleServer能够接收到SimpleClient发送的所有数据。
2.5.5 SO_RCVBUF选项
·设置该选项:public void setReceiveBufferSize(int size) throws SocketException
·读取该选项:public int getReceiveBufferSize() throws SocketException
SO_RCVBUF表示Socket的用于输入数据的缓冲区的大小。一般说来,传输大的连续的数据块(比如基于HTTP或FTP的通信)可以使用较大的缓冲区,这可以减少传输数据的次数,提高传输数据的效率。而对于交互式的通信方式(比如Telnet和网络游戏),则应该采用小的缓冲区,确保小批量的数据能及时发送给对方。这种设定缓冲区大小的原则也同样适用于Socket的SO_SNDBUF选项。
如果底层Socket不支持SO_RCVBUF选项,那么setReceiveBufferSize()方法会抛出SocketException。
2.5.6 SO_SNDBUF选项
·设置该选项:public void setSendBufferSize(int size) throws SocketException
·读取该选项:public int getSendBufferSize() throws SocketException
SO_SNDBUF表示Socket用于输出数据的缓冲区的大小。如果底层Socket不支持SO_SNDBUF选项,setSendBufferSize()方法会抛出SocketException。
2.5.7 SO_KEEPALIVE选项
·设置该选项:public void setKeepAlive(boolean on) throws SocketException
·读取该选项:public int getKeepAlive() throws SocketException
当SO_KEEPALIVE选项为true时,表示底层的TCP实现会监视该连接是否有效。当连接处于空闲状态(即连接的两端没有互相传送数据)超过了2小时,本地的TCP实现会发送一个数据包给远程的Socket,如果远程Socket没有发回响应,TCP实现就会持续尝试发送11分钟,直到接收到响应为止。如果在12分钟内未收到响应,TCP实现就会自动关闭本地Socket,断开连接。在不同的网络平台上,TCP实现尝试与远程Socket对话的时限会有所差别。
SO_KEEPALIVE选项的默认值为false,表示TCP不会监视连接是否有效,不活动的客户端可能会永久存在下去,而不会注意到服务器已经崩溃。
以下代码把SO_KEEPALIVE选项设为true。

2.5.8 OOBINLINE选项
·设置该选项:public void setOOBInline(boolean on) throws SocketException
·读取该选项:public int getOOBInline() throws SocketException
当OOBINLINE为true时,表示支持发送1字节的TCP紧急数据。Socket类的sendUrgentData(int data)方法用于发送1字节的TCP紧急数据。
OOBINLINE的默认值为false,在这种情况下,当接收方收到紧急数据后不做任何处理,直接将其丢弃。如果用户希望发送紧急数据,应该把OOBINLINE设为true。

此时接收方会把接收到的紧急数据与普通数据放在同样的队列中。值得注意的是,除非使用一些更高层次的协议,否则接收方处理紧急数据的能力非常有限。当紧急数据到来时,接收方不会得到任何通知,因此接收方很难区分普通数据与紧急数据,只好按照同样的方式处理它们。
2.5.9 IP服务类型选项
当用户通过邮局发送普通信、挂号信或者快件时,实际上选择了邮局提供的不同的服务。发送普通信的价格最低,但发送速度慢,并且可靠性没有保证。发送挂号信的价格稍高,但可靠性有保证。发送快件的价格最高,发送速度最快,并且可靠性有保证。
在Internet上传输数据也分为不同的服务类型,它们有不同的定价。用户可以根据自己的需求,选择不同的服务类型。例如发送视频需要较高的带宽,快速到达目的地,以保证接收方看到连续的画面,而发送电子邮件可以使用较低的带宽,延迟几个小时到达目的地也没关系。
IP规定了一些服务类型,用来定性地描述服务的质量,举例如下。
·低成本:发送成本低。
·高可靠性:保证把数据可靠地送达目的地。
·最高吞吐量:一次可以接收或发送大批量的数据。
·最小延迟:传输数据的速度快,把数据快速送达目的地。
这些服务类型还可以进行组合,例如,可以同时要求获得高可靠性和最小延迟。服务类型存储在IP数据包头部的名为IP_TOS的8位字段(1字节)中,Socket类中提供了设置和读取服务类型的方法。
·设置服务类型:public void setTrafficClass(int trafficClass) throws SocketException
·读取服务类型:public int getTrafficClass() throws SocketException
服务类型用1字节来表示,取值范围是0到255之间的整数。这个服务类型数据也会被复制到TCP数据包头部的8位字段中。在目前的网络协议中,对这个表示服务类型的字节又做了进一步的细分。
·高六位:表示DSCP值(Differentiated Service Code Point),即表示不同的服务类型代码号。DSCP允许最多有64(2的6次方)种服务类型。
·低两位:表示ECN值(Explicit Congestion Notification),即显式拥塞通知信息。64个DSCP值到底表示什么含义,这是由具体的网络和路由器决定的。下面是比较常见的DCSP值。
·默认服务类型:取值是“000000”。
·加速转发类型:取值是“101110”。服务特点是低损耗、低延迟、低抖动。
·保证转发类型:共有12个取值,参见表2-1。保证以指定速率传送。
表2-1 保证转发类型的12个DSCP取值

其中第1类有最低转发优先级,第4类有最高转发优先级。也就是说,当网络出现阻塞时,第4类的数据包被优先转发。每一类又包含3个取值,其中低丢包率的服务类型丢弃数据包的概率小,而高丢包率的服务类型丢弃数据包的概率大。
加速转发类型比其他服务类型有更高的优先级。例如以下代码使得Socket采用加速转发类型来收发数据。

值得注意的是,DCSP值仅仅为底层的网络实现提供一个参考,有些底层Socket实现会忽略DCSP值,对它不进行任何处理。
2.5.10 设定连接时间、延迟和带宽的相对重要性
从JDK1.5开始,为Socket类提供了一个setPerformancePreferences()方法。

以上方法的3个参数表示网络传输数据的3项指标。
·参数connectionTime:表示用最少时间建立连接。
·参数latency:表示最小延迟。
·参数bandwidth:表示最高带宽。
setPerformancePreferences()方法被用来设定这3项指标之间的相对重要性。可以为这些参数赋予任意的整数,这些整数之间的相对大小就决定了相应参数的相对重要性。例如,如果参数connectionTime为2,参数latency为1,而参数bandwidth为3,就表示最高带宽最重要,其次是最少连接时间,最后是最小延迟。
值得注意的是,setPerformancePreferences()方法所做的设置仅仅为底层的网络实现提供一个参考,有些底层Socket实现会忽略这一设置,对它不进行任何处理。