ServerSocket类的构造方法有四种重载形式,它们的定义如下:
public ServerSocket() throws IOException
public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
在上面的构造方法中涉及到了三个参数:port、backlog和bindAddr。其中port是ServerSocket对象要绑定的端口,backlog是请求队列的长度,bindAddr是ServerSocket对象要绑定的IP地址。
一、通过构造方法绑定端口
通过构造方法绑定端口是创建ServerSocket对象最常用的方式。可以通过如下的构造方法来绑定端口:
public ServerSocket(int port) throws IOException
如果port参数所指定的端口已经被绑定,构造方法就会抛出IOException异常。但实际上抛出的异常是BindException。从图4.2的异常类继承关系图可以看出,所有和网络有关的异常都是IOException类的子类。因此,为了ServerSocket构造方法还可以抛出其他的异常,就使用了IOException。
如果port的值为0,系统就会随机选取一个端口号。但随机选取的端口意义不大,因为客户端在连接服务器时需要明确知道服务端程序的端口号。可以通过ServerSocket的toString方法输出和ServerSocket对象相关的信息。下面的代码输入了和ServerSocket对象相关的信息。
ServerSocket serverSocket = new ServerSocket(1320);
System.out.println(serverSocket);
System.out.println(serverSocket);
运行结果:
ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=1320]
上面的输出结果中的addr是服务端绑定的IP地址,如果未绑定IP地址,这个值是0.0.0.0,在这种情况下,ServerSocket对象将监听服务端所有网络接口的所有IP地址。port永远是0。localport是ServerSocket绑定的端口,如果port值为0(不是输出结果的port,是ServerSocket构造方法的参数port),localport是一个随机选取的端口号。
在操作系统中规定1 ~ 1023为系统使用的端口号。端口号的最小值是1,最大值是65535。在Windows中用户编写的程序可以绑定端口号小于1024的端口,但在Linux/Unix下必须使用root登录才可以绑定小于1024的端口。在前面的文章中曾使用Socket类来判断本机打开了哪些端口,其实使用ServerSocket类也可以达到同样的目的。基本原理是用ServerSocket来绑定本机的端口,如果绑定某个端口时抛出BindException异常,就说明这个端口已经打开,反之则这个端口未打开。
package server;
import java.net.*;
public class ScanPort
{
public static void main(String[] args)
{
if (args.length == 0)
return;
int minPort = 0, maxPort = 0;
String ports[] = args[0].split("[-]");
minPort = Integer.parseInt(ports[0]);
maxPort = (ports.length > 1) ? Integer.parseInt(ports[1]) : minPort;
for (int port = minPort; port <= maxPort; port++)
try
{
ServerSocket serverSocket = new ServerSocket(port);
serverSocket.close();
}
catch (Exception e)
{
System.err.println(e.getClass());
System.err.println("端口" + port + "已经打开!");
}
}
}
import java.net.*;
public class ScanPort
{
public static void main(String[] args)
{
if (args.length == 0)
return;
int minPort = 0, maxPort = 0;
String ports[] = args[0].split("[-]");
minPort = Integer.parseInt(ports[0]);
maxPort = (ports.length > 1) ? Integer.parseInt(ports[1]) : minPort;
for (int port = minPort; port <= maxPort; port++)
try
{
ServerSocket serverSocket = new ServerSocket(port);
serverSocket.close();
}
catch (Exception e)
{
System.err.println(e.getClass());
System.err.println("端口" + port + "已经打开!");
}
}
}
在上面的代码中输出了创建ServerSocket对象时抛出的异常类的信息。ScanPort通过命令行参数将待扫描的端口号范围传入程序,参数格式为:minPort-maxPort,如果只输入一个端口号,ScanPort程序只扫描这个端口号。
测试
java server.ScanPort 1-1023
运行结果class java.net.BindException
端口80已经打开!
class java.net.BindException
端口135已经打开!
端口80已经打开!
class java.net.BindException
端口135已经打开!
二、设置请求队列的长度
在编写服务端程序时,一般会通过多线程来同时处理多个客户端请求。也就是说,使用一个线程来接收客户端请求,当接到一个请求后(得到一个Socket对象),会创建一个新线程,将这个客户端请求交给这个新线程处理。而那个接收客户端请求的线程则继续接收客户端请求,这个过程的实现代码如下:
ServerSocket serverSocket = new ServerSocket(1234); // 绑定端口
// 处理其他任务的代码
while(true)
{
Socket socket = serverSocket.accept(); // 等待接收客户端请求
// 处理其他任务的代码
new ThreadClass(socket).start(); // 创建并运行处理客户端请求的线程
}
// 处理其他任务的代码
while(true)
{
Socket socket = serverSocket.accept(); // 等待接收客户端请求
// 处理其他任务的代码
new ThreadClass(socket).start(); // 创建并运行处理客户端请求的线程
}
上面代码中的ThreadClass类是Thread类的子类,这个类的构造方法有一个Socket类型的参数,可以通过构造方法将Socket对象传入ThreadClass对象,并在ThreadClass对象的run方法中处理客户端请求。这段代码从表面上看好象是天衣无缝,无论有多少客户端请求,只要服务器的配置足够高,就都可以处理。但仔细思考上面的代码,我们可能会发现一些问题。如果在第2行和第6行有足够复杂的代码,执行时间也比较长,这就意味着服务端程序无法及时响应客户端的请求。
假设第2行和第6行的代码是Thread.sleep(3000),这将使程序延迟3秒。那么在这3秒内,程序不会执行accept方法,因此,这段程序只是将端口绑定到了1234上,并未开始接收客户端请求。如果在这时一个客户端向端口1234发来了一个请求,从理论上讲,客户端应该出现拒绝连接错误,但客户端却显示连接成功。究其原因,就是这节要讨论的请求队列在起作用。
在使用ServerSocket对象绑定一个端口后,操作系统就会为这个端口分配一个先进先出的队列(这个队列长度的默认值一般是50),这个队列用于保存未处理的客户端请求,因此叫请求队列。而ServerSocket类的accept方法负责从这个队列中读取未处理的客户端请求。如果请求队列为空,accept则处于阻塞状态。每当客户端向服务端发来一个请求,服务端会首先将这个客户端请求保存在请求队列中,然后accept再从请求队列中读取。这也可以很好地解释为什么上面的代码在还未执行到accept方法时,仍然可以接收一定数量的客户端请求。如果请求队列中的客户端请求数达到请求队列的最大容量时,服务端将无法再接收客户端请求。如果这时客户端再向服务端发请求,客户端将会抛出一个SocketException异常。
ServerSocket类有两个构造方法可以使用backlog参数重新设置请求队列的长度。在以下几种情况,仍然会采用操作系统限定的请求队列的最大长度:
- backlog的值小于等于0。
- backlog的值大于操作系统限定的请求队列的最大长度。
- 在ServerSocket构造方法中未设置backlog参数。
下面积代码演示了请求队列的一些特性,请求队列长度通过命令行参数传入SetRequestQueue。
package server;
import java.net.*;
class TestRequestQueue
{
public static void main(String[] args) throws Exception
{
for (int i = 0; i < 10; i++)
{
Socket socket = new Socket("localhost", 1234);
socket.getOutputStream().write(1);
System.out.println("已经成功创建第" + String.valueOf(i + 1) + "个客户端连接!");
}
}
}
public class SetRequestQueue
{
public static void main(String[] args) throws Exception
{
if (args.length == 0)
return;
int queueLength = Integer.parseInt(args[0]);
ServerSocket serverSocket = new ServerSocket(1234, queueLength);
System.out.println("端口(1234)已经绑定,请按回车键开始处理客户端请求!");
System.in.read();
int n = 0;
while (true)
{
System.out.println("<准备接收第" + (++n) + "个客户端请求!");
Socket socket = serverSocket.accept();
System.out.println("正在处理第" + n + "个客户端请求");
Thread.sleep(3000);
System.out.println("第" + n + "个客户端请求已经处理完毕!>");
}
}
}
import java.net.*;
class TestRequestQueue
{
public static void main(String[] args) throws Exception
{
for (int i = 0; i < 10; i++)
{
Socket socket = new Socket("localhost", 1234);
socket.getOutputStream().write(1);
System.out.println("已经成功创建第" + String.valueOf(i + 1) + "个客户端连接!");
}
}
}
public class SetRequestQueue
{
public static void main(String[] args) throws Exception
{
if (args.length == 0)
return;
int queueLength = Integer.parseInt(args[0]);
ServerSocket serverSocket = new ServerSocket(1234, queueLength);
System.out.println("端口(1234)已经绑定,请按回车键开始处理客户端请求!");
System.in.read();
int n = 0;
while (true)
{
System.out.println("<准备接收第" + (++n) + "个客户端请求!");
Socket socket = serverSocket.accept();
System.out.println("正在处理第" + n + "个客户端请求");
Thread.sleep(3000);
System.out.println("第" + n + "个客户端请求已经处理完毕!>");
}
}
}
测试(按着以下步骤操作)
1. 执行如下命令(在执行这条命令后,先不要按回车键):
1. 执行如下命令(在执行这条命令后,先不要按回车键):
java server.SetRequestQueue 2
运行结果:
端口(1234)已经绑定,请按回车键开始处理客户端请求!
2. 执行如下命令:
2. 执行如下命令:
java server.TestRequestQueue
运行结果:
已经成功创建第1个客户端连接!
已经成功创建第2个客户端连接!
Exception in thread "main" java.net.SocketException: Connection reset by peer: socket write error
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:92)
at java.net.SocketOutputStream.write(SocketOutputStream.java:115)
at server.TestRequestQueue.main(SetRequestQueue.java:12)
已经成功创建第2个客户端连接!
Exception in thread "main" java.net.SocketException: Connection reset by peer: socket write error
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:92)
at java.net.SocketOutputStream.write(SocketOutputStream.java:115)
at server.TestRequestQueue.main(SetRequestQueue.java:12)
3. 按回车键继续执行SetRequestQueue后,运行结果如下:
端口(1234)已经绑定,请按回车键开始处理客户端请求!
<准备接收第1个客户端请求!
正在处理第1个客户端请求
第1个客户端请求已经处理完毕!>
<准备接收第2个客户端请求!
正在处理第2个客户端请求
第2个客户端请求已经处理完毕!>
<准备接收第3个客户端请求!
<准备接收第1个客户端请求!
正在处理第1个客户端请求
第1个客户端请求已经处理完毕!>
<准备接收第2个客户端请求!
正在处理第2个客户端请求
第2个客户端请求已经处理完毕!>
<准备接收第3个客户端请求!
从第二步的运行结果可以看出,当TestRequestQueue创建两个Socket连接之后,服务端的请求队列已满,并且服务端暂时无法继续执行(由于System.in.read()的原因而暂停程序的执行,等待用户的输入)。因此,服务端程序无法再接收客户端请求。这时TestRequestQueue抛出了一个SocketException异常。在TestRequestQueue已经创建成功的两个Socket连接已经保存在服务端的请求队列中。在这时按任意键继续执行SetRequestQueue。accept方法就会从请求队列中将这两个客户端请求队列中依次读出来。从第三步的运行结果可以看出,服务端处理完这两个请求后(一个<…>包含的就是一个处理过程),请求队列为空,这时accept处理阻塞状态,等待接收第三个客户端请求。如果这时再运行TestRequestQueue,服务端会接收几个客户端请求呢?如果将请求队列的长度设为大于10的数,TestRequestQueue的运行结果会是什么呢?读者可以自己做一下这些实验,看看和自己认为的结果是否一致。
三、绑定IP地址
三、绑定IP地址
在有多个网络接口或多个IP地址的计算机上可以使用如下的构造方法将服务端绑定在某一个IP地址上:
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
bindAddr参数就是要绑定的IP地址。如果将服务端绑定到某一个IP地址上,就只有可以访问这个IP地址的客户端才能连接到服务器上。如一台机器上有两块网卡,一块网卡连接内网,另一块连接外网。如果用Java实现一个Email服务器,并且只想让内网的用户使用它。就可以使用这个构造方法将ServerSocket对象绑定到连接内网的IP地址上。这样外网就无法访问Email服务器了。可以使用如下代码来绑定IP地址:
ServerSocket serverSocket = new
ServerSocket(1234, 0, InetAddress.getByName("192.168.18.10"));
ServerSocket(1234, 0, InetAddress.getByName("192.168.18.10"));
上面的代码将IP地址绑定到了192.168.18.10上,因此,服务端程序只能使用绑定了这个IP地址的网络接口进行通讯。
四、默认构造方法的使用
四、默认构造方法的使用
除了使用ServerSocket类的构造方法绑定端口外,还可以用ServerSocket的bind方法来完成构造方法所做的工作。要想使用bind方法,必须得用ServerSocket类的默认构造方法(没有参数的构造方法)来创建ServerSocket对象。bind方法有两个重载形式,它们的定义如下:
public void bind(SocketAddress endpoint) throws IOException
public void bind(SocketAddress endpoint, int backlog) throws IOException
public void bind(SocketAddress endpoint, int backlog) throws IOException
bind方法不仅可以绑定端口,也可以设置请求队列的长度以及绑定IP地址。bind方法的作用是为了在建立ServerSocket对象后设置ServerSocket类的一些选项。而这些选项必须在绑定端口之前设置,一但绑定了端口后,再设置这些选项将不再起作用。下面的代码演示了bind方法的使用及如何设置ServerSocket类的选项。
ServerSocket serverSocket1 = new ServerSocket();
serverSocket1.setReuseAddress(true);
serverSocket1.bind(new InetSocketAddress(1234));
ServerSocket serverSocket2 = new ServerSocket();
serverSocket2.setReuseAddress(true);
serverSocket2.bind(new InetSocketAddress("192.168.18.10", 1234));
ServerSocket serverSocket3 = new ServerSocket();
serverSocket3.setReuseAddress(true);
serverSocket3.bind(new InetSocketAddress("192.168.18.10", 1234), 30);
serverSocket1.setReuseAddress(true);
serverSocket1.bind(new InetSocketAddress(1234));
ServerSocket serverSocket2 = new ServerSocket();
serverSocket2.setReuseAddress(true);
serverSocket2.bind(new InetSocketAddress("192.168.18.10", 1234));
ServerSocket serverSocket3 = new ServerSocket();
serverSocket3.setReuseAddress(true);
serverSocket3.bind(new InetSocketAddress("192.168.18.10", 1234), 30);
在上面的代码中设置了SO_REUSEADDR 选项(这个选项将在后面的文章中详细讨论)。如果使用下面的代码,这个选项将不起作用。
ServerSocket serverSocket3 = new ServerSocket(1234);
serverSocket3.setReuseAddress(true);
serverSocket3.setReuseAddress(true);
在第6行绑定了IP地址和端口。使用构造方法是无法得到这个组合的(想绑定IP地址,必须得设置backlog参数),因此,bind方法比构造方法更灵活。
本文转自 androidguy 51CTO博客,原文链接:http://blog.51cto.com/androidguy/214408,如需转载请自行联系原作者