Netty框架学习笔记

前言

本文是作者在做rpc框架项目时,需要使用Netty网络框架进行实现,因此学习了一些Netty相关的知识,在次仅仅是为了记录所学过的知识,可能并不全面,仅作为日后复习使用

Netty

上文我们讲了几种IO模式,但都是使用的java的原生io

使用原生io有以下问题:

  1. NIO的类库和API繁杂,使用麻烦
  2. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。
  3. 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。

        Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,例如Hadoop的RPC框架Avro就使用了Netty作为底层通信框架,其他还有业界主流的RPC框架(dubbo等),也使用Netty来构建高性能的异步通信能力。

        以下是Netty的主要优点:

  1. API使用简单,开发门槛低
  2. 功能强大,预置了多种编解码功能,支持多种主流协议
  3. 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展
  4. 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优

在讲Netty之前,必须先要了解一些Reactor设计模式的相关知识

Reactor模式

    Netty的NIO和上一章所说的NIO是有区别的,Netty的采用了Reactor模式

    常见的Reactor线程模型有三种:

  1. Reactor单线程模型        
  2. Reactor多线程模型
  3. 主从Reactor多线程模型(主要)

Reactor单线程模型

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

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

Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分派请求到处理器链中。

单线程模型不能充分利用多核资源,所以实际使用的不多

对于一些小容量应用场景,可以使用单线程模型,但是对于高负载、大并发的应用却不合适,主要原因如下:

  1. 一个NIO线程同时处理成百上千的链路,性能上无法支撑。即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送
  2. NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往进行重发,这更加重了NIO线程的负载,最终导致大量消息积压和处理超时,NIO线程会成为系统的性能瓶颈
  3. 可靠性问题。一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通讯模块不可用,不能接收和处理外部信息,造成节点故障

Reactor多线程模型

多线程模型和单线程模型最大的区别是 多线程有一组NIO线程来处理I/O操作,而且

  1. 一个专门的NIO线程--acceptor线程用于监听服务端,接收客户端的TCP连接请求
  2. 网络I/O操作--读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送
  3. 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题

主从Reactor多线程模型

         这个和多线程模型最大的区别是:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。

        这样处理大量连接时,不再是一个线程负责连接的建立以及分配业务处理等操作,而是一个线程池(主Reactor)来处理。然后有读写请求时,会将该连接的读写请求分配到另一个线程池(从Reactor)的某个线程来处理。

Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。

Reactor部分分为mainReactor和subReactor两部分。mainReactor负责监听处理新的连接,然后将后续的事件处理交给subReactor,subReactor对事件处理的方式,也由阻塞模式变成了多线程处理,引入了任务队列的模式。

这种模式的工作流程是:

1、主线程MainReactor对象通过select监听客户端事件,收到事件后,通过Acceptor处理客户端连接事件

2、当Acceptor处理完连接事件后(与客户端建立好 Socket 连接),MainReactor会将该连接分配给SubReactor。(即:MainReactor 只负责监听客户端连接请求,和客户端建立连接之后将连接交由 SubReactor 监听后面的 IO 事件。)

3、SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理。

4、当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理。

5、Handler通过read从连接上读取数据,将请求数据分发给Worker线程池业务处理

6、Worker 线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler。Handler 通过 send 向客户端发送响应数据。

7、一个 MainReactor 可以对应多个 SubReactor,即一个 MainReactor 线程可以对应多个 SubReactor 线程。


这种模式的优点是:

  1. MainReactor 线程与 SubReactor 线程的数据交互简单职责明确,MainReactor 线程只需要接收新连接,SubReactor 线程完成后续的业务处理。

  2. MainReactor 线程与 SubReactor 线程的数据交互简单, MainReactor 线程只需要把新连接传给 SubReactor 线程,SubReactor 线程无需返回数据。

  3. 多个 SubReactor 线程能够应对更高的并发请求。

这种模式的缺点是编程复杂度较高。

Netty架构

Netty架构整体上就对应以上主从Reactor多线程模型

在Netty中,也共有两个线程组,Boss线程组和Worker线程组。

Boss线程对应着对连接的处理和分派,相当于mainReactor;Work线程 对应着subReactor,使用多线程负责读写事件的分发和处理。

架构图说明:

  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 中循环执行以下三个步骤:
    1. select:轮询注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)
    2. processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到某个 WorkerNioEventLoop 上的 Selector 上
    3. runAllTasks:再去以此循环处理任务队列中的其他任务
  6. 每个 WorkerNioEventLoop 中循环执行以下三个步骤:
    1. select:轮询注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)
    2. processSelectedKeys:在对应的 NioSocketChannel 上处理 read/write 事件
    3. runAllTasks:再去以此循环处理任务队列中的其他任务
  7. 在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用了 Channel,即通过 Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。

Netty案例代码

接下来就是以个基于Netty的Tcp Server和Client的代码示例

TCP Server:

package com.mszlu.rpc.tcp.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.CharsetUtil;

public class TCPServer{
    public static void main(String[] args) throws InterruptedException {

        // 创建 BossGroup 和 WorkerGroup
        // 1. bossGroup 只处理连接请求
        // 2. 业务处理由 workerGroup 来完成
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 创建服务器端的启动对象
            //ServerBootstrap 服务器端的引导类,
            // 一个 Netty 应用程序通常由一个引导类开始,主要是用来配置整个 Netty 程序、设置业务处理类(Handler)、绑定端口、发起连接等
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 配置参数
            bootstrap
                    // 设置线程组
                    .group(bossGroup, workerGroup)
                    // 说明服务器端通道的实现类(便于 Netty 做反射处理)
                    //服务端首先创建一个 NioServerSocketChannel 作为服务器端通道,
                    // 每当接收一个客户端连接就产生一个 NioSocketChannel 应对该客户端
                    .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();
        }
    }
}

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

  1. NioSocketChannel:非阻塞的 TCP 客户端 Channel
  2. NioServerSocketChannel:非阻塞的 TCP 服务器端 Channel
  3. NioDatagramChannel:非阻塞的 UDP Channel
  4. NioSctpChannel:非阻塞的 SCTP 客户端 Channel
  5. NioSctpServerChannel:非阻塞的 SCTP 服务器端 Channel

TCP Client:

package com.mszlu.rpc.tcp.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.CharsetUtil;

public class TCPClient {

    public static void main(String[] args) throws InterruptedException {

        // 客户端只需要一个事件循环组,可以看做 BossGroup
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try {
            // 创建客户端的启动对象
            //Bootstrap 客户端引导类
            Bootstrap bootstrap = new Bootstrap();
            // 配置参数
            bootstrap
                    // 设置线程组
                    .group(eventLoopGroup)
                    // 说明客户端通道的实现类(便于 Netty 做反射处理)
                    //客户端创建一个 NioSocketChannel 作为客户端通道,去连接服务器
                    .channel(NioSocketChannel.class)
                    // handler()方法用于给 BossGroup 设置业务处理器
                    .handler(
                            // 创建一个通道初始化对象
                            //当一个链接建立时,我们需要知道怎么来接收或者发送数据,
                            // 当然,我们有各种各样的Handler实现来处理它,
                            // 那么ChannelInitializer便是用来配置这些Handler,
                            // 它会提供一个ChannelPipeline,并把Handler加入到ChannelPipeline
                            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 的异步模型
            //在Netty中所有的IO操作都是异步的,因此,你不能立刻得知消息是否被正确处理,
            // 但是我们可以过一会等它执行完成或者直接注册一个监听,
            // 具体的实现就是通过Future和ChannelFutures,
            // 他们可以注册一个监听,当操作执行成功或失败时监听会自动触发。
            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();
        }
    }
}

--注:材料来源于码神之路

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

次元工程师!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值