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
在上面的构造方法中涉及到了三个参数: 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);
运行结果:
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 . * ; 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 .BindException 端口80已经打开! class .BindException 端口135已经打开!
二、 设置请求队列的长度
在编写服务端程序时,一般会通过多线程来同时处理多个客户端请求。也就是说,使用一个线程来接收客户端请求,当接到一个请求后(得到一个 Socket对象),会创建一个新线程,将这个客户端请求交给这个新线程处理。而那个接收客户端请求的线程则继续接收客户端请求,这个过程的实现代码如下:
ServerSocket serverSocket = new ServerSocket( 1234 ); // 绑定端口 // 处理其他任务的代码 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 . * ; 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. 执行如下命令(在执行这条命令后,先不要按回车键):
java server.SetRequestQueue 2
运行结果:
端口 (1234)已经绑定,请按回车键开始处理客户端请求! 2. 执行如下命令:
java server.TestRequestQueue
运行结果:
已经成功创建第1个客户端连接! 已经成功创建第2个客户端连接! Exception in thread " main " .SocketException: Connection reset by peer: socket write error at .SocketOutputStream.socketWrite0(Native Method) at .SocketOutputStream.socketWrite(SocketOutputStream.java: 92 ) at .SocketOutputStream.write(SocketOutputStream.java: 115 ) at server.TestRequestQueue.main(SetRequestQueue.java: 12 )
3. 按回车键继续执行SetRequestQueue后,运行结果如下:
端口( 1234 )已经绑定,请按回车键开始处理客户端请求! <准备接收第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地址上:
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 " ));
上面的代码将 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
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 );
在上面的代码中 设置了 SO_REUSEADDR 选项(这个选项将在后面的文章中详细讨论)。如果使用下面的代码,这个选项将不起作用。
ServerSocket serverSocket3 = new ServerSocket( 1234 ); serverSocket3.setReuseAddress( true );
在第6 行绑定了 IP地址和端口。使用构造方法是无法得到这个组合的(想绑定 IP地址,必须得设置 backlog参数),因此, bind方法比构造方法更灵活。