Netty这一篇就够啦

135 阅读8分钟

Netty核心组件:

  1. Channel
    Channel是Java NIO的一个基本构造。可以把Channel看做入站或出站数据的载体。所以可以被打开或者是被关闭,连接或者是断开连接。可以理解为socket,所有的io操作均从管道中进行,但是Netty中Channel所提供的接口,会降低Socket复杂性。Netty中会预定义一些:
  • EmbeddedChannel
  • LocalservcerChannel
  • NioDatagramChannel
  • NioSctpChannel
  • NioSocketChannel
    常用方法: IMG_8B0D325D2E01-1.jpeg
  1. 回调
    Netty在内部使用回调方式来处理事件,当一个回调被触发时,相关的事件可以被一个ChannelHandler的实现处理。
  2. Future-ChannelFuture
    可以理解为异步操作的结果占位符,在将来的某个时刻完成,并且提供访问结果的方法。跟java中线程中的futrue一样使用。 Netty中提供ChannelFuture类,每个Netty的出站I/O操作都会返回一个ChannelFuture,可以获取结果,并且不是阻塞的。
  3. 事件 Netty中使用不同的事件来通知我们状态的改变或是操作的状态。每个事件都可以被分发给ChannelHandler类中的某个用户实现的方法。(事件驱动范式)
  4. ChannelHandler 在启动时候可以将ChannelHandler注册到ChannelPipeline中,提供了入站handler接口和出站handler接口如下图,都可以被放入同一个pipeline中。 IMG_F9F9F8DEDFCF-1.jpeg 在此处涉及到适配器类。为了将自定义编写的ChannelHandler所需要的工作降到最低,提供了一些接口的默认实现。
  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
  • ChannelDuplexHandler 实际过程中还会有编码器和解码器,其实都是Handler的实现类,用于在Pipeline中进行数据的解析,便于后续的handler使用。
  1. ChannelPipeline
    由多个ChannelHandler组成ChannelPipeline,不同Channelhandler中由上下文ChannelHandlerContext用于传递数据。此处使用的是责任链模式。 IMG_541C7483FDE4-1.jpeg

ChannelPipeline接口:由多个channelhandler组成channelpipeline ChannelHandlerContext:使得ChannelHandler能够和ChannelPipeline以及其他的ChannelHandler交互。说白了实际上就是,在ChannelPipeline执行时候,是一个责任链模式,ChannelHandlerContext作为全局的上下文参数,在各个handler中进行流转。

  1. EventLoop 定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件 image.png
  • 一个EventLoopGroup包含一个或多个EventLoop
  • 一个EventLoop整个生命周期之和一个thread绑定
  • 一个Channel在整个生命周期只注册于一个EventLoop,所以总是一个线程在执行,避免多线程冲突
  • 一个EventLoop可能会分配给多个Channel
  1. 引导-Bootstrap 用于Netty启动的启动类,称为引导。为应用程序网络层配置提供了容器,涉及将一个进行绑定到某个端口。或将一个进行连接到某个指定主机的指定端口上的进程。所以分为服务端和客户端。

传输

  1. 阻塞传输 -oio
    工作流程: IMG_A88A77D9B00F-1.jpeg
  2. 异步传输 -nio
    Nio在实现上主要依赖JDK1.4时候提供的选择器, 选择器背后基本概念可以理解为是一个注册表,在那里会把可以请求在channel的状态变化得到通知。(channel的生命周期变化).
    选择器的枚举:
    IMG_C6DD8A74D5DA-1.jpeg 处理流程: IMG_1421EDD0A517-1.jpeg
  3. JVM内部传输 -Local
  4. 测试ChannelHandler时使用 -Embedded
  5. epool传输 需要系统支持

使用nett中的不同方式传输可以很快地切换,可以看到下面的示例中只需要替换EventLoopGroup的实现和对应的channel即可。

  • 使用nettyOio 传输实例:
public void server() throws InterruptedException {
        final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi", CharsetUtil.UTF_8));
        EventLoopGroup group = new OioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(group)
                .channel(OioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(9999))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel sc) throws Exception {
                        sc.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
//                                super.channelActive(ctx);
                                ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
                            }
                        });
                    }
                });
        ChannelFuture sync = b.bind().sync();
        sync.channel().closeFuture().sync();
    }
  • 使用nettyNio 传输示例:
public void server() throws InterruptedException {
        final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi", CharsetUtil.UTF_8));
        EventLoopGroup group = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(group)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(9999))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel sc) throws Exception {
                        sc.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
//                                super.channelActive(ctx);
                                ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
                            }
                        });
                    }
                });
        ChannelFuture sync = b.bind().sync();
        sync.channel().closeFuture().sync();
    }

ByteBuf 容器

Netty的数据容器ByteBufJDK中提供的是ByteBuffer,使用起来会很复杂并且繁琐

ByteBuf API的优点

  1. 用户可以自定义缓冲区类型扩展;
  2. 通过内置的符合缓冲区类型实现了透明的零拷贝;
  3. 容量可以按需增长(类似于JDK的StringBuilder)
  4. 读和写使用了不同的索引,JDK的ByteBuffer只使用了一个索引,所以每次要切换
  5. 支持方法的链式调用;
  6. 支持引用计数;
  7. 支持池化;

工作原理:
IMG_373306942781-1.jpeg 拥有 readerIndex 和 writeIndex,readerIndex 代表可以读的位置,writeIndex代表写的位置,如果使用时候进行越界都会进行报错抛出异常提示。readerIndex前面的称为可丢弃字节,writeIndex后面的称为可写字节。因为内部是个数组,当发生丢弃操作时候,实际上是覆盖操作。此处和JDK的区别是JDK提供的ByteBuffer只提供一个索引,所以每次使用时候需要filp()方法进行切换,使用Netty的ByteBuf不需要操心这些。

使用模式:

  1. 堆缓冲区
    存在jvm内部堆空间中。被称为支撑数组模式,使用垃圾回收机制进行回收。
  2. 直接缓冲区
    使用本地内存。主要目的是避免每次调用本地I/O之前或之后将缓冲区的内容复制到一个中间缓冲区。内存的分配和释放会比较昂贵,使用时需要将本地内存数据复制到jvm中。
  3. 复合缓冲区 既可以包含堆缓冲区,又可以包含直接缓存区。多个ByteBuf可以放在一起使用,作为一个聚合视图。

派生缓冲区

可以通过以下几个方法进行创建:duplicate(),slice(),Unpooled.unmodifiableBuffer(),order(ByteOrder),readSlice()进行创建,称之为派生缓冲区。实际上是与原缓冲区共用一个底层对象,相当于副本,对数据改动同时影响原有对象,。

使用copy()方法可以进行复制操作,完全复制出一个ByteBuf,可以理解为是深拷贝,与源对象不冲突。

常用的方法

get()和他set()操作不会改变索引中的数组,write()和read()会改变索引的位置。

  1. get() IMG_7E4E400289AF-1.jpeg
  2. set() IMG_C4A8A29D912D-1.jpeg
  3. read() IMG_1ADC74919DD2-1.jpeg
  4. wirte() IMG_41CC9A0BF9AC-1.jpeg

ByteBuf分配

  1. 按需分配 ByteBufAllocator 接口,是一个工厂,可以创建不同类型的ByteBuf,可以创建池化的ByteBuf,基于堆或是基于直接内存的,或是组合的。常用的api image.png
  2. Unpooled 缓冲区 创建一个非池化的bytebuf image.png

引用计数

引用计数是池化的关键所在,通过某个对象所持有的的资源不在被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。实现了接口 ReferenceCounted,释放需要手动释放,容易出现内存泄漏。一般来说,是由最后访问的对象来负责将它释放。

ChannelHandler相关

channel的生命周期

  1. channel被创建
  2. 被注册到eventloop
  3. 处于活跃状态
  4. 没有连接到远程节点。

channelhandler的生命周期

  1. 将channelhandler加入到channelpipeline中
  2. 从channelhandler从channelpipeline中移除
  3. 在处理过程中在channelpipeline中有错误产生时候被调用。

ChannelInboundHandler接口的生命周期:

image.png

ChannelOutboundHandler接口的生命周期:

image.png

Handler适配器

适配器提供了接口的基本实现,能够让开发者快速的使用其中某个节点的动作。handler的类层次结构图: image.png

channel的内存泄漏检测方式

  1. DISABLED:禁用内存泄漏检测
  2. SIMPLE:使用1%的默认采样检测并且报告任何发现的泄漏,默认级别。
  3. ADVANCED:使用默认的采样率,报告所发现的任何的泄漏以及对应的消息被访问的位置。
  4. PARANOID:对每次的样本都进行采样,对性能有影响,一般用于调试阶段使用。

EventLoop和线程模型

EventLoop接口:
事件循环。所有的事件都交由eventloop进行处理,netty的Eventloop是协同设计的一部分,采用了两个基本的API:并发和网络编程。 eventloop的类的层次结构: image.png 一个EventLoop由一个不会改变的线程thread驱动,任务可以直接提交给EventLoop实现,立即执行。

  • io操作触发的事件将流经安装了一个或者多个的channelhandler的channelpipeline。传播这些事件的方法调用可以随后被channelhandler所拦截,并且可以按需地处理事件。 (重点)所有的io操作和时间都由已经被分配给了eventloop的那个thread来处理。

  • eventloop提供的任务调度。当提交了一个调度任务,分配到eventloop中,如果线程为eventloop所在的线程,那么任务会直接被执行,如果不是将会被放入eventloop的队列中,等在事件触发进行执行。不要将一个长时间执行的任务放到队列中,因为只有一个线程在执行,会导致后面的任务晚执行。 EventLoop的执行逻辑: image.png

EventLoop线程的分配:

1.异步传输:一个eventloop 只有一个线程,一个eventloop下面会有多个channel。 image.png 2.阻塞传输: 一个eventloop 只有一个线程,一个eventloop下面只有channel。 image.png

支持websocket

从http或者https协议转换到 websock时,会使用一个种成为升级握手的机制。会判断url中的数据。 image.png