Netty教程

摘自Netty官网:

“Netty是一个NIO C/S框架,能够快速、简单的开发协议服务器和客户端等网络应用。它能够很大程度上简单化、流水线化开发网络应用,例如TCP/UDP socket服务器。

Netty是一个主要用于编写高并发网络系统、网络应用和服务的Java库。Netty和 标准的Java APIs一个主要的差别在于它的异步API。这个名词对于不同的人来说意味着不同的东西,可能和非阻塞、事件驱动有相同的意义。先不管这些,如果你之前从未使用过异步API,习惯于编写顺序执行的程序,那么转换思路编写Netty程序有点麻烦。在这里简要的介绍我如何解决这个问题。

编写一个Netty示例并启动。和其他Java API一样,发起request很容易。在处理response需要转换思维,因为没有response。几乎每个方法调用都是异步的,这意味着没有返回值,而且调用常常是立刻返回的。结果(如果有的话)是 由 其他线程传回的。这是普通API和异步API的基本区别。假如一个客户端API提 供一个方法来从服务器获取widget的 数量。

标准API

1
publicintgetWidgetCount();

当一个线程调用getWidgetCount(),一段时间过后一个int值会被会返回。

异步API

1
2
3
4
5
publicWidgetCountListener myListener = newWidgetCountListener() {
     publicvoidonWidgetCount(intwidgetCount) {
          ...... doyour thing with the widget count
     }
};

在我举例的getWidgeCount API异步版本中,方法调用没有返回数据,而且即刻执行完毕。然而,它接受了一个response处理器参数,当获取到widget数 据会回调这个监听器,然后监听器可以利用这个结果执行任意有用的操作。

这个复杂的附加层看起来是不必要的,但这却是利用Netty等API实现的高性能的应用、服务的一个关键特征。客户端不需要为线程被阻塞在等待服务器响应而浪费资源。当数据可用时会通知他们。对于一个线程发起一个调用的情况,这种方式可能有些过度设计,但是设想有上百个线程百万次的执行这个方法。而且,NIO最重要的好处是Selectors能够用来将我们感兴趣的事件通知委托给底层的操作系统,甚至是硬件来完成。当16bytes已经成功从网络写入到一个服务器或者14bytes经过网络从一个服务器读到本地等操作会被回调通知,很明显是一个底层而且细节的实现方式。但是通过一系列的抽象,开发人员使用Java NIO和Netty API能 够从更宏观、抽象的级别上来处理这些问题。

在本篇介绍中,我会从一些基本的概念讲起,然后列举部分核心的构建代码块,最后介绍一些代码示例。

这些是如何工作的呢?

Netty的基本数据结构是ChannelBuffer。Netty的JavaDocs定义:

一个可以被随机或顺序访问任意个数字节的序列。这个接口为原生byte数组(byte[])和Java NIO buffers提供了一个抽象的概念。

Netty中数据如何被分发也是由ChannelBuffer完成的。如果你的程序只是处理byte数组和byte缓冲区,那么你会很幸运因为使用ChannelBuffer。 然而,如果你需要处理更高级的数据,例如java对象,把这个数据发送到一个远程服务器,然后再接受另外一个返回的对象,那么这些byte计划需要被转义。或者,如果你需要发起一个请求到一个HTTP服务器,你的请求或许是从发起一个字符串类型URL来开始它的生命周期,但是它需要被包装成一个HTTP服 务器能够理解的request,然后分解为原始的byte,最后通过网络发送。HTTP服务器需要接收这些byte,并且把它们解析一个符合格式的HTTP request。一旦请求成功,这个流程会被反过来执行:服务器的响应(或许是一个JPEG图片或者JavaScript文件)必须被包装成一个HTTP response,转换为bytes,然后返回给发起请求的客户端。

在Netty中,byte在网络流通的基本抽象是Channel。 再看一下Netty的API说明:

网络socket或者网络组件的连接器(Nexus)可以实现I/O操作,例如:读、写、连接和绑定。

用Nexus来描述Channel是 很恰当的(来自dictionary.com对nexus的解释):

  1. a means of connection; tie; link
  2. a connected series or group
  3. the core or center, as of a matter or situation

Channel被提供给用户来与Netty交互的API。更符合实际情况的说法,Channel是socket的抽象描述。但是Channel不一定是一个socket,也可以是一个文件、或者其他抽象的东西。因此,Nexus很适合描述Channel。总而言之,Channel提供接口来连接目标,并写入数据。你或许会疑问为什么没有读操作?确实没有。要知道,这和上面提到的异步getWidgetCount方法一样,没有返回值。

所有基于Channel的方法可以分为2种:

  1. 同步的属性获取方法:用于提供Channel本身的信息。
  2. I/O操 作,例如:绑定、断开连接、写入。

类型2中的方法是异步的,他们都返回一个ChannelFuture的延迟对象。ChannelFuture是保存未知结果的容器,但是结果可用时肯定会被返回的。ChannelFuture有点像WidgetCountListener,不同的地方在于你需要把ChannelFuture注册到你的监听器中,而不是把监听器注册到你的代码流程中。(更简单的API,对吧?)你可以实现ChannelFutureListener接口来完成一个监听器,这个接口有一个方法会在操作完成的时候被调用:public void operationComplete(ChannelFuture future)。在操作完成后这个方法被调用,但并不意味这操作成功,所以这个延迟对象可以用来查询操作的最终结果-成功或失败。

Netty主要用于NIO,但Netty的Channel实现类支持老版本的、同步的IO(OIO)。OIO有一些优点,而且Netty对它的实现方式和NIO一样,这些实现模块能够被替换和重用。


Channel不是直接被创建的,而是通过ChannelFactory来创建。ChannelFactory有2种类别,一种用来实现客户端Channel,另一种用于服务器端的Channel。对于这2种分类,都有不同的实现类来处理相应的I/O通讯协议:

UDP的ChannelFactory在客户端和服务器端的实现相同的,因为UDP是无连接协议。还有其他2种类型:

在本篇博客中主要涉及TCP NIO Channel, 但是要注意创建不同ChannelFactory的方式上有细微的差别。

TCP NIO ChannelFactory的构造方法使用相同类型的参数,还有一些重载方法。基本上,这个Factory需 要2个 线程池/Executors

  • Boss线程:由这个线程池提供的线程是boss种类的,用于创建、连接、绑定socket,然后把这些socket传 给worker线程池。在服务器端每个监听的socket都 有一个boss线 程来处理。在客户端,只有一个boss线程来处理所有的socket。
  • Worker线程:Worker线 程执行所有的异步I/O。 他们不是通用的线程,开发人员需要注意不要把与其不同的任务赋给线程,这可能导致线程被阻塞、无法处理他们真正关心的任务,反过来会导致死锁和一些莫名其妙的性能问题。

在客户端只有一个boss线程,为什么NioClientSocketChannelFactory还需要一个Executors

  1. boss线程能够被延迟加载,而且没有任务需要处理的时候可以被释放,但是在线程池中保留少量的线程比在需要的时候创建一个新的线程、然后在空闲的时候销毁它更有效率。
  2. 还有可能多个不同的ChannelFactory被创建,应该让这些ChannelFactory共用一个线程池,而不是每个工厂独享一个线程池。

因为NIO ChannelFactory是唯一可以异步的处理socket连接、服务器端socket的绑定,所以是唯一使用boss线 程池的ChannelFactory。 其他的种类有的使用虚拟连接(Local),有的是同步的连接(OIO) 或者无状态连接(UDP)。HttpTunelingClientSocketChannelFactory是客户端socket ChannelFactory的简单封装,是否使用boss线程是可选的,而且也没给它配置boss线程。

关于ChannelFactory需要注意:在Netty中处理逻辑的过程中,ChannelFactory需要申请资源,包括线程池。如果使用ChannelFactory之 后,一定要调用它的releaseExternalResources()方 法来保证它申请的所有资源被释放。

总之,发送东西到一个监听状态的服务器:

  1. 创建一个Channel。
  2. 将Channel连接到远程监听的socket。
  3. 调用Channel的write(Object message)方法。

传对象到Channel很灵活?不是如此,如果按照下面的方式做会出现什么?

1
channel.write(newDate());

Netty会抛出这个异常:

1
java.lang.IllegalArgumentException: unsupported messagetype: class java.util.Date

那支持什么样的对象呢?ChannelBuffer。但是Channel有一个叫做Pipeline的构造器(准确的说是ChannelPipeline)。一个Pipeline是一组拦截器组成的,这些拦截器能够处理和转换传给他们的值。当一个拦截器处理完成后,处理后的值会被传给下一个拦截器。这些拦截器被称作ChannelHandler。Pipeline会严格保证ChannelHandler实例的顺序。通常,第一个ChannelHandler会 接收一个原始的ChannelBuffer,而最后一个ChannelHandler(被称作Sink)会任何转给它的东西输出。在Pipeline的某个流程中,你可以实现一个ChannelHandler来做些有用的操作。ChannelHandler只是一个标识性接口,没有任何方法,因此处理器的实现很灵活,但是任何一个处理器都需要相应或者转发ChannelEvent(这里有很多专业的术语)。

这个ChannelEvent又是什么鬼东西呢?在这两段落中,把ChannelEvent当做一个含有ChannelBuffer的包裹。至于ChannelBuffer已经介绍过,它是Channel的基本数据单位。在下一篇中会有ChannelEvent的详细介绍。

1、总体结构

先放上一张漂亮的Netty总体结构图,下面的内容也主要围绕该图上的一些核心功能做分析,但对如Container Integration及Security Support等高级可选功能,本文不予分析。

2、网络模型

Netty是典型的Reactor模型结构,关于Reactor的详尽阐释,可参考POSA2,这里不做概念性的解释。而应用Java NIO构建Reactor模式,Doug Lea(就是那位让人无限景仰的大爷)在“Scalable IO in Java”中给了很好的阐述。这里截取其PPT中经典的图例说明 Reactor模式的典型实现:

1、这是最简单的单Reactor单线程模型。Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分派请求到处理器链中。该模型 适用于处理器链中业务处理组件能快速完成的场景。不过,这种单线程模型不能充分利用多核资源,所以实际使用的不多。

2、相比上一种模型,该模型在处理器链部分采用了多线程(线程池),也是后端程序常用的模型。

3、 第三种模型比起第二种模型,是将Reactor分成两部分,mainReactor负责监听server socket,accept新连接,并将建立的socket分派给subReactor。subReactor负责多路分离已连接的socket,读写网 络数据,对业务处理功能,其扔给worker线程池完成。通常,subReactor个数上可与CPU个数等同。

说完Reacotr模型的三种形式,那么Netty是哪种呢?其实,我还有一种Reactor模型的变种没说,那就是去掉线程池的第三种形式的变种,这也 是Netty NIO的默认模式。在实现上,Netty中的Boss类充当mainReactor,NioWorker类充当subReactor(默认 NioWorker的个数是Runtime.getRuntime().availableProcessors())。在处理新来的请求 时,NioWorker读完已收到的数据到ChannelBuffer中,之后触发ChannelPipeline中的ChannelHandler流。

Netty是事件驱动的,可以通过ChannelHandler链来控制执行流向。因为ChannelHandler链的执行过程是在 subReactor中同步的,所以如果业务处理handler耗时长,将严重影响可支持的并发数。这种模型适合于像Memcache这样的应用场景,但 对需要操作数据库或者和其他模块阻塞交互的系统就不是很合适。Netty的可扩展性非常好,而像ChannelHandler线程池化的需要,可以通过在 ChannelPipeline中添加Netty内置的ChannelHandler实现类–ExecutionHandler实现,对使用者来说只是 添加一行代码而已。对于ExecutionHandler需要的线程池模型,Netty提供了两种可 选:1) MemoryAwareThreadPoolExecutor 可控制Executor中待处理任务的上限(超过上限时,后续进来的任务将被阻 塞),并可控制单个Channel待处理任务的上限;2) OrderedMemoryAwareThreadPoolExecutor 是  MemoryAwareThreadPoolExecutor 的子类,它还可以保证同一Channel中处理的事件流的顺序性,这主要是控制事件在异步处 理模式下可能出现的错误的事件顺序,但它并不保证同一Channel中的事件都在一个线程中执行(通常也没必要)。一般来 说,OrderedMemoryAwareThreadPoolExecutor 是个很不错的选择,当然,如果有需要,也可以DIY一个。

3、 buffer

org.jboss.netty.buffer包的接口及类的结构图如下:

该包核心的接口是ChannelBuffer和ChannelBufferFactory,下面予以简要的介绍。

Netty使用ChannelBuffer来存储并操作读写的网络数据。ChannelBuffer除了提供和ByteBuffer类似的方法,还提供了 一些实用方法,具体可参考其API文档。ChannelBuffer的实现类有多个,这里列举其中主要的几个:

1)HeapChannelBuffer:这是Netty读网络数据时默认使用的ChannelBuffer,这里的Heap就是Java堆的意思,因为 读SocketChannel的数据是要经过ByteBuffer的,而ByteBuffer实际操作的就是个byte数组,所以 ChannelBuffer的内部就包含了一个byte数组,使得ByteBuffer和ChannelBuffer之间的转换是零拷贝方式。根据网络字 节续的不同,HeapChannelBuffer又分为BigEndianHeapChannelBuffer和 LittleEndianHeapChannelBuffer,默认使用的是BigEndianHeapChannelBuffer。Netty在读网络 数据时使用的就是HeapChannelBuffer,HeapChannelBuffer是个大小固定的buffer,为了不至于分配的Buffer的 大小不太合适,Netty在分配Buffer时会参考上次请求需要的大小。

2)DynamicChannelBuffer:相比于HeapChannelBuffer,DynamicChannelBuffer可动态自适应大 小。对于在DecodeHandler中的写数据操作,在数据大小未知的情况下,通常使用DynamicChannelBuffer。

3)ByteBufferBackedChannelBuffer:这是directBuffer,直接封装了ByteBuffer的 directBuffer。

对于读写网络数据的buffer,分配策略有两种:1)通常出于简单考虑,直接分配固定大小的buffer,缺点是,对一些应用来说这个大小限制有时是不 合理的,并且如果buffer的上限很大也会有内存上的浪费。2)针对固定大小的buffer缺点,就引入动态buffer,动态buffer之于固定 buffer相当于List之于Array。

buffer的寄存策略常见的也有两种(其实是我知道的就限于此):1)在多线程(线程池) 模型下,每个线程维护自己的读写buffer,每次处理新的请求前清空buffer(或者在处理结束后清空),该请求的读写操作都需要在该线程中完成。 2)buffer和socket绑定而与线程无关。两种方法的目的都是为了重用buffer。

Netty对buffer的处理策略是:读 请求数据时,Netty首先读数据到新创建的固定大小的HeapChannelBuffer中,当HeapChannelBuffer满或者没有数据可读 时,调用handler来处理数据,这通常首先触发的是用户自定义的DecodeHandler,因为handler对象是和ChannelSocket 绑定的,所以在DecodeHandler里可以设置ChannelBuffer成员,当解析数据包发现数据不完整时就终止此次处理流程,等下次读事件触 发时接着上次的数据继续解析。就这个过程来说,和ChannelSocket绑定的DecodeHandler中的Buffer通常是动态的可重用 Buffer(DynamicChannelBuffer),而在NioWorker中读ChannelSocket中的数据的buffer是临时分配的 固定大小的HeapChannelBuffer,这个转换过程是有个字节拷贝行为的。

对ChannelBuffer的创建,Netty内部使用的是ChannelBufferFactory接口,具体的实现有 DirectChannelBufferFactory和HeapChannelBufferFactory。对于开发者创建 ChannelBuffer,可使用实用类ChannelBuffers中的工厂方法。

4、Channel

和Channel相关的接口及类结构图如下:

从该结构图也可以看到,Channel主要提供的功能如下:

1)当前Channel的状态信息,比如是打开还是关闭等。
2)通过ChannelConfig可以得到的Channel配置信息。
3)Channel所支持的如read、write、bind、connect等IO操作。
4)得到处理该Channel的ChannelPipeline,既而可以调用其做和请求相关的IO操作。

在Channel实现方面,以通常使用的nio socket来说,Netty中的NioServerSocketChannel和NioSocketChannel分别封装了java.nio中包含的 ServerSocketChannel和SocketChannel的功能。

5、ChannelEvent

如前所述,Netty是事件驱动的,其通过ChannelEvent来确定事件流的方向。一个ChannelEvent是依附于Channel的 ChannelPipeline来处理,并由ChannelPipeline调用ChannelHandler来做具体的处理。下面是和 ChannelEvent相关的接口及类图:

对于使用者来说,在ChannelHandler实现类中会使用继承于ChannelEvent的MessageEvent,调用其 getMessage()方法来获得读到的ChannelBuffer或被转化的对象。

6、ChannelPipeline

Netty 在事件处理上,是通过ChannelPipeline来控制事件流,通过调用注册其上的一系列ChannelHandler来处理事件,这也是典型的拦截 器模式。下面是和ChannelPipeline相关的接口及类图:

事件流有两种,upstream事件和downstream事件。在ChannelPipeline中,其可被注册的ChannelHandler既可以 是 ChannelUpstreamHandler 也可以是ChannelDownstreamHandler ,但事件在ChannelPipeline传递过程中只会调用匹配流的ChannelHandler。在事件流的过滤器链 中,ChannelUpstreamHandler或ChannelDownstreamHandler既可以终止流程,也可以通过调用 ChannelHandlerContext.sendUpstream(ChannelEvent)或 ChannelHandlerContext.sendDownstream(ChannelEvent)将事件传递下去。下面是事件流处理的图示:

从上图可见,upstream event是被Upstream Handler们自底向上逐个处理,downstream event是被Downstream Handler们自顶向下逐个处理,这里的上下关系就是向ChannelPipeline里添加Handler的先后顺序关系。简单的理 解,upstream event是处理来自外部的请求的过程,而downstream event是处理向外发送请求的过程。

服务端处 理请求的过程通常就是解码请求、业务逻辑处理、编码响应,构建的ChannelPipeline也就类似下面的代码片断:

1
2
3
4
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder",newMyProtocolDecoder());
pipeline.addLast("encoder",newMyProtocolEncoder());
pipeline.addLast("handler",newMyBusinessLogicHandler());

其中,MyProtocolDecoder是ChannelUpstreamHandler类型,MyProtocolEncoder是 ChannelDownstreamHandler类型,MyBusinessLogicHandler既可以是 ChannelUpstreamHandler类型,也可兼ChannelDownstreamHandler类型,视其是服务端程序还是客户端程序以及 应用需要而定。

补充一点,Netty对抽象和实现做了很好的解耦。像org.jboss.netty.channel.socket包, 定义了一些和socket处理相关的接口,而org.jboss.netty.channel.socket.nio、 org.jboss.netty.channel.socket.oio等包,则是和协议相关的实现。

7、codec framework

对于请求协议的编码解码,当然是可以按照协议格式自己操作ChannelBuffer中的字节数据。另一方面,Netty也做了几个很实用的codec helper,这里给出简单的介绍。

1)FrameDecoder:FrameDecoder内部维护了一个 DynamicChannelBuffer成员来存储接收到的数据,它就像个抽象模板,把整个解码过程模板写好了,其子类只需实现decode函数即可。 FrameDecoder的直接实现类有两个:(1)DelimiterBasedFrameDecoder是基于分割符 (比如\r\n)的解码器,可在构造函数中指定分割符。(2)LengthFieldBasedFrameDecoder是基于长度字段的解码器。如果协 议 格式类似“内容长度”+内容、“固定头”+“内容长度”+动态内容这样的格式,就可以使用该解码器,其使用方法在API DOC上详尽的解释。
2)ReplayingDecoder: 它是FrameDecoder的一个变种子类,它相对于FrameDecoder是非阻塞解码。也就是说,使用 FrameDecoder时需要考虑到读到的数据有可能是不完整的,而使用ReplayingDecoder就可以假定读到了全部的数据。
3)ObjectEncoder 和ObjectDecoder:编码解码序列化的Java对象。
4)HttpRequestEncoder和 HttpRequestDecoder:http协议处理。

下面来看使用FrameDecoder和ReplayingDecoder的两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicclassIntegerHeaderFrameDecoderextendsFrameDecoder {
    protectedObject decode(ChannelHandlerContext ctx, Channel channel,
                ChannelBuffer buf)throwsException {
        if(buf.readableBytes() <4) {
            returnnull;
        }
        buf.markReaderIndex();
        intlength = buf.readInt();
        if(buf.readableBytes() < length) {
            buf.resetReaderIndex();
            returnnull;
        }
        returnbuf.readBytes(length);
    }
}

而使用ReplayingDecoder的解码片断类似下面的,相对来说会简化很多。

1
2
3
4
5
6
publicclassIntegerHeaderFrameDecoder2extendsReplayingDecoder {
    protectedObject decode(ChannelHandlerContext ctx, Channel channel,
            ChannelBuffer buf, VoidEnum state)throwsException {
        returnbuf.readBytes(buf.readInt());
    }
}

就实现来说,当在ReplayingDecoder子类的decode函数中调用ChannelBuffer读数据时,如果读失败,那么 ReplayingDecoder就会catch住其抛出的Error,然后ReplayingDecoder接手控制权

转载于:https://my.oschina.net/liting/blog/424431

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值