NIO入门

108 阅读10分钟

IO 模型

基本说明

1、IO模型就是用什么样的通道进行数据的发送和接收。很大程度的决定了程序通信的性能。

2、Java共支持3种 I/O模式:BIO、NIO、 AIO

3、Java BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一 个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。

4、Java NIO : 同步非阻塞。服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册 到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

BIO 应用实例

1、使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯

2、要求使用线程池机制改善,可以连接多个客户端。

3、服务器端可以接收多个客户端发送的请求的数据(用telnet)

4、代码如下

public class BIOServer {
    public static void main(String[] args) throws IOException {

        //思路
        //1、创建一个线程池
        //2、如果有客户端连接,就创建一个线程,与其通讯
        ExecutorService executorService = Executors.newCachedThreadPool();

        //创建serverSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器启动了........");
        while (true) {
            //服务器端有一个主线程,一直处于监听状态,如果有一个客户端连接了,就会产生一个子线程使其通讯,主线程返回来再次进行监听。以此类推
            System.out.println("线程ID="+Thread.currentThread().getId()+"名字="+Thread.currentThread().getName());
            //等待客户端的连接
            System.out.println("等待连接.....");

            //accept方法会阻塞
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");

            //创建一个线程与之通讯
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    //可以和客户端通讯
                    handler(socket);
                }
            });
        }
    }

    //编写一个handler方法,和客户端通讯
    public static void handler(Socket socket) {

        byte[] bytes = new byte[1024];
        try {
            System.out.println("线程ID="+Thread.currentThread().getId()+"名字="+Thread.currentThread().getName());
            //通过socket 获取输入流
            InputStream inputStream = socket.getInputStream();
            //循环读取客户端发送过来的数据
            while (true) {
                System.out.println("线程ID="+Thread.currentThread().getId()+"名字="+Thread.currentThread().getName());
                System.out.println("read.....");
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes, 0, read)); //输出客户端的数据
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.getStackTrace();
        } finally {
            try {
                System.out.println("关闭client连接");
                socket.close();
            } catch (Exception e) {
                e.getStackTrace();
            }

        }

    }

}

服务器端有一个主线程,一直处于监听状态,如果有一个客户端连接了,就会产生一个子线程使其通讯,主线程返回来再次进行监听。以此类推


BIO问题分析

1、每个请求都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据 Write 。

2、当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。

3)、连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费

NIO介绍

  1. Java NIO 全称 java non-blocking IO,是指 JDK 提供的新API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞

  2. NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io包中的很多类进行改写。

  3. NIO 有三大核心部分:Channel(通道)Buffer(缓冲区) ,Selector(选择器)

  4. NIO是 面向缓冲区 ,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。

  5. Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情

  6. 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。

  7. HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

NIO 和 BIO 的比较

  1. BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多

  2. BIO 是阻塞的,NIO 则是非阻塞的

  3. BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。

buffer简单代码:

public static void main(String[] args) {

        //创建一个buffer  大小为5 可存放5个int
        IntBuffer intBuffer = IntBuffer.allocate(5);
        //向buffer中存放数据
        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(i*2);
        }

        //从buffer中读取数据
        //将buffer切换 读写切换
        intBuffer.flip();
        while (intBuffer.hasRemaining()){
            System.out.println(intBuffer.get());
        }

    }

NIO 三大核心原理示意图

当客户端连接一个server的时候,由这个serversocketchannel 与这个客户端对应的socketchannel,然后与服务器之间进行通讯

📚 说明:

  1. 每个channel 都会对应一个Buffer

  2. Selector 对应一个线程, 一个线程对应多个channel(连接)

  3. 该图反应了有三个channel 注册到 该selector

  4. 程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念

  5. Selector 会根据不同的事件,在各个通道上切换

  6. Buffer 就是一个内存块 , 底层是有一个数组

  7. 数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO的Buffer 是可以读也可以写, 需要 flip 方法切换

  8. channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统通道就是双向的。

缓冲区(buffer)

缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组) ,该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。

Buffer类及其子类

  • 在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类, 类的层级关系图:

  • Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息
//标记
private int mark = -1;  
//位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备
    private int position = 0;
//表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
    private int limit;
//容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
    private int capacity;

通道 channel

1、NIO的通道类似于流,但是有一些区别:

• 通道可以同时进行读写,而流只能读或者只能写

• 通道可以实现异步读写数据

• 通道可以从缓冲读数据,也可以写数据到缓冲

2、 BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道

(Channel)是双向的,可以读操作,也可以写操作。

3、 Channel在NIO中是一个接口public interface Channel extends Closeable{}

4、常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel。【ServerSocketChanne 类似ServerSocket , SocketChannel 类似 Socket】

5、FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP的数据读写。

代码演示:

1、将‘hello’ 写入到 文件

2、文件不存在就创建

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

        String str ="hello";

        FileOutputStream inputStream = new FileOutputStream("D:\file.txt");

        FileChannel channel = inputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        byteBuffer.put(str.getBytes());

        byteBuffer.flip();

        channel.write(byteBuffer);

        inputStream.close();

    }

3、将文件读取到程序台屏幕

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

        FileInputStream inputStream = new FileInputStream("d:\file.txt");
        FileChannel channel = inputStream.getChannel();


        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        channel.read(byteBuffer);

        System.out.println(new String(byteBuffer.array()));

        inputStream.close();
    }

4、也可以循环读取

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

    FileInputStream inputStream = new FileInputStream("1.txt");
    FileChannel channel01 = inputStream.getChannel();

    FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
    FileChannel channel02 = fileOutputStream.getChannel();

    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

    while (true){
        byteBuffer.clear();
        int read = channel01.read(byteBuffer);
        if(read==-1){
            break;
        }
        byteBuffer.flip();
        channel02.write(byteBuffer);
    }

        inputStream.close();
        fileOutputStream.close();
}

注意事项:

1、ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。

2、可以将一个普通Buffer 转成只读Buffer

3、NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成

4、NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering

selector选择器

1、Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)。

2) Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector) ,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

3、只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。

4、 避免了多线程之间的上下文切换导致的开销。

🕜serversocketchannel 和 socketchannel区别:

serversocketchannel在服务端监听新的客户端Socket连接。

socket具体负责进行读写操作,把缓冲区的数据写入通道,或者把通道里面的数据读到缓冲区。

代码演示

一个入门案例。实现服务端和客户端之间的数据简单通讯。

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


        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //创建选择器
        Selector selector = Selector.open();
        //监听端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //将serverSocketChannel注册到 selector 关心事件 为 op_accept
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //循环等待客户端连接
        while (true){
            //没有事件发生
            if(selector.select(2000)==0){
                System.out.println("服务器等待了 2秒 没有客户端连接");
                continue;
            }
            //如果有连接 获取相应事件的集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                if(key.isAcceptable()){  //代表有新的客户端连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功 生成了一个socketchannel"+socketChannel.hashCode());
                    //将 socketchannel设置为 非阻塞
                    socketChannel.configureBlocking(false);
                    //注册到 selector  关联一个buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if(key.isReadable()){
                    //通过key 获取channel
                    SocketChannel channel = (SocketChannel)key.channel();
                    //获取关联的buffer
                    ByteBuffer attachment = (ByteBuffer)key.attachment();
                    channel.read(attachment);
                    System.out.println("客户端"+new String(attachment.array()));
                }
                iterator.remove();
            }
        }

    }
 public static void main(String[] args) throws IOException {
        //得到网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //提供服务器的IP和端口
        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6666);
        //连接服务器
        if(!socketChannel.connect(socketAddress)){
            while (!socketChannel.finishConnect()){
                System.out.println("连接需要时间 客户端不会阻塞");
            }
        }
        //连接成功
        String str ="hello";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //发送数据 将数据写入到 channel
        socketChannel.write(buffer);
        System.in.read();
    }