今天有空我们来聊聊Netty,先来一段官方概述。
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速
和简单
的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发
。“快速”和“简单”并不用产生维护性或性能上的问题。
Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性
。
总而言之,Netty就是一个集成了多种网络协议,便于我们快速简单高效稳定的开发出应用的一个异步通信框架。(博主用Netty主要就是基于tcp协议对接一些设备,例如摄像头,温度,湿度,烟雾传感器,智慧水表等等等等公司自研的设备)
话不多说,下面就对Netty的启动流程
,零拷贝
,服务端and客户端线程模型
,以及NioEventLoop设计原理
来做个简单地讲解
大佬勿喷,评论区留言☺
Netty的启动流程(ServerBootstrap
),就是创建NioEventLoopGroup
(内部可能包含多个NioEventLoop
,每个eventLoop
是一个线程,内部包含一个FIFO
的taskQueue
和Selector
)和ServerBootstrap
实例,并进行bind
的过程(bind
流程涉及到channel
的创建和注册),之后就可以对外提供服务了。
Netty的启动流程中,涉及到多个操作,比如register、bind、注册对应事件
等,为了不影响main线程执行,这些工作以task
的形式提交给NioEventLoop
,由NioEventLoop
来执行这些task
,也就是register、bind、注册事件
等操作。
NioEventLoop
(准确来说是SingleThreadEventExecutor
)中包含了private volatile Thread thread
,该thread
变量的初始化是在new
的线程第一次执行run
方式时才赋值的,这种形式挺新颖的。
Netty的零拷贝主要体现在三个方面:
就如上所说,ByteBuf可以分为HeapByteBuf和DirectByteBuf,当使用DirectByteBuf可以实现零拷贝
CompositeByteBuf将多个ByteBuf封装成一个ByteBuf,对外提供封装后的ByteBuf接口
DefaultFileRegion是Netty的文件传输类,它通过transferTo方法将文件直接发送到目标Channel,而不需要循环拷贝的方式,提升了传输性能
Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化
Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱,把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来
AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多
Linux上AIO不够成熟,处理回调结果速度跟不到处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)
服务器启动 -> 客户端连接 -> 服务器处理连接 -> 服务器处理客户端数据 -> 客户端处理服务器数据
我们直接看这行代码:
bootstrap.connect(new InetSocketAddress(host, port));
通过帮助类ClientBootstrap来连接服务器。
Debug源码进去发现最后是某个Channel类进行connect操作。
而这个Channel是如何来的呢?其实是从前面的 ChannelFactory
和ChannelPipelineFactory
得到的。
Channel.connect -> AbstractChannel.connect -> Channels.connect(…);
Channels是Channel的帮助类,封装一些常用的操作。在封装操作时,基本都是触发事件。
这里发起一个connectd的Downstream的事件。
所有的事件都是丢给ChannelPipeline
进行管理,ChannelPipeline
使用了责任链模式来将事件传送给注册到Pipeline
中的ChannelHandler
,由ChannelHandler进行处理。如果遍历了所有的ChannelHandler
后则交给ChannelSink
进行处理,ChannelSink
根据不同的事件进行不同的处理,对于connect
事件,ChannelSink
发送连接操作后则将该Channel
注册到NioWorker
中,以后的任何事件都通过NioWorker
(封装selector
的操作)来进行处理。
ClientBootstrap.connect
->
Channel.connect->
AbstractChannel.connect->
Channels.connect(…)->
发送connect事件->
ChannelSink->
发起实际的连接操作->
将Channel注册给Nioworker
bootstrap.bind(…)
->
触发ServerSocketChannel.open()的事件->
捕捉open事件,channel.bind->
Channels.bind(…)->
发起bind命令->
PipelineSink进行处理->
使用socket进行bind,等待连接事件。
服务器启动后
NioServerSocketPipelineSink.Boss.run()在监听accept事件
->
捕捉到accept事件->
将NioWorker进行注册NioSocketChannel->
向java.nio.SocketChannel注册op_read的监听。
当客户端连接Server后
就会发起Connected的upstream事件
->
通过Pipeline进行处理->
SimpleChannelUpstreamHandler.handleUpstream()->
EchoClientHandler.channelConnected()
接收数据:
NioWorker.run()
->
nioworker. processSelectedKeys()->
Nioworker. Read()将从SocketChannel读取的数据封装成 ChannelBuffer->
发送upstream事件:fireMessageReceived(channel,buffer)->
由注册到Pipeline中的Hanlder进行处理: EchoServerHandler. messageReceived(…)
发送数据:
e.getChannel().write(e.getMessage());
->
Channels.write()->
发起downstream事件->
NioServerSocketPipelineSink. handleAcceptedSocket()将向外写的事件放入Channel中,然后通过NioWorker.writeFromUserCode()进行发送。
connect
的downstream
事件。连接完毕后,产生upstream
的connect
事件。pipeline
中的Handler
进行处理Pipeline
传送完后,都必须都通ChannelSink进行处理。Sink
默认处理了琐碎的操作,必须连接、读写
等等。Channels一般是发送事件
处理IO事件
的核心类,并承担了分发的责任。串行化设计避免线程竞争
我们知道当系统在运行过程中,如果频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗。
串行执行Handler链
为了解决上述问题,Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO
线程NioEventLoop
负责,这就意味着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险,对于用户而言,甚至不需要了解Netty的线程细节,这确实是个非常好的设计理念。