自定义RPC项目——常见问题及详解(Netty篇)

        书接上回,自定义RPC项目——常见问题及详解(1)_李孛欢的博客-CSDN博客,我们接着来谈,这个RPC项目的常见问题:

项目地址:https://blog.csdn.net/qq_40856284/category_10138756.html

        三,有关Netty

        本篇文章主要介绍有关Netty相关的知识以及项目对Netty的使用,在这之前读者还需要了解基本的IO模型、JDK中NIO基本组件等知识。

        如果你的简历在项目介绍中写到用 Netty实现网络传输,那我想就必须将Netty相关知识吃透,所以这个项目看似简单,实则可挖掘的东西是非常多的,那么,Netty是什么以及我们为什么要用它,这是首先要回答的问题。

        1.谈谈你对Netty的认识

        Netty是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务端和客户端。 注意:1.异步,Netty并未采用异步IO,这里的异步主要是指Netty使用多线程实现方法调用和处理结果相分离。2.基于事件驱动,这个是指Netty底层采用多路复用机制,只有在IO事件发生时才进行处理。

        Netty相对于JavaNIO有以下优势:

        ①.NIO需要自己构建协议,Netty很多协议都帮我们搭建好了        

        ②.Netty帮我们解决了TCP传输问题,如粘包、半包

        ③.NIO在linux系统中的实现存在epoll空轮询bug,这会导致CPU占用率100%

        ④.Netty对原NIO的API进行了增强,如ByteBuffer ==> ByteBuf

        ⑤.Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口

         Netty相对于其他网络应用框架(如Mina)有以下优势:

        ①.Netty的社区活跃,不断更新,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入

        ②.Netty是现在应用最广泛的网络应用框架,地位相当于Spring之于JavaEE,久经考验

        ③.Netty中的API简洁好用,文档更优秀

        2.Netty怎么实现高性能的?or Netty高性能主要依赖了哪些特性? or Netty为什么快?etc.

        这个问题我觉得主要从Netty的IO/线程模型以及内存零拷贝机制来回答!

        Netty作为高性能IO组件的佳作,其核心就在于巧妙地将高性能的IO模型和线程模型结合,相得益彰,达到了高性能 、高吞吐、低延迟、低消耗的目标。首先要明确,IO模型是是决定如何来收发数据的,而线程模型是决定如何来处理数据的,这两者对于Netty的性能都有着非常大的影响。下面我们来具体谈谈Netty的IO模型和线程模型。

        Netty的非阻塞I/O(NIO)的实现关键是基于I/O多路复用模型,在I/O复用模型中,会用到select,这个函数也会使进程阻塞,但是和阻塞I/O所不同的的。多路复用不会像阻塞式IO一样阻塞在某一事件中,而是同时监听多个事件,一旦有请求到达就进行处理,如下图所示。

         Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端连接。当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。另外,传统的I/O是面向字节流字符流的,以流式的方式顺序地从一个Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。在Netty中, 抛弃了传统的 I/O流, 而是引入了Channel和Bytebuf。基于buffer操作不像传统IO的顺序操作, 而是可以随意地读取任意位置的数据。

        接着谈Netty的线程模型:主从Reactors多线程模型,含有多个Reactor:MainReactorSubReactor,如下图所示

  • MainReactor负责客户端的连接请求,并将请求转交给SubReactor
  • SubReactor负责相应通道的IO读写请求
  • 非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理

         那么这里说的Reactor是什么呢,这是Reactor线程模型的概念,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。

Reactor模型中有2个关键组成:

  • Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
  • Handlers 处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作

         可以这样理解,Reactor就是一个执行while (true) { selector.select(); …}循环的线程,会源源不断的产生新的事件,所以称作为反应堆。

         最后看内存零拷贝!零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率(在内核态中借助DMA解放CPU)。而且,零拷贝技术减少了用户空间和操作系统内核空间之间因为上下文切换而带来的开销。具体怎么实现的呢,我们先看下面这段代码,将磁盘的一个文件写入网卡。

         上面这段代码会经历一下过程,首先Java本身不具备IO读写能力,因此调用read方法后会从用户态切换到内核态,内核状态下操作系统将数据读到内核缓冲区,这期间用户线程阻塞,CPU不参与拷贝(使用DMA解放CPU)。然后从内核态切换到用户态,将数据从内核缓冲区读入用户缓冲区(即代码中的buf),这期间CPU参与拷贝。然后调用write方法(用户态与内核态的切换),这时将数据从用户缓冲区写入Socket缓冲区,cpu会参与拷贝。最后将socket缓冲区的数据写入网卡。这个过程,数据拷贝4次,用户态和内核态的切换3次

         上述过程,java中NIO可以进行优化,使用ByteBuffer.allocate() DirectByteBuffer使用直接内存做缓存。效果如下。和上述不同就是,这里的用户缓冲区可以看作内核缓冲区。这个过程,数据拷贝3次,用户态和内核态的切换3次

         进一步优化,在linux2.1提供的sendFile方法,Java调用transferTo/transferFrom方法拷贝数据(一次切换),直接从内存缓冲区将数据写入socket缓冲区。到了linux2.4,同样的方法,能够直接把数据从内存缓冲区写入网卡(少量数据写入socket缓冲区),再减少了一次数据拷贝,一共两次数据拷贝。下面这两种都可以说是零拷贝!!这里的零是指不用在java内存中进行拷贝!最后只有一次用户态和内核态的切换。基于以上分析,记住两点零拷贝的优点:1.减少了用户态和内核态的切换,2.不经过java内存,内核中使用DMA解法CPU进行数据拷贝工作,提高了CPU的利用效率。

        3.说一下Netty的启动流程?

        首先看服务端上代码!如下图所示:

过程简单总结如下:

1.启动器,负责组装netty组件,启动服务器

2.通过group方法加入了一个eventloop的组(包括BossEventLoop 负责处理可连接事件,WorkerEventloop(包括了thread和selector)负责可读事件)

3.通过channel方法选择一个serverChannel的实现(netty支持多种实现,包括nio,bio等),相当于netty对原生的serverChannel做了封装

4.boss负责处理连接,worker(child)负责处理读写,通过childHandler,就相当于告诉作为worker的eventloop需要执行哪些操作(handler)

5.channel代表和客户端进行数据读写的通道Initializer代表一个初始化器。channelInitializer就相当于一个handler,它负责添加别的handler

6.添加具体的handler。StringDecodr将ByteBuf转化为字符串,后面一个就是自定义的handler

7.绑定监听端口

        客户端的启动类似,如图,不展开解释了

         4.Netty中有哪些重要的组件?

        在这里只是简单介绍一下,有兴趣的可以去自行了解每个组件的细节和原理!!

  • Channel:Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 等。(理解为数据通道
  • ByteBuf:字节缓冲区,是Netty对NIO中ByteBuffer的增强。(理解为具体的数据,经过加工会变成具体的类型对象,最后又恢复为ByteBuf
  • EventLoop:主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情。循环处理事件(理解为工人或干活的线程
  • ChannelHandler:充当了所有处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。(理解为数据的处理工序
  • ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。(理解为流水线
  • ChannelFuture:Netty 框架中涉及到非常多的异步操作,因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。

        Netty可问的实在太多,还包括Netty怎么解决粘包半包的,Netty的两个线程池,如何保证长连接,Netty实现异步编程,以及上面组件的具体细节等等,我这里就不一一展开了,下面我们对着项目,来看Netty在我们这个项目中的使用!

         4.项目中如何实现心跳保持的?

        第一:  理解概念。何为心跳?所谓心跳,,即在 TCP 长连接中,客户端和服务器之间定期发送的一种特殊的数据包,通知对方自己还在线,以确保 TCP 连接的有效性。一个连接如果长时间不用,防火墙或者路由器就会断开该连接,心跳机制是解决该问题的,用户可以灵活自定义断开时机。

        第二:具体实现项目中使用Netty对心跳机制有两个层面实现,第一个是TCP层面,之前给通道初始化设置的值 .option(ChannelOption.SO_KEEPALIVE, true) 就是TCP的心跳机制,第二个层面是通过通道中的处理IdleStateHandler来实现,可以自定义心跳检测间隔时长,以及具体检测的逻辑实现。

        服务端:在NettyServer类中向管道注册Handler(相当于往流水线上加工序),.addLast(new IdleStateHandler(30//读超时, 0, 0, TimeUnit.SECONDS)),即设定IdleStateHandler心跳检测每30秒进行一次读检测,如果30秒内ChannelRead()方法未被调用则触发一次userEventTrigger()方法

 ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .option(ChannelOption.SO_BACKLOG, 256)
                    .option(ChannelOption.SO_KEEPALIVE, true)//注意
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS))//注意
                                    .addLast(new CommonEncoder(serializer))
                                    .addLast(new CommonDecoder())
                                    .addLast(new NettyServerHandler());
                        }
                    });
  @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.READER_IDLE) {
                logger.info("长时间未收到心跳包,断开连接...");
                ctx.close();
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

        客户端:在ChannelProvider类中向管道注册Handler,.addLast(new IdleStateHandler(0, 5//写超时, 0, TimeUnit.SECONDS)),即设定每5秒进行一次写检测,如果5秒内write()方法未被调用则触发一次userEventTrigger()方法,该方法在NettyClientHandler类中实现

 bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) {
                /*自定义序列化编解码器*/
                // RpcResponse -> ByteBuf
                ch.pipeline().addLast(new CommonEncoder(serializer))
                        .addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS))
                        .addLast(new CommonDecoder())
                        .addLast(new NettyClientHandler());
            }
        });
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.WRITER_IDLE) {
                logger.info("发送心跳包 [{}]", ctx.channel().remoteAddress());
                Channel channel = ChannelProvider.get((InetSocketAddress) ctx.channel().remoteAddress(), CommonSerializer.getByCode(CommonSerializer.DEFAULT_SERIALIZER));
                RpcRequest rpcRequest = new RpcRequest();
                rpcRequest.setHeartBeat(true);
                channel.writeAndFlush(rpcRequest).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

        第三:熟悉原理。从上面实现可以看出,实现的关键在于核心Handler — IdleStateHandler

        首先看看IdleStateHandler的构造器

public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {
    this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);
}

这里解释下三个参数的含义:

  • readerIdleTimeSeconds: 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件.
  • writerIdleTimeSeconds: 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件.
  • allIdleTimeSeconds: 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.

        实现原理可以看下面这篇博客:

        Netty学习(五)—IdleStateHandler心跳机制_zhenyutu的博客-CSDN博客_idlestatehandler

        好啦,Netty篇给大家总结到这里,如果感觉对你有帮助,可以给博主点个赞加个关注!

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

李孛欢

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

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

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

打赏作者

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

抵扣说明:

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

余额充值