首页 > 技术文章 > Java面试——Netty

zhengzhaoxiang 2020-11-19 17:07 原文

一、BIO、NIO 和 AIO


【1】阻塞 IO(Blocking I/O):同步阻塞I/O模式,当一条线程执行 read() 或者 write() 方法时,这条线程会一直阻塞直到读取一些数据或者写出去的数据已经全部写出,在这期间这条线程不能做任何其他的事情。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。但是,当面对十万甚至百万级连接的时候,传统的 BIO模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

【2】非阻塞 NIO(New I/O):NIO是一种同步非阻塞的 I/O模型,NIO 与原有的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区、基于通道的操作。NIO 将以更加高效的方式进行文件读写操作。JAVA NIO的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO系统,需要获取用于连接 IO设备的通道以及用于容纳数据的缓冲区。对数据进行处理。阻塞IO 会一直等待,所以非阻塞IO 是用来解决 IO线程与 Socket 之间的解耦问题【通过引入事件机制】,如果 Socket 发送缓冲区可写的话会通知 IO线程进行 write,如果 Socket 的接收缓冲区可读的话会通知 IO线程进行 read。NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel。对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

【3】I/O 复用模型:非阻塞 IO中引入的机制就是 IO多路复用模型,Linux 提供了 epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描(select/poll),因此性能更高。当有fd 就绪时,立即回调函数 rollback。IO多路复用技术底层就是通过 epoll 函数实现的。

【4】AIO (Asynchronous I/O):AIO 也就是 NIO2。在 Java 7 中引入了 NIO 的改进版 NIO2,它是异步非阻塞的IO模型。异步 IO 是基于事件回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。

二、Netty 的各大组件


【1】Channel:Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bindconnectreadwrite 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channel 的 EventLoop。在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而 Netty 的 Channel 则提供的一系列的 API ,它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势: ①、在 Channel 接口层,采用  Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。②、Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。③、具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,有 Channel 统一负责和调度,功能实现更加灵活。

【2】EventLoop:Netty 基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。Channel 为 Netty 网络操作抽象类,EventLoop 主要是为 Channel 处理 I/O 操作,两者配合参与 I/O 操作。ChannelEventLoopThreadEventLoopGroup之间的关系:
  ①、一个 EventLoopGroup 包含一个或多个 EventLoop
  ②、一个 EventLoop 在它的生命周期内只能与一个 Thread绑定。
  ③、所有 EnventLoop 处理的 I/O事件都将在它专有的 Thread 上被处理
  ④、一个 Channel 在它的生命周期内只能注册于一个 EventLoop
  ⑤、一个 EventLoop 可被分配至一个或多个 Channel
当一个连接到达时,Netty 就会注册一个 Channel,然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上,在该 Channel的整个生命周期中都是有这个绑定的 EventLoop 来服务的。

【3】ChannelFuture:Netty 为异步非阻塞,即所有的 I/O 操作都为异步的,因此,我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口,通过该接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

【4】ChannelHandler:ChannelHandler 为 Netty 中最核心的组件,它充当了所有处理入站出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接数据接收异常数据转换等。ChannelHandler 有两个核心子类 ChannelInboundHandler ChannelOutboundHandler,其中 ChannelInboundHandler 用于接收、处理入站数据和事件,而 ChannelOutboundHandler 则相反。

【5】ChannelPipeline:ChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。一个数据或者事件可能会被多个 Handler 处理,在这个过程中,数据或者事件经流 ChannelPipeline,由 ChannelHandler 处理。在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler。当一个数据流进入 ChannlePipeline 时,它会从 ChannelPipeline 头部开始传给第一个 ChannelInboundHandler ,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部的 “最后” 一个ChannelOutboundHandler,当它处理完成后会传递给前一个 ChannelOutboundHandler 。当 ChannelHandler 被添加到 ChannelPipeline 时,它将会被分配一个 ChannelHandlerContext它代表了 ChannelHandler 和 ChannelPipeline 之间的绑定。其中 ChannelHandler 添加到 ChannelPipeline 过程如下:
①、一个 ChannelInitializer 的实现被注册到了 ServerBootStrap 中;
②、当 ChannelInitializer.initChannel() 方法被调用时,ChannelInitializer 将在 ChannelPipeline 中安装一组自定义的ChannelHandler;
③、ChannelInitializer 将它自己从 ChannelPipeline 中移除;

三、Netty 的线程模型


【博客连接】:链接

四、TCP 粘包/拆包的原因及解决方法


【博客连接】:链接

五、了解哪几种序列化协议?包括使用场景和如何去选择


【博客连接:序列化的缺点】:链接
【博客连接:编辑码框架 Protobuf】:链接
【博客连接:编辑码框架 Thrift】:链接

六、Netty 的零拷贝实现


【博客连接】:链接 

七、Netty 的高性能分析


【1】传输:IO 模型在很大程度上决定了框架的性能,相比于BIO,Netty建议采用异步通信模式,因为 NIO一个线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO "一连接一线程模型",架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。正如代码中所示,使用的是 NioEventLoopGroup 和 NioSocketChannel 来提升传输效率。
【2】协议:采用什么样的通信协议,对系统的性能极其重要,Netty 默认提供了对 Google Protobuf 的支持,也可以通过扩展Netty 的编解码接口,用户可以实现其它的高性能序列化框架。
【3】线程:Netty 使用了Reactor线程模型,但 Reactor 模型不同,对性能的影响也非常大,上面线程模型中就介绍常用的 Reactor 模型的三种情况。

八、基于BIO实现的Server端,当建立了100个连接时,会有多少个线程?如果基于NIO,又会是多少个线程? 为什么?


BIO由于不是 NIO那样的事件机制,在连接的 IO读取上,无论是否真的有读/写发生,都需要阻塞住当前的线程,对于基于 BIO实现的 Server端,通常的实现方法都是用一个线程去 accept连接,当连接建立后,将这个连接的 IO读写放到一个专门的处理线程,所以当建立100个连接时,通常会产生1个 Accept线程 + 100个处理线程

NIO通过事件来触发,这样就可以实现在有需要读/写的时候才处理,不用阻塞当前线程,NIO在处理 IO的读写时,当从网卡缓冲区读或写入缓冲区时,这个过程是串行的,所以用太多线程处理 IO事件其实也没什么意义,连接事件由于通常处理比较快,用1个线程去处理就可以,IO事件呢,通常会采用 cpu core数+1cpu core数 * 2,这个的原因是 IO线程通常除了从缓冲区读写外还会做些比较轻量的例如解析协议头等,这些是可以并发的,为什么不只用1个线程处理,是因为当并发的 IO事件非常多时,1个线程的效率不足以发挥出多核CPU 的能力,从而导致这个地方成为瓶颈,这种在分布式场景中比较明显,按照这个,也就更容易理解为什么在基于 Netty等写程序时,不要在 IO线程里直接做过多动作,而应该把这些动作转移到另外的线程池里去处理,就是为了能保持好 IO事件能被高效处理。从上面可以看出,对于大多数需要建立大量连接,但并发读写并不会同时产生的场景而言,NIO的优势是非常明显的。这种关于BIO、NIO的问法的变化空间是非常大的,还可以进一步拓展问问AIO和BIO、NIO的根本不同。

九、Netty 的心跳检测及重连机制怎么实现的


【博客连接】:链接

十、ChannelInboundHandlerAdapter 与 SimpleChannelInboundHandler 的区别


【博客连接】:链接


----关注公众号,获取更多内容----

推荐阅读