PancrasL的博客

Netty - 高性能网络应用框架

2021-06-16

img

1. 什么是Netty?

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。

2. Netty的特性

2.1 并发高

2.2 传输快

Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。
Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。

img

img

2.3 封装好

Netty是对Java NIO的封装,易于使用,性能好

3. Netty中的关键概念

3.1 Channel

Channel是对Socket的抽象,NioSocketChannel的底层是基于java.nio.SocketChannel实现的,NioServerSocketChannel的底层是基于Java.nio.ServerSocketChannel实现的

  • Channel,表示一个连接,可以理解为每一个请求,就是一个Channel。

  • ChannelHandler,核心处理业务就在这里,用于处理业务请求。

  • ChannelHandlerContext,用于传输业务数据。

  • ChannelPipeline,用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext。

他们的交互流程是:

  1. 事件传递给 ChannelPipeline 的第一个 ChannelHandler
  2. ChannelHandler 通过关联的 ChannelHandlerContext 传递事件给 ChannelPipeline 中的 下一个
  3. ChannelHandler 通过关联的 ChannelHandlerContext 传递事件给 ChannelPipeline 中的 下一个

img

NioEventLoopGroup可以理解为一个线程池,内部维护了一组线程,每个线程负责处理多个Channel上的事件,而一个Channel只对应于一个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
Server
*/
// 监听线程组,监听客户端请求
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 工作线程组,处理与客户端的数据通讯
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)

/**
Client
*/
ServerBootstrap bootstrap = new Bootstrap();
// 处理与服务端通信的线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
bootstrap.group(workerGroup)

3.2 Bootstrap

客户端和服务端的对象工厂,用来创建具体的Channel

3.3 EventLoop

任务执行者:

  • 每个Channel有且仅有一个EventLoop与之关联

3.4 Pipeline

每个Channel有且仅有一个ChannelPipeline与之对应

3.5 Future和ChannelFuture

3.5.1 Future

  • java.util.concurrent.Future:记录异步执行的状态,调用get()方法阻塞到任务完成
  • io.netty.util.concurrent.Future:扩展了Java的Future,实现了监听器(Listener)接口,可以通过监听器回调来处理任务执行结果

3.5.2 ChannelFuture

扩展了Netty的Future,表示一种没有返回值的异步调用,同时和一个Channel进行绑定。

3.4 ByteBuf

ByteBuf是一个存储字节的容器,最大特点就是使用方便,它既有自己的读索引和写索引,方便你对整段字节缓存进行读写,也支持get/set,方便你对其中每一个字节进行读写,他的数据结构如下图所示:

img

有三种使用模式:

  1. Heap Buffer 堆缓冲区
    堆缓冲区是ByteBuf最常用的模式,他将数据存储在堆空间。

  2. Direct Buffer 直接缓冲区

    直接缓冲区是ByteBuf的另外一种常用模式,他的内存分配都不发生在堆,jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存,这样做有两个好处

    • 通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
    • DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.
  3. Composite Buffer 复合缓冲区
    复合缓冲区相当于多个不同ByteBuf的视图,这是netty提供的,jdk不提供这样的功能。

3.5 Codec

Netty中的编码/解码器,通过它完成字节与pojo、pojo与pojo的相互转换,从而实现自定义协议。

4. Netty进阶

4.1 IO线程和业务线程分离

客户端调用服务,服务端的操作基本都是修改数据库数据或获取数据库数据。数据库的操作可以认为是比较耗时的,所以在Netty的I/O线程中不适合处理这些操作。

I/O线程:服务端Netty的I/O线程是处理客户端的连接和处理数据读写的(根据主从Reactor多线程模型,已经将网络读写和客户端进行连接分开),耗时的业务逻辑是不适合也在I/O线程中执行的。

业务线程:处理比较耗时的业务。

  • 方法一:在添加 pipeline 中的 handler 时候,添加一个Netty提供的线程池。
  • 方法二:在ChannelHandler的回调方法中,使用自己定义的业务线程池。

4.2 Netty和Reactor

4.2.1 单线程模型

Acceptor的处理和Handler的处理都在一个线程中

1
2
3
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup);// 底层调用了 server.group(bossGroup, bossGroup)

4.2.2 多线程模型

有一个专门的线程Acceptor用于监听客户端的TCP连接请求

客户端连接后的IO操作都有一个特定的NIO线程池负责

1
2
3
4
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup);

4.2.3 主从多线程模型

将Acceptor由一个线程改为一个线程池(例如需要在客户端连接时增加一些权限校验等操作的场景)

客户端连接后的IO操作都有一个特定的NIO线程池负责

1
2
3
4
EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup);

4.3 Netty和拆包、粘包

4.3.1 TCP拆包、粘包

严格来讲,TCP是不存在粘包拆包的,这里的粘包拆包是指应用层上的一个数据包会被合并为一个或分割为多个TCP数据包进行传输。

原因是TCP数据包有大小限制,因此业务上一个完整的包在发送时可能会被拆分为多个TCP包进行发送,此为拆包;也有可能把多个小的包封装成一个大的数据包发送,此为粘包

4.3.2 粘包问题的解决策略

底层的TCP无法理解上层业务数据,因此只能通过应用层协议来解决TCP拆包、粘包问题,其问题核心就是如何处理应用消息边界,主要解决思路就是把不同应用数据包用分隔符分隔开,具体实现有以下方法:

  1. 消息定长
  2. 数据包尾部添加特殊分隔符,例如换行符(FTP采用)
  3. 将消息分为消息头和消息体,消息头是定长的,且包含了消息体的长度
  4. 其他自定义方案,例如根据消息头类型来默认消息体长度

Netty的处理方案:每个Channel仅和一个Handler绑定,如果数据包不完整,Channel中的数据读取的时候可以对数据包进行保存,等下次解析时再对这个数据包进行组装解析,直到获取完整的数据包后再将数据包向下传递。

解决方案:

  • LineBasedFrameDecoder:行解码器
  • DelimiterBasedFrameDecoder:分隔符解码器
  • FixedLengthFrameDecoder:固定长度解码器
  • LengthFieldBasedFrameDecoder:length属性解码器

4.4 设计模式在Netty中的应用

// todo

5. Netty常见问题

// todo

Reference

[1] https://www.jianshu.com/p/b9f3f6a16911