Netty框架的基本构成

什么是Netty

官网上说Netty是一个异步的事件驱动的网络应用框架,便于我们快速开发稳定高性能的服务端及客户端。具体来说,Netty是一种让我们快速简单的开发网络程序的NIO框架,它大大的简化而且流式化了像TCP和UDP Socket服务端的网络编程。

嗯,就理解Netty是NIO的一种实现就好了,原生的NIO编程太难用,所以就有了Netty这个框架封装一把,让我们用起来更简单顺手。

同时官网还给出了Netty的基本构成,如下图:

image.png

为什么用Netty

一因为快,二因为好用,几乎我们用的所有中间件框架都不外乎这两个原因。

前面已经说了,原生手写NIO太麻烦,才产生了Netty,这算原因之一,另外它的好用体现在于可拓展事件模型;支持 TCP、UDP、HTTP、WebSocket 等协议;提供安全传输、压缩、大文件传输、编解码支持等等。

还有就是快,快除了NIO非阻塞的特性,还有就是采用了上面图片里显示的零拷贝技术,所谓零拷贝就是不在堆内存,而是另外开一块区域去读取数据(所谓的使用了堆外内存),通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。这样做最小化不必要的内存复制,减少了资源的消耗。

Netty里面有哪些东西

之前一篇提到了Netty中的Reactor模式,Netty的构成其实也是围绕这个模式的实现来的,主要包括了Selector,EventLoopGroup/EventLoop、ChannelPipeline,当然除了和模式相关的这三个组件,还有一个Buffer,到这里由和我们之前说的NIO实现的三大构成Selector,Channel,Buffer对上了,以下我们分别来看下这几样东西。

Selector:

Selector即为NIO中提供的SelectableChannel多路复用器,充当着demultiplexer的角色,这里Netty基本使用了NIO中的Selector一套,封装了一个实现类SelectedSelectionKeySetSelector,实现如下:

final class SelectedSelectionKeySetSelector extends Selector {
    private final SelectedSelectionKeySet selectionKeys;
    private final Selector delegate;

    SelectedSelectionKeySetSelector(Selector delegate, SelectedSelectionKeySet selectionKeys) {
        this.delegate = delegate;
        this.selectionKeys = selectionKeys;
    }

    public boolean isOpen() {
        return this.delegate.isOpen();
    }

    public SelectorProvider provider() {
        return this.delegate.provider();
    }

    public Set keys() {
        return this.delegate.keys();
    }

    public Set selectedKeys() {
        return this.delegate.selectedKeys();
    }

    public int selectNow() throws IOException {
        this.selectionKeys.reset();
        return this.delegate.selectNow();
    }

    public int select(long timeout) throws IOException {
        this.selectionKeys.reset();
        return this.delegate.select(timeout);
    }

    public int select() throws IOException {
        this.selectionKeys.reset();
        return this.delegate.select();
    }

    public Selector wakeup() {
        return this.delegate.wakeup();
    }

    public void close() throws IOException {
        this.delegate.close();
    }
}

ChannelPipeline:

ChannelPipeline其实是担任着Reactor模式中的请求处理器这个角色。ChannelPipeline的默认实现是DefaultChannelPipeline,DefaultChannelPipeline本身维护着一个用户不可见的tail和head的ChannelHandler,他们分别位于链表队列的头部和尾部。tail在更上层的部分,而head在靠近网络层的方向。

在Netty中关于ChannelHandler有两个重要的接口,ChannelInBoundHandler和ChannelOutBoundHandler。inbound可以理解为网络数据从外部流向系统内部,而outbound可以理解为网络数据从系统内部流向系统外部。用户实现的ChannelHandler可以根据需要实现其中一个或多个接口,将其放入Pipeline中的链表队列中,ChannelPipeline会根据不同的IO事件类型来找到相应的Handler来处理,同时链表队列是责任链模式的一种变种,自上而下或自下而上所有满足事件关联的Handler都会对事件进行处理。

ChannelInBoundHandler对从客户端发往服务器的报文进行处理,一般用来执行半包/粘包,解码,读取数据,业务处理等;ChannelOutBoundHandler对从服务器发往客户端的报文进行处理,一般用来进行编码,发送报文到客户端。

下图是对ChannelPipeline执行过程的说明:
image.png

EventLoopGroup/EventLoop:

Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO线程EventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险。这也解释了为什么Netty线程模型去掉了Reactor主从模型中线程池。

具体的我们可以看一个NioEventLoopGroup类实现,可以看到这里提供了线程,选择策略等参数:

public class NioEventLoopGroup extends MultithreadEventLoopGroup {
    public NioEventLoopGroup() {
        this(0);
    }

    public NioEventLoopGroup(int nThreads) {
        this(nThreads, (Executor)null);
    }

    public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) {
        this(nThreads, threadFactory, SelectorProvider.provider());
    }

    public NioEventLoopGroup(int nThreads, Executor executor) {
        this(nThreads, executor, SelectorProvider.provider());
    }

    public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory, SelectorProvider selectorProvider) {
        this(nThreads, threadFactory, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
    }

    public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory, SelectorProvider selectorProvider, SelectStrategyFactory selectStrategyFactory) {
        super(nThreads, threadFactory, new Object[]{selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()});
    }

    public NioEventLoopGroup(int nThreads, Executor executor, SelectorProvider selectorProvider) {
        this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
    }

    public NioEventLoopGroup(int nThreads, Executor executor, SelectorProvider selectorProvider, SelectStrategyFactory selectStrategyFactory) {
        super(nThreads, executor, new Object[]{selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()});
    }

    public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, SelectorProvider selectorProvider, SelectStrategyFactory selectStrategyFactory) {
        super(nThreads, executor, chooserFactory, new Object[]{selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()});
    }

    public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, SelectorProvider selectorProvider, SelectStrategyFactory selectStrategyFactory, RejectedExecutionHandler rejectedExecutionHandler) {
        super(nThreads, executor, chooserFactory, new Object[]{selectorProvider, selectStrategyFactory, rejectedExecutionHandler});
    }

    public void setIoRatio(int ioRatio) {
        Iterator var2 = this.iterator();

        while(var2.hasNext()) {
            EventExecutor e = (EventExecutor)var2.next();
            ((NioEventLoop)e).setIoRatio(ioRatio);
        }

    }

    public void rebuildSelectors() {
        Iterator var1 = this.iterator();

        while(var1.hasNext()) {
            EventExecutor e = (EventExecutor)var1.next();
            ((NioEventLoop)e).rebuildSelector();
        }

    }

    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        return new NioEventLoop(this, executor, (SelectorProvider)args[0], ((SelectStrategyFactory)args[1]).newSelectStrategy(), (RejectedExecutionHandler)args[2]);
    }
}

Buffer:

Netty提供的经过扩展的Buffer相对NIO中的有个许多优势,作为数据存取非常重要的一块,我们来看看Netty中的Buffer有什么特点。

1)ByteBuf读写指针

在ByteBuffer中,读写指针都是position,而在ByteBuf中,读写指针分别为readerIndex和writerIndex,直观看上去ByteBuffer仅用了一个指针就实现了两个指针的功能,节省了变量,但是当对于ByteBuffer的读写状态切换的时候必须要调用flip方法,而当下一次写之前,必须要将Buffe中的内容读完,再调用clear方法。每次读之前调用flip,写之前调用clear,这样无疑给开发带来了繁琐的步骤,而且内容没有读完是不能写的,这样非常不灵活。相比之下我们看看ByteBuf,读的时候仅仅依赖readerIndex指针,写的时候仅仅依赖writerIndex指针,不需每次读写之前调用对应的方法,而且没有必须一次读完的限制。

2)零拷贝

Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。

Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

3)引用计数与池化技术

在Netty中,每个被申请的Buffer对于Netty来说都可能是很宝贵的资源,因此为了获得对于内存的申请与回收更多的控制权,Netty自己根据引用计数法去实现了内存的管理。Netty对于Buffer的使用都是基于直接内存(DirectBuffer)实现的,大大提高I/O操作的效率,然而DirectBuffer和HeapBuffer相比之下除了I/O操作效率高之外还有一个天生的缺点,即对于DirectBuffer的申请相比HeapBuffer效率更低,因此Netty结合引用计数实现了PolledBuffer,即池化的用法,当引用计数等于0的时候,Netty将Buffer回收致池中,在下一次申请Buffer的没某个时刻会被复用。

参考资料:https://blog.csdn.net/tugangkai/article/details/80560495
https://netty.io/

你可能感兴趣的:(Netty框架的基本构成)