BIO、NIO、AIO

83 阅读7分钟

先来了解一下Java中的IO流,IO(Input Output)用于实现数据的输入和输出操作,Java把不同的输入/输出源抽象表述为流(Stream)。

  • 按照数据流向,可以分为输入流和输出流。
  • 按照数据类型,可以分为字节流和字符流。
  • 按照处理功能,可以分为节点流和处理流。

Java共支持3种网络编程的IO模型:BIO、NIO、AIO,它们之间的区别在于处理请求的过程是同步还是异步,堵塞还是非堵塞。

同步与异步

同步指的是方法一旦调用,调用者必须等到方法调用返回后,才能继续执行后面的行为。

异步指的是调用后立即返回,调用者不必等待方法内的代码执行结束,就可以继续执行后面的行为。

堵塞与非堵塞

阻塞指的是遇到同步等待后,一直等待同步方法处理完成。

非堵塞指的是遇到同步等待后,不等待方法执行完成,先操作其它流程,隔断时间后再观察同步方法是否完成。

BIO、NIO、AIO适用场景

  • BIO方式适用于连接数目比较少且固定的架构,这种方式对服务器资源要求比较高,但是程序简单易理解。
  • NIO方式适用于连接数目多且连接比较短的架构,比如连天服务器、服务器间通讯等。
  • AIO方式适用于连接数目多且连接比较长的架构,比如图像服务器等。

BIO

BIO(Blocking IO)同步堵塞IO,是JDK1.4前的传统IO模型。服务器实现模式是一个连接一个线程,客户端有连接请求时,服务端会启动一个线程来处理请求,所以在性能和可靠性方面存在着巨大的瓶颈,在高并发的场景下,机器资源很快就会被耗尽。

BIO编程流程

  1. 服务端启动一个ServerSocket
  2. 客户端启动Socket与服务端通信,默认情况下服务端会对每个客户端请求创建一个线程与之通信。
  3. 客户端发出请求后,等待服务端线程响应。
  4. 客户端线程等待请求结束后,再继续执行。

BIO应用实例

BIO的主要阻塞的点是:

  • 建立连接
  • 读:in.read()
  • 写:out.write()

(1)BIO服务端代码

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

public class BIOServer {
    public static void main(String[] args) throws Exception {
        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.bind(new InetSocketAddress(9000));
            while (true) {
                //客户端每一个socket创建一个线程
                Socket socket = server.accept();
                // 启动请求处理线程
                new Thread(new HandleTask(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            server.close();
        }
    }
}
class HandleTask implements Runnable {

    private Socket socket;

    public HandleTask(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        InputStream in = null;
        OutputStream out = null;
        try {
            // 读数据
            in = socket.getInputStream();
            byte[] buffer = new byte[1024];
            while (in.read(buffer) != -1) {
                System.out.println("读取请求数据:>> " + new String(buffer));
            }
            // 写数据
            out = socket.getOutputStream();
            out.write("World".getBytes());
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOCloseUtils.close(out,in);
        }
    }
}

(2)客户端代码

public class BIOClient {
    public static void main(String[] args) {
        Socket socket = null;
        OutputStream out = null;
        InputStream in = null;
        try {
            // 与Server建立连接
            socket = new Socket("127.0.0.1", 9000);
            // 发送数据
            out = socket.getOutputStream();
            out.write("Hello".getBytes());
            out.flush();
            // 完成发送数据
            socket.shutdownOutput();
            // 接收数据
            in = socket.getInputStream();
            byte[] buffer = new byte[1024];
            // 阻塞式读取
            while (in.read(buffer) != -1) {
                System.out.println("读取相应数据:>> " + new String(buffer));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOCloseUtils.close(socket,out,in);
        }
    }
}

(3)工具类

public class IOCloseUtils {
    public static void close(AutoCloseable... closeables) {
        for (AutoCloseable closeable : closeables) {
            if (closeable != null) {
                try {
                    closeable.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

BIO进阶版

基于线程池的伪异步 IO,为解决同步堵塞IO面临的一个请求需要一个线程处理的问题,我们可以对线程模型进行优化,通过线程池来处理多个客户端的请求接入,通过线程池控制控制服务端线程数。由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相对于传统的一连接一线程模型,是一种改良。但是,这种方案底层依旧采用同步堵塞模型,因此没有解决BIO本质的缺陷。

实例代码

代码只是针对线程添加了线程池。

public class BIOThreadPoolServer {
    public static void main(String[] args) throws Exception {
        //创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.bind(new InetSocketAddress(9000));
            while (true) {
                //客户端每一个socket创建一个线程
                Socket socket = server.accept();
                // 启动请求处理线程
                threadPool.submit(new HandleTask(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            server.close();
        }
    }
}

NIO

NIO(Non-Blocking IO)同步非堵塞IO,是JDK1.4开始提供的API。线程发起IO请求,立即返回(非堵塞)。同步指的是必须等待IO缓冲区内的数据准备就绪,非堵塞指的是用户线程不用等待IO缓冲区,可以先做一些其他操作,但是需要定时轮询检查IO缓冲区数据是否已就绪。

NIO是支持面向缓冲区,基于通道的IO操作,它有三大核心部分:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(多路复用选择器)

Buffer

缓冲区的本质是一块可以写入数据,然后可以从中读取数据的内存,可以理解成是一个容器对象,该对象提供了一组方法,可以更轻松地操作内存块。

Channel

BIO的stream是单向的,Channel是双向的,既可以从通道中读取数据,又可以写数据到通道。也可以异步读写数据。

Selector

Selector可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。所以一个单独的线程可以管理多个channel。

  • 每个Channel会对应一个Buffer。
  • 一个线程对象一个Selector,一个Selector对应多个Channel。

NIO应用实例

与BIO的Socket和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel这种不同的套接字通道实现。

(1)NIO服务端代码

public class NIOServer {
    public static void main(String[] args) throws Exception {
        // 创建通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        // 绑定端口
        serverSocketChannel.bind(new InetSocketAddress(9001));
        // 获取选择器
        Selector selector = Selector.open();
        // 将通道注册到选择器上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (selector.select() > 0) {
            // 获取选择器中的所有注册的通道中已经就绪好的事件
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey sk = it.next();
                // 判断这个事件具体是什么
                if (sk.isAcceptable()) {
                    SocketChannel channel = serverSocketChannel.accept();
                    channel.configureBlocking(false);
                    // 将该通道注册到选择器上
                    channel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端注册到Selector:" + selector.keys().size());
                } else if (sk.isReadable()) {
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    // 读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len = 0;
                    while ((len = sChannel.read(buf)) > 0) {
                        buf.flip();
                        System.out.println("来自客户端的请求:" + new String(buf.array(), 0, buf.remaining()));
                        buf.clear();
                    }
                }
                // 取消
                it.remove();
            }
        }
    }
}

(2)NIO客户端

public class NIOClient {                                                                                                                                                                                             
    public static void main(String[] args) throws IOException {                                                
        // 获取通道                                                                                                
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9001));            
        socketChannel.configureBlocking(false);                                                                
        // 分配缓冲区                                                                                               
        ByteBuffer buf = ByteBuffer.allocate(1024);                                                            
        // 发送数据                                                                                                
        String str = "你好,服务端";                                                                                 
        buf.put(str.getBytes());                                                                               
        buf.flip();                                                                                            
        socketChannel.write(buf);                                                                              
        buf.clear();                                                                                           
        // 关闭通道                                                                                                
        socketChannel.close();                                                                                 
    }                                                                                                          
}                                                                                                              

AIO

AIO(Asynchronous IO)异步非堵塞IO,与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。

AIO应用实例

(1)AIO服务端代码

public class AIOServer {

    public static void main(String[] args) throws IOException, InterruptedException {
        AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 9002));
        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel result, Void attachment) {
                try {
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    buffer.put("服务端 TO 客户端 :say hello".getBytes());
                    buffer.flip();
                    result.write(buffer, null, new CompletionHandler<Integer, Void>(){
                        @Override
                        public void completed(Integer result, Void attachment) {
                            System.out.println("服务端发送消息成功");
                        }

                        @Override
                        public void failed(Throwable exc, Void attachment) {
                            System.out.println("发送失败");
                        }
                    });

                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    result.read(readBuffer, null, new CompletionHandler<Integer, Void>() {
                        //成功时调用
                        @Override
                        public void completed(Integer result, Void attachment) {
                            System.out.println("接收客户端消息:" + new String(readBuffer.array()));
                        }
                        //失败时调用
                        @Override
                        public void failed(Throwable exc, Void attachment) {
                            System.out.println("读取失败");
                        }
                    });

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            //失败时
            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });
    }
}

(2)AIO客户端代码

public class AIOClient {
    public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
        AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
        Future<Void> future = client.connect(new InetSocketAddress("127.0.0.1", 9002));
        future.get();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        client.read(buffer, null, new CompletionHandler<Integer, Void>() {
            //成功时调用
            @Override
            public void completed(Integer result, Void attachment) {
                System.out.println("接收服务端消息:" + new String(buffer.array()));
            }
            //失败时调用
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("客户端接收消息失败");
            }
        });

        ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
        writeBuffer.put("客户端 TO 服务端 :say hi".getBytes());
        writeBuffer.flip();

        Future<Integer> write = client.write(writeBuffer);
        Integer r = write.get();
        if(r>0){
            System.out.println("客户端消息发送成功");
        }
       //休眠线程
        TimeUnit.SECONDS.sleep(1000L);
    }
}

BIO、NIO、AIO对比

同步堵塞IO-BIO伪异步IO非堵塞IO-NIO异步IO-AIO
客户端个数:IO线程1:1M:NM:1(1个IO线程处理多个请求)M:0(不需要额外启动线程,被动回调)
堵塞堵塞堵塞非堵塞非堵塞
同步同步同步同步异步
API使用难度简单简单复杂复杂
调试难度简单简单复杂复杂
可靠性非常差
吞吐量

Gitee地址:gitee.com/renxiaoshi/…