从零开始实现简易版Netty(二) MyNetty pipeline流水线

1. Netty pipeline流水线介绍

在上一篇博客中lab1版本的MyNetty参考netty实现了一个极其精简的reactor模型。按照计划,lab2版本的MyNetty需要实现pipeline流水线,以支持不同的逻辑处理模块的解耦。

由于本文属于系列博客,读者需要对之前的博客内容有所了解才能更好地理解本文内容。

在lab1版本中,MyNetty的EventLoop处理逻辑中,允许使用者配置一个EventHandler,并在处理read事件时调用其实现的自定义fireChannelRead方法。

这一机制在实现demo中的简易echo服务器时是够用的,但在实际的场景中,一个完备的网络程序,业务想要处理的IO事件有很多类型,并且不希望在一个大而全的臃肿的处理器中处理所有的IO事件,而是能够模块化的拆分不同的处理逻辑,实现架构上的灵活解耦。

因此netty提供了pipeline流水线机制,允许用户在使用netty时能按照自己的需求,按顺序组装自己的处理器链条。

1.1 netty的IO事件

在实际的网络环境中,有非常多不同类型的IO事件,最典型的就是读取来自远端的数据(read)以及向远端写出发送数据(write)。

netty对这些IO事件进行了抽象,并允许用户编写自定义的处理器监听或是主动触发这些事件。

netty按照事件数据流的传播方向将IO事件分成了入站(InBound)与出站(OutBound)两大类,由远端输入传播到本地应用程序的事件被叫做入站事件,从本地应用程序触发向远端传播的事件叫出站事件。

主要的入站事件有channelRead、channelActive等,主要的出站事件有write、connect、bind等。

1.2 netty的IO事件处理器与pipeline流水线

针对InBound入站IO事件,netty抽象出了ChannelInboundHandler接口;针对OutBound出站IO事件,netty抽象出了ChannelOutboundHandler接口。

用户可以编写一系列继承自对应ChannelHandler接口的自定义处理器,将其绑定到ChannelPipeline中。每一个Channel都对应一个ChannelPipeline,ChannelPipeline实例是独属于某个特定channel连接的。

2. MyNetty实现pipeline流水线

经过上述对于netty的IO事件与pipeline流水线简要介绍后,读者对netty的流水线虽然有了一定的概念,但对具体的细节还是知之甚少。下面我们结合MyNetty的源码,展开介绍netty的流水线机制实现。

2.1 MyNetty的事件处理器

/**
* 事件处理器(相当于netty中ChannelInboundHandler和ChannelOutboundHandler合在一起)
* */
public interface MyChannelEventHandler { // ========================= inbound入站事件 ==============================
void channelRead(MyChannelHandlerContext ctx, Object msg) throws Exception; void exceptionCaught(MyChannelHandlerContext ctx, Throwable cause) throws Exception; // ========================= outbound出站事件 ==============================
void close(MyChannelHandlerContext ctx) throws Exception; void write(MyChannelHandlerContext ctx, Object msg) throws Exception;
}
  • 前面说到,netty将入站与出站事件用两个不同的ChannelEventHandler接口进行了抽象,而在MyNetty中因为最终要支持的IO事件没有netty那么多,所以出站、入站的处理接口进行了合并。

    这样做虽然在架构上不如netty那样拆分开来的设计优雅,但相对来说理解起来会更加简单。
  • 未来MyChannelEventHandler还会随着迭代支持更多的IO事件,但这是个渐进的过程,目前lab2中只需要支持少数几个IO事件便能满足需求。

2.2 MyNetty的pipeline流水线与ChannelHandler上下文

MyNetty pipeline流水线实现
public interface MyChannelEventInvoker {

    // ========================= inbound入站事件 ==============================
void fireChannelRead(Object msg); void fireExceptionCaught(Throwable cause); // ========================= outbound出站事件 ==============================
void close(); void write(Object msg);
}
/**
* pipeline首先自己也是一个Invoker
*
* 包括head和tail两个哨兵节点
* */
public class MyChannelPipeline implements MyChannelEventInvoker { private static final Logger logger = LoggerFactory.getLogger(MyChannelPipeline.class); private final MyNioChannel channel; /**
* 整条pipeline上的,head和tail两个哨兵节点
*
* inbound入站事件默认都从head节点开始向tail传播
* outbound出站事件默认都从tail节点开始向head传播
* */
private final MyAbstractChannelHandlerContext head;
private final MyAbstractChannelHandlerContext tail; public MyChannelPipeline(MyNioChannel channel) {
this.channel = channel; head = new MyChannelPipelineHeadContext(this);
tail = new MyChannelPipelineTailContext(this); head.setNext(tail);
tail.setPrev(head);
} @Override
public void fireChannelRead(Object msg) {
// 从head节点开始传播读事件(入站)
MyChannelPipelineHeadContext.invokeChannelRead(head,msg);
} @Override
public void fireExceptionCaught(Throwable cause) {
// 异常传播到了pipeline的末尾,打印异常信息
onUnhandledInboundException(cause);
} @Override
public void close() {
// 出站事件,从尾节点向头结点传播
tail.close();
} @Override
public void write(Object msg) {
tail.write(msg);
} public void addFirst(MyChannelEventHandler handler){
// 非sharable的handler是否重复加入的校验
checkMultiplicity(handler); MyAbstractChannelHandlerContext newCtx = newContext(handler); MyAbstractChannelHandlerContext oldFirstCtx = head.getNext();
newCtx.setPrev(head);
newCtx.setNext(oldFirstCtx);
head.setNext(newCtx);
oldFirstCtx.setPrev(newCtx);
} public void addLast(MyChannelEventHandler handler){
// 非sharable的handler是否重复加入的校验
checkMultiplicity(handler); MyAbstractChannelHandlerContext newCtx = newContext(handler); // 加入链表尾部节点之前
MyAbstractChannelHandlerContext oldLastCtx = tail.getPrev();
newCtx.setPrev(oldLastCtx);
newCtx.setNext(tail);
oldLastCtx.setNext(newCtx);
tail.setPrev(newCtx);
} private static void checkMultiplicity(MyChannelEventHandler handler) {
if (handler instanceof MyChannelEventHandlerAdapter) {
MyChannelEventHandlerAdapter h = (MyChannelEventHandlerAdapter) handler; if (!h.isSharable() && h.added) {
// 一个handler实例不是sharable,但是被加入到了pipeline一次以上,有问题
throw new MyNettyException(
h.getClass().getName() + " is not a @Sharable handler, so can't be added or removed multiple times.");
} // 第一次被引入,当前handler实例标记为已加入
h.added = true;
}
} public MyNioChannel getChannel() {
return channel;
} private void onUnhandledInboundException(Throwable cause) {
logger.warn("An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
"It usually means the last handler in the pipeline did not handle the exception.", cause);
} private MyAbstractChannelHandlerContext newContext(MyChannelEventHandler handler) {
return new MyDefaultChannelHandlerContext(this,handler);
}
}
  • pipeline实现了ChannelEventInvoker接口,ChannelEventInvoker与ChannelEventHandler中对应IO事件的方法是一一对应的,唯一的区别在于其方法中缺失了(MyChannelHandlerContext ctx)参数。

    Invoker接口用于netty内部触发流水线的事件传播,而Handler接口用于用户自定义IO事件触发时的事件处理器。
  • 同时,pipeline流水线中定义了两个关键属性,head和tail,其都是AbstractChannelHandlerContext类型的,其内部工作原理我们在下一小节展开。

    pipeline提供了addFirst和addLast两个方法(netty中提供了非常多功能类似的方法,MyNetty简单起见只实现了最常用的两个),允许将用户自定义的ChannelHandler挂载在pipeline中,与head、tail组成一个双向链表,而入站出站事件会按照双向链表中节点的顺序进行传播。
  • 对于入站事件(比如fireChannelRead),事件从head节点开始,从前到后的在流水线的handler链表中传播;而出站事件(比如write), 事件则从tail节点开始,从后往前的在流水线的handler链表中传播。

2.3 MyNetty ChannelHandlerContext上下文实现

下面我们来深入讲解ChannelHandlerContext上下文原理,看看一个具体的事件在pipeline的双向链表中的传播是如何实现的。

MyChannelHandlerContext上下文接口定义
public interface MyChannelHandlerContext extends MyChannelEventInvoker {

    MyNioChannel channel();

    MyChannelEventHandler handler();

    MyChannelPipeline getPipeline();

    MyNioEventLoop executor();
}
MyAbstractChannelHandlerContext上下文骨架类
public abstract class MyAbstractChannelHandlerContext implements MyChannelHandlerContext{

    private static final Logger logger = LoggerFactory.getLogger(MyAbstractChannelHandlerContext.class);

    private final MyChannelPipeline pipeline;

    private final int executionMask;

    /**
* 双向链表前驱/后继节点
* */
private MyAbstractChannelHandlerContext prev;
private MyAbstractChannelHandlerContext next; public MyAbstractChannelHandlerContext(MyChannelPipeline pipeline, Class<? extends MyChannelEventHandler> handlerClass) {
this.pipeline = pipeline; this.executionMask = MyChannelHandlerMaskManager.mask(handlerClass);
} @Override
public MyNioChannel channel() {
return pipeline.getChannel();
} public MyAbstractChannelHandlerContext getPrev() {
return prev;
} public void setPrev(MyAbstractChannelHandlerContext prev) {
this.prev = prev;
} public MyAbstractChannelHandlerContext getNext() {
return next;
} public void setNext(MyAbstractChannelHandlerContext next) {
this.next = next;
} @Override
public MyNioEventLoop executor() {
return this.pipeline.getChannel().getMyNioEventLoop();
} @Override
public void fireChannelRead(Object msg) {
// 找到当前链条下最近的一个支持channelRead方法的MyAbstractChannelHandlerContext(inbound事件,从前往后找)
MyAbstractChannelHandlerContext nextHandlerContext = findContextInbound(MyChannelHandlerMaskManager.MASK_CHANNEL_READ); // 调用找到的那个ChannelHandlerContext其handler的channelRead方法
MyNioEventLoop myNioEventLoop = nextHandlerContext.executor();
if(myNioEventLoop.inEventLoop()){
invokeChannelRead(nextHandlerContext,msg);
}else{
// 防并发,每个针对channel的操作都由自己的eventLoop线程去执行
myNioEventLoop.execute(()->{
invokeChannelRead(nextHandlerContext,msg);
});
}
} @Override
public void fireExceptionCaught(Throwable cause) {
// 找到当前链条下最近的一个支持exceptionCaught方法的MyAbstractChannelHandlerContext(inbound事件,从前往后找)
MyAbstractChannelHandlerContext nextHandlerContext = findContextInbound(MyChannelHandlerMaskManager.MASK_EXCEPTION_CAUGHT); // 调用找到的那个ChannelHandlerContext其handler的exceptionCaught方法 MyNioEventLoop myNioEventLoop = nextHandlerContext.executor();
if(myNioEventLoop.inEventLoop()){
invokeExceptionCaught(nextHandlerContext,cause);
}else{
// 防并发,每个针对channel的操作都由自己的eventLoop线程去执行
myNioEventLoop.execute(()->{
invokeExceptionCaught(nextHandlerContext,cause);
});
}
} @Override
public void close() {
// 找到当前链条下最近的一个支持close方法的MyAbstractChannelHandlerContext(outbound事件,从后往前找)
MyAbstractChannelHandlerContext nextHandlerContext = findContextOutbound(MyChannelHandlerMaskManager.MASK_CLOSE); MyNioEventLoop myNioEventLoop = nextHandlerContext.executor();
if(myNioEventLoop.inEventLoop()){
doClose(nextHandlerContext);
}else{
// 防并发,每个针对channel的操作都由自己的eventLoop线程去执行
myNioEventLoop.execute(()->{
doClose(nextHandlerContext);
});
}
} private void doClose(MyAbstractChannelHandlerContext nextHandlerContext){
try {
nextHandlerContext.handler().close(nextHandlerContext);
} catch (Throwable t) {
logger.error("{} do close error!",nextHandlerContext,t);
}
} @Override
public void write(Object msg) {
// 找到当前链条下最近的一个支持write方法的MyAbstractChannelHandlerContext(outbound事件,从后往前找)
MyAbstractChannelHandlerContext nextHandlerContext = findContextOutbound(MyChannelHandlerMaskManager.MASK_WRITE); MyNioEventLoop myNioEventLoop = nextHandlerContext.executor();
if(myNioEventLoop.inEventLoop()){
doWrite(nextHandlerContext,msg);
}else{
// 防并发,每个针对channel的操作都由自己的eventLoop线程去执行
myNioEventLoop.execute(()->{
doWrite(nextHandlerContext,msg);
});
}
} private void doWrite(MyAbstractChannelHandlerContext nextHandlerContext, Object msg) {
try {
nextHandlerContext.handler().write(nextHandlerContext,msg);
} catch (Throwable t) {
logger.error("{} do write error!",nextHandlerContext,t);
}
} @Override
public MyChannelPipeline getPipeline() {
return pipeline;
} public static void invokeChannelRead(MyAbstractChannelHandlerContext next, Object msg) {
try {
next.handler().channelRead(next, msg);
}catch (Throwable t){
// 处理抛出的异常
next.invokeExceptionCaught(t);
}
} public static void invokeExceptionCaught(MyAbstractChannelHandlerContext next, Throwable cause) {
next.invokeExceptionCaught(cause);
} private void invokeExceptionCaught(final Throwable cause) {
try {
this.handler().exceptionCaught(this, cause);
} catch (Throwable error) {
// 如果捕获异常的handler依然抛出了异常,则打印debug日志
logger.error(
"An exception {}" +
"was thrown by a user handler's exceptionCaught() " +
"method while handling the following exception:",
ThrowableUtil.stackTraceToString(error), cause);
}
} private MyAbstractChannelHandlerContext findContextInbound(int mask) {
MyAbstractChannelHandlerContext ctx = this;
do {
// inbound事件,从前往后找
ctx = ctx.next;
} while (needSkipContext(ctx, mask)); return ctx;
} private MyAbstractChannelHandlerContext findContextOutbound(int mask) {
MyAbstractChannelHandlerContext ctx = this;
do {
// outbound事件,从后往前找
ctx = ctx.prev;
} while (needSkipContext(ctx, mask)); return ctx;
} private static boolean needSkipContext(MyAbstractChannelHandlerContext ctx, int mask) {
// 如果与运算后为0,说明不支持对应掩码的操作,需要跳过
return (ctx.executionMask & (mask)) == 0;
}
}
MyChannelPipelineHeadContext pipeline哨兵头结点
/**
* pipeline的head哨兵节点
* */
public class MyChannelPipelineHeadContext extends MyAbstractChannelHandlerContext implements MyChannelEventHandler { public MyChannelPipelineHeadContext(MyChannelPipeline pipeline) {
super(pipeline,MyChannelPipelineHeadContext.class);
} @Override
public void channelRead(MyChannelHandlerContext ctx, Object msg) {
ctx.fireChannelRead(msg);
} @Override
public void exceptionCaught(MyChannelHandlerContext ctx, Throwable cause) {
ctx.fireExceptionCaught(cause);
} @Override
public void close(MyChannelHandlerContext ctx) throws Exception {
// 调用jdk原生的channel方法,关闭掉连接
ctx.getPipeline().getChannel().getJavaChannel().close();
} @Override
public void write(MyChannelHandlerContext ctx, Object msg) throws Exception {
// 往外写的操作,一定是socketChannel
SocketChannel socketChannel = (SocketChannel) ctx.getPipeline().getChannel().getJavaChannel(); if(msg instanceof ByteBuffer){
socketChannel.write((ByteBuffer) msg);
}else{
// msg走到head节点的时候,必须是ByteBuffer类型
throw new Error();
}
} @Override
public MyChannelEventHandler handler() {
return this;
}
}
MyChannelPipelineHeadContext pipeline哨兵尾结点
/**
* pipeline的tail哨兵节点
* */
public class MyChannelPipelineTailContext extends MyAbstractChannelHandlerContext implements MyChannelEventHandler { private static final Logger logger = LoggerFactory.getLogger(MyChannelPipelineTailContext.class); public MyChannelPipelineTailContext(MyChannelPipeline pipeline) {
super(pipeline, MyChannelPipelineTailContext.class);
} @Override
public void channelRead(MyChannelHandlerContext ctx, Object msg) {
// 如果channelRead事件传播到了tail节点,说明用户自定义的handler没有处理好,但问题不大,打日志警告下
onUnhandledInboundMessage(ctx,msg);
} @Override
public void exceptionCaught(MyChannelHandlerContext ctx, Throwable cause) {
// 如果exceptionCaught事件传播到了tail节点,说明用户自定义的handler没有处理好,但问题不大,打日志警告下
onUnhandledInboundException(cause);
} @Override
public void close(MyChannelHandlerContext ctx) throws Exception {
// do nothing
logger.info("close op, tail context do nothing");
} @Override
public void write(MyChannelHandlerContext ctx, Object msg) throws Exception {
// do nothing
logger.info("write op, tail context do nothing");
} @Override
public MyChannelEventHandler handler() {
return this;
} private void onUnhandledInboundException(Throwable cause) {
logger.warn(
"An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
"It usually means the last handler in the pipeline did not handle the exception.",
cause);
} private void onUnhandledInboundMessage(MyChannelHandlerContext ctx, Object msg) {
logger.debug(
"Discarded inbound message {} that reached at the tail of the pipeline. " +
"Please check your pipeline configuration.", msg); logger.debug("Discarded message pipeline : {}. Channel : {}.",
ctx.getPipeline(), ctx.channel());
}
}
  • AbstractChannelHandlerContext作为ChannelHandlerContext子类的基础骨架,是理解Netty中IO事件传播机制的重中之重。AbstractChannelHandlerContext做为ChannelPipeline的实际节点,其拥有prev和next两个属性,用于关联链表中的前驱和后继。
  • 在触发IO事件时,AbstractChannelHandlerContext会按照一定的规则(具体原理在下一节展开)找到下一个需要处理当前类型IO事件的事件处理器(findContextInbound与findContextOutBound方法)。
  • 在找到后会先判断当前线程与目标MyAbstractChannelHandlerContext的执行器线程是否相同(inEventLoop),如果是则直接触发对应handler的回调方法;如果不是则将当前事件包装成一个任务交给next节点的executor执行。

    这样设计的主要原因是netty作为一个高性能网络框架,是非常忌讳使用同步锁的。EventLoop线程是按照引入taskQueue队列多写单读的方式消费IO事件以及相关任务的,这样可以避免处理IO事件时防止不同线程间并发而大量加锁。
  • 举个例子,一个聊天服务器,用户a通过连接A发送了一条消息给服务端,而服务端需要通过连接b将消息同步给用户b,连接a和连接b属于不同的EventLoop线程。

    连接a所在的EventLoop在接受到读事件后,需要往连接b写出数据,此时不能直接由连接a的线程执行channel的写出操作(inEventLoop为false),而必须通过execute方法写入taskQueue交给管理连接b的EventLoop线程,让它异步的处理。

    试想如果能允许别的EventLoop线程来回调触发不属于它的channel的IO事件,那么所有的ChannelHandler都必须考虑多线程并发的问题而被迫引入同步机制,导致性能大幅降低。
  • netty中可以在ChannelHandler中主动的触发一些IO事件,比如write写出事件。如果是使用ChannelHandlerContext.write写出,则传播的起点是当前Handler节点;而如果是ChannelHandlerContext.channel.write的方式写出,其底层就是调用的是pipeline.write,其传播的起点则是tail哨兵节点。

    结合MyNetty中上述pipeline相关的代码,相信读者应该能更好的理解netty中的这一传播机制。

2.4 ChannelHandler mask掩码过滤机制

通常情况,用户自定义的IO事件处理器一般都是各司其职的,不会对每一种IO事件都感兴趣。比如最经典的编解码handler,一般来说encode编码处理器只关心写出到远端的出站事件,而decode解码处理器只关心读取到数据的入站事件。

但编码、解码处理器都是位于pipeline的同一个链表中的,因此IO事件理论上会在链表中的所有处理器中传播。同时由于netty允许ChannelHandler在内部自行决定是否将事件往下一个handler节点传播,因此如果不引入特别的机制,则意味着用户自定义的每一个ChannelHandler都必须实现所有的接口方法,并在内部添加模版代码来确保事件能够继续在pipeline中传播(比如都必须实现fireChannelRead方法,并且都调用ctx.fireChannelRead方法让事件能向后传播)。

netty中为ChannelHandler定义了非常多的IO事件接口,如果每个ChannelHandler都必须实现所有的IO事件接口,netty的用户在实现自定义处理器时会非常痛苦,同时在高并发下不必要的方法调用也会对性能有所影响。

为了解决上述问题,netty提供了Skip机制,允许用户在编写自定义处理器时仅关心自己感兴趣的IO事件,而其它事件在进行传播时能自动的跳过当前handler节点在pipeline中继续传播。

在2.3的AbstractChannelHandlerContext实现中,可以发现事件传播的过程中关键的两个方法(findContextInbound/findContextOutbound)都是基于needSkipContext方法来实现的。

needSkipContext方法中基于AbstractChannelHandlerContext中的一个属性executionMask来决定是否需要跳过某个ChannelHandler。

下面我们结合MyNetty的源码来看看这个executionMask属性是如何被计算得出,又是如何基于该掩码进行handler过滤的。

/**
* 计算并缓存每一个类型的handler所需要处理方法掩码的管理器
*
* 参考自netty的ChannelHandlerMask类
* */
public class MyChannelHandlerMaskManager { private static final Logger logger = LoggerFactory.getLogger(MyChannelHandlerMaskManager.class); public static final int MASK_EXCEPTION_CAUGHT = 1; // ==================== inbound ==========================
public static final int MASK_CHANNEL_REGISTERED = 1 << 1;
public static final int MASK_CHANNEL_UNREGISTERED = 1 << 2;
public static final int MASK_CHANNEL_ACTIVE = 1 << 3;
public static final int MASK_CHANNEL_INACTIVE = 1 << 4;
public static final int MASK_CHANNEL_READ = 1 << 5;
public static final int MASK_CHANNEL_READ_COMPLETE = 1 << 6;
// static final int MASK_USER_EVENT_TRIGGERED = 1 << 7;
// static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 8; // ===================== outbound =========================
public static final int MASK_BIND = 1 << 9;
public static final int MASK_CONNECT = 1 << 10;
// static final int MASK_DISCONNECT = 1 << 11;
public static final int MASK_CLOSE = 1 << 12;
// static final int MASK_DEREGISTER = 1 << 13;
public static final int MASK_READ = 1 << 14;
public static final int MASK_WRITE = 1 << 15;
public static final int MASK_FLUSH = 1 << 16; private static final ThreadLocal<Map<Class<? extends MyChannelEventHandler>, Integer>> MASKS =
ThreadLocal.withInitial(() -> new WeakHashMap<>(32)); public static int mask(Class<? extends MyChannelEventHandler> clazz) {
// 对于非共享的handler,会随着channel的创建而被大量创建
// 为了避免反复的计算同样类型handler的mask掩码而引入缓存,优先从缓存中获得对应处理器类的掩码
Map<Class<? extends MyChannelEventHandler>, Integer> cache = MASKS.get();
Integer mask = cache.get(clazz);
if (mask == null) {
// 缓存中不存在,计算出对应类型的掩码值
mask = calculateChannelHandlerMask(clazz);
cache.put(clazz, mask);
}
return mask;
} private static int calculateChannelHandlerMask(Class<? extends MyChannelEventHandler> handlerType) {
int mask = 0; // MyChannelEventHandler中的方法一一对应,如果支持就通过掩码的或运算将对应的bit位设置为1 if(!needSkip(handlerType,"channelRead", MyChannelHandlerContext.class,Object.class)){
mask |= MASK_CHANNEL_READ;
} if(!needSkip(handlerType,"exceptionCaught", MyChannelHandlerContext.class,Throwable.class)){
mask |= MASK_EXCEPTION_CAUGHT;
} if(!needSkip(handlerType,"close", MyChannelHandlerContext.class)){
mask |= MASK_CLOSE;
} if(!needSkip(handlerType,"write", MyChannelHandlerContext.class,Object.class)){
mask |= MASK_WRITE;
} return mask;
} private static boolean needSkip(Class<?> handlerType, String methodName, Class<?>... paramTypes) {
try {
Method method = handlerType.getMethod(methodName, paramTypes); // 如果有skip注解,说明需要跳过
return method.isAnnotationPresent(Skip.class);
} catch (NoSuchMethodException e) {
// 没有这个方法,就不需要设置掩码
return false;
}
}
}
/**
* 用于简化用户自定义的handler的适配器
*
* 由于所有支持的方法都加上了@Skip注解,子类只需要重写想要关注的方法即可,其它未重写的方法将会在事件传播时被跳过
* */
public class MyChannelEventHandlerAdapter implements MyChannelEventHandler{ /**
* 当前是否已经被加入sharable缓存
* */
public volatile boolean added; @Skip
@Override
public void channelRead(MyChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
} @Skip
@Override
public void exceptionCaught(MyChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause);
} @Skip
@Override
public void close(MyChannelHandlerContext ctx) throws Exception {
ctx.close();
} @Skip
@Override
public void write(MyChannelHandlerContext ctx, Object msg) throws Exception {
ctx.write(msg);
} private static ConcurrentHashMap<Class<?>, Boolean> isSharableCacheMap = new ConcurrentHashMap<>(); public boolean isSharable() {
/**
* MyNetty中直接用全局的ConcurrentHashMap来缓存handler类是否是sharable可共享的,实现起来很简单
* 而netty中利用FastThreadLocal做了优化,避免了不同线程之间的锁争抢
* 高并发下每分每秒都会创建大量的链接以及所属的Handler,优化后性能会有很大提升
*
* See <a href="https://github.com/netty/netty/issues/2289">#2289</a>.
*/
Class<?> clazz = getClass();
Boolean sharable = isSharableCacheMap.computeIfAbsent(
clazz, k -> clazz.isAnnotationPresent(Sharable.class));
return sharable;
}
}
  • 在ChannelHandler被加入到pipeline时,会被包装成AbstractChannelHandlerContext节点加入链表。在AbstractChannelHandlerContext的构造方法中,计算出对应ChannelHandler的掩码。
  • ChannelHandler中的每个IO事件的方法都对应mask掩码的一个bit位,bit位为1则代表对该IO事件感兴趣,为0则代表不感兴趣需要跳过。在IO事件传播时,通过对应掩码进行与操作快速的判断是否需要跳过该节点。
  • 具体每一位的掩码值是通过方法上是否含有@Skip注解来判断的,带上了该注解就表示对当前IO事件不感兴趣,传播时需要跳过该ChannelHandler。
  • 掩码的计算引入了map缓存,相同类型的ChannelHandler实例的掩码不需要重复计算,在创建大量连接时,其对应pipeline中的AbstractChannelHandlerContext实例也会被大量创建,使用缓存能很好的提高性能。
  • Netty为入站,出站的ChannelHandler分别提供了ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter两个适配器,其方法中默认都带上了@Skip注解。

    在实际开发时,用户可以选择令自己的自定义ChannelHandler继承对应的Adapter,重写感兴趣的IO事件的方法。重写后的方法不会带@Skip注解,会在IO事件传播时触发其自定义的方法逻辑。

2.5 @Sharable防共享检测原理简单介绍

netty还提供了防共享检测机制,用来避免用户错误的使用共享ChannelHandler。

  • 正常情况下,每个ChannelPipeline中对应的ChannelEventHandler实例都是互相独立的,但在一些场景下使用共享的ChannelHandler能带来更好的性能。对于一些无状态的,或者架构上就是全局唯一的handler(比如dubbo中维护业务线程池的Handler),令其在不同的Channel中共享是一个好的选择。
  • netty会在ChannelHandler加入到pipeline时对其进行检查,如果存在一个ChannelHandler实例被不止一次的注册到netty中,netty会认为其被错误的注册。因为默认情况下,一个ChannelHandler实例不能同时被注册到一个以上的channel中,否则其将出现并发问题,netty会抛出异常来警告用户。

    而只有当用户在对应的ChannelHandler上显式标记上@Sharable注解,明确了其就是可以共享,已经考虑过并发的可能性时,才能在重复注册时通过校验。
  • 从个人的经历来说,我在初次使用netty时曾对@Sharable注解的功能有过误解。第一感觉是在构造流水线时,被打上了@Sharable注解的Handler会类似spring的单例模式一样,即使重复注册也会被netty自动的弄成全局唯一。

    但在了解了其工作原理后发现是反过来的,@Sharable更多的是起到一个检查的作用,避免用户错误的重复注册并发不安全的ChannelHandler。

2.6 EventLoop改造接入pipeline流水线

目前lab2版本的EventLoop还比较简单,只是在处理读事件的时候从原来的直接调用EventHandler的fireChannelRead方法,改造成了调用pipeline的fireChannelRead方法,令读事件在整个ChannelHandler流水线中传播。

    private void processReadEvent(SelectionKey key) throws Exception {
SocketChannel socketChannel = (SocketChannel)key.channel(); // 目前所有的attachment都是MyNioChannel
MyNioSocketChannel myNioChannel = (MyNioSocketChannel) key.attachment(); // 简单起见,buffer不缓存,每次读事件来都新创建一个
// 暂时也不考虑黏包/拆包场景(Netty中靠ByteToMessageDecoder解决,后续再分析其原理),理想的认为每个消息都小于1024,且每次读事件都只有一个消息
ByteBuffer readBuffer = ByteBuffer.allocate(1024); int byteRead = socketChannel.read(readBuffer);
logger.info("processReadEvent byteRead={}",byteRead);
if(byteRead == -1){
// 简单起见不考虑tcp半连接的情况,返回-1直接关掉连接
socketChannel.close();
}else{
// 将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
readBuffer.flip();
// 根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[readBuffer.remaining()];
// 将缓冲区可读字节数组复制到新建的数组中
readBuffer.get(bytes); if(myNioChannel != null) {
// 触发pipeline的读事件
myNioChannel.getChannelPipeline().fireChannelRead(bytes);
}else{
logger.error("processReadEvent attachment myNioChannel is null!");
}
}
}

3.MyNettyBootstrap与新版本Echo服务器demo实现

在实现了pipeline流水线功能后,配置自定义事件处理器的方式也要有所改变,MyNetty参考netty实现了一个简单的Client/Server的Bootstrap。

其中构建pipeline的方式与netty有所不同,netty中使用了一个特殊的ChannelInboundHandler,即ChannelInitializer。ChannelInitializer会在连接被注册时触发initChannel方法,执行用户自定义的组装pipeline的逻辑,然后再将这个特殊的Handler从链表中remove掉以完成最终channel链表的构建。

而MyNetty简单起见,并没有支持用户自定义channel的惰性创建,也不支持在运行时动态的增加或删除pipeline中链表中的handler(所以没有那些handler状态的临界值判断),而是直接设计了一个MyChannelPipelineSupplier接口,在MyNIOChannel被创建时,也一并创建pipeline中的handler链表。

服务端Bootstrap
public class MyNioServerBootstrap {

    private static final Logger logger = LoggerFactory.getLogger(MyNioServerBootstrap.class);

    private final InetSocketAddress endpointAddress;

    private final MyNioEventLoopGroup bossGroup;

    public MyNioServerBootstrap(InetSocketAddress endpointAddress,
MyChannelPipelineSupplier childChannelPipelineSupplier,
int bossThreads, int childThreads) {
this.endpointAddress = endpointAddress; MyNioEventLoopGroup childGroup = new MyNioEventLoopGroup(childChannelPipelineSupplier,childThreads);
this.bossGroup = new MyNioEventLoopGroup(childChannelPipelineSupplier, bossThreads, childGroup);
} public void start() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); MyNioEventLoop myNioEventLoop = this.bossGroup.next(); myNioEventLoop.execute(()->{
try {
Selector selector = myNioEventLoop.getUnwrappedSelector();
serverSocketChannel.socket().bind(endpointAddress);
SelectionKey selectionKey = serverSocketChannel.register(selector, 0);
// 监听accept事件
selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_ACCEPT);
logger.info("MyNioServer do start! endpointAddress={}",endpointAddress);
} catch (IOException e) {
logger.error("MyNioServer do bind error!",e);
}
});
}
}
客户端Bootstrap
public class MyNioClientBootstrap {

    private static final Logger logger = LoggerFactory.getLogger(MyNioClientBootstrap.class);

    private final InetSocketAddress remoteAddress;

    private final MyNioEventLoopGroup eventLoopGroup;

    private MyNioSocketChannel myNioSocketChannel;

    private final MyChannelPipelineSupplier myChannelPipelineSupplier;

    public MyNioClientBootstrap(InetSocketAddress remoteAddress, MyChannelPipelineSupplier myChannelPipelineSupplier) {
this.remoteAddress = remoteAddress; this.eventLoopGroup = new MyNioEventLoopGroup(myChannelPipelineSupplier, 1); this.myChannelPipelineSupplier = myChannelPipelineSupplier;
} public void start() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); MyNioEventLoop myNioEventLoop = this.eventLoopGroup.next(); myNioEventLoop.execute(()->{
try {
Selector selector = myNioEventLoop.getUnwrappedSelector(); myNioSocketChannel = new MyNioSocketChannel(selector,socketChannel,myChannelPipelineSupplier); myNioEventLoop.register(myNioSocketChannel); // doConnect
// Returns: true if a connection was established,
// false if this channel is in non-blocking mode and the connection operation is in progress;
if(!socketChannel.connect(remoteAddress)){
// 简单起见也监听READ事件,相当于netty中开启了autoRead
int clientInterestOps = SelectionKey.OP_CONNECT | SelectionKey.OP_READ; myNioSocketChannel.getSelectionKey().interestOps(clientInterestOps); // 监听connect事件
logger.info("MyNioClient do start! remoteAddress={}",remoteAddress);
}else{
logger.info("MyNioClient do start connect error! remoteAddress={}",remoteAddress); // connect操作直接失败,关闭连接
socketChannel.close();
}
} catch (IOException e) {
logger.error("MyNioClient do connect error!",e);
}
});
}
}

原来的Echo服务端/客户端demo也对逻辑进行了拆分,将业务逻辑和编解码逻辑拆分成了不同的ChannelHandler。

Echo服务器与客户端编解码处理器实现
public class EchoMessageEncoder extends MyChannelEventHandlerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(EchoMessageEncoder.class);

    @Override
public void write(MyChannelHandlerContext ctx, Object msg) throws Exception {
// 写事件从tail向head传播,msg一定是string类型
String message = (String) msg; ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
writeBuffer.put(message.getBytes(StandardCharsets.UTF_8));
writeBuffer.flip(); logger.info("EchoMessageEncoder message to byteBuffer, " +
"message={}, writeBuffer={}",message,writeBuffer); ctx.write(writeBuffer);
}
}
public class EchoMessageDecoder extends MyChannelEventHandlerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(EchoMessageDecoder.class);

    @Override
public void channelRead(MyChannelHandlerContext ctx, Object msg) throws Exception {
// 读事件从head向tail传播,msg一定是string类型
String receivedStr = new String((byte[]) msg, StandardCharsets.UTF_8); logger.info("EchoMessageDecoder byteBuffer to message, " +
"msg={}, receivedStr={}",msg,receivedStr); // 当前版本,不考虑黏包拆包等各种问题,decoder只负责将byte转为string
ctx.fireChannelRead(receivedStr);
}
}
Echo服务器与客户端demo
public class ServerDemo {
public static void main(String[] args) throws IOException {
MyNioServerBootstrap myNioServerBootstrap = new MyNioServerBootstrap(
new InetSocketAddress(8080),
// 先简单一点,只支持childEventGroup自定义配置pipeline
new MyChannelPipelineSupplier() {
@Override
public MyChannelPipeline buildMyChannelPipeline(MyNioChannel myNioChannel) {
MyChannelPipeline myChannelPipeline = new MyChannelPipeline(myNioChannel);
// 注册自定义的EchoServerEventHandler
myChannelPipeline.addLast(new EchoMessageEncoder());
myChannelPipeline.addLast(new EchoMessageDecoder());
myChannelPipeline.addLast(new EchoServerEventHandler());
return myChannelPipeline;
}
},1,5);
myNioServerBootstrap.start();
}
}
public class ClientDemo {
public static void main(String[] args) throws IOException {
MyNioClientBootstrap myNioClientBootstrap = new MyNioClientBootstrap(new InetSocketAddress(8080),new MyChannelPipelineSupplier() {
@Override
public MyChannelPipeline buildMyChannelPipeline(MyNioChannel myNioChannel) {
MyChannelPipeline myChannelPipeline = new MyChannelPipeline(myNioChannel);
// 注册自定义的EchoClientEventHandler
myChannelPipeline.addLast(new EchoMessageEncoder());
myChannelPipeline.addLast(new EchoMessageDecoder());
myChannelPipeline.addLast(new EchoClientEventHandler());
return myChannelPipeline;
}
});
myNioClientBootstrap.start();
}
}

总结

  • 在lab2中,MyNetty实现了pipeline流水线机制,允许用户构造自定义处理器链条,进行功能的解耦。同时也提供了一个Bootstrap脚手架帮助用户更快捷的实现自己的网络应用程序。

    相信在了解了MyNetty的简易版本流水线功能实现后,能帮助读者更好的理解netty中更加复杂的pipeline工作原理。
  • 目前为止,受限于MyNetty现版本的简陋功能,我们的Echo服务应用程序还非常原始,大量极端场景下的临界条件都没有处理。比如分配的ByteBuffer是固定大小,无法动态扩容,接受的消息体过大就会出错;发送和接受的消息也存在黏包、拆包的问题,等等。千里之行始于足下,在后续的lab中,MyNetty会逐步的完善上述提到的问题。
  • 在迭代MyNetty的过程中,读者也将能够体会到Netty的强大之处。因为在普通使用者无法直接感知的地方,netty底层处理了大量的边界情况,这才使得普通开发者能够基于netty高效的构建起一个健壮的网络应用程序。

博客中展示的完整代码在我的github上:https://github.com/1399852153/MyNetty (release/lab2_pipeline_handle 分支),内容如有错误,还请多多指教。

从零开始实现简易版Netty(二) MyNetty pipeline流水线的更多相关文章

  1. Netty核心组件介绍及手写简易版Tomcat

    Netty是什么: 异步事件驱动框架,用于快速开发高i性能服务端和客户端 封装了JDK底层BIO和NIO模型,提供高度可用的API 自带编码解码器解决拆包粘包问题,用户只用关心业务逻辑 精心设计的Re ...

  2. Android学习之路——简易版微信为例(二)

    1 概述 从这篇博文开始,正式进入简易版微信的开发.深入学习前,想谈谈个人对Android程序开发一些理解,不一定正确,只是自己的一点想法.Android程序开发不像我们在大学时候写C控制台程序那样, ...

  3. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

  4. 简易版自定义BaseServlet

    这几天在学Java Web,一直在思考Servlet重用的问题,就用java的反射机制实现自定义的简易版BaseServlet; 该方式有点像struts2 利用映射获取前端的参数.有兴趣的同学可以自 ...

  5. 依赖注入[5]: 创建一个简易版的DI框架[下篇]

    为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在<依赖注入[4]: 创建一个简易版的DI框架[上篇]> ...

  6. 依赖注入[4]: 创建一个简易版的DI框架[上篇]

    本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章(<控制反转>.<基于IoC的设计模式>和< 依赖注入模式>)从纯理论的角度 ...

  7. .NET CORE学习笔记系列(2)——依赖注入[4]: 创建一个简易版的DI框架[上篇]

    原文https://www.cnblogs.com/artech/p/net-core-di-04.html 本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章从 ...

  8. 贪吃蛇(简易版)Leslie5205912著

    # include <stdio.h># include <string.h># include <windows.h># include <stdlib.h ...

  9. Ocelot简易教程(二)之快速开始2

    为什么这篇的标题叫"Ocelot简易教程(二)之快速开始2"呢,因为很多朋友跟我说上一篇" Ocelot简易教程(二)之快速开始1"内容太少了,只是简单介绍Oc ...

  10. Ocelot简易教程(二)之快速开始1

    Ocelot简易教程目录 Ocelot简易教程(一)之Ocelot是什么 Ocelot简易教程(二)之快速开始1 Ocelot简易教程(二)之快速开始2 Ocelot简易教程(三)之主要特性及路由详解 ...

随机推荐

  1. ELF-Virus简易病毒程序分析

    系统功能概述 ELF-Virus实现了一个简单的病毒程序,能够感染当前目录下的ELF格式的可执行文件.病毒程序通过将自身代码附加到目标文件中,并在文件末尾添加一个特定的签名来标记文件已被感染.感染后的 ...

  2. StringBuilder案例

    1.案例一 如图 这里无法使用反转方法的原因是,s属于String类型,而反转的方法存在于StringBuilder类型,所以我们要将s的类型转换为StringBuilder String--> ...

  3. 【HTML】步骤进度条组件

    HTML步骤进度条 效果图 思路 分份: 有多少个步骤就可以分成多少分,每份宽度应该为100%除以步骤数,故以上效果图中的每份宽度应该为25%,每份用一个div. 每份: 每份中可以看成是三个元素,一 ...

  4. datasnap的restful服务器

    说真话,这玩意真的简单好用.但你要控制好: 1.内存泄漏和异常处理好: 2.有没有发现,通过服务器对数据库进行读写时,在资源管理器中,如果是sql server,就会看到连接1433的连接一直挂在那里 ...

  5. 备份一个http/https请求,用的比较多的POST json数据

    var data = new object[] { new { sn = SN, mac = Mac } }; var jobj = await Task.Run(() => { try { u ...

  6. [开源] .Net 使用 ORM 访问 神舟通用数据库(神通)

    前言 天津神舟通用数据技术有限公司(简称"神舟通用公司"),隶属于中国航天科技集团(CASC).是国内从事数据库.大数据解决方案和数据挖掘分析产品研发的专业公司.公司获得了国家核高 ...

  7. .net core项目代码提交忽略文件.gitignore的配置

    根据语言自动生成 1. 访问 .gitignore.io 首先,访问 https://www.gitignore.io/.这是一个非常有用的网站,可以根据你的开发环境自动生成 .gitignore 文 ...

  8. PyQt6安装与配置(附带Vscode配置)

    1. 安装PyQt6和PyQt-tools pip install PyQt6 pip install PyQt6-tools 2. Vscode配置QtDesigner 安装PyQt Integra ...

  9. Web前端入门第 44 问:CSS 循环动画 animation 效果演示

    相关属性 @keyframes 定义动画的关键帧序列 animation-name 指定 @keyframes 动画的名称 animation-duration 动画单次循环的持续时间(必需属性,否则 ...

  10. servlet @WebServlet注解

    web开发中可以通过web.xml写servlet标签表明一个类是Servlet,servlet3.0后可以使用@WebServlet表示一个类为Servlet. @WebServlet 参数 说明 ...