Netty

JDK 原生 NIO 程序的问题

1)NIO 的类库和 API 繁杂,使用麻烦:你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
2)需要具备其他的额外技能做铺垫:例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。
3)可靠性能力补齐,开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。
4)JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。

事件驱动模型

  • 主要包括 4 个基本组件:
    • 1)事件队列(event queue):接收事件的入口,存储待处理事件;
    • 2)分发器(event mediator):将不同的事件分发到不同的业务逻辑单元;
    • 3)事件通道(event channel):分发器与处理器之间的联系渠道;
    • 4)事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作。

Reactor 线程模型

  • Reactor 模型中有 2 个关键组成:
    • 1)Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。
    • 2)Handlers:处理程序执行 I/O 事件要完成的实际操作,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

Netty高性能的原理

netty的高性能体现在I/O 模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。

  • Netty I/O 模型
    基于NIO,Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端连接。

  • Netty线程模型
    Netty 主要基于主从 Reactors 多线程模型。

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

    特 别 说 明 的 是 : 虽 然 N e t t y 的 线 程 模 型 基 于 主 从 R e a c t o r 多 线 程 , 借 用 了 M a i n R e a c t o r 和 S u b R e a c t o r 的 结 构 。 但 实 际 实 现 上 S u b R e a c t o r 和 W o r k e r 线 程 在 同 一 个 线 程 池 中 , 如 下 代 码 : \color{red}{特别说明的是:虽然 Netty 的线程模型基于主从 Reactor 多线程,借用了 MainReactor 和 SubReactor 的结构。但实际实现上 SubReactor 和 Worker 线程在同一个线程池中,如下代码:} Netty线Reactor线MainReactorSubReactorSubReactorWorker线线:

    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    ServerBootstrap server = new ServerBootstrap();
    server.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)

    上面代码中的 bossGroup 和 workerGroup 是 Bootstrap 构造方法中传入的两个对象,这两个 group 均是线程池:
    1)bossGroup 线程池则只是在 Bind 某个端口后,获得其中一个线程作为 MainReactor,专门处理端口的 Accept 事件,每个端口对应一个 Boss 线程;
    2)workerGroup 线程池会被各个 SubReactor 和 Worker 线程充分利用。

netty中的异步处理

  • Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。

  • 当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。
    常见有如下操作:
    1)通过 isDone 方法来判断当前操作是否完成;
    2)通过 isSuccess 方法来判断已完成的当前操作是否成功;
    3)通过 getCause 方法来获取已完成的当前操作失败的原因;
    4)通过 isCancelled 方法来判断已完成的当前操作是否被取消;
    5)通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则立即通知指定的监听器。

  • 代码示例

Netty框架的工作原理

  • NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。
    每个 Boss NioEventLoop 循环执行的任务包含 3 步:

    1)轮询 Accept 事件;
    2)处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上;
    3)处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。

  • 每个 Worker NioEventLoop 循环执行的任务包含 3 步:
    1)轮询 Read、Write 事件;
    2)处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理;
    3)处理任务队列中的任务,runAllTasks。

    • 其中任务队列中的 Task 有 3 种典型使用场景:
      ① 用户程序自定义的普通任务:

      ② 非当前 Reactor 线程调用 Channel 的各种方法:
      例如在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费。

      ③ 用户自定义定时任务:

Netty的核心组件

NioEventLoopGroup

  • 主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。

  • NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。

  • 职责:
    (1). 作为服务端 Acceptor 线程,负责处理客户端的请求接入。
    (2). 作为客户端 Connector 线程,负责注册监听连接操作位,用于判断异步连接结果。
    (3). 作为IO 线程,监听网络读操作位,负责从 SocketChannel 中读取报文。
    (4). 作为 IO 线程,负责向 SocketChannel 写入报文发送给对方,如果发生写半包,会自动注册监听写事件,用于后续继续发送半包数据,直到数据全部发送完成。
    (5). 作为定时任务线程,可以执行定时任务,例如链路空闲检测和发送心跳消息等。
    (6). 作为线程执行器可以执行普通的任务线程(Runnable)。

NioEventLoop

  • NioEventLoop 中维护了一个线程和任务队列 taskQueue,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务
    (1) I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
    (2) 非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。

  • 每个NioEventLoop都绑定了一个Selector,所以在Netty的线程模型中,是由多个Selector在监听IO就绪事件。而Channel注册到Selector。

    • 一个Channel绑定一个NioEventLoop,相当于一个连接绑定一个线程,这个连接所有的ChannelHandler都是在一个线程中执行的,避免了多线程干扰。更重要的是ChannelPipline链表必须严格按照顺序执行。单线程的设计能够保证ChannelHandler的顺序执行。
    • 一个NioEventLoop的selector可以被多个Channel注册,也就是说多个Channel共享一个EventLoop。EventLoop的Selecctor对这些Channel进行监听。
  • 注意, 因为 EventLoop 既需要执行 IO 操作, 又需要执行 task, 因此我们在调用 EventLoop.execute 方法提交任务时, 不要提交耗时任务, 更不能提交一些会造成阻塞的任务, 不然会导致我们的 IO 线程得不到调度, 影响整个程序的并发量。
    解决方法: Netty本身提供了EventExecutorGroup作为业务操作线程池,使用示例如下:

    问题又来了,前面说过Netty是采用串行化设计理念处理连接上的IO操作的,这样做可以避免线程竞争,线程上下文切换带来的额外性能损耗。那如果NIO线程与业务线程同时往channel里写数据,这个不是破坏了Netty的串行化理念。刚开始我也这么想,可后来看到Netty底层的write方法才发现并不存在这个问题。

    这里会判断当前EventExecutor是否是NIO线程,如果是则执行Write操作,否则仅仅是创建一个异步Task,执行该异步Task,该异步Task会到该连接对应的NIO线程里去执行,最终保证还是一个NIO线程串行化地处理该连接上的IO操作。
    当然如果不想用EventExecutorGroup,也可以像最下面的示例一样,将长时间的任务丢进自己的业务线程池里处理。

ChannelHandler

ChannelPipeline不是单独存在,它肯定会和Channel、ChannelHandler、ChannelHandlerContext关联在一起,所以有关概念这里一起讲。另外接下来讲述的几个概念可以让你在无形中搞清楚 C h a n n e l H a n d l e r 单 线 程 顺 序 执 行 的 原 理 \color{red}{ChannelHandler单线程顺序执行的原理} ChannelHandler线

  • 概念

    ChannelHandler下主要是两个子接口

    • ChannelInboundHandler(入站): 处理输入数据和Channel状态类型改变。
      适配器: ChannelInboundHandlerAdapter(适配器设计模式)
      常用的: SimpleChannelInboundHandler
    • ChannelOutboundHandler(出站): 处理输出数据
      适配器: ChannelOutboundHandlerAdapter

    每一个Handler都一定会处理出站或者入站(可能两者都处理数据),例如对于入站的Handler可能会继承SimpleChannelInboundHandler或者ChannelInboundHandlerAdapter,而SimpleChannelInboundHandler又是继承于ChannelInboundHandlerAdapter,最大的区别在于SimpleChannelInboundHandler会对没有外界引用的资源进行一定的清理,并且入站的消息可以通过泛型来规定。

  • Channel 生命周期(执行顺序也是从上倒下)
    (1)channelRegistered: channel注册到一个EventLoop。
    (2)channelActive: 变为活跃状态(连接到了远程主机),可以接受和发送数据
    (3)channelInactive: channel处于非活跃状态,没有连接到远程主机
    (4)channelUnregistered: channel已经创建,但是未注册到一个EventLoop里面,也就是没有和Selector绑定

  • ChannelHandler 生命周期
    handlerAdded: 当 ChannelHandler 添加到 ChannelPipeline 中调用
    handlerRemoved: 当 ChannelHandler 从 ChannelPipeline 移除时调用
    exceptionCaught: 当 ChannelPipeline 执行抛出异常时调用

ChannelPipeline

  • 概念

    ChannelPipeline类是ChannelHandler实例对象的链表,用于处理或截获通道的接收和发送数据。它提供了一种高级的截取过滤模式(类似serverlet中的filter功能),让用户可以在ChannelPipeline中完全控制一个事件以及如何处理ChannelHandler与ChannelPipeline的交互。
    对于每个新的通道Channel,都会创建一个新的ChannelPipeline,并将其pipeline附加到channel中。

    下图描述ChannelHandler与pipeline中的关系,一个io操作可以由一个ChannelInboundHandler或ChannelOutboundHandle进行处理,并通过调用ChannelInboundHandler处理入站io或通过ChannelOutboundHandler处理出站IO。

  • 常用方法

    addFirst(...)   //添加ChannelHandler在ChannelPipeline的第一个位置
    addBefore(...)   //在ChannelPipeline中指定的ChannelHandler名称之前添加ChannelHandler
    addAfter(...)   //在ChannelPipeline中指定的ChannelHandler名称之后添加ChannelHandler
    addLast(...)   //在ChannelPipeline的末尾添加ChannelHandler
    remove(...)   //删除ChannelPipeline中指定的ChannelHandler
    replace(...)   //替换ChannelPipeline中指定的ChannelHandler
    

    ChannelPipeline可以动态添加、删除、替换其中的ChannelHandler,这样的机制可以提高灵活性。示例:

    1.    ChannelPipeline pipeline = ch.pipeline(); 
    2.    FirstHandler firstHandler = new FirstHandler(); 
    3.    pipeline.addLast("handler1", firstHandler); 
    4.    pipeline.addFirst("handler2", new SecondHandler()); 
    5.    pipeline.addLast("handler3", new ThirdHandler()); 
    6.    pipeline.remove("“handler3“"); 
    7.    pipeline.remove(firstHandler); 
    8.    pipeline.replace("handler2", "handler4", new FourthHandler());
    
  • 入站出站Handler执行顺序
    一般的项目中,inboundHandler和outboundHandler有多个,在Pipeline中的执行顺序?
    重点记住: InboundHandler顺序执行OutboundHandler逆序执行

    问题: 下面的handel的执行顺序?

    ch.pipeline().addLast(new InboundHandler1());
    ch.pipeline().addLast(new OutboundHandler1());
    ch.pipeline().addLast(new OutboundHandler2());
    ch.pipeline().addLast(new InboundHandler2());
      或者:
    ch.pipeline().addLast(new OutboundHandler1());
    ch.pipeline().addLast(new OutboundHandler2());
    ch.pipeline().addLast(new InboundHandler1());
    ch.pipeline().addLast(new InboundHandler2());
    

    其实上面的执行顺序都是一样的:

    InboundHandler1--> InboundHandler2 -->OutboundHandler2 -->OutboundHandler1
    

    结论
    1)InboundHandler顺序执行,OutboundHandler逆序执行
    2)InboundHandler之间传递数据,通过ctx.fireChannelRead(msg)
    3)InboundHandler通过ctx.write(msg),则会传递到outboundHandler
    4) 使用ctx.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然outboundhandler会不执行;但是使用channel.write(msg)、pipline.write(msg)情况会不一致,都会执行,那是因为channel和pipline会贯穿整个流。
    5) outBound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接受数据,先outbound再inbound,服务端则相反。

ChannelHandlerContext

  • ChannelPipeline并不是直接管理ChannelHandler,而是通过ChannelHandlerContext来间接管理,这一点通过ChannelPipeline的默认实现DefaultChannelPipeline可以看出来。

  • DefaultChannelHandlerContext和DefaultChannelPipeline是ChannelHandlerContext和ChannelPipeline的默认实现,在DefaultPipeline内部DefaultChannelHandlerContext组成了一个双向链表。 我们看下DefaultChannelPipeline的构造函数:

/**
  * 可以看到,DefaultChinnelPipeline 内部使用了两个特殊的Hander 来表示Handel链的头和尾。
  */
 public DefaultChannelPipeline(AbstractChannel channel) {
        if (channel == null) {
            throw new NullPointerException("channel");
        }
        this.channel = channel;
 
        TailHandler tailHandler = new TailHandler();
        tail = new DefaultChannelHandlerContext(this, null, generateName(tailHandler), tailHandler);
 
        HeadHandler headHandler = new HeadHandler(channel.unsafe());
        head = new DefaultChannelHandlerContext(this, null, generateName(headHandler), headHandler);
 
        head.next = tail;
        tail.prev = head;
    }
  • 所以对于DefaultChinnelPipeline它的Handler头部和尾部的Handler是固定的,我们所添加的Handler是添加在这个头和尾之前的Handler。(下面这个图更加清晰)

pipeline中组件的关系

  • 先大致说下什么是Channel

    通常来说, 所有的 NIO 的 I/O 操作都是从 Channel 开始的. 一个 channel 类似于一个 stream。在Netty中,Channel是客户端和服务端建立的一个连接通道。
    虽然java Stream 和 NIO Channel都是负责I/O操作,但他们还是有许多区别的:
    1)我们可以在同一个 Channel 中执行读和写操作, 然而同一个 Stream 仅仅支持读或写。
    2)Channel 可以异步地读写, 而 Stream 是阻塞的同步读写。
    3)Channel 总是从 Buffer 中读取数据, 或将数据写入到 Buffer 中。

    几者的关系图如下:

  • 总结
    一个Channel包含一个ChannelPipeline,创建Channel时会自动创建一个ChannelPipeline,每个Channel都有一个管理它的pipeline,这关联是永久性的。
    每一个ChannelPipeline中可以包含多个ChannelHandler。所有ChannelHandler都会顺序加入到ChannelPipeline中,ChannelHandler实例与ChannelPipeline之间的桥梁是ChannelHandlerContext实例。

pipeline的整个传播流程

为了搞清楚事件如何在Pipeline里传播,让我们从Channel的抽象子类AbstractChannel开始,下面是AbstractChannel#write()方法的实现:

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
    // ...
    @Override
    public Channel write(Object msg) {
        return pipeline.write(msg);
    }
    // ...
}

AbstractChannel直接调用了Pipeline的write()方法:

再看DefaultChannelPipeline的write()方法实现:

final class DefaultChannelPipeline implements ChannelPipeline {
    // ...
    @Override
    public ChannelFuture write(Object msg) {
        return tail.write(msg);
    }
    // ...
}

因为write是个outbound事件,所以DefaultChannelPipeline直接找到tail部分的context,调用其write()方法:

接着看DefaultChannelHandlerContext的write()方法

final class DefaultChannelHandlerContext extends DefaultAttributeMap implements ChannelHandlerContext {
    // ...
    @Override
    public ChannelFuture write(Object msg) {
        return write(msg, newPromise());
    }
 
    @Override
    public ChannelFuture write(final Object msg, final ChannelPromise promise) {
        if (msg == null) {
            throw new NullPointerException("msg");
        }
 
        validatePromise(promise, true);
 
        write(msg, false, promise);
 
        return promise;
    }
 
    private void write(Object msg, boolean flush, ChannelPromise promise) {
        DefaultChannelHandlerContext next = findContextOutbound();
        next.invokeWrite(msg, promise);
        if (flush) {
            next.invokeFlush();
        }
    }
 
    private DefaultChannelHandlerContext findContextOutbound() {
        DefaultChannelHandlerContext ctx = this;
        do {
            ctx = ctx.prev;
        } while (!ctx.outbound);
        return ctx;
    }
 
    private void invokeWrite(Object msg, ChannelPromise promise) {
        try {
            ((ChannelOutboundHandler) handler).write(this, msg, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    }
 
    // ...
}

context的write()方法沿着context链往前找,直至找到一个outbound类型的context为止,然后调用其invokeWrite()方法

invokeWrite()接着调用handler的write()方法

最后看看ChannelOutboundHandlerAdapter的write()方法实现:

public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelOutboundHandler {
    // ...
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        ctx.write(msg, promise);
    }
    // ...
}

默认的实现调用了context的write()方法而不做任何处理,这样write事件就沿着outbound链继续传播:

可见,Pipeline的事件传播,是靠Pipeline、Context和Handler共同协作完成的。

Server端示例代码

public static void main(String[] args) {
       // 创建mainReactor
       NioEventLoopGroup boosGroup = newNioEventLoopGroup();
       // 创建工作线程组
       NioEventLoopGroup workerGroup = newNioEventLoopGroup();
 
       finalServerBootstrap serverBootstrap = newServerBootstrap();
               .group(boosGroup, workerGroup)    // 组装NioEventLoopGroup
               .channel(NioServerSocketChannel.class)    // 设置channel类型为NIO类型
               .option(ChannelOption.SO_BACKLOG, 1024)    // 设置连接配置参数
               .childOption(ChannelOption.SO_KEEPALIVE, true)
               .childOption(ChannelOption.TCP_NODELAY, true)
               .childHandler(newChannelInitializer<NioSocketChannel>() {    // 配置入站、出站事件handler
                   @Override
                   protectedvoidinitChannel(NioSocketChannel ch) {
                       ch.pipeline().addLast(...);    // 配置入站、出站事件channel
                       ch.pipeline().addLast(...);
                   }
           });
 
           int port = 8080;    // 绑定端口
           serverBootstrap.bind(port).addListener(future -> {
               if(future.isSuccess()) {
                   System.out.println(newDate() + ": 端口["+ port + "]绑定成功!");
               } else{
                   System.err.println("端口["+ port + "]绑定失败!");
               }
           });
    }

TCP 粘包/拆包的原因及解决方法

  • TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。

  • TCP粘包/分包的原因:

    • 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象。而应用程序写入数据小于套接字缓冲区大小,网卡将应用程序多次写入的数据发送到网络上,这将会发生粘包现象;
    • 进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
    • 以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。
  • 解决方法

    • 消息定长:FixedLengthFrameDecoder类
    • 包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder或自定义分隔符类 :DelimiterBasedFrameDecoder
    • 将消息分为消息头和消息体:LengthFieldBasedFrameDecoder类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值