Netty源码分析之ChannelPipeline—出站事件的传播
上篇文章中我们梳理了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—出站事件的传播的更多相关文章
- Netty源码分析之ChannelPipeline—入站事件的传播
之前的文章中我们说过ChannelPipeline作为Netty中的数据管道,负责传递Channel中消息的事件传播,事件的传播分为入站和出站两个方向,分别通知ChannelInboundHandle ...
- Netty源码分析之ChannelPipeline—异常事件的传播
ChannelHandler中异常的获取与处理是通过继承重写exceptionCaught方法来实现的,本篇文章我们对ChannelPipeline中exceptionCaught异常事件的传播进行梳 ...
- 【Netty源码分析】ChannelPipeline(二)
在上一篇博客[Netty源码学习]ChannelPipeline(一)中我们只是大体介绍了ChannelPipeline相关的知识,其实介绍的并不详细,接下来我们详细介绍一下ChannelPipeli ...
- Netty源码分析之ChannelPipeline(一)—ChannelPipeline的构造与初始化
Netty中ChannelPipeline实际上类似与一条数据管道,负责传递Channel中读取的消息,它本质上是基于责任链模式的设计与实现,无论是IO事件的拦截器,还是用户自定义的ChannelHa ...
- [编织消息框架][netty源码分析]6 ChannelPipeline 实现类DefaultChannelPipeline职责与实现
ChannelPipeline 负责channel数据进出处理,如数据编解码等.采用拦截思想设计,经过A handler处理后接着交给next handler ChannelPipeline 并不是直 ...
- Netty源码分析之ChannelPipeline(二)—ChannelHandler的添加与删除
上篇文章中,我们对Netty中ChannelPipeline的构造与初始化进行了分析与总结,本篇文章我们将对ChannelHandler的添加与删除操作进行具体的的代码分析: 一.ChannelHan ...
- netty源码分析系列文章
netty源码分析系列文章 nettynetty源码阅读netty源码分析 想在年终之际将对netty研究的笔记记录下来,先看netty3,然后有时间了再写netty4的,希望对大家有所帮助,这个是 ...
- Netty 源码分析——ChannelPipeline
Netty 源码分析--ChannelPipeline 通过前面的两章我们分析了客户端和服务端的流程代码,其中在初始化 Channel 的时候一定会看到一个 ChannelPipeline.所以在 N ...
- Netty 源码分析系列(二)Netty 架构设计
前言 上一篇文章,我们对 Netty做了一个基本的概述,知道什么是Netty以及Netty的简单应用. Netty 源码分析系列(一)Netty 概述 本篇文章我们就来说说Netty的架构设计,解密高 ...
随机推荐
- python学习笔记(1)python数据类型
一.数据类型计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值.但是,计算机能处理的远不止数值,还可以处理文本.图形.音频.视频.网页等各种各样的数据,不同的数据,需要 ...
- 1076 Wifi密码 (15 分)
题目:传送门 下面是微博上流传的一张照片:“各位亲爱的同学们,鉴于大家有时需要使用 wifi,又怕耽误亲们的学习,现将 wifi 密码设置为下列数学题答案:A-1:B-2:C-3:D-4:请同学们自己 ...
- java String、StringBuilder
Java中的String和StringBuilder类: 1.String对象是不可变的.每一个看起来修改了String值的方法,实际上都是创建了全新的String对象.代码示例如下: String ...
- [洛谷P4777] [模板] 扩展中国剩余定理
扩展中国剩余定理,EXCRT. 题目传送门 重温一下中国剩余定理. 中国剩余定理常被用来解线性同余方程组: x≡a[1] (mod m[1]) x≡a[2] (mod m[2]) ...... x≡a ...
- 洛谷P4180【Beijing2010组队】次小生成树Tree
题目描述: 小C最近学了很多最小生成树的算法,Prim算法.Kurskal算法.消圈算法等等.正当小C洋洋得意之时,小P又来泼小C冷水了.小P说,让小C求出一个无向图的次小生成树,而且这个次小生成树还 ...
- 转:zabbix 2.0.6监控cisco交换机 2950 2960s 3560G
转自: http://blog.chinaunix.net/uid-24250828-id-3806551.html 想在zabbix 上监控交换机端口的流量,找了两天的模板,包括官方的和网友写的.在 ...
- SpringMVC在使用过程中的错误
HTTP Status 500 - Request processing failed; nested exception is org.springframework.validation.Bind ...
- Servlet与JSP概念理解
Servlet是用Java编写的服务端程序.需要部署到servlet容器上才能运行,tomcat 就是一个servlet容器. 1.Servlet的生命周期 客户端请求该 Servlet --> ...
- XML的四种解析器(dom_sax_jdom_dom4j)原理及性能比较[收藏]
1)DOM(JAXP Crimson解析器) DOM是用与平台和语言无关的方式表示XML文档的官方W3C标准.DOM是以层次结构组织的节点或信息片断的集合.这个层次结构允许开发人员在树中寻找特定 ...
- Oracle中的列转行实现字段拼接用例
文章目录 Oracle中的列转行实现字段拼接 场景 在SQL使用过程中经常有这种需求:将某列字段拼接成in('XX','XX','XX','XX','XX','XX' ...)做为查询条件. 实现 s ...