最近工作中用到了Netty,于是借此机会整理出来。关于什么是Netty,以及它的好处就不多说了,网上资料很多。
学习Netty前我们应该着重理解下面这些对象,这些都是Netty的核心概念,对我们学习Netty很有帮助
一种是用于客户端的Bootstrap,一种是用于服务端的ServerBootstrap。
有两个区别:
1、“ServerBootstrap”监听在服务器监听一个端口轮询客户端的“Bootstrap”,客户端的Bootstrap需要调用connect进行连接,返回ChannelFuture对象,可以异步的阻塞直到关闭成功fu.channel().closeFuture().sync();
2 服务端引导对象包含两个EventLoopGroup对象,客户端只有一个,这里服务端的ServerBootstrap可以认为有2个channels组,第一组包含一个单例ServerChannel,代表持有一个绑定了本地端口的socket;第二组包含所有的Channel,代表服务器已接受了的连接。
上图中,EventLoopGroupA唯一的目的就是接受连接然后交给EventLoopGroupB。Netty可以使用两个不同的Group,因为服务器程序需要接受很多客户端连接的情况下,一个EventLoopGroup将是程序性能的瓶颈,因为事件循环忙于处理连接请求,没有多余的资源和空闲来处理业务逻辑,最后的结果会是很多连接请求超时。若有两EventLoops,即使在高负载下,所有的连接也都会被接受,因为EventLoops接受连接不会和哪些已经连接了的处理共享资源。
下面这幅图表示当服务端只有一个 EventLoopGroup 的情况下,也就是处理 IO 和接受连接只使用一个 EventLoopGroup 对象。是EventLoop的集合,包括一个或多个EventLoop,而EventLoop关联一个channel,是执行实际工作的线程,EventLoop总是绑定一个单一的线程,在其生命周期内不会改变。
当注册一个Channel后,Netty将这个Channel绑定到一个EventLoop,在Channel的生命周期内总是被绑定到一个EventLoop。在Netty IO操作中,你的程序不需要同步,因为一个指定通道的所有IO始终由同一个线程来执行。
下图显示了EventLoop和EventLoopGroup的关系:
这里显示EventLoop是一个EventLoopGroup,两个的关系:EventLoopGroup可以包含很多个EventLoop,每个Channel绑定一个EventLoop不会被改变,因为EventLoopGroup包含少量的EventLoop的Channels,很多Channel会共享同一个EventLoop。这意味着在一个Channel保持EventLoop繁忙会禁止其他Channel绑定到相同的EventLoop。我们可以理解为EventLoop是一个事件循环线程,而EventLoopGroup是一个事件循环集合。
要明白Netty程序wirte或read时发生了什么,首先要对Handler是什么有一定的了解。Handlers自身依赖于ChannelPipeline来决定它们执行的顺序,因此不可能通过ChannelPipeline定义怎么处理我们的程序一些业务逻辑,同理也不能通过ChannelHandler定义ChannelPipeline的行为。
ChannelHandler是一段执行业务逻辑处理数据的代码,它们来来往往的通过ChannelPipeline。实际上,ChannelHandler是定义一个handler的父接口,ChannelInboundHandler和ChannelOutboundHandler都实现ChannelHandler接口,如下图:
Netty中有两个方向的数据流,上图显示的入站(ChannelInboundHandler)和出站(ChannelOutboundHandler),数据是从用户应用程序到远程主机则是“出站(outbound)”,相反若数据时从远程主机到用户应用程序则是“入站(inbound)”。
为了使数据从一端到达另一端,一个或多个ChannelHandler将以某种方式操作数据。这些ChannelHandler会在程序的“引导”阶段被添加ChannelPipeline中,(这里的引导就是我们上面所说的创建ServerBootstrap对象)并且被添加的顺序将决定处理数据的顺序。ChannelPipeline的作用我们可以理解为用来管理ChannelHandler的一个容器,每个ChannelHandler处理各自的数据(例如入站数据只能由ChannelInboundHandler处理),处理完成后将转换的数据放到ChannelPipeline中交给下一个ChannelHandler继续处理,直到最后一个ChannelHandler处理完成。
下图显示了ChannelPipeline的处理过程:
上图显示ChannelInboundHandler和ChannelOutboundHandler都要经过相同的ChannelPipeline。
在ChannelPipeline中,如果消息被读取或有任何其他的入站事件,消息将从ChannelPipeline的头部开始传递给第一个ChannelInboundHandler,这个ChannelInboundHandler可以处理该消息或将消息传递到下一个ChannelInboundHandler中,一旦在ChannelPipeline中没有剩余的ChannelInboundHandler后,ChannelPipeline就知道消息已被所有的饿Handler处理完成了。
反过来也是如此,任何出站事件或写入将从ChannelPipeline的尾部开始,并传递到最后一个ChannelOutboundHandler。ChannelOutboundHandler的作用和ChannelInboundHandler相同,它可以传递事件消息到下一个Handler或者自己处理消息。不同的是ChannelOutboundHandler是从ChannelPipeline的尾部开始,而ChannelInboundHandler是从ChannelPipeline的头部开始,当处理完第一个ChannelOutboundHandler处理完成后会出发一些操作,比如一个写操作。
一个事件能传递到下一个ChannelInboundHandler或上一个ChannelOutboundHandler,在ChannelPipeline中通过使用ChannelHandlerContext调用每一个方法。Netty提供了抽象的事件基类称为ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。每个都提供了在ChannelPipeline中通过调用相应的方法将事件传递给下一个Handler的方法的实现。可以覆盖父类的方法,做自己的一些操作。
当一个ChannelHandler添加到ChannelPipeline中时获得一个ChannelHandlerContext,Netty中发送消息有两种方法:直接写入通道或写入ChannelHandlerContext对象。这两种方法的主要区别如下:
Netty提供了一系列的“Adapter”类,这让事情变的很简单。每个handler负责转发时间到ChannelPipeline的下一个handler。在*Adapter类(和子类)中是自动完成的,因此我们只需要在感兴趣的*Adapter中重写方法。
Netty有一下适配器:
我们都知道网络中只能传输的是字节。所以在netty发送或接收消息后,Netty必须将消息数据从一种形式转化为另一种。接收消息后,需要将消息从字节码转成Java对象(由某种解码器解码);发送消息前,需要将Java对象转成字节(由某些类型的编码器进行编码)。
一般做游戏开发使用的协议都是简单的基本类型,所以使用字节编解码器就足够了。Netty默认自己提供了ByteToMessageDecoder、MessageToByteEncoder。还有Google的协议“ProtobufEncoder”和“ProtobufDecoder”。
其实针对跨语言平台比较简单的方法就是统一用JSON作为数据的格式。
NIO传输是目前最常用的方式,它通过使用选择器提供了完全异步的方式操作所有的I/O,NIO从Java 1.4才被提供。NIO中,我们可以注册一个通道或获得某个通道的改变的状态,通道状态有下面几种改变:
处理完改变的状态后需重新设置他们的状态,用一个线程来检查是否有已准备好的Channel,如果有则执行相关事件。在这里可能只同时一个注册的事件而忽略其他的。选择器所支持的操作在SelectionKey中定义,具体如下:
NIO在处理过程也会有一定的延迟,若连接数不大的话,延迟一般在毫秒级,但是其吞吐量依然比OIO模式的要高。Netty中的NIO传输是“zero-file-copy”,也就是零文件复制,这种机制可以让程序速度更快,更高效的从文件系统中传输内容,零复制就是我们的应用程序不会将发送的数据先复制到JVM堆栈在进行处理,而是直接从内核空间操作。接下来我们将讨论OIO传输,它是阻塞的。
OIO就是java中提供的Socket接口,java最开始只提供了阻塞的Socket,阻塞会导致程序性能低。下面是OIO的处理流程图,若想详细了解,可以参阅其他相关资料。