java-NIO相关内容

18

1.NIO概述

NIO全称Java Non-blocking lO或Java New lO,是从JDK1.4开始引入的一套新的IO,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。

BIO、NIO、AIO(NIO2)

  • BIO 阻塞式
  • NIO 非阻塞式
  • JDK1.7后 新增AIO (Asynchronous IO) 异步IO

IO操作模式:

  • PIO(Programing IO)
    • 所有的IO操作由CPU处理,CPU占用率比较高。
  • DMA(Direct Memory Access) 直接内存访问 硬件实现
    • CPU把IO操作控制权交给DMA控制器,只能以固定的方式读写,CPU空闲做其它工作。
  • 通道方式(Channel)硬件实现
    • 能执行有限通道指令的IO控制器,代替CPU管理控制外设。
    • 通道有自己的指令系统,是一个协处理器,具有更强的独立处理数据输入和输出的能力。

为了配合硬件的发展,新增NIO。

[wppay]

2.NIO组成

NIO三个核心部分:

  • Buffer:缓冲区
  • Channel:通道
  • Selector:选择器(轮询器) 实现多路复用网络编程

NIO和普通IO的区别:

3.Buffer缓冲区

Java NIO中的Buffer用于和NIO通道进行交互。

缓冲区本质上是一块可以写入数据,也可以从中读取数据的内存。

Buffer是一个抽象类,子类包含:

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

由于子类也是抽象类,所以不能实例化对象。

4.Buffer基本使用步骤

  • 1.创建缓冲区
    • 间接缓冲区:
      • 在堆中开辟,易于管理,垃圾回收器可以回收,空间有限,读写文件速度较慢。
    • 直接缓冲区:
      • 直接在物理内存中开辟空间,空间比较大,读写文件速度快
      • 缺点:不受垃圾回收器控制,创建和销毁耗性能
  • 2.调用put方法写入数据到Buffer
  • 3.调flip()方法
  • 4.调用get方法从Buffer中读取数据
  • 5调用clear()方法或者compact()方法

5.课堂案例

public class ByteBufferDemo {
  public static void main(String[] args) {
    // 1创建缓冲区
    // 底层实现
    // allocate -> HeapByteBuffer ->  super -> final byte[] hb;
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 2添加数据
    byte[] data = "helloworld".getBytes();
    buffer.put(data);
    // 3反转buffer(把写入模式改成读取模式)
    buffer.flip();
    // 4获取数据
    //        for(int i=0;i<buffer.limit();i++) {
    //            byte b = buffer.get();
    //            System.out.print((char)b);
    //        }
    byte[] data2 = new byte[buffer.limit()];
    buffer.get(data2);
    System.out.println(new String(data2));
    // 5清空buffer
    buffer.clear();
  }
}

6.Buffer核心方法

7.Buffer原理

Buffer包括三个重要属性:

  • capacity
  • position
  • limit
public class ByteBufferDemo2 {
  public static void main(String[] args) {
    // 1创建缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    System.out.println(
        "放数据前位置:" + buffer.position() + " 限制:" + buffer.limit() + " 容量:" + buffer.capacity());
    // 2添加数据
    byte[] data = "helloworld".getBytes();
    buffer.put(data);
    System.out.println(
        "放数据后位置:" + buffer.position() + " 限制:" + buffer.limit() + " 容量:" + buffer.capacity());
    // 3反转buffer(把写入模式改成读取模式)
    buffer.flip();
    System.out.println(
        "反转位置:" + buffer.position() + " 限制:" + buffer.limit() + " 容量:" + buffer.capacity());
    // 4获取数据
    //        for(int i=0;i<buffer.limit();i++) {
    //            byte b = buffer.get();
    //            System.out.print((char)b);
    //        }
    byte[] data2 = new byte[buffer.limit()];
    buffer.get(data2);
    System.out.println(new String(data2));
    System.out.println(
        "读完位置:" + buffer.position() + " 限制:" + buffer.limit() + " 容量:" + buffer.capacity());
    // 5清空buffer
    buffer.clear();
    System.out.println(
        "清空之后位置:" + buffer.position() + " 限制:" + buffer.limit() + " 容量:" + buffer.capacity());
  }
}

rewind()与compact()方法实践

rewind():pos指针返回头部可以重新读取。

compact():将未读的元素整理压缩到前部。

public class ByteBufferDemo3 {
  public static void main(String[] args) {
    // 1创建缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 2添加数据
    byte[] data = "helloworld".getBytes();
    buffer.put(data);
    // 3反转buffer(把写入模式改成读取模式)
    buffer.flip();
    byte[] data2 = new byte[5];
    buffer.get(data2);
    System.out.println(new String(data2));
    System.out.println(
        "读取5个之后位置:" + buffer.position() + " 限制:" + buffer.limit() + " 容量:" + buffer.capacity());
    //        System.out.println("重绕");
    //        buffer.rewind();// 重绕缓冲区
    //        buffer.get(data2);
    //        System.out.println(new String(data2));

    // 5清空buffer
    // buffer.clear();
    buffer.compact(); // 整理压缩
    // System.out.println("清空之后位置:"+buffer.position()+" 限制:"+buffer.limit()+"
    // 容量:"+buffer.capacity());
    System.out.println(
        "compact之后位置:" + buffer.position() + " 限制:" + buffer.limit() + " 容量:" + buffer.capacity());
    buffer.flip();
    buffer.get(data2);
    System.out.println(new String(data2));
  }
}

8.Channel通道

Channel类似流。数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中。

9.Channel相关API

Channel接口主要实现类:

  • FileChannel 读写文件通道TCP
  • ServerSocketChannel 侦听套接字通道TCP
  • SocketChannel 套接字通道UDP
  • DatagramChannel 数据包通道UDP

FileChannel

  • java nio 中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。
  • FileChannel无法设置为非阻塞模式
  • 创建方式有三种:
    • 使用文件字节流或RandomAccessFile来获取一个FileChannel实例。
    • 使用Channels工具类
    • JDK1.7之后才能使用,FileChannel.open()方法。

FileChannel读写文件

public class FileChannelDemo {
  public static void main(String[] args) throws Exception {
    // write();
    read();
  }

  public static void write() throws Exception {
    // 1 创建FileChannel
    // 1.1使用文件字节流或RandomAccessFile来获取一个FileChannel实例。
    // FileOutputStream fos=new FileOutputStream("d:\\out.txt");
    // FileChannel channel = fos.getChannel();

    // RandomAccessFile raf=new RandomAccessFile("d:\\out.txt", "rw");
    // FileChannel channel = raf.getChannel();

    // 1.2 使用Channels工具类
    // FileChannel channel = (FileChannel) Channels.newChannel(new FileOutputStream("d:\\out.txt"));

    // JDK1.7之后才能使用, FileChannel.open()方法。
    FileChannel channel =
        FileChannel.open(Paths.get("d:\\out.txt"), StandardOpenOption.CREATE, StandardOpenOption.WRITE);

    // 2 写入文件
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put("helloworld".getBytes()); // position 10  limit 1024  capaicy1024
    buffer.flip();
    channel.write(buffer);
    // 3关闭
    channel.close();
  }
//读取存在乱码问题  GBK中文2个字节,当缓冲区为5时出现乱码
  public static void read() throws Exception {
    // 1创建FileChannel
    FileChannel channel = new RandomAccessFile("d:\\out.txt", "r").getChannel();
    // 2读取
    // 创建解码器
    CharsetDecoder decoder = Charset.forName("gbk").newDecoder();
    // 创建字符缓冲区
    CharBuffer charBuffer = CharBuffer.allocate(5);

    ByteBuffer buffer = ByteBuffer.allocate(5);
    while (channel.read(buffer) > 0) {
      buffer.flip();
      // 解码
      decoder.decode(buffer, charBuffer, false);

      charBuffer.flip();
      System.out.println(charBuffer.toString());
      buffer.compact(); // 压缩整理
      charBuffer.clear();
    }
    // 3关闭
    channel.close();
  }
}

复制文件

public class CopyFile {
    public static void main(String[] args) throws Exception {
        // 使用NIO实现copy
        // 1 创建FileChannel
        FileChannel readChannel = new RandomAccessFile("d:\\图片\\copy.jpg", "r").getChannel();
        FileChannel writeChannel = new RandomAccessFile("d:\\222.jpg", "rw").getChannel();
        // 2读取和写入(间接缓冲区,堆)
        // ByteBuffer buffer=ByteBuffer.allocate(1024);
        // 直接缓冲区 在直接内存中(物理空间)
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        while (readChannel.read(buffer) > 0) {
            buffer.flip();
            writeChannel.write(buffer);
            buffer.clear();
        }
        // 3关闭
        readChannel.close();
        writeChannel.close();
        System.out.println("复制完毕");
    }
}

间接缓冲区

直接缓冲区

10.内存映射文件

直接缓冲区的使用,可以提高读写的速度。但是直接缓冲区的创建和销毁的开销比较大,一般大文件操作或能显著提高读写性能时使用。

内存映射文件也属于直接缓冲区。

注意:如果文件超过2G,需要分多个文件映射。

public class MappedByteBufferDemo {
    public static void main(String[] args) throws Exception{
        //使用内存映射文件复制
        //1 创建FileChannel
        FileChannel readChannel=FileChannel.open(Paths.get("d:\\111.wmv"), StandardOpenOption.READ);
        FileChannel writeChannel=FileChannel.open(Paths.get("d:\\222.wmv"), StandardOpenOption.CREATE,StandardOpenOption.WRITE);
        //2 映射(直接内存)
        MappedByteBuffer map = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
        writeChannel.write(map);
        //3关闭
        readChannel.close();
        writeChannel.close();
        System.out.println("执行完毕");
    }
}

11.NIO实现网络编程

  • 传统的网络编程是一种阻塞模式,在一些任务小,高并发情况下效率不高。
  • 在NIO中提供了实现非阻塞式网络编程API:
  • ServerSocketChannel
    • ServerSocketChannel是一个基于通道的socket监听器,等同于ServerSocket类。
  • SocketChannel
    • SocketChannel是一个基于通道的客户端套接字,等同于Socket类。

阻塞式网络编程

服务端:

public class TcpServer {
    public static void main(String[] args) throws Exception {
        //1创建ServerSocketChannel
        ServerSocketChannel listener=ServerSocketChannel.open();
        //2绑定IP地址和端口号
        listener.bind(new InetSocketAddress("10.9.62.184", 8888));
        //3侦听、阻塞
        System.out.println("服务器已启动...");
        SocketChannel socketChannel = listener.accept();
        //4读取数据
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        socketChannel.read(buffer);
        //5处理数据
        buffer.flip();
        String data=new String(buffer.array(),0,buffer.limit());
        System.out.println(socketChannel.getRemoteAddress()+"说:"+data);
        //6关闭
        buffer.clear();
        socketChannel.close();
        listener.close();
    }
}

客户端:

public class TcpClient {
    public static void main(String[] args) throws Exception{
        //1创建SocketChannel
        SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("10.9.62.184", 8888));
        //2写入
        ByteBuffer buffer= ByteBuffer.allocate(1024);
        buffer.put("好久不见".getBytes());
        buffer.flip();
        socketChannel.write(buffer);
        //3关闭
        buffer.clear();
        socketChannel.close();
    }
}

Selector

  • Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。
  • Selector允许单线程处理多个Channel。
  • 仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。这样会大量的减少线程之间上下文切换的开销。
  • Selector:
    • 选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。
  • SelectionKey:
    • 选择键封装了特定的通道与特定的选择器的注册关系。
    • 选择键支持四种操作类型;
      • SelectionKey.OP_CONNECT
      • SelectionKey.OP_ACCEPT
      • SelectionKey.OP_READ
      • SelectionKey.OP_WRITE

非阻塞式网络编程

服务端:

public class ChatServer {
    public static void main(String[] args) throws Exception {
        // 1 创建ServerSocketChannel
        ServerSocketChannel listener = ServerSocketChannel.open();
        // 2 绑定地址和端口号
        listener.bind(new InetSocketAddress("10.9.62.184", 6666));
        // 3设置模式为非阻塞模式
        listener.configureBlocking(false);
        // 4创建Selector 选择器(轮询器)
        Selector selector = Selector.open();
        // 5把listener注册到selector
        listener.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("聊天室已启动...");
        // 6轮询处理(阻塞方法) 返回键的个数
        while (selector.select() > 0) {
            // 7获取所有的选择键
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> it = selectedKeys.iterator();
            while (it.hasNext()) {
                SelectionKey selectionKey = it.next();
                // 8如果有客户链接
                if (selectionKey.isAcceptable()) {
                    // 9接收请求,不是阻塞方法
                    SocketChannel socketChannel = listener.accept();
                    // 10设置非阻塞模式
                    socketChannel.configureBlocking(false);
                    // 11注册selector
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    // 读取数据
                    // 12根据selectionKey 获取SocketChannel
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len;
                    try {
                        while ((len = channel.read(buffer)) > 0) {// 如果客户端没有正常关闭,出现异常
                            buffer.flip();
                            String data = new String(buffer.array(), 0, buffer.limit());
                            System.out.println(channel.getRemoteAddress() + "说:" + data);
                            buffer.clear();
                        }
                        // 正常结束
                        if (len == -1) {
                            System.out.println(channel.getRemoteAddress() + "退出了聊天");
                            channel.close();
                        }
                    } catch (IOException e) {
                        System.out.println(channel.getRemoteAddress() + "异常退出了聊天");
                        channel.close();
                    }

                }
                it.remove();// 处理过的键删除...
            }
        }
        // 关闭listener
        listener.close();
    }
}

客户端:

public class ChatClient {
    public static void main(String[] args) throws Exception {
        // 1创建SocketChannel
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("10.9.62.184", 6666));
        // 2设置为非阻塞模式
        socketChannel.configureBlocking(false);
        Scanner input = new Scanner(System.in);
        // 3处理
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            String data = input.next();
            buffer.put(data.getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
            if (data.equals("886") || data.equals("byebye")) {
                break;
            }
        }
        // 4关闭
        buffer.clear();
        socketChannel.close();
    }
}

[/wppay]