关于java:Netty面试题2021-最新版

45次阅读

共计 21300 个字符,预计需要花费 54 分钟才能阅读完成。

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 作为通信的根底。

5.Netty 高性能体现在哪些方面?

  • IO 线程模型:同步非阻塞,用起码的资源做更多的事。
  • 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
  • 内存池设计:申请的内存能够重用,次要指间接内存。外部实现是用一颗二叉查找树治理内存分配情况。
  • 串形化解决读写:防止应用锁带来的性能开销。
  • 高性能序列化协定:反对 protobuf 等高性能序列化协定。

6.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 中实现读:注册读就绪事件和相应的事件处理器、事件散发器期待事件、事件到来,激活散发器,散发器调用事件对应的处理器、事件处理器实现理论的读操作,解决读到的数据,注册新的事件,而后返还控制权。

7.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()轮询拿到曾经就绪的事件

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、int32、int64、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 简介

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

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 的对 JDK 自带的 NIO 的 API 进行封装,解决上述问题,次要特点有:

  • 设计优雅 实用于各种传输类型的对立 API – 阻塞和非阻塞 Socket 基于灵便且可扩大的事件模型,能够清晰地拆散关注点 高度可定制的线程模型 – 单线程,一个或多个线程池 真正的无连贯数据报套接字反对(自 3.1 起)
  • 使用方便 具体记录的 Javadoc,用户指南和示例 没有其余依赖项,JDK 5(Netty 3.x)或 6(Netty 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:Related projects

Netty 高性能设计

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

I/ O 模型

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

阻塞 I /O

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

特点

  • 每个申请都须要独立的线程实现数据 read,业务解决,数据 write 的残缺操作

问题

  • 当并发数较大时,须要创立大量线程来解决连贯,系统资源占用较大
  • 连贯建设后,如果以后线程临时没有数据可读,则线程就阻塞在 read 操作上,造成线程资源节约

I/ O 复用模型

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

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

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 的数量和 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 多线程模型的图

特地阐明的是:尽管 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 的架构设计

性能个性

  • 传输服务 反对 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 与之对应, 它们的组成关系如下:

一个 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 + "] 绑定失败!");
            }
        });
}
  • 根本过程如下:
  • 1 初始化创立 2 个 NioEventLoopGroup,其中 boosGroup 用于 Accetpt 连贯建设事件并散发申请,workerGroup 用于解决 I / O 读写事件和业务逻辑
  • 2 基于 ServerBootstrap(服务端启动疏导类),配置 EventLoopGroup、Channel 类型,连贯参数、配置入站、出站事件 handler
  • 3 绑定端口,开始工作

联合下面的介绍的 Netty Reactor 模型,介绍服务端 Netty 的工作架构图:

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

总结

当初稳固举荐应用的支流版本还是 Netty4,Netty5 中应用了 ForkJoinPool,减少了代码的复杂度,然而对性能的改善却不显著,所以这个版本不举荐应用,官网也没有提供下载链接。

Netty 入门门槛绝对较高,其实是因为这方面的材料较少,并不是因为他有多难,大家其实都能够像搞透 Spring 一样搞透 Netty。在学习之前,倡议先了解透整个框架原理构造,运行过程,能够少走很多弯路。

作者:thinkwon
起源:https://thinkwon.blog.csdn.ne…
本文首发于公众号:Java 版 web 我的项目,欢送关注获取更多精彩内容

正文完
 0