Netty 面试题 (一网打净,持续更新)


学习材料:最易懂的Netty图书

Netty很是复杂难懂,但有是必须掌握的内容,全网最易懂的Netty书籍为《Netty Zookeeper Redis 高并发实战react

为何说是全网最易懂的Netty书籍呢? 请参见《Netty Redis Zookeeper 高并发实战》 一书的读者评价面试

疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备算法

img

Netty基础题

1.Netty 是什么?

Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。Netty是基于nio的,它封装了jdk的nio,让咱们使用起来更加方法灵活。编程

2.Netty 的特色是什么?

  • 高并发:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通讯框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能获得了很大提升。
  • 传输快:Netty 的传输依赖于零拷贝特性,尽可能减小没必要要的内存拷贝,实现了更高效率的传输。
  • 封装好:Netty 封装了 NIO 操做的不少细节,提供了易于使用调用接口。

3.Netty 的优点有哪些?

  • 使用简单:封装了 NIO 的不少细节,使用更简单。
  • 功能强大:预置了多种编解码功能,支持多种主流协议。
  • 定制能力强:能够经过 ChannelHandler 对通讯框架进行灵活地扩展。
  • 性能高:经过与其余业界主流的 NIO 框架对比,Netty 的综合性能最优。
  • 稳定:Netty 修复了已经发现的全部 NIO 的 bug,让开发人员能够专一于业务自己。
  • 社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。

4.Netty 的应用场景有哪些?

典型的应用有:阿里分布式服务框架 Dubbo,默认使用 Netty 做为基础通讯组件,还有 RocketMQ 也是使用 Netty 做为通信的基础。json

Netty常见的使用场景以下:bootstrap

  • 互联网行业 在分布式系统中,各个节点之间须要远程服务调用,高性能的RPC框架必不可少,Netty做为异步高新能的通讯框架,每每做为基础通讯组件被这些RPC框架使用。 典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通讯,Dubbo协议默认使用Netty做为基础通讯组件,用于实现各进程节点之间的内部通讯。
  • 游戏行业 不管是手游服务端仍是大型的网络游戏,Java语言获得了愈来愈普遍的应用。Netty做为高性能的基础通讯组件,它自己提供了TCP/UDP和HTTP协议栈。 很是方便定制和开发私有协议栈,帐号登陆服务器,地图服务器之间能够方便的经过Netty进行高性能的通讯
  • 大数据领域 经典的Hadoop的高性能通讯和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通讯,它的Netty Service基于Netty框架二次封装实现

5 Netty的特色

Netty的对JDK自带的NIO的API进行封装,解决上述问题,主要特色有:设计模式

  • 设计优雅 适用于各类传输类型的统一API - 阻塞和非阻塞Socket 基于灵活且可扩展的事件模型,能够清晰地分离关注点 高度可定制的线程模型 - 单线程,一个或多个线程池 真正的无链接数据报套接字支持(自3.1起)
  • 使用方便 详细记录的Javadoc,用户指南和示例 没有其余依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了
  • 高性能 吞吐量更高,延迟更低 减小资源消耗 最小化没必要要的内存复制
  • 安全 完整的SSL / TLS和StartTLS支持
  • 社区活跃,不断更新 社区活跃,版本迭代周期短,发现的BUG能够被及时修复,同时,更多的新功能会被加入

6.Netty 高性能表如今哪些方面?

  • IO 线程模型:同步非阻塞,用最少的资源作更多的事。
  • 内存零拷贝:尽可能减小没必要要的内存拷贝,实现了更高效率的传输。
  • 内存池设计:申请的内存能够重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配状况。
  • 串形化处理读写:避免使用锁带来的性能开销。
  • 高性能序列化协议:支持 protobuf 等高性能序列化协议。

7.BIO、NIO和AIO的区别?

BIO:一个链接一个线程,客户端有链接请求时服务器端就须要启动一个线程进行处理。线程开销大。
伪异步IO:将请求链接放入线程池,一对多,但线程仍是很宝贵的资源。

NIO:一个请求一个线程,但客户端发送的链接请求都会注册到多路复用器上,多路复用器轮询到链接有I/O请求时才启动一个线程进行处理。

AIO:一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,

BIO是面向流的,NIO是面向缓冲区的;BIO的各类流是阻塞的。而NIO是非阻塞的;BIO的Stream是单向的,而NIO的channel是双向的。

NIO的特色:事件驱动模型、单线程处理多任务、非阻塞I/O,I/O读写再也不阻塞,而是返回0、基于block的传输比基于流的传输更高效、更高级的IO函数zero-copy、IO多路复用大大提升了Java网络应用的可伸缩性和实用性。基于Reactor线程模型。

在Reactor模式中,事件分发器等待某个事件或者可应用或个操做的状态发生,事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来作实际的读写操做。如在Reactor中实现读:注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操做,处理读到的数据,注册新的事件,而后返还控制权。

8.NIO的组成?

Buffer:与Channel进行交互,数据是从Channel读入缓冲区,从缓冲区写入Channel中的

flip方法 : 反转此缓冲区,将position给limit,而后将position置为0,其实就是切换读写模式

clear方法 :清除此缓冲区,将position置为0,把capacity的值给limit。

rewind方法 : 重绕此缓冲区,将position置为0

DirectByteBuffer可减小一次系统空间到用户空间的拷贝。但Buffer建立和销毁的成本更高,不可控,一般会用内存池来提升性能。直接缓冲区主要分配给那些易受基础系统的本机I/O 操做影响的大型、持久的缓冲区。若是数据量比较小的中小应用状况下,能够考虑使用heapBuffer,由JVM进行管理。

Channel:表示 IO 源与目标打开的链接,是双向的,但不能直接访问数据,只能与Buffer 进行交互。经过源码可知,FileChannel的read方法和write方法都致使数据复制了两次!

Selector可以使一个单独的线程管理多个Channel,open方法可建立Selector,register方法向多路复用器器注册通道,能够监听的事件类型:读、写、链接、accept。注册事件后会产生一个SelectionKey:它表示SelectableChannel 和Selector 之间的注册关系,wakeup方法:使还没有返回的第一个选择操做当即返回,唤醒的

缘由是:注册了新的channel或者事件;channel关闭,取消注册;优先级更高的事件触发(如定时器事件),但愿及时处理。

Selector在Linux的实现类是EPollSelectorImpl,委托给EPollArrayWrapper实现,其中三个native方法是对epoll的封装,而EPollSelectorImpl. implRegister方法,经过调用epoll_ctl向epoll实例中注册事件,还将注册的文件描述符(fd)与SelectionKey的对应关系添加到fdToKey中,这个map维护了文件描述符与SelectionKey的映射。

fdToKey有时会变得很是大,由于注册到Selector上的Channel很是多(百万链接);过时或失效的Channel没有及时关闭。fdToKey老是串行读取的,而读取是在select方法中进行的,该方法是非线程安全的。

Pipe:两个线程之间的单向数据链接,数据会被写到sink通道,从source通道读取

NIO的服务端创建过程:Selector.open():打开一个Selector;ServerSocketChannel.open():建立服务端的Channel;bind():绑定到某个端口上。并配置非阻塞模式;register():注册Channel和关注的事件到Selector上;select()轮询拿到已经就绪的事件

9 JDK原生NIO程序的问题

JDK原生也有一套网络应用程序API,可是存在一系列问题,主要以下:

  • NIO的类库和API繁杂,使用麻烦,你须要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
  • 须要具有其它的额外技能作铺垫,例如熟悉Java多线程编程,由于NIO编程涉及到Reactor模式,你必须对多线程和网路编程很是熟悉,才能编写出高质量的NIO程序
  • 可靠性能力补齐,开发工做量和难度都很是大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特色是功能开发相对容易,可是可靠性能力补齐工做量和难度都很是大
  • JDK NIO的BUG,例如臭名昭著的epoll bug,它会致使Selector空轮询,最终致使CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,可是直到JDK1.7版本该问题仍旧存在,只不过该bug发生几率下降了一些而已,它并无被根本解决

Netty 进阶 题

Netty高性能设计

Netty做为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据

I/O模型

用什么样的通道将数据发送给对方,BIO、NIO或者AIO,I/O模型在很大程度上决定了框架的性能

阻塞I/O

传统阻塞型I/O(BIO)能够用下图表示:

Blocking I/O

特色

  • 每一个请求都须要独立的线程完成数据read,业务处理,数据write的完整操做

问题

  • 当并发数较大时,须要建立大量线程来处理链接,系统资源占用较大
  • 链接创建后,若是当前线程暂时没有数据可读,则线程就阻塞在read操做上,形成线程资源浪费

I/O复用模型

img

在I/O复用模型中,会用到select,这个函数也会使进程阻塞,可是和阻塞I/O所不一样的的,这两个函数能够同时阻塞多个I/O操做,并且能够同时对多个读操做,多个写操做的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操做函数

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

Nonblocking I/O

Netty的IO线程NioEventLoop因为聚合了多路复用器Selector,能够同时并发处理成百上千个客户端链接。当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程能够进行其余任务。线程一般将非阻塞 IO 的空闲时间用于在其余通道上执行 IO 操做,因此单独的线程能够管理多个输入和输出通道。

因为读写操做都是非阻塞的,这就能够充分提高IO线程的运行效率,避免因为频繁I/O阻塞致使的线程挂起,一个I/O线程能够并发处理N个客户端链接和读写操做,这从根本上解决了传统同步阻塞I/O一链接一线程模型,架构的性能、弹性伸缩能力和可靠性都获得了极大的提高。

基于buffer

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

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

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

线程模型

数据报如何读取?读取以后的编解码在哪一个线程进行,编解码后的消息如何派发,线程模型的不一样,对性能的影响也很是大。

事件驱动模型

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

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

以GUI的逻辑处理为例,说明两种逻辑的不一样:

  • 轮询方式 线程不断轮询是否发生按钮点击事件,若是发生,调用处理逻辑
  • 事件驱动方式 发生点击事件把事件放入事件队列,在另外线程消费的事件列表中的事件,根据事件类型调用相关事件处理逻辑

这里借用O’Reilly 大神关于事件驱动模型解释图

事件驱动模型

主要包括4个基本组件:

  • 事件队列(event queue):接收事件的入口,存储待处理事件
  • 分发器(event mediator):将不一样的事件分发到不一样的业务逻辑单元
  • 事件通道(event channel):分发器与处理器之间的联系渠道
  • 事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操做

能够看出,相对传统轮询模式,事件驱动有以下优势:

  • 可扩展性好,分布式的异步架构,事件处理器之间高度解耦,能够方便扩展事件处理逻辑
  • 高性能,基于队列暂存事件,能方便并行异步处理事件

Reactor线程模型

Reactor是反应堆的意思,Reactor模型,是指经过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。

Reactor模型中有2个关键组成:

  • Reactor Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件作出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
  • Handlers 处理程序执行I/O事件要完成的实际事件,相似于客户想要与之交谈的公司中的实际官员。Reactor经过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操做

Reactor模型

取决于Reactor的数量和Hanndler线程数量的不一样,Reactor模型有3个变种

  • 单Reactor单线程
  • 单Reactor多线程
  • 主从Reactor多线程

能够这样理解,Reactor就是一个执行while (true) { selector.select(); …}循环的线程,会源源不断的产生新的事件,称做反应堆很贴切。

篇幅关系,这里再也不具体展开Reactor特性、优缺点比较,有兴趣的读者能够参考我以前另一篇文章:《理解高性能网络模型》

Netty线程模型

Netty主要基于主从Reactors多线程模型(以下图)作了必定的修改,其中主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor:

  • MainReactor负责客户端的链接请求,并将请求转交给SubReactor
  • SubReactor负责相应通道的IO读写请求
  • 非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理

这里引用Doug Lee大神的Reactor介绍:Scalable IO in Java里面关于主从Reactor多线程模型的图

主从Rreactor多线程模型

特别说明的是: 虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构,可是实际实现上,SubReactor和Worker线程在同一个线程池中:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)

123456

上面代码中的bossGroup 和workerGroup是Bootstrap构造方法中传入的两个对象,这两个group均是线程池

  • bossGroup线程池则只是在bind某个端口后,得到其中一个线程做为MainReactor,专门处理端口的accept事件,每一个端口对应一个boss线程
  • workerGroup线程池会被各个SubReactor和worker线程充分利用

异步处理

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能马上获得结果。实际处理这个调用的部件在完成后,经过状态、通知和回调来通知调用者。

Netty中的I/O操做是异步的,包括bind、write、connect等操做会简单的返回一个ChannelFuture,调用者并不能马上得到结果,经过Future-Listener机制,用户能够方便的主动获取或者经过通知机制得到IO操做结果。

当future对象刚刚建立时,处于非完成状态,调用者能够经过返回的ChannelFuture来获取操做执行的状态,注册监听函数来执行完成后的操,常见有以下操做:

  • 经过isDone方法来判断当前操做是否完成
  • 经过isSuccess方法来判断已完成的当前操做是否成功
  • 经过getCause方法来获取已完成的当前操做失败的缘由
  • 经过isCancelled方法来判断已完成的当前操做是否被取消
  • 经过addListener方法来注册监听器,当操做已完成(isDone方法返回完成),将会通知指定的监听器;若是future对象已完成,则理解通知指定的监听器

例以下面的的代码中绑定端口是异步操做,当绑定操做处理完,将会调用相应的监听器处理逻辑

serverBootstrap.bind(port).addListener(future -> {
        if (future.isSuccess()) {
            System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
        } else {
            System.err.println("端口[" + port + "]绑定失败!");
        }
    });

12345678

相比传统阻塞I/O,执行I/O操做后线程会被阻塞住, 直到操做完成;异步处理的好处是不会形成线程阻塞,线程在I/O操做期间能够执行别的程序,在高并发情形下会更稳定和更高的吞吐量。

Netty架构设计

前面介绍完Netty相关一些理论介绍,下面从功能特性、模块组件、运做过程来介绍Netty的架构设计

功能特性

Netty功能特性图

  • 传输服务 支持BIO和NIO
  • 容器集成 支持OSGI、JBossMC、Spring、Guice容器
  • 协议支持 HTTP、Protobuf、二进制、文本、WebSocket等一系列常见协议都支持。 还支持经过实行编码解码逻辑来实现自定义协议
  • Core核心 可扩展事件模型、通用通讯API、支持零拷贝的ByteBuf缓冲对象

模块组件

Bootstrap、ServerBootstrap

Bootstrap意思是引导,一个Netty应用一般由一个Bootstrap开始,主要做用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。

Future、ChannelFuture

正如前面介绍,在Netty中全部的IO操做都是异步的,不能马上得知消息是否被正确处理,可是能够过一会等它执行完成或者直接注册一个监听,具体的实现就是经过Future和ChannelFutures,他们能够注册一个监听,当操做执行成功或失败时监听会自动触发注册的监听事件。

Channel

Netty网络通讯的组件,可以用于执行网络I/O操做。 Channel为用户提供:

  • 当前网络链接的通道的状态(例如是否打开?是否已链接?)
  • 网络链接的配置参数 (例如接收缓冲区大小)
  • 提供异步的网络I/O操做(如创建链接,读写,绑定端口),异步调用意味着任何I / O调用都将当即返回,而且不保证在调用结束时所请求的I / O操做已完成。调用当即返回一个ChannelFuture实例,经过注册监听器到ChannelFuture上,能够I / O操做成功、失败或取消时回调通知调用方。
  • 支持关联I/O操做与对应的处理程序

不一样协议、不一样的阻塞类型的链接都有不一样的 Channel 类型与之对应,下面是一些经常使用的 Channel 类型

  • NioSocketChannel,异步的客户端 TCP Socket 链接
  • NioServerSocketChannel,异步的服务器端 TCP Socket 链接
  • NioDatagramChannel,异步的 UDP 链接
  • NioSctpChannel,异步的客户端 Sctp 链接
  • NioSctpServerChannel,异步的 Sctp 服务器端链接 这些通道涵盖了 UDP 和 TCP网络 IO以及文件 IO.

Selector

Netty基于Selector对象实现I/O多路复用,经过 Selector, 一个线程能够监听多个链接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就能够自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络链接完成等),这样程序就能够很简单地使用一个线程高效地管理多个 Channel 。

NioEventLoop

NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:

  • I/O任务 即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。
  • 非IO任务 添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。

两种任务的执行时间比由变量ioRatio控制,默认为50,则表示容许非IO任务执行的时间与IO任务的执行时间相等。

NioEventLoopGroup

NioEventLoopGroup,主要管理eventLoop的生命周期,能够理解为一个线程池,内部维护了一组线程,每一个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。

ChannelHandler

ChannelHandler是一个接口,处理I / O事件或拦截I / O操做,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。

ChannelHandler自己并无提供不少方法,由于这个接口有许多的方法须要实现,方便使用期间,能够继承它的子类:

  • ChannelInboundHandler用于处理入站I / O事件
  • ChannelOutboundHandler用于处理出站I / O操做

或者使用如下适配器类:

  • ChannelInboundHandlerAdapter用于处理入站I / O事件
  • ChannelOutboundHandlerAdapter用于处理出站I / O操做
  • ChannelDuplexHandler用于处理入站和出站事件

ChannelHandlerContext

保存Channel相关的全部上下文信息,同时关联一个ChannelHandler对象

ChannelPipline

保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操做。 ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户能够彻底控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。

下图引用Netty的Javadoc4.1中ChannelPipline的说明,描述了ChannelPipeline中ChannelHandler一般如何处理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler处理,并经过调用ChannelHandlerContext中定义的事件传播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))转发到其最近的处理程序。

I/O Request
                                            via Channel or
                                        ChannelHandlerContext
                                                      |
  +---------------------------------------------------+---------------+
  |                           ChannelPipeline         |               |
  |                                                  \|/              |
  |    +---------------------+            +-----------+----------+    |
  |    | Inbound Handler  N  |            | Outbound Handler  1  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  |               |                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler N-1 |            | Outbound Handler  2  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  .               |
  |               .                                   .               |
  | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
  |        [ method call]                       [method call]         |
  |               .                                   .               |
  |               .                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler  2  |            | Outbound Handler M-1 |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  |               |                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler  1  |            | Outbound Handler  M  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  +---------------+-----------------------------------+---------------+
                  |                                  \|/
  +---------------+-----------------------------------+---------------+
  |               |                                   |               |
  |       [ Socket.read() ]                    [ Socket.write() ]     |
  |                                                                   |
  |  Netty Internal I/O Threads (Transport Implementation)            |
  +-------------------------------------------------------------------+

123456789101112131415161718192021222324252627282930313233343536373839

入站事件由自下而上方向的入站处理程序处理,如图左侧所示。 入站Handler处理程序一般处理由图底部的I / O线程生成的入站数据。 一般经过实际输入操做(例如SocketChannel.read(ByteBuffer))从远程读取入站数据。

出站事件由上下方向处理,如图右侧所示。 出站Handler处理程序一般会生成或转换出站传输,例如write请求。 I/O线程一般执行实际的输出操做,例如SocketChannel.write(ByteBuffer)。

在 Netty 中每一个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系以下:

img

一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 而且每一个 ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站事件在一个双向链表中,入站事件会从链表head日后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。

工做原理架构

初始化并启动Netty服务端过程以下:

public static void main(String[] args) {
        // 建立mainReactor
        NioEventLoopGroup boosGroup = new NioEventLoopGroup();
        // 建立工做线程组
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        final ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap 
                 // 组装NioEventLoopGroup 
                .group(boosGroup, workerGroup)
                 // 设置channel类型为NIO类型
                .channel(NioServerSocketChannel.class)
                // 设置链接配置参数
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                // 配置入站、出站事件handler
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) {
                        // 配置入站、出站事件channel
                        ch.pipeline().addLast(...);
                        ch.pipeline().addLast(...);
                    }
    });

        // 绑定端口
        int port = 8080;
        serverBootstrap.bind(port).addListener(future -> {
            if (future.isSuccess()) {
                System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
            } else {
                System.err.println("端口[" + port + "]绑定失败!");
            }
        });
}

12345678910111213141516171819202122232425262728293031323334353637
  • 基本过程以下:
  • 1 初始化建立2个NioEventLoopGroup,其中boosGroup用于Accetpt链接创建事件并分发请求, workerGroup用于处理I/O读写事件和业务逻辑
  • 2 基于ServerBootstrap(服务端启动引导类),配置EventLoopGroup、Channel类型,链接参数、配置入站、出站事件handler
  • 3 绑定端口,开始工做

结合上面的介绍的Netty Reactor模型,介绍服务端Netty的工做架构图:

服务端Netty Reactor工做架构图

server端包含1个Boss NioEventLoopGroup和1个Worker NioEventLoopGroup,NioEventLoopGroup至关于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每一个NioEventLoop包含1个selector和1个事件循环线程。

每一个Boss NioEventLoop循环执行的任务包含3步:

  • 1 轮询accept事件
  • 2 处理accept I/O事件,与Client创建链接,生成NioSocketChannel,并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上 *3 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务。

每一个Worker NioEventLoop循环执行的任务包含3步:

  • 1 轮询read、write事件;
  • 2 处I/O事件,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理
  • 3 处理任务队列中的任务,runAllTasks。

其中任务队列中的task有3种典型使用场景

  • 1 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        //...
    }
});

1234567
  • 2 非当前reactor线程调用channel的各类方法 例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,而后调用write类方法向该用户推送消息,就会进入到这种场景。最终的write会提交到任务队列中后被异步消费。
  • 3 用户自定义定时任务
ctx.channel().eventLoop().schedule(new Runnable() {
    @Override
    public void run() {

    }
}, 60, TimeUnit.SECONDS);

1234567

Netty 高级 题

8.Netty的线程模型?

Netty经过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理。

单线程模型:全部I/O操做都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的链接请求,向服务端发起链接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路,性能上没法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。

多线程模型:有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的TCP 链接请求;NIO 线程池负责网络IO 的操做,即消息的读取、解码、编码和发送;1 个NIO 线程能够同时处理N 条链路,可是1 个链路只对应1 个NIO 线程,这是为了防止发生并发操做问题。但在并发百万客户端链接或须要安全认证时,一个Acceptor 线程可能会存在性能不足问题。

主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端链接,将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,从新注册到Sub 线程池的线程上,用于处理I/O 的读写等操做,从而保证mainReactor只负责接入认证、握手等操做;

9.TCP 粘包/拆包的缘由及解决方法?

TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分红多个包进行发送,也可能把小的封装成一个大的数据包发送。

TCP粘包/分包的缘由:

应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用屡次写入的数据发送到网络上,这将会发生粘包现象;

进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。

解决方法

消息定长:FixedLengthFrameDecoder类

包尾增长特殊字符分割:

  • 行分隔符类:LineBasedFrameDecoder
  • 或自定义分隔符类 :DelimiterBasedFrameDecoder

将消息分为消息头和消息体:LengthFieldBasedFrameDecoder类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。

10.什么是 Netty 的零拷贝?

Netty 的零拷贝主要包含三个方面:

  • Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不须要进行字节缓冲区的二次拷贝。若是使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,而后才写入 Socket 中。相比于堆外直接内存,消息在发送过程当中多了一次缓冲区的内存拷贝。
  • Netty 提供了组合 Buffer 对象,能够聚合多个 ByteBuffer 对象,用户能够像操做一个 Buffer 那样方便的对组合 Buffer 进行操做,避免了传统经过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。
  • Netty 的文件传输采用了 transferTo 方法,它能够直接将文件缓冲区的数据发送到目标 Channel,避免了传统经过循环 write 方式致使的内存拷贝问题。

11.Netty 中有哪一种重要组件?

  • Channel:Netty 网络操做抽象类,它除了包括基本的 I/O 操做,如 bind、connect、read、write 等。
  • EventLoop:主要是配合 Channel 处理 I/O 操做,用来处理链接的生命周期中所发生的事情。
  • ChannelFuture:Netty 框架中全部的 I/O 操做都为异步的,所以咱们须要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操做执行成功或者失败时,监听就会自动触发返回结果。
  • ChannelHandler:充当了全部处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各类事件,这里的事件很普遍,好比能够是链接、数据接收、异常、数据转换等。
  • ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 建立时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。

12.Netty 发送消息有几种方式?

Netty 有两种发送消息的方式:

  • 直接写入 Channel 中,消息从 ChannelPipeline 当中尾部开始移动;
  • 写入和 ChannelHandler 绑定的 ChannelHandlerContext 中,消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动。

13.默认状况 Netty 起多少线程?什么时候启动?

Netty 默认是 CPU 处理器数的两倍,bind 完以后启动。

14.了解哪几种序列化协议?

序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。

影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能(CPU资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。

Java默认提供的序列化:没法跨语言、序列化后的码流太大、序列化的性能差

XML,优势:人机可读性好,可指定元素或特性的名称。缺点:序列化数据只包含数据自己以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。适用场景:当作配置文件存储数据,实时数据转换。

JSON,是一种轻量级的数据交换格式,优势:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与XML相比,其协议比较简单,解析速度比较快。缺点:数据的描述性比XML差、不适合性能要求为ms级别的状况、额外空间开销比较大。适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。

Fastjson,采用一种“假定有序快速匹配”的算法。优势:接口简单易用、目前java语言中最快的json库。缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。适用场景:协议交互、Web输出、Android客户端

Thrift,不只是序列化协议,仍是一个RPC框架。优势:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具备较强的兼容性、支持二进制压缩编码。缺点:使用者较少、跨防火墙访问时,不安全、不具备可读性,调试代码时相对困难、不能与其余传输层协议共同使用(例如HTTP)、没法支持向持久层直接读写数据,即不适合作数据持久化序列化协议。适用场景:分布式系统的RPC解决方案

Avro,Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题。优势:支持丰富的数据类型、简单的动态语言结合功能、具备自我描述属性、提升了数据解析速度、快速可压缩的二进制数据形式、能够实现远程过程调用RPC、支持跨编程语言实现。缺点:对于习惯于静态类型语言的用户不直观。适用场景:在Hadoop中作Hive、Pig和MapReduce的持久化数据格式。

Protobuf,将数据结构以.proto文件进行描述,经过代码生成工具能够生成对应数据结构的POJO对象和Protobuf相关的方法和属性。优势:序列化后码流小,性能高、结构化数据存储格式(XML JSON等)、经过标识字段的顺序,能够实现协议的前向兼容、结构化的文档更容易管理和维护。缺点:须要依赖于工具生成代码、支持的语言相对较少,官方只支持Java 、C++ 、python。适用场景:对性能要求高的RPC调用、具备良好的跨防火墙的访问属性、适合应用层对象的持久化

其它

protostuff 基于protobuf协议,但不须要配置proto文件,直接导包便可
Jboss marshaling 能够直接序列化java类, 无须实java.io.Serializable接口
Message pack 一个高效的二进制序列化格式
Hessian 采用二进制协议的轻量级remoting onhttp工具
kryo 基于protobuf协议,只支持java语言,须要注册(Registration),而后序列化(Output),反序列化(Input)

15.如何选择序列化协议?

具体场景

对于公司间的系统调用,若是性能要求在100ms以上的服务,基于XML的SOAP协议是一个值得考虑的方案。
基于Web browser的Ajax,以及Mobile app与服务端之间的通信,JSON协议是首选。对于性能要求不过高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON也是很是不错的选择。
对于调试环境比较恶劣的场景,采用JSON或XML可以极大的提升调试效率,下降系统开发成本。
当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro之间具备必定的竞争关系。
对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择。若是持久化后的数据存储在hadoop子项目里,Avro会是更好的选择。

对于持久层非Hadoop项目,以静态类型语言为主的应用场景,Protobuf会更符合静态类型语言工程师的开发习惯。因为Avro的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro是更好的选择。
若是须要提供一个完整的RPC解决方案,Thrift是一个好的选择。
若是序列化以后须要支持不一样的传输层协议,或者须要跨防火墙访问的高性能场景,Protobuf能够优先考虑。
protobuf的数据类型有多种:bool、double、float、int3二、int6四、string、bytes、enum、message。protobuf的限定符:required: 必须赋值,不能为空、optional:字段能够赋值,也能够不赋值、repeated: 该字段能够重复任意次数(包括0次)、枚举;只能用指定的常量集中的一个值做为其值;

protobuf的基本规则:每一个消息中必须至少留有一个required类型的字段、包含0个或多个optional类型的字段;repeated表示的字段能够包含0个或多个数据;[1,15]以内的标识号在编码的时候会占用一个字节(经常使用),[16,2047]以内的标识号则占用2个字节,标识号必定不能重复、使用消息类型,也能够将消息嵌套任意多层,可用嵌套消息类型来代替组。

protobuf的消息升级原则:不要更改任何已有的字段的数值标识;不能移除已经存在的required字段,optional和repeated类型的字段能够被移除,但要保留标号不能被重用。新添加的字段必须是optional或repeated。由于旧版本程序没法读取或写入新增的required限定符的字段。

编译器为每个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来建立消息类接口的)。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();

Netty中的使用:ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类;ProtobufDecoder(UserProto.User.getDefaultInstance())这是建立的UserProto.java文件中的解码类;ProtobufVarint32LengthFieldPrepender 对protobuf协议的消息头上加上一个长度为32的整形字段,用于标志这个消息的长度的类;ProtobufEncoder 是编码类

将StringBuilder转换为ByteBuf类型:copiedBuffer()方法

16.Netty 支持哪些心跳类型设置?

  • readerIdleTime:为读超时时间(即测试端必定时间内未接受到被测试端消息)。
  • writerIdleTime:为写超时时间(即测试端必定时间内向被测试端发送消息)。
  • allIdleTime:全部类型的超时时间。

17.Netty 和 Tomcat 的区别?

  • 做用不一样:Tomcat 是 Servlet 容器,能够视为 Web 服务器,而 Netty 是异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。
  • 协议不一样:Tomcat 是基于 http 协议的 Web 服务器,而 Netty 能经过编程自定义各类协议,由于 Netty 自己本身能编码/解码字节流,全部 Netty 能够实现,HTTP 服务器、FTP 服务器、UDP 服务器、RPC 服务器、WebSocket 服务器、Redis 的 Proxy 服务器、MySQL 的 Proxy 服务器等等。

18.NIOEventLoopGroup源码?

NioEventLoopGroup(实际上是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children [], 默认大小是处理器核数 * 2, 这样就构成了一个线程池,初始化EventExecutor时NioEventLoopGroup重载newChild方法,因此children元素的实际类型为NioEventLoop。

线程启动时调用SingleThreadEventExecutor的构造方法,执行NioEventLoop类的run方法,首先会调用hasTasks()方法判断当前taskQueue是否有元素。若是taskQueue中有元素,执行 selectNow() 方法,最终执行selector.selectNow(),该方法会当即返回。若是taskQueue没有元素,执行 select(oldWakenUp) 方法

select ( oldWakenUp) 方法解决了 Nio 中的 bug,selectCnt 用来记录selector.select方法的执行次数和标识是否执行过selector.selectNow(),若触发了epoll的空轮询bug,则会反复执行selector.select(timeoutMillis),变量selectCnt 会逐渐变大,当selectCnt 达到阈值(默认512),则执行rebuildSelector方法,进行selector重建,解决cpu占用100%的bug。

rebuildSelector方法先经过openSelector方法建立一个新的selector。而后将old selector的selectionKey执行cancel。最后将old selector的channel从新注册到新的selector中。rebuild后,须要从新执行方法selectNow,检查是否有已ready的selectionKey。

接下来调用processSelectedKeys 方法(处理I/O任务),当selectedKeys != null时,调用processSelectedKeysOptimized方法,迭代 selectedKeys 获取就绪的 IO 事件的selectkey存放在数组selectedKeys中, 而后为每一个事件都调用 processSelectedKey 来处理它,processSelectedKey 中分别处理OP_READ;OP_WRITE;OP_CONNECT事件。

最后调用runAllTasks方法(非IO任务),该方法首先会调用fetchFromScheduledTaskQueue方法,把scheduledTaskQueue中已经超过延迟执行时间的任务移到taskQueue中等待被执行,而后依次从taskQueue中取任务执行,每执行64个任务,进行耗时检查,若是已执行时间超过预先设定的执行时间,则中止执行非IO任务,避免非IO任务太多,影响IO任务的执行。

每一个NioEventLoop对应一个线程和一个Selector,NioServerSocketChannel会主动注册到某一个NioEventLoop的Selector上,NioEventLoop负责事件轮询。

Outbound 事件都是请求事件, 发起者是 Channel,处理者是 unsafe,经过 Outbound 事件进行通知,传播方向是 tail到head。Inbound 事件发起者是 unsafe,事件的处理者是 Channel, 是通知事件,传播方向是从头至尾。

内存管理机制,首先会预申请一大块内存Arena,Arena由许多Chunk组成,而每一个Chunk默认由2048个page组成。Chunk经过AVL树的形式组织Page,每一个叶子节点表示一个Page,而中间节点表示内存区域,节点本身记录它在整个Arena中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点如下的全部节点都已被分配了。大于8k的内存分配在poolChunkList中,而PoolSubpage用于分配小于8k的内存,它会把一个page分割成多段,进行内存分配。

ByteBuf的特色:支持自动扩容(4M),保证put方法不会抛出异常、经过内置的复合缓冲类型,实现零拷贝(zero-copy);不须要调用flip()来切换读/写模式,读取和写入索引分开;方法链;引用计数基于AtomicIntegerFieldUpdater用于内存回收;PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。

Netty 使用总结

如今稳定推荐使用的主流版本仍是Netty4,Netty5 中使用了 ForkJoinPool,增长了代码的复杂度,可是对性能的改善却不明显,因此这个版本不推荐使用,官网也没有提供下载连接。

Netty 入门门槛相对较高,实际上是由于这方面的资料较少,并非由于他有多难,你们其实均可以像搞透 Spring 同样搞透 Netty。在学习以前,建议先理解透整个框架原理结构,运行过程,能够少走不少弯路。

疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备


回到◀疯狂创客圈

疯狂创客圈 - Java高并发研习社群,为你们开启大厂之门

相关文章
相关标签/搜索