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