【领你入门Netty】- Netty线程模型与入门程序详解

2 篇文章 0 订阅

一、Netty 简介

  Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 I/O 程序。它是一个基于 NIO 的网络编程框架,相比于 Java 原生的 NIO,它具有如下的优势:

  • 支持三种 I/O 模型同时支持三种 Reactor 模型。
  • 支持很多应用层的协议,提供了很多编解码器。
  • 能够很好地解决 TCP 长连接所带来的缺陷(粘包、半包等)。
  • 提供了应用层的 Keep Alive 机制。
  • 有更好的性能。

  此外,Netty 具有如下特性:

  • 统一的 API,支持多种传输类型,阻塞的和非阻塞的。
  • 真正的无连接数据报套接字支持。
  • 链接逻辑组件以支持复用。
  • 得益于池化和复用,拥有更低的资源消耗。
  • 简单而强大的线程模型,即主从 Reactor 多线程模型。

二、Netty 线程模型

  在介绍 Netty 的线程模型之前,首先需要了解下 Reactor 模型,Reactor 模型基于事件驱动,特别适合处理海量的 I/O 事件。根据 Reactor 的数量和处理资源池的线程数量的不同,Reactor 模型有以下 3 种不同的实现:

  • 单 Reactor 单线程模型。
  • 单 Reactor 多线程模型。
  • 主从 Reactor 多线程模型。
2.1 单 Reactor 单线程模型

  单 Reactor 单线程模型指所有的 I/O 操作都在同一个 NIO 线程上完成,NIO 线程的职责如下:

  • 作为 NIO 服务端,接收客户端的 TCP 连接。
  • 作为 NIO 客户端,向服务端发起 TCP 连接。
  • 读取通信对端的请求或者应答消息。
  • 向通信对端发送消息请求或者应答消息。

  单 Reactor 单线程模型图如下所示:

在这里插入图片描述
  通过 Reactor 的 select 监听客户端的请求事件,若是连接请求事件,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理后续的业务请求。若不是连接请求事件,则通过 Reactor 的 dispatch 调用该连接对应的 Handler 进行业务处理,即通过 dispatch 将对应的 ByteBuffer 分发到连接对应的 Handler 上进行消息解码,最后 NIO 线程将需要回送的消息进行编码发送给客户端。

  单 Reactor 单线程不存在多线程、进程通信、竞争的问题。但因为只有一个线程同时处理成百上千的链路,无法发挥多核 CPU 的优势,且Handler 在处理一个耗时的业务时,会导致整个进程无法处理其他的连接事件,造成客户端连接超时和超时重发,最终导致大量消息积压和处理超时。此外,若线程意外终止,或陷入死循环,会导致整个系统的通信模块不可用,不能接收和处理外部消息,从而造成结点故障

  综上所述,单 Reactor 单线程适合小容量应用场景,如客户端连接数较少、业务处理迅速。

2.2 单 Reactor 多线程模型

  单 Reactor 多线程模型如下图所示,其具有如下特点:

  • 有专门的一个 NIO 线程 Acceptor 线程用于监听服务端口,接收客户端的 TCP 连接请求。
  • 网络 I/O 操作(读或写等)由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送。
  • 1 个 NIO 线程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,避免发生并发操作问题。
  • Reactor 依然是以单线程的方式运行,在客户端高并发连接场景或者服务端需要对客户端握手进行安全认证的情况下容易出现性能瓶颈。
  • Worker 线程池会存在多线程的数据共享和竞争。

在这里插入图片描述
  根据上图,单 Reactor 多线程模型的工作过程如下:

  1)Reactor 对象通过 select 监控客户端的请求事件,根据收到的事件类型,使用 dispatch 进行分发。

  2)若是请求连接事件,则分发给 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 用于处理后续的业务。

  3)若不是请求连接事件,则分发给连接对应的 Handler 进行处理。

  4)Handler 只负责响应事件,它通过 read 读取数据后,会分发给 Worker 线程池中的任意一个线程进行业务处理。

  5)Worker 线程池在线程可用的情况下,分配独立的线程完成业务过程,并将处理结果返回给 Handler

  6)Handler 收到响应后,通过 send 将结果返回给 client。

2.3 主从 Reactor 多线程模型

  主从 Reactor 多线程模型如下图所示,其具有如下特点:

  • 服务端用于接收客户端连接的不再是个 1 个单独的 NIO 线程,而是一个独立的 NIO 线程池。
  • Acceptor 接收到客户端 TCP 连接请求处理完成后(可能包含接入认证等),将新创建的 SocketChannel注册到 SubReactor 的某个 I/O 线程上,由它负责 SocketChannel 的读写和编解码工作。
  • Acceptor 线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到 SubReactor 线程池的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。
  • Reactor 主线程即 MainReactor,它可以对应多个 Reactor 子线程即 SubReactor。

在这里插入图片描述
  根据上图,主从 Reactor 多线程模型的工作过程如下:

  1)Reactor 主线程的 MainReactor 对象通过 select 监听客户端的连接请求事件,通过 Acceptor 处理连接请求事件。

  2)当 Acceptor 处理连接事件后,MainReactor 创建 SocketChannel,并将其注册到 SubReactor。

  3)SubReactor 监听连接队列中的连接,并创建对应的 Handler 进行各种事件的处理。

  4)当连接队列中的连接有新事件发生时,SubReactor 会调用对应的 Handler 处理。

  5)Handler 通过 read 读取数据,并分发给 Work 线程池中的任意一个 Worker 线程处理。

  6)Worker 线程池分配独立的 Worker 线程进行业务处理,并返回结果给 Handler

  7)Handler 收到响应结果后,再通过 send 将结果返回给客户端。

2.4 Reactor 模型小结

  Reactor 可类比为生活中的前台接待员和服务员。

  • 单 Reactor 单线程模型,可类比为:前台接待员和服务员是同一个人,全程提供一条龙服务。
  • 单 Reactor 多线程模型,可类比为:只有一个前台接待员,多个服务员,前台接待员只负责接待。
  • 主从 Reactor 多线程模型,可类比为多个前台接待员,多个服务员。

  Reactor 模型具有如下优点:

  • 响应快,不必为单个同步时间所阻塞,即使 Reactor 本身是同步的。
  • 可以最大程度地避免复杂的多线程与同步问题,且避免了多线程/进程间的上下文切换。
  • 扩展性好,可方便地通过增加 Reactor 实例的个数来充分利用 CPU 资源。
  • 复用性好,Reactor 模型本身与具体事件的处理逻辑无关

三、Netty 线程模型

  Netty 线程模型是在主从 Reactor 多线程模型的基础上进行改进和优化的。其模型图如下图所示:

在这里插入图片描述
  从上图可以看到,Netty 抽象出两组线程池,其中 BossGroup 负责接收客户端的连接请求,WorkerGroup 负责网络读写请求,即具体的业务处理BossGroupWorkerGroup 的类型都是 NioEventLoopGroup,每个 NioEventLoopGroup 相当于一个事件循环组,在这个事件循环组中包含多个事件循环,每个事件循环都是 NioEventLoop,即一个不断循环地执行任务的线程。此外,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的网络通讯

BossGroup 中的 NioEventLoop 循环执行的步骤如下:

  1)轮询 Accept 事件,即客户端请求连接事件。

  2)处理 Accept 事件,与 client 建立通讯连接,并生成 SocketChannel,进而将其包装成 NioSocketChannel,再 NioSocketChannel 注册到 WorkGroup 中的任意一个 NioEventLoopSelector 中。

  3)处理任务队列中的任务,即 runAllTasks

  WorkGroup 中的 NioEventLoop 循环执行的步骤如下:

  1)轮询 readwrite 事件。

  2)处理 I/O 事件,即 readwrite 事件,在该连接对应的 NioSocketChannel 中处理。

  3)处理任务队列中的任务,即 runAllTasks

  在图中还存在一个 ChannelPipeline 即处理器管道。每个 WorkGroup 在处理业务时,都会使用它。ChannelPipeline 是一个包含了 ChannelHandlerContext 的双向链表,每个 ChannelHandlerContext 都关联着一个 ChannelHandler 处理器WorkerGroup 在进行业务处理时,会经过 ChannelPipeline 中的 ChannelHandler 的一系列处理(解码,处理业务,编码)。

四、Netty 入门程序

  项目的目录结构如下所示:

在这里插入图片描述

  首先编写服务端程序。

/**
 * 配置 Netty 服务端信息
 */
public class MyServer {

    // 服务端需要监听的端口
    private static final int port = 9999;

    public static void main(String[] args) {

        // 创建两个线程组 bossGroup 和 workGroup
        // bossGroup 只负责 accept 客户端的请求,即只处理连接请求
        // workGroup 处理客户端的业务请求
        // 两者都是无限循环的线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        // 服务端启动引导类 ServerBootstrap
        ServerBootstrap bootstrap = new ServerBootstrap();

        try {
            // 配置服务端启动引导类
            bootstrap.group(bossGroup,workGroup) // 配置两个线程组 bossGroup 和 workGroup
                    .channel(NioServerSocketChannel.class) // 使用 NioServerSocketChannel 作为服务端的通道实现
                    // 服务端处理客户端连接请求是顺序处理的,即同一时间只能处理一个客户端连接
                    // 当多个客户端到来时,将不能处理的客户端连接请求放在等待队列中,所以 ChannelOption.SO_BACKLOG 用于配置该等待队列的大小
                    .option(ChannelOption.SO_BACKLOG,128)
                    // 若配置该选项,若建立的连接在 2 小时内没有数据通信,TCP 会自动发送一个活动探测数据报文来测试连接状态
                    // 因此,该选项用于长时间没有数据交流的连接
                    .childOption(ChannelOption.SO_KEEPALIVE,true)
                    // 配置 workGroup 线程组的处理器,即实际上是配置 ChannelPipeline 中的 ChannelHandler 链
                    .childHandler(new MyServerChannelInitializer());

            // 异步绑定需要监听的服务端口,并生成一个 ChannelFuture 对象
            ChannelFuture channelFuture = bootstrap.bind(port).sync();

            // 为 ChannelFuture 添加监听器,即端口绑定成功时的操作,重写 operationComplete 方法回调即可
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (channelFuture.isSuccess()){
                        System.out.println("Myserver is ready............");
                    }
                }
            });

            // 对通过 Channel 关闭的动作进行异步监听
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            System.out.println("throw exceptions: " + e.getMessage());
        } finally {
            // 关闭连接
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

/**
 * 配置客户端和服务端的通道连接对象为 SocketChannel 和 ChannelPipeline 中的 ChannelHandler 链
 */
public class MyServerChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        // 配置 ChannelPipeline 的 handler,从 ChannelPipeline 的 tail 前插入
        pipeline.addLast(new MyServerHandler());
    }
}

/**
 * 配置处理器 ChannelHandler
 */
public class MyServerHandler extends ChannelInboundHandlerAdapter {

    /*
     * @MethodName: channelRead
     * @param: ctx 上下文对象,即 ChannelPipeline 中实际存储的对象,通过该上下文对象可以得到对象的 channelHandler
     * @param: msg 客户端发送的消息,默认是 Object
     * @Description: TODO 每当有消息产生时,都会回调该方法,即服务端实际的业务处理过程
     * @Throws:
     **/
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 将 msg 转换成 ByteBuf
        ByteBuf byteBuf = (ByteBuf) msg;
        String clientMsg = byteBuf.toString(StandardCharsets.UTF_8);

        System.out.println("客户端的远程地址:" + ctx.channel().remoteAddress());
        System.out.println("客户端:" + clientMsg);
    }

    /*
     * @MethodName: channelReadComplete
     * @param: ctx
     * @Description: TODO 服务端数据读取完毕后的回调执行
     * @Throws:
     **/
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {

        // writeAndFlush 等价于 write() + flush() 方法的结合,即将数据写入通道,并刷新到客户端
        // 此时通过一个非池化对象 Unpooled,可视为 Netty 提供的一个专门操作缓冲区的工具类
        // copiedBuffer(CharSequence string, Charset charset) 通过给定的数据和字符编码集返回一个 ByteBuf 对象
        ctx.channel().writeAndFlush(
                Unpooled.copiedBuffer("Hello, client, I am server", StandardCharsets.UTF_8)
        );

    }

    /*
     * @MethodName: exceptionCaught
     * @param: ctx
     * @param: cause
     * @Description: TODO 异常发生时的方法回调,若不处理,会将异常传递给 ChannelPipeline 中的下一个 ChannelHandler,若一直没有处理,则会传递到 ChannelPipeline 的尾部,所以程序中应至少提供一个重写了 exceptionCaught() 方法的 ChannelHandler
     * @Throws:
     **/
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("throw exception:" + cause.getMessage());
        ctx.close();
    }
}

  接下来是客户端程序。

/**
 * 配置 Netty 客户端信息
 */
public class MyClient {

    private static final String serverHost = "127.0.0.1";
    private static final int port = 9999;

    private static final InetSocketAddress serverAddress = new InetSocketAddress(serverHost, port);

    public static void main(String[] args) {

        EventLoopGroup clientGroup = new NioEventLoopGroup();

        // 客户端启动的引导类 Bootstrap,与服务端的不同
        Bootstrap bootstrap = new Bootstrap();

        try {

            // 因为只有一个 EventLoopGroup 线程组,所以不存在 childHandler 和 ChildOption
            bootstrap.group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new MyClientChannelInitializer());

            ChannelFuture channelFuture = bootstrap.connect(serverAddress).sync();

            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    System.out.println("Myclient is ready...............");
                }
            });

            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            System.out.println("throw exceptions: " + e.getMessage());
        } finally {
            clientGroup.shutdownGracefully();
        }
    }
}

public class MyClientChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline pipeline = socketChannel.pipeline();

        pipeline.addLast(new MyClientHandler());
    }
}

public class MyClientHandler extends ChannelInboundHandlerAdapter {

    /*
     * @MethodName: channelActive
     * @param: ctx
     * @Description: TODO 当 Channel 就绪时,会回调该方法
     * @Throws:
     **/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 给服务端回送消息
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Hello, server, I am client",StandardCharsets.UTF_8));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf byteBuf = (ByteBuf) msg;
        String serverMsg = byteBuf.toString(StandardCharsets.UTF_8);

        System.out.println("服务端的远程地址:" + ctx.channel().remoteAddress());
        System.out.println("服务端:" + serverMsg);

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("throw exception:" + cause.getMessage());
        ctx.close();
    }
}

  分别启动 MyServer 和 MyClient,测试结果如下图所示:

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值