关于Netty,总结一些知识点

111 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

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 完之后启动。