BIO与NIO的一点理解


BIO即阻塞IO,NIO是非阻塞IO。NIO 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 BIO 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。NIO被称为 no-blocking io 或者 new io都说得通。如果使用BIO作为服务端,多个请求到来时由于是阻塞的,那么处理是串行的。用一个BIO代码例子来帮助理解

BIO的网络编程

本例子实现简单的web服务器(简单模仿Tomcat的请求和响应),下面将从最原始的版本逐渐改进BIO服务,来帮助理解BIO的工作过程

BIO服务-版本1

服务器端的实现:

    package com.bio.socket;

    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;

    /**
     * 服务器端
     * @author MOTUI
     *
     */
    public class BIOServerSocket {

        public static void main(String[] args) throws Exception {

            //1.创建serverSocket
            ServerSocket serverSocket = new ServerSocket();

            //2.设置端口号
            serverSocket.bind(new InetSocketAddress(8989));

            //3.获取报文,阻塞直到有连接进来
            Socket socket = serverSocket.accept();

            //a.获得客户端的意图   request
            InputStream is = socket.getInputStream();
            //字符流和字节流的转换
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            //存储读取的数据
            StringBuilder sb = new StringBuilder();
            String line = null;
            //读取数据,没有数据也会阻塞
            while((line = br.readLine()) != null){
                sb.append(line);
            }
            System.out.println("服务器收到的数据:"+sb.toString());
            //b.返回客户端数据
            OutputStream os = socket.getOutputStream();
            //获得输出流
            PrintWriter pw = new PrintWriter(os);
            pw.write("接收的数据:"+sb.toString()+"  服务器已经接收到数据");
            //将数据发送
            pw.flush();
            br.close();
            pw.close();
            socket.close();
        }
    }

客户端的实现:

    package com.bio.socket;

    import java.io.BufferedReader;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.InetSocketAddress;
    import java.net.Socket;

    /**
     * 客户端
     * @author MOTUI
     *
     */
    public class BIOClientSocket {

        public static void main(String[] args) throws Exception {

            //1.创建Socket
            Socket socket = new Socket();

            //2.连接服务器
            socket.connect(new InetSocketAddress("192.168.0.117",8989));

            //a.发送数据到服务器
            OutputStream os = socket.getOutputStream();
            //获得输出流
            PrintWriter pw = new PrintWriter(os);
            pw.write("这是客户端数据");
            //将数据发送
            pw.flush();
            //告知服务器已经到了流的结尾
            socket.shutdownOutput();

            //b.获得服务器的回应
            InputStream is = socket.getInputStream();
            //字符流和字节流的转换
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            //存储读取的数据
            StringBuilder sb = new StringBuilder();
            String line = null;
            //读取数据
            while((line = br.readLine()) != null){
                sb.append(line);
            }
            //打印服务器回应数据
            System.out.println(sb.toString());

            br.close();
            pw.close();
            socket.close();
        }
    }

服务器端和客户端运行结束。这样的结果并和我们想要的结果不同,我们需要的是服务器端一直监听客户端发送的请求并作出响应。我们【服务器端】做如下的修改,客户端代码不做任何修改。

BIO服务-版本2

服务器端的实现:

package com.bio.socket;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 服务器端
 * @author MOTUI
 *
 */
public class BIOServerSocket {

    public static void main(String[] args) throws Exception {

        //1.创建serverSocket
        ServerSocket serverSocket = new ServerSocket();

        //2.设置端口号
        serverSocket.bind(new InetSocketAddress(8989));

        while(true){

            //3.阻塞直到有连接进来
            Socket socket = serverSocket.accept();

            //a.获得客户端的意图   request
            InputStream is = socket.getInputStream();
            //字符流和字节流的转换
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            //存储读取的数据
            StringBuilder sb = new StringBuilder();
            String line = null;
            //读取数据,没有数据也会阻塞
            while((line = br.readLine()) != null){
                sb.append(line);
            }
            System.out.println("服务器收到的数据:"+sb.toString());
            //b.返回客户端数据
            OutputStream os = socket.getOutputStream();
            //获得输出流
            PrintWriter pw = new PrintWriter(os);
            pw.write("接收的数据:"+sb.toString()+"  服务器已经接收到数据");
            //将数据发送
            pw.flush();
            br.close();
            pw.close();
            socket.close();
        }
    }
}

在执行的代码中加上while(true) ,保证执行代码一直执行。这样就达到了一直监听客户端的请求的目的,但是还是有问题:我们处理客户端的请求的时候会存在一个请求没有处理完成,另一个请求不能处理。(在处理的过程中最耗时的操作就是I/O操作)。为了解决这个问题,我们进行如下修改。

BIO服务-版本3

服务器端的修改实现:

package com.bio.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 服务器端
 * @author MOTUI
 *
 */
public class BIOServerSocket {

    public static void main(String[] args) throws Exception {

        //1.创建serverSocket
        ServerSocket serverSocket = new ServerSocket();

        //2.设置端口号
        serverSocket.bind(new InetSocketAddress(8989));

        while(true){

            //3.阻塞直到有连接进来
            final Socket socket = serverSocket.accept();

            //启动线程
            new Thread(){
                public void run() {
                    try {
                        //a.获得客户端的意图   request
                        InputStream is = socket.getInputStream();
                        //字符流和字节流的转换
                        BufferedReader br = new BufferedReader(new InputStreamReader(is));

                        //存储读取的数据
                        StringBuilder sb = new StringBuilder();
                        String line = null;
                        //读取数据,没有数据也会阻塞
                        while((line = br.readLine()) != null){
                            //将数据存储在StringBuilder中
                            sb.append(line);
                        }
                        System.out.println("服务器收到的数据:"+sb.toString());
                        //b.返回客户端数据
                        OutputStream os = socket.getOutputStream();
                        //获得输出流
                        PrintWriter pw = new PrintWriter(os);
                        pw.write("接收的数据:"+sb.toString()+"  服务器已经接收到数据");
                        //将数据发送
                        pw.flush();
                        br.close();
                        pw.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }finally{
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                };
            }.start();
        }
    }
}

客户端代码修改为:

package com.bio.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;

/**
 * 客户端
 * @author MOTUI
 *
 */
public class BIOClientSocket {

    public static void main(String[] args) throws Exception {

        //使用线程模拟用户 并发访问
        for (int i = 0; i < 20; i++) {
            new Thread(){
                public void run() {
                    try {
                        //1.创建Socket
                        Socket socket = new Socket();

                        //2.连接服务器
                        socket.connect(new InetSocketAddress("192.168.0.117",8989));

                        //a.发送数据到服务器
                        OutputStream os = socket.getOutputStream();
                        //获得输出流
                        PrintWriter pw = new PrintWriter(os);
                        pw.write("这是客户端数据");
                        //将数据发送
                        pw.flush();
                        //告知服务器已经到了流的结尾
                        socket.shutdownOutput();

                        //b.获得服务器的回应
                        InputStream is = socket.getInputStream();
                        //字符流和字节流的转换
                        InputStreamReader isr = new InputStreamReader(is);
                        BufferedReader br = new BufferedReader(isr);

                        //存储读取的数据
                        StringBuilder sb = new StringBuilder();
                        String line = null;
                        //读取数据
                        while((line = br.readLine()) != null){
                            sb.append(line);
                        }
                        System.out.println(sb.toString());

                        br.close();
                        pw.close();
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                };
            }.start();
        }   
    }
}

到这里我们感觉这种方式已经很合理了。在了解高并发之前,这的确看着不错。但是我们可以思考一个问题:如果我们的并发打到万级或者百万级或者更高的时候,我们的程序有没有问题呢?我们每有一个请求就创建一个线程,而且我们创建线程之后并没有关注这个线程是否正确执行?是否执行结束?我们都没有关注,这时候就出现问题了,当高并发情况下有很多的线程处于阻塞状态,而我们的系统资源已经占用,系统对I/O的处理就会慢,对I/O的处理变慢就会导致我们的线程阻塞,恶性循环直到系统假死(宕机)。这样的处理是不是不合理呢。我们现在虽然无法做到线程不阻塞(不阻塞就是NIO的方式),但是我们可以进行其他的做法,我们可以控制线程的创建个数,这样就会相对打到一个平衡。

BIO服务-版本4

服务器端修改为:

package com.bio.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 服务器端
 * @author MOTUI
 *
 */
public class BIOServerSocket {

    public static void main(String[] args) throws Exception {

        //1.创建serverSocket
        ServerSocket serverSocket = new ServerSocket();

        //2.设置端口号
        serverSocket.bind(new InetSocketAddress(8989));

        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        while(true){

            //3.阻塞直到有连接进来
            final Socket socket = serverSocket.accept();

            executorService.submit(new Runnable() {

                @Override
                public void run() {
                    try {
                        System.out.println("当前线程ID:"+Thread.currentThread().getId());

                        //a.获得客户端的意图   request
                        InputStream is = socket.getInputStream();
                        //字符流和字节流的转换
                        InputStreamReader isr = new InputStreamReader(is);
                        BufferedReader br = new BufferedReader(isr);

                        //存储读取的数据
                        StringBuilder sb = new StringBuilder();
                        String line = null;
                        //读取数据,没有数据也会阻塞
                        while((line = br.readLine()) != null){
                            sb.append(line);
                        }
                        System.out.println("服务器收到的数据:"+sb.toString());
                        //b.返回客户端数据
                        OutputStream os = socket.getOutputStream();
                        //获得输出流
                        PrintWriter pw = new PrintWriter(os);
                        pw.write("接收的数据:"+sb.toString()+"  服务器已经接收到数据");
                        //将数据发送
                        pw.flush();
                        br.close();
                        pw.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }finally{
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }       
                }
            });
        }
    }
}

到此我们的程序就算相对完善的。在服务器端我们设置了线程的最大数为10,所以我们的程序最大并发量为10,这样我们的程序就不需要一直新建线程浪费资源,只需要等待别人用完还回线程池,然后拿到继续使用即可。但是我们的程序依然是线程阻塞的。

NIO的网络编程

NIO是非阻塞IO,与BIO的区别就是在BIO中阻塞的地方都可以设置成非阻塞(监听连接的 accept() 方法,以及从socket中读取数据的方法)。

阻塞与非阻塞IO

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)

NIO与BIO的主要区别:面向流与面向缓冲

Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

NIO三大核心组件

在java.io 中最为核心的一个概念是流(Stream),面向流的编程。一个流要么是输入流,要么是输出流,不可能同时是输入流又是输出流。java.nio是面向缓冲的,Buffer 本身就是一块内存,底层实现上,它实际上是个数组.数据的读写都是通过 Buffer 来实现的。

Selector,Channel 和 Buffer 的关系图


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
云原生 云原生
随着虚拟化技术的成熟和分布式架构的普及,用来部署、管理和运行应用的云平台被越来越多的提及。IaaS、PaaS和SaaS是云计算的3种基本服务类型,它们是关注硬件基础设施的基础设施即服务、关注软件和中间件平台的平台即服务以及关注业务应用的软件
2022-06-26
Next 
Redis的数据结构 Redis的数据结构
在看Redis的一些底层数据结构实现时,想到一个以前没有关注过的问题,就是Redis是如何把数据分配到集群中的每一个节点的。我们知道Redis用Cluster模式部署下,数据是分布式存储的,假设客户端随机请求到集群中的一台Redis服务,而
2022-06-12
  TOC