Netty(DotNetty)原理解析

1、背景介绍

DotNetty是微软的Azure团队,使用C#实现的Netty的版本发布。不但使用了C#和.Net平台的技术特色,而且保留了Netty原来绝大部分的编程接口。让咱们在使用时,彻底能够依照Netty官方的教程来学习和使用DotNetty应用程序。html

Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。java

2、NIO

他并非 Java 独有的概念,NIO表明的一个词汇叫着IO多路复用。它是由操做系统提供的系统调用,早期这个操做系统调用的名字是select,可是性能低下,后来渐渐演化成了 Linux 下的epoll和Mac里的kqueue。咱们通常就说是epoll,由于没有人拿苹果电脑做为服务器使用对外提供服务。而Netty就是基于Java NIO技术封装的一套框架。为何要封装,由于原生的Java NIO使用起来没那么方便,并且还有臭名昭著的bug,Netty把它封装以后,提供了一个易于操做的使用模式和接口,用户使用起来也就便捷多了。react

 

说NIO以前先说一下BIO(Blocking IO),如何理解这个Blocking呢?linux

1.客户端监听(Listen)时,Accept是阻塞的,只有新链接来了,Accept才会返回,主线程才能继读写socket时,Read是阻塞的,只有请求消息来了,Read才能返回,子线程才能继续处理。编程

2.读写socket时,Write是阻塞的,只有客户端把消息收了,Write才能返回,子线程才能继续读取下一个请求。后端

3.传统的BIO模式下,从头至尾的全部线程都是阻塞的,这些线程就干等着,占用系统的资源,什么事也不干。设计模式

Netty 的非阻塞 I/O 的实现关键是基于 I/O 复用模型,这里用 Selector 对象表示:安全

 

1.Netty 的 IO 线程 NioEventLoop 因为聚合了多路复用器 Selector,能够同时并发处理成百上千个客户端链接。服务器

2.当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程能够进行其余任务。网络

3.线程一般将非阻塞 IO 的空闲时间用于在其余通道上执行 IO 操做,因此单独的线程能够管理多个输入和输出通道。

4.因为读写操做都是非阻塞的,这就能够充分提高 IO 线程的运行效率,避免因为频繁 I/O 阻塞致使的线程挂起。

5.一个 I/O 线程能够并发处理 N 个客户端链接和读写操做,这从根本上解决了传统同步阻塞 I/O 一链接一线程模型,架构的性能、弹性伸缩能力和可靠性都获得了极大的提高。

基于Buffer

传统的 I/O 是面向字节流或字符流的,以流式的方式顺序地从一个 Stream 中读取一个或多个字节, 所以也就不能随意改变读取指针的位置。

在 NIO 中,抛弃了传统的 I/O 流,而是引入了 Channel 和 Buffer 的概念。在 NIO 中,只能从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。

基于 Buffer 操做不像传统 IO 的顺序操做,NIO 中能够随意地读取任意位置的数据。

事件驱动模型

一般,咱们设计一个事件处理模型的程序有两种思路:

  • 1.轮询方式,线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑。
  • 2.事件驱动方式,发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,实际上是设计模式中观察者模式的思路。

事件机制,它能够用一个线程把Accept,读写操做,请求处理的逻辑全干了。若是什么事都没得作,它也不会死循环,它会将线程休眠起来,直到下一个事件来了再继续干活,这样的一个线程称之为NIO线程。用伪代码表示:

while true {
    events = takeEvents(fds)  // 获取事件,若是没有事件,线程就休眠
    for event in events {
        if event.isAcceptable {
            doAccept() // 新连接来了
        } elif event.isReadable {
            request = doRead() // 读消息
            if request.isComplete() {
                doProcess()
            }
        } elif event.isWriteable {
            doWrite()  // 写消息
        }
    }
}复制代码

  

Reactor线程模型

Reactor单线程模型

一个NIO线程+一个accept线程:

 

因为Reactor模式使用的是异步非阻塞IO,全部的IO操做都不会致使阻塞,理论上一个线程能够独立处理全部IO相关的操做。从架构层面看,一个NIO线程确实能够完成其承担的职责。例如,经过Acceptor类接收客户端的TCP链接请求消息,链路创建成功以后,经过Dispatch将对应的ByteBuffer派发到指定的Handler上进行消息解码。用户线程能够经过消息编码经过NIO线程将消息发送给客户端。

对于一些小容量应用场景,可使用单线程模型。可是对于高负载、大并发的应用场景却不合适,主要缘由以下:

1)一个NIO线程同时处理成百上千的链路,性能上没法支撑,即使NIO线程的CPU负荷达到100%,也没法知足海量消息的编码、解码、读取和发送;

2)当NIO线程负载太重以后,处理速度将变慢,这会致使大量客户端链接超时,超时以后每每会进行重发,这更加剧了NIO线程的负载,最终会致使大量消息积压和处理超时,成为系统的性能瓶颈;

3)可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会致使整个系统通讯模块不可用,不能接收和处理外部消息,形成节点故障。

 

Reactor多线程模型

 

Reactor多线程模型的特色:

1)有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP链接请求;

2)网络IO操做-读、写等由一个NIO线程池负责,线程池能够采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;

3)1个NIO线程能够同时处理N条链路,可是1个链路只对应1个NIO线程,防止发生并发操做问题。

在绝大多数场景下,Reactor多线程模型均可以知足性能需求;可是,在极个别特殊场景中,一个NIO线程负责监听和处理全部的客户端链接可能会存在性能问题。例如并发百万客户端链接,或者服务端须要对客户端握手进行安全认证,可是认证自己很是损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型。

 

Reactor主从模型

主从Reactor线程模型的特色是:服务端用于接收客户端链接的再也不是个1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP链接请求处理完成后(可能包含接入认证等),将新建立的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工做。Acceptor线程池仅仅只用于客户端的登录、握手和安全认证,一旦链路创建成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操做。

利用主从NIO线程模型,能够解决1个服务端监听线程没法有效处理全部客户端链接的性能不足问题。

它的工做流程总结以下:

  1. 从主线程池中随机选择一个Reactor线程做为Acceptor线程,用于绑定监听端口,接收客户端链接;
  2. Acceptor线程接收客户端链接请求以后建立新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操做;
  3. 步骤2完成以后,业务层的链路正式创建,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,从新注册到Sub线程池的线程上,用于处理I/O的读写操做.

 

Netty能够基于如上三种模型进行灵活的配置。

总结

Netty是创建在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象。

在Netty里面,Accept链接可使用单独的线程池去处理,读写操做又是另外的线程池来处理。

Accept链接和读写操做也可使用同一个线程池来进行处理。而请求处理逻辑既可使用单独的线程池进行处理,也能够跟放在读写线程一块处理。线程池中的每个线程都是NIO线程。用户能够根据实际状况进行组装,构造出知足系统需求的高性能并发模型。

原文出处:https://www.cnblogs.com/hahahayang/p/11231474.html

相关文章
相关标签/搜索