二、用于获得和设置Socket选项的getter和setter方法
Socket
选择可以指定Socket
类发送和接受数据的方式。在JDK1.4
中共有8
个Socket
选择可以设置。这8
个选项都定义在java.net.SocketOptions
接口中。定义如下:
public
final
static
int
TCP_NODELAY
=
0x0001
;
public final static int SO_REUSEADDR = 0x04 ;
public final static int SO_LINGER = 0x0080 ;
public final static int SO_TIMEOUT = 0x1006 ;
public final static int SO_SNDBUF = 0x1001 ;
public final static int SO_RCVBUF = 0x1002 ;
public final static int SO_KEEPALIVE = 0x0008 ;
public final static int SO_OOBINLINE = 0x1003 ;
public final static int SO_REUSEADDR = 0x04 ;
public final static int SO_LINGER = 0x0080 ;
public final static int SO_TIMEOUT = 0x1006 ;
public final static int SO_SNDBUF = 0x1001 ;
public final static int SO_RCVBUF = 0x1002 ;
public final static int SO_KEEPALIVE = 0x0008 ;
public final static int SO_OOBINLINE = 0x1003 ;
有趣的是,这8
个选项除了第一个没在SO
前缀外,其他7
个选项都以SO
作为前缀。其实这个SO
就是
Socket Option
的缩写;因此,在Java
中约定所有以SO
为前缀的常量都表示Socket
选项;当然,也有例外,如TCP_NODELAY
。在Socket
类中为每一个选项提供了一对get
和set
方法,分别用来获得和设置这些选项。
1. TCP_NODELAY
public
boolean
getTcpNoDelay()
throws
SocketException
public void setTcpNoDelay( boolean on) throws SocketException
public void setTcpNoDelay( boolean on) throws SocketException
在默认情况下,客户端向服务器发送数据时,会根据数据包的大小决定是否立即发送。当数据包中的数据很少时,如只有1
个字节,而数据包的头却有几十个字节(IP
头+TCP
头)时,系统会在发送之前先将较小的包合并到软大的包后,一起将数据发送出去。在发送下一个数据包时,系统会等待服务器对前一个数据包的响应,当收到服务器的响应后,再发送下一个数据包,这就是所谓的Nagle
算法;在默认情况下,Nagle
算法是开启的。
这种算法虽然可以有效地改善网络传输的效率,但对于网络速度比较慢,而且对实现性的要求比较高的情况下(如游戏、Telnet
等),使用这种方式传输数据会使得客户端有明显的停顿现象。因此,最好的解决方案就是需要Nagle
算法时就使用它,不需要时就关闭它。而使用setTcpToDelay
正好可以满足这个需求。当使用setTcpNoDelay(true)
将Nagle
算法关闭后,客户端每发送一次数据,无论数据包的大小都会将这些数据发送出去。
2. SO_REUSEADDR
public
boolean
getReuseAddress()
throws
SocketException
public void setReuseAddress( boolean on) throws SocketException
public void setReuseAddress( boolean on) throws SocketException
通过这个选项,可以使多个Socket
对象绑定在同一个端口上。其实这样做并没有多大意义,但当使用close
方法关闭Socket
连接后,Socket
对象所绑定的端口并不一定马上释放;系统有时在Socket
连接关闭才会再确认一下是否有因为延迟面未到达的数据包,这完全是在底层处理的,也就是说对用户是透明的;因此,在使用Socket
类时完全不会感觉到。
这种处理机制对于随机绑定端口的Socket
对象没有什么影响,但对于绑定在固定端口的Socket
对象就可能会抛出“Address already in use: JVM_Bind”
例外。因此,使用这个选项可以避免个例外的发生。
package
mynet;
import java.net. * ;
import java.io. * ;
public class Test
{
public static void main(String[] args)
{
Socket socket1 = new Socket();
Socket socket2 = new Socket();
try
{
socket1.setReuseAddress( true );
socket1.bind( new InetSocketAddress( " 127.0.0.1 " , 88 ));
System.out.println( " socket1.getReuseAddress(): "
+ socket1.getReuseAddress());
socket2.bind( new InetSocketAddress( " 127.0.0.1 " , 88 ));
}
catch (Exception e)
{
System.out.println( " error: " + e.getMessage());
try
{
socket2.setReuseAddress( true );
socket2.bind( new InetSocketAddress( " 127.0.0.1 " , 88 ));
System.out.println( " socket2.getReuseAddress(): "
+ socket2.getReuseAddress());
System.out.println( " 端口88第二次绑定成功! " );
}
catch (Exception e1)
{
System.out.println(e.getMessage());
}
}
}
}
import java.net. * ;
import java.io. * ;
public class Test
{
public static void main(String[] args)
{
Socket socket1 = new Socket();
Socket socket2 = new Socket();
try
{
socket1.setReuseAddress( true );
socket1.bind( new InetSocketAddress( " 127.0.0.1 " , 88 ));
System.out.println( " socket1.getReuseAddress(): "
+ socket1.getReuseAddress());
socket2.bind( new InetSocketAddress( " 127.0.0.1 " , 88 ));
}
catch (Exception e)
{
System.out.println( " error: " + e.getMessage());
try
{
socket2.setReuseAddress( true );
socket2.bind( new InetSocketAddress( " 127.0.0.1 " , 88 ));
System.out.println( " socket2.getReuseAddress(): "
+ socket2.getReuseAddress());
System.out.println( " 端口88第二次绑定成功! " );
}
catch (Exception e1)
{
System.out.println(e.getMessage());
}
}
}
}
上面的代码的运行结果如下:
socket1.getReuseAddress():true
error:Address already in use: JVM_Bind
socket2.getReuseAddress():true
端口88第二次绑定成功!
error:Address already in use: JVM_Bind
socket2.getReuseAddress():true
端口88第二次绑定成功!
使用SO_REUSEADDR选项时有两点需要注意:
1. 必须在调用bind方法之前使用setReuseAddress方法来打开SO_REUSEADDR选项。因此,要想使用SO_REUSEADDR选项,就不能通过Socket类的构造方法来绑定端口。
2. 必须将绑定同一个端口的所有的Socket对象的SO_REUSEADDR选项都打开才能起作用。如在例程4-12中,socket1和socket2都使用了setReuseAddress方法打开了各自的SO_REUSEADDR选项。
1. 必须在调用bind方法之前使用setReuseAddress方法来打开SO_REUSEADDR选项。因此,要想使用SO_REUSEADDR选项,就不能通过Socket类的构造方法来绑定端口。
2. 必须将绑定同一个端口的所有的Socket对象的SO_REUSEADDR选项都打开才能起作用。如在例程4-12中,socket1和socket2都使用了setReuseAddress方法打开了各自的SO_REUSEADDR选项。
3. SO_LINGER
public
int
getSoLinger()
throws
SocketException
public void setSoLinger( boolean on, int linger) throws SocketException
public void setSoLinger( boolean on, int linger) throws SocketException
这个Socket
选项可以影响close
方法的行为。在默认情况下,当调用close
方法后,将立即返回;如果这时仍然有未被送出的数据包,那么这些数据包将被丢弃。如果将linger
参数设为一个正整数n
时(n
的值最大是65,535)
,在调用close
方法后,将最多被阻塞n
秒。在这n
秒内,系统将尽量将未送出的数据包发送出去;如果超过了n
秒,如果还有未发送的数据包,这些数据包将全部被丢弃;而close
方法会立即返回。如果将linger
设为0
,和关闭SO_LINGER
选项的作用是一样的。
如果底层的Socket
实现不支持SO_LINGER
都会抛出SocketException
例外。当给linger
参数传递负数值时,setSoLinger
还会抛出一个IllegalArgumentException
例外。可以通过getSoLinger
方法得到延迟关闭的时间,如果返回-1
,则表明SO_LINGER
是关闭的。例如,下面的代码将延迟关闭的时间设为1
分钟:
if
(socket.getSoLinger()
==
-
1
) socket.setSoLinger(
true
,
60
);
4. SO_TIMEOUT
public
int
getSoTimeout()
throws
SocketException
public void setSoTimeout( int timeout) throws SocketException
public void setSoTimeout( int timeout) throws SocketException
这个Socket
选项在前面已经讨论过。可以通过这个选项来设置读取数据超时。当输入流的read
方法被阻塞时,如果设置timeout
(timeout
的单位是毫秒),那么系统在等待了timeout
毫秒后会抛出一个InterruptedIOException
例外。在抛出例外后,输入流并未关闭,你可以继续通过read
方法读取数据。
如果将timeout
设为0
,就意味着read
将会无限等待下去,直到服务端程序关闭这个Socket
。这也是timeout
的默认值。如下面的语句将读取数据超时设为30
秒:
socket1.setSoTimeout(
30
*
1000
);
当底层的Socket
实现不支持SO_TIMEOUT
选项时,这两个方法将抛出SocketException
例外。不能将timeout
设为负数,否则setSoTimeout
方法将抛出IllegalArgumentException
例外。
5. SO_SNDBUF
public
int
getSendBufferSize()
throws
SocketException
public void setSendBufferSize( int size) throws SocketException
public void setSendBufferSize( int size) throws SocketException
在默认情况下,输出流的发送缓冲区是8096
个字节(8K
)。这个值是Java
所建议的输出缓冲区的大小。如果这个默认值不能满足要求,可以用setSendBufferSize
方法来重新设置缓冲区的大小。但最好不要将输出缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。
如果底层的Socket
实现不支持SO_SENDBUF
选项,这两个方法将会抛出SocketException
例外。必须将size
设为正整数,否则setSendBufferedSize
方法将抛出IllegalArgumentException
例外。
6. SO_RCVBUF
public
int
getReceiveBufferSize()
throws
SocketException
public void setReceiveBufferSize( int size) throws SocketException
public void setReceiveBufferSize( int size) throws SocketException
在默认情况下,输入流的接收缓冲区是8096
个字节(8K
)。这个值是Java
所建议的输入缓冲区的大小。如果这个默认值不能满足要求,可以用setReceiveBufferSize
方法来重新设置缓冲区的大小。但最好不要将输入缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。
如果底层的Socket
实现不支持SO_RCVBUF
选项,这两个方法将会抛出SocketException
例外。必须将size
设为正整数,否则setReceiveBufferSize
方法将抛出IllegalArgumentException
例外。
7. SO_KEEPALIVE
public
boolean
getKeepAlive()
throws
SocketException
public void setKeepAlive( boolean on) throws SocketException
public void setKeepAlive( boolean on) throws SocketException
如果将这个Socket
选项打开,客户端Socket
每隔段的时间(大约两个小时)就会利用空闲的连接向服务器发送一个数据包。这个数据包并没有其它的作用,只是为了检测一下服务器是否仍处于活动状态。如果服务器未响应这个数据包,在大约11
分钟后,客户端Socket
再发送一个数据包,如果在12
分钟内,服务器还没响应,那么客户端Socket
将关闭。如果将Socket
选项关闭,客户端Socket
在服务器无效的情况下可能会长时间不会关闭。SO_KEEPALIVE
选项在默认情况下是关闭的,可以使用如下的语句将这个SO_KEEPALIVE
选项打开:
socket1.setKeepAlive(
true
);
8. SO_OOBINLINE
public
boolean
getOOBInline()
throws
SocketException
public void setOOBInline( boolean on) throws SocketException
public void setOOBInline( boolean on) throws SocketException
如果这个Socket
选项打开,可以通过Socket
类的sendUrgentData
方法向服务器发送一个单字节的数据。这个单字节数据并不经过输出缓冲区,而是立即发出。虽然在客户端并不是使用OutputStream
向服务器发送数据,但在服务端程序中这个单字节的数据是和其它的普通数据混在一起的。因此,在服务端程序中并不知道由客户端发过来的数据是由OutputStream
还是由sendUrgentData
发过来的。下面是sendUrgentData
方法的声明:
public
void
sendUrgentData(
int
data)
throws
IOException
虽然sendUrgentData
的参数data
是int
类型,但只有这个int
类型的低字节被发送,其它的三个字节被忽略。下面的代码
演示了如何使用SO_OOBINLINE
选项来发送单字节数据。
package
mynet;
import java.net. * ;
import java.io. * ;
class Server
{
public static void main(String[] args) throws Exception
{
ServerSocket serverSocket = new ServerSocket( 1234 );
System.out.println( " 服务器已经启动,端口号:1234 " );
while ( true )
{
Socket socket = serverSocket.accept();
socket.setOOBInline( true );
InputStream in = socket.getInputStream();
InputStreamReader inReader = new InputStreamReader(in);
BufferedReader bReader = new BufferedReader(inReader);
System.out.println(bReader.readLine());
System.out.println(bReader.readLine());
socket.close();
}
}
}
public class Client
{
public static void main(String[] args) throws Exception
{
Socket socket = new Socket( " 127.0.0.1 " , 1234 );
socket.setOOBInline( true );
OutputStream out = socket.getOutputStream();
OutputStreamWriter outWriter = new OutputStreamWriter(out);
outWriter.write( 67 ); // 向服务器发送字符"C"
outWriter.write( " hello world\r\n " );
socket.sendUrgentData( 65 ); // 向服务器发送字符"A"
socket.sendUrgentData( 322 ); // 向服务器发送字符"B"
outWriter.flush();
socket.sendUrgentData( 214 ); // 向服务器发送汉字”中”
socket.sendUrgentData( 208 );
socket.sendUrgentData( 185 ); // 向服务器发送汉字”国”
socket.sendUrgentData( 250 );
socket.close();
}
}
import java.net. * ;
import java.io. * ;
class Server
{
public static void main(String[] args) throws Exception
{
ServerSocket serverSocket = new ServerSocket( 1234 );
System.out.println( " 服务器已经启动,端口号:1234 " );
while ( true )
{
Socket socket = serverSocket.accept();
socket.setOOBInline( true );
InputStream in = socket.getInputStream();
InputStreamReader inReader = new InputStreamReader(in);
BufferedReader bReader = new BufferedReader(inReader);
System.out.println(bReader.readLine());
System.out.println(bReader.readLine());
socket.close();
}
}
}
public class Client
{
public static void main(String[] args) throws Exception
{
Socket socket = new Socket( " 127.0.0.1 " , 1234 );
socket.setOOBInline( true );
OutputStream out = socket.getOutputStream();
OutputStreamWriter outWriter = new OutputStreamWriter(out);
outWriter.write( 67 ); // 向服务器发送字符"C"
outWriter.write( " hello world\r\n " );
socket.sendUrgentData( 65 ); // 向服务器发送字符"A"
socket.sendUrgentData( 322 ); // 向服务器发送字符"B"
outWriter.flush();
socket.sendUrgentData( 214 ); // 向服务器发送汉字”中”
socket.sendUrgentData( 208 );
socket.sendUrgentData( 185 ); // 向服务器发送汉字”国”
socket.sendUrgentData( 250 );
socket.close();
}
}
由于运行上面的代码
需要一个服务器类,因此,在
加了一个类名为Server
的服务器类,关于服务端套接字的使用方法将会在后面的文章中详细讨论。在类Server类
中只使用了ServerSocket
类的accept
方法接收客户端的请求。并
从客户端传来的数据中读取两行字符串,并显示在控制台上。
测试
由于本例使用了127.0.0.1
,因Server
和Client类
必须在同一台机器上运行。
运行Server
java mynet.Server
运行Client
java mynet.Client
在服务端控制台的输出结果
服务器已经启动,端口号:
1234
ABChello world
中国
ABChello world
中国
在ClienT类中使用了sendUrgentData
方法向服务器发送了字符'A'(65)
和'B'(66)
。但发送'B'时
实际发送的是322
,由于sendUrgentData
只发送整型数的低字节。因此,实际发送的是66
。十进制整型322
的二进制形式如图1
所示。
图1 十进制整型322的二进制形式
从图1可以看出,虽然322分布在了两个字节上,但它的低字节仍然是66。
在Client类中使用flush
将缓冲区中的数据发送到服务器。我们可以从输出结果发现一个问题,在Client类中
先后向服务器发送了'C'
、
"hello world"r"n"
、'A'
、'B'
。而在服务端程序的控制台上显示的却是ABChello world
。这种现象说明使用sendUrgentData
方法发送数据后,系统会立即将这些数据发送出去;而使用write
发送数据,必须要使用flush
方法才会真正发送数据。
在Client类中
向服务器发送"
中国"
字符串。由于"
中"
是由214
和208
两个字节组成的;而"
国"
是由185
和250
两个字节组成的;因此,可分别发送这四个字节来传送"
中国"
字符串。
注意:在使用setOOBInline方法打开SO_OOBINLINE选项时要注意是必须在客户端和服务端程序同时使用setOOBInline方法打开这个选项,否则无法命名用sendUrgentData来发送数据。
国内最棒的Google Android技术社区(eoeandroid),欢迎访问!
《银河系列原创教程》发布
《Java Web开发速学宝典》出版,欢迎定购
《银河系列原创教程》发布
《Java Web开发速学宝典》出版,欢迎定购