Netty是一款异步的事件驱动的网络应用程序框架,支持快速开发可维护、高性能且面向协议的服务器和客户端。Netty主要是对Java的NIO包进行的封装。Netty特性具体如表5-1所示。
表5-1 Netty特性总结
网络上有一个形象的比喻来形容Netty客户端和服务器端的交互模式。把一个人比作一个Client,把山比作一个Server,人走到山旁,就和山建立了连接,人向山大喊了一声,就代表向山发送了数据,人的喊声经过山的反射形成了回声,这个回声就是服务器的响应数据。如果人离开,就代表断开了连接,当然人也可以再回来。好多人可以同时向山大喊,他们的喊声一定会得到山的回应,具体如图5-6所示。
图5-6 Netty客户端和服务器端的交互模式
本节我们写一个简单的Demo,具体步骤如下:
步骤01 完整的NettyServer包含两部分:BootsTrapping用于配置服务器端基本信息;ServerHandler用于真正的业务逻辑处理。
首先我们开发服务类NettyServer,具体代码如下所示:
01 创建一个ServerBootstrap实例。
02 创建EventLoopGroup处理各种事件,如处理连接请求,发送、接收数据等。
03 定义本地InetSocketAddress(port),让Server进行绑定。
04 创建childHandler来处理每一个连接请求。
05 所有准备就绪后,调用ServerBootstrap.bind()方法绑定Server。
接下来开发NettyServerHandler类来处理真正的业务,具体代码如下所示:
步骤02 开发NettyClient,连接到Server,向Server写数据,等待Server返回数据,最后关闭连接。和Server端类似,只不过Client端要同时指定连接主机的IP和Port。具体代码如下所示:
01 创建一个ServerBootstrap实例。
02 创建一个EventLoopGroup来处理各种事件,如处理连接请求,发送、接收数据等。
03 定义一个远程InetSocketAddress。
04 当连接完成之后,Handler会被执行一次。
05 所有准备就绪后,调用ServerBootstrap.connect()方法连接Server。
同样继承一个SimpleChannelInboundHandler来实现业务逻辑代码NettyClientHandler,需要重写其中的3个方法,具体代码如下所示:
步骤03 运行NettyServer类启动服务端,可在控制台中查看打印的信息:
//省略信息 //服务启动并监听8080端口 Server started and listen on /0:0:0:0:0:0:0:0:8080
运行NettyClient类启动客户端,可在控制台中查看打印的信息:
//客户端打印的信息 client connected...... Server said:hello client!
同时,也可以在服务端控制台再次查看打印的信息:
//服务端打印的信息 Client said:hello Server!
步骤04 至此,第一个Netty Demo开发完成。
为了更好地理解和进一步深入Netty,先总体认识一下Netty用到的组件及它们在整个Netty架构中是如何协调工作的。Netty应用中必不可少的组件有:
Bootstrap或ServerBootstrap:一个Netty应用,通常由一个Bootstrap开始,它的主要作用是配置整个Netty程序,串联起各个组件。
Handler:为了支持各种协议和处理数据的方式,便诞生了Handler组件。Handler主要用来处理各种事件,这里的事件很广泛,可以是连接、数据接收、异常、数据转换等。ChannelInboundHandler是一个最常用的Handler,作用是处理接收到数据时的事件,也就是说,我们的业务逻辑一般就写在Handler里面,ChannelInboundHandler用来处理我们的核心业务逻辑。
ChannelInitializer:当一个连接建立时,我们需要知道如何接收或者发送数据。当然,我们有各种各样的Handler实现来处理它,ChannelInitializer便是用来配置这些Handler的,它会提供一个ChannelPipeline,并把Handler加入ChannelPipeline。
ChannelPipeline:一个Netty应用,基于ChannelPipeline机制,这种机制需要依赖于EventLoop和EventLoopGroup,这三个组件(ChannelPipeline、EventLoop以及EventLoopGroup)都和事件或者事件处理相关。
EventLoop:目的是为Channel处理IO操作,一个EventLoop可以为多个Channel服务。
EventLoopGroup:包含多个EventLoop。
Channel:代表一个Socket连接,或者其他和IO操作相关的组件,它和EventLoop一起用来参与IO处理。
Future:在Netty中所有的IO操作都是异步的。因此,你不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成,或者直接注册一个监听,具体的实现是通过Future和ChannelFuture完成的。它们可以注册一个监听,当操作执行成功或失败时监听会自动触发。总之,所有的操作都会返回一个ChannelFuture。
一个Channel会对应一个EventLoop,而一个EventLoop会对应一个线程,也就是说,仅有一个线程在负责一个Channel的IO操作。当一个连接到达,Netty会注册一个Channel,然后EventLoopGroup会分配一个EventLoop绑定到Channel上,在这个Channel的整个生命周期中,都会由绑定的这个EventLoop来为它服务,而EventLoop就是一个线程。
EventLoop和EventLoopGroup的关系如何呢?我们前面说过一个EventLoopGroup包含多个Eventloop,从图5-7中可以看出,EventLoop其实继承自EventloopGroup,也就是说,在某些情况下,我们可以把一个EventLoopGroup当作一个EventLoop来用。
我们利用Bootstrapping来配置Netty应用,它有两种类型:Bootstrap和ServerBootstrap。Bootstrap用于Client端,ServerBootstrap用于Server端。
图5-7 EventLoop类继承图
ServerBootstrap用于Server端,通过调用bind()方法来绑定到一个端口监听连接;Bootstrap用于Client端,需要调用connect()方法来连接服务器端,但我们也可以通过调用bind()方法返回的ChannelFuture获取Channel去连接服务器端。
客户端的Bootstrap一般用一个EventLoopGroup,而服务器端的ServerBootstrap会用到两个(这两个也可以是同一个实例)。为何服务器端要用到两个EventLoopGroup呢?这么设计有明显的好处,如果一个ServerBootstrap有两个EventLoopGroup,就可以把第一个EventLoopGroup专门用来负责绑定到端口监听连接事件,而把第二个EventLoopGroup用来处理每个接收到的连接,具体如图5-8所示。
图5-8 两个EventLoopGroup的情况
如果仅由一个EventLoopGroup处理所有请求和连接的话,在并发量很大的情况下,这个EventLoopGroup就可能会忙于处理已经接收到的连接而不能及时处理新的连接请求,用两个的话,会有专门的线程来处理连接请求,不会导致请求超时的情况,大大提高了并发处理能力。
我们知道一个Channel需要由一个EventLoop来绑定,而且两者一旦绑定就不会再改变。一般情况下,一个EventLoopGroup中的EventLoop数量会少于Channel数量,因此很有可能出现多个Channel共用一个EventLoop的情况,这意味着如果一个Channel中的EventLoop很忙的话,就会影响这个Eventloop对其他Channel的处理,这也是我们不能阻塞EventLoop的原因。
当然,我们的Server也可以只用一个EventLoopGroup,由一个实例来处理连接请求和IO事件,具体如图5-9所示。
图5-9 一个EventLoopGroup的情况
我们的应用程序中用到的最多的应该是ChannelHandler,可以这么想象,数据在一个ChannelPipeline中流动,而ChannelHandler便是其中一个个小阀门,这些数据会经过每一个ChannelHandler并且被它处理。
ChannelHandler有两个子类:ChannelInboundHandler和ChannelOutboundHandler,具体如图5-10所示。这两个子类对应两个数据流向,如果数据是从外部流入我们的应用程序的,就看作是Inbound,相反便是Outbound。其实ChannelHandler和Servlet有些类似,一个ChannelHandler处理完接收到的数据会传给下一个Handler,或者什么都不处理,直接传递给下一个。ChannelPipeline具体原理如图5-11所示。
图5-10 ChannelHandler的两个子类
图5-11 ChannelPipeline简单原理图
从图5-11可知,一个ChannelPipeline可以将ChannelInboundHandler和ChannelOutboundHandler混合在一起,当一个数据流进入ChannelPipeline时,它会从ChannelPipeline头部开始传给第一个ChannelInboundHandler,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部“最后”一个ChannelOutboundHandler,当它处理完成后会传递给前一个ChannelOutboundHandler。
数据在各个Handler之间传递,需要调用方法中传递的ChanneHandlerContext来操作,Netty的API中提供了两个基类:ChannelOutboundHandlerAdapter和ChannelOutboundHandlerAdapter,它们仅仅实现了调用ChanneHandlerContext来把消息传递给下一个Handler,因为我们只关心处理数据,因此程序中可以继承这两个基类来帮助我们做这些,而我们仅需实现处理数据的部分即可。
InboundHandler和OutboundHandler在ChannelPipeline中是混合在一起的,因为它们各自实现的是不同的接口。对于Inbound Event,Netty会自动跳过OutboundHandler,相反若是Outbound Event,ChannelInboundHandler会被忽略掉。
当一个ChannelHandler被加入ChannelPipeline中时,它便会获得一个ChannelHandlerContext的引用,而ChannelHandlerContext可以用来读写Netty中的数据流。因此,现在有两种方式来发送数据,一种是把数据直接写入Channel,另一种是把数据写入ChannelHandlerContext,它们的区别是写入Channel的话,数据流会从Channel的头开始传递,而如果写入ChannelHandlerContext,数据流就会流入管道中的下一个Handler。
Netty中有很多Handler,具体是哪种Handler,还要看它们继承的是InboundAdapter还是OutboundAdapter。当然,Netty还提供了一系列的Adapter来帮助我们简化开发。我们知道在ChannelPipeline中每一个Handler都负责把Event传递给下一个Handler,有了这些辅助Adapter,这些额外的工作都可以自动完成,我们只需要覆盖实现真正关心的部分即可。此外,还有一些Adapter会提供一些额外的功能,比如编码和解码。下面我们就来看一下其中的3种常用的ChannelHandler。
(1)Encoder(编码器)和Decoder(解码器)
在网络传输时只能传输字节流,需要把message转换为bytes,与之对应,我们在接收数据后,必须把接收到的bytes再转换成message。我们把bytes转换成message这个过程称作Decode(解码),把message转换成bytes这个过程称为Encode(编码)。
Netty中提供了很多现成的编码/解码器,从它们的名字中便可以知道其用途,如ByteToMessageDecoder、MessageToByteEncoder以及专门用来处理Google ProtoBuf协议的ProtobufEncoder、ProtobufDecoder。
对于Decoders,很容易便可以知道它是继承自ChannelInboundHandlerAdapter或ChannelInboundHandler的,因为解码是把ChannelPipeline传入的bytes解码成我们可以理解的message。Decoder会覆盖其中的ChannelRead()方法,在方法中调用具体的decode方法解码传递过来的字节流,然后通过调用ChannelHandlerContext.fireChannelRead(decodedMessage)方法把编码好的message传递给下一个Handler。
(2)SimpleChannelInboundHandler
其实我们最关心的事情是如何处理接收到的解码后的数据,真正的业务逻辑便是处理接收到的数据。Netty提供了一个常用的基类SimpleChannelInboundHandler<T>,其中T就是这个Handler处理的数据的类型,消息到达这个Handler时,Netty会自动调用这个Handler中的channelRead0(ChannelHandlerContext,T)方法,T是传递过来的数据对象,在这个方法中可以任意编写我们所需的业务逻辑。