一篇文章,读懂Netty的高性能架构之道

一篇文章,读懂Netty的高性能架构之道

Netty是由JBOSS提供的一个java开源框架,是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,做为一个异步NIO框架,Netty的全部IO操做都是异步非阻塞的,经过Future-Listener机制,用户能够方便的主动获取或者经过通知机制得到IO操做结果。 html

做为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通讯行业等得到了普遍的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。java

为何选择Netty

Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是数一数二的,它已经获得成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty做为底层通讯框架;不少其余业界主流的RPC框架,也使用Netty来构建高性能的异步通讯能力。算法

Netty的特色

  • 高并发编程

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通讯框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能获得了很大提升 。segmentfault

  • 传输快后端

Netty的传输快其实也是依赖了NIO的一个特性——零拷贝api

  • 封装好数组

Netty封装了NIO操做的不少细节,提供易于使用的API浏览器

为何选择Netty

JDK 原生也有一套网络应用程序 API,可是存在一系列问题,主要以下:缓存

1)NIO 的类库和 API 繁杂,使用麻烦:你须要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。

2)须要具有其余的额外技能作铺垫:例如熟悉 Java 多线程编程,由于 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程很是熟悉,才能编写出高质量的 NIO 程序。

3)可靠性能力补齐,开发工做量和难度都很是大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。NIO 编程的特色是功能开发相对容易,可是可靠性能力补齐工做量和难度都很是大。

4)JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会致使 Selector 空轮询,最终致使 CPU 100%。官方声称在 JDK 1.6 版本的 update 18 修复了该问题,可是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生几率下降了一些而已,它并无被根本解决

Netty框架的优点

  • API使用简单,开发门槛低;

  • 功能强大,预置了多种编解码功能,支持多种主流协议;

  • 定制能力强,能够经过ChannelHandler对通讯框架进行灵活地扩展;

  • 性能高,经过与其余业界主流的NIO框架对比,Netty的综合性能最优;

  • 成熟、稳定,Netty修复了已经发现的全部JDK NIO BUG,业务开发人员不须要再为NIO的BUG而烦恼;

  • 社区活跃,版本迭代周期短,发现的BUG能够被及时修复,同时,更多的新功能会加入;

  • 经历了大规模的商业应用考验,质量获得验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业获得成功商用,证实了它已经彻底可以知足不一样行业的商业应用了。

Netty的核心组件

Netty应用中必不可少的组件:

  • Bootstrap or ServerBootstrap

  • EventLoop

  • EventLoopGroup

  • ChannelPipeline

  • Channel

  • Future or ChannelFuture

  • ChannelInitializer

  • ChannelHandler

1.Bootstrap

一个Netty应用一般由一个Bootstrap开始,它主要做用是配置整个Netty程序,串联起各个组件

Handler,为了支持各类协议和处理数据的方式,便诞生了Handler组件。Handler主要用来处理各类事件,这里的事件很普遍,好比能够是链接、数据接收、异常、数据转换等。

2.ChannelInboundHandler

一个最经常使用的Handler。这个Handler的做用就是处理接收到数据时的事件,也就是说,咱们的业务逻辑通常就是写在这个Handler里面的,ChannelInboundHandler就是用来处理咱们的核心业务逻辑。

3.ChannelInitializer

当一个连接创建时,咱们须要知道怎么进行接收或者发送数据,固然,咱们有各类各样的Handler实现来处理它,那么ChannelInitializer即是用来配置这些Handler,它会提供一个ChannelPipeline并把Handler加入到ChannelPipeline

4.ChannelPipeline

一个Netty应用基于ChannelPipeline机制,这种机制须要依赖于EventLoop和EventLoopGroup,由于它们三个都和事件或者事件处理相关。

EventLoops的目的是为Channel处理IO操做,一个EventLoop能够为多个Channel服务。

EventLoopGroup会包含多个EventLoop。

5.Channel

表明了一个Socket连接,或者其它和IO操做相关的组件,它和EventLoop一块儿用来参与IO处理。

6.Future

在Netty中全部的IO操做都是异步的,所以,你不能马上得知消息是否被正确处理,可是咱们能够过一会等它执行完成或者直接注册一个监听,具体的实现就是经过Future和ChannelFutures,他们能够注册一个监听,当操做执行成功或失败时监听会自动触发。

总之,全部的操做都会返回一个ChannelFuture

Netty的应用场景

1.互联网行业

在分布式系统中,各个节点之间须要远程服务调用,高性能的RPC框架必不可少,Netty做为异步高新能的通讯框架,每每做为基础通讯组件被这些RPC框架使用

典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通讯,Dubbo协议默认使用Netty做为基础通讯组件,用于实现各进程节点之间的内部通讯

除了 Dubbo 以外,淘宝的消息中间件 RocketMQ 的消息生产者和消息消费者之间,也采用 Netty 进行高性能、异步通讯。

2.游戏行业

不管是手游服务端仍是大型的网络游戏,Java语言获得了愈来愈普遍的应用。Netty做为高性能的基础通讯组件,它自己提供了TCP/UDP和HTTP协议栈。

很是方便定制和开发私有协议栈,帐号登陆服务器,地图服务器之间能够方便的经过Netty进行高性能的通讯

3.大数据领域

经典的Hadoop的高性能通讯和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通讯,它的Netty Service基于Netty框架二次封装实现。

Netty架构分析

Netty 采用了比较典型的三层网络架构进行设计,逻辑架构图以下所示:

#第一层Reactor 通讯调度层,它由一系列辅助类完成,包括 NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父 类、ByteBuffer 以及由其衍生出来的各类 Buffer、Unsafe 以及其衍生出的各类内部类等。该层的主要职责就是监听网络的读写和链接操做,负责将网络层的数据读取到内存缓冲区中,而后触发各类网络事件,例如链接建立、链接激活、读事 件、写事件等等,将这些事件触发到 PipeLine 中,由 PipeLine 充当的职责链来进行后续的处理。

#第二层职责链 PipeLine,它负责事件在职责链中的有序传播,同时负责动态的编排职责链,职责链能够选择监听和处理本身关心的事件,它能够拦截处理和向后/向前传播事件,不一样的应用的 Handler 节点的功能也不一样,一般状况下,每每会开发编解码 Handler 用于消息的编解码,它能够将外部的协议消息转换成内部的 POJO 对象,这样上层业务侧只须要关心处理业务逻辑便可,不须要感知底层的协议差别和线程模型差别,实现了架构层面的分层隔离。

#第三层业务逻辑处理层。能够分为两类:纯粹的业务逻辑处理,例如订单处理;应用层协议管理,例如 HTTP 协议、FTP 协议等。

I/O模型

传统同步阻塞I/O模式以下图所示:

几种I/O模型的功能和特性对比:

Netty的I/O模型基于非阻塞I/O实现,底层依赖的是JDK NIO框架的Selector。Selector提供选择已经就绪的任务的能力。简单来说,Selector会不断地轮询注册在其上的Channel,若是某个Channel上面有新的TCP链接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,而后经过SelectionKey能够获取就绪Channel的集合,从而进行后续的I/O操做。

一个多路复用器Selector能够同时轮询多个Channel,因为JDK1.5_update10版本(+)使用了epoll()代替传统的select实现,因此它并无最大链接句柄1024/2048的限制。这也就意味着只须要一个线程负责Selector的轮询,就能够接入成千上万的客户端,这确实是个很是巨大的技术进步。使用非阻塞I/O模型以后,Netty解决了传统同步阻塞I/O带来的性能、吞吐量和可靠性问题。

线程调度模型

经常使用的Reactor线程模型有三种,分别以下:

#Reactor单线程模型:Reactor单线程模型,指的是全部的I/O操做都在同一个NIO线程上面完成。对于一些小容量应用场景,可使用单线程模型。

#Reactor多线程模型:Rector多线程模型与单线程模型最大的区别就是由一组NIO线程处理I/O操做。主要用于高并发、大业务量场景。

#主从Reactor多线程模型:主从Reactor线程模型的特色是服务端用于接收客户端链接的再也不是个1个单独的NIO线程,而是一个独立的NIO线程池。利用主从NIO线程模型,能够解决1个服务端监听线程没法有效处理全部客户端链接的性能不足问题。

Netty的线程模型

说完了Reactor的三种模型,那么Netty是哪种呢?其实Netty的线程模型是Reactor模型的变种,那就是去掉线程池的第三种形式的变种,这也是Netty NIO的默认模式。

事实上,Netty的线程模型并不是固定不变,经过在启动辅助类中建立不一样的EventLoopGroup实例并经过适当的参数配置,就能够支持上述三种Reactor线程模型.

在大多数场景下,并行多线程处理能够提高系统的并发性能。可是,若是对于共享资源的并发访问处理不当,会带来严重的锁竞争这最终会致使性能的降低。为了尽量的避免锁竞争带来的性能损耗,能够经过串行化设计,即消息的处理尽量在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。

为了尽量提高性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操做,避免多线程竞争致使的性能降低。表面上看,串行化设计彷佛CPU利用率不高,并发程度不够。可是,经过调整NIO线程池的线程参数,能够同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工做线程模型性能更优。

Reactor模型

Java NIO非堵塞技术实际是采起反应器模式,或者说是观察者(observer)模式为咱们监察I/O端口,若是有内容进来,会自动通知咱们,这样,咱们就没必要开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。

NIO 有一个主要的类Selector,这个相似一个观察者,只要咱们把须要探知的socketchannel告诉Selector,咱们接着作别的事情,当有事件发生时,他会通知咱们,传回一组SelectionKey,咱们读取这些Key,就会得到咱们刚刚注册过的socketchannel,而后,咱们从这个Channel中读取数据,接着咱们能够处理这些数据。

反应器模式与观察者模式在某些方面极为类似:当一个主体发生改变时,全部依属体都获得通知。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联

通常模型

EventLoopGroup:对应于Reactor模式中的定时器的角色,不断地检索是否有事件可用(I/O线程-BOSS),而后交给分离者将事件分发给对应的事件绑定的handler(WORK线程)

经验分享:在客户端编程中常常容易出如今EVENTLOOP上作定时任务的,若是定时任务耗时很长或者存在阻塞,那么可能会将I/O操做挂起(由于要等到定时任务作完才能作别的操做)。解决方法:用独立的EventLoopGroup

序列化方式

影响序列化性能的关键因素总结以下:

- 序列化后的码流大小(网络带宽占用)

- 序列化&反序列化的性能(CPU资源占用)

- 并发调用的性能表现:稳定性、线性增加、偶现的时延毛刺等

对Java序列化和二进制编码分别进行性能测试,编码100万次,测试结果代表:Java序列化的性能只有二进制编码的6.17%左右。

Netty默认提供了对Google Protobuf的支持,经过扩展Netty的编解码接口,用户能够实现其它的高性能序列化框架,例如Thrift的压缩二进制编解码框架。

不一样的应用场景对序列化框架的需求也不一样,对于高性能应用场景Netty默认提供了Google的Protobuf二进制序列化框架,若是用户对其它二进制序列化框架有需求,也能够基于Netty提供的编解码框架扩展实现。

Netty架构剖析之可靠性

Netty面临的可靠性挑战:

\1. 做为RPC框架的基础网络通讯框架,一旦故障将致使没法进行远程服务(接口)调用。

\2. 做为应用层协议的基础通讯框架,一旦故障将致使应用协议栈没法正常工做。

\3. 网络环境复杂(例如推送服务的GSM/3G/WIFI网络),故障不可避免,业务却不能中断。

从应用场景看,Netty是基础的通讯框架,一旦出现Bug,轻则须要重启应用,重则可能致使整个业务中断。它的可靠性会影响整个业务集群的数据通讯和交换,在当今以分布式为主的软件架构体系中,通讯中断就意味着整个业务中断,分布式架构下对通讯的可靠性要求很是高。

从运行环境看,Netty会面临恶劣的网络环境,这就要求它自身的可靠性要足够好,平台可以解决的可靠性问题须要由Netty自身来解决,不然会致使上层用户关注过多的底层故障,这将下降Netty的易用性,同时增长用户的开发和运维成本。

Netty的可靠性是如此重要,它的任何故障均可能会致使业务中断,蒙受巨大的经济损失。所以,Netty在版本的迭代中不断加入新的可靠性特性来知足用户日益增加的高可靠和健壮性需求。

链路有效性检测

Netty提供的心跳检测机制分为三种:

- 读空闲,链路持续时间t没有读取到任何消息

- 写空闲,链路持续时间t没有发送任何消息

- 读写空闲,链路持续时间t没有接收或者发送任何消息

当网络发生单通、链接被防火墙拦截住、长时间GC或者通讯线程发生非预期异常时,会致使链路不可用且不易被及时发现。特别是异常发生在凌晨业务低谷期间,当早晨业务高峰期到来时,因为链路不可用会致使瞬间的大批量业务失败或者超时,这将对系统的可靠性产生重大的威胁。

从技术层面看,要解决链路的可靠性问题,必须周期性的对链路进行有效性检测。目前最流行和通用的作法就是心跳检测。

心跳检测机制分为三个层面:

\1. TCP层面的心跳检测,即TCP的Keep-Alive机制,它的做用域是整个TCP协议栈;

\2. 协议层的心跳检测,主要存在于长链接协议中。例如SMPP协议;

\3. 应用层的心跳检测,它主要由各业务产品经过约定方式定时给对方发送心跳消息实现。

Keep-Alive仅仅是TCP协议层会发送连通性检测包,但并不表明设置了Keep-Alive就是长链接了。

心跳检测的目的就是确认当前链路可用,对方活着而且可以正常接收和发送消息。

作为高可靠的NIO框架,Netty也提供了基于链路空闲的心跳检测机制:

- 读空闲,链路持续时间t没有读取到任何消息

- 写空闲,链路持续时间t没有发送任何消息

- 读写空闲,链路持续时间t没有接收或者发送任何消息(netty自带心跳处理Handler IdleStateHandler

客户端和服务端之间链接断开机制

TCP链接的创建须要三个分节(三次握手),终止则须要四个分节。

对于大量短链接的状况下,常常出现卡在FIN_WAIT2和TIMEWAIT状态的链接,等待系统回收,可是操做系统底层回收的时间频率很长,致使SOCKET被耗尽。

TCP状态图

TCP/IP半关闭

从上述讲的TCP关闭的四个分节能够看出,被动关闭执行方,发送FIN分节的前提是TCP套接字对应应用程序调用close产生的。若是服务端有数据发送给客户端那么可能存在服务端在接受到FIN以后,须要将数据发送到客户端才能发送FIN字节。这种处于业务考虑的情形一般称为半关闭。

半关闭可能致使大量socket处于CLOSE_WAIT状态

谁负责关闭链接合理

链接关闭触发的条件一般分为以下几种:

\1. 数据发送完成(发送到对端而且收到响应),关闭链接

\2. 通讯过程当中产生异常

\3. 特殊指令强制要求关闭链接

对于第一种,一般关闭时机是,数据发送完成方发起(客户端触发居多); 对于第二种,异常产生方触发(例如残包、错误数据等)发起。可是此种状况可能也致使压根没法发送FIN。对于第三种,一般是用于运维等。由命令发起方产生。

流量整形

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。

Netty的流量整形有两个做用:

\1. 防止因为上下游网元性能不均衡致使下游网元被压垮,业务流程中断

\2. 防止因为通讯模块接收消息过快,后端业务线程处理不及时致使的"撑死"问题

流量整形的原理示意图以下:

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。

流量监管TP(Traffic Policing)就是对流量进行控制,经过监督进入网络的流量速率,对超出部分的流量进行“惩罚”,使进入的流量被限制在一个合理的范围以内,从而保护网络资源和用户的利益。

流量整形与流量监管的主要区别在于,流量整形对流量监管中须要丢弃的报文进行缓存——一般是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。流量整形与流量监管的另外一区别是,整形可能会增长延迟,而监管几乎不引入额外的延迟。

#全局流量整形:全局流量整形的做用范围是进程级的,不管你建立了多少个Channel,它的做用域针对全部的Channel。用户能够经过参数设置:报文的接收速率、报文的发送速率、整形周期。[GlobalChannelTrafficShapingHandler]

#链路级流量整形:单链路流量整形与全局流量整形的最大区别就是它以单个链路为做用域,能够对不一样的链路设置不一样的整形策略。[ChannelTrafficShapingHandler针对于每一个channel]

优雅停机

Netty的优雅停机三部曲: 1. 再也不接收新消息 2. 退出前的预处理操做 3. 资源的释放操做

Java的优雅停机一般经过注册JDK的ShutdownHook来实现,当系统接收到退出指令后,首先标记系统处于退出状态,再也不接收新的消息,而后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行。

一般优雅退出须要有超时控制机制,例如30S,若是到达超时时间仍然没有完成退出前的资源回收等操做,则由停机脚本直接调用kill -9 pid,强制退出。

在实际项目中,Netty做为高性能的异步NIO通讯框架,每每用做基础通讯框架负责各类协议的接入、解析和调度等,例如在RPC和分布式服务框架中,每每会使用Netty做为内部私有协议的基础通讯框架。 当应用进程优雅退出时,做为通讯框架的Netty也须要优雅退出,主要缘由以下:

\1. 尽快的释放NIO线程、句柄等资源

\2. 若是使用flush作批量消息发送,须要将积攒在发送队列中的待发送消息发送完成

\3. 正在write或者read的消息,须要继续处理

\4. 设置在NioEventLoop线程调度器中的定时任务须要执行或者清理

Netty架构剖析之安全性

Netty面临的安全挑战:

- 对第三方开放

- 做为应用层协议的基础通讯框架

安全威胁场景分析:

#对第三方开放的通讯框架:若是使用Netty作RPC框架或者私有协议栈,RPC框架面向非授信的第三方开放,例如将内部的一些能力经过服务对外开放出去,此时就须要进行安全认证,若是开放的是公网IP,对于安全性要求很是高的一些服务,例如在线支付、订购等,须要经过SSL/TLS进行通讯。

#应用层协议的安全性:做为高性能、异步事件驱动的NIO框架,Netty很是适合构建上层的应用层协议。因为绝大多数应用层协议都是公有的,这意味着底层的Netty须要向上层提供通讯层的安全传输功能。

SSL/TLS

Netty安全传输特性:

- 支持SSL V2和V3

- 支持TLS

- 支持SSL单向认证、双向认证和第三方CA认证。

SSL单向认证流程图以下:

Netty经过SslHandler提供了对SSL的支持,它支持的SSL协议类型包括:SSL V二、SSL V3和TLS。

#单向认证:单向认证,即客户端只验证服务端的合法性,服务端不验证客户端。

#双向认证:与单向认证不一样的是服务端也须要对客户端进行安全认证。这就意味着客户端的自签名证书也须要导入到服务端的数字证书仓库中。

#CA认证:基于自签名的SSL双向认证,只要客户端或者服务端修改了密钥和证书,就须要从新进行签名和证书交换,这种调试和维护工做量是很是大的。所以,在实际的商用系统中每每会使用第三方CA证书颁发机构进行签名和验证。咱们的浏览器就保存了几个经常使用的CA_ROOT。每次链接到网站时只要这个网站的证书是通过这些CA_ROOT签名过的。就能够经过验证了。

可扩展的安全特性

经过Netty的扩展特性,能够自定义安全策略:

- IP地址黑名单机制

- 接入认证

- 敏感信息加密或者过滤机制

IP地址黑名单是比较经常使用的弱安全保护策略,它的特色就是服务端在与客户端通讯的过程当中,对客户端的IP地址进行校验,若是发现对方IP在黑名单列表中,则拒绝与其通讯,关闭链路。

接入认证策略很是多,一般是较强的安全认证策略,例如基于用户名+密码的认证,认证内容每每采用加密的方式,例如Base64+AES等。

Netty架构剖析之扩展性

经过Netty的扩展特性,能够自定义安全策略:

- 线程模型可扩展

- 序列化方式可扩展

- 上层协议栈可扩展

- 提供大量的网络事件切面,方便用户功能扩展

Netty的架构可扩展性设计理念以下:

\1. 判断扩展点,事先预留相关扩展接口,给用户二次定制和扩展使用

\2. 主要功能点都基于接口编程,方便用户定制和扩展。

粘连包解决方案

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

出现粘包现象的缘由是多方面的,它既可能由发送方形成,也可能由接收方形成。发送方引发的粘包是由TCP协议自己形成的,TCP为提升传输效率,发送方每每要收集到足够多的数据后才发送一包数据。若连续几回发送的数据都不多,一般TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引发的粘包是因为接收方用户进程不及时接收数据,从而致使粘包现象。这是由于接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据还没有被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据以后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

粘包状况有两种

\1. 粘在一块儿的包都是完整的数据包

\2. 粘在一块儿的包有不完整的包

解决粘连包的方法大体分为以下三种:

\1. 发送方开启TCP_NODELAY

\2. 接收方简化或者优化流程尽量快的接收数据

\3. 认为强制分包每次只读一个完整的包

对于以上三种方式,第一种会加剧网络负担,第二种治标不治本,第三种算比较合理的。

第三种又能够分两种方式:

\1. 每次都只读取一个完整的包,若是不足一个完整的包,就等下次再接收,若是缓冲区有N个包要接受,那么须要分N次才能接收完成

\2. 有多少接收多少,将接収的数据缓存在一个临时的缓存中,交由后续的专门解码的线程/进程处理

以上两种分包方式,若是强制关闭程序,数据会存在丢失,第一种数据丢失在接收缓冲区;第二种丢失在程序自身缓存

Netty自带的几种粘连包解决方案:

\1. DelimiterBasedFrameDecoder (带分隔符)

\2. FixedLengthFrameDecoder (定长)

\3. LengthFieldBasedFrameDecoder(将消息分为消息头和消息体,消息头中包含消息总长度的字段)

Netty解包组包

对于TCP编程最常遇到的就是根据具体的协议进行组包或者解包。

根据协议的不一样大体能够分为以下几种类型:

\1. JAVA平台之间经过JAVA序列化进行解包组包(object->byte->object)

\2. 固定长度的包结构(定长每一个包都是M个字节的长度)

\3. 带有明确分隔符协议的解包组包(例如HTTP协议\r\n\r\n)

\4. 可动态扩展的协议(每一个包都添加一个消息头),此种协议一般遵循消息头+消息体的机制,其中消息头的长度是固定的,消息体的长度根据具体业务的不一样长度可能不一样。例如(SMPP协议、CMPP协议)

#序列化协议组包解包

可使用的有:MessagePack、Google ProtobufHessian2

#固定长度解包组包

FixedLengthFrameDecoder 解包,MessageToByteEncoder 组包

#带有分隔符协议的解包组包

DelimiterBasedFrameDecoder 解包,MessageToByteEncoder 组包

#HTTP

io.netty.codec.http

#消息头固定长度,消息体不固定长度协议解包组包

LengthFieldBasedFrameDecoder

须要注意的是:对于解码的Handler必须作到在将ByteBuf解析成Object以后,须要将ByteBuf release()。

Netty Client断网重连机制

对于长链接的程序断网重连几乎是程序的标配。

断网重连具体能够分为两类:

  1. CONNECT失败,须要重连

  2. 程序运行过程当中断网、远程强制关闭链接、收到错误包必须重连

对于第一种解决方案是:实现ChannelFutureListener 用来启动时监测是否链接成功,不成功的话重试

Future-Listener机制

在并发编程中,咱们一般会用到一组非阻塞的模型:Promise,Future,Callback。

其中的Future表示一个可能尚未实际完成的异步任务的结果,针对这个结果添加Callback以便在执行任务成功或者失败后作出响应的操做。而经由Promise交给执行者,任务执行者经过Promise能够标记任务完成或者失败。以上这套模型是不少异步非阻塞框架的基础。具体的理解可参见JDK的FutureTask和Callable。JDK的实现版本,在获取最终结果的时候,不得不作一些阻塞的方法等待最终结果的到来。Netty的Future机制是JDK机制的一个子版本,它支持给Future添加Listener,以方便EventLoop在任务调度完成以后调用。

数据安全性之滑动窗口协议

咱们假设一个场景,客户端每次请求服务端必须获得服务端的一个响应,因为TCP的数据发送和数据接收是异步的,就存在必须存在一个等待响应的过程。该过程根据实现方式不一样能够分为一下几类(部分是错误案例):

\1. 每次发送一个数据包,而后进入休眠(sleep)或者阻塞(await)状态,直到响应回来或者超时,整个调用链结束。此场景是典型的一问一答的场景,效率极其低下

\2. 读写分离,写模块只负责写,模块则负责接收响应,而后作后续的处理。此种场景能尽量的利用带宽进行读写。可是此场景不作控速操做可能致使大量报文丢失或者重复发送

\3. 实现相似于Windowed Protocol。此窗口是以上两种方案的折中版,即容许必定数量的批量发送,又能保证数据的完整性。

 

对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解

 

做者:@xys1228 本文为做者原创,转载请注明出处:http://www.javashuo.com/article/p-pededtnv-bu.html Email:yongshun1228@gmail.com


目录

经过 CompositeByteBuf 实现零拷贝

经过 wrap 操做实现零拷贝

经过 slice 操做实现零拷贝

经过 FileRegion 实现零拷贝


此文章已同步发布在个人 segmentfault 专栏.

根据 Wiki 对 Zero-copy 的定义:

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.

即所谓的 Zero-copy, 就是在操做数据时, 不须要将数据 buffer 从一个内存区域拷贝到另外一个内存区域. 由于少了一次内存的拷贝, 所以 CPU 的效率就获得的提高.

在 OS 层面上的 Zero-copy 一般指避免在 用户态(User-space)内核态(Kernel-space) 之间来回拷贝数据. 例如 Linux 提供的 mmap 系统调用, 它能够将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改能够直接反映到内核空间; 一样地, 内核空间对这段区域的修改也直接反映用户空间. 正由于有这样的映射关系, 咱们就不须要在 用户态(User-space)内核态(Kernel-space) 之间拷贝数据, 提升了数据传输的效率.

而须要注意的是, Netty 中的 Zero-copy 与上面咱们所提到到 OS 层面上的 Zero-copy 不太同样, Netty的 Zero-coyp 彻底是在用户态(Java 层面)的, 它的 Zero-copy 的更多的是偏向于 优化数据操做 这样的概念.

Netty 的 Zero-copy 体如今以下几个个方面:

  • Netty 提供了 CompositeByteBuf, 它能够将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝.

  • 经过 wrap 操做, 咱们能够将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操做.

  • ByteBuf 支持 slice 操做, 所以能够将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝.

  • 经过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 能够直接将文件缓冲区的数据发送到目标 Channel, 避免了传统经过循环 write 方式致使的内存拷贝问题.

下面咱们就来简单了解一下这几种常见的零拷贝操做.

 

经过 CompositeByteBuf 实现零拷贝

假设咱们有一份协议数据, 它由头部和消息体组成, 而头部和消息体是分别存放在两个 ByteBuf 中的, 即:

`ByteBuf header = ...``ByteBuf body = ...`

咱们在代码处理中, 一般但愿将 header 和 body 合并为一个 ByteBuf, 方便处理, 那么一般的作法是:

ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());



allBuf.writeBytes(header);



allBuf.writeBytes(body);

能够看到, 咱们将 header 和 body 都拷贝到了新的 allBuf 中了, 这无形中增长了两次额外的数据拷贝操做了.

那么有没有更加高效优雅的方式实现相同的目的呢? 咱们来看一下 CompositeByteBuf 是如何实现这样的需求的吧.

`ByteBuf header = ...`
`ByteBuf body = ...`
`CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();`
`compositeByteBuf.addComponents(``true``, header, body);`

上面代码中, 咱们定义了一个 CompositeByteBuf 对象, 而后调用

`public` `CompositeByteBuf addComponents(``boolean` `increaseWriterIndex, ByteBuf... buffers) {``...``}`

方法将 headerbody 合并为一个逻辑上的 ByteBuf, 即:

不过须要注意的是, 虽然看起来 CompositeByteBuf 是由两个 ByteBuf 组合而成的, 不过在 CompositeByteBuf 内部, 这两个 ByteBuf 都是单独存在的, CompositeByteBuf 只是逻辑上是一个总体.

上面 CompositeByteBuf 代码还以一个地方值得注意的是, 咱们调用 addComponents(boolean increaseWriterIndex, ByteBuf... buffers) 来添加两个 ByteBuf, 其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex. 若是咱们调用的是

`compositeByteBuf.addComponents(header, body);`

那么其实 compositeByteBufwriteIndex 仍然是0, 所以此时咱们就不可能从 compositeByteBuf 中读取到数据, 这一点但愿你们要特别注意.

除了上面直接使用 CompositeByteBuf 类外, 咱们还可使用 Unpooled.wrappedBuffer 方法, 它底层封装了 CompositeByteBuf 操做, 所以使用起来更加方便:

`ByteBuf header = ...``ByteBuf body = ...` 
`ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);`

 

经过 wrap 操做实现零拷贝

例如咱们有一个 byte 数组, 咱们但愿将它转换为一个 ByteBuf 对象, 以便于后续的操做, 那么传统的作法是将此 byte 数组拷贝到 ByteBuf 中, 即:

    `byte``[] bytes = ...
   ``ByteBuf byteBuf = Unpooled.buffer();`
   `byteBuf.writeBytes(bytes);`

显然这样的方式也是有一个额外的拷贝操做的, 咱们可使用 Unpooled 的相关方法, 包装这个 byte 数组, 生成一个新的 ByteBuf 实例, 而不须要进行拷贝操做. 上面的代码能够改成:

`byte``[] bytes = ...`
`ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);`

能够看到, 咱们经过 Unpooled.wrappedBuffer 方法来将 bytes 包装成为一个 UnpooledHeapByteBuf 对象, 而在包装的过程当中, 是不会有拷贝操做的. 即最后咱们生成的生成的 ByteBuf 对象是和 bytes 数组共用了同一个存储空间, 对 bytes 的修改也会反映到 ByteBuf 对象中.

Unpooled 工具类还提供了不少重载的 wrappedBuffer 方法:

`public` `static` `ByteBuf wrappedBuffer(``byte``[] array)`
`public` `static` `ByteBuf wrappedBuffer(``byte``[] array, ``int` `offset, ``int` `length)` `public` `static` `ByteBuf wrappedBuffer(ByteBuffer buffer)`
`public` `static` `ByteBuf wrappedBuffer(ByteBuf buffer)` 
`public` `static` `ByteBuf wrappedBuffer(``byte``[]... arrays)`
`public` `static` `ByteBuf wrappedBuffer(ByteBuf... buffers)`
`public` `static` `ByteBuf wrappedBuffer(ByteBuffer... buffers)`
`public` `static` `ByteBuf wrappedBuffer(``int` `maxNumComponents, ``byte``[]... arrays)`
`public` `static` `ByteBuf wrappedBuffer(``int` `maxNumComponents, ByteBuf... buffers)`
`public` `static` `ByteBuf wrappedBuffer(``int` `maxNumComponents, ByteBuffer... buffers)`

这些方法能够将一个或多个 buffer 包装为一个 ByteBuf 对象, 从而避免了拷贝操做.

 

经过 slice 操做实现零拷贝

slice 操做和 wrap 操做恰好相反, Unpooled.wrappedBuffer 能够将多个 ByteBuf 合并为一个, 而 slice 操做能够将一个 ByteBuf 切片多个共享一个存储区域的 ByteBuf 对象. ByteBuf 提供了两个 slice 操做方法:

`public` `ByteBuf slice();`
`public` `ByteBuf slice(``int` `index, ``int` `length);`

不带参数的 slice 方法等同于 buf.slice(buf.readerIndex(), buf.readableBytes()) 调用, 即返回 buf 中可读部分的切片.slice(int index, int length) 方法相对就比较灵活了, 咱们能够设置不一样的参数来获取到 buf 的不一样区域的切片.

下面的例子展现了 ByteBuf.slice 方法的简单用法:

`ByteBuf byteBuf = ...`
`ByteBuf header = byteBuf.slice(``0``, ``5``);`
`ByteBuf body = byteBuf.slice(``5``, ``10``);`

slice 方法产生 header 和 body 的过程是没有拷贝操做的, header 和 body 对象在内部实际上是共享了 byteBuf 存储空间的不一样部分而已. 即:

 

经过 FileRegion 实现零拷贝

Netty 中使用 FileRegion 实现文件传输的零拷贝, 不过在底层 FileRegion 是依赖于 Java NIO FileChannel.transfer 的零拷贝功能.

首先咱们从最基础的 Java IO 开始吧. 假设咱们但愿实现一个文件拷贝的功能, 那么使用传统的方式, 咱们有以下实现:

`public` `static` `void` `copyFile(String srcFile, String destFile) ``throws` `Exception {``    ``byte``[] temp = ``new` `byte``[``1024``];``    
    ``FileInputStream in = ``new` `FileInputStream(srcFile);``  
    ``FileOutputStream out = ``new` `FileOutputStream(destFile);``   
    ``int` `length;`` 
        ``while` `((length = in.read(temp)) != -``1``)
    {``       
        ``out.write(temp, ``0``, length);``   
        ``}` `   
        ``in.close();``   
        ``out.close();`
        `}`

上面是一个典型的读写二进制文件的代码实现了. 不用我说, 你们确定都知道, 上面的代码中不断中源文件中读取定长数据到 temp 数组中, 而后再将 temp 中的内容写入目的文件, 这样的拷贝操做对于小文件却是没有太大的影响, 可是若是咱们须要拷贝大文件时, 频繁的内存拷贝操做就消耗大量的系统资源了. 下面咱们来看一下使用 Java NIO 的 FileChannel 是如何实现零拷贝的:

`public` `static` `void` `copyFileWithFileChannel(String srcFileName, String destFileName) ``throws` `Exception {``   
    ``RandomAccessFile srcFile = ``new` `RandomAccessFile(srcFileName, ``"r"``);``
    ``FileChannel srcFileChannel = srcFile.getChannel();` `   
    ``RandomAccessFile destFile = ``new` `RandomAccessFile(destFileName, ``"rw"``);``    ``FileChannel destFileChannel = destFile.getChannel();` `    ``long` `position = ``0``;``    ``long` `count = srcFileChannel.size();` `    ``srcFileChannel.transferTo(position, count, destFileChannel);``}`

能够看到, 使用了 FileChannel 后, 咱们就能够直接将源文件的内容直接拷贝(transferTo) 到目的文件中, 而不须要额外借助一个临时 buffer, 避免了没必要要的内存操做.

有了上面的一些理论知识, 咱们来看一下在 Netty 中是怎么使用 FileRegion 来实现零拷贝传输一个文件的:

`@Override`
`public` `void` `channelRead0(ChannelHandlerContext ctx, String msg)``throws` `Exception {``    ``RandomAccessFile raf = ``null``;``    
``long` `length = -``1``;``   
``try` `{``        
   ``// 1. 经过 RandomAccessFile 打开一个文件.``    
    ``raf = ``new` `RandomAccessFile(msg, ``"r"``);``      
    ``length = raf.length();``    ``}
                                                                                                     ``catch` `(Exception e) {``       
                                                                                                         ``ctx.writeAndFlush(``"ERR: "` `+ e.getClass().getSimpleName() + ``": "` `+ e.getMessage() + ``'\n'``);``        
                                                                                                         ``return``;``  
                                                                                                             ``} ``finally` 
                                                                                                         `{``        ``if` `(length < ``0` `&& raf != ``null``) {``    
                                                                                                             ``raf.close();``       
                                                                                                             ``}``    ``}` `  
                                                                                                         ``ctx.write(``"OK: "` `+ raf.length() + ``'\n'``);``  
                                                                                                         ``if` `(ctx.pipeline().get(SslHandler.``class``) == ``null``) {``       
                                                                                                         ``// SSL not enabled - can use zero-copy file transfer.``       
                                                                                                         ``// 2. 调用 raf.getChannel() 获取一个 FileChannel.``        
                                                                                                         ``// 3. 将 FileChannel 封装成一个 DefaultFileRegion``       
                                                                                                         ``ctx.write(``new` `DefaultFileRegion(raf.getChannel(), ``0``, length));``    
                                                                                                         ``} ``else` `{``       
                                                                                                         ``// SSL enabled - cannot use zero-copy file transfer.``     
                                                                                                         ``ctx.write(``new` `ChunkedFile(raf));``   
                                                                                                         ``}``  
                                                                                                         ``ctx.writeAndFlush(``"\n"``);``}`

上面的代码是 Netty 的一个例子, 其源码在 netty/example/src/main/java/io/netty/example/file/FileServerHandler.java 能够看到, 第一步是经过 RandomAccessFile 打开一个文件, 而后 Netty 使用了 DefaultFileRegion 来封装一个 FileChannel 即:

`new` `DefaultFileRegion(raf.getChannel(), ``0``, length)`

当有了 FileRegion 后, 咱们就能够直接经过它将文件的内容直接写入 Channel 中, 而不须要像传统的作法: 拷贝文件内容到临时 buffer, 而后再将 buffer 写入 Channel. 经过这样的零拷贝操做, 无疑对传输大文件颇有帮助.

精彩问答

问:据我以前了解到,Java的NIO selector底层在Windows下的实现是起两个随机端口互联来监测链接或读写事件,在Linux上是利用管道实现的;我有遇到过这样的需求,须要占用不少个固定端口作服务端,若是在Windows下,利用NIO框架(Mina或Netty)就有可能会形成端口冲突,这种状况有什么好的解决方案吗?

你说的问题确实存在,Linux使用Pipe实现网络监听,Windows要启动端口。目前没有更好的办法,建议的方式是做为服务端的端口能够规划一个范围,而后根据节点和进程信息动态生成,若是发现端口冲突,能够在规划范围内基于算法从新生成一个新的端口。

问:请我,我如今将Spring与Netty作了整合,使用Spring的Service开启 Netty主线程,可是中止整个运行容器的时候,Netty的TCP Server端口不能释放?退出处理时,有什么好的办法释放Netty Server端口么?

实际上,由谁拉起Netty 主线程并不重要。咱们须要作的就是当应用容器退出的时候(Spring Context销毁),在退出以前调用Netty 的优雅退出接口便可实现端口、NIO线程资源的释放。请参考这篇文章:http://www.infoq.com/cn/articles/netty-elegant-exit-mechanism-and-principles

问:适合用Netty写Web通讯么?

Netty不是Web框架,没法解析JSP、HTML、JS等,可是它能够作Web 通讯,例如可使用Netty重写Tomcat的HTTP/HTTPS 通讯协议栈。

问:能不能讲解一下Netty的串行无锁化设计,如何在串行和并行中达到最优?

为了尽量提高性能,Netty采用了串行无锁化设计,在IO线程内部进行串行操做,避免多线程竞争致使的性能降低。表面上看,串行化设计彷佛CPU利用率不高,并发程度不够。可是,经过调整NIO线程池的线程参数,能够同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工做线程模型性能更优。Netty的NioEventLoop读取到消息以后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操做致使的锁的竞争,从性能角度看是最优的。

相关文章
相关标签/搜索