JAVA NIO入门

三大组件简介

Channel,Buffer,Selector

Channel(通道)

有点类似于Stream。不过与Stream的单向通道不同,它是可以读写的双向通道。

常见的 Channel 有

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

Buffer(缓冲区)

用于缓冲读写数据,是一块内存区。数据必须经过Buffer。Channel要和Buffer配合使用,数据要不从Buffer写入Channel,要不从Channel读入Buffer

常见的Buffer有

  • ByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

Buffer和Channel例子如下

 public static void main(String[] args) throws IOException {
        FileChannel channel = new FileInputStream("test.txt").getChannel();//获取通道
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);//获取缓存区
        while (true){
            long read = channel.read(byteBuffer);//从channel读取数据,向buffer写入
            if(read==-1){//没有内容了
                break;
            }
            byteBuffer.flip();//切换至 读模式
            while (byteBuffer.hasRemaining()){
                byte b = byteBuffer.get();
                System.out.println((char) b);
            }
            byteBuffer.clear();//切换至写模式
        }
    }
}

Buffer常见方法

在这里插入图片描述
常用API

//为Buffer分配空间
Bytebuffer buf = ByteBuffer.allocate(16);
//向 buffer 写入数据:两种,调用channel的read或者buffer的put
int readBytes = channel.read(buf);
buf.put((byte)127);
//从 buffer 读取数据:两种,调用channel的write或者uffer的get
int writeBytes = channel.write(buf);
byte b = buf.get();

Selector(选择器)

允许单线程处理多个channel,获取这些channel上发送的事件,这些channel工作在非阻塞模式,不会让一个线程阻塞在一个channel
在这里插入图片描述
使用方法
创建:

Selector selector = Selector.open();

绑定channel事件

channel.configureBlocking(false);
SelectionKey key = channel.register(selector,SelectionKey.OP_ACCEPT);
//四种事件类型
SelectionKey.OP_ACCEPT 服务器端成功接受连接时触发
SelectionKey.OP_CONNECT  客户端连接成功时触发
SelectionKey.OP_READ  数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
SelectionKey.OP_WRITE 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

监听 Channel 事件

int count = selector.select(); //阻塞直到绑定事件发生
int count = selector.select(long timeout); //阻塞直到绑定事件发生,或是超时(时间单位为 ms)
int count = selector.selectNow();//不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

处理Accept事件

例子

public class NIOSelectorAcceptServer {
    public static void main(String[] args) throws IOException {
        try (ServerSocketChannel channel = ServerSocketChannel.open()) {
            channel.bind(new InetSocketAddress(8080));
            System.out.println(channel);
            Selector selector = Selector.open();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                int count = selector.select();
                System.out.println("select count:" + count);
                // 获取所有事件
                Set<SelectionKey> keys = selector.selectedKeys();
                // 遍历所有事件,逐一处理
                Iterator<SelectionKey> iter = keys.iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    // 判断事件类型
                    if (key.isAcceptable()) {
                        ServerSocketChannel c = (ServerSocketChannel) key.channel();
                        // 必须处理
                        SocketChannel sc = c.accept();
                        System.out.println(sc);
                    }
                    // 处理完毕,必须将事件移除
                    iter.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

处理read事件

public class NIOSelectorReadServer {

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

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);

        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        serverSocketChannel.bind(new InetSocketAddress(9000));

        System.out.println("服务器启动成功");
        while (true){
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if(key.isAcceptable()){
                        ServerSocketChannel channel = (ServerSocketChannel)key.channel();
                        SocketChannel socketChannel = channel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector,SelectionKey.OP_READ);
                        System.out.println("客户端连接成功");
                    }else if(key.isReadable()){
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(4);
                        int read = sc.read(buffer);
                        if(read == -1) {
                            key.cancel();
                            sc.close();
                        } else {
                            buffer.flip();
                            System.out.println("接收消息:" + new String(buffer.array()));
                        }
                    }
            }
            iterator.remove();
        }
    }
}

处理write事件

public class NIOSelectorWriteServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        ssc.bind(new InetSocketAddress(9000));
        while (true){
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();
                if(key.isAcceptable()){
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    SelectionKey scKey = sc.register(selector, SelectionKey.OP_READ);
                    StringBuffer sb = new StringBuffer();
                    for (int i = 0;i<90000000;i++){
                        sb.append("a");
                    }
                    ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
                    int write = sc.write(buffer);
                    // 3. write 表示实际写了多少字节
                    System.out.println("实际写入字节:" + write);
                    // 4. 如果有剩余未读字节,才需要关注写事件
                    if (buffer.hasRemaining()) {
                        // read 1  write 4
                        // 在原有关注事件的基础上,多关注 写事件
                        scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);
                        // 把 buffer 作为附件加入 sckey
                        scKey.attach(buffer);
                    }
                }else if(key.isWritable()){
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    SocketChannel sc = (SocketChannel) key.channel();
                    int write = sc.write(buffer);
                    System.out.println("实际写入字节:" + write);
                    if (!buffer.hasRemaining()) { // 写完了
                        key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
                        key.attach(null);
                    }
                }
            }
        }
    }
}

零拷贝

如果将一个文件通过socket写出

传统的IO

在这里插入图片描述

1:java本身不具备IO读写能力,需要从java程序的用户态转化到内核态,通过内核的读能力将数据读到内核缓冲区。操作系统通过DMA(Direct Memory Access)实现文件读。
2:用户态切换到内核态,将数据从内核缓冲区读到用户缓冲区(byte[] buf)
3:调用write方法,把数据从用户缓冲区读到socket缓冲区
4:往网卡写数据,由于java不具备该项能力,会将用户态转化到内核态,使用DMA将数据从socket缓冲区读到网卡

总结:
3次用户态和内核态的切换,比较重量级
4次数据的拷贝

NIO的优化

在这里插入图片描述
通过 DirectByteBuf

ByteBuffer.allocate(10)  HeapByteBuffer 使用的还是 java 内存 
ByteBuffer.allocateDirect(10)  DirectByteBuffer 使用的是操作系统内存

与传统IO相比少了第2步。这是因为DirectByteBuf将堆外内存印射到jvm内存直接使用

总结
3次用户态和内核态的切换
3次数据的拷贝

NIO进一步优化

在这里插入图片描述
1:java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区
2:数据从内核缓冲区传输到 socket 缓冲区
3:最后使用 DMA 将 socket 缓冲区的数据写入网卡

总结
1次用户态和内核态的切换
3次数据拷贝

NIO再进一步优化

在这里插入图片描述
1: java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区
2:只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗
3:使用 DMA 将 内核缓冲区的数据写入网卡

总结
1次用户态和内核态转化
2次数据拷贝

所谓的零拷贝不是真正的无拷贝,而是不会拷贝数据到jvm内存中,优点有:
更少的用户和内核态转化
不利用CPU计算
适合小文件传输


版权声明:本文为qq_37376024原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。