- netty 底层实际上也是一个c/s的I/O,其是对NIO的封装和优化,也就是说其也是依赖于selector原理的。
Reactor模型
- 响应式编程:当某种事件发生的时候,触发回调对应的方法,程序开始相应并处理该事件
- 正如序言的案例:当有事件发生的时候,才会触发调用处理器进行数据处理
- 具体分析:
Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的Handler去处理。
Handler:负责处理非阻塞的行为,标识系统管理的资源;同时将handler与事件绑定。
Reactor为单个线程,需要处理accept连接,同时发送请求到处理器中。
- 缺点:若需处理的数据量过大,会造成连接阻塞情况;
- 改进:使用多线程处理业务逻辑。
- 将处理器的执行放入线程池,多线程进行业务处理。但Reactor仍为单个线程。
- 改进:对于多个CPU的机器,为充分利用系统资源,将Reactor拆分为两部分。
- mainReactor负责监听连接,accept连接给subReactor处理,为什么要单独分一个Reactor来处理监听呢?因为像TCP这样需要经过3次握手才能建立连接,这个建立连接的过程也是要耗时间和资源的,单独分一个Reactor来处理,可以提高性能。
零拷贝技术
- 在“将本地磁盘中文件发送到网络中”这一场景中,零拷贝技术是提升 IO 效率的一个利器。
- 与其他技术的对比:
1)直接 IO 技术
内核缓冲区是 Linux 系统的 Page Cahe。为了加快磁盘的 IO,Linux 系统会把磁盘上的数据以 Page 为单位缓存在操作系统的内存里,这里的 Page 是 Linux 系统定义的一个逻辑概念,一个 Page 一般为 4K。
可以看出,整个过程有四次数据拷贝,读进来两次,写回去又两次:磁盘–>内核缓冲区–>Socket 缓冲区–>网络。
2)内存映射文件技术
可以看出,整个过程有三次数据拷贝,不再经过应用程序内存,直接在内核空间中从内核缓冲区拷贝到 Socket 缓冲区
接口为:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
3)零拷贝技术
内核缓冲区到 Socket 缓冲区之间并没有做数据的拷贝,只是一个地址的映射。底层的网卡驱动程序要读取数据并发送到网络上的时候,看似读取的是 Socket 的缓冲区中的数据,其实直接读的是内核缓冲区中的数据。
零拷贝中所谓的“零”指的是内存中数据拷贝的次数为 0。
Linux系统接口:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
Java接口:
FileChannel.transderTo(long position, long count, WritableByteChannel target);
netty调用流程图(Server)
- Boss Group和Work Group相当于reactor中的mainGroup和subGroup(实际就是一个线程池),每个NioEventGroup都相当于一个selector,其作用是分发待处理的数据。
- BossGroup 中的线程(可以有多个,也可以是一个)专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。每个线程都包含一个 Selector,用于监听注册在其上的 Channel。
- NioEventLoopGroup(Boss Group和Work Group也可以称为BossNioEventLoopGroup和WorkNioEventLoopGroup) 相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个 NioEventLoop。
- NioEventLoop 表示一个不断循环的执行事件处理的线程,每个 NioEventLoop 都包含一个 Selector,用于监听注册在其上的 Socket 网络连接(Channel)。
具体调用步骤:
每个 BossNioEventLoop 中循环执行以下三个步骤:
1)select:轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)
2)processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到某个 WorkerNioEventLoop 上的 Selector 上
3)runAllTasks:再去以此循环处理任务队列中的其他任务
每个 WorkerNioEventLoop 中循环执行以下三个步骤:
1)select:轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)
2)processSelectedKeys:在对应的 NioSocketChannel 上处理 read/write 事件
3)runAllTasks:再去以此循环处理任务队列中的其他任务
在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用了 Channel,即通过 Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器
Channel Handler
- Netty 中的 ChannelHandler 的作用是,在当前 ChannelHandler 中处理 IO 事件,并将其传递给 ChannelPipeline 中下一个 ChannelHandler 处理,因此多个 ChannelHandler 形成一个责任链,责任链位于 ChannelPipeline 中。
- 数据在基于 Netty 的服务器或客户端中的处理流程是:读取数据–>解码数据–>处理数据–>编码数据–>发送数据。其中的每个过程都用得到 ChannelHandler 责任链;
- ChannelPipeline 提供了 ChannelHandler 链的容器:
以客户端应用程序为例,如果事件的方向是从客户端到服务器的,我们称事件是出站的,那么客户端发送给服务器的数据会通过 Pipeline 中的一系列 ChannelOutboundHandler 进行处理;
如果事件的方向是从服务器到客户端的,我们称事件是入站的,那么服务器发送给客户端的数据会通过 Pipeline 中的一系列 ChannelInboundHandler 进行处理。
Pipeline 组件
- 上一节说到,Netty 的 ChannelPipeline,它维护了一个 ChannelHandler 责任链,负责拦截或者处理 inbound(入站)和 outbound(出站)的事件和操作
- 每个 Netty Channel 包含了一个 ChannelPipeline(其实 Channel 和 ChannelPipeline 互相引用),而 ChannelPipeline 又维护了一个由 ChannelHandlerContext 构成的双向循环列表,其中的每一个 ChannelHandlerContext 都包含一个 ChannelHandler。
- 在处理入站事件的时候,入站事件及数据会从 Pipeline 中的双向链表的头 ChannelHandlerContext 流向尾 ChannelHandlerContext,并依次在其中每个 ChannelInboundHandler(例如解码 Handler)中得到处理;出站事件及数据会从 Pipeline 中的双向链表的尾 ChannelHandlerContext 流向头 ChannelHandlerContext,并依次在其中每个 ChannelOutboundHandler(例如编码 Handler)中得到处理
TaskQueue
- 在 Netty 的每一个 NioEventLoop 中都有一个 TaskQueue,设计它的目的是在任务提交的速度大于线程的处理速度的时候起到缓冲作用。或者用于异步地处理 Selector 监听到的 IO 事件.
- Netty 中的任务队列有三种使用场景:
1)处理用户程序的自定义普通任务的时候
2)处理用户程序的自定义定时任务的时候
3)非当前 Reactor 线程调用当前 Channel 的各种方法的时候。
netty简单案例
- netty使用了reactor模型,一个简单的调用案例如下:
服务端代码:
/**
* 需要的依赖:
* <dependency>
* <groupId>io.netty</groupId>
* <artifactId>netty-all</artifactId>
* <version>4.1.52.Final</version>
* </dependency>
*/
public static void main(String[] args) throws InterruptedException {
// 创建 BossGroup 和 WorkerGroup
// 1. bossGroup 只处理连接请求
// 2. 业务处理由 workerGroup 来完成
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建服务器端的启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
// 配置参数
bootstrap
// 设置线程组
.group(bossGroup, workerGroup)
// 说明服务器端通道的实现类(便于 Netty 做反射处理)
.channel(NioServerSocketChannel.class)
// 设置等待连接的队列的容量(当客户端连接请求速率大
// 于 NioServerSocketChannel 接收速率的时候,会使用
// 该队列做缓冲)
// option()方法用于给服务端的 ServerSocketChannel
// 添加配置
.option(ChannelOption.SO_BACKLOG, 128)
// 设置连接保活
// childOption()方法用于给服务端 ServerSocketChannel
// 接收到的 SocketChannel 添加配置
.childOption(ChannelOption.SO_KEEPALIVE, true)
// handler()方法用于给 BossGroup 设置业务处理器
// childHandler()方法用于给 WorkerGroup 设置业务处理器
.childHandler(
// 创建一个通道初始化对象
new ChannelInitializer<SocketChannel>() {
// 向 Pipeline 添加业务处理器
@Override
protected void initChannel(
SocketChannel socketChannel
) throws Exception {
socketChannel.pipeline().addLast(
new NettyServerHandler()
);
// 可以继续调用 socketChannel.pipeline().addLast()
// 添加更多 Handler
}
}
);
System.out.println("server is ready...");
// 绑定端口,启动服务器,生成一个 channelFuture 对象,
// ChannelFuture 涉及到 Netty 的异步模型,后面展开讲
ChannelFuture channelFuture = bootstrap.bind(8080).sync();
// 对通道关闭进行监听
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
/**
* 自定义一个 Handler,需要继承 Netty 规定好的某个 HandlerAdapter(规范)
* InboundHandler 用于处理数据流入本端(服务端)的 IO 事件
* InboundHandler 用于处理数据流出本端(服务端)的 IO 事件
*/
static class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道有数据可读时执行
*
* @param ctx 上下文对象,可以从中取得相关联的 Pipeline、Channel、客户端地址等
* @param msg 客户端发送的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 接收客户端发来的数据
System.out.println("client address: "
+ ctx.channel().remoteAddress());
// ByteBuf 是 Netty 提供的类,比 NIO 的 ByteBuffer 性能更高
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("data from client: "
+ byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 数据读取完毕后执行
*
* @param ctx 上下文对象
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
// 发送响应给客户端
ctx.writeAndFlush(
// Unpooled 类是 Netty 提供的专门操作缓冲区的工具
// 类,copiedBuffer 方法返回的 ByteBuf 对象类似于
// NIO 中的 ByteBuffer,但性能更高
Unpooled.copiedBuffer(
"hello client! i have got your data.",
CharsetUtil.UTF_8
)
);
}
/**
* 发生异常时执行
*
* @param ctx 上下文对象
* @param cause 异常对象
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// 关闭与客户端的 Socket 连接
ctx.channel().close();
}
}
客户端代码:
/**
* 需要的依赖:
* <dependency>
* <groupId>io.netty</groupId>
* <artifactId>netty-all</artifactId>
* <version>4.1.52.Final</version>
* </dependency>
*/
public static void main(String[] args) throws InterruptedException {
// 客户端只需要一个事件循环组,可以看做 BossGroup
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
// 创建客户端的启动对象
Bootstrap bootstrap = new Bootstrap();
// 配置参数
bootstrap
// 设置线程组
.group(eventLoopGroup)
// 说明客户端通道的实现类(便于 Netty 做反射处理)
.channel(NioSocketChannel.class)
// handler()方法用于给 BossGroup 设置业务处理器
.handler(
// 创建一个通道初始化对象
new ChannelInitializer<SocketChannel>() {
// 向 Pipeline 添加业务处理器
@Override
protected void initChannel(
SocketChannel socketChannel
) throws Exception {
socketChannel.pipeline().addLast(
new NettyClientHandler()
);
// 可以继续调用 socketChannel.pipeline().addLast()
// 添加更多 Handler
}
}
);
System.out.println("client is ready...");
// 启动客户端去连接服务器端,ChannelFuture 涉及到 Netty 的异步模型,后面展开讲
ChannelFuture channelFuture = bootstrap.connect(
"127.0.0.1",
8080).sync();
// 对通道关闭进行监听
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
/**
* 自定义一个 Handler,需要继承 Netty 规定好的某个 HandlerAdapter(规范)
* InboundHandler 用于处理数据流入本端(客户端)的 IO 事件
* InboundHandler 用于处理数据流出本端(客户端)的 IO 事件
*/
static class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道就绪时执行
*
* @param ctx 上下文对象
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
// 向服务器发送数据
ctx.writeAndFlush(
// Unpooled 类是 Netty 提供的专门操作缓冲区的工具
// 类,copiedBuffer 方法返回的 ByteBuf 对象类似于
// NIO 中的 ByteBuffer,但性能更高
Unpooled.copiedBuffer(
"hello server!",
CharsetUtil.UTF_8
)
);
}
/**
* 当通道有数据可读时执行
*
* @param ctx 上下文对象
* @param msg 服务器端发送的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 接收服务器端发来的数据
System.out.println("server address: "
+ ctx.channel().remoteAddress());
// ByteBuf 是 Netty 提供的类,比 NIO 的 ByteBuffer 性能更高
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("data from server: "
+ byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 发生异常时执行
*
* @param ctx 上下文对象
* @param cause 异常对象
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// 关闭与服务器端的 Socket 连接
ctx.channel().close();
}
}
使用 Channel 构建网络 IO 程序的时候,不同的协议、不同的阻塞类型和 Netty 中不同的 Channel 对应,常用的 Channel 有:
NioSocketChannel:非阻塞的 TCP 客户端 Channel(本案例的客户端使用的 Channel)
NioServerSocketChannel:非阻塞的 TCP 服务器端 Channel(本案例的服务器端使用的 Channel)
NioDatagramChannel:非阻塞的 UDP Channel
NioSctpChannel:非阻塞的 SCTP 客户端 Channel
NioSctpServerChannel:非阻塞的 SCTP 服务器端 Channel
小结
使用netty能够更快的相应用户,其主要原因在于以下三点:
1)reactor模型
2)pipeline组件
3)零拷贝技术
参考文档:
https://cloud.tencent.com/developer/article/1754078