Netty基础:NIO三大组件

一、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.3 NIO 非阻塞网络编程原理分析图

        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,完成相关的业务处理
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值