zoukankan      html  css  js  c++  java
  • IO模型之BIO代码详解及其优化演进

    一、BIO简介

      BIO是java1.4之前唯一的IO逻辑,在客户端通过socket向服务端传输数据,服务端监听端口。由于传统IO读数据的时候如果数据没有传达,IO会一直等待输入传入,所以当有请求过来的时候,新起一条线程对数据进行等待、处理,导致每一个链接都对应着服务器的一个线程。

      BIO是同步阻塞的,如图所示:

              

    二、原理简述

      BIO对应linux io模型的阻塞IO,服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。

      重要组件
        ServerSocket:负责绑定IP地址,启动监听端口
        Socket:负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

    三、手写服务端

      首先我们来用ServerSocket和Socket写个服务端,并进行测试,代码以及注释如下:

    public class BioServerSingle {  //blocking
        public static void main(String[] args) {
            // 服务端开启一个端口进行监听
            int port = 8080;
            ServerSocket serverSocket = null;   //服务端
            Socket socket;  //客户端
            InputStream in = null;
            OutputStream out = null;
            try {
    
                serverSocket = new ServerSocket(port);  //通过构造函数创建ServerSocket,指定监听端口,如果端口合法且空闲,服务器就会监听成功
                // 通过无限循环监听客户端连接,如果没有客户端接入,则会阻塞在accept操作
                while (true) {
                    System.out.println("Waiting for a new Socket to establish" + " ," + new Date().toString());
                    socket = serverSocket.accept();//阻塞  三次握手
                    in = socket.getInputStream();
                    byte[] buffer = new byte[1024];
                    int length = 0;
                    while ((length = in.read(buffer)) > 0) {//阻塞
                        System.out.println("input is:" + new String(buffer, 0, length) + " ," + new Date().toString());
                        out = socket.getOutputStream();
                        out.write("success".getBytes());
                        System.out.println("Server end" + " ," + new Date().toString());
                    }
    
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 必要的清理活动
                if (serverSocket != null) {
                    try {
                        serverSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

      执行代码main方法启动服务端,并查看堆栈信息,可以看到服务端阻塞在ServerSocket的accept方法,如截图:

            

       此处即是ServerSocket的第一个阻塞点,接下来在控制台执行命令:

    telnet localhost 8080

      打印此时堆栈信息可以看到ServerSocket阻塞在read方法,如截图所示:

            

      如果此时在控制台输入一些内容,那么服务端将接收到控制台输入,并会成功将服务端想要输出的的内容返回给控制台,如图所示:

            

       如图所示,这样就完成了一次socket的连接与数据传输,但是这样的Socket并未关闭,你仍旧可以继续在控制台输入内容,然后服务端接收输入,因为这一次的Socket并未关闭,从哪里可以看出来呢?

          

      如图所示:执行的代码并未重新等待一次新的Socket客户端请求,而且原先的Socket客户端(即控制台)仍旧可以继续同服务端进行输入输出交互,同时也可以打印堆栈信息进行确认,此时依旧阻塞在read方法而不是accept方法,此处不再截图。如果此时关闭客户端Socket(即控制台),那么此次Socket通信将结束,然后重新阻塞在Accept继续监听端口,等待新的客户端连接的到来。

      因此可以确认ServerSocket两个阻塞点,分别是accept、read,accept阻塞等待新的socket通信建立请求,read阻塞等待客户端输入。

    四、手写客户端

    public class BioClient {
    
        public static void main(String[] args) {
            Socket clientSocket = null;
            OutputStream outputStream = null;
            InputStream inputStream  = null;
            try{
                // 新建一个Socket请求
                clientSocket = new Socket("localhost",8080);
                System.out.println("Build the connection successfully!"+" ,"+new Date().toString());
                outputStream = clientSocket.getOutputStream();
                inputStream = clientSocket.getInputStream();
                byte[] buffer = new byte[1024];
                Scanner scanner = new Scanner(System.in);
                while(true){
                    System.out.println("Please input a String !"+" ,"+new Date().toString());
                    String string = scanner.nextLine();
                    if("end".equals(string)){
                        System.out.println("Client end"+" ,"+new Date().toString());
                        break;
                    }
                    outputStream.write(("This is a new request: "+string).getBytes());
                    int length = 0;
                    if ((length = inputStream.read(buffer)) > 0) {//阻塞
                    }
                    System.out.println("The response is:" + new String(buffer, 0, length)+" ,"+new Date().toString());
                }
    
            }catch (Exception e){
    
            } finally {
                if (clientSocket != null) {
                    try {
                        clientSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (outputStream != null) {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    }

      启动main方法,如下截图(客户端和服务端):

              

               

      客户端可以一直接受请求,直到输入end结束此次客户端输入,那么服务端能一直接受请求,然后给出答复,当客户端断开请求后,服务端重新恢复监听,等待新的socket请求的到来。

      因此,在BIO通信模型中,Socket通信的发起以及结束都是客户端进行的。

    五、多Socket客户端请求处理

      上面进行的是1对1的Socket通信建立,如果已经有一个Socket请求在处理的时候,此时再来一个socket请求,那么会怎样呢,我们通过两个控制台简历请求进行测试。

             

       如截图所示,客户端1和客户端建立Socket请求的通信并未报错,客户端①可以正常通信,但是客户端②的请求并未得到响应。

      我们因此可以知道BIO是一对一应答模型,一个Socekt只能支持一个通信,如果一个客户端没有断开,当前线程会阻塞在读操作。要想实现同时处理多个通信,那么就必须建立多个Socket,因此此时服务端可以通过多线程,在socket请求到来时,单独为通信建立一个Socket进行通信,代码如下:

    public class BioServerThread {
        public static void main(String[] args) {
            int port=8080;
            ServerSocket serverSocket=null;
            try{
    
                serverSocket=new ServerSocket(port);
                Socket socket=null;
                while (true){
                    socket=serverSocket.accept();//拿到socket
                    //连接量大的时候 会拖垮cpu
                    new Thread(new SocketHandler(socket)).start();
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if (serverSocket!=null){
                    try {
                        serverSocket.close();
                        serverSocket=null;
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public class SocketHandler implements Runnable {
        private Socket socket;
    
        public SocketHandler(Socket socket) {
            this.socket=socket;
        }
    
        public void run() {
            InputStream in = null;
            OutputStream out = null;
            try {
                in = socket.getInputStream();
                byte[] buffer = new byte[1024];
                int length = 0;
                while ((length = in.read(buffer)) > 0) {//阻塞
                    System.out.println("input is:" + new String(buffer, 0, length));
                    out = socket.getOutputStream();
                    out.write("success".getBytes());
                    System.out.println("end");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (out != null) {
                    try {
                        out.close();
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
                if (socket!=null){
                    try {
                        socket.close();
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

      此时用两个控制台建立两个socket请求进行测试:

              

      如图所示,客户端①和客户端②同时得到了服务端的返回,服务端成功完成了多请求的处理。

    六、线程池管理Socket

      虽然为了解决该问题可以引入多线程,实现伪异步IO,但是因为处理线程和客户端是1:1的关系,随着客户端请求增大,线程数随着上升,会极大的消耗cpu资源,引起服务器异常。为了保障服务器资源可以实现线程池,如果发生读取数据较慢时,大量并发的情况下,其他接入的客户都只能一直等待。

      代码如下:

    public class ServerHandlerExcutePool {
        private ExecutorService executor;
    
        public ServerHandlerExcutePool(int maxPoolSize, int queueSize){
            executor=new ThreadPoolExecutor(2, maxPoolSize, 120L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));
        }
    
        public void execute(Runnable task){
            executor.execute(task);
        }
    }
    public class BioServerThreadPool {
        public static void main(String[] args) {
            int port=8080;
            ServerSocket serverSocket=null;
            try{
                //1000000
                serverSocket=new ServerSocket(port);
                Socket socket=null;
                //100000    100000
                ServerHandlerExcutePool excutePool=new ServerHandlerExcutePool(2,100);
                while (true){
                    socket=serverSocket.accept();
                    excutePool.execute(new SocketHandler(socket));
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if (serverSocket!=null){
                    try {
                        serverSocket.close();
                        serverSocket=null;
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

      如截图所示,我设置线程池最多处理两个请求,那么此时第三个客户端的连接依旧无法处理,这样避免了创建大量的线程创建新的Socket,多余线程池大容量的请求只能阻塞。

            

       综上所述,BIO的特点是缺乏弹性伸缩能力,在高并发这种大量请求来临时,并不具备强大的处理能力。

  • 相关阅读:
    Java实现 蓝桥杯VIP 算法提高 P0404
    Java实现 蓝桥杯VIP 算法提高 P0404
    Java实现 蓝桥杯VIP 算法提高 P0404
    Java实现 蓝桥杯VIP 算法提高 P0404
    Java实现 蓝桥杯VIP 算法提高 P0404
    Java实现 蓝桥杯VIP 算法训练 排列问题
    Java实现 蓝桥杯VIP 算法训练 排列问题
    Java实现 蓝桥杯VIP 算法训练 排列问题
    Java实现 蓝桥杯VIP 算法训练 排列问题
    关于模态/非模态对话框不响应菜单的UPDATE_COMMAND_UI消息(对对WM_INITMENUPOPUP消息的处理)
  • 原文地址:https://www.cnblogs.com/jing99/p/11993041.html
Copyright © 2011-2022 走看看