Netty---核心组件的深度解析

Netty—核心组件的深度解析

Netty入站和出站机制

基本思想:

  • 入站操作主要是指读取数据的操作;而出站操作主要是指写入数据的操作
  • 入站会从先读取,再执行入站的Handler;
  • 出站会先执行出站的Handler,再写入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UJcUXMDV-1619253586163)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210420195326696.png)]

其中Decoder是ChannelInboundHandlerAdapter(入站),Encoder是ChannelOutboundHandlerAdapter(出站)

注意:

  • 无论是解码还是编码处理器,接收的消息类型必须和待处理的消息类型一致,否则该处理器不会被执行
  • 在解码器进行数据解码时,需要判断缓存区(byteBuf)的数据是否足够,否者接收到的结果和期望结果可能不一致

常用的编解码器:

  • 解码器:

    • ReplayingDecoder,拓展了ByteToMessageDecoder,使用这个类,我们不必调用readableBytes方法来判断缓冲区大小

      • //参数S指定了用户状态管理的类型,使用Void代表不需要状态管理
        public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
        
      • 并不是所有的bytrBuf操作都支持,如果调用了一个不支持的方法,会抛出UnsupportOperationException。

      • ReplayingDecoder在某些情况下可能会稍慢于ByteToMessageDecoder,例如网络缓慢并且消息格式复杂时,消息会拆分为多个碎片,速度变慢。

    • LineBasedFrameDecoder,使用行尾控制符(\n或者\r\n)作为分隔符来解析数据

    • DelimiterBasedFrameDecoder,使用自定义的特殊字符作为消息的分隔符

    • HttpObjectDecoder,一个HTTP数据的解码器

    • LengthFieldBasedFrameDecoder,通过指定长度来标识整包消息,这样就可以自动处理黏包和半包消息。

TCP粘包和拆包基本介绍

  • TCP是面向连接的,面向流的,提供高性能可靠服务服务。收发两端(C/S)都要有一一成对的socket。因此,发送端为了将多个发给接受端的包更有效的发给对方,使用优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合成一个大的数据块,然后进行封包。这样虽然提高了效率,但是接受端就难于分辨出完整的数据包了,因为面向流的通信是没有消息保护边界的
  • 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们说的粘包,拆包问题
解决方案
  • 使用自定义协议+编解码器来解决
  • 关键就是要解决服务器每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免TCP粘包,拆包
  • 就是定义一个自定义一个协议(对象),每次传输消息时,先将消息长度(int)传输过去,在传输消息(object)------->编码(encoder)。接受端接收时,先接受到消息长度(int),然后按照消息长度接收相应长度的消息(object)------>解码(decoder)。

Netty接收请求过程

**总体流程:**接受请求—>创建一个新的NioSocketChannel---->注册到一个workEventLoop上----->注册selector read事件

  • 服务器轮询Accept事件,获取事件后调用unsafe的read方法,这个unsafe时serverSocket的内部类,该方法内部由2部分组成
    • doReadMessage用于创建NioSocketChannel对象,该对象包装JDK的NioChannel客户端。该方法会像创建ServerSocketChannel类似创建相关的pipeline,unsafe,config
    • 随后执行pipeline.fireChannelRead方法,并将自己绑定到一个choose选择器选择的workGroup中的一个EventLoop上,并返回一个0,表示注册成功。

Pipeline调用handler

  • context包装handler,多个context在pipeline中形成双向链表,入站方向叫inbound,由head节点开始;出站方向叫outbound,由tail开始;

  • 节点中间的传递通过AbstractChannelHandlerContext类内部的fire系列完成,找到当前节点的下一个节点不断循环传播。是一个过滤器形式,完成对handler 的调度。

  • 过程

    • pipeline首先会调用Context的静态方法fireXXX,并传入context

    • 然后静态方法调用context的invoker方法,而invoker方法内部会调用该context所包含的handler的真正XX方法,调用结束后,如果还需要向后传递,就调用context的firexxx2方法,循环往复。

Netty心跳服务

说明:

netty提供了IdleStateHandler,ReadTimeoutHandler,WriteTimeoutHandler三个handler检测连接的有效性

ReadTimeoutHandler和WriteTimeoutHandler会自动关闭连接,属于异常处理。重点是IdleStateHandler

基本属性:

  • private final boolean observeOutput;		//是否考虑出站慢的情况,默认为false
    private final long readerIdleTimeNanos;		//读事件的空闲时间,0则禁用事件
    private final long writerIdleTimeNanos;		//写事件的空闲时间,0则禁用事件
    private final long allIdleTimeNanos;		//读写事件的空闲时间,0则禁用事件
    

方法:

  • public void handlerAdded(ChannelHandlerContext ctx)		//当handler被添加到pipeline中时,则调用initialize方法
    
  • initialize():

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pP8APjlm-1619253586167)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210422184617463.png)]

ReaderIdleTimeoutTask的run方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wEDYphG9-1619253586169)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210422184118703.png)]

小结:

  • IdleStateHandler可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会触发用户的handler的userEventTriggered方法。用户可以在这个方法中尝试向对方发送信息,如果发送失败,则关闭连接。
  • IdleStateHandler的实现基于EventLoop的定时任务,每次读写都会记录一个值,在定时任务运行的时候,通过计算当前时间和上次时间发生的时间的结果,来判断是否空闲。
  • 内部有三个定时任务,分别对应读事件,写事件,读写事件,通常用户监听读写事件就足够了。
  • 同时IdleStateHandler内部也考虑了一些极端情况,客户端接收缓慢,一次读写数据的速度超过了设置的空闲时间。netty通过构造方法中的observeOutput属性来决定是否对出站缓冲区的情况进行判断。
  • 如果出站缓慢,netty不认为这是空闲,也就不触发空闲事件。但第一次无论如何也是要触发的。因为第一次判断无法判断是出站缓慢还是空闲。当然出站缓慢的话,可能会造成OOM,OOM比空闲时间问题大。
  • 所以当应用出现内存溢出,oom之类,并且写空闲极少发生,那就需要注意是不是数据出站速度过慢。
  • 还有一个问题就是ReadTimeoutHandler,它继承于IdleStateHandler,当触发读空闲事件的时候,就会触发 ctx.fireExceptionCaugh方法,并传入一个ReadTimeoutException,然后关闭socket。
  • WriteTimeoutHandler的是实现不是基于IdleStateHandler的,它的原理是,当调用write方法时,会创建一个定时任务,任务内容是根据传入的promise的完成情况来判断是否超过了写的时间。当定时任务根据指定时间开始运行,发现promise的isDone返回false,表明还没有写完,说明超时了,则抛出异常。当write方法完成后,会打断定时任务

NioEventLoop剖析

说明:

  • ScheduleExecutorService接口表示一个定时任务接口,EventLoop可以接受定时任务
  • EventLoop接口,一旦channel注册,就注册该channel对应的所有IO操作。
  • SingleThreadEventExecutor表示这是一个单个线程的线程池
  • EventLoop是一个单例的线程池,里面含有一个死循环的线程,不断做着三件事情,监听端口,处理端口事件,处理队列事件。每个EventLoop都可以绑定多个channel,而每个channel始终只能由一个EventLoop来处理。

小结:

  • 每次执行execute方法都是向队列中添加任务。当第一次添加时就启动线程,执行run方法,而run方法是整个eventLoop的核心,不停的loop,做三件事情
    • 调用selector的selector方法,默认阻塞一秒钟,如果有定时任务,则在定时任务剩余时间的基础上加上0.5秒进行阻塞。当执行execute方法时,也就是添加任务的时候,唤醒selector,防止selector阻塞时间过长。
    • 当selector返回的时候,回调用processSelectedKeys方法对selectKey进行处理
    • 当processSelectedKeys方法执行结束之后,则按照ioRatio的比例执行runAllTask方法,默认是IO任务时间和非IO任务时间是相同的,
    • 你也可以根据应用特点进行调优:
      • NioEventLoop.setIoRatio:设置事件循环中I/O所需时间的百分比。取值范围为1-100。默认值为50,这意味着事件循环将尝试花费与非I/O任务相同的时间用于I/O。该数字越小,可用于非I/O任务的时间就越多。如果值设置为100,则此功能将被禁用,事件循环将不会尝试平衡I/O和非I/O任务。

任务加入异步线程池

目的:

  • 在netty中做耗时的,不可预料的操作,比如数据库,网络请求,会严重影响netty对socket的处理速度。

  • 解决方法就是将任务加入到异步线程池中

    • handler中加入线程池

      •     //定义一个业务线程池
        	static final DefaultEventExecutorGroup GROUP=new DefaultEventExecutorGroup(6);
            
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                GROUP.submit(new Callable<Object>() {
                    @Override
                    public Object call() throws Exception {
                        System.out.println("这里处理耗时的事务");
                        return null;
                    }
                });
            }
        
      • AbstractChannelHandlerContext.write()

      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UScDmT9t-1619253586173)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210422214226463.png)]

    • context中添加线程

      • public class NettyServer {
            
           //在这里创建线程池
            public static final DefaultEventExecutorGroup GROUP=new DefaultEventExecutorGroup(3);
            public static void main(String[] args) throws InterruptedException {
                EventLoopGroup boosGroup = new NioEventLoopGroup(1);
                EventLoopGroup workGroup = new NioEventLoopGroup(1);
        
                try {
                ServerBootstrap bootstrap = new ServerBootstrap();
                bootstrap.group(boosGroup,workGroup)    
                        .channel(NioServerSocketChannel.class)    
                        .option(ChannelOption.SO_BACKLOG,128)  
                        .childOption(ChannelOption.SO_KEEPALIVE,true)   
                        .childHandler(new ChannelInitializer<SocketChannel>(){
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                ChannelPipeline pipeline = socketChannel.pipeline();
                                pipeline.addLast(GROUP,new NettyServerHandler());
                            }
                        });
                ChannelFuture channelFuture = bootstrap.bind(8888).sync();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K1Z5tdxz-1619253586176)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20210422220059856.png)]

小结:

  • 第一种方法handler中添加异步,可能更加的自由,比如如果需要访问数据库,可以异步执行,如果不需要就不异步执行,异步会拖动接口响应时间。因为需要将任务放到mpscTask中。如果IO时间很短,task很多,可能一个循环下来都没有时间整个task,导致响应时间达不到指标
  • 第二种方式是netty的标准方式,但是这么做整个netty都交给了业务线程,不论耗不耗时,都要加入队列里,不够灵活
  • 各有优劣,从灵活度考虑,第一种较好。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值