欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

NIO学习

程序员文章站 2022-07-04 08:45:18
...

1.NIO

public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);//大小为10的一个缓冲区

        for (int i = 0; i < buffer.capacity(); i++) { //小位缓冲区的大小
            int randomNumber = new SecureRandom().nextInt(20);  //生成随机数更好的方法
            buffer.put(randomNumber);
        }

        buffer.flip();//翻转 io流的角色变换

        while(buffer.hasRemaining()) { //buffer是否有剩余
            System.out.println(buffer.get());
        }
    }
//12 0 4 6 13 11 16 15 0 4

1.NIO的体系分析

  • java.io中最为核心的一个概念是流,面向流编程,java中,一个流要么是输入流,要么是输出流,不可能同时既是输入流又是输出流.
  • java.nio中拥有3个核心概念:Selector,Channel与Buffer,面向块(block)或是缓冲区(buffer)编程的.Buffer本身就是一块内存,底层实现上,它实际上是个数组,数据的读丶写都是通过buffer来实现的.
  • 除了数组以为,buffer还提供了对数据的结构化访问方式,并且可以追踪到系统的读写过程.
  • Java中的7种原生数据类型都有各自对应的Buffer类型,如IntBuffer,LongBuffer等,并没有BooleanBuffer类型.
  • channel指的是可以向其写入数据或是从中读取数据的对象,它类似于java.stream.但是Channel是双向的,既可以读取流也可以是写入流.
  • 所有数据的读写都是通过Buffer来进行的,永远不会出现直接向Channel写入数据的情况,或是直接从Channel读取数据的情况.
  • Channel反映出底层操作系统的真实情况:在linux系统中,底层操作系统的通道就是双向.
//从文件读取数据
public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("NioTest2.txt");
        FileChannel fileChannel = fileInputStream.getChannel(); //文件流获取通道对象

        ByteBuffer byteBuffer = ByteBuffer.allocate(512); //创建一个byteBuffer对象存储
        fileChannel.read(byteBuffer); //将fileChannel读到ByteBuffer中去,主要是channel从流中读取.

        byteBuffer.flip();

        while (byteBuffer.hasRemaining()) {
            byte b = byteBuffer.get();
            System.out.println("Character: "+ (char)b);
        }

        fileInputStream.close();
    }
//把一个信息写入系统文件中
public static void main(String[] args) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("NioTest3.txt");
        FileChannel fileChannel = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        byte[] messages = "hello world welcome, da jia hao".getBytes();
        for (int i = 0; i < messages.length; i++) {
            byteBuffer.put(messages[i]);
        }


        byteBuffer.flip();

        fileChannel.write(byteBuffer); //byteBuffer写入channel中,理解为写入到读取的流中.

        fileOutputStream.close();
    }

不管是读还是写,都跟Channel关联着.都是Channel对buffer的操作.

2.Buffer.filp()方法

public final Buffer flip() { //buffer.filp()方法源码
    // Invariants: 0 <= mark <= position <= limit <= capacity
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

buffer中的flip方法涉及到bufer中的Capacity,Position和Limit三个概念。其中Capacity在读写模式下都是固定的,就是我们分配的缓冲大小,Position类似于读写指针,表示当前读(写)到什么位置,Limit在写模式下表示最多能写入多少数据,此时和Capacity相同,在读模式下表示最多能读多少数据,此时和缓存中的实际数据大小相同。在写模式下调用flip方法,那么limit就设置为了position当前的值(即当前写了多少数据),postion会被置为0,以表示读操作从缓存的头开始读。也就是说调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。

  • NIO Buffer中的3个重要状态属性含义:

    • position:
    • limit:
    • capacity:指buffer包含元素的个数.
  • InBuffer.allocate();完成初始化动作:

    • 调用HeapIntBuffer构造函数
    • position = 0;limit = capacity;
  • Buffer.put()方法,参数有hb[]数组,向buffer放数据,然后position++;

  • Buffer.clear();回到初始状态/重新读取会进行覆盖所以相当于清空.

3.读取文件,文件通道法

public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("input.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("output.txt");

        FileChannel inputChannel = fileInputStream.getChannel();
        FileChannel outputChannel = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(4);

        while(true) {
            byteBuffer.clear(); //如果注释掉这句会怎么样???		*会出现死循环,不停的读取*

            int read = inputChannel.read(byteBuffer);

            if (read == -1) {
                break;
            }

            byteBuffer.flip();

            outputChannel.write(byteBuffer);
        }
        inputChannel.close();
        outputChannel.close();
    }

fileChannel在调用read方法时会先对position和limit的值进行检查,如果limit与position的值相等或者position大于limit则直接返回0,不会再调用操作系统的read方法了。

所以read == 0,不等于-1就不会跳出,然后进行filp(),position指针指向0,继续write(),所以就会一直写.

  • NIO读取文件涉及的3个步骤:
    • 1.从FileInputStream获取到FileChannel对象.
    • 2.创建Buffer
    • 3.将数据Channel读取到Buffer中.
  • 绝对方法与相对方法的含义:
    • 1.相对方法:limit值与position值会在操作时被考虑到 //filp()
    • 2.绝对方法:完全忽略掉limit值和position值 //get() put()

4.Buffer深入了解

        ByteBuffer byteBuffer = ByteBuffer.allocate(64);
        //ByteBuffer类型化的put和get方法
        byteBuffer.putInt(10);
        byteBuffer.putLong(5000000000L);
        byteBuffer.putDouble(14.5659);
        byteBuffer.putChar('我');
        byteBuffer.putShort((short)2);
        byteBuffer.putChar('你');

        byteBuffer.flip();

        System.out.println(byteBuffer.getInt());
        System.out.println(byteBuffer.getLong());
        System.out.println(byteBuffer.getDouble());
        System.out.println(byteBuffer.getChar()); //改为getInt() 会报BufferUnderflowException
        System.out.println(byteBuffer.getShort());
        System.out.println(byteBuffer.getChar());
        System.out.println(byteBuffer);
		ByteBuffer buffer = ByteBuffer.allocate(10);

        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte)i);
        }

        buffer.position(2);
        buffer.limit(6);
        //Slice Buffer和原有的buffer底层数据就是一份,是原有buffer的备份
        ByteBuffer sliceBuffer = buffer.slice();

        for (int i = 0; i < sliceBuffer.capacity(); i++) {
            byte b = sliceBuffer.get(i);
            b *= 2;
            sliceBuffer.put(i,b);
        }

        buffer.position(0);
        buffer.limit(buffer.capacity());

        while (buffer.hasRemaining()) {
            System.out.println(buffer.get());
        }
  • 1.Slice Buffer和原有的buffer底层数据就是一份,是原有buffer的备份.
		ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println(buffer.getClass());

        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte)i);
        }

        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());

        readOnlyBuffer.position(0);
//        readOnlyBuffer.put((byte)2);
  • 1.两个buffer的pos.lim.cap都是独立的.
  • 2.只读buffer,我们可以随时将一个普通Buffer调用asReadOnlyBuffer方法返回一个只读Buffer,但是不能将一个只读Buffer转换为读写Buffer

5.NIO的堆外内存和零拷贝

1.Buffe.allocateDirect()的方法:返回一个DirectByteBuffer类

return new DirectByteBuffer(capacity);

2.DirectByteBuffer肯定有一个对象可以访问堆外内存(native上),就是Long address,保存的是堆外内存的地址

3.ByteBuffery是在堆上产生的,数据存储在堆上,而IO设备要进行交互,堆会将数据拷贝到操作系统生成的堆外内存上进行交互.称为间接缓冲区.

4.如果不进行拷贝,直接进行堆内存和IO设备交互,会发生,如果正在交互中,发生GC,会将一部分不用的数据标记整理进行压缩腾出大块内存给新的Java对象,会造成本来的不被回收的数据压缩好数据顺序变化乱套了.不能进行GC,会出现OutMarayErrorEX异常(内存溢出).

5.DirectByteBuffer同过address指向操作系统生成的堆外内存,直接将数据存储到堆外内存的区域中,跟IO设备进行交互,省去一步拷贝过程,称为零拷贝.数据用完后,操作系统会自动释放生成的堆外内存.

6.内存映射文件

1.MappedByteBuffer:内存映射文件是一种允许Java程序直接从内存访问的一种特殊文件.

2.我们可以将整个文件的一部分映射到内存当中,由操作系统来负责相关的页面请求,并且将内存的修改写入文件当中,我们的应用成为只需要处理内存的数据,可以实现非常迅速的IO操作.

3.用于内存映射的内存本身是在Java的堆外内存.

		//第二个参数读写状态rw,可读可写
        RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw");
        FileChannel fileChannel = randomAccessFile.getChannel();

        //根据map获取内存映射文件,参数为:模式(读写模式),起始位置(起始映射),映射大小
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);

        mappedByteBuffer.put(0,(byte)'a');
        mappedByteBuffer.put(3,(byte)'b');

        randomAccessFile.close();
  • 可以直接将文件在内存里面去修改,数据改变.
		//文件锁的概念
		RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest10.txt","rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        //获取文件锁,参数3个为:锁的起始位置,锁多少个,是否为共享锁
        FileLock fileLock = fileChannel.lock(3,6,true);
        System.out.println("valid: " + fileLock.isValid());//是否有效  //true
        System.out.println("lock: " + fileLock.isShared());			//true

        fileLock.release();
        randomAccessFile.close();

7.Buffer的Scattering和Gathering(分散读取和聚集写入)

		//建立客户端和服务端的链接 aio异步io
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress address = new InetSocketAddress(8899);
        serverSocketChannel.socket().bind(address);

        int messageLength = 2 + 3 + 4;
        ByteBuffer[] buffers = new ByteBuffer[3];

        buffers[0] = ByteBuffer.allocate(2);
        buffers[1] = ByteBuffer.allocate(3);
        buffers[2] = ByteBuffer.allocate(4);

        SocketChannel socketChannel = serverSocketChannel.accept();

        while (true) {
            //进行读操作
            int bytesRead = 0;
            while (bytesRead < messageLength) {
                long r = socketChannel.read(buffers);
                bytesRead += r;
                System.out.println("bytesRead : " + bytesRead);
               //System.out.println(Arrays.toString(buffers));
                Arrays.asList(buffers).stream().map(buffer -> "pos: " + buffer.position()+
                        ", limit: " + buffer.limit()).forEach(System.out::println);
            }

            Arrays.asList(buffers).forEach(buffer -> buffer.flip());
            //进行写操作
            long bytesWritten = 0;
            while (bytesWritten < messageLength) {
                long r = socketChannel.write(buffers);
                bytesWritten += r;
            }
            Arrays.asList(buffers).forEach(buffer -> buffer.clear());

            System.out.println("bytesRead :" + bytesRead + ", bytesWritten: " + bytesWritten +
                    ", messageLength: " + messageLength);
        }
  • 采用telnet localhost 8899去测试

8.字符集Charset

编码: 字符串 -> 字节数组

解码: 字节数组 -> 字符串

Map<String,Charset>Charset.availableCharsets(); //包含的字符集
		Charset cs1 = Charset.forName("GBK");
        //获取编码器
        CharsetEncoder charsetEncoder = cs1.newEncoder();
        //获取解码器
        CharsetDecoder charsetDecoder = cs1.newDecoder();
        
        CharBuffer charBuffer = CharBuffer.allocate(1024);
        charBuffer.put("我爱学习");
        charBuffer.flip();

        //编码
        ByteBuffer byteBuffer = charsetEncoder.encode(charBuffer);

        for (int i = 0; i < 8; i++) {
            System.out.println(byteBuffer.get());
        }
        byteBuffer.flip();
        //解码
        CharBuffer charBuffer1 = charsetDecoder.decode(byteBuffer);
        System.out.println(charBuffer1.toString());

输出:
-50
-46
-80
-82
-47
-89
-49
-80
我爱学习

9.Selector源码分析

1.回顾传统IO的socket写法,缺点:连接一个客户端就要生成一个线程,在一个操作系统上,可运行的线程是有限的.

//服务端
ServerSocket serverSocket = .....
serverSocket.bind(8899);

while(true) {
    Socket socket - serverSocket.accept();//阻塞方法
    new Tread(socket);
    run(){
        socket.getInputStream().
            ....
            ....
    }
}

//客户端
Socket socket = new Socket("ip","port");
socket.connect();

2.selector选择器的概述

  • selector可以调用这个类的open方法创建.open本身是会从系统的选择提供器中提供一个选择器.
Selector selector = Selector.open();
  • 将selector注册到Channel上,这个步骤是通过SelectionKey进行判断表示的,一个选择器会包含3种SelectionKey.
    • key set: 对应selector和channel注册完成后的所有的key(事件的可能性).
    • selected-key: 是key set中包含的所有key的一个子集.在所有里面只选择我们要的.
    • cancelled-key: 表示在key set中选择好的key,但是后来取消了,保存在cancelled-key集合中.
  • select()是一个阻塞的方法,当在channel上发生一个或多个事件时,返回的对象是一个selectKey的集合.其中的 每个selectkey都标识者channel通道上事件感兴趣的部分.
  • 我们系统所用的selector对象实际上就是sun.nio.ch.DefaultSelectorSelectorProvider.create()创建的KQueueSelectorProvider创建的一个selector对象.
//由此证明
sout(SelectorProvider.provider().getClass());
sout(sun.nio.ch.DefaultSelectorSelectorProvider.create())
   
输出:
sun.nio.ch.KQueueSelectorProvider
sun.nio.ch.KQueueSelectorProvider
//是同一个
  • 体现出NIO编程和传统编程的显著特点,主要就是通过selector实现的
  • 5个channel跟客户发起连接的socketchannel,创建好selector从里面开始去选择,每一个事件一旦产生后optionkey就出来了,携带者与每一个channel所关联的特性,我们通过channel方法可以获取到数据.
  • 在整个NIO中事件模型非常重要!

上一篇: NIO学习

下一篇: NIO学习一、NIO简介