二、Netty核心组件

Netty的核心组件有:

  • Bootstrap
  • EventLoopGroup
  • Channel
  • ChannelHandler
  • ChannelPipeline
  • ChannelHandlerContext
  • ChannelOption
  • ByteBuf
  • ChannelFuture

Bootstrap

Bootstrap负责装配Netty的其他组件和启动服务。从上个例子可以看到,Netty的组件较多,如果不使用Bootstrap而是自己负责装配Netty组件,那工作量就很大了。Bootstrap又分为ServerBootstrap和Bootstrap。ServerBootstrap负责装配Netty服务器,Bootstrap负责装配客户端。

现在看下客户端Bootstrap的用法:

 public static void startClient() throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();

        ChannelFuture connectFuture = bootstrap.group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .remoteAddress(new InetSocketAddress("localhost", 10800))
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast();
                    }
                })
                .connect();

        connectFuture.sync();

        Channel channel = connectFuture.channel();

        channel.writeAndFlush();

    }

客户端Bootstrap也需要装配ventLoopGroup,对应的Channel类型,remoteAddress配置要连接的远程服务器地址,设置底层的TCP选项。因为是客户端,所以没有父子通道的概念。所以配置的选项和handle处理器的方法名是optionhandler。最后调用connect连接远程服务器。这不会阻塞,因为Netty是异步的,会返回ChannelFuture。可以通过ChannelFuture获取Channel,就可以读取和写入了。

EventLoopGroup

EventLoopGroup扩展了Reactor线程的功能,用一个线程池实现了多个Reactor线程负责的工作,充分利用了现代CPU的并发处理能力。EventLoopGroup按照服务端的功能又分了两类,一类是Boss EventLoopGroup负责监听客户端的连接,另一类是处理IO及数据传输,称为Work EventLoopGroup。Boss EventLoopGroup监听客户端的连接,接受客户端的连接后将之后的IO处理及数据传输都交给Work EventLoopGroup。

从继承结构上看,OioEventLoopGroup是兼容BIO的,已经过时。还有EmbeddedEventLoop是测试时使用。EpollEventLoopGroup是Linux系统下使用,Linux系统提供了epoll多路复用支持。KQueueEventLoopGroup是在其他类Unix系统上使用。

Channel

netty中的channel提供的功能类似NIO中的SocketChannel,都是数据传输的通道。不过Netty中的channel提供的功能更丰富。主要提供了以下几类功能:
1、获取EventLoop,ChannelFuture等相关的方法
2、判断状态的API,比如isOpen,isRegistered,isActive等
3、出发IO事件的方法,比如read,write

ChannelHandler,ChannelHandlerContext,ChannelPipeline

ChannelHandler是处理逻辑业务的地方。可以从channel中读取,也可以写入数据到channel中,然后进行业务处理。ChannelHandler分为ChannelInboundHandler和ChannelOutboundHandler。ChannelInboundHandler负责读取数据时处理数据。ChannelOutboundHandler负责在写入数据时处理数据。

在初探netty中,ServerBootstrap用childHandler方法配置ChannelHandler时:

	childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline().addLast(new DiscardChannelHandle());
                }
            })

是将ChannelHandler添加到了Channel中的pipeline中。也就是说每个channel都有自己的一条pipeline,由pipeline来调用ChannelHandler进行业务逻辑处理。pipeline的默认实现是DefaultChannelPipeline。pipeline是个双向链表,DefaultChannelPipeline有head,tail两个属性,分别指向双向链表的头和尾。不管是ChannelInboundHandler,还是ChannelOutboundHandler,都需要加入到pipeline中。

每条pipeline如上图所示。当从channel中读取时,数据从head往后传递,如果是ChannelInboundHandler(入站handle),则该handler处理数据后往后传递。直到tail。如果不想往后传递,也可以从中间中断。ChannelInboundHandler可以继承ChannelInboundHandlerAdapter,在channelRead方法中调用super.channelRead(ctx, msg) 或调用ctx.fireChannelRead(msg)将数据往下一站传递。如果不想传递到下一站,不调用这些方法即可。

当写入数据到channel中时,数据从tail往前传递,如果遇到ChannelOutboundHandler(出站handle)则处理,写入数据时不能从pipeline中中断。

那什么是ChannelHandlerContext呢?pipeline中的每个节点不是ChannelHandler,而是ChannelHandlerContext。ChannelHandlerContext封装了pipeline和ChannelHandler。

    ChannelHandlerContext fireChannelRegistered();
    
    ChannelHandlerContext fireChannelUnregistered();

    ChannelHandlerContext fireChannelActive();

    ChannelHandlerContext fireChannelInactive();

    ChannelHandlerContext fireExceptionCaught(Throwable var1);

    ChannelHandlerContext fireUserEventTriggered(Object var1);

    ChannelHandlerContext fireChannelRead(Object var1);

    ChannelHandlerContext fireChannelReadComplete();

    ChannelHandlerContext fireChannelWritabilityChanged();

从ChannelHandlerContext接口定义看到,有很多firexxx方法。当调用这些方法(看作是发生了事件)时,pipeline中的每个节点会执行相应的方法来相应相应的事件。

ChannelOption

ChannelOption设置底层tcp的属性,影响tcp的行为。平时使用最多的是TCP协议。比较重要的几个tcp属性已经在初探netty讨论,这里不在过多赘述。

ByteBuf

ByteBuf类似NIO中的ByteBuffer,但是比ByteBuffer更好用,提供的功能更多。

ByteBuf分成了四个部分,readerIndex之前是已废弃的部分,readerIndex与writerIndex之间的数据是已读的数据,capacity是初始容量,maxCapacity是最大容量。writerIndex与capacity之间是可写的部分。writerIndex与maxCapacity之间是最大可写的部分。当写入的数据大于capacity时,capacity会增大扩容。

    @Test
    public void test1() {
        ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer(10, 100);

        print(buffer);

        buffer.writeInt(4);
        print(buffer);

        buffer.writeInt(4);
        buffer.writeInt(4);
        print(buffer);
    }

    public void print(ByteBuf buffer) {
        System.out.println("--------------------------------------");
        System.out.println("readerIndex="+buffer.readerIndex());
        System.out.println("readableBytes="+buffer.readableBytes());
        System.out.println("writerIndex="+buffer.writerIndex());
        System.out.println("writableBytes="+buffer.writableBytes());
        System.out.println("capacity="+buffer.capacity());
        System.out.println("maxCapacity="+buffer.maxCapacity());
        System.out.println("maxWritableBytes="+buffer.maxWritableBytes());
        System.out.println("--------------------------------------");
    }
    

PooledByteBufAllocator是内存分配器,是一种池化分配器,对已分配的内存重复使用。还有UnpooledByteBufAllocator是非池化的内存分配器。buffer方法分配的是java堆内存。还可以调用这两种内存分配器的directBuffer方法分配堆外内存。

分配速度 使用效率
java堆内存
堆外内存

java堆内存可以由jvm回收,不担心内存泄漏。但是堆外内存必须要手动回收,避免内存泄漏。

ByteBuf的readXX系列方法会改变readerIndex,而getXX系列方法不会改变readerIndex。

    @Test
    public void test2() {
        ByteBuf buffer = UnpooledByteBufAllocator.DEFAULT.buffer(10, 100);

        print(buffer);

        buffer.writeInt(4);
        print(buffer);

        buffer.writeInt(5);
        print(buffer);
        
        int i = buffer.readInt();
        System.out.println(i);
        print(buffer);

        int anInt = buffer.getInt(8);
        System.out.println(anInt);
        print(buffer);
    }

Netty还实现了零拷贝。零拷贝是不需要将数据从一个内存区拷贝到另一个内存区。操作系统层面上的零拷贝是避免在内核态和用户态之间来回拷贝数据,通常是通过mmap将内核缓冲区映射到用户缓冲区。而Netty的零拷贝与操作系统的零拷贝有些不一样:

1、Netty的零拷贝是用户层面。

2、Netty提供了CompositeByteBuf类,将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了多个ByteBuf之间的拷贝。

很多协议都分成了几部分,比如常用的http请求,分成了请求头和请求体。

    @Test
    public void test3() {
        ByteBuf head = PooledByteBufAllocator.DEFAULT.buffer();
        ByteBuf body = PooledByteBufAllocator.DEFAULT.buffer();

        head.writeBytes(); // 写入数据
        body.writeBytes(); // 写入数据

        ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer(head.readableBytes() + body.readableBytes());
        buffer.writeBytes(head);
        buffer.writeBytes(body); 
        
        // 写入buff
    }

有时要分别操作头部和请求体,之后在合在一起发送给客户端。这样就需要两次拷贝。

    @Test
    public void test4() {
        ByteBuf head = PooledByteBufAllocator.DEFAULT.buffer();
        ByteBuf body = PooledByteBufAllocator.DEFAULT.buffer();
        head.writeBytes(); // 写入数据
        body.writeBytes(); // 写入数据

        CompositeByteBuf byteBufs = PooledByteBufAllocator.DEFAULT.compositeBuffer();
        byteBufs.addComponents(true,head,body);
        // byteBufs
    }

注意:byteBufs.addComponents(true,head,body)中的第一个参数,true表示会增加writerIndex。如果是调用byteBufs.addComponents(head,body)则不会增加writerIndex。此时读取会读不到数据。

如果用CompositeByteBuf将head和body组合成一个逻辑上的ByteBuf,就可以节省了两次拷贝。还可以调用ByteBuf byteBuf = Unpooled.wrappedBuffer(head, body)也可以实现相同的功能。

3、使用wrap将byte[],ByteBuf,ByteBuffer包装成一个ByteBuf,避免了拷贝。

像平时将byte[]写入ByteBuf:

ByteBuf buffer = PooledByteBufAllocator.DEFAULT.buffer();
byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8);
buffer.writeBytes(bytes);

调用ByteBuf的writeBytes方法将byte[]复制到ByteBuf的内部缓冲区。但是这涉及到了拷贝。

Unpooled工具类提供了wrappedXX系列方法,用于将一个或多个byte[],ByteBuf,ByteBuffer包装成一个ByteBuf。

ByteBuf wrappedBuffer(byte[] array)
ByteBuf wrappedBuffer(byte[] array, int offset, int length)
ByteBuf wrappedBuffer(ByteBuffer buffer)
ByteBuf wrappedBuffer(long memoryAddress, int size, boolean doFree)
ByteBuf wrappedBuffer(ByteBuf buffer)
ByteBuf wrappedBuffer(byte[]... arrays)
ByteBuf wrappedBuffer(ByteBuf... buffers)
ByteBuf wrappedBuffer(ByteBuffer... buffers)
ByteBuf wrappedBuffer(int maxNumComponents, byte[]... arrays) 
ByteBuf wrappedBuffer(int maxNumComponents, ByteBuf... buffers)
ByteBuf wrappedBuffer(int maxNumComponents, ByteBuffer... buffers)

以上这些方法并不涉及拷贝操作,包装后的ByteBuf与传入的参数使用的是同一个内存。

4、使用slice操作实现零拷贝

slice操作与wrap操作正好相反,slice将一个ByteBuf分成了几个逻辑上的ByteBuf。这些ByteBuf共用同一块内存

ByteBuf slice()
ByteBuf slice(int index, int length)

ByteBuf提供了两个slice方法,将同一块ByteBuf划分成几个逻辑上的ByteBuf。

如上图所示,head和body使用slice操作同一个ByteBuf后,修改了属于自己的writerIndex和readerIndex,但是还是指向同一块内存。

5、通过FileRegion包装的FileChannel.tranferTo实现文件传输,避免拷贝

看下官网的例子:

public class FileServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush("HELLO: Type the path of the file to retrieve.\n");
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        RandomAccessFile raf = null;
        long length = -1;
        try {
            raf = new RandomAccessFile(msg, "r");
            length = raf.length();
        } catch (Exception e) {
            ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');
            return;
        } finally {
            if (length < 0 && raf != null) {
                raf.close();
            }
        }

        ctx.write("OK: " + raf.length() + '\n');
        if (ctx.pipeline().get(SslHandler.class) == null) {
            // SSL not enabled - can use zero-copy file transfer.
            ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));
        } else {
            // SSL enabled - cannot use zero-copy file transfer.
            ctx.write(new ChunkedFile(raf));
        }
        ctx.writeAndFlush("\n");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();

        if (ctx.channel().isActive()) {
            ctx.writeAndFlush("ERR: " +
                    cause.getClass().getSimpleName() + ": " +
                    cause.getMessage() + '\n').addListener(ChannelFutureListener.CLOSE);
        }
    }
}

FileRegion封装了FileChannel,直接将数据发送到Channel。

ChannelFuture

在Netty中所有的IO操作都是异步的,所有的IO操作都将立刻返回。这是通过返回ChannelFuture实现的。

public interface ChannelFuture extends Future<Void> {

    /**
     * Returns a channel where the I/O operation associated with this
     * future takes place.
     */
    Channel channel();

    @Override
    ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> listener);

    @Override
    ChannelFuture addListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);

    @Override
    ChannelFuture removeListener(GenericFutureListener<? extends Future<? super Void>> listener);

    @Override
    ChannelFuture removeListeners(GenericFutureListener<? extends Future<? super Void>>... listeners);

    @Override
    ChannelFuture sync() throws InterruptedException;

    @Override
    ChannelFuture syncUninterruptibly();

    @Override
    ChannelFuture await() throws InterruptedException;

    @Override
    ChannelFuture awaitUninterruptibly();

    /**
     * Returns {@code true} if this {@link ChannelFuture} is a void future and so not allow to call any of the
     * following methods:
     * <ul>
     *     <li>{@link #addListener(GenericFutureListener)}</li>
     *     <li>{@link #addListeners(GenericFutureListener[])}</li>
     *     <li>{@link #await()}</li>
     *     <li>{@link #await(long, TimeUnit)} ()}</li>
     *     <li>{@link #await(long)} ()}</li>
     *     <li>{@link #awaitUninterruptibly()}</li>
     *     <li>{@link #sync()}</li>
     *     <li>{@link #syncUninterruptibly()}</li>
     * </ul>
     */
    boolean isVoid();
}

还可以通过ChannelFuture获取到对应的Channel。同时还可以添加GenericFutureListener:

public interface GenericFutureListener<F extends Future<?>> extends EventListener {

    /**
     * Invoked when the operation associated with the {@link Future} has been completed.
     *
     * @param future  the source {@link Future} which called this callback
     */
    void operationComplete(F future) throws Exception;
}

当IO操作完成时会调用这些GenericFutureListener。

还可以调用ChannelFuture.sync让异步操作变成同步等待执行完成。

posted @ 2024-10-19 16:04  shigp1  阅读(41)  评论(0编辑  收藏  举报