分享

ServerSocket用法详解

 QomoIT 2020-01-20

在客户/服务器通信模式中,服务器端需要创建监听特定端口的ServerSocket,ServerSocket负责接收客户连接请求。本章首先介绍ServerSocket类的各个构造方法,以及成员方法的用法,接着介绍服务器如何用多线程来处理与多个客户的通信任务。

本章提供线程池的一种实现方式。线程池包括一个工作队列和若干工作线程。服务器程序向工作队列中加入与客户通信的任务,工作线程不断从工作队列中取出任务并执行它。本章还介绍了java.util.concurrent包中的线程池类的用法,在服务器程序中可以直接使用它们。

3.1  构造ServerSocket

ServerSocket的构造方法有以下几种重载形式:

◆ServerSocket()throws IOException 
◆ServerSocket(int port) throws IOException 
◆ServerSocket(int port, int backlog) throws IOException
◆ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException   

在以上构造方法中,参数port指定服务器要绑定的端口(服务器要监听的端口),参数backlog指定客户连接请求队列的长度,参数bindAddr指定服务器要绑定的IP地址。

3.1.1  绑定端口

除了第一个不带参数的构造方法以外,其他构造方法都会使服务器与特定端口绑定,该端口由参数port指定。例如,以下代码创建了一个与80端口绑定的服务器:

ServerSocket serverSocket=new ServerSocket(80);
◆端口已经被其他服务器进程占用;如果运行时无法绑定到80端口,以上代码会抛出IOException,更确切地说,是抛出BindException,它是IOException的子类。BindException一般是由以下原因造成的:

◆在某些操作系统中,如果没有以超级用户的身份来运行服务器程序,那么操作系统不允许服务器绑定到1~1023之间的端口。

如果把参数port设为0,表示由操作系统来为服务器分配一个任意可用的端口。由操作系统分配的端口也称为匿名端口。对于多数服务器,会使用明确的端口,而不会使用匿名端口,因为客户程序需要事先知道服务器的端口,才能方便地访问服务器。在某些场合,匿名端口有着特殊的用途,本章3.4节会对此作介绍。

3.1.2  设定客户连接请求队列的长度

当服务器进程运行时,可能会同时监听到多个客户的连接请求。例如,每当一个客户进程执行以下代码:

Socket socket=new Socket(www.,80);

就意味着在远程www.主机的80端口上,监听到了一个客户的连接请求。管理客户连接请求的任务是由操作系统来完成的。操作系统把这些连接请求存储在一个先进先出的队列中。许多操作系统限定了队列的最大长度,一般为50。当队列中的连接请求达到了队列的最大容量时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过ServerSocket的accept()方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。

对于客户进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户与服务器的连接建立成功,客户进程从Socket构造方法中正常返回。如果客户进程发出的连接请求被服务器拒绝,Socket构造方法就会抛出ConnectionException。

ServerSocket构造方法的backlog参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。值得注意的是,在以下几种情况中,仍然会采用操作系统限定的队列的最大长度:

◆backlog参数的值大于操作系统限定的队列的最大长度;
◆backlog参数的值小于或等于0;
◆在ServerSocket构造方法中没有设置backlog参数。

以下例程3-1的Client.java和例程3-2的Server.java用来演示服务器的连接请求队列的特性。

例程3-1  Client.java

  1. import java.net.*;
  2. public class Client {
  3. public static void main(String args[])throws Exception{
  4. final int length=100;
  5. String host="localhost";
  6. int port=8000;
  7. Socket[] sockets=new Socket[length];
  8. for(int i=0;i<length;i++){ // 试图建立100次连接
  9. sockets[i]=new Socket(host, port);
  10. System.out.println("第"+(i+1)+"次连接成功");
  11. }
  12. Thread.sleep(3000);
  13. for(int i=0;i<length;i++){
  14. sockets[i].close(); //断开连接
  15. }
  16. }
  17. }

  1. import java.io.*;
  2. import java.net.*;
  3. public class Server {
  4. private int port=8000;
  5. private ServerSocket serverSocket;
  6. public Server() throws IOException {
  7. serverSocket = new ServerSocket(port,3); //连接请求队列的长度为3
  8. System.out.println("服务器启动");
  9. }
  10. public void service() {
  11. while (true) {
  12. Socket socket=null;
  13. try {
  14. socket = serverSocket.accept(); //从连接请求队列中取出一个连接
  15. System.out.println("New connection accepted " +
  16. socket.getInetAddress() + ":" +socket.getPort());
  17. }catch (IOException e) {
  18. e.printStackTrace();
  19. }finally {
  20. try{
  21. if(socket!=null)socket.close();
  22. }catch (IOException e) {e.printStackTrace();}
  23. }
  24. }
  25. }
  26. public static void main(String args[])throws Exception {
  27. Server server=new Server();
  28. Thread.sleep(60000*10); //睡眠10分钟
  29. //server.service();
  30. }
  31. }

例程3-2  Server.java

Client试图与Server进行100次连接。在Server类中,把连接请求队列的长度设为3。这意味着当队列中有了3个连接请求时,如果Client再请求连接,就会被Server拒绝。下面按照以下步骤运行Server和Client程序。

(1)把Server类的main()方法中的“server.service();”这行程序代码注释掉。这使得服务器与8000端口绑定后,永远不会执行serverSocket.accept()方法。这意味着队列中的连接请求永远不会被取出。先运行Server程序,然后再运行Client程序,Client程序的打印结果如下:

  1. 第1次连接成功
  2. 第2次连接成功
  3. 第3次连接成功
  4. Exception in thread "main" java.net.ConnectException: Connection refused: connect
  5. at java.net.PlainSocketImpl.socketConnect(Native Method)
  6. at java.net.PlainSocketImpl.doConnect(Unknown Source)
  7. at java.net.PlainSocketImpl.connectToAddress(Unknown Source)
  8. at java.net.PlainSocketImpl.connect(Unknown Source)
  9. at java.net.SocksSocketImpl.connect(Unknown Source)
  10. at java.net.Socket.connect(Unknown Source)
  11. at java.net.Socket.connect(Unknown Source)
  12. at java.net.Socket.(Unknown Source)
  13. at java.net.Socket.(Unknown Source)
  14. at Client.main(Client.java:10)
(2)把Server类的main()方法按如下方式修改:从以上打印结果可以看出,Client与Server在成功地建立了3个连接后,就无法再创建其余的连接了,因为服务器的队列已经满了。
  1. public static void main(String args[])throws Exception {
  2. Server server=new Server();
  3. //Thread.sleep(60000*10); //睡眠10分钟
  4. server.service();
  5. }

作了以上修改,服务器与8 000端口绑定后,就会在一个while循环中不断执行serverSocket.accept()方法,该方法从队列中取出连接请求,使得队列能及时腾出空位,以容纳新的连接请求。先运行Server程序,然后再运行Client程序,Client程序的打印结果如下:

  1. 第1次连接成功
  2. 第2次连接成功
  3. 第3次连接成功
  4. 第100次连接成功

从以上打印结果可以看出,此时Client能顺利与Server建立100次连接。

3.1.3  设定绑定的IP地址

如果主机只有一个IP地址,那么默认情况下,服务器程序就与该IP地址绑定。ServerSocket的第4个构造方法ServerSocket(int port, int backlog, InetAddress bindAddr)有一个bindAddr参数,它显式指定服务器要绑定的IP地址,该构造方法适用于具有多个IP地址的主机。假定一个主机有两个网卡,一个网卡用于连接到Internet, IP地址为222.67.5.94,还有一个网卡用于连接到本地局域网,IP地址为192.168.3.4。如果服务器仅仅被本地局域网中的客户访问,那么可以按如下方式创建ServerSocket:

ServerSocket serverSocket=new ServerSocket(8000,10,InetAddress.getByName ("192.168.3.4"));

3.1.4  默认构造方法的作用

ServerSocket有一个不带参数的默认构造方法。通过该方法创建的ServerSocket不与任何端口绑定,接下来还需要通过bind()方法与特定端口绑定。

这个默认构造方法的用途是,允许服务器在绑定到特定端口之前,先设置ServerSocket的一些选项。因为一旦服务器与特定端口绑定,有些选项就不能再改变了。

在以下代码中,先把ServerSocket的SO_REUSEADDR选项设为true,然后再把它与8000端口绑定:

  1. ServerSocket serverSocket=new ServerSocket();
  2. serverSocket.setReuseAddress(true); //设置ServerSocket的选项
  3. serverSocket.bind(new InetSocketAddress(8000)); //与8000端口绑定

如果把以上程序代码改为:

  1. ServerSocket serverSocket=new ServerSocket(8000);
  2. serverSocket.setReuseAddress(true); //设置ServerSocket的选项

那么serverSocket.setReuseAddress(true)方法就不起任何作用了,因为SO_ REUSEADDR选项必须在服务器绑定端口之前设置才有效。

3.2  接收和关闭与客户的连接

ServerSocket的accept()方法从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的Socket对象,并将它返回。如果队列中没有连接请求,accept()方法就会一直等待,直到接收到了连接请求才返回。

接下来,服务器从Socket对象中获得输入流和输出流,就能与客户交换数据。当服务器正在进行发送数据的操作时,如果客户端断开了连接,那么服务器端会抛出一个IOException的子类SocketException异常:

java.net.SocketException: Connection reset by peer

这只是服务器与单个客户通信中出现的异常,这种异常应该被捕获,使得服务器能继续与其他客户通信。

以下程序显示了单线程服务器采用的通信流程:

  1. public void service() {
  2. while (true) {
  3. Socket socket=null;
  4. try {
  5. socket = serverSocket.accept(); //从连接请求队列中取出一个连接
  6. System.out.println("New connection accepted " +
  7. socket.getInetAddress() + ":" +socket.getPort());
  8. //接收和发送数据
  9. }catch (IOException e) {
  10. //这只是与单个客户通信时遇到的异常,可能是由于客户端过早断开连接引起的
  11. //这种异常不应该中断整个while循环
  12. e.printStackTrace();
  13. }finally {
  14. try{
  15. if(socket!=null)socket.close(); //与一个客户通信结束后,要关闭Socket
  16. }catch (IOException e) {e.printStackTrace();}
  17. }
  18. }
  19. }

与单个客户通信的代码放在一个try代码块中,如果遇到异常,该异常被catch代码块捕获。try代码块后面还有一个finally代码块,它保证不管与客户通信正常结束还是异常结束,最后都会关闭Socket,断开与这个客户的连接。

3.3  关闭ServerSocket

ServerSocket的close()方法使服务器释放占用的端口,并且断开与所有客户的连接。当一个服务器程序运行结束时,即使没有执行ServerSocket的close()方法,操作系统也会释放这个服务器占用的端口。因此,服务器程序并不一定要在结束之前执行ServerSocket的close()方法。

在某些情况下,如果希望及时释放服务器的端口,以便让其他程序能占用该端口,则可以显式调用ServerSocket的close()方法。例如,以下代码用于扫描1~65535之间的端口号。如果ServerSocket成功创建,意味着该端口未被其他服务器进程绑定,否者说明该端口已经被其他进程占用:

  1. for(int port=1;port<=65535;port++){
  2. try{
  3. ServerSocket serverSocket=new ServerSocket(port);
  4. serverSocket.close(); //及时关闭ServerSocket
  5. }catch(IOException e){
  6. System.out.println("端口"+port+" 已经被其他服务器进程占用");
  7. }
  8. }

以上程序代码创建了一个ServerSocket对象后,就马上关闭它,以便及时释放它占用的端口,从而避免程序临时占用系统的大多数端口。

ServerSocket的isClosed()方法判断ServerSocket是否关闭,只有执行了ServerSocket的close()方法,isClosed()方法才返回true;否则,即使ServerSocket还没有和特定端口绑定,isClosed()方法也会返回false。

ServerSocket的isBound()方法判断ServerSocket是否已经与一个端口绑定,只要ServerSocket已经与一个端口绑定,即使它已经被关闭,isBound()方法也会返回true。

如果需要确定一个ServerSocket已经与特定端口绑定,并且还没有被关闭,则可以采用以下方式:

boolean isOpen=serverSocket.isBound() && !serverSocket.isClosed();

3.4  获取ServerSocket的信息

ServerSocket的以下两个get方法可分别获得服务器绑定的IP地址,以及绑定的端口:

◆public InetAddress getInetAddress()
◆public int getLocalPort()

前面已经讲到,在构造ServerSocket时,如果把端口设为0,那么将由操作系统为服务器分配一个端口(称为匿名端口),程序只要调用getLocalPort()方法就能获知这个端口号。如例程3-3所示的RandomPort创建了一个ServerSocket,它使用的就是匿名端口。

#p#

例程3-3  RandomPort.java

  1. import java.io.*;
  2. import java.net.*;
  3. public class RandomPort{
  4. public static void main(String args[])throws IOException{
  5. ServerSocket serverSocket=new ServerSocket(0);
  6. System.out.println("监听的端口为:"+serverSocket.getLocalPort());
  7. }
  8. }

多次运行RandomPort程序,可能会得到如下运行结果:

  1. C:\chapter03\classes>java RandomPort
  2. 监听的端口为:3000
  3. C:\chapter03\classes>java RandomPort
  4. 监听的端口为:3004
  5. C:\chapter03\classes>java RandomPort
  6. 监听的端口为:3005

多数服务器会监听固定的端口,这样才便于客户程序访问服务器。匿名端口一般适用于服务器与客户之间的临时通信,通信结束,就断开连接,并且ServerSocket占用的临时端口也被释放。

FTP(文件传输)协议就使用了匿名端口。如图3-1所示,FTP协议用于在本地文件系统与远程文件系统之间传送文件。

 

图3-1  FTP协议用于在本地文件系统与远程文件系统之间传送文件

FTP使用两个并行的TCP连接:一个是控制连接,一个是数据连接。控制连接用于在客户和服务器之间发送控制信息,如用户名和口令、改变远程目录的命令或上传和下载文件的命令。数据连接用于传送文件。TCP服务器在21端口上监听控制连接,如果有客户要求上传或下载文件,就另外建立一个数据连接,通过它来传送文件。数据连接的建立有两种方式。

(1)如图3-2所示,TCP服务器在20端口上监听数据连接,TCP客户主动请求建立与该端口的连接。

 

图3-2  TCP服务器在20端口上监听数据连接

(2)如图3-3所示,首先由TCP客户创建一个监听匿名端口的ServerSocket,再把这个ServerSocket监听的端口号(调用ServerSocket的getLocalPort()方法就能得到端口号)发送给TCP服务器,然后由TCP服务器主动请求建立与客户端的连接。

 

图3-3  TCP客户在匿名端口上监听数据连接

以上第二种方式就使用了匿名端口,并且是在客户端使用的,用于和服务器建立临时的数据连接。在实际应用中,在服务器端也可以使用匿名端口。

3.5  ServerSocket选项

ServerSocket有以下3个选项。

◆SO_TIMEOUT:表示等待客户连接的超时时间。
◆SO_REUSEADDR:表示是否允许重用服务器所绑定的地址。
◆SO_RCVBUF:表示接收数据的缓冲区的大小。

3.5.1  SO_TIMEOUT选项

◆设置该选项:public void setSoTimeout(int timeout) throws SocketException
◆读取该选项:public int getSoTimeout () throws IOException

SO_TIMEOUT表示ServerSocket的accept()方法等待客户连接的超时时间,以毫秒为单位。如果SO_TIMEOUT的值为0,表示永远不会超时,这是SO_TIMEOUT的默认值。

当服务器执行ServerSocket的accept()方法时,如果连接请求队列为空,服务器就会一直等待,直到接收到了客户连接才从accept()方法返回。如果设定了超时时间,那么当服务器等待的时间超过了超时时间,就会抛出SocketTimeoutException,它是InterruptedException的子类。

如例程3-4所示的TimeoutTester把超时时间设为6秒钟。

#p#

例程3-4  TimeoutTester.java

  1. import java.io.*;
  2. import java.net.*;
  3. public class TimeoutTester{
  4. public static void main(String args[])throws IOException{
  5. ServerSocket serverSocket=new ServerSocket(8000);
  6. serverSocket.setSoTimeout(6000); //等待客户连接的时间不超过6秒
  7. Socket socket=serverSocket.accept();
  8. socket.close();
  9. System.out.println("服务器关闭");
  10. }
  11. }

运行以上程序,过6秒钟后,程序会从serverSocket.accept()方法中抛出Socket- TimeoutException:

  1. C:\chapter03\classes>java TimeoutTester
  2. Exception in thread "main" java.net.SocketTimeoutException: Accept timed out
  3. at java.net.PlainSocketImpl.socketAccept(Native Method)
  4. at java.net.PlainSocketImpl.accept(Unknown Source)
  5. at java.net.ServerSocket.implAccept(Unknown Source)
  6. at java.net.ServerSocket.accept(Unknown Source)
  7. at TimeoutTester.main(TimeoutTester.java:8)

如果把程序中的“serverSocket.setSoTimeout(6000)”注释掉,那么serverSocket. accept()方法永远不会超时,它会一直等待下去,直到接收到了客户的连接,才会从accept()方法返回。

Tips:服务器执行serverSocket.accept()方法时,等待客户连接的过程也称为阻塞。本书第4章的4.1节(线程阻塞的概念)详细介绍了阻塞的概念。

3.5.2  SO_REUSEADDR选项

◆设置该选项:public void setResuseAddress(boolean on) throws SocketException
◆读取该选项:public boolean getResuseAddress() throws SocketException

这个选项与Socket的SO_REUSEADDR选项相同,用于决定如果网络上仍然有数据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口。

当ServerSocket关闭时,如果网络上还有发送到这个ServerSocket的数据,这个ServerSocket不会立刻释放本地端口,而是会等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。

许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,使得服务器程序无法绑定到该端口,服务器启动失败,并抛出BindException:

Exception in thread "main" java.net.BindException: Address already in use: JVM_Bind

为了确保一个进程关闭了ServerSocket后,即使操作系统还没释放端口,同一个主机上的其他进程还可以立刻重用该端口,可以调用ServerSocket的setResuse- Address(true)方法:

if(!serverSocket.getResuseAddress())serverSocket.setResuseAddress(true);

值得注意的是,serverSocket.setResuseAddress(true)方法必须在ServerSocket还没有绑定到一个本地端口之前调用,否则执行serverSocket.setResuseAddress(true)方法无效。此外,两个共用同一个端口的进程必须都调用serverSocket.setResuseAddress(true)方法,才能使得一个进程关闭ServerSocket后,另一个进程的ServerSocket还能够立刻重用相同端口。

3.5.3  SO_RCVBUF选项

◆设置该选项:public void setReceiveBufferSize(int size) throws SocketException
◆读取该选项:public int getReceiveBufferSize() throws SocketException

SO_RCVBUF表示服务器端的用于接收数据的缓冲区的大小,以字节为单位。一般说来,传输大的连续的数据块(基于HTTP或FTP协议的数据传输)可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信(Telnet和网络游戏),则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。

SO_RCVBUF的默认值与操作系统有关。例如,在Windows 2000中运行以下代码时,显示SO_RCVBUF的默认值为8192:

  1. ServerSocket serverSocket=new ServerSocket(8000);
  2. System.out.println(serverSocket.getReceiveBufferSize()); //打印8192

无论在ServerSocket绑定到特定端口之前或之后,调用setReceiveBufferSize()方法都有效。例外情况下是如果要设置大于64K的缓冲区,则必须在ServerSocket绑定到特定端口之前进行设置才有效。例如,以下代码把缓冲区设为128K:

  1. ServerSocket serverSocket=new ServerSocket();
  2. int size=serverSocket.getReceiveBufferSize();
  3. if(size<131072) serverSocket.setReceiveBufferSize(131072); //把缓冲区的大小设为128K
  4. serverSocket.bind(new InetSocketAddress(8000)); //与8000端口绑定
3.5.4  设定连接时间、延迟和带宽的相对重要性执行serverSocket.setReceiveBufferSize()方法,相当于对所有由serverSocket.accept()方法返回的Socket设置接收数据的缓冲区的大小。

◆public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)

该方法的作用与Socket的setPerformancePreferences()方法的作用相同,用于设定连接时间、延迟和带宽的相对重要性。

转载自http://www.51cto.com/specbook/11/40196.htm

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多