java

位置:IT落伍者 >> java >> 浏览文章

Java网络编程从入门到精通(18):Socket类的getter和setter方法(2)


发布日期:2023年08月29日
 
Java网络编程从入门到精通(18):Socket类的getter和setter方法(2)

用于获得和设置Socket选项的getter和setter方法

Socket选择可以指定Socket类发送和接受数据的方式在JDK中共有个Socket选择可以设置个选项都定义在SocketOptions接口中定义如下

publicfinalstaticintTCP_NODELAY=x;

publicfinalstaticintSO_REUSEADDR=x;

publicfinalstaticintSO_LINGER=x;

publicfinalstaticintSO_TIMEOUT=x;

publicfinalstaticintSO_SNDBUF=x;

publicfinalstaticintSO_RCVBUF=x;

publicfinalstaticintSO_KEEPALIVE=x;

publicfinalstaticintSO_OOBINLINE=x;

有趣的是个选项除了第一个没在SO前缀外其他个选项都以SO作为前缀其实这个SO就是Socket Option的缩写因此在Java中约定所有以SO为前缀的常量都表示Socket选项当然也有例外如TCP_NODELAY在Socket类中为每一个选项提供了一对get和set方法分别用来获得和设置这些选项

TCP_NODELAY

publicbooleangetTcpNoDelay()throwsSocketException

publicvoidsetTcpNoDelay(booleanon)throwsSocketException

在默认情况下客户端向服务器发送数据时会根据数据包的大小决定是否立即发送当数据包中的数据很少时如只有个字节而数据包的头却有几十个字节(IP头+TCP头)时系统会在发送之前先将较小的包合并到软大的包后一起将数据发送出去在发送下一个数据包时系统会等待服务器对前一个数据包的响应当收到服务器的响应后再发送下一个数据包这就是所谓的Nagle算法在默认情况下Nagle算法是开启的

这种算法虽然可以有效地改善网络传输的效率但对于网络速度比较慢而且对实现性的要求比较高的情况下(如游戏Telnet等)使用这种方式传输数据会使得客户端有明显的停顿现象因此最好的解决方案就是需要Nagle算法时就使用它不需要时就关闭它而使用setTcpToDelay正好可以满足这个需求当使用setTcpNoDelay(true)将Nagle算法关闭后客户端每发送一次数据无论数据包的大小都会将这些数据发送出去

SO_REUSEADDR

publicbooleangetReuseAddress()throwsSocketException

publicvoidsetReuseAddress(booleanon)throwsSocketException

通过这个选项可以使多个Socket对象绑定在同一个端口上其实这样做并没有多大意义但当使用close方法关闭Socket连接后Socket对象所绑定的端口并不一定马上释放系统有时在Socket连接关闭才会再确认一下是否有因为延迟面未到达的数据包这完全是在底层处理的也就是说对用户是透明的因此在使用Socket类时完全不会感觉到

这种处理机制对于随机绑定端口的Socket对象没有什么影响但对于绑定在固定端口的Socket对象就可能会抛出Address already in use JVM_Bind例外因此使用这个选项可以避免个例外的发生

package mynet;

import*;

importjavaio*;

publicclass Test

{

publicstaticvoidmain(String[]args)

{

Socketsocket=newSocket();

Socketsocket=newSocket();

try

{

socketsetReuseAddress(true);

socketbind(newInetSocketAddress());

Systemoutprintln(socketgetReuseAddress():

+socketgetReuseAddress());

socketbind(newInetSocketAddress());

}

catch(Exceptione)

{

Systemoutprintln(error:+egetMessage());

try

{

socketsetReuseAddress(true);

socketbind(newInetSocketAddress());

Systemoutprintln(socketgetReuseAddress():

+socketgetReuseAddress());

Systemoutprintln(端口第二次绑定成功!);

}

catch(Exceptione)

{

Systemoutprintln(egetMessage());

}

}

}

}

上面的代码的运行结果如下

socketgetReuseAddress():true

error:Addressalreadyinuse:JVM_Bind

socketgetReuseAddress():true

端口第二次绑定成功!

使用SO_REUSEADDR选项时有两点需要注意

必须在调用bind方法之前使用setReuseAddress方法来打开SO_REUSEADDR选项因此要想使用SO_REUSEADDR选项就不能通过Socket类的构造方法来绑定端口

必须将绑定同一个端口的所有的Socket对象的SO_REUSEADDR选项都打开才能起作用如在例程socket和socket都使用了setReuseAddress方法打开了各自的SO_REUSEADDR选项

SO_LINGER

publicintgetSoLinger()throwsSocketException

publicvoidsetSoLinger(booleanonintlinger)throwsSocketException

这个Socket选项可以影响close方法的行为在默认情况下当调用close方法后将立即返回如果这时仍然有未被送出的数据包那么这些数据包将被丢弃如果将linger参数设为一个正整数n时(n的值最大是在调用close方法后将最多被阻塞n秒在这n秒内系统将尽量将未送出的数据包发送出去如果超过了n秒如果还有未发送的数据包这些数据包将全部被丢弃而close方法会立即返回如果将linger设为和关闭SO_LINGER选项的作用是一样的

如果底层的Socket实现不支持SO_LINGER都会抛出SocketException例外当给linger参数传递负数值时setSoLinger还会抛出一个IllegalArgumentException例外可以通过getSoLinger方法得到延迟关闭的时间如果返回则表明SO_LINGER是关闭的例如下面的代码将延迟关闭的时间设为分钟

if(socketgetSoLinger()==)socketsetSoLinger(true);

SO_TIMEOUT

publicintgetSoTimeout()throwsSocketException

publicvoidsetSoTimeout(inttimeout)throwsSocketException

这个Socket选项在前面已经讨论过可以通过这个选项来设置读取数据超时当输入流的read方法被阻塞时如果设置timeout(timeout的单位是毫秒)那么系统在等待了timeout毫秒后会抛出一个InterruptedIOException例外在抛出例外后输入流并未关闭你可以继续通过read方法读取数据

如果将timeout设为就意味着read将会无限等待下去直到服务端程序关闭这个Socket这也是timeout的默认值如下面的语句将读取数据超时设为

socketsetSoTimeout(*);

当底层的Socket实现不支持SO_TIMEOUT选项时这两个方法将抛出SocketException例外不能将timeout设为负数否则setSoTimeout方法将抛出IllegalArgumentException例外

SO_SNDBUF

publicintgetSendBufferSize()throwsSocketException

publicvoidsetSendBufferSize(intsize)throwsSocketException

在默认情况下输出流的发送缓沖区是个字节(K)这个值是Java所建议的输出缓沖区的大小如果这个默认值不能满足要求可以用setSendBufferSize方法来重新设置缓沖区的大小但最好不要将输出缓沖区设得太小否则会导致传输数据过于频繁从而降低网络传输的效率

如果底层的Socket实现不支持SO_SENDBUF选项这两个方法将会抛出SocketException例外必须将size设为正整数否则setSendBufferedSize方法将抛出IllegalArgumentException例外

SO_RCVBUF

publicintgetReceiveBufferSize()throwsSocketException

publicvoidsetReceiveBufferSize(intsize)throwsSocketException

在默认情况下输入流的接收缓沖区是个字节(K)这个值是Java所建议的输入缓沖区的大小如果这个默认值不能满足要求可以用setReceiveBufferSize方法来重新设置缓沖区的大小但最好不要将输入缓沖区设得太小否则会导致传输数据过于频繁从而降低网络传输的效率

如果底层的Socket实现不支持SO_RCVBUF选项这两个方法将会抛出SocketException例外必须将size设为正整数否则setReceiveBufferSize方法将抛出IllegalArgumentException例外

SO_KEEPALIVE

publicbooleangetKeepAlive()throwsSocketException

publicvoidsetKeepAlive(booleanon)throwsSocketException

如果将这个Socket选项打开客户端Socket每隔段的时间(大约两个小时)就会利用空闲的连接向服务器发送一个数据包这个数据包并没有其它的作用只是为了检测一下服务器是否仍处于活动状态如果服务器未响应这个数据包在大约分钟后客户端Socket再发送一个数据包如果在分钟内服务器还没响应那么客户端Socket将关闭如果将Socket选项关闭客户端Socket在服务器无效的情况下可能会长时间不会关闭SO_KEEPALIVE选项在默认情况下是关闭的可以使用如下的语句将这个SO_KEEPALIVE选项打开

socketsetKeepAlive(true);

SO_OOBINLINE

publicbooleangetOOBInline()throwsSocketException

publicvoidsetOOBInline(booleanon)throwsSocketException

如果这个Socket选项打开可以通过Socket类的sendUrgentData方法向服务器发送一个单字节的数据这个单字节数据并不经过输出缓沖区而是立即发出虽然在客户端并不是使用OutputStream向服务器发送数据但在服务端程序中这个单字节的数据是和其它的普通数据混在一起的因此在服务端程序中并不知道由客户端发过来的数据是由OutputStream还是由sendUrgentData发过来的下面是sendUrgentData方法的声明

publicvoidsendUrgentData(intdata)throwsIOException

虽然sendUrgentData的参数data是int类型但只有这个int类型的低字节被发送其它的三个字节被忽略下面的代码演示了如何使用SO_OOBINLINE选项来发送单字节数据

package mynet;

import*;

importjavaio*;

class Server

{

publicstaticvoidmain(String[]args)throwsException

{

ServerSocketserverSocket=newServerSocket();

Systemoutprintln(服务器已经启动端口号);

while(true)

{

Socketsocket=serverSocketaccept();

socketsetOOBInline(true);

InputStreamin=socketgetInputStream();

InputStreamReaderinReader=newInputStreamReader(in);

BufferedReaderbReader=newBufferedReader(inReader);

Systemoutprintln(bReaderreadLine());

Systemoutprintln(bReaderreadLine());

socketclose();

}

}

}

publicclass Client

{

publicstaticvoidmain(String[]args)throwsException

{

Socketsocket=newSocket();

socketsetOOBInline(true);

OutputStreamout=socketgetOutputStream();

OutputStreamWriteroutWriter=newOutputStreamWriter(out);

outWriterwrite();//向服务器发送字符C

outWriterwrite(helloworld\r\n);

socketsendUrgentData();//向服务器发送字符A

socketsendUrgentData();//向服务器发送字符B

outWriterflush();

socketsendUrgentData();//向服务器发送汉字

socketsendUrgentData();

socketsendUrgentData();//向服务器发送汉字

socketsendUrgentData();

socketclose();

}

}

由于运行上面的代码需要一个服务器类因此在加了一个类名为Server的服务器类关于服务端套接字的使用方法将会在后面的文章中详细讨论在类Server类中只使用了ServerSocket类的accept方法接收客户端的请求并从客户端传来的数据中读取两行字符串并显示在控制台上

测试

由于本例使用了因Server和Client类必须在同一台机器上运行

运行Server

javamynetServer

运行Client

javamynetClient

在服务端控制台的输出结果

服务器已经启动端口号

ABChelloworld

中国

在ClienT类中使用了sendUrgentData方法向服务器发送了字符A)和B但发送B时实际发送的是由于sendUrgentData只发送整型数的低字节因此实际发送的是十进制整型的二进制形式如图所示

十进制整型的二进制形式

从图可以看出虽然分布在了两个字节上但它的低字节仍然是

在Client类中使用flush将缓沖区中的数据发送到服务器我们可以从输出结果发现一个问题在Client类中先后向服务器发送了Chello worldrnAB而在服务端程序的控制台上显示的却是ABChello world这种现象说明使用sendUrgentData方法发送数据后系统会立即将这些数据发送出去而使用write发送数据必须要使用flush方法才会真正发送数据

在Client类中向服务器发送中国字符串由于是由两个字节组成的是由两个字节组成的因此可分别发送这四个字节来传送中国字符串

注意在使用setOOBInline方法打开SO_OOBINLINE选项时要注意是必须在客户端和服务端程序同时使用setOOBInline方法打开这个选项否则无法命名用sendUrgentData来发送数据

               

上一篇:探索Java语言与JVM中的Lambda表达式

下一篇:Java设计模式-----Mediator中介者模式