网站建设没有预付款,360建筑网官方网站,自己开发一个app软件,郑州做网站那家做的好本文基于 Netty 4.1 展开介绍相关理论模型、使用场景、基本组件、整体架构#xff0c;知其然且知其所以然#xff0c;希望给大家在实际开发实践、学习开源项目方面提供参考。 Netty 是一个异步事件驱动的网络应用程序框架#xff0c;用于快速开发可维护的高性能协议服… 本文基于 Netty 4.1 展开介绍相关理论模型、使用场景、基本组件、整体架构知其然且知其所以然希望给大家在实际开发实践、学习开源项目方面提供参考。 Netty 是一个异步事件驱动的网络应用程序框架用于快速开发可维护的高性能协议服务器和客户端。
一、JDK 原生 NIO 程序的问题
JDK 原生也有一套网络应用程序 API但是存在一系列问题主要如下
NIO 的类库和 API 繁杂使用麻烦。你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。需要具备其他的额外技能做铺垫。例如熟悉 Java 多线程编程因为 NIO 编程涉及到 Reactor 模式你必须对多线程和网路编程非常熟悉才能编写出高质量的 NIO 程序。可靠性能力补齐开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。
NIO 编程的特点是功能开发相对容易但是可靠性能力补齐工作量和难度都非常大。
JDK NIO 的 Bug。例如臭名昭著的 Epoll Bug它会导致 Selector 空轮询最终导致 CPU 100%。
官方声称在 JDK 1.6 版本的 update 18 修复了该问题但是直到 JDK 1.7 版本该问题仍旧存在只不过该 Bug 发生概率降低了一些而已它并没有被根本解决。
二、Netty 的特点
Netty 对 JDK 自带的 NIO 的 API 进行封装解决上述问题主要特点有
设计优雅适用于各种传输类型的统一 API 阻塞和非阻塞 Socket基于灵活且可扩展的事件模型可以清晰地分离关注点高度可定制的线程模型 - 单线程一个或多个线程池真正的无连接数据报套接字支持自 3.1 起。使用方便详细记录的 Javadoc用户指南和示例没有其他依赖项JDK 5Netty 3.x或 6Netty 4.x就足够了。高性能吞吐量更高延迟更低减少资源消耗最小化不必要的内存复制。安全完整的 SSL/TLS 和 StartTLS 支持。社区活跃不断更新社区活跃版本迭代周期短发现的 Bug 可以被及时修复同时更多的新功能会被加入。
三、Netty 常见使用场景
Netty 常见的使用场景如下
互联网行业。在分布式系统中各个节点之间需要远程服务调用高性能的 RPC 框架必不可少Netty 作为异步高性能的通信框架往往作为基础通信组件被这些 RPC 框架使用。典型的应用有阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信Dubbo 协议默认使用 Netty 作为基础通信组件用于实现各进程节点之间的内部通信。
游戏行业。无论是手游服务端还是大型的网络游戏Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件它本身提供了 TCP/UDP 和 HTTP 协议栈。非常方便定制和开发私有协议栈账号登录服务器地图服务器之间可以方便的通过 Netty 进行高性能的通信。
大数据领域。经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架默认采用 Netty 进行跨界点通信它的 Netty Service 基于 Netty 框架二次封装实现。
三、Netty 高性能设计 Netty 作为异步事件驱动的网络高性能之处主要来自于其 I/O 模型和线程处理模型前者决定如何收发数据后者决定如何处理数据。
3.1、I/O 模型
用什么样的通道将数据发送给对方BIO、NIO 或者 AIOI/O 模型在很大程度上决定了框架的性能。
1、同步阻塞IO 阻塞指的是I/O方法调用比如read在数据或者状态还没有就绪的时候一直等待直到就绪才返回。比如阻塞模式下的Socket的read方法如果没有接收到数据read方法将会阻塞程序就会停在那里不能做其他的事情。这种模型下如果服务器想处理多个连接那么就要为每个Socket连接创建一个单独的线程开销会很大。
传统阻塞型 I/O(BIO)可以用下图表示 特点如下
每个请求都需要独立的线程完成数据 Read业务处理数据 Write 的完整操作问题。当并发数较大时需要创建大量线程来处理连接系统资源占用较大。连接建立后如果当前线程暂时没有数据可读则线程就阻塞在 Read 操作上造成线程资源浪费。
2、同步非阻塞IO 非阻塞模型是I/O方法调用比如read无论数据有没有就绪会马上返回。如果有数据就会读到数据如果没有数据就返回一个错误码。应用程序需要用轮询的方式不断去检测数据有没有就绪但是程序不会被阻塞除了轮询程序还可以做其他事情。但是这种轮询的方式会浪费CPU的时间效率不够高。 3、IO多路复用 IO多路复用模型是建立在内核提供的多路分离函数select基础之上的使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。 如上图《多路分离函数select》所示用户首先将需要进行IO操作的socket添加到select中然后阻塞等待select系统调用返回。当数据到达时socket被激活select函数返回。用户线程正式发起read请求读取数据并继续执行。 从流程上来看使用select函数进行IO请求和同步阻塞模型没有太大的区别甚至还多了添加监视socket以及调用select函数的额外操作效率更差。但是使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket然后不断地调用select读取被激活的socket即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中必须通过多线程的方式才能达到这个目的。 然而使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求但是每个IO请求的过程还是阻塞的在select函数上阻塞平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求然后去做自己的事情等到数据到来时再进行处理则可以提高CPU的利用率。 如上图《Reactor实现多路复用》所示EventHandler抽象类表示IO事件处理器它拥有IO文件句柄Handle通过get_handle获取以及对Handle的操作handle_event读/写等。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理EventHandler注册、删除等并使用handle_events实现事件循环不断调用同步事件多路分离器一般是内核的多路分离函数select只要某个文件句柄被激活可读/写等select就返回阻塞handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。 如上图《Reactor实现多路复用》所示通过Reactor的方式可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作异步而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时则通知相应的用户线程或执行用户线程的回调函数执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的因此多路IO复用模型也被称为异步阻塞IO模型。注意这里的所说的阻塞是指select函数执行时线程被阻塞而不是指socket。一般在使用IO多路复用模型时socket都是设置为NONBLOCK的不过这并不会产生影响因为用户发起IO请求时数据已经到达了用户线程一定不会被阻塞。 IO多路复用是最常使用的IO模型但是其异步程度还不够“彻底”因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO而非真正的异步IO。
4、异步IO 异步模型是指用户给I/O方法调用比如read提供一个回调方法I/O方法调用会立刻返回等到数据就绪时回调方法会执行。这个看上去很好用把所有的处理都推给了系统用户只需要关心回调方法中的数据处理就行了但是在高并发情况下处理好系统I/O程序和用户程序之间的CPU竞争比较困难。 如上图《Proactor设计模式》所示Proactor模式和Reactor模式在结构上比较相似不过在用户Client使用方式上差别较大。Reactor模式中用户线程通过向Reactor对象注册感兴趣的事件监听然后事件触发时调用事件处理函数。而Proactor模式中用户线程将AsynchronousOperation读/写等、Proactor以及操作完成时的CompletionHandler注册到AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提供了一组异步操作API读/写等供用户使用当用户线程调用异步API后便继续执行自己的任务。AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作实现真正的异步。当异步IO操作完成时AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor和CompletionHandler取出然后将CompletionHandler与IO操作的结果数据一起转发给ProactorProactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以绑定一个Proactor对象但是一般在操作系统中Proactor被实现为Singleton模式以便于集中化分发操作完成事件。 如上图《Proactor实现异步IO》所示异步IO模型中用户线程直接使用内核提供的异步IO API发起read请求且发起后立即返回继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时由内核负责读取socket中的数据并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部ProactorProactor将IO完成的信息通知给用户线程一般通过调用用户线程注册的完成事件处理函数完成异步IO。 相比于IO多路复用模型异步IO并不十分常用不少高性能并发服务程序使用IO多路复用模型多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善更多的是采用IO多路复用模型模拟异步IO的方式IO事件触发时不直接通知用户线程而是将数据读写完毕后放到用户指定的缓冲区中。Java7之后已经支持了异步IO感兴趣的读者可以尝试使用。
3.2、Reactor多线程模型
1、大部分网络服务包括以下处理步骤
read request读取客户端发送过来的byte数据decode request把byte数据解码成特定类型的数据process (compute) service根据请求数据进行业务处理encode reply把处理结果转换成byte数据send reply发送byte数据给客户端
2、Reactor多线程模型图 Reactor - 负责响应IO事件把事件分发给相应的处理代码。Reactor运行在一个独立的线程中非Thread Pool中的线程。具体来说Reactor主要有两个职责一个是处理来自客户端的连接事件处理代码由acceptor实现另一个是处理读取和发送数据的事件处理代码由Handler实现。Acceptor - 用以接受客户端的连接请求然后创建Handler对连接进行后续的处理读取处理发送数据。Handler - 事件处理类用以实现具体的业务逻辑。图中readdecodecomputeencode和send都是由handler实现的。Thread Pool - Thread Pool中的thread被称作worker thread。Handler中的decodecompute和encode是用worker thread执行的。
值得注意的是Handler中的read和send方法是在Reactor线程而不是worker thread中执行的。这意味着对socket数据的读取发送数据和对数据的处理是在不同的线程中进行的.
3、Reactor多线程模型的主要问题
read和send会影响接受客户端连接的性能
前面分析过read和send是在Reactor线程中执行的接受客户端的连接请求也是在Reactor线程中执行。这使得如果有read或者send耗时较长会影响其他客户端连接的速度。Read和send性能不够高效
网络服务对于来自同一客户端的read和send是串行的但是对于不同客户端之间的read和send是可以并行进行的。由于read和send运行在Reactor单线程中不能充分发挥硬件能力。线程上下文切换带来额外开销
前面提到的处理客户端请求的步骤依次是readdecodeprocessencodesend。由于read和send是在Reactor线程中执行而decodeprocess和encode是在worker thread线程中执行引入了额外的线程切换开销这种开销在高并发的时候会体现出来。
4、实际应用中的多线程模型改进后的模型图 Reactor线程专门用于接受客户端连接通过acceptor创建多个Event Loop 组成Event Loop Pool每个Event Loop都有自己的Selector并且运行在独立的线程上Acceptor对于每一个客户端的连接从EventLoopPool中选择一个Event Loop进行处理并且保证每个客户端连接在整个生命周期中都是由同一个Event Loop线程来处理从而使得Handler中的实现-readdecodeprocessencodesend-都在同一个线程中执行。整个线程模型除了高效的性能还有非常重要的一点是Handler的实现不需要加锁一方面对性能有帮助另一方面避免多线程编程的复杂度。