Netty线程模型详解

Netty线程模型详解_第1张图片

概述

今天有空我们来聊聊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是一个线程,内部包含一个FIFOtaskQueueSelector)和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线程模型

服务端线程模型
Netty线程模型详解_第2张图片

服务器启动 -> 客户端连接 -> 服务器处理连接 -> 服务器处理客户端数据 -> 客户端处理服务器数据


1:客户端连接:

我们直接看这行代码:
bootstrap.connect(new InetSocketAddress(host, port));

通过帮助类ClientBootstrap来连接服务器。
Debug源码进去发现最后是某个Channel类进行connect操作。

而这个Channel是如何来的呢?其实是从前面的 ChannelFactoryChannelPipelineFactory得到的。

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:客户端:客户端的流程和服务器端类似。


总结:

  1. Netty将操作封装成事件,比如: 发起连接时,产生connectdownstream事件。连接完毕后,产生upstreamconnect事件。
  2. 所有的事件都是放入Pipeline进行传送,传送的过程中可能被注册到pipeline中的Handler进行处理
  3. Pipeline传送完后,都必须都通ChannelSink进行处理。Sink默认处理了琐碎的操作,必须连接、读写等等。
  4. Channels:几乎所有的操作都能在这里找到,当然Channels一般是发送事件
  5. NioWork: 处理IO事件的核心类,并承担了分发的责任。


NioEventLoop设计原理

串行化设计避免线程竞争
我们知道当系统在运行过程中,如果频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗

串行执行Handler链
为了解决上述问题,Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO线程NioEventLoop负责,这就意味着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险,对于用户而言,甚至不需要了解Netty的线程细节,这确实是个非常好的设计理念。

你可能感兴趣的:(Netty,网络协议)