6

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

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=Mzg5MjQ5MzY2Mg%3D%3D&%3Bmid=2247485475&%3Bidx=1&%3Bsn=0dc48b1f26a8f31137758963a184fe45
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Urqaqeq.gif!mobile

上篇带大家了解了 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 suppport
Unnecessary threading model (too much abstraction without usage)

不比nio快在Unix系统上

不支持数据报

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

v2MV7nR.png!mobile
所以我们可以总结出以下四点:

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

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

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

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

NIO简介

Java NIO是 java 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<SelectionKey> 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<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> 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之路。公众号内有大量的技术文章、海量视频资源、精美脑图,不妨来 关注 一下! 回复【 资料 】领取大量学习资源和免费书籍!

niyeIvJ.jpg!mobile

转发朋友圈是对我最大的支持!

觉得有点东西就点一下“赞和在看”吧!感谢大家的支持了!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK