Channel是理解和使用Netty的核心。Channel的涉及内容较多,这里我使用由浅入深的介绍方法。在这篇文章中,我们主要介绍Channel部分中Pipeline实现机制。为了避免枯燥,借用一下《盗梦空间》的“梦境”概念,希望大家喜欢。

一层梦境:Channel实现概览

在Netty里,Channel是通讯的载体,而ChannelHandler负责Channel中的逻辑处理。

那么ChannelPipeline是什么呢?我觉得可以理解为ChannelHandler的容器:一个Channel包含一个ChannelPipeline,所有ChannelHandler都会注册到ChannelPipeline中,并按顺序组织起来。

在Netty中,ChannelEvent是数据或者状态的载体,例如传输的数据对应MessageEvent,状态的改变对应ChannelStateEvent。当对Channel进行操作时,会产生一个ChannelEvent,并发送到ChannelPipeline。ChannelPipeline会选择一个ChannelHandler进行处理。这个ChannelHandler处理之后,可能会产生新的ChannelEvent,并流转到下一个ChannelHandler。

例如,一个数据最开始是一个MessageEvent,它附带了一个未解码的原始二进制消息ChannelBuffer,然后某个Handler将其解码成了一个数据对象,并生成了一个新的MessageEvent,并传递给下一步进行处理。

到了这里,可以看到,其实Channel的核心流程位于ChannelPipeline中。于是我们进入ChannelPipeline的深层梦境里,来看看它具体的实现。

二层梦境:ChannelPipeline的主流程

Netty的ChannelPipeline包含两条线路:Upstream和Downstream。Upstream对应上行,接收到的消息、被动的状态改变,都属于Upstream。Downstream则对应下行,发送的消息、主动的状态改变,都属于Downstream。ChannelPipeline接口包含了两个重要的方法:sendUpstream(ChannelEvent e)和sendDownstream(ChannelEvent e),就分别对应了Upstream和Downstream。

对应的,ChannelPipeline里包含的ChannelHandler也包含两类:ChannelUpstreamHandler和ChannelDownstreamHandler。每条线路的Handler是互相独立的。它们都很简单的只包含一个方法:ChannelUpstreamHandler.handleUpstream和ChannelDownstreamHandler.handleDownstream。

Netty官方的javadoc里有一张图(ChannelPipeline接口里),非常形象的说明了这个机制(我对原图进行了一点修改,加上了ChannelSink,因为我觉得这部分对理解代码流程会有些帮助):

什么叫ChannelSink呢?ChannelSink包含一个重要方法ChannelSink.eventSunk,可以接受任意ChannelEvent。"sink"的意思是"下沉",那么"ChannelSink"好像可以理解为"Channel下沉的地方"?实际上,它的作用确实是这样,也可以换个说法:"处于末尾的万能Handler"。最初读到这里,也有些困惑,这么理解之后,就感觉简单许多。只有Downstream包含ChannelSink,这里会做一些建立连接、绑定端口等重要操作。为什么UploadStream没有ChannelSink呢?我只能认为,一方面,不符合"sink"的意义,另一方面,也没有什么处理好做的吧!

这里有个值得注意的地方:在一条“流”里,一个ChannelEvent并不会主动的"流"经所有的Handler,而是由上一个Handler显式的调用ChannelPipeline.sendUp(Down)stream产生,并交给下一个Handler处理。也就是说,每个Handler接收到一个ChannelEvent,并处理结束后,如果需要继续处理,那么它需要调用sendUp(Down)stream新发起一个事件。如果它不再发起事件,那么处理就到此结束,即使它后面仍然有Handler没有执行。这个机制可以保证最大的灵活性,当然对Handler的先后顺序也有了更严格的要求。

顺便说一句,在Netty 3.x里,这个机制会导致大量的ChannelEvent对象创建,因此Netty 4.x版本对此进行了改进。twitter的finagle框架实践中,就提到从Netty 3.x升级到Netty 4.x,可以大大降低GC开销。有兴趣的可以看看这篇文章:https://blog.twitter.com/2013/netty-4-at-twitter-reduced-gc-overhead

下面我们从代码层面来对这里面发生的事情进行深入分析,这部分涉及到一些细节,需要打开项目源码,对照来看,会比较有收获。

三层梦境:深入ChannelPipeline内部

DefaultChannelPipeline的内部结构

ChannelPipeline的主要的实现代码在DefaultChannelPipeline类里。列一下DefaultChannelPipeline的主要字段:

public class DefaultChannelPipeline implements ChannelPipeline {

private volatile Channel channel;

private volatile ChannelSink sink;

private volatile DefaultChannelHandlerContext head;

private volatile DefaultChannelHandlerContext tail;

private final Map name2ctx =

new HashMap(4);

}

这里需要介绍一下ChannelHandlerContext这个接口。顾名思义,ChannelHandlerContext保存了Netty与Handler相关的的上下文信息。而咱们这里的DefaultChannelHandlerContext,则是对ChannelHandler的一个包装。一个DefaultChannelHandlerContext内部,除了包含一个ChannelHandler,还保存了"next"和"prev"两个指针,从而形成一个双向链表。

因此,在DefaultChannelPipeline中,我们看到的是对DefaultChannelHandlerContext的引用,而不是对ChannelHandler的直接引用。这里包含"head"和"tail"两个引用,分别指向链表的头和尾。而name2ctx则是一个按名字索引DefaultChannelHandlerContext用户的一个map,主要在按照名称删除或者添加ChannelHandler时使用。

sendUpstream和sendDownstream

前面提到了,ChannelPipeline接口的两个重要的方法:sendUpstream(ChannelEvent e)和sendDownstream(ChannelEvent e)。所有事件的发起都是基于这两个方法进行的。Channels类有一系列fireChannelBound之类的fireXXXX方法,其实都是对这两个方法的facade包装。

下面来看一下这两个方法的实现(对代码做了一些简化,保留主逻辑):

public void sendUpstream(ChannelEvent e) {

DefaultChannelHandlerContext head = getActualUpstreamContext(this.head);

head.getHandler().handleUpstream(head, e);

}

private DefaultChannelHandlerContext getActualUpstreamContext(DefaultChannelHandlerContext ctx) {

DefaultChannelHandlerContext realCtx = ctx;

while (!realCtx.canHandleUpstream()) {

realCtx = realCtx.next;

if (realCtx == null) {

return null;

}

}

return realCtx;

}

这里最终调用了ChannelUpstreamHandler.handleUpstream来处理这个ChannelEvent。有意思的是,这里我们看不到任何"将Handler向后移一位"的操作,但是我们总不能每次都用同一个Handler来进行处理啊?实际上,我们更为常用的是ChannelHandlerContext.handleUpstream方法(实现是DefaultChannelHandlerContext.sendUpstream方法):

public void sendUpstream(ChannelEvent e) {

DefaultChannelHandlerContext next = getActualUpstreamContext(this.next);

DefaultChannelPipeline.this.sendUpstream(next, e);

}

可以看到,这里最终仍然调用了ChannelPipeline.sendUpstream方法,但是它会将Handler指针后移。

我们接下来看看DefaultChannelHandlerContext.sendDownstream:

public void sendDownstream(ChannelEvent e) {

DefaultChannelHandlerContext prev = getActualDownstreamContext(this.prev);

if (prev == null) {

try {

getSink().eventSunk(DefaultChannelPipeline.this, e);

} catch (Throwable t) {

notifyHandlerException(e, t);

}

} else {

DefaultChannelPipeline.this.sendDownstream(prev, e);

}

}

与sendUpstream好像不大相同哦?这里有两点:一是到达末尾时,就如梦境二所说,会调用ChannelSink进行处理;二是这里指针是往前移的,所以我们知道了:

**UpstreamHandler是从前往后执行的,DownstreamHandler是从后往前执行的。**在ChannelPipeline里添加时需要注意顺序了!

DefaultChannelPipeline里还有些机制,像添加/删除/替换Handler,以及ChannelPipelineFactory等,比较好理解,就不细说了。

回到现实:Pipeline解决的问题

好了,深入分析完代码,有点头晕了,我们回到最开始的地方,来想一想,Netty的Pipeline机制解决了什么问题?

我认为至少有两点:

一是提供了ChannelHandler的编程模型,基于ChannelHandler开发业务逻辑,基本不需要关心网络通讯方面的事情,专注于编码/解码/逻辑处理就可以了。Handler也是比较方便的开发模式,在很多框架中都有用到。

二是实现了所谓的"Universal Asynchronous API"。这也是Netty官方标榜的一个功能。用过OIO和NIO的都知道,这两套API风格相差极大,要从一个迁移到另一个成本是很大的。即使是NIO,异步和同步编程差距也很大。而Netty屏蔽了OIO和NIO的API差异,通过Channel提供对外接口,并通过ChannelPipeline将其连接起来,因此替换起来非常简单。

理清了ChannelPipeline的主流程,我们对Channel部分的大致结构算是弄清楚了。可是到了这里,我们依然对一个连接具体怎么处理没有什么概念,下篇文章,我们会分析一下,在Netty中,捷径如何处理连接的建立、数据的传输这些事情。

PS: Pipeline这部分拖了两个月,终于写完了。中间写的实在缓慢,写个高质量(至少是自认为吧!)的文章不容易,但是仍不忍心这部分就此烂尾。中间参考了一些优秀的文章,还自己使用netty开发了一些应用。以后这类文章,还是要集中时间来写完好了。

参考资料:

java channelpipeline,Netty那点事(三)Channel与Pipeline相关推荐

  1. JAVA NIO Netty

    一.BIO        1.阻塞IO与非阻塞IO Java IO的各种流是阻塞的.这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入.该线 ...

  2. Java 和 HTTP 的那些事(四) HTTPS 和 证书

    Java 和 HTTP 的那些事(四) HTTPS 和 证书 说起 HTTP 的那些事,则不得不提 HTTPS ,而说起 HTTPS ,则不得不提数字证书.这篇博客将从 Java 的角度,学习 HTT ...

  3. 读取Java文件到byte数组的三种方式及Java文件操作大全(包括文件加密,String加密)

    读取Java文件到byte数组的三种方式 package zs;import java.io.BufferedInputStream; import java.io.ByteArrayOutputSt ...

  4. 【手把手带你学Java EE】多线程那些事,你了解了吗?

    [手把手带你学Java EE]多线程那些事,你了解了吗? 线程 概念 意义 进程和线程的区别 面试题:谈谈进程和线程的区别和联系 Java中的多线程编程 创建线程的方法 方法一 方法二 方法三 &am ...

  5. java多线程做一件事_关于Java的十件事

    java多线程做一件事 那么,您从一开始就一直在使用Java? 还记得那些被称为" Oak"的日子,OO仍然是热门话题,C ++人士认为Java没有机会,Applet还是一件事吗? ...

  6. java源代码实例倒计时_Java倒计时三种实现方式代码实例

    写完js倒计时,突然想用java实现倒计时,写了三种实现方式 一:设置时长的倒计时: 二:设置时间戳的倒计时: 三:使用java.util.Timer类实现的时间戳倒计时 代码如下: package ...

  7. java你可能不知道的事(2)--堆和栈

    为什么80%的码农都做不了架构师?>>>    在java语言的学习和使用当中你可能已经了解或者知道堆和栈,但是你可能没有完全的理解它们.今天我们就一起来学习堆.栈的特点以及它们的区 ...

  8. Java 数组转成集合List三种方法和(数组、集合List、Set相互转换)

    Java 数组转成集合List 三种方法 package com.list;import java.util.ArrayList; import java.util.Arrays; import ja ...

  9. 数据结构java 图实验代码_20172329 2018-2019 《Java软件结构与数据结构》实验三报告...

    20172329 2018-2019-2 <Java软件结构与数据结构>实验三报告 课程:<Java软件结构与数据结构>html 班级: 1723java 姓名: 王文彬lin ...

最新文章

  1. MTK 平台上如何给 camera 添加一种 preview size
  2. 液冷模块是什么?有什么作用与意义?
  3. 栈与队列基本操作及其应用
  4. OpenCASCADE绘制测试线束:形状修复命令之一般命令
  5. 备战美赛,这些你应该知道的知识点
  6. MIP 技术进展月报:储存功能全新上线,MIP-Cache域名升级,校验更严谨
  7. SpringBoot引入本地jar包
  8. 减治法在查找算法中的应用(JAVA)--快速查找
  9. 如何通过JS获取元素宽高
  10. PowerDesigner教程系列(五)概念数据模型
  11. 分组数据筛选(group by后取出每组的第n条数据)
  12. python 编译成exe vmp加密_加密软件VMProtect入门教程
  13. 犀牛6.0grasshopper翻译插件_Rhino 6.0犀牛三维建模 软件下载安装包教程(附下载方法)...
  14. EP100底层封装-LibCreateSourceFile()函数
  15. java计算机毕业设计快滴预约平台MyBatis+系统+LW文档+源码+调试部署
  16. 低价主机,怎么找性价比虚拟主机香港空间
  17. 电脑如何录屏?分享4个屏幕录制的好方法,建议收藏
  18. BPEL与XPDL的定位区别
  19. 短视频平台还能这样赚钱?无需拍视频,每天能赚200多详细流程
  20. 宝塔面板网页访问不了

热门文章

  1. jquery eaayui 学习(四)datagrid
  2. 渗透测试(Penetration Testing)
  3. 删除 setup.py 安装的 Python 软件包
  4. Windows下编译安装kafka管理工具 kafka-manager (详细)
  5. 三种Hash算法对比以及秒传原理.
  6. 【Dexclassloader】学习
  7. 【Matplotlib】 标注一些点
  8. VSTO学习笔记(四)从SharePoint 2010中下载文件
  9. Oracle 查看表空间大小及其扩展
  10. MyEclipse的快捷使用(含关联源码和Doc的方式)