
概述
今天有空我们来聊聊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的线程细节,这确实是个非常好的设计理念。
1863





