Netty系列| Netty创始人告诉你为什么选择NIO

上篇带大家了解了IO的概念,同步异步,阻塞非阻塞的区别,没有看过的小伙伴可以去看下哦

本篇是Netty系列的第二篇,带大家来着重解析NIO,作为Netty的核心,它到底有什么特别的地方呢?

跟着狼王往下看....

前言

我们先来想一个问题,为什么Netty使用NIO,而不是AIO呢?

我想各位心中肯定有自己的答案了,让我们带着问题往下看吧

Netty为什么选择NIO

我们先来重温下这两个的区别:

NIO模型
同步非阻塞NIO有同步阻塞和同步非阻塞两种模式,一般讲的是同步非阻塞,服务器实现模式为一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

AIO模型
异步非阻塞
服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,注:AIO又称为NIO2.0,在JDK7才开始支持。

然后看下Netty作者在这个问题上的原话:

Not faster than NIO (epoll) on unix systems (which is true)
There is no daragram suppportUnnecessary threading model (too much abstraction without usage)

不比nio快在Unix系统上

不支持数据报

不必要的线程模型(太多没什么用的抽象化)

  • Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化

  • Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱,把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来

  • AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多

  • Linux上AIO不够成熟,处理回调结果速度跟不到处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)

  • NIO简介

    Java NIOjava 1.4 之后新出的一套IO接口,这里的的新是相对于原有标准的Java IO和Java Networking接口。NIO提供了一种完全不同的操作方式。

    NIO中的N可以理解为Non-blocking,不单纯是New。

    它支持面向缓冲的,基于通道的I/O操作方法。 随着JDK 7的推出,NIO系统得到了扩展,为文件系统功能和文件处理提供了增强的支持。由于NIO文件类支持的这些新的功能,NIO被广泛应用于文件处理。

    NIO与IO的区别

    1 Channels and Buffers(通道和缓冲区)

    IO是面向流的,NIO是面向缓冲区的

  • 标准的IO编程接口是面向字节流和字符流的。而NIO是面向通道和缓冲区的,数据总是从通道中读到buffer缓冲区内,或者从buffer缓冲区写入到通道中;( NIO中的所有I/O操作都是通过一个通道开始的。)

  • Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方;

  • Java NIO是面向缓存的I/O方法。将数据读入缓冲器,使用通道进一步处理数据。在NIO中,使用通道和缓冲区来处理I/O操作。

  • 2 Non-blocking IO(非阻塞IO)

    IO流是阻塞的,NIO流是不阻塞的

  • Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

  • Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了

  • 3 Selectors(选择器)

    NIO有选择器,而IO没有。

  • 选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。

  • 线程之间的切换对于操作系统来说是昂贵的。因此,为了提高系统效率选择器是有用的。

  • NIO三大核心组件

    NIO有3个实体:Buffer(缓冲区),Channel(通道),Selector(多路复用器)。

  • Buffer是客户端存放服务端信息的一个容器,服务端如果把数据准备好了,就会通过Channel往Buffer里面传。Buffer有7个类型:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。

  • Channel是客户端与服务端之间的双工连接通道。所以在请求的过程中,客户端与服务端中间的Channel就在不停的执行“连接、询问、断开”的过程。直到数据准备好,再通过Channel传回来。Channel主要有4个类型:FileChannel(从文件读取数据)、DatagramChannel(读写UDP网络协议数据)、SocketChannel(读写TCP网络协议数据)、ServerSocketChannel(可以监听TCP连接)

  • Selector是服务端选择Channel的一个复用器。Seletor有两个核心任务:监控数据是否准备好,应答Channel。具体说来,多个Channel反复轮询时,Selector就看该Channel所需的数据是否准备好了;如果准备好了,则将数据通过Channel返回给该客户端的Buffer,该客户端再进行后续其他操作;如果没准备好,则告诉Channel还需要继续轮询;多个Channel反复询问Selector,Selector为这些Channel一一解答。

  • Buffer

    Buffer常见子类

    ByteBuffer,存储字节数据到缓冲区,进行网络通信使用最频繁
     
    ShortBuffer,存储字符串数据到缓冲区 
     
    CharBuffer,存储字符数据到缓冲区 
     
    IntBuffer,存储整数数据到缓冲区 
     
    LongBuffer,存储长整型数据到缓冲区 
     
    DoubleBuffer,存储小数到缓冲区 
     
    FloatBuffer,存储小数到缓冲区

    Buffer类属性解析

    属性描述Capacity缓冲区容量,在缓冲区创建时被设定并且不能改变Limit表示缓冲区的当前读写终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的Position位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备Mark标记

    常见方法:

    public final int capacity( )//返回此缓冲区的容量
    public final int position( )//返回此缓冲区的位置
    public final Buffer position (int newPositio)//设置此缓冲区的位置
    public final int limit( )//返回此缓冲区的限制
    public final Buffer limit (int newLimit)//设置此缓冲区的限制
    public final Buffer mark( )//在此缓冲区的位置设置标记
    public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
    public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
    public final Buffer flip( )//反转此缓冲区
    public final Buffer rewind( )//重绕此缓冲区
    public final int remaining( )//返回当前位置与限制之间的元素数
    public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
    public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
     
    public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
    public abstract Object array();//返回此缓冲区的底层实现数组
    public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
    public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区

    ByteBuffer常用方法

    public abstract class ByteBuffer {
        //缓冲区创建相关api
        public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
        public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
        public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
        //构造初始化位置offset和上界length的缓冲区
        public static ByteBuffer wrap(byte[] array,int offset, int length)
         //缓存区存取相关API
        public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
        public abstract byte get (int index);//从绝对位置get
        public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
        public abstract ByteBuffer put (int index, byte b);//从绝对位置上put }

    Channel

    常见channel类

    1.FileChannel    //文件io操作

    2.DatagramChannel    //UDP数据读写

    3.ServerSocketChannel和SocketChannel  //TCP数据读写

     FileChannel常用方法

    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, WritableByteChannel target),把数据从当前通道复制给目标通道
    代码实践:

    通过FileChannel和ByteBuffer读写文件

    public void writeToFile() {
     
            try {
                //1.创建一个输出流,并通过输出流获取channel
                FileOutputStream out = new FileOutputStream("D:\\fileChannelTest.txt");
                final FileChannel channel = out.getChannel();
                //2.通过byteBuffer读取字符串并写入到channel中
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                buffer.put(("hello,world!").getBytes());
                buffer.flip(); //反转buffer的流向
                channel.write(buffer);
                channel.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
     
        public void readFromFile() {
     
            try {
                //1.获取输入流,并转化成channel
                File file = new File("D:\\fileChannelTest.txt");
                FileInputStream inputStream = new FileInputStream(file);
                final FileChannel channel = inputStream.getChannel();
     
                //2.从通道中读取数据到buffer,并输出到控制台
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                while(true) {  //循环读取直到全部读取到buffer中
                    buffer.clear(); //清空缓存区,只是把标记初始化,数据不会清楚
                    int read = channel.read(buffer);
                    if (read == -1) {  //读取完毕,退出循环
                        break;
                    }
                }
                System.out.println("content is " + new String(buffer.array()));
                channel.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
     
        }

    文件拷贝

    public void readFromFile() {
     
            try {
                //1.获取输入流,并获取对应的FileChannel
                File file = new File("D:\\fileChannelTest.txt");
                FileInputStream inputStream = new FileInputStream(file);
                final FileChannel channel = inputStream.getChannel();
     
                //2.从通道中读取数据到buffer,并输出到控制台
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                while(true) {  //循环读取直到全部读取到buffer中
                    buffer.clear(); //清空缓存区,只是把标记初始化,数据不会清楚
                    int read = channel.read(buffer);
                    if (read == -1) {  //读取完毕,退出循环
                        break;
                    }
                }
                System.out.println("content is " + new String(buffer.array()));
                channel.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
     
        }

    注意事项:通过ByteBuffer进行对象的传输时,写入的类型和读取的类型必须一致,否则可能会出现BufferUnderFlowException异常

    Selector

    Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,如果没有事件发生时,当前线程可以处理其他事情

    常见方法

    public abstract class Selector implements Closeable { 
    public static Selector open();//得到一个选择器对象
    public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将
    对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
    public Set selectedKeys();//从内部集合中得到所有的 SelectionKey 
    }

    NIO客户端和服务端代码实现

    服务端

    实现流程

    构建NIO服务端

    1.创建ServerSocketChannel,并绑定5555端口

    2.创建selector对象,并将ServerSocketChannel注册到seletor中,监听accept事件

    3.通过selectKey.isAcceptable判断是否有客户端建立连接,并注册连接的SocketChannel到selector,监听对应的read事件

    4.通过selectKey.isReadable判断通道是否发生读事件,并获取对应的socketChannel读到缓冲区中,并输出数据

    代码实现:

    public static void main(String[] args) throws  Exception{
            //创建ServerSocketChannel,-->> ServerSocket
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            InetSocketAddress inetSocketAddress = new InetSocketAddress(5555);
            serverSocketChannel.socket().bind(inetSocketAddress);
            serverSocketChannel.configureBlocking(false); //设置成非阻塞
     
            //开启selector,并注册accept事件
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
     
            while(true) {
                selector.select(2000);  //监听所有通道
                //遍历selectionKeys
                Set selectionKeys = selector.selectedKeys();
                Iterator iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if(key.isAcceptable()) {  //处理连接事件
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);  //设置为非阻塞
                        System.out.println("client:" + socketChannel.getLocalAddress() + " is connect");
                        socketChannel.register(selector, SelectionKey.OP_READ); //注册客户端读取事件到selector
                    } else if (key.isReadable()) {  //处理读取事件
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        SocketChannel channel = (SocketChannel) key.channel();
                        channel.read(byteBuffer);
                        System.out.println("client:" + channel.getLocalAddress() + " send " + new String(byteBuffer.array()));
                    }
                    iterator.remove();  //事件处理完毕,要记得清除
                }
            }
         }

    客户端

    1.创建客户端SocketChannel,并绑定ip和端口号

    2.通过ByteBuffer和SocketChannel发送消息到服务端

    代码实现:

    public static void main(String[] args) throws Exception{
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 5555);
     
            if(!socketChannel.connect(inetSocketAddress)) {
                while (!socketChannel.finishConnect()) {
                    System.out.println("客户端正在连接中,请耐心等待");
                }
            }
     
            ByteBuffer byteBuffer = ByteBuffer.wrap("hello,world".getBytes());
            socketChannel.write(byteBuffer);
            socketChannel.close();    }

    总结

    本文狼王带你了解了NIO了解了为什么Netty选择NIO,解析了NIO三大核心组件:Buffer(缓冲区),Channel(通道),Selector(多路复用器)

    从代码层面更直观的展示,并提供了相应的代码实现思路

    Netty系列的第二篇也结束了,通过这两篇的铺垫,下篇将会正式开始讲Netty,后续我会不断更新该系列文章,由浅至深,从简到难,多方位多角度的带你认识Netty这个网络框架!希望你们是我最好的观众!

    假如面试中你被问到这些,我相信你看了这篇一定能拨动面试官的心!

    乐于输出干货的Java技术公众号:Garnett的Java之路。公众号内有大量的技术文章、海量视频资源、精美脑图,不妨来关注一下!回复【资料】领取大量学习资源和免费书籍!

    你可能感兴趣的:(Netty系列| Netty创始人告诉你为什么选择NIO)