最q很多从事移动互联网和物联网开发的同学l我发邮件或者微博私信我Q咨询推送服务相关的问题。问题五花八门,在帮助大家答疑解惑的q程中,我也寚w题进行了(jin)ȝQ大概可以归Uؓ(f)如下几类Q?/p>
׃咨询者众多,x点也比较集中Q我希望通过本文的案例分析和Ҏ(gu)送服务设计要点的ȝQ帮助大家在实际工作中少走弯路?/p>
Ud互联|时代,推?Push)服务成ؓ(f)App应用不可或缺的重要组成部分,推送服务可以提升用L(fng)z跃度和留存率。我们的手机每天接收到各U各L(fng)q告和提C消息等大多数都是通过推送服务实现的?/p>
随着物联|的发展Q大多数的智能家居都支持Ud推送服务,未来所有接入物联网的智能设备都是推送服务的客户端,q就意味着推送服务未来会(x)面(f)量的设备和l端接入?/p>
Ud推送服务的主要特点如下Q?/p>
Z(jin)解决上述弊端Q一些企业也l出?jin)自q解决Ҏ(gu)Q例如京东云推出的推送服务,可以实现多应用单服务单连接模式,使用AlarmManager定时?j)蟩节省电(sh)量和流量?/p>
家居MQTT消息服务中间Ӟ保持10万用户在UKq接Q?万用户ƈ发做消息h。程序运行一D|间之后,发现内存泄露Q怀疑是Netty的Bug。其它相关信息如下:(x)
首先需要dump内存堆栈Q对疑似内存泄露的对象和引用关系q行分析Q如下所C:(x)
我们发现Netty的ScheduledFutureTask增加?076%Q达?10W个左右的实例Q通过对业务代码的分析发现用户使用IdleStateHandler用于在链路空闲时q行业务逻辑处理Q但是空闲时间设|的比较大,?5分钟?/p>
Netty 的IdleStateHandler?x)根据用L(fng)使用场景Q启动三cd时Q务,分别是:(x)ReaderIdleTimeoutTask?WriterIdleTimeoutTask和AllIdleTimeoutTaskQ它们都?x)被加入到NioEventLoop的Task队列中被调度 和执行?/p>
?于超时时间过长,10W个长链接链\?x)创?0W个ScheduledFutureTask对象Q每个对象还保存有业务的成员变量Q非常消耗内存。用L(fng) 持久代设|的比较大,一些定时Q务被老化到持久代中,没有被JVM垃圾回收掉,内存一直在增长Q用戯认ؓ(f)存在内存泄露?/p>
事实上,我们q一步分析发玎ͼ用户的超时时间设|的非常不合理,15分钟的超时达不到设计目标Q重新设计之后将时旉讄?5U,内存可以正常回收Q问题解冟?/p>
如果?00个长q接Q即便是长周期的定时dQ也不存在内存泄露问题,在新生代通过minor GC可以实现内存回收。正是因为十万的长q接Q导致小问题被放大,引出?jin)后l的各种问题?/p>
事实上,如果用户实有长周期q行的定时Q务,该如何处理?对于量长连接的推送服务,代码处理E有不慎Q就满盘皆输Q下面我们针对Netty的架构特点,介绍下如何用Netty实现百万U客L(fng)的推送服务?/p>
作ؓ(f)高性能的NIO框架Q利用Netty开发高效的推送服务技术上是可行的Q但是由于推送服务自w的复杂性,惌开发出E_、高性能的推送服务ƈ非易事,需要在设计阶段针对推送服务的特点q行合理设计?/p>
百万长连接接入,首先需要优化的是Linux内核参数Q其中Linux最大文件句柄数是最重要的调优参C一Q默认单q程打开的最大句柄数?024Q通过ulimit -a可以查看相关参数Q示例如下:(x)
[root@lilinfeng ~]# ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 256324 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 1024 ......后箋输出省略
当单个推送服务接收到的链接超q上限后Q就?x)?#8220;too many open files”Q所有新的客L(fng)接入失败?/p>
通过vi /etc/security/limits.conf d如下配置参数Q修改之后保存,注销当前用户Q重新登录,通过ulimit -a 查看修改的状态是否生效?/p>
* soft nofile 1000000 * hard nofile 1000000
需要指出的是,管我们可以单个进E打开的最大句柄数修改的非常大Q但是当句柄数达C定数量之后Q处理效率将出现明显下降Q因此,需要根据服务器的硬仉|和处理能力q行合理讄。如果单个服务器性能不行也可以通过集群的方式实现?/p>
从事Ud推送服务开发的同学可能都有体会(x)Q移动无U网l可靠性非常差Q经常存在客L(fng)重置q接Q网l闪断等?/p>
在百万长q接的推送系l中Q服务端需要能够正处理这些网l异常,设计要点如下Q?/p>
最 后特别需要注意的一点就是close_wait q多问题Q由于网l不E_l常?x)导致客L(fng)断连Q如果服务端没有能够?qing)时关闭socketQ就?x)导致处于close_wait状态的链\q多?close_wait状态的链\q不释放句柄和内存等资源Q如果积压过多可能会(x)Dpȝ句柄耗尽Q发?#8220;Too many open files”异常Q新的客L(fng)无法接入Q涉?qing)创建或者打开句柄的操作都失败?/p>
下面对close_wait状态进行下单介l,被动关闭TCPq接状态迁Ud如下所C:(x)
?-1 被动关闭TCPq接状态迁Ud
close_wait 是被动关闭连接是形成的,Ҏ(gu)TCP状态机Q服务器端收到客L(fng)发送的FINQTCP协议栈会(x)自动发送ACKQ链接进入close_wait状态。但如果 服务器端不执行socket的close()操作Q状态就不能由close_waitq移到l(f)ast_ackQ则pȝ中会(x)存在很多close_wait?态的q接。通常来说Q一个close_wait?x)维持至?个小时的旉Q系l默认超时时间的?200U,也就?时Q。如果服务端E序因某个原因导 致系l造成一堆close_wait消耗资源,那么通常是等不到释放那一刻,pȝ已崩溃?/p>
Dclose_waitq多的可能原因如下:(x)
下面我们l合Netty的原理,Ҏ(gu)在的故障点进行分析?/p>
?计要?Q不要在Netty的I/OU程上处理业务(?j)蟩发送和(g)除外)(j)。Why? 对于Javaq程Q线E不能无限增长,q就意味着Netty的ReactorU程数必L敛。Netty的默认值是CPU核数 * 2Q通常情况下,I/O密集型应用徏议线E数量讄大些Q但q主要是针对传统同步I/O而言Q对于非dI/OQ线E数q不讄太大Q尽没有最?|但是I/OU程数经验值是[CPU核数 + 1QCPU核数*2 ]之间?/p>
?如单个服务器支撑100万个长连接,服务器内核数?2Q则单个I/OU程处理的链接数L = 100/(32 * 2) = 15625?假如?S有一ơ消息交互(新消息推送、心(j)x息和其它理消息Q,则^均CAPS = 15625 / 5 = 3125?U。这个数值相比于Netty的处理性能而言压力q不大,但是在实际业务处理中Q经怼(x)有一些额外的复杂逻辑处理Q例如性能l计、记录接口日 志等Q这些业务操作性能开销也比较大Q如果在I/OU程上直接做业务逻辑处理Q可能会(x)dI/OU程Q媄(jing)响对其它链\的读写操作,q就?x)导致被动关闭的?路不能及(qing)时关闭,造成close_wait堆积?/p>
设计要点2Q在I/OU程上执行自定义Task要当?j)。Netty的I/O处理U程NioEventLoop支持两种自定义Task的执行:(x)
Z么NioEventLoop要支持用戯定义Runnable和ScheduledFutureTask的执行,q不是本文要讨论的重点,后箋?x)有专题文章q行介绍。本文重点对它们的媄(jing)响进行分析?/p>
?NioEventLoop中执行Runnable和ScheduledFutureTaskQ意味着允许用户在NioEventLoop中执行非I/O?作类的业务逻辑Q这些业务逻辑通常用消息报文的处理和协议管理相兟뀂它们的执行?x)抢占NioEventLoop I/Od的CPU旉Q如果用戯定义Taskq多Q或者单个Task执行周期q长Q会(x)DI/Od操作被阻塞,q样也间接导致close_wait 堆积?/p>
所 以,如果用户在代码中使用C(jin)Runnable和ScheduledFutureTaskQ请合理讄ioRatio的比例,通过 NioEventLoop的setIoRatio(int ioRatio)Ҏ(gu)可以讄该|默认gؓ(f)50Q即I/O操作和用戯定义d的执行时间比?Q??/p>
我的是当服务端处理v量客L(fng)长连接的时候,不要在NioEventLoop中执行自定义TaskQ或者非?j)蟩cȝ定时d?/p>
?计要?QIdleStateHandler使用要当?j)。很多用户会(x)使用IdleStateHandler做心(j)跛_送和(g),q种用法值得提倡。相比于?己启定时d发送心(j)跻Iq种方式更高效。但是在实际开发中需要注意的是,在心(j)跳的业务逻辑处理中,无论是正常还是异常场景,处理时g要可控,防止时g不可 控导致的NioEventLoop被意外阻塞。例如,?j)蟩时或者发生I/O异常Ӟ业务调用Email发送接口告警,׃Email服务端处理超Ӟ?致邮件发送客L(fng)被阻塞,U联引vIdleStateHandler的AllIdleTimeoutTaskd被阻塞,最lNioEventLoop?路复用器上其它的链\d被阻塞?/p>
对于ReadTimeoutHandler和W(xu)riteTimeoutHandlerQ约束同样存在?/p>
百万U的推送服务,意味着?x)存在百万个长连接,每个长连接都需要靠和App之间的心(j)xl持链\。合理设|心(j)跛_期是非常重要的工作,推送服务的?j)蟩周期讄需要考虑Ud无线|络的特炏V?/p>
?一台智能手上移动网l时Q其实ƈ没有真正q接上InternetQ运营商分配l手机的IP其实是运营商的内|IPQ手机终端要q接上Internet q必通过q营商的|关q行IP地址的{换,q个|关UCؓ(f)NAT(NetWork Address Translation)Q简单来说就是手机终端连接Internet 其实是Ud内网IPQ端口,外网IP之间怺映射?/p>
GGSN(GateWay GPRS Support Note)模块实C(jin)NAT功能Q由于大部分的移动无U网l运营商Z(jin)减少|关NAT映射表的负荷Q如果一个链路有一D|间没有通信时就?x)删除其对?表,造成链\中断Q正是这U刻意羃短空闲连接的释放时Q原本是惌省信道资源的作用Q没惛_让互联网的应用不得以q高于正帔R率发送心(j)xl护推送的?q接。以中移动的2.5G|络ZQ大U?分钟左右的基带空Ԍq接׃(x)被释放?/p>
?于移动无U网l的特点Q推送服务的?j)蟩周期q不能设|的太长Q否则长q接?x)被释放Q造成频繁的客L(fng)重连Q但是也不能讄太短Q否则在当前~Zl一?j)蟩?架的机制下很Ҏ(gu)D信o(h)风暴Q例如微信心(j)跳信令风暴问题)(j)。具体的?j)蟩周期q没有统一的标准,180S也许是个不错的选择Q微信ؓ(f)300S?/p>
在Netty中,可以通过在ChannelPipeline中增加I(yng)dleStateHandler的方式实现心(j)x,在构造函C指定链\I闲旉Q然后实现空闲回调接口,实现?j)蟩的发送和(g),代码如下Q?/p>
public void initChannel({@link Channel} channel) { channel.pipeline().addLast("idleStateHandler", new {@link IdleStateHandler}(0, 0, 180)); channel.pipeline().addLast("myHandler", new MyHandler()); } 拦截链\I闲事gq处理心(j)跻I(x) public class MyHandler extends {@link ChannelHandlerAdapter} { {@code @Override} public void userEventTriggered({@link ChannelHandlerContext} ctx, {@link Object} evt) throws {@link Exception} { if (evt instanceof {@link IdleStateEvent}} { //?j)蟩处? } } }
对于镉K接,每个链\都需要维护自q消息接收和发送缓冲区QJDK原生的NIOcd使用的是java.nio.ByteBuffer,它实际是一个长度固定的Byte数组Q我们都知道数组无法动态扩容,ByteBuffer也有q个限制Q相关代码如下:(x)
public abstract class ByteBuffer extends Buffer implements Comparable { final byte[] hb; // Non-null only for heap buffers final int offset; boolean isReadOnly;
?量无法动态扩展会(x)l用户带来一些麻?ch),例如׃无法预测每条消息报文的长度,可能需要预分配一个比较大的ByteBufferQ这通常也没有问题。但是在 量推送服务系l中Q这?x)给服务端带来沉重的内存负担。假讑֍条推送消息最大上限ؓ(f)10KQ消息^均大ؓ(f)5KQؓ(f)?jin)满?0K消息的处 理,ByteBuffer的容量被讄?0KQ这h条链路实际上多消耗了(jin)5K内存Q如果长链接链\Cؓ(f)100万,每个链\都独立持?ByteBuffer接收~冲区,则额外损耗的d?Total(M) = 1000000 * 5K = 4882M。内存消耗过大,不仅仅增加了(jin)g成本Q而且大内存容易导致长旉的Full GCQ对pȝE_性会(x)造成比较大的冲击?/p>
实际上,最灉|的处理方式就是能够动态调整内存,x收缓冲区可以Ҏ(gu)以往接收的消息进行计,动态调整内存,利用CPU资源来换内存资源Q具体的{略如下Q?/p>
q运的是QNetty提供的ByteBuf支持定w动态调_(d)对于接收~冲区的内存分配器,Netty提供?jin)两U:(x)
相对于FixedRecvByteBufAllocatorQ用AdaptiveRecvByteBufAllocator更ؓ(f)合理Q可以在创徏客户端或者服务端的时候指定RecvByteBufAllocatorQ代码如下:(x)
Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT)
如果默认没有讄Q则使用AdaptiveRecvByteBufAllocator?/p>
另外值得注意的是Q无论是接收~冲是发送缓冲区Q缓冲区的大徏议设|ؓ(f)消息的^均大,不要讄成最大消息的上限Q这?x)导致额外的内存(gu)费。通过如下方式可以讄接收~冲区的初始大小Q?/p>
/** * Creates a new predictor with the specified parameters. * * @param minimum * the inclusive lower bound of the expected buffer size * @param initial * the initial buffer size when no feed back was received * @param maximum * the inclusive upper bound of the expected buffer size */ public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum)
对于消息发送,通常需要用戯己构造ByteBufq编码,例如通过如下工具cd建消息发送缓冲区Q?/p>
?-2 构造指定容量的~冲?/p>
推送服务器承蝲?jin)v量的镉K接,每个镉K接实际就是一个会(x)话。如果每个会(x)话都持有?j)蟩数据、接收缓冲区、指令集{数据结构,而且q些实例随着消息的处理朝生夕灭,q就?x)给服务器带来沉重的GC压力Q同时消耗大量的内存?/p>
最 有效的解决策略就是用内存池Q每个NioEventLoopU程处理N个链路,在线E内部,链\的处理时串行的。假如A链\首先被处理,它会(x)创徏接收~?冲区{对象,待解码完成之后,构造的POJO对象被封装成Task后投递到后台的线E池中执行,然后接收~冲Z(x)被释放,每条消息的接收和处理都会(x)重复?收缓冲区的创建和释放。如果用内存池Q则当A链\接收到新的数据报之后Q从NioEventLoop的内存池中申L(fng)闲的ByteBufQ解码完成之 后,调用releaseByteBuf释放到内存池中,供后lB链\l箋使用?/p>
使用内存池优化之后,单个NioEventLoop的ByteBuf甌和GCơ数从原来的N = 1000000/64 = 15625 ơ减ؓ(f)最?ơ(假设每次甌都有可用的内存)(j)?/p>
下面我们以推特用Netty4的PooledByteBufAllocatorq行GC优化作ؓ(f)案例Q对内存池的效果q行评估Q结果如下:(x)
垃圾生成速度是原来的1/5Q而垃圾清理速度快了(jin)5倍。用新的内存池机制Q几乎可以把|络带宽压满?/p>
Netty4 之前的版本问题如下:(x)每当收到C息或者用户发送信息到q程端,Netty 3均会(x)创徏一个新的堆~冲区。这意味着Q对应每一个新的缓冲区Q都?x)有一个new byte[capacity]。这些缓冲区?x)导致GC压力Qƈ消耗内存带宽。ؓ(f)?jin)安全v见,新的字节数组分配时会(x)用零填充Q这?x)消耗内存带宽。然而,用零 填充的数l很可能?x)再ơ用实际的数据填充,q又?x)消耗同L(fng)内存带宽。如果Java虚拟机(JVMQ提供了(jin)创徏新字节数l而又无需用零填充的方式,那么?们本来就可以内存带宽消耗减?0%Q但是目前没有那样一U方式?/p>
在Netty 4中实C(jin)一个新的ByteBuf内存池,它是一个纯Java版本?nbsp;jemalloc QF(tun)acebook也在用)(j)。现在,Netty不会(x)再因为用零填充缓冲区而浪费内存带宽了(jin)。不q,׃它不依赖于GCQ开发h员需要小?j)内存泄漏。如果忘记在处理E序中释攄冲区Q那么内存用率?x)无限地增长?/p>
Netty默认不用内存池Q需要在创徏客户端或者服务端的时候进行指定,代码如下Q?/p>
Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
使用内存池之后,内存的申请和释放必须成对出现Q即retain()和release()要成对出玎ͼ否则?x)导致内存泄霌Ӏ?/p>
值得注意的是Q如果用内存池Q完成ByteBuf的解码工作之后必L式的调用ReferenceCountUtil.release(msg)Ҏ(gu)收缓冲区ByteBufq行内存释放Q否则它?x)被认?f)仍然在用中Q这样会(x)D内存泄露?/p>
通常情况下,大家都知道不能在Netty的I/OU程上做执行旉不可控的操作Q例如访问数据库、发送Email{。但是有个常用但是非常危险的操作却容易被忽略Q那便是记录日志?/p>
?常,在生产环境中Q需要实时打印接口日志,其它日志处于ERRORU别Q当推送服务发生I/O异常之后Q会(x)记录异常日志。如果当前磁盘的WIO比较高,?能会(x)发生写日志文件操作被同步dQ阻塞时间无法预。这׃(x)DNetty的NioEventLoopU程被阻塞,Socket链\无法被及(qing)时关闭、其 它的链\也无法进行读写操作等?/p>
以最常用的log4jZQ尽它支持异步写日志(AsyncAppenderQ,但是当日志队列满之后Q它?x)同步阻塞业务线E,直到日志队列有空闲位|可用,相关代码如下Q?/p>
synchronized (this.buffer) { while (true) { int previousSize = this.buffer.size(); if (previousSize < this.bufferSize) { this.buffer.add(event); if (previousSize != 0) break; this.buffer.notifyAll(); break; } boolean discard = true; if ((this.blocking) && (!Thread.interrupted()) && (Thread.currentThread() != this.dispatcher)) //判断是业务线E? { try { this.buffer.wait();//d业务U程 discard = false; } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }
cMq类BUGh极强的隐蔽性,往往WIO高的旉持箋非常短,或者是偶现的,在测试环境中很难模拟此类故障Q问题定位难度非常大。这p求读者在qx写代码的时候一定要当心(j)Q注意那些隐性地雗?/p>
常用的TCP参数Q例如TCP层面的接收和发送缓冲区大小讄Q在Netty中分别对应ChannelOption的SO_SNDBUF和SO_RCVBUFQ需要根据推送消息的大小Q合理设|,对于量长连接,通常32K是个不错的选择?/p>
另外一个比较常用的优化手段是软中断,如图所C:(x)如果所有的软中断都q行在CPU0相应|卡的硬件中断上Q那么始l都是cpu0在处理Y中断Q而此时其它CPU资源p费?jin),因?f)无法q行的执行多个Y中断?/p>
?-3 中断信息
?于等?.6.35版本的Linux kernel内核Q开启RPSQ网l通信性能提升20%之上。RPS的基本原理:(x)Ҏ(gu)数据包的源地址Q目的地址以及(qing)目的和源端口Q计出一个hash| 然后Ҏ(gu)q个hash值来选择软中断运行的cpu。从上层来看Q也是说将每个q接和cpul定Qƈ通过q个hash|来均衡Y中断q行在多个cpu 上,从而提升通信性能?/p>
最重要的参数调整有两个Q?/p>
李林锋,2007q毕业于东北大学Q?008q进入华为公总事高性能通信软g的设计和开发工作,?qNIO设计和开发经验,_NNetty、Mina{NIO框架。Netty中国C创始人,《Netty权威指南》作者?/p>
Netty是一个和MINAcM的Java NIO框架Q目前的最新版本是4.0.13Q这两个框架的主要作者好像都?a target="_blank" style="color: #ca0000; text-decoration: none;">同一个韩国h?/p>
Channel是Netty最核心(j)的接口,一个Channel是一个联lSocket的通道Q通过ChannelQ你可以对Socketq行各种操作?/p>
用Netty~写|络E序的时候,你很直接操UChannelQ而是通过ChannelHandler来间接操UChannel?/p>
Netty中的所有handler都实现自ChannelHandler接口。按照输?gu)出来分,分?f)ChannelInboundHandler、ChannelOutboundHandler两大c?/span>?/p> ChannelInboundHandler对从客户端发往服务器的报文q行处理Q一般用来执行解码、读取客L(fng)数据、进行业务处理等Q?/p> ChannelOutboundHandler对从服务器发往客户端的报文q行处理Q一般用来进行编码、发送报文到客户端?/p> ChannelPipeline实际上应该叫做ChannelHandlerPipelineQ可以把ChannelPipeline看成是一个ChandlerHandler的链表,当需要对Channelq行某种处理的时候,Pipeline负责依次调用每一个Handlerq行处理。每个Channel都有一个属于自qPipelineQ调用Channel#pipeline()Ҏ(gu)可以获得Channel的PipelineQ调用Pipeline#channel()Ҏ(gu)可以获得Pipeline的Channel?/p> ChannelPipeline的方法有很多Q其中一部分是用来管理ChannelHandler的,如下面这些:(x) ChannelPipelineq不是直接管理ChannelHandlerQ而是通过ChannelHandlerContext来间接管理,q一炚w过ChannelPipeline的默认实现DefaultChannelPipeline可以看出来?/p> 调用ChannelHandlerContext#channel()Ҏ(gu)可以得到和Contextl定的ChannelQ调?span style="box-sizing: border-box; font-weight: 700;">ChannelHandlerContext#handler()ChannelPipeline
ChannelPipeline addFirst(String name, ChannelHandler handler); ChannelPipeline addLast(String name, ChannelHandler handler); ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler); ChannelPipeline addAfter(String baseName, String name, ChannelHandler handler); ChannelPipeline remove(ChannelHandler handler); ChannelHandler remove(String name); ChannelHandler removeFirst(); ChannelHandler removeLast(); ChannelPipeline replace(ChannelHandler oldHandler, String newName, ChannelHandler newHandler); ChannelHandler replace(String oldName, String newName, ChannelHandler newHandler); ChannelHandler first(); ChannelHandler last(); ChannelHandler get(String name);
ChannelHandlerContext
DefaultChannelHandlerContext和DefaultChannelPipeline是ChannelHandlerContext和ChannelPipeline的默认实玎ͼ下面是它们的部分代码Q?/p>
final class DefaultChannelHandlerContext extends DefaultAttributeMap implements ChannelHandlerContext { volatile DefaultChannelHandlerContext next; volatile DefaultChannelHandlerContext prev; private final boolean inbound; private final boolean outbound; private final AbstractChannel channel; private final DefaultChannelPipeline pipeline; private final String name; private final ChannelHandler handler; private boolean removed; // ... }
final class DefaultChannelPipeline implements ChannelPipeline { // ... final DefaultChannelHandlerContext head; final DefaultChannelHandlerContext tail; // ... }
从上面的代码可以看出Q在DefaultPipeline内部QDefaultChannelHandlerContextl成?jin)一个双向链表:(x)
再来看看DefaultChannelPipeline的构造函敎ͼ(x)
public DefaultChannelPipeline(AbstractChannel channel) { if (channel == null) { throw new NullPointerException("channel"); } this.channel = channel; TailHandler tailHandler = new TailHandler(); tail = new DefaultChannelHandlerContext(this, null, generateName(tailHandler), tailHandler); HeadHandler headHandler = new HeadHandler(channel.unsafe()); head = new DefaultChannelHandlerContext(this, null, generateName(headHandler), headHandler); head.next = tail; tail.prev = head; }
可以看到QDefaultChinnelPipeline内部使用?jin)两个特D的Handler来表CHandler铄头和:(x)
Netty中的所有handler都实现自ChannelHandler接口。按照输?gu)出来分,分?f)ChannelInboundHandler、ChannelOutboundHandler两大cRChannelInboundHandler对从客户端发往服务器的报文q行处理Q一般用来执行解码、读取客L(fng)数据、进行业务处理等QChannelOutboundHandler对从服务器发往客户端的报文q行处理Q一般用来进行编码、发送报文到客户端?/p>
从上面DefaultChannelHandlerContext代码可以知道QHandler实际上分ZU,Inbound和OutboundQ这一点也可以从ChannelHandler接口的子接口得到证明Q?/p>
public interface ChannelInboundHandler extends ChannelHandler { // ... } public interface ChannelOutboundHandler extends ChannelHandler { // ... }
Z(jin)搞清楚事件如何在Pipeline里传播,让我们从Channel的抽象子cAbstractChannel开始,下面是AbstractChannel#write()Ҏ(gu)的实玎ͼ(x)
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel { // ... @Override public Channel write(Object msg) { return pipeline.write(msg); } // ... }
AbstractChannel直接调用?jin)Pipeline的write()Ҏ(gu)Q?/p>
final class DefaultChannelPipeline implements ChannelPipeline { // ... @Override public ChannelFuture write(Object msg) { return tail.write(msg); } // ... }
因ؓ(f)write是个outbound事gQ所以DefaultChannelPipeline直接扑ֈtail部分的contextQ调用其write()Ҏ(gu)Q?/p>
接着看DefaultChannelHandlerContext的write()Ҏ(gu)Q?/span>
final class DefaultChannelHandlerContext extends DefaultAttributeMap implements ChannelHandlerContext { // ... @Override public ChannelFuture write(Object msg) { return write(msg, newPromise()); } @Override public ChannelFuture write(final Object msg, final ChannelPromise promise) { if (msg == null) { throw new NullPointerException("msg"); } validatePromise(promise, true); write(msg, false, promise); return promise; } private void write(Object msg, boolean flush, ChannelPromise promise) { DefaultChannelHandlerContext next = findContextOutbound(); next.invokeWrite(msg, promise); if (flush) { next.invokeFlush(); } } private DefaultChannelHandlerContext findContextOutbound() { DefaultChannelHandlerContext ctx = this; do { ctx = ctx.prev; } while (!ctx.outbound); return ctx; } private void invokeWrite(Object msg, ChannelPromise promise) { try { ((ChannelOutboundHandler) handler).write(this, msg, promise); } catch (Throwable t) { notifyOutboundHandlerException(t, promise); } } // ... }
context的write()Ҏ(gu)沿着context铑־前找Q直xC个outboundcd的context为止Q然后调用其invokeWrite()Ҏ(gu)Q?/p>
invokeWrite()接着调用handler的write()Ҏ(gu)Q?/p>
最后看看ChannelOutboundHandlerAdapter的write()Ҏ(gu)实现Q?/p>
public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelOutboundHandler { // ... @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { ctx.write(msg, promise); } // ... }
默认的实现调用了(jin)context的write()Ҏ(gu)而不做Q何处理,q样write事g沿着outbound铄l传播:(x)
可见QPipeline的事件传播,是靠PipelineQContext和Handler共同协作完成的?/p>