中间件系列之Netty-3-Channel

503 阅读7分钟

通过上一篇文章,已经对Netty的使用有了一个感性的认识,下面我会对Netty中重要的概念加以说明。首先就是Channel。

1.概述

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

在之后引入NIO编程之后,我们使用java.nio.channels包中的ServerSocketChannelSocketChannel来代表服务端与客户端。

在Netty中,对Java中的BIO、NIO编程api都进行了进一步的封装,分别如下:

  1. 使用了OioServerSocketChannelOioSocketChannel对java.net包中的ServerSocketSocket进行了封装

  2. 使用NioServerSocketChannelNioSocketChannel对java.nio.channels包中的ServerSocketChannelSocketChannel进行了封装。

    具体继承关系如下:

1588691941553

需要注意,在OioServerSocketChannelOioSocketChannel是没有通道Channel的,虽然名字中带有Channel...以OioSocketChannel为例,并且也能注意到其已经是过时的类了,推荐还是使用NioServerSocketChannelNioSocketChannel

@Deprecated
public class OioSocketChannel extends OioByteStreamChannel implements SocketChannel {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(OioSocketChannel.class);

    private final Socket socket;
    ...
}

NioSocketChannelNioServerSocketChannelAbstractNioChannel的javaChannel()进行了覆写

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

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

2.ChannelConfig

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

1588693023086

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

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

通常Channel实例,在创建的时候,就会创建其对应的ChannelConfig实例。例如NioSocketChannel在构造方法中创建了其对应的ChannelConfig实现。

public class NioSocketChannel extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel {
    ...
    private final SocketChannelConfig config;
	...
     public NioSocketChannel(Channel parent, SocketChannel socket) {
        super(parent, socket);
        config = new NioSocketChannelConfig(this, socket.socket());
    }
    ...
    @Override
    public SocketChannelConfig config() {//覆写config方法,返回SocketChannelConfig实例
        return config;
    }
}

此外,在ChannelConfig中,我们还可以注意到如下方法

 	Map<ChannelOption<?>, Object> getOptions();

    boolean setOptions(Map<ChannelOption<?>, ?> options);

    <T> T getOption(ChannelOption<T> option);

    <T> boolean setOption(ChannelOption<T> option, T value);

其中有个ChannelOption类,可以认为ChannelConfig中用了一个Map来保存参数,Map的key是ChannelOptionChannelConfig 定义了相关方法来获取和修改Map中的值。

当我们想修改一个Map中的参数时,例如我们希望NioSocketChannel在工作过程中,使用PooledByteBufAllocator来分配内存,则可以使用类似以下方式来设置

Channel ch = ...;
SocketChannelConfig cfg = (SocketChannelConfig) ch.getConfig();
cfg.setOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

或者

Channel ch = ...;
SocketChannelConfig cfg = (SocketChannelConfig) ch.getConfig();
cfg.setAllocator(PooledByteBufAllocator.DEFAULT);

除此之外,还有更多的自定义配置

ChannelOption.ALLOCATOR
ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK
ChannelOption.WRITE_BUFFER_LOW_WATER_MARK
ChannelOption.MESSAGE_SIZE_ESTIMATOR
ChannelOption.AUTO_CLOSE
.....

每一种ChannelOption,除了可以使用setOption方法来进行设置,在ChannelConfig接口中都为其设置了对应的快捷set/get方法。

public interface SocketChannelConfig extends ChannelConfig {
    ...
    @Override
    SocketChannelConfig setAllocator(ByteBufAllocator allocator);

    @Override
    SocketChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator);

    @Override
    SocketChannelConfig setAutoRead(boolean autoRead);

    @Override
    SocketChannelConfig setAutoClose(boolean autoClose);
    ...
}

3.ChannelHander

在NIO编程中,我们经常需要对channel的输入和输出事件进行处理,Netty抽象出一个ChannelHandler概念,专门用于处理此类事件。又因为IO事件分为输入和输出,因此ChannelHandler又具体的分为ChannelInboundHandlerChannelOutboundHandler ,分别用于某个阶段输入输出事件的处理。

类继承如图:

1588694395086

对于ChannelHandlerAdapterChannelInboundHandlerAdapter ChannelOutboundHandlerAdapter,从名字就可以看出来其作用是适配器。

通常,处理IO事件时,会分成几个阶段。以读取数据为例,通常我们的处理顺序是:

处理半包或者粘包问题-->数据的解码(或者说是反序列化)-->数据的业务处理

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

ChannelPipeline

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

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;
    }
}

可以看到,ChannelPipeline的定义实在AbstractChannel的构造方法中,而每个Channel只会构建一次,从而确保每个Channel实例唯一对应一个ChannelPipleLine 实例。

ChannelPipeline 除了负责配置handler的顺序,还负责在收到读/写事件之后按照顺序调用这些handler。假设有如下ChannelPipeline定义:

ChannelPipeline p = ...;
p.addLast("1", new InboundHandlerA());
p.addLast("2", new InboundHandlerB());
p.addLast("3", new OutboundHandlerA());
p.addLast("4", new OutboundHandlerB());
p.addLast("5", new InboundOutboundHandlerX());

其中,InboundHandlerAInboundHandlerB实现了ChannelInboundHandler接口,OutboundHandlerAOutboundHandlerB实现了ChannelOutboundHandler接口,InboundOutboundHandlerX则同时实现了这两个接口,前面的1、2、3、4、5并不是handler的编号,而是handler的名字

当一个输入事件来的时候,输出事件处理器是不会发生作用的;反之亦然。因此:

  • 当一个输入事件来了之后,事件处理器的调用顺序为1,2,5
  • 当一个输出事件来了之后,事件处理器的处理顺序为5,4,3。(注意输出事件的处理器发挥作用的顺序与定义的顺序是相反的)

另外需要说明的是:

  • 默认情况下,一个ChannelPipeline实例中,同一个类型ChannelHandler只能被添加一次,如果需要多次添加,则需要在该ChannelHandler实现类上添加@Sharable注解。
  • ChannelPipeline中,每一个ChannelHandler都是有一个名字的,而且名字必须的是唯一的。如果没有显示的指定名字,则会按照规则起一个默认的名字

ChannelHandlerContext

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

ChannelPipeline的默认实现类是DefaultChannelPipeline,以DefaultChannelPipelineaddLast方法为例,如果查看源码,最终会定位到以下方法:

@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;
}

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

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;
}

而在ChannelPipelineChannelHandlerContext中,都定义了相同的9个以fire开头的方法,例如在ChannelPipeline中:

1588727700592

可以看到这9个方法与ChannelInboundHandler的方法是一一对应的

从总体上来说,在调用的时候,是按照如下顺序进行的:

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

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

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