在Java类中,getter和setter方法占了很大的比重。由于Java中没有定义属性的关键字;因此,getter和setter方法用于获得和设置Java类的属性值;如getName和setName方法用于设置name属性的值。如果某个属性只有getter方法,那这个属性是只读的;如果只有setter方法,那么这个属性是只写的。在Socket类中也有很多这样的属性来获得和Socket相关的信息,以及对Socket对象的状态进行设置。
一、用于获得信息的getter方法
我们可以从Socket对象中获得3种信息。
对于客户端来说,服务器的信息只有3个:域名、IP地址)和端口。Socket类为我们提供了3个方法来得到这3个信息。
(1)public InetAddress getInetAddress()这个方法返回一个InetAddress对象。通过这个对象,可以得到服务器的IP、域名等信息。
Socket socket = new Socket("www.ptpress.com.cn", 80);
//IP 地址(字符串形式)
System.out.println(socket.getInetAddress().getHostAddress());
//域名
System.out.println(socket.getInetAddress().getHostName());
(2)public int getPort()
这个方法可以以整数形式获得服务器的端口号。
Socket socket = new Socket("www.ptpress.com.cn", 80);
System.out.println(socket.getPort());
(3)public SocketAddressgetRemoteSocketAddress()
这个方法是将getInetAddress和getPort方法结合在了一起;利用这个方法可以同时得到服务器的IP和端口号。但这个方法返回了一个SocketAddress对象,这个对象只能作为connect方法的参数用于连接服务器;而要想获得服务器的IP和端口号,必须得将SocketAddress转换为它的子类InetSocketAddress。
Socket socket = new Socket("www.ptpress.com.cn", 80);
System.out.println(((InetSocketAddress)socket.getRemoteSocketAddress()).getHostName());
System.out.println(((InetSocketAddress)socket.getRemoteSocketAddress()).getPort());
注意:以上3个方法都可以在调用Socket对象关闭后调用。它们所获得的信息在Socket对象关闭后仍然有效。如果直接使用IP连接服务器时,getHostName和getHostAddress的返回值是一样的,都是服务器的IP。
2、本机信息
与服务器信息一样,本机信息也有3个:本地IP、域名和绑定的本地端口号。这些信息也可以通过3个方法来获得。
(1)publicInetAddress getLocalAddress()
这个方法返回了本机的InetAddress对象。通过这个方法可以得到本机的IP和机器名。当本机绑定了多个IP时,Socket对象使用哪一个IP连接服务器,就返回哪个IP。如果本机使用ADSL上网,并且通过Socket对象连接到Internet上的某一个IP或域名上(如www.ptpress.com.cn),则getLocalAddress将返回“ADSL连接”所临时绑定的IP;因此,我们可以通过getLocalAddress得到ADSL的临时IP。
Socket socket = new Socket();
socket.connect(new InetSocketAddress("www.ptpress.com.cn", 80));
System.out.println(socket.getLocalAddress().getHostAddress());
System.out.println(socket.getLocalAddress().getHostName());
(2)publicint getLocalPort()
通过这个方法可以得到Socket对象所绑定的本机的一个端口号;如果未绑定端口号,则返回一个从1024到65,535之间的随机数。因此,使用这个方法可能每次得到的端口号不一样。
Socket socket = new Socket();
// 如果使用下面的bind方法进行端口绑定的话,getLocalPort方法将返回100
// socket.bind(new InetSocketAddress("127.0.0.1", 100));
socket.connect(new InetSocketAddress("www.ptpress.com.cn" 80));
System.out.println(socket.getLocalPort())
(3)public SocketAddressgetLocalSocketAddress()
这个方法和getRemoteSocketAddress方法类似,也是同时得到了本地IP和Socket对象所绑定的端口号。如果要得到本地IP和端口号,必须将这个方法的返回值转换为InetSocketAddress对象。
Socket socket = new Socket("www.ptpress.com.cn", 80);
System.out.println(((InetSocketAddress)socket.getLocalSocketAddress()).getHostName());
System.out.println(((InetSocketAddress)socket.getLocalSocketAddress()).getPort());
3、用于传输数据的输入、输出流
输入、输出流在前面的章节已经被多次用到。在这里让我们来简单回顾一下。
(1)public InputStream getInputStream() throws IOException
用于获得从服务器读取数据的输入流。它所得的流是最原始的源。为了操作更方便,我们经常使用InputStreamReader和BufferedReader来读取从服务器传过来的字符串数据。
Socket socket = new Socket("www.ptpress.com.cn", 80);
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
System.out.println(bufferedReader.readLine());
(2)public OutputStream getOutputStream() throws IOException
用于获得向服务器发送数据的输出流。输出流可以通过OutputStreamWriter和BufferedWriter向服务器写入字符串数据。
Socket socket = new Socket("www.ptpress.com.cn", 80);
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
bufferedWriter.write("你好");
bufferedWriter.flush();
注意:在使用OutputStream的write方法输出数据后,必须使用flush方法刷新输出缓冲区,以便将输出缓冲区中的数据发送出去。如果要输出字符串,使用OutputStreamWriter和BufferedWriter都可以;它们的write方法都可以直接使用字符串作为参数来输出数据。而这一点与相应的InputStreamReader和BufferedReader不同;它们中只有BufferedReader有readLine方法,因此,必须使用BufferedReader才能直接读取字符串数据。二、用于获取和设置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;
有
趣的是,这8个选项除了第一个没有SO前缀外,其他7个选项都以SO作为前缀。其实这个SO就是SocketOption的缩写;因此,在Java中约定所有以SO为前缀的常量都表示Socket选项;当然,也有例外,如TCP_NODELAY。在Socket类中为每一个选项提供了一对get和set方法,分别用来获得和设置这些选项。
1、TCP_NODELAY
public boolean getTcpNoDelay() 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
通过这个选项,可以使多个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());
}
}
}
}
上面
的代码的运行结果如下:
socket1.getReuseAddress():true
error:Address already in use: JVM_Bind
socket2.getReuseAddress():true
端口88第二次绑定成功!
使用SO_REUSEADDR选项时有两点需要注意:
a.必须在调用bind方法之前使用setReuseAddress方法来打开SO_REUSEADDR选项。因此,要想使用SO_REUSEADDR选项,就不能通过Socket类的构造方法来绑定端口。
b.必须将绑定同一个端口的所有的Socket对象的SO_REUSEADDR选项都打开才能起作用。如在上面的代码中,socket1和socket2都使用了setReuseAddress方法打开了各自的SO_REUSEADDR选项。
3、SO_LINGER
public int getSoLinger() 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
这个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
在默认情况下,输出流的发送缓冲区是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
在默认情况下,输入流的接收缓冲区是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
如果将这个
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
如果这个
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();
}
}
由于运行上面的代码需要一个服务器类,因此,在加了一个类名为Server的服务器类,关于服务端套接字的使用方法将会在后面的文章中详细讨论。在类Server类中只使用了ServerSocket类的accept方法接收客户端的请求。并从客户端传来的数据中读取两行字符串,并显示在控制台上。
测试:
由于本例使用了127.0.0.1,因Server和Client类必须在同一台机器上运行。
1)运行Serverjava mynet.Server
2)运行Client
java mynet.Client
在服务端控制台的输出结果
服务器已经启动,端口号:1234
ABChello world
中国
在ClienT类中使用了sendUrgentData
方法向服务器发送了字符'A'(65)
和'B'(66)
。但发送'B'时
实际发送的是322
,由于sendUrgentData
只发送整型数的低字节。因此,实际发送的是66
。
在Client类中使用flush 将缓冲区中的数据发送到服务器。我们可以从输出结果发现一个问题,在Client类中 先后向服务器发送了'C' 、 "helloworld"r"n" 、'A' 、'B' 。而在服务端程序的控制台上显示的却是ABChello world 。这种现象说明使用 sendUrgentData方法发送数据后,系统会立即将这些数据发送出去;而使用write发送数据,必须要使用flush方法才会真正发送数据。
在Client类中向服务器发送"中国"字符串。由于"中"是由214和208两个字节组成的;而"国"是由185和250两个字节组成的;因此,可分别发送这四个字节来传送"中国"字符串。
注意:在使用setOOBInline方法打开SO_OOBINLINE选项时要注意是必须在客户端和服务端程序同时使用setOOBInline方法打开这个选项,否则无法命名用sendUrgentData来发送数据。