很多情况下,我们只是会使用市面上优秀的框架,例如:Spring全家桶、Dubbo、HttpClient、Netty等。但却不知道这些框架底层的设计思想,也不会去阅读框架的源码。在使用上遇到问题的时候,也只是胡乱百度。
最近在工作中,接触网络I/O比较多。背景:使用Socket编写客户端,发送TCP自定义协议。在做性能测试时,TPS指标始终不如人意。也尝试过使用Netty,但是在压测时,会出现OOM情况。既然Netty是市面上公认的优秀的IO框架,那么我带着这个问题,尝试性的打开Netty源码进行阅读,通过Debug方式,查看调用栈,一步一步排查出影响问题所在。
为何要深度去理解一款框架:
- 学习前辈的编码习性,命名规范,注释规范。
- 理解框架中代码的设置模式。
- 有助于在日常工作中,提升编码效率与质量。
本文以Netty的NIO进行举例讲解,在阅读本文前,需要对java-NIO和Netty有一定的使用经验与了解。
深度了解Netty,需要了解Netty各个模块下的组件,在运行中起到的作用
Netty分为五个模块,每个模块都负责相应的功能,模块之间也会存在依赖性。以下为Netty的模块
bootstrap
Netty服务启动项,启动客户端或服务端buffer
对java.nio.ByteBuffer
进行封装,提供更简洁的操作API,IO操作以及编解码都是通过buffer
模块进行读写channel
对java.nio.channels.Channel
进行封装,底层依然是java提供的Channel,在应用层开发接触的是Netty的Channelhandler
通道读写处理器,Netty已内置了几个,只需要编写编解码处理器util
工具包,为Netty服务提供工具类、日志处理、并发包执行器
模块以及模块下的组件
挑选重要的组件进行了解
util
包路径:io.netty.util.concurrent
异步操作
Future
Netty所有操作皆为异步的,例如:启动服务/发起连接/注册通道Promise
Future包装类,为Future操作结果赋值
并发处理
EventExecutor
通道事件的执行器,继承java.util.concurrent.ExecutorService
EventExecutorGroup
管理EventExecutor
channel 核心
包路径:io.netty.channel
通道
Channel
通道,表示一个连接或者服务端ChannelConfig
通道相关的配置
异步结果
ChannelFuture
继承util包下的Future
ChannelPromise
继承util包下的Promise
读取事件循环执行器
EventLoop
循环检查&执行通道的事件,事件类型:READ、WRITE、CONNECT、ACCEPTEventLoopGroup
管理EventLoop
通道IO回调处理器
ChannelPipeline
处理通道事件的链,将事件结果回调至ChannelHandler
,例如:绑定/连接/IO,责任链模式ChannelHandlerContext
当前通道的上下文处理器,调用handler回调方法ChannelHandler
通道事件回调处理器的顶级接口ChannelOutboundHandler
继承ChannelHandler
,处理通道读事件ChannelInboundHandler
继承ChannelHandler
,处理通道写事件
初始化
ChannelInitializer
继承ChannelInboundHandler
,处理通道初始化事件,实现了handlerAdded
NIO
NioEventLoop
继承EventLoop
处理NIO通道IO事件NioEventLoopGroup
继承EventLoopGroup
,管理NioEventLoop
NioSocketChannel
包装java.nio.channels.SelectableChannel
handler
包路径:io.netty.handler
编解码的核心两个基类,都继承ChannelHandler
,也就是都是回调处理器
ByteToMessageDecoder
读取通道字节,转换为对象。MessageToByteEncoder
将对象转换为字节,写入通道
buffer
包路径:io.netty.buffer
ByteBuf
buffer的顶级基类,提供了writeBytes(InputStream in, int length)
,writeBytes(ScatteringByteChannel in, int length)
,setBytes(int index, InputStream in, int length)
,setBytes(int index, ScatteringByteChannel in, int length)
对通道的消息进行读取ByteBufAllocator
分配buffer
bootstrap
包路径:io.netty.bootstrap
ServerBootstrap
服务端启动Bootstrap
客户端启动
模块-组件的UML类图(继承关系/依赖关系)
异步操作结果:Futrue,Promise
Netty中基本所有的操作都是异步的,既然是异步的,那么需要获取到操作结果,那就需要用到Futrue
。
操作项有:绑定端口(bind)、建立连接(connect)、关闭连接(disconnect)、关闭通道(close)、写数据(write)等…
大部分的操作都是返回
DefaultChannelPromise
,通过isSuccess
和cause
判断成功或失败。
事件执行器
通道事件执行器的顶级接口,定义事件执行器的抽象方法,且制定好模板方法,在模板方法中再调用子类实现的抽象方法。
具备有事件执行器组的理念,负责管理执行器。
EventExecutor
执行器的接口,该接口的实现类处理事件执行
EventExecutorGroup
管理EventExecutor
,类似于线程池,由他的实现类创建和获取EventExecutor实例。
通道操作&通道IO事件执行器
EventLoop
事件执行器的顶级接口,处理通道的IO事件EventLoopGroup
管理EventLoop
,提供创建和获取EventLoop
实例
以NIO
举例
NioEventLoopGroup
管理NioEventLoop
NioEventLoop
NIO通道的事件执行器
在启动Netty服务时传入的bossGroup
,workerGroup
就是EventLoopGroup
的实现类
new ServerBootstrap().group(bossGroup, workerGroup)
NIO 设计图
其它IO也是和这个设计一样,区别于通道事件的实现
- 创建
NioServerSocketChannel
实例,并通过BossGroup
注册至某个NioEventLoop
实例上。 - 每一个
NioEventLoop
实例都有各自的Selector
,接收到注册通道事件时,将其再注册至Selector
- 由
NioEventLoop
的线程轮询Selector
,查询IO事件,并处理事件。 - IO事件再由
ChannelPipeline
链式调用ChannelHandler
内部流程图
服务端:ServerBootstrap
继承AbstractBootstrap
,因为启动客户端的Bootstrap
也是继承它,所以仅提供服务端和客户端共有的方法实现。
启动服务端
代码如下,可查阅相关注释
- 创建
ServerBootstrap
实例 - 设置boos,worker事件执行器组:
EventLoopGroup
- 设置通道类型:
Channel
- 设置
NIO_SOCKET
参数 - 设置
buffer
分配器:默认是:UnpooledByteBufAllocator
非池化buffer分配器 - 设置在每次读取通道数据时, 分配多大空间的buffer,默认是:
AdaptiveRecvByteBufAllocator
自适应buffer分配器基于读取到的字节数,动态调整分配buffer大小 - 设置
childHandler
客户端IO事件处理器,当每次有新的连接时,调用该方法进行初始化通道的Handler
// 创建ServerBootstrap实例
this.bootstrap = new ServerBootstrap();
// 服务端监听客户端连接的事件组, 只需要一个线程数即可. 只会创建一个服务端, 也只会注册到一个NioEventLoop上
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("boss"));
// 处理客户端IO事件组,
workerGroup = new NioEventLoopGroup(10, new DefaultThreadFactory("worker"));
this.bootstrap.group(bossGroup, workerGroup)
// 启动所使用的通道实现类, 会使用反射进行实例化
.channel(NioServerSocketChannel.class)
// NIO_SOCKET 参数
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
/**
* Buffer分配器, PooledByteBufAllocator 池化Buffer
*/
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
/**
* 在每次读取通道数据时, 分配多大空间的buffer, 固定1024
* 参考: {@link io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read()}
*/
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(1024))
// 设置客户端通道IO事件回调, 当有新的通道建立连接时, 调用initChannel方法, 初始化Handler链
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) throws Exception {
// 往通道Handler链上添加自定义的处理器, 编解码器,
ch.pipeline()
.addLast("handler", new MyHandler());
}
});
// 绑定端口, IO操作返回Future
ChannelFuture channelFuture = this.bootstrap.bind(9000);
// 同步等待结果, 如果发生InterruptedException, 则中断当前线程:Thread.currentThread().interrupt();
channelFuture.syncUninterruptibly();
if (channelFuture.isSuccess()) {
System.out.println("服务启动成功");
} else {
// 失败原因
channelFuture.cause().printStackTrace();
}
// 获取通道
channel = channelFuture.channel();
服务端绑定端口
前文有提到Netty的绑定端口事件是异步,下图说明绑定端口的事件流程图
同步
同步操作步骤,就是将异步回调配置好
- 反射创建
NioServerSocketChannel
实例 - 配置通道的
option
、attr
- 在通道处理器链上添加
ChannelInitializer
,当有客户端建立连接时,回调该处理器。 - 将通道注册至
EventLoop
上
异步
将Channel注册至Selector,并监听端口
其他IO操作失败大同小异