Netty基础学习(二)

前面两篇基础介绍了Netty的一些核心概念,本篇将从netty的设计上进行讲解。

3. Netty的线程模型

netty的线程模型是基于reactor模式的,关于reactor模式的理解,可参考Netty百万级高并发支持_西木风落的博客-优快云博客_netty高并发解决方案

在讲netty的线程模型之前,我们需要理解为什么设计出netty多种线程模型,其根本原因是我们应用系统网络通信需要,来看一下真实场景下的网络通信:

 总结起来客户端和服务端的网络交互有:

  • server启动,监听web端口;
  • client端TCP三次握手,与server建立连接,监听连接事件OP_AACEPT;
  • server通过channel从网卡中读取数据;
  • server根据通信协议解析二进制码流;
  • server执行对应的业务操作,将业务执行结果返回到client,通常涉及到协议编码、压缩等。

线程模型需要解决的问题:连接监听、网络读写、编码、解码、业务执行这些操作步骤如何运用多线程编程,提升性能。 

3.1 Reactor单线程模型

所有的IO都在一个NIO线程上完成,

由于Reactor使用的异步非阻塞IO,理论上可以处理所有的IO操作。但对于高负荷,大并发的场景,却不适合,主要原因有

  • 单NIO处理成千上百的链路,性能上无法支撑,即使NIO的CPU达到100%,仍不能满足海量的消息编码、解码,接收和发送。
  • NIO线程负载过重后,会导致处理速度变慢,造成大量客户端连接超时后重发,会更进一步加剧消息积压
  • 可靠性问题,一旦NIO进入死循环或者异常后,整个通信系统都将不可用。
public static void main(String[] args) {
        EventLoopGroup eventLoopGroup = null;
        try {
            eventLoopGroup = new NioEventLoopGroup(1);
            //创建客户端启动对象
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel socketChannel) throws Exception {
                            //字符串解码器
                            socketChannel.pipeline().addLast("docode", new StringDecoder());
                            //字符串编码器
                            socketChannel.pipeline().addLast("encode", new StringEncoder());
                            //信息读取处理器
                            socketChannel.pipeline().addLast(new ClientInputMessageHandler());
                        }
                    });
            // 绑定端口
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
            //关闭通道进行监听
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if(eventLoopGroup != null){
                eventLoopGroup.shutdownGracefully();
            }
        }
    }

    public static class ClientInputMessageHandler extends SimpleChannelInboundHandler<String>{
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
            System.out.println("服务端发送的消息:" + s);
        }
        // 建立连接时信息
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            ctx.writeAndFlush("hello 服务端...");
        }
        // 异常时处理机制
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            super.exceptionCaught(ctx, cause);
        }
    }
}

3.2 Reactor多线程模型

与单线程模型最大的区别是:

  • 有一个专门的线程Acceptor,处理客户端TCP的连接请求
  • 网络IO操作交给一个NIO线程池, 线程池中的线程处理链路的编码、解码,发送和接收
  • 一个NIO线程可以处理多个链路,但是一个链路只会属于一个NIO线程,防止并发操作引入的数据污染。

3.3 主从reactor多线程模型

在个别特殊情况下,一个acceptor线程处理高并发的连接请求,可能存在性能问题。比如,需要对客户端连接进行安全认证,认证本身是非常耗性能的。所以引入了主从模式的多线程模型。

 服务端用于接收客户端连接请求不再是单一的NIO线程,而是一个线程池。

Netty的线程模型并不是一直不变的,它取决于启动时的参数设置。Netty的最佳实践是

  • 创建两个EventLoopGroup,用于逻辑隔离Acceptor NIO和 NIO IO。
  • 尽量不要在channelHandler中启动用户线程
  • 解码要放在解码的Handler中,尽量不要放到用户线程中
  • 如果业务逻辑简单,没有复杂的业务计算,也没有可能会导致线程阻塞的磁盘、DB或者网络操作等,可以把业务逻辑放到NIO线程中完成,不要切换到用户线程
  • 如果业务逻辑复杂,尽量把业务逻辑派发到业务逻辑的线程池中,尽快释放Netty的NIO线程。

 4. netty的内存管理

4.1 ByteBuffer和ByteBuf

Java NIO为了减少频繁的IO操作,引入了ByteBuffer,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O 。ByteBuffer具有4个重要的属性:mark、position、limit、capacity ,以及两个重要的方法clear()、flip()

public abstract class Buffer {
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    ...

}

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset)
{
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}

ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
    this(mark, pos, lim, cap, null, 0);
}
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw createCapacityException(capacity);
    return new HeapByteBuffer(capacity, capacity);
}
...
}

ByteBuffer分配的时候长度固定,一旦分配完成后,容量不能动态扩展,并且ByteBuffer只有一个标识控制位position,写之前要手动调用clear(), 读之前要手动调用flip(). 

Netty自行封装了ByteBuf,增加了两个指针 readerIndex 和 writeIndex 来分别指向读的位置和写的位置,不需要每次为读写做准备,直接设置读写指针进行读写操作即可。

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
    public ByteBuf() {
    }
    public abstract int readerIndex();
    public abstract ByteBuf readerIndex(int var1);
    public abstract int writerIndex();
    public abstract ByteBuf writerIndex(int var1);
    ...
}

而且clear和flip方法仅仅是设置writerIndex和readerIndex的值,可以根据需要重复利用内存空间。而且可以控制读内容的大小。

4.2 netty的zeroCopy

Netty的零拷贝体现在三个方面:

1. Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{...
    DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
          // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
       att = null;
     }
...
}
 public static void main(String[] args) {
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer(100);
        System.out.println(byteBuf.capacity());
        byte[] data = "this is test".getBytes();
        byteBuf.writeBytes(data);
        System.out.println(byteBuf.readerIndex());
        System.out.println(byteBuf.writerIndex());

        // 观察内存占用大小
        ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(1024*1024*1024);
        System.out.println(directBuf.capacity());
    }

2. Netty提供了组合Buffer对象Composite Buffers,可以聚合多个ByteBuffer对象。传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。

public CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers) {
        this.addComponents0(increaseWriterIndex, this.components.size(), buffers, 0, buffers.length);
        this.consolidateIfNeeded();
        return this;
    }
public static void main(String[] args) {
        ByteBuf byteBuf1 = ByteBufAllocator.DEFAULT.directBuffer(100);
        byte[] dataStr1 = "this is test buf1".getBytes();
        byteBuf1.writeBytes(dataStr1);

        ByteBuf byteBuf2 = ByteBufAllocator.DEFAULT.directBuffer();
        byte[] dataStr2 = "this is test buf2. hahahaha".getBytes();
        byteBuf1.writeBytes(dataStr2);

        CompositeByteBuf byteBuf = ByteBufAllocator.DEFAULT.compositeBuffer();
        System.out.println(byteBuf.capacity());
        byteBuf.addComponents(byteBuf1, byteBuf2);
        System.out.println(byteBuf.capacity());

        byte[] data = new byte[dataStr1.length+dataStr2.length];
        byteBuf.getBytes(0, data);
        System.out.println(new String(data));
    }

 3. Unpooled.wrappedBuffer

Unpooled.wrappedBuffer 方法可以将不同的数据源的一个或者多个数据包装成一个大的 ByteBuf 对象,包装的过程中不会发生数据拷贝操作,包装后生成的 ByteBuf 对象和原始 ByteBuf 对象是共享底层的 byte 数组。

public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuffer... buffers) {
        switch (buffers.length) {
            case 0:
                break;
            case 1:
                if (buffers[0].hasRemaining()) {
                    return wrappedBuffer(buffers[0].order(BIG_ENDIAN));
                }
                break;
            default:
                List<ByteBuf> components = new ArrayList(buffers.length);
                ByteBuffer[] var3 = buffers;
                int var4 = buffers.length;
                // 利用CompositeByteBuf,将底层的byteBuf组合起来
                for(int var5 = 0; var5 < var4; ++var5) {
                    ByteBuffer b = var3[var5];
                    if (b == null) {
                        break;
                    }

                    if (b.remaining() > 0) {
                        components.add(wrappedBuffer(b.order(BIG_ENDIAN)));
                    }
                }

                if (!components.isEmpty()) {
                    return new CompositeByteBuf(ALLOC, false, maxNumComponents, components);
                }
        }

        return EMPTY_BUFFER;
    }

4. ByteBuf.slice

ByteBuf.slice 和 Unpooled.wrappedBuffer 的逻辑相反,是将一个 ByteBuf 对象切分成多个共享同一个底层存储的 ByteBuf 对象。原理是AbstractUnpooledSlicedByteBuf.adjustment可以调整buf的起始位置。

5. Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值