NIO学习

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中事件模型非常重要!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值