阅读 102

BIO、NIO

前言

在日常的开发中,IO是无处不在的,但是真真正正做IO相关的开发,应该是比较少的,今天我们来了解一下

同步/异步

同步指两个或两个以上的事物随时间变化保持一定的相对关系,在计算机的世界里,我们可以把事物看成请求。在同步的场景中,请求必须等待返回结果才能往下执行,多个请求必须逐个逐个执行。异步跟同步相反,请求不需要等到返回结果就可以往下执行,多个请求可以同时执行。比如:你去一个饭店吃饭,同步的场景下,你点完一个菜,必须等厨师洗菜,煮菜,上菜才能点下一个菜。异步的场景下,你点完一个菜,厨师就可以干活了,你也可以接着点下一个菜

阻塞/非阻塞

阻塞指有障碍不能通过,非阻塞自然是指没有障碍能通过了。我们可以用堵车来形容阻塞跟非阻塞,阻塞的时候,车是不能动的,非阻塞的时候,车是能动的,在线程中,不能动的状态是线程挂起,能动的状态是线程运行

差异

乍一看,同步/异步,阻塞/非阻塞很像,其实两者的关注点不同 同步/异步关注的是多个请求能不能同时进行 阻塞/非阻塞关注的是线程执行时的状态

BIO

image.png

如上图

BIO(Blocking I/O):同步阻塞IO,每当有一个客户端连接服务端,服务端就启动一个线程处理,如果这个连接不做任何事情,会造成不必要的线程开销

代码示例
public class BIOServer {
​
    public static void main(String[] args) throws IOException {
​
        // BIO的场景下,每当有一个客户端连接,服务端就需要启动一个线程,所以使用线程池的方式优化
        ExecutorService executorService = Executors.newCachedThreadPool();
​
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器启动了");
​
        while (true) {
            // 监听、等待客户端连接
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");
            // 创建一个线程进行通讯
            executorService.execute(() -> handle(socket));
        }
​
    }
​
​
    public static void handle(Socket socket) {
        System.out.println("线程信息 ID = " + Thread.currentThread().getId() + ", 名字 = " + Thread.currentThread().getName());
​
        try {
            byte[] b = new byte[1024];
            InputStream stream = socket.getInputStream();
​
            while (true) {
                int read = stream.read(b);
                if (read != -1) {
                    System.out.println(new String(b, 0, read));
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码
问题
  1. 每个客户端的请求都需要创建单独的线程处理,当并发数较大时,服务端需要创建大量的线程,占用大量的资源
  2. 连接建立后,如果当前线程没有数据可读,线程会阻塞在read操作上,造成资源浪费

NIO

因为BIO的种种问题,Java做了一系列的改进,即NIO,是同步非阻塞的

image.png

我们先从一个整体的角度看看NIO

  1. NIO中有三大核心,分别是Selector、Channel、Buffer,每个Channel都对应一个Buffer,多个Channel可以注册到同一个Selector中,一个Selector对应一个线程
  2. NIO是面向缓冲区的编程,同一个Buffer,既可以读,也可以写,这跟BIO不同,BIO是面向流的编程,要么是输入流,要么是输出流
  3. NIO是非阻塞的,一个线程可以处理多个通道,当前通道没有读写操作,并不会阻塞在当前通道,而是会处理有读写操作的通道
Buffer

Buffer底层就是数组,在这个数组中,有三个特别重要的变量,分别是position、limit、capacity

  1. position:指向下一个将要被写入或者读取的元素索引
  2. limit:指向缓冲区的终点,
  3. capacity:容量,表示缓冲区的最大容量

新建一个Buffer时,position、limit、capacity指针的指向如下

image.png

这个Buffer新增一个元素时,position、limit、capacity指针的指向如下

image.png

这个Buffer新增第五个元素时,position、limit、capacity指针的指向如下

image.png

当我们要对缓冲区进行读取时,需要进行缓冲区翻转,翻转后position、limit、capacity指针的指向如下

image.png

示例代码
public class BufferTest {
​
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(10);
​
        for (int i = 0; i < 10; i++) {
            int temp = new Random().nextInt(100);
            System.out.println(temp);
            intBuffer.put(temp);
        }
        // 进行Buffer的翻转
        intBuffer.flip();
        System.out.println("-------------------");
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}
复制代码
源码

常用Buffer子类,分别代表了存储的数据类型,见名知意,很好懂

ByteBuffer、IntBuffer、LongBuffer、ShortBuffer、CharBuffer、DoubleBuffer、FloatBuffer

当我们使用IntBuffer.allocate(10)这个方法时,源码如下

public static IntBuffer allocate(int capacity) {
      if (capacity < 0)
        throw new IllegalArgumentException();
      // IntBuffer是个抽象类,分配空间的时候实际上创建的是HeapIntBuffer对象
      return new HeapIntBuffer(capacity, capacity);
}
复制代码
// 进行Buffer的翻转时,只是改变了指针的指向
public final Buffer flip() {
      limit = position;
      position = 0;
      mark = -1;
      return this;
}
复制代码
Channel

Channel类似流,但是跟流不太一样,流是单向的,逆流而上的水大家应该没有看过吧,像FileInputStream只能用来读取,Channel是双向的,既可以读,也可以写,看看下面一段代码,感受一下Channel的作用

public class ChannelTest {
    
    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("input.txt");
        FileChannel inputStreamChannel = fileInputStream.getChannel();
​
        FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
        FileChannel outputStreamChannel = fileOutputStream.getChannel();
​
​
        ByteBuffer byteBuffer = ByteBuffer.allocate(64);
​
        while (true) {
            // 重置数组指针,这步操作非常重要
            byteBuffer.clear();
​
            int read = inputStreamChannel.read(byteBuffer);
            // -1表示读到文件结尾
            if (read == -1) {
                break;
            }
​
            byteBuffer.flip();
            outputStreamChannel.write(byteBuffer);
​
        }
​
        inputStreamChannel.close();
        outputStreamChannel.close();
    }
    
}
复制代码
三大组件配合使用

Selector、Channel、Buffer三大组件配合使用

public class NIOServer {
​
​
    public static void main(String[] args) throws IOException {
​
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        Selector selector = Selector.open();
        // 监听端口6666
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        serverSocketChannel.configureBlocking(false);
        // channel注册到selector中,关心事件为 OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
​
        while (true) {
​
            if (selector.select(1000) == 0) {
                System.out.println("服务器等待一秒,无连接");
                continue;
            }
            // 如果返回的 >0,就获取相关的SelectionKey集合
            // 通过SelectionKey反向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
​
​
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
​
            while (iterator.hasNext()) {
                // 获取到SelectionKey
                SelectionKey selectionKey = iterator.next();
​
                // 不同事件,做不同的处理
                // 如果是acceptable,说明有新的客户端连接
                if (selectionKey.isAcceptable()) {
                    // 该客户端生成一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功,生成了一个socketChannel, socketChannel :" + socketChannel.hashCode());
                    socketChannel.configureBlocking(false);
                    // channel注册到selector中,并且关联一个buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
​
                if (selectionKey.isReadable()) {
                    // 通过key反向获取channel
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    // 获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    channel.read(buffer);
                    System.out.println("form 客户端 " + new String(buffer.array()));
                }
                iterator.remove();
            }
        }
    }
    
}
复制代码

参考资料

zhuanlan.zhihu.com/p/66148226

文章分类
后端
文章标签