【Netty】(二)NIO 中的三大组件的介绍与使用

1. NIO 基本介绍

Java NIO 是同步非阻塞的。NIO 相关的类放在 java.nio 包及子包下面,并且对原生的 IO 进行了很多类的改写。

NIO 是面向缓冲区或者是面向块编程的:数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中移动,这就增加了处理过程中的灵活性。

它有三大核心组件:

  • Channel:通道
  • Buffer:缓冲区
  • Selector:选择器

其核心组件结构如下图:
【Netty】(二)NIO 中的三大组件的介绍与使用
由上面的图可以看出:

  1. 一个 Channel 对应一个 Buffer
  2. 一个 Selector 对应一个 Thread,但对应多个 Channel
  3. Selector 会根据不同事件在各个通道上切换
  4. 数据的读取/写入是通过 Buffer,这个跟 BIO 不同。BIO 是直接与通道打交道的
  5. NIO 中的 Buffer 是双向的(既可以读又可以写),但需要 flip() 方法切换;BIO 中不是双向流,要么是一个单独的输入流,要么就是一个输出流
  6. Channel 也是双向的

Client 不直接与 Channel 交互,而是通过中间媒介 Buffer 进行交互。

2. Buffer

Buffer 本质上是一个可以读、写数据的内存块,可以理解为一个容器对象。

Java 中的基本数据类型除了 boolean 类型外,其余的都有与之对应的 Buffer 类型。

下面以 IntBuffer 为例:

Buffer 使用示例:

public class BufferDemo {

    public static void main(String[] args) {
        // 1.创建 Buffer
        IntBuffer intBuffer = IntBuffer.allocate(5);
        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(i);
        }
        // 2.Buffer 转换。写 --> 读
        intBuffer.flip();
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

这里创建了一个 IntBuffer,大小为 5。然后,往其里面 put 了 5 个整数(Buffer 写)。由于 Buffer 既可以写又可以读。所以,在进行读取之前,进行 Buffer 切换 intBuffer.flip()

3. Channel

NIO 的通道类似于流,但有如下区别:

  1. 通道可以同时进行读、写,而流只能进行读或者写
  2. 通道可以实现异步读、写数据
  3. 通道可以从缓冲区读数据,也可以写数据到缓冲区

Channel 是一个接口:

public interface Channel extends Closeable {
	// ...
}

常用的 Channel 类有:

  • FileChannle:用于文件的数据的读、写
  • DatagramChannel:用于 UDP 的数据的读、写
  • ServerSocketChannel:用于 TCP 的数据的读、写
  • SocketChannel:用于 TCP 的数据的读、写

示例一:本地文件写数据

使用 ByteBuffer 和 FileChannel 将 “hello,JAVA” 写入到某个磁盘文件

public static void main(String[] args) throws Exception {
    String str = "hello, JAVA";
    FileOutputStream out = new FileOutputStream("E:\\temp.txt");
    FileChannel fileChannel = out.getChannel();
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    byteBuffer.put(str.getBytes());
    byteBuffer.flip();
    fileChannel.write(byteBuffer);
    out.close();
}

运行上述代码后,便会在 E 盘中生成一个 temp.txt 文件,并且,其里面有内容。

示例二:本地文件读数据

使用 ByteBuffer 和 FileChannel 将 temp.txt 文件中的内容读取出来

public static void main(String[] args) throws Exception {
    File file = new File("E:\\temp.txt");
    FileInputStream in = new FileInputStream(file);
    FileChannel fileChannel = in.getChannel();
    ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
    fileChannel.read(byteBuffer);
    // 将字节转化为字符串
    String result = new String(byteBuffer.array());
    System.out.println(result);
    in.close();
}

示例三:本地文件读、写数据

通过 FileChannel 和一个 Buffer 完成某个文件的拷贝

public class FileChannelRwDemo {

    public static void main(String[] args) throws Exception{
        FileInputStream in = new FileInputStream("E:\\temp.txt");
        FileChannel inFileChannel = in.getChannel();
        FileOutputStream out = new FileOutputStream("temp.txt");
        FileChannel outFileChannel = out.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        while (true) {
        	// 此行代码是重点
            byteBuffer.clear();
            
            int read = inFileChannel.read(byteBuffer);
            if (-1 == read) {
                break;
            }
            byteBuffer.flip();
            outFileChannel.write(byteBuffer);
        }
        in.close();
        out.close();
    }
}

4. Selector

Selector 能够检测多个注册的通道上是否有事件发生。如果有事件发生,便获取事件,然后针对每个事件进行相应的处理。

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

Selector 工作流程:

  1. 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
  2. SocketChannel 注册到 Selector 上(SelectableChannel#register())
  3. 注册后返回一个 SelectionKey,会和该 Selector 关联
  4. Selector 通过 select() 方法进行监听。该方法返回有事件发生的通道数
  5. 进一步可得到 SelectionKey
  6. SelectionKey 通过 channel() 方法反向获取 SocketChannel,然后,可以进行操作

示例:通过 NIO,进行服务端与客户端数据通讯

服务端:

public class NioServer {

    public static void main(String[] args) throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        // 设置非阻塞
        serverSocketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        // 将ServerSocketChannel注册到Selector
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            if (selector.select(1000) == 0) {
                System.out.println("服务器等待了1秒,无连接");
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 如果有客户端连接
                if (selectionKey.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功,生成了一个 socketChannel :" + socketChannel.hashCode());
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (selectionKey.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
                    // 获取与Channel关联的Buffer
                    ByteBuffer byteBuffer = (ByteBuffer)selectionKey.attachment();
                    socketChannel.read(byteBuffer);
                    System.out.println("服务端收到了:" + new String(byteBuffer.array()));
                }
                iterator.remove();
            }
        }
    }
}

客户端:

public class NioClient {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        // (正在)连接服务端
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("因为连接需要时间,客户端不用阻塞,可以做其它工作...");
            }
        }
        // 连接成功
        String str = "Hello Java";
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(byteBuffer);
        System.in.read();
    }
}

先运行服务端,再运行客户端,服务端打印出如下信息:
【Netty】(二)NIO 中的三大组件的介绍与使用

上一篇:网络IO模型对比(BIO、NIO、AIO)


下一篇:IDEA使用JDBC链接MySql(java编程)