Netty整体架构

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013857458/article/details/82527722

每学习一门语言或者一种框架,写一个Hello World都是一个不错的入门,上一篇我们用Netty实现了一个简单的Hello World,简单粗暴直接上代码,这篇文章将从Netty整体结构上介绍。本人文笔不好,平时记云笔记只需自己能看懂,写这种公开博客功力尚浅,望看到博客的朋友见谅。

Netty概述

Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持。作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。

Netty 利用 Java 高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 构建一个客户端/服务端,其具有高并发、传输快、封装好等特点。

  • 高并发
    Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高 。

  • 传输快
    Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。

  • 封装好
    Netty封装了NIO操作的很多细节,提供易于使用的API,还有心跳、重连机制、拆包粘包方案等特性,使开发者能能够快速高效的构建一个稳健的高并发应用。

Netty的线程模型

并发系统可以采用多种并发编程模型来实现。并发模型指定了系统中的线程如何通过协作来完成分配给它们的作业。不同的并发模型采用不同的方式拆分作业,同时线程间的协作和交互方式也不相同。

对于网络请求一般可以分为两个处理阶段,一是接收请求任务,二是处理网络请求。根据不同阶段处理方式分为以下几种线程模型:

1. 串行化处理模型

这里写图片描述
这个模型中用一个线程来处理网络请求连接和任务处理,当worker接受到一个任务之后,就立刻进行处理,也就是说任务接受和任务处理是在同一个worker线程中进行的,没有进行区分。这样做存在一个很大的问题是,必须要等待某个task处理完成之后,才能接受处理下一个task。

而通常情况下,任务的处理过程会比任务的接受流程慢得多。例如在处理任务的时候,我们可能会需要访问远程数据库,这属于一种网络IO。通常情况下IO操作是比较耗时的,这直接影响了下一个任务的接受,而且通常在IO操作的时候,CPU是比较空闲的,白白浪费了资源。

因此可以把接收任务和处理任务两个阶段分开处理,一个线程接收任务,放入任务队列,另外的线程异步处理任务队列中的任务。
这里写图片描述

并行化处理模型

这里写图片描述

由于任务处理一般比较缓慢,会导致任务队列中任务积压长时间得不到处理,这时可以使用多线程来处理。这里使用的是一个公共的任务队列,多线程环境中不免要通过加锁来保证线程安全,我们常用的线程池就是这种模式。可以通过为每个线程维护一个任务队列来改进这种模型。

Reactor线程模型

这里写图片描述
reactor线程模型关注的是:任务接受之后,对处理过程继续进行切分,划分为多个不同的步骤,每个步骤用不同的线程来处理,也就是原本由一个线程处理的任务现在由多个线程来处理,每个线程在处理完自己的步骤之后,还需要将任务转发到下阶段线程继续进行处理。

Netty的Reactor线程模型

这里写图片描述
其中mainReacotor,subReactor,Thread Pool是三个线程池。mainReactor负责处理客户端的连接请求,并将accept的连接注册到subReactor的其中一个线程上;subReactor负责处理客户端通道上的数据读写;Thread Pool是具体的业务逻辑线程池,处理具体业务。

Netty具体线程模型

这里写图片描述

  1. 如何理解NioEventLoop和NioEventLoopGroup
    1)NioEventLoop实际上就是工作线程,可以直接理解为一个线程。NioEventLoopGroup是一个线程池,线程池中的线程就是NioEventLoop。
    2)实际上bossGroup中有多个NioEventLoop线程,每个NioEventLoop绑定一个端口,也就是说,如果程序只需要监听1个端口的话,bossGroup里面只需要有一个NioEventLoop线程就行了。

  2. 每个NioEventLoop都绑定了一个Selector,所以在Netty的线程模型中,是由多个Selecotr在监听IO就绪事件。而Channel注册到Selector。

  3. 一个Channel绑定一个NioEventLoop,相当于一个连接绑定一个线程,这个连接所有的ChannelHandler都是在一个线程中执行的,避免了多线程干扰。更重要的是ChannelPipline链表必须严格按照顺序执行的。单线程的设计能够保证ChannelHandler的顺序执行。

  4. 一个NioEventLoop的selector可以被多个Channel注册,也就是说多个Channel共享一个EventLoop。EventLoop的Selecctor对这些Channel进行检查。

在监听一个端口的情况下,一个NioEventLoop通过一个NioServerSocketChannel监听端口,处理TCP连接。后端多个工作线程NioEventLoop处理IO事件。每个Channel绑定一个NioEventLoop线程,1个NioEventLoop线程关联一个selector来为多个注册到它的Channel监听IO就绪事件。NioEventLoop是单线程执行,保证Channel的pipline在单线程中执行,保证了ChannelHandler的执行顺序。

Channel

Netty中通道是对java原生网络编程api的封装,其顶级接口是Channel。

以TCP编程为例 ,在java中,有两种方式:

  • 基于BIO,JDK1.4之前,我们通常使用java.net包中的ServerSocket和Socket来代表服务端和客户端。

  • 基于NIO,Jdk1.4引入nio编程之后,我们使用java.nio.channels包中的ServerSocketChannel和SocketChannel来代表服务端与客户端。

在Netty中,对java中的BIO、NIO编程api都进行了封装,分别:

  • 使用了OioServerSocketChannel,OioSocketChannel对java.net包中的ServerSocket与Socket进行了封装

  • 使用NioServerSocketChannel和NioSocketChannel对java.nio.channels包中的ServerSocketChannel和SocketChannel进行了封装。

Netty中的Channel继承关系图
这里写图片描述
NioServerSocketChannel和NioSocketChannel中有对应的java通道,可以看到这二者都继承自AbstractNioChannel,其维护了netty中的channel与java nio中channel的对应关系,并提供了javaChannel()方法获取对应的java 中的channel:

public abstract class AbstractNioChannel extends AbstractChannel {
 ...
private final SelectableChannel ch;
 ...
 protected SelectableChannel javaChannel() {//获取对应的java通道
    return ch;
 }
}

SelectableChannel是java nio中的类,我们前面提到nio包中的SocketChannel、ServerSocketChannel都是其子类,NioSocketChannel和NioServerSocketChannel对AbstractNioChannel的javaChannel()进行了覆写,如下:

io.netty.channel.socket.nio.NioServerSocketChannel#javaChannel

@Override
protected ServerSocketChannel javaChannel() {//返回java.nio.channels.ServerSocketChannel
  return (ServerSocketChannel) super.javaChannel();
}

io.netty.channel.socket.nio.NioSocketChannel#javaChannel

@Override
protected SocketChannel javaChannel() {//返回java.nio.channels.SocketChannel
    return (SocketChannel) super.javaChannel();
}

NioServerSocketChannel和NioSocketChannel在构造方法中会创建对应的ServerSocket和Socket实例,以NIOServerSocketChannel为例:

public class NioServerSocketChannel extends AbstractNioMessageChannel
                             implements io.netty.channel.socket.ServerSocketChannel {
    private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
    private static ServerSocketChannel newSocket(SelectorProvider provider) {
        try {
           return provider.openServerSocketChannel();
        } catch (IOException e) {
            throw new ChannelException(
                    "Failed to open a server socket.", e);
        }
    }

    public NioServerSocketChannel() {
        this(newSocket(DEFAULT_SELECTOR_PROVIDER));
    }

    public NioServerSocketChannel(SelectorProvider provider) {
        this(newSocket(provider));
    }

  public NioServerSocketChannel(ServerSocketChannel channel) {
        super(null, channel, SelectionKey.OP_ACCEPT);
        config = new NioServerSocketChannelConfig(this, javaChannel().socket());
    }
    ……
}

io.netty.channel.nio.AbstractNioChannel 构造方法

public abstract class AbstractNioChannel extends AbstractChannel {
    private final SelectableChannel ch;
    protected final int readInterestOp;
    volatile SelectionKey selectionKey;

     protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
        super(parent);
        this.ch = ch;
        this.readInterestOp = readInterestOp;
        try {
            ch.configureBlocking(false);
        } catch (IOException e) {
            try {
                ch.close();
            } catch (IOException e2) {
                if (logger.isWarnEnabled()) {
                    logger.warn(
                            "Failed to close a partially initialized socket.", e2);
                }
            }

            throw new ChannelException("Failed to enter non-blocking mode.", e);
        }
    }
}

NioSocketChannel也是类似的创建过程。通常Channel实例,在创建的时候,就会创建其对应的ChannelConfig实例。

ChannelConfig

在Netty中,每种Channel都有对应的配置,用ChannelConfig来表示,ChannelConfig是一个接口,每个特定的Channel实现类都有自己对应的ChannelConfig实现类,如:

  • NioSocketChannel的对应的配置类为NioSocketChannelConfig

  • NioServerSocketChannel的对应的配置类为NioServerSocketChannelConfig

#ChannelConfig的继承关系图

这里写图片描述
在Channel接口中定义了一个方法config(),用于获取特定通道实现的配置,子类需要实现这个接口。

public interface Channel extends AttributeMap, Comparable<Channel> {
...
ChannelConfig config();
...
}

ChannelHander

在nio编程中,我们经常需要对channel的输入和输出事件进行处理,Netty抽象出一个ChannelHandler概念,专门用于处理此类事件。

因为IO事件分为输入和输出,因此ChannelHandler又具体的分为ChannelInboundHandler和ChannelOutboundHandler ,分别用于某个阶段输入输出事件的处理。

ChannelHandler的继承管理图
这里写图片描述

ChannelHandlerAdapter、ChannelInboundHandlerAdapter 、ChannelOutboundHandlerAdapter是Netty提供的适配器,对于输入输出事件,只需要继承适配器,重写感兴趣的方法即可。

在处理channel的IO事件时,我们通常会分成几个阶段。以读取数据为例,通常我们的处理顺序是:
处理半包或者粘包问题-->数据的解码(或者说是反序列化)-->数据的业务处理

可以看到不同的阶段要执行不同的功能,因此通常我们会编写多个ChannelHandler,来实现不同的功能。而且多个ChannelHandler之间的顺序不能颠倒,例如我们必须先处理粘包解包问题,之后才能进行数据的业务处理。

ChannelPipeline

Netty中通过ChannelPipeline来保证ChannelHandler之间的处理顺序。每一个Channel对象创建的时候,都会自动创建一个关联的ChannelPipeline对象,我们可以通过io.netty.channel.Channel对象的pipeline()方法获取这个对象实例。

ChannelPipeline 的具体的创建过程定义AbstractChannel类的构造方法中:

package io.netty.channel;
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
....
private final DefaultChannelPipeline pipeline;
....
protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();//创建默认的pipeline
}
....
protected DefaultChannelPipeline newChannelPipeline() {
    return new DefaultChannelPipeline(this);
}
....
@Override
    public ChannelPipeline pipeline() {//实现Chnannel定义的pipeline方法,返回pipeline实例
        return pipeline;
    }
}

因为ChannelPipleLine的创建是定义在AbstractChannel的构造方法中的,而每个Channel只会被创建一次,只会调用一次构造方法,因此每个Channel实例唯一对应一个ChannelPipleLine 实例。

从上述代码中,我们可以看到ChannelPipleLine的具体创建过程实际上是通过return new DefaultChannelPipeline(this);实现的。DefaultChannelPipeline是ChannelPipeline的默认实现类。

注意:
1、默认情况下,一个ChannelPipeline实例中,同一个类型ChannelHandler只能被添加一次,如果添加多次,则会抛出异常,具体参见io.netty.channel.DefaultChannelPipeline#checkMultiplicity。如果需要多次添加同一个类型的ChannelHandler的话,则需要在该ChannelHandler实现类上添加@Sharable注解。

2、在ChannelPipeline中,每一个ChannelHandler都是有一个名字的,而且名字必须的是唯一的,如果名字重复了,则会抛出异常,参见io.netty.channel.DefaultChannelPipeline#checkDuplicateName。

3、如果添加ChannelHanler的时候没有显示的指定名字,则会按照规则其起一个默认的名字。具体规则如下,如果ChannelPipeline中只有某种类型的handler实例只有一个,如XXXHandler,YYYHandler,则其名字分别为XXXHandler#0,YYYHandler#0,如果同一类型的Handler有多个实例,则每次之后的编号加1。具体可参见io.netty.channel.DefaultChannelPipeline#generateName方法。

ChannelHandlerContext

前面提到可以通过ChannelPipeline的添加方法,按照顺序添加ChannelHandler,并在之后按照顺序进行调用。事实上,每个ChannelHandler会被先封装成ChannelHandlerContext。之后再封装进ChannelPipeline中。

以DefaultChannelPipeline的addLast方法为例,如果查看源码,最终会定位到以下方法:

DefaultChannelPipeline#addLast(EventExecutorGroup, String,ChannelHandler)

@Override
public ChannelPipeline addLast(EventExecutorGroup group, final String name, ChannelHandler handler) {
    synchronized (this) {
        checkDuplicateName(name);//check这种类型的handler实例是否允许被添加多次
       //将handler包装成一个DefaultChannelHandlerContext类
        AbstractChannelHandlerContext newCtx = new DefaultChannelHandlerContext(this, group, name, handler);
        addLast0(name, newCtx);//维护AbstractChannelHandlerContext的先后关系
    }

    return this;
}

可以看到的确是先将ChannelHandler当做参数构建成一个DefaultChannelHandlerContext实例之后,再调用addLast0方法维护ChannelHandlerContext的先后关系,从而确定了ChannelHandler的先后关系。
这里写图片描述

ChannelPipeline的默认实现类是DefaultChannelPipeline,ChannelHandlerContext的默认实现类是DefaultChannelHandlerContext。

DefaultChannelPipeline内部是通过一个双向链表记录ChannelHandler的先后关系,而双向链表的节点是AbstractChannelHandlerContext类。

以下是AbstractChannelHandlerContext类的部分源码(双向链表节点):

abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
        implements ChannelHandlerContext, ResourceLeakHint {
...
volatile AbstractChannelHandlerContext next;//当前节点的上一个节点
volatile AbstractChannelHandlerContext prev;//当前节点的下一个节点
...
}

DefaultChannelPipeline内部通过两个哨兵节点HeadContext和TailContext作为链表的开始和结束,熟悉双向链表数据结构的同学,肯定知道,设置哨兵可以在移除节点的时候,不需要判断是否是最后一个节点。相关源码如下:

public class DefaultChannelPipeline implements ChannelPipeline {
...
private static final String HEAD_NAME = generateName0(HeadContext.class);
private static final String TAIL_NAME = generateName0(TailContext.class);
...
final AbstractChannelHandlerContext head;//双向链表的头元素
final AbstractChannelHandlerContext tail;//双向列表的尾部元素

private final Channel channel;
....
protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
     ....
    tail = new TailContext(this);//创建双向链表头部元素实例
    head = new HeadContext(this);//创建双向链表的尾部元素实例
    //设置链表关系
    head.next = tail;
    tail.prev = head;
}
....
....
private void addLast0(AbstractChannelHandlerContext newCtx) {
   //设置ChannelHandler的先后顺序关系
    AbstractChannelHandlerContext prev = tail.prev;
    newCtx.prev = prev;
    newCtx.next = tail;
    prev.next = newCtx;
    tail.prev = newCtx;
   }
 }
}
ChannelHander、ChannelPipeline、ChannelHandlerContext的联合工作过程

DefaultChannelPipeline是将ChannelHander包装成AbstractChannelHandlerContext类之后,再添加到链表结构中的,从而实现handler的级联调用。
ChannelInboundHandler 接口定义的9个方法:

public interface ChannelInboundHandler extends ChannelHandler {
    void channelRegistered(ChannelHandlerContext ctx) throws Exception;
    void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
    void channelActive(ChannelHandlerContext ctx) throws Exception;
    void channelInactive(ChannelHandlerContext ctx) throws Exception;
    void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
    void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
    void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;
    void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

而在ChannelPipeline和ChannelHandlerContext中,都定义了相同的9个以fire开头的方法,如下所示
这里写图片描述
可以发现这两个接口定义的9个方法与ChannelInboundHandler定义的9个方法是一一对应的,只是在定义每个方法的时候,在前面加了1个fire。

调用顺序

  1. 先是ChannelPipeline中的fireXXX方法被调用

  2. ChannelPipeline中的fireXXX方法接着调用ChannelPipeline维护的ChannelHandlerContext链表中的第一个节点即HeadContext 的fireXXX方法

  3. ChannelHandlerContext 中的fireXXX方法调用ChannelHandler中对应的XXX方法。由于可能存在多个ChannelHandler,因此每个ChannelHandler的xxx方法又要负责调用下一个ChannelHandlerContext的fireXXX方法,直到整个调用链完成

各fire方法调用时机

1、 fireChannelRegistered() 和fireChannelActive()是在Channel注册到EventLoop中时调用的,只会被调用一次

io.netty.channel.AbstractChannel.AbstractUnsafe#register0

private void register0(ChannelPromise promise) {
    try {
        ...
        doRegister();//注册通道到EventLoop中
        registered = true;
        safeSetSuccess(promise);
        pipeline.fireChannelRegistered();//注册成功,调用fireChannelRegistered()
        if (isActive()) {
            pipeline.fireChannelActive();//如果激活,调用fireChannelActive()
        }
    } catch (Throwable t) {
        ....
    }
}

类似的,当取消注册时候fireChannelInactive()、fireChannelUnregistered()会被调用

2、fireChannelRead(Object msg) 和fireChannelReadComplete()在有数据需要读取的情况下会被触发,可能会被触发多次

io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read

@Override
public void read() {
    ....
    try {
        int totalReadAmount = 0;
        boolean readPendingReset = false;
        do {
            ....
            pipeline.fireChannelRead(byteBuf);//有数据要读取,调用fireChannelRead
            ....
        } while (++ messages < maxMessagesPerRead);

        pipeline.fireChannelReadComplete();//数据读取完成,调用fireChannelReadComplete()
        ....
    } catch (Throwable t) {
        handleReadException(pipeline, byteBuf, t, close);
    } finally {
        ....
    }
}

3、fireExceptionCaught()方法,在读取数据出错的情况下,会被调用
在上述代码片段中,有一个handleReadException方法,表示如果读取数据出错的处理逻辑,其内部会调用fireExceptionCaught()

io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#handleReadException

private void handleReadException(ChannelPipeline pipeline,
                        ByteBuf byteBuf, Throwable cause, boolean close) {
    ....
    pipeline.fireExceptionCaught(cause);//出现异常时,调用fireExceptionCaught
    if (close || cause instanceof IOException) {
        closeOnRead(pipeline);
    }
}

4、fireUserEventTriggered(Object event) 当正在读取数据的时候,如果连接关闭,调用此方法
上述代码片段在处理异常的时候,会判断异常类型是否是IOException或者连接是否关闭,如果是,则调用closeOnRead方法,这个方法内部会调用 fireUserEventTriggered(Object event)

io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#closeOnRead

private void closeOnRead(ChannelPipeline pipeline) {
    SelectionKey key = selectionKey();
    setInputShutdown();
    if (isOpen()) {
        if (Boolean.TRUE.equals(config().getOption(ChannelOption.ALLOW_HALF_CLOSURE))) {
            key.interestOps(key.interestOps() & ~readInterestOp);
            pipeline.fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE);//调用fireUserEventTriggered方法
        } else {
            close(voidPromise());
        }
    }
}

5、fireChannelWritabilityChanged()方法,当有数据需要输出的时候被调用
io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes

void incrementPendingOutboundBytes(int size) {
    ...
    long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
    if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
        if (WRITABLE_UPDATER.compareAndSet(this, 1, 0)) {
            channel.pipeline().fireChannelWritabilityChanged();//需要输出数据,调用fireChannelWritabilityChanged()
        }
    }
}

以上分析了Channel,与NIO中Channel的关系,Channel的处理组件ChannelHandler,及如何保证多个ChannelHandler的顺序调用等。

NIO中有三大组件Channel、Selector、Buffer。Netty 也有自己的缓存组件ByteBuf。

ByteBuf

ByteBuf的类图
这里写图片描述

可以使用两种方式对ByteBuf进行分类:按底层实现方式和按是否使用对象池。

  1. 按底层实现

    • HeapByteBuf
      HeapByteBuf的底层实现为JAVA堆内的字节数组。堆缓冲区与普通堆对象类似,位于JVM堆内存区,可由GC回收,其申请和释放效率较高。常规JAVA程序使用建议使用该缓冲区。
    • DirectByteBuf
      DirectByteBuf的底层实现为操作系统内核空间的字节数组。直接缓冲区的字节数组位于JVM堆外的NATIVE堆,由操作系统管理申请和释放,而DirectByteBuf的引用由JVM管理。直接缓冲区由操作系统管理,一方面,申请和释放效率都低于堆缓冲区,另一方面,却可以大大提高IO效率。由于进行IO操作时,常规下用户空间的数据(JAVA即堆缓冲区)需要拷贝到内核空间(直接缓冲区),然后内核空间写到网络SOCKET或者文件中。如果在用户空间取得直接缓冲区,可直接向内核空间写数据,减少了一次拷贝,可大大提高IO效率,这也是常说的零拷贝。
    • CompositeByteBuf
      CompositeByteBuf,顾名思义,有以上两种方式组合实现。这也是一种零拷贝技术,想象将两个缓冲区合并为一个的场景,一般情况下,需要将后一个缓冲区的数据拷贝到前一个缓冲区;而使用组合缓冲区则可以直接保存两个缓冲区,因为其内部实现组合两个缓冲区并保证用户如同操作一个普通缓冲区一样操作该组合缓冲区,从而减少拷贝操作。
  2. 按是否使用对象池

    • UnpooledByteBuf
      UnpooledByteBuf为不使用对象池的缓冲区,不需要创建大量缓冲区对象时建议使用该类缓冲区。
    • PooledByteBuf
      PooledByteBuf为对象池缓冲区,当对象释放后会归还给对象池,所以可循环使用。当需要大量且频繁创建缓冲区时,建议使用该类缓冲区。Netty4.1默认使用对象池缓冲区,4.0默认使用非对象池缓冲区。

猜你喜欢

转载自blog.csdn.net/u013857458/article/details/82527722