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

简介: 本文为原创,如需转载,请注明作者和出处,谢谢! 上一篇:Java网络编程从入门到精通(24):实现HTTP断点续传下载工具(附源代码) 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

    在上面的构造方法中涉及到了三个参数:portbacklogbindAddr。其中portServerSocket对象要绑定的端口,backlog是请求队列的长度,bindAddrServerSocket对象要绑定的IP地址。

一、通过构造方法绑定端口

通过构造方法绑定端口是创建ServerSocket对象最常用的方式。可以通过如下的构造方法来绑定端口:

public  ServerSocket( int  port)  throws  IOException

如果port参数所指定的端口已经被绑定,构造方法就会抛出IOException异常。但实际上抛出的异常是BindException。从图4.2异常类继承关系图可以看出,所有和网络有关的异常都是IOException类的子类。因此,为了ServerSocket构造方法还可以抛出其他的异常,就使用了IOException

如果port的值为0,系统就会随机选取一个端口号。但随机选取的端口意义不大,因为客户端在连接服务器时需要明确知道服务端程序的端口号。可以通过ServerSockettoString方法输出和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永远是0localportServerSocket绑定的端口,如果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连接已经保存在服务端的请求队列中。在这时按任意键继续执行SetRequestQueueaccept方法就会从请求队列中将这两个客户端请求队列中依次读出来。从第三步的运行结果可以看出,服务端处理完这两个请求后(一个<…>包含的就是一个处理过程),请求队列为空,这时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类的构造方法绑定端口外,还可以用ServerSocketbind方法来完成构造方法所做的工作。要想使用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方法比构造方法更灵活。

目录
相关文章
|
27天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
160 60
【Java并发】【线程池】带你从0-1入门线程池
|
14天前
|
缓存 安全 Java
【Java并发】【synchronized】适合初学者体质入门的synchronized
欢迎来到我的Java线程同步入门指南!我不是外包员工,梦想是写高端CRUD。2025年我正在沉淀中,博客更新速度加快,欢迎点赞、收藏、关注。 本文介绍Java中的`synchronized`关键字,适合初学者。`synchronized`用于确保多个线程访问共享资源时不会发生冲突,避免竞态条件、保证内存可见性、防止原子性破坏及协调多线程有序访问。
48 8
【Java并发】【synchronized】适合初学者体质入门的synchronized
|
15天前
|
存储 监控 Java
《从头开始学java,一天一个知识点》之:数组入门:一维数组的定义与遍历
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。明日预告:《多维数组与常见操作》。 通过实例讲解数组的核心认知、趣味场景应用、企业级开发规范及优化技巧,帮助你快速掌握Java数组的精髓。
57 23
|
16天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
66 23
|
5天前
|
设计模式 存储 安全
【Java并发】【AQS】适合初学者体质的AQS入门
AQS这是灰常重要的哈,很多JUC下的框架的核心,那都是我们的AQS,所以这里,我们直接开始先研究AQS。 那说到研究AQS,那我们应该,使用开始说起🤓 入门 什么是AQS? AQS(Abst
28 8
|
23天前
|
人工智能 运维 API
云栖大会 | Terraform从入门到实践:快速构建你的第一张业务网络
云栖大会 | Terraform从入门到实践:快速构建你的第一张业务网络
|
2月前
|
自然语言处理 Java
Java中的字符集编码入门-增补字符(转载)
本文探讨Java对Unicode的支持及其发展历程。文章详细解析了Unicode字符集的结构,包括基本多语言面(BMP)和增补字符的表示方法,以及UTF-16编码中surrogate pair的使用。同时介绍了代码点和代码单元的概念,并解释了UTF-8的编码规则及其兼容性。
121 60
|
1月前
|
安全 网络协议 Java
Java网络编程封装
Java网络编程封装原理旨在隐藏底层通信细节,提供简洁、安全的高层接口。通过简化开发、提高安全性和增强可维护性,封装使开发者能更高效地进行网络应用开发。常见的封装层次包括套接字层(如Socket和ServerSocket类),以及更高层次的HTTP请求封装(如RestTemplate)。示例代码展示了如何使用RestTemplate简化HTTP请求的发送与处理,确保代码清晰易维护。
|
3月前
|
JSON Dart 前端开发
鸿蒙应用开发从入门到入行 - 篇7:http网络请求
在本篇文章里,您将掌握鸿蒙开发工具DevEco的基本使用、ArkUI里的基础组件,并通过制作一个简单界面掌握使用
116 8
|
3月前
|
Web App开发 网络协议 安全
网络编程懒人入门(十六):手把手教你使用网络编程抓包神器Wireshark
Wireshark是一款开源和跨平台的抓包工具。它通过调用操作系统底层的API,直接捕获网卡上的数据包,因此捕获的数据包详细、功能强大。但Wireshark本身稍显复杂,本文将以用抓包实例,手把手带你一步步用好Wireshark,并真正理解抓到的数据包的各项含义。
171 2

热门文章

最新文章