Java网络编程从入门到精通(25):创建ServerSocket对象

简介:
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  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已经打开!
二、 设置请求队列的长度
在编写服务端程序时,一般会通过多线程来同时处理多个客户端请求。也就是说,使用一个线程来接收客户端请求,当接到一个请求后(得到一个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  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. 执行如下命令(在执行这条命令后,先不要按回车键):
java server.SetRequestQueue  2

   运行结果:
端口(1234) 已经绑定,请按回车键开始处理客户端请求!
    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 )

    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 方法比构造方法更灵活。






 本文转自 androidguy 51CTO博客,原文链接: http://blog.51cto.com/androidguy/214408 ,如需转载请自行联系原作者
相关文章
|
5月前
|
存储 Oracle Java
java零基础学习者入门课程
本课程为Java零基础入门教程,涵盖环境搭建、变量、运算符、条件循环、数组及面向对象基础,每讲配示例代码与实践建议,助你循序渐进掌握核心知识,轻松迈入Java编程世界。
490 0
|
5月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
277 1
|
5月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
296 1
|
6月前
|
Java
java入门代码示例
本文介绍Java入门基础,包含Hello World、变量类型、条件判断、循环及方法定义等核心语法示例,帮助初学者快速掌握Java编程基本结构与逻辑。
519 0
|
6月前
|
Java API 数据库
2025 年最新 Java 实操学习路线,从入门到高级应用详细指南
2025年Java最新实操学习路线,涵盖从环境搭建到微服务、容器化部署的全流程实战内容,助你掌握Java 21核心特性、Spring Boot 3.2开发、云原生与微服务架构,提升企业级项目开发能力,适合从入门到高级应用的学习需求。
2077 0
|
6月前
|
前端开发 Java 数据库连接
帮助新手快速上手的 JAVA 学习路线最详细版涵盖从入门到进阶的 JAVA 学习路线
本Java学习路线涵盖从基础语法、面向对象、异常处理到高级框架、微服务、JVM调优等内容,适合新手入门到进阶,助力掌握企业级开发技能,快速成为合格Java开发者。
833 3
|
6月前
|
监控 Java API
2025 年全新出炉的 Java 学习路线:从入门起步到实操精通的详细指南
2025年Java学习路线与实操指南,涵盖Java 21核心特性、虚拟线程、Spring Boot 3、微服务、Spring Security、容器化部署等前沿技术,助你从入门到企业级开发进阶。
1418 0
|
7月前
|
NoSQL Java 关系型数据库
Java 从入门到进阶完整学习路线图规划与实战开发最佳实践指南
本文为Java开发者提供从入门到进阶的完整学习路线图,涵盖基础语法、面向对象、数据结构与算法、并发编程、JVM调优、主流框架(如Spring Boot)、数据库操作(MySQL、Redis)、微服务架构及云原生开发等内容,并结合实战案例与最佳实践,助力高效掌握Java核心技术。
817 1
|
7月前
|
JSON 移动开发 网络协议
Java网络编程:Socket通信与HTTP客户端
本文全面讲解Java网络编程,涵盖TCP与UDP协议区别、Socket编程、HTTP客户端开发及实战案例,助你掌握实时通信、文件传输、聊天应用等场景,附性能优化与面试高频问题解析。
|
7月前
|
Java 测试技术 API
Java IO流(二):文件操作与NIO入门
本文详解Java NIO与传统IO的区别与优势,涵盖Path、Files类、Channel、Buffer、Selector等核心概念,深入讲解文件操作、目录遍历、NIO实战及性能优化技巧,适合处理大文件与高并发场景,助力高效IO编程与面试准备。

热门文章

最新文章