45 张图深度解析 Netty 架构与原理(四)

简介: 作为一个学 Java 的,如果没有研究过 Netty,那么你对 Java 语言的使用和理解仅仅停留在表面水平,会点 SSH 写几个 MVC,访问数据库和缓存,这些只是初等 Java 程序员干的事。如果你要进阶,想了解 Java 服务器的深层高阶知识,Netty 绝对是一个必须要过的门槛。 接下来我们会学习一个 Netty 系列教程,Netty 系列由「架构与原理」,「源码」,「架构」三部分组成,今天我们先来看看第一部分:Netty 架构与原理初探,大纲如下:

2.3. Netty 的模样

Netty 的设计主要基于主从 Reactor 多线程模式,并做了一定的改进。本节将使用一种渐进式的描述方式展示 Netty 的模样,即先给出 Netty 的简单版本,然后逐渐丰富其细节,直至展示出 Netty 的全貌。

简单版本的 Netty 的模样如下:

116.jpg070

关于这张图,作以下几点说明:

1)BossGroup 线程维护 Selector,ServerSocketChannel 注册到这个 Selector 上,只关注连接建立请求事件(相当于主 Reactor)。

2)当接收到来自客户端的连接建立请求事件的时候,通过 ServerSocketChannel.accept 方法获得对应的 SocketChannel,并封装成 NioSocketChannel 注册到 WorkerGroup 线程中的 Selector,每个 Selector 运行在一个线程中(相当于从 Reactor)。

3)当 WorkerGroup 线程中的 Selector 监听到自己感兴趣的 IO 事件后,就调用 Handler 进行处理。

我们给这简单版的 Netty 添加一些细节:

1177.jpg

关于这张图,作以下几点说明:

1)有两组线程池:BossGroup 和 WorkerGroup,BossGroup 中的线程(可以有多个,图中只画了一个)专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。

2)BossGroup 和 WorkerGroup 含有多个不断循环的执行事件处理的线程,每个线程都包含一个 Selector,用于监听注册在其上的 Channel。

3)每个 BossGroup 中的线程循环执行以下三个步骤:

3.1)轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

3.2)处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到 WorkerGroup 中某个线程上的 Selector 上

3.3)再去以此循环处理任务队列中的下一个事件

4)每个 WorkerGroup 中的线程循环执行以下三个步骤:

4.1)轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

4.2)在对应的 NioSocketChannel 上处理 read/write 事件

4.3)再去以此循环处理任务队列中的下一个事件

我们再来看下终极版的 Netty 的模样,如下图所示(图片来源于网络):

118.jpg

关于这张图,作以下几点说明:

1)Netty 抽象出两组线程池:BossGroup 和 WorkerGroup,也可以叫做 BossNioEventLoopGroup 和 WorkerNioEventLoopGroup。每个线程池中都有 NioEventLoop 线程。BossGroup 中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。BossGroup 和 WorkerGroup 的类型都是 NioEventLoopGroup。

2)NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个 NioEventLoop。

3)NioEventLoop 表示一个不断循环的执行事件处理的线程,每个 NioEventLoop 都包含一个 Selector,用于监听注册在其上的 Socket 网络连接(Channel)。

4)NioEventLoopGroup 可以含有多个线程,即可以含有多个 NioEventLoop。

5)每个 BossNioEventLoop 中循环执行以下三个步骤:

5.1)select:轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

5.2)processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到某个 WorkerNioEventLoop 上的 Selector 上

5.3)runAllTasks:再去以此循环处理任务队列中的其他任务

6)每个 WorkerNioEventLoop 中循环执行以下三个步骤:

6.1)select:轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

6.2)processSelectedKeys:在对应的 NioSocketChannel 上处理 read/write 事件

6.3)runAllTasks:再去以此循环处理任务队列中的其他任务

7)在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用了 Channel,即通过 Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。这里暂时不详细展开讲解 Pipeline。

2.4. 基于 Netty 的 TCP Server/Client 案例

下面我们写点代码来加深理解 Netty 的模样。下面两段代码分别是基于 Netty 的 TCP Server 和 TCP Client。

服务端代码为:

/**
 * 需要的依赖:
 * <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();
    }
}

什么?你觉得使用 Netty 编程难度和工作量更大了?不会吧不会吧,你要知道,你通过这么两段简短的代码得到了一个基于主从 Reactor 多线程模式的服务器,一个高吞吐量和并发量的服务器,一个异步处理服务器……你还要怎样?

对上面的两段代码,作以下简单说明:

1)Bootstrap 和 ServerBootstrap 分别是客户端和服务器端的引导类,一个 Netty 应用程序通常由一个引导类开始,主要是用来配置整个 Netty 程序、设置业务处理类(Handler)、绑定端口、发起连接等。

2)客户端创建一个 NioSocketChannel 作为客户端通道,去连接服务器。

3)服务端首先创建一个 NioServerSocketChannel 作为服务器端通道,每当接收一个客户端连接就产生一个 NioSocketChannel 应对该客户端。

4)使用 Channel 构建网络 IO 程序的时候,不同的协议、不同的阻塞类型和 Netty 中不同的 Channel 对应,常用的 Channel 有:

  • NioSocketChannel:非阻塞的 TCP 客户端 Channel(本案例的客户端使用的 Channel)
  • NioServerSocketChannel:非阻塞的 TCP 服务器端 Channel(本案例的服务器端使用的 Channel)
  • NioDatagramChannel:非阻塞的 UDP Channel
  • NioSctpChannel:非阻塞的 SCTP 客户端 Channel
  • NioSctpServerChannel:非阻塞的 SCTP 服务器端 Channel
    ......

启动服务端和客户端代码,调试以上的服务端代码,发现:

1)默认情况下 BossGroup 和 WorkerGroup 都包含 16 个线程(NioEventLoop),这是因为我的 PC 是 8 核的 NioEventLoop 的数量=coreNum*2。这 16 个线程相当于主 Reactor。

119.jpg120.jpg121.jpg

其实创建 BossGroup 和 WorkerGroup 的时候可以指定 NioEventLoop 数量,如下:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(16);

这样就能更好地分配线程资源。

2)每一个 NioEventLoop 包含如下的属性(比如自己的 Selector、任务队列、执行器等):

122.jpg

3)将代码断在服务端的 NettyServerHandler.channelRead 上:

123.jpg

可以看到 ctx 中包含的属性如下:

124.jpg

可以看到:

  • 当前 ChannelHandlerContext ctx 是位于 ChannelHandlerContext 责任链中的一环,可以看到其 next、prev 属性
  • 当前 ChannelHandlerContext ctx 包含一个 Handler
  • 当前 ChannelHandlerContext ctx 包含一个 Pipeline
  • Pipeline 本质上是一个双向循环列表,可以看到其 tail、head 属性
  • Pipeline 中包含一个 Channel,Channel 中又包含了该 Pipeline,两者互相引用
    ……

从下一节开始,我将深入剖析以上两段代码,向读者展示 Netty 的更多细节。

相关文章
|
16天前
|
Linux 编译器 开发者
Linux设备树解析:桥接硬件与操作系统的关键架构
在探索Linux的庞大和复杂世界时🌌,我们经常会遇到许多关键概念和工具🛠️,它们使得Linux成为了一个强大和灵活的操作系统💪。其中,"设备树"(Device Tree)是一个不可或缺的部分🌲,尤其是在嵌入式系统🖥️和多平台硬件支持方面🔌。让我们深入了解Linux设备树是什么,它的起源,以及为什么Linux需要它🌳。
Linux设备树解析:桥接硬件与操作系统的关键架构
|
26天前
|
设计模式 安全 Java
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
31 0
|
1天前
|
缓存 JavaScript 前端开发
|
2天前
|
SQL 分布式计算 资源调度
一文解析 ODPS SQL 任务优化方法原理
本文重点尝试从ODPS SQL的逻辑执行计划和Logview中的执行计划出发,分析日常数据研发过程中各种优化方法背后的原理,覆盖了部分调优方法的分析,从知道怎么优化,到为什么这样优化,以及还能怎样优化。
|
2天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
11 0
|
2天前
|
JSON Java Maven
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
7 0
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
|
2天前
|
前端开发 Java
SpringBoot之三层架构的详细解析
SpringBoot之三层架构的详细解析
13 0
|
2天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。
|
12天前
|
机器学习/深度学习 分布式计算 BI
Flink实时流处理框架原理与应用:面试经验与必备知识点解析
【4月更文挑战第9天】本文详尽探讨了Flink实时流处理框架的原理,包括运行时架构、数据流模型、状态管理和容错机制、资源调度与优化以及与外部系统的集成。此外,还介绍了Flink在实时数据管道、分析、数仓与BI、机器学习等领域的应用实践。同时,文章提供了面试经验与常见问题解析,如Flink与其他系统的对比、实际项目挑战及解决方案,并展望了Flink的未来发展趋势。附带Java DataStream API代码样例,为学习和面试准备提供了实用素材。
34 0
|
4天前
|
敏捷开发 监控 数据管理
构建高效微服务架构的五大关键策略
【4月更文挑战第20天】在当今软件开发领域,微服务架构已经成为一种流行的设计模式,它允许开发团队以灵活、可扩展的方式构建应用程序。本文将探讨构建高效微服务架构的五大关键策略,包括服务划分、通信机制、数据管理、安全性考虑以及监控与日志。这些策略对于确保系统的可靠性、可维护性和性能至关重要。

推荐镜像

更多