Netty基础:NIO三大组件

20 篇文章 0 订阅

一、NIO三大核心原理关系:

Selector、Channel和Buffer之间的关系图(简单版):

关系图说明:

  1. 每个channel都对应一个Buffer
  2. Selector对应一个线程,一个线程对应多个channel(连接)
  3. 上图反应了有三个channel注册到该Selector
  4. 程序切换到哪个channel,是由**事件(Event)**决定的
  5. Selector会根据不同的事件,在各个通道上切换
  6. Buffer本质上就是一个内存块,底层是一个数组
  7. 数据的读取或写入是通过Buffer进行的,这个特性和BIO有本质区别,BIO中要么是输入流,或者是输出流,不能双向。但是NIO的Buffer是可以读也可以写,只是需要flip()方法切换
  8. channel是双向的,可以返回底层操作系统的情况。

二、缓冲区Buffer

缓冲区Buffer,缓冲区本质上是一个可以读写数据的内存块,可以理解为是一个容器对象(含数组),该对象提供一组方法,****可以轻松的使用内存块。**缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的通道 ,但是读取或写入数据都必须经由Buffer**,如下图所示:

2.1 Buffer类及其子类

1. 在NIO中,Buffet是一个顶层父类,是一个抽象类,类的层级关系如下图所示,最常用的是ByteBuffer。

2. Buffer类定义了所有缓冲区都具有的四个属性,来提供关于其所包含的数据元素的信息:

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

属性

描述

capacity

容量,即可以容纳的最大数据量,在缓冲区创建时被设定并且不能改变

limit

表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作且极限是可以修改的。

position

位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变值,为下次读写准备。

mark

标记

示意图如下:

我们使用 IntBuffer intBuffer = IntBuffer.allocate(5); 语句,创建一个Buffer对象,其本质是在内存中创建一个容量为5的空间,且buffer刚创建时,capacity属性为5,limit属性也为5,position位置指向0。

刚创建时,capacity为5是因为该buffer本身创建时容量是5,而limit为5,是因为接下来的操作,也就是向Buffer写入数据的操作,不能超出容量5。我们接着向Buffer中写入数据,每写入一个数据,position指针自动+1向后移动一位,直到position与limit的值相等,无法继续操作。

**当我们要从Buffer中读取数据的时候,首先要使用flip()函数,将buffer转换,进行读写切换,**flip()函数执行的操作如下所示:

public final Buffer flip(){

????????//flip反转之后进行的操作不能超过上次的操作范围。如我们通过flip()函数,要读Buffer
????????//中的数据,那么读取的边界就是之前写入数据的边界,之前数据写到哪里了呢
? ? ? ? //position指明了位置
? ? ? ? limit = position;?

? ? ? ? position = 0;

? ? ? ? mark = -1;

????????return this;
}

2.2 Buffer类相关方法:

2.3 ByteBuffer

java的基本数据类型中,除了boolean之外,都有一个Buffer类型与之对应,但是,最常用的是ByteBuffer类,该类的主要方法如下:

三、通道Channel

3.1 基本介绍

**1)**NIO的通道类似于流,但是有一些区别:

  • 通道是双向的可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以从缓冲写数据

**2)**BIO中的 Stream是单向的,例如 FileInputStream 对象只能进行读取数据的操作,NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作

3)****ChannelNIO 中是一个接口 public interface Channel extends Closeable{}

4) 常用的Channel类有、FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel

5) FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于Tcp的数据读写。

无论再怎么想象,那些从来没有发生过的事情也总是无法深入,更何况,从来不敢想象你会回来!

3.2 FileChannel类

FileChannel类主要用来对本地文件进行IO操作,创建的方法如下:

  • public int read(ByteBuffer dst), 从通道读取数据并放到缓冲区中
  • public int write(ByteBuffer src), 把缓冲区中的数据写入到通道中
  • public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
  • _public long transferTo(long position, long count, WriteableByteChannel target),_把数据从当前通道复制给目标通道。

案例1:本地文件写数据

    public static void main(String[] args) throws IOException {
        String str = "wangy";
        //创建一个输出流 ->用于获取到一个channel
        FileOutputStream fileOutputStream = new FileOutputStream("d:\file01.txt");

        //通过fileOutputStream获取对应的FileChannel
        //这里体现了多态,此处fileChannel真是类型是FileChannelImpl, FileChannel是一个抽象类
        FileChannel fileChannel = fileOutputStream.getChannel();

        //创建ByteBuffer缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //将str数据放入到ByteBuffer中
        byteBuffer.put(str.getBytes(StandardCharsets.UTF_8));

        //将byteBuffer中的数据写入到channel中(该操作对byteBuffer来说是将数据读出来,对fileChannel通道来说,是将缓冲区读出的数据写入到通道)
        byteBuffer.flip(); // 反转缓冲区,由写变为读
        fileChannel.write(byteBuffer);

        //关闭流
        fileOutputStream.close();
    }

案例2:本地文件读数据

    public static void main(String[] args) throws IOException {
        //创建文件输入流
        File file = new File("d:\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(file);

        //通过fileInputStream 获取到对应的FileChannel ->实际类型为FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();

        //创建ByteBuffer缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());

        //将通道的数据读入到Buffer
        fileChannel.read(byteBuffer);

        //将byteBuffer的字节数据转为string
        System.out.println(new String(byteBuffer.array()));

        //关闭流
        fileInputStream.close();
    }

案例三:使用一个Buffer完成文件拷贝

/**
 * 拷贝
 */
public class NIOFileChannel03 {
    public static void main(String[] args) throws IOException {
        //文件对象
        File file1= new File("d:\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(file1);
        FileChannel fileChannel01 = fileInputStream.getChannel(); //由输入流获取Channel通道

        //创建缓冲对象
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)file1.length());

        //创建文件输出流对象
        File file2 = new File("d:\file02.txt");
        FileOutputStream fileOutputStream = new FileOutputStream(file2);
        FileChannel fileChannel2 = fileOutputStream.getChannel(); //由输出流获取Channel通道
        //循环读取
        while(true){
            /**
             *     public Buffer clear() {
             *         position = 0;
             *         limit = capacity;
             *         mark = -1;
             *         return this;
             *     }
             */
            byteBuffer.clear(); //清空buffer,复位,非常重要
            //从通道读取数据,放入到缓冲区
            int read = fileChannel01.read(byteBuffer);
            if(read == -1){ //读完所有数据
                break;
            }
            //将buffer中的数据写入到fileChannel02,即写入到文件file02.txt
            byteBuffer.flip();
            fileChannel2.write(byteBuffer);
        }

        //关闭流
        fileInputStream.close();
        fileOutputStream.close();
    }
}

案例四:transferFrom方法拷贝文件

使用FileChannel(通道)和方法transferFrom,完成文件的拷贝。

public class NIOFileChannel04 {

    public static void main(String[] args) throws IOException {
        //创建相关的流
        File file1= new File("D:\星空.jpg");
        FileInputStream fileInputStream = new FileInputStream(file1);
        File file2 = new File("D:\星空2.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream(file2);

        //获取各个流对应的filechannel
        FileChannel sourceChannel = fileInputStream.getChannel();
        FileChannel destChannel = fileOutputStream.getChannel();

        //使用trans
        destChannel.transferFrom(sourceChannel,0,sourceChannel.size());

        //关闭相关流
        sourceChannel.close();;
        destChannel.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}

3.3 Buffer和Channel的注意事项和细节

  • ByteBuffer支持类型化的put和get, put放入的是什么数据类型,get就应该使用相应的数据类型来取出,负责可能有BufferUnderflowExceptiion异常。

    public class NIOByteBufferPutGet {
    public static void main(String[] args) {
    //创建一个Budder
    ByteBuffer buffer = ByteBuffer.allocate(64);

        //以类型化方式放入数据
        buffer.putInt(100);
        buffer.putLong(9L);
        buffer.putChar('w');
        buffer.putShort((short)4);
    
        //取出数据
        buffer.flip();
        System.out.println(buffer.getInt());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getChar());
        System.out.println(buffer.getShort());
    }
    

    }

  • 可以将一个普通Buffer转成只读Buffer

    public class ReadOnlyBuffer {
    public static void main(String[] args) {
    //创建一个Buffer
    ByteBuffer buffer = ByteBuffer.allocate(64);

        for(int i=0;i<64;i++){
            buffer.put((byte)i);
        }
    
        //读取
        buffer.flip();
        //得到一个只读的Buffer
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer(); //由ByteBuffer获取一个只读的Buffer
        while(readOnlyBuffer.hasRemaining()){
            System.out.println(readOnlyBuffer.get());
        }
    
        readOnlyBuffer.put((byte)100); //抛出异常,ReadOnlyBufferException
    }
    

    }

  • NIO提供MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改(不用拷贝),而如何同步到文件,由NIO来完成。

    /**

    • 说明:
    •  MappedByteBuffer可以让文件直接在内存(堆外内存)中修改,操作系统不需要拷贝文件
      

    /
    public class MappedByteBufferTest {
    public static void main(String[] args) throws IOException {
    //准备文件
    RandomAccessFile randomAccessFile = new RandomAccessFile(“D:\a.txt”,“rw”);
    //获取对应的通道
    FileChannel channel = randomAccessFile.getChannel();
    /
    *
    * 参数1:FileChannel.MapMode.READ_WRITE 使用读写模式
    * 参数2: 0, 可以直接修改的起始位置
    * 参数3: 5, 映射到内存的大小(不是索引位置),即将a。txt的多少个字节映射到内存,可以直接修改的范围就是0-5(最多修改5个字节,索引到4)
    *
    * MappedByteBuffer是一个抽象类,此处实际类型是DirectByteBuffer
    */
    MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE,0,5);

        mappedByteBuffer.put(0,(byte) 'H');
        mappedByteBuffer.put(3,(byte)'5');
    
        randomAccessFile.close();
    }
    

    }

  • 前面的读写操作,都是通过一个Buffer完成的,NIO还支持多个Budder(即Buffer数组)完成读写操作即Scattering和Gathering

    /**

    • Scattering:将数据写入到buffer时,可以采用Buffer数组,依次写入

    • Gathering: 从Buffer读取数据时,可以采用Buffer数组,依次读
      */
      public class ScatteringAndGatheringTest {
      public static void main(String[] args) throws IOException {
      //使用ServerSocketChannel和SocketChannel
      ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);

       //绑定端口到Socket,并启动
       serverSocketChannel.socket().bind(inetSocketAddress);
      
       //创建Buffer数组
       ByteBuffer[] byteBuffers = new ByteBuffer[2];
       byteBuffers[0] = ByteBuffer.allocate(5);
       byteBuffers[1] = ByteBuffer.allocate(3);
      
       //等待客户端的连接
       SocketChannel socketChannel = serverSocketChannel.accept();
      
       int messageLength = 8; //假定从客户端接收8个字节
       //循环读取数据
       while(true){
           int byteRead = 0;
           while(byteRead<messageLength){
               long l = socketChannel.read(byteBuffers);
               byteRead+=l; //累计读取到的字节数
               System.out.println("byteRead="+byteRead);
               //使用流打印,看看当前的buffer的position和limit
               Arrays.asList(byteBuffers).stream().map(buffer->"position="+buffer.position()+",Limit="+buffer.limit()).forEach(System.out::println);
           }
      
           //将所有的Budder进行flip反转
           Arrays.asList(byteBuffers).forEach(buffer-> buffer.flip());
      
           //将数据读出,显示到客户端
           long byteWrite = 0;
           while(byteWrite<messageLength){
               long l = socketChannel.write(byteBuffers);
               byteWrite+=l;
           }
      
           //将所有的buffer进行复位
           Arrays.asList(byteBuffers).forEach(buffer->buffer.clear());
           System.out.println("byteRead:="+byteRead+" byteWrite="+byteWrite+", MessageLength"+messageLength);
       }
      

      }
      }

四、Selecter选择器

Java NIO,是非阻塞的IO,可以使用一个线程,处理多个客户端连接,其就是通过Selector(选择器实现的)。Selector选择器特性如下:

  • **Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,**便获取事件,然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  • 只有在连接/通道真正有读写事件发生时,才会进行读写,这大大减少了系统开销,并且不必 为每个连接都创建一个线程,不用去维护多个线程。避免了多线程之间的上下文切换导致的开销

4.1 Selector示意图和特点说明

Nonblocking I/O

特点说明如下:

  • Netty的IO线程NioEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
  • 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
  • 线程通常将非阻塞IO的空闲时间用于在其他通道上执行I/O操作,所以单个线程可以管理多个输入和输出通道。
  • 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起
  • 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

4.2 Selector 类相关方法

Selector类是一个抽象类,常用方法及说明如下:

注意事项:

  1. NIO中的ServerSocketChannel功能类似ServerSocket,ServerChannel功能类似Socket。
  2. Selector相关方法说明
    1. selector.selector() //阻塞
    2. selector.select(1000) //阻塞1000毫秒,在1000毫秒后返回
    3. selector.wakeup(); //唤醒selevctor
    4. selector.selectNow() //不阻塞,立马返回

4.3NIO 非阻塞网络编程原理分析图

NIO非阻塞网络编程相关的 SelectorSelectionKeyServerSocketChannel、及SocketChannel关系梳理图**:**

上图说明:

  1. 当客户端请求连接时,会通过ServerSocketChannel得到SocketChannel;
  2. 然后,将SocketChannel注册到Selector上;通过函数**register(Selector sel, int ops)**注册。一个selector上可以注册多个SocketChannel;
  3. SocketChannel注册到Selector上,会返回一个SelectionKey,会和该Selector关联;
  4. Selector通过_select()_方法,返回有事件发生的通道的个数并进一步得到有事件发生的SelectionKey
  5. 在获取到有事件发生的SelectionKey之后,通过_channel()_可以反向获取到SocketChannel;
  6. 最终,可以通过获取到的有事件发生的SocketChannel,完成相关的业务处理
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值