从零开始实现简易版Netty(二) MyNetty pipeline流水线
1. Netty pipeline流水线介绍
在上一篇博客中lab1版本的MyNetty参考netty实现了一个极其精简的reactor模型。按照计划,lab2版本的MyNetty需要实现pipeline流水线,以支持不同的逻辑处理模块的解耦。
由于本文属于系列博客,读者需要对之前的博客内容有所了解才能更好地理解本文内容。
- lab1版本博客:从零开始实现简易版Netty(一) MyNetty Reactor模式
在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的事件处理器
java
/**
* 事件处理器(相当于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流水线实现
java
public interface MyChannelEventInvoker {
// ========================= inbound入站事件 ==============================
void fireChannelRead(Object msg);
void fireExceptionCaught(Throwable cause);
// ========================= outbound出站事件 ==============================
void close();
void write(Object msg);
}
java
/**
* 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上下文接口定义
java
public interface MyChannelHandlerContext extends MyChannelEventInvoker {
MyNioChannel channel();
MyChannelEventHandler handler();
MyChannelPipeline getPipeline();
MyNioEventLoop executor();
}
MyAbstractChannelHandlerContext上下文骨架类
java
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哨兵头结点
java
/**
* 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哨兵尾结点
java
/**
* 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过滤的。
java
/**
* 计算并缓存每一个类型的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;
}
}
}
java
/**
* 用于简化用户自定义的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流水线中传播。
java
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
java
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
java
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服务器与客户端编解码处理器实现
java
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);
}
}
java
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
java
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();
}
}
java
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 分支),内容如有错误,还请多多指教。