【网络编程socket】BIO & Socket和ServerSocket API & 入门例子


相关文章:
【网络编程socket】图解 Java NIO BIO MIO AIO 四大IO模型与原理
【网络编程socket】BIO & Socket和ServerSocket API & 入门例子
【网络编程socket】java NIO编程示例以及流程详解

概述

ServerSocket是基于BIO的

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地址。

1.1 、绑定端口

除不带参数的构造方法以外,其他构造方法都会使服务器与特定端口绑定,该端口由参数port指定。

如果端口被其他服务进程占用,或是,在某些系统中,若没有以超级用户身份运行服务器程序,操作系统不允许服务器绑定到1-1023的端口时,会抛出BindException。

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

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

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

ServerSocket构造方法的backlog参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。

在以下几种情况,仍然采用操作系统限定的队列最大长度:

  • backlog参数的值大于操作系统限定的队列的最大长度;

  • backlog参数的值小于或等于0;
    在这里插入图片描述

  • 在ServerSocket构造方法中没有设置backlog参数。

1.3、设定绑定的IP地址

若主机只有一个地址,则服务器默认绑定该地址;若主机有多个地址,则可以调用ServerSocket(int port, int backlog, InetAddress bindAddr)构造方法设置主机ip地址。

1.4、默认构造方法的作用

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

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

2、接收和关闭与客户的连接

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

服务器从Socket对象中获得输入流和输出流

,就能与客户交换数据。当服务器正在进行发送数据的操作时,如果客户端断开了连接,那么服务器端会抛出一个IOException的子类SocketException异常:java.net.SocketException: Connection reset by peer。

3、关闭ServerSocket

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

在某些情况下,如果希望及时释放服务器的端口,以便让其他程序能占用该端口,则可以显式调用ServerSocket的close()方法。

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

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

4、获取ServerSocket的信息

  • public InetAddress getInetAddress():获取服务器绑定的ip地址;
  • public int getLocalPort():获取服务器绑定的端口;

在构造ServerSocket时,如果把端口设为0,那么将由操作系统为服务器分配一个端口(称为匿名端口),程序只要调用getLocalPort()方法就能获知这个端口号。多数服务器会监听固定的端口,这样才便于客户程序访问服务器。匿名端口一般适用于服务器与客户之间的临时通信,通信结束,就断开连接,并且ServerSocket占用的临时端口也被释放。

5、ServerSocket选项

ServerSocket有以下3个选项。

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

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的子类。

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。

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

5.3、SO_RCVBUF选项

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

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

5.4、设定连接时间、延迟和带宽的相对重要性

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

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

6、创建多线程服务器

许多实际应用要求服务器具有同时为多个客户提供服务的能力。HTTP服务器就是最明显的例子。任何时刻,HTTP服务器都可能接收到大量的客户请求,每个客户都希望能快速得到HTTP服务器的响应。如果长时间让客户等待,会使网站失去信誉,从而降低访问量。

可以用并发性能来衡量一个服务器同时响应多个客户的能力。一个具有好的并发性能的服务器,必须符合两个条件:

  • 能同时接收并处理多个客户连接;
  • 对于每个客户,都会迅速给予响应。
用多个线程来同时为多个客户提供服务,这是提高服务器的并发性能的最常用的手段。

以下将按照3中方式来实现EchoServer,它们都使用多线程。

  • 为每个客户分配一个工作线程。
  • 使用线程池,由其中的工作线程来为客户服务。
    可以利用JDK的Java类库中现成的线程池,由它的工作线程来为客户服务。

6.1、 为每个客户分配一个线程

服务器的主线程负责接收客户的连接,每次接收到一个客户连接,就会创建一个工作线程,由它负责与客户的通信。

public static void start(){
  try{
    ServerSocket serverSocket = new ServerSocket(PORT);
    System.out.println("server listen on port:" + PORT);
    while (true){
      try {
        Socket client = serverSocket.accept();
        System.out.println("receive client connect, localPort=" + client.getPort());
        //每次都new一个新的线程
        new Thread(new EchoServer.HandlerServer(client)).start();
      }catch (Exception e){
        System.out.println("client exception,e=" + e.getMessage());
      }
    }
  }catch(Exception e){
    System.out.println("server exception,e=" + e.getMessage());
  }
}

6.2、使用JDK类库提供的线程池

 static ExecutorService executorServices = Executors.newCachedThreadPool();

7. 完整的入门例子

服务端:

package com.io.bio;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BioServer {

    private static int PORT = 8080;

    private static ServerSocket server;

    static ExecutorService executorServices = Executors.newCachedThreadPool();

    public  static void main(String[] args) throws IOException{
        if(server != null) return;
        try{
            //创建一个服务端,构造方法内自动完成bind,listen操作
            server = new ServerSocket(PORT);
            System.out.println("服务器启动成功,准备接受请求");
            while(true){
                //等待客户端的connector,如果没有新的连接,就一直阻塞
                Socket socket = server.accept();
                //加入线程池,负责处理消息
                executorServices.submit(new ServerHandler(socket));
            }
        }finally{
            if(server != null){
                System.out.println("服务器已关闭。");
                server.close();
                server = null;
            }
        }
    }


}

/**
 * 负责处理消息
 */
class ServerHandler implements Runnable{
    private Socket socket;
    public ServerHandler(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        InputStream in = null;
        OutputStream out = null;
        try{
            byte[] recv = new byte[1024];
            in = socket.getInputStream();
            //注意:这个read方法会阻塞,如果客户端连接过来后,迟迟不发消息过来,那么服务端此次就一直阻塞下去
            in.read(recv);
            System.out.println("服务器收到消息:" + new String(recv));
            out = socket.getOutputStream();
            out.write("hello client ".getBytes());
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            if(in != null){
                try {
                    in.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                in = null;
            }
            if(in != null){
                try {
                    out.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                out = null;
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                socket = null;
            }
        }
    }
}

客户端:

package com.io.bio;

import java.io.*;
import java.net.Socket;

public class BioClient {
   
    private static int SERVER_PORT = 8080;
    private static String SERVER_IP = "127.0.0.1";
    public static void main(String[] args){
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try{
            socket = new Socket(SERVER_IP,SERVER_PORT);
            String hello = "hello server";
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(hello.getBytes());
            outputStream.flush();
            InputStream inputStream = socket.getInputStream();
            byte[] rev = new byte[1024];
            inputStream.read(rev);
            System.out.println("client receive: "+new String(rev));
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            if(in != null){
                try {
                    in.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                in = null;
            }
            if(in != null){
                try {
                    out.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                out = null;
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                socket = null;
            }
        }
    }
}

分别启动服务端和客户端代码:
服务端:
在这里插入图片描述
客户端,收到服务端的响应就关闭了:
在这里插入图片描述





参考:
《Socket和ServerSocket的简单介绍及例子》

《ServerSocket详解》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值