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的特点是缺乏弹性伸缩能力,在高并发这种大量请求来临时,并不具备强大的处理能力。

原文地址:https://www.cnblogs.com/jing99/p/11993041.html

时间: 2024-07-30 10:15:34

IO模型之BIO代码详解及其优化演进的相关文章

IO模型(epoll)--详解-03

写在前面 epoll是开发linux高性能服务器的必备技术至,epoll本质,是服务端程序员的必须掌握的知识. 七.epoll的原理和流程 本节会以示例和图表来讲解epoll的原理和流程. 创建epoll对象 如下图所示,当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中epfd所代表的对象).eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列.内核创建eventpoll对象 创建一个代表该epoll的eventpoll

Java IO模型:BIO、NIO、AIO

Java IO模型:BIO.NIO.AIO 本来是打算直接学习网络框架Netty的,但是先补充了一下自己对Java 几种IO模型的学习和理解.分别是 BIO.NIO.AIO三种IO模型. IO模型的基本说明 BIO模型图 缺点: 如果有很多个Client,则会产生很多个线程.压力主要是在服务器端.客户端的压力并不大. 另外建立连接之后,并不是在时时刻刻的使用.会有空间时间. 会阻塞. NIO模型图 特点: 事件驱动 多路复用 Netty底层使用的NIO模型 AIO模型 目前还未得到广泛运用.异步

tiny_cnn代码详解(3)——层间继承关系

在上一篇博文中我们顺利将tiny_cnn的程序调试通过,在这篇博文中我们尝试从整体角度给出对tiny_cnn这个深度学习框架的解读,重点论述一下其各个层直接类封装的继承关系. 一.卷积神经网络快速入门 tiny_cnn作为卷积神经网络的一种实现形式,在探讨其框架结构之前,首先需要简要介绍一些卷积神经网络相关的知识.首先,给出经典卷积神经网络的网络结构: 这个是经典的LeNet-5的网络结构图,五层网络.最早用于支票上的手写数字识别,也是最早的商业化的深度学习模型.从上图中可以看出,卷积神经网络主

Github-jcjohnson/torch-rnn代码详解

Github-jcjohnson/torch-rnn代码详解 [email protected] http://www.cnblogs.com/swje/ 作者:Zhouwan  2016-3-18 声明: 1)本文仅供学术交流,非商用.所以每一部分具体的参考资料并没有详细对应.如果某部分不小心侵犯了大家的利益,还望海涵,并联系博主删除. 2)本人才疏学浅,整理总结的时候难免出错,还望各位前辈不吝指正,谢谢. 请联系:[email protected] 或[email protected] 本研

DeepLearning tutorial(3)MLP多层感知机原理简介+代码详解

DeepLearning tutorial(3)MLP多层感知机原理简介+代码详解 @author:wepon @blog:http://blog.csdn.net/u012162613/article/details/43221829 本文介绍多层感知机算法,特别是详细解读其代码实现,基于python theano,代码来自:Multilayer Perceptron,如果你想详细了解多层感知机算法,可以参考:UFLDL教程,或者参考本文第一部分的算法简介. 经详细注释的代码:放在我的gith

DeepLearning tutorial(4)CNN卷积神经网络原理简介+代码详解

DeepLearning tutorial(4)CNN卷积神经网络原理简介+代码详解 @author:wepon @blog:http://blog.csdn.net/u012162613/article/details/43225445 本文介绍多层感知机算法,特别是详细解读其代码实现,基于python theano,代码来自:Convolutional Neural Networks (LeNet).经详细注释的代码和原始代码:放在我的github地址上,可下载. 一.CNN卷积神经网络原理

DeepLearning tutorial(1)Softmax回归原理简介+代码详解

DeepLearning tutorial(1)Softmax回归原理简介+代码详解 @author:wepon @blog:http://blog.csdn.net/u012162613/article/details/43157801 本文介绍Softmax回归算法,特别是详细解读其代码实现,基于python theano,代码来自:Classifying MNIST digits using Logistic Regression,参考UFLDL. 一.Softmax回归简介 关于算法的详

java io系列12之 BufferedInputStream详解

目录1. BufferedInputStream 介绍2. BufferedInputStream 源码分析(基于jdk1.7.40)3. 示例代码 BufferedInputStream 是缓冲输入流.它继承于FilterInputStream. BufferedInputStream 的作用是为另一个输入流添加一些功能,例如,提供"缓冲功能"以及支持"mark()标记"和"reset()重置方法".BufferedInputStream 本质

jQuery选择器代码详解(四)——Expr.preFilter

原创文章,转载请注明出处,多谢! Expr.preFilter是tokenize方法中对ATTR.CHILD.PSEUDO三种选择器进行预处理的方法.具体如下: Expr.preFilter : { "ATTR" : function(match) { /* * 完成如下任务: * 1.属性名称解码 * 2.属性值解码 * 3.若判断符为~=,则在属性值两边加上空格 * 4.返回最终的mtach对象 * * match[1]表示属性名称, * match[1].replace(rune