上篇文章中我们梳理了ChannelPipeline中入站事件的传播,这篇文章中我们看下出站事件的传播,也就是ChannelOutboundHandler接口的实现。

1、出站事件的传播示例

我们对上篇文章中的示例代码进行改造,在ChannelPipeline中加入ChannelOutboundHandler出站实现

public class ServerApp {
public static void main(String[] args) {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup work = new NioEventLoopGroup(2);
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, work).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// p.addLast(new LoggingHandler(LogLevel.INFO));
// 向ChannelPipeline中添加自定义channelHandler
p.addLast(new OutHandlerA());
p.addLast(new ServerHandlerA());
p.addLast(new ServerHandlerB());
p.addLast(new ServerHandlerC());
p.addLast(new OutHandlerB());
p.addLast(new OutHandlerC()); }
});
bootstrap.bind(8050).sync(); } catch (Exception e) {
// TODO: handle exception
} } } public class OutHandlerA extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
System.err.println(this.getClass().getName()+msg);
ctx.writeAndFlush((ByteBuf)msg);
}
} public class OutHandlerB extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx,Object msg,ChannelPromise promise) {
System.out.println(this.getClass().getName()+msg);
ctx.write((ByteBuf)msg);
}
} public class OutHandlerC extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx,Object msg,ChannelPromise promise) {
System.out.println(this.getClass().getName()+"--"+msg);
ctx.write((ByteBuf)msg);
}
}

然后我们在ServerHandlerA的channelRead方法中执行ctx的write方法,模拟消息出站事件的发生。

public class ServerHandlerA  extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object object) {
ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.buffer();
byteBuf.writeByte(1);
byteBuf.writeByte(2);
ctx.channel().write(byteBuf);
//ctx.write(byteBuf);
}

上面channelRead方法中write方法的调用有两种方式 ctx.channel().write 与 ctx.write,这两种方式有何区别呢,我们首先看下这两种方式的运行结果

ctx.channel().write

io.netty.example.echo.my.OutHandlerC--PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)
io.netty.example.echo.my.OutHandlerB--PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)
io.netty.example.echo.my.OutHandlerA--PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)

ctx.write

io.netty.example.echo.my.OutHandlerA--PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)

可以看到当调用ctx.channel().write时,消息在管道中传播的顺序是从尾部一直传递到最上层的OutboundHandler;而 ctx.write会从所在的 handler 向前找 OutboundHandler。

那么这两种方式区别是否就如结果所示呢,下面我们就开始对这两种方法的内部实现进行分析

2、出站事件传播的分析

ctx.channel().write与 ctx.write  分别用的是AbstractChannel与AbstractChannelHandlerContext的write方法

AbstractChannel 的 write方法

    @Override
public ChannelFuture write(Object msg) {
return pipeline.write(msg);
}

AbstractChannelHandlerContext 的 write方法

    @Override
public ChannelFuture write(Object msg) {
return write(msg, newPromise());
}

上面代码中AbstractChannel的 wirte方法最终调用的是pipeline的write方法,我们进入pipeline内部查看,可以看到pipeline的write方法默认从尾部AbstractChannelHandlerContext节点开始调用。

    @Override
public final ChannelFuture write(Object msg) {
return tail.write(msg);
}

继续向下跟踪最终它们调用的都是AbstractChannelHandlerContext 的 write方法,下面我们看下方法内部的具体实现。

    private void write(Object msg, boolean flush, ChannelPromise promise) {
ObjectUtil.checkNotNull(msg, "msg");
try {
if (isNotValidPromise(promise, true)) {//检查ChannelPromise是否有效
ReferenceCountUtil.release(msg);
// cancelled
return;
}
} catch (RuntimeException e) {
ReferenceCountUtil.release(msg);
throw e;
} //寻找上一个AbstractChannelHandlerContext节点
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {//与当前线程是否一致
if (flush) {//确定是否要把数据冲刷到远程节点
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else { //如果不一致的封装成writeTask任务线程
final AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
//把该线程任务交给对应的EventExecutor执行
if (!safeExecute(executor, task, promise, m)) {
// We failed to submit the AbstractWriteTask. We need to cancel it so we decrement the pending bytes
// and put it back in the Recycler for re-use later.
//
// See https://github.com/netty/netty/issues/8343.
task.cancel();
}
}
}

主要关注下findContextOutbound(),这个方法的作用就是获取当前AbstractChannelHandlerContext节点的上一个节点prev

    private AbstractChannelHandlerContext findContextOutbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.prev;//获取当前节点的上一个节点
} while (!ctx.outbound);//判断是不是出站节点
return ctx;
}

最终通过next.invokeWrite(m, promise)回调方法,调用下一个节点中封装的ChannelOutboundHandler的write方法,从而实现write方法事件的传递

        private void invokeWrite(Object msg, ChannelPromise promise) {
if (invokeHandler()) {//判断当前ChannelOutboundHandler是否已经被添加到pipeline中(handlerAdded事件触发)
invokeWrite0(msg, promise);
} else {
write(msg, promise);
}
} private boolean invokeHandler() {
// Store in local variable to reduce volatile reads.
int handlerState = this.handlerState;
return handlerState == ADD_COMPLETE || (!ordered && handlerState == ADD_PENDING);
} private void invokeWrite0(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).write(this, msg, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
}

到这里整个出站事件的传播流程已经基本清晰了,wirte方法本身就是一个寻找并回调下一个节点中wirte方法的过程。

3、write与writeAndFlush

在上面代码中可以看到这两个方法主要在于是否会在执行write方法后,是否会执行flush方法。

    private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) { //是否调用回调方法
//调用write与flush回调方法,最终调用自定义hander的对应实现
invokeWrite0(msg, promise);
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}

这里需要注意的是invokeFlush0()在invokeWrite0后执行,也就是必须等到消息出站事件传递完毕后,才会调用flush把数据冲刷到远程节点。简单理解就是你无论是在OutHandlerA、OutHandlerB还是OutHandlerC中调用writeAndFlush,最后都是要在write事件传递完毕才会flush数据的。

同时我们需要注意到当write与flush事件从OutHandlerA再往上传递时,OutHandlerA的的上一个节点就是Pipeline的头节点HeadContext,我们看下HeadContext的write与flush方法实现;

        @Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
unsafe.write(msg, promise);
} @Override
public void flush(ChannelHandlerContext ctx) {
unsafe.flush();
}

到这里我们可以看出,消息的真正入队与发送最终是通过HeadContext的write与flush方法实现。

通过以上的分析我们可以看到Pipeline出站事件的传播流程,同时我们需要注意ctx.write与ctx.channel().write的区别以及消息的发送最终是通头部节点调用unsafe的write与flush方法实现的,其中如有不足与不正确的地方还望指出与海涵。

关注微信公众号,查看更多技术文章。

Netty源码分析之ChannelPipeline—出站事件的传播的更多相关文章

  1. Netty源码分析之ChannelPipeline—入站事件的传播

    之前的文章中我们说过ChannelPipeline作为Netty中的数据管道,负责传递Channel中消息的事件传播,事件的传播分为入站和出站两个方向,分别通知ChannelInboundHandle ...

  2. Netty源码分析之ChannelPipeline—异常事件的传播

    ChannelHandler中异常的获取与处理是通过继承重写exceptionCaught方法来实现的,本篇文章我们对ChannelPipeline中exceptionCaught异常事件的传播进行梳 ...

  3. 【Netty源码分析】ChannelPipeline(二)

    在上一篇博客[Netty源码学习]ChannelPipeline(一)中我们只是大体介绍了ChannelPipeline相关的知识,其实介绍的并不详细,接下来我们详细介绍一下ChannelPipeli ...

  4. Netty源码分析之ChannelPipeline(一)—ChannelPipeline的构造与初始化

    Netty中ChannelPipeline实际上类似与一条数据管道,负责传递Channel中读取的消息,它本质上是基于责任链模式的设计与实现,无论是IO事件的拦截器,还是用户自定义的ChannelHa ...

  5. [编织消息框架][netty源码分析]6 ChannelPipeline 实现类DefaultChannelPipeline职责与实现

    ChannelPipeline 负责channel数据进出处理,如数据编解码等.采用拦截思想设计,经过A handler处理后接着交给next handler ChannelPipeline 并不是直 ...

  6. Netty源码分析之ChannelPipeline(二)—ChannelHandler的添加与删除

    上篇文章中,我们对Netty中ChannelPipeline的构造与初始化进行了分析与总结,本篇文章我们将对ChannelHandler的添加与删除操作进行具体的的代码分析: 一.ChannelHan ...

  7. netty源码分析系列文章

    netty源码分析系列文章 nettynetty源码阅读netty源码分析  想在年终之际将对netty研究的笔记记录下来,先看netty3,然后有时间了再写netty4的,希望对大家有所帮助,这个是 ...

  8. Netty 源码分析——ChannelPipeline

    Netty 源码分析--ChannelPipeline 通过前面的两章我们分析了客户端和服务端的流程代码,其中在初始化 Channel 的时候一定会看到一个 ChannelPipeline.所以在 N ...

  9. Netty 源码分析系列(二)Netty 架构设计

    前言 上一篇文章,我们对 Netty做了一个基本的概述,知道什么是Netty以及Netty的简单应用. Netty 源码分析系列(一)Netty 概述 本篇文章我们就来说说Netty的架构设计,解密高 ...

随机推荐

  1. Windows 10操作系统针对不同环境下的安装方法

    一.电脑系统能正常运行 1.解压win10镜像文件 到电脑的非系统分区,运行setup安装文件 2.点击setup应用程序,准备安装 3.准备安装 4.等待安装过程结束,重启即可. 二.光盘安装 1. ...

  2. Redis 事物、悲观、乐观锁 (详细)

    1,概论 事物这东西相信大家都不陌生吧,在学习Spring,Mybatis等框架中, 只要是涉及到数据存储和修改的,都会有事物的存在, 废话就不多说了下面我们来简单的介绍下Redis事物以及锁. 2, ...

  3. SpringMVC基本使用步骤

    使用Spring MVC,第一步就是使用Spring提供的前置控制器,即Servlet的实现类DispatcherServlet拦截url:org.springframework.web.servle ...

  4. Pandas写excel总结:写入多个sheet、1个sheet写入多行、向已有sheet追加数据

    1.最简单最基础的写:1excel1sheet df.to_excel("test.xlxs") 2.在一个excel文件里面写入多个sheet writer=pd.ExcelWr ...

  5. SQLite数据库迁移MySQL(MariaDB)完整步骤

    第一步(SQLite导出数据库): 命令方式导出数据库 > .output d:/data/lagou.sql //导出路径及文件名 > .dump //开始导出 修改lagou.sql文 ...

  6. 吴裕雄--天生自然KITTEN编程:一箭穿心

  7. figure设置坐标轴

    import matplotlib.pyplot as plt import numpy as np x=np.linspace(-3,3,50) y1=x*2+1 y2=x**2 plt.plot( ...

  8. javaScript系列 [28]

    本文介绍JavaScript事件相关的知识点,主要包括事件流.事件处理程序.事件对象(event)以及常见事件类型和事件委托等相关内容. 在网页开发涉及的三种基础技术(HTML CSS JavaScr ...

  9. uboot--tftp

    一.      概述 U-boot中的TFTP用于发送较小的文件.下层使用UDP协议,发送使用UDP 69端口,每次发送的最大分组为512 Bytes.发送双方采用超时重传机制.数据传输模式为octe ...

  10. 联想拯救者y7000使用体验

    前言 我以前的电脑是在电商平台买的二手电脑,期间觉得软件的运行速度慢,又在网上买了一个128G的固态硬盘安装上.就从大一到大四上学期这么使用了三年半的时间.因为自己需要运行一些吃内存的软件,而我的这个 ...