netty应用(一)--netty入门及组件

1.概述

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端。

注意:这里的异步不要和异步IO混淆,这里的异步指Netty使用多线程来完成结果传送,让发送请求的线程不阻塞。

优点

Netty vs NIO

  • Nio开发工作量大,bug多
  • 需要自己构建协议
  • netty解决 TCP 传输问题,如粘包、半包
  • Nio中在liunx系统中epoll 空轮询导致 CPU 100%
  • netty对API进行增强,使之更易用,如 FastThreadLocal=>ThreadLocal,ByteBuf=> ByteBuffer。

Netty vs 其它网络应用框架

  • Mina 由apache 维护,将来 3.x 版本可能会有较大重构,破坏API向下兼容性,Netty 的开发选代更速,API 更简洁、文档更优秀
  • 久经考验,20年,Netty 版本
    2.x 2004
    3.x 2008
    4.x 2013
    5.x 已废弃(没有明显的性能提升,维护成本高)

2.Hello World

maven依赖

        <!--netty依赖-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.51.Final</version>
        </dependency>

server端

public class HelloServer {
    public static void main(String[] args) {
    
        // 1. 服务器端的启动器,组装netty组件,启动服务
        new ServerBootstrap()
            // 2. BossEvenLoop,WorkEventLoop 监听事件 一个selector一个thread
            .group(new NioEventLoopGroup())
            // 3.信道的实现,nio / oio(即bio)
            .channel(NioServerSocketChannel.class)
            // 4.添加处理器,用于处理消息 ChannelInitializer也相当于一个处理器,不同的是需要它来添加其他处理器
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    // 5.通过pipeline来添加处理器 stringDecoder将byteBuf解码成string
                    ch.pipeline().addLast(new StringDecoder());
                    // 自定义的处理器,监听到读事件后会执行
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            System.out.println(msg);
                        }
                    });
                }
            })
            // 6.绑定端口
            .bind(8080);
    }
}

client端

public class HelloClient {
    public static void main(String[] args) throws InterruptedException {

        // 1. 创建客户端启动器
        new Bootstrap()
            // 2.选择事件轮询组
            .group(new NioEventLoopGroup())
                // 3.指定信道类型
            .channel(NioSocketChannel.class)
                // 4.添加处理器
            .handler(new ChannelInitializer<NioSocketChannel>() {
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    // 将消息编码成byteBuf
                    nioSocketChannel.pipeline().addLast(new StringEncoder());
                }
            })
                // 5.连接
            .connect(new InetSocketAddress("localhost", 8080))
                // 6.阻塞 等待connect之后再往下执行
            .sync()
            .channel()// 与服务器建立的信道
                // 7.发送消息
            .writeAndFlush("hello world");
    }
}

控制台
在这里插入图片描述

3.组件

3.1 EventLoop

事件循环对象:
EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理Channel上源源不断的 io 事件。
继承关系

  • 继承自ScheduledExecutorService,因此包含了线程池中所有的方法,同时还继承自 netty 自己的OrderedEventExecutor(有顺的)
    1、提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
    2、提供了parent() 方法来看看自己属于哪个 EventLoopGroup

在这里插入图片描述

3.1.1 EventLoopGroup

EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全),注意这里是IO对象,其他事件是可以换EventLoop的。

  • 继承自 netty 的 EventExecutorGroup
    1、实现了 Iterable 接口提供遍历 EventLoop 的能力
    2、另有 next 方法获取集合中下一个 EventLoop

3.1.2 处理普通任务

代码

    public static void main(String[] args) {

        // 支持处理IO事件、普通任务和定时任务。
        // 可以指定线程数,如果是无参构造,线程基本取NettyRuntime.availableProcessors() * 2
        NioEventLoopGroup loopGroup = new NioEventLoopGroup(2);

        // 不支持处理IO事件,支持普通任务和定时任务,处理非IO事件时可以使用
        DefaultEventLoopGroup group = new DefaultEventLoopGroup();
        
        // 1.next()获取下一个线程
        System.out.println(loopGroup.next());
        System.out.println(loopGroup.next());
        System.out.println(loopGroup.next());

        // 2. 执行普通任务
        loopGroup.next().execute(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("普通任务");
        });

        log.info("main线程");
        
        // 3. 执行定时任务
        loopGroup.next().scheduleAtFixedRate(()->log.info("定时任务"),2,1, TimeUnit.SECONDS);
    }

控制台
在这里插入图片描述

3.1.3 处理io任务

server代码

@Slf4j
public class EventLoopGroupServer {

    public static void main(String[] args) {
        new ServerBootstrap().group(new NioEventLoopGroup())
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf message = (ByteBuf) msg;
                            log.info(message.toString(Charset.defaultCharset()));
                        }
                    });
                }
            })
            .bind(8080);
    }
}

client端
在这里插入图片描述
控制台
在这里插入图片描述
结论:对于同一个channel上的发送请求,是由EventLoop中的指定的线程来执行的,也就是绑定起来了,后续此信道上的io操作都会是这个线程来执行,但是一个线程可以管多个channel

3.1.4 代码优化

优化一:细化EventLoopGroup,分工更明细
优化二:将耗时的业务处理,迁移到非NioEventLoopGroup处理

代码

    public static void main(String[] args) {

        DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
        new ServerBootstrap()
            // 优化一:此方法也可以接收两个传参,分工更加明细
            // 参数1:boss,负责处理accept事件,不需要设置多个线程,因为对于accept这一种类型的,只会创建一个线程来处理
            // 参数2:worker,负责socketChannel上的读写
            .group(new NioEventLoopGroup(), new NioEventLoopGroup(4))
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast("handler1", new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf message = (ByteBuf) msg;
                            log.info(message.toString(Charset.defaultCharset()));
                            // 传递给下一个处理器
                            ctx.fireChannelRead(msg);
                        }
                        // 优化二:对于比较耗时的业务,可以指定其他的eventLoopGroup进行处理,减少同一线程下某一信道慢处理导致其他信道阻塞
                    }).addLast(defaultEventLoopGroup,"handler2", new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf message = (ByteBuf) msg;
                            log.info(message.toString(Charset.defaultCharset()));
                        }
                    });
                }
            })
            .bind(8080);
    }

控制台
在这里插入图片描述

3.2 Channel

3.2.1 基本用法

  • close() 可以用来关闭Channel

  • closeFuture() 用来处理 Channel 的关闭
    1、sync 方法作用是同步等待 Channel关闭
    2、而 addListener 方法是异步等待 Channel 关闭

  • pipeline() 方法添加处理器

  • write() 方法将数据写入,不会立刻就发出,而是会写入缓冲区,当缓冲区的数据满了或者调用了flush() 方法的时候就会立刻发出。

  • writeAndFlush() 方法将数据写入并立刻刷出

3.2.2 ChannelFuture-连接

原因:connect(new InetSocketAddress(“localhost”, 8080))方法是异步的,后续操作需要在确保连接建立完成后执行,避免造成影响。
方法一:channelFuture.sync() 同步阻塞,等待连接建立完成后再执行后续操作。
方法二:addListener监听连接,连接建立完成后,由连接线程执行后续操作
方法一代码

    public static void main(String[] args) throws InterruptedException {
        ChannelFuture channelFuture = new Bootstrap().group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                        nioSocketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                // 1. 异步非阻塞,执行连接操作的是nio线程,不是main
                .connect(new InetSocketAddress("localhost", 8080));

        // 2. 此处同步阻塞,避免在连接还没建立完成前业务向下执行
        channelFuture.sync();
        Channel channel = channelFuture.channel();
        channel.writeAndFlush("hello world");
    }

方法二代码

//        channelFuture.sync();
        // 2. 监听连接是否完成
        channelFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                Channel channel = channelFuture.channel();
                channel.writeAndFlush("hello world");
            }
        });

3.2.3 ChannelFuture-关闭

同理,close()方法也是异步关闭的,所以我们在channel关闭后执行一些后置处理的话,也分为同步和异步两种处理方式。channelFuture.sync()同步阻塞和addListener异步处理
sync()代码

    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        ChannelFuture channelFuture = new Bootstrap().group(group)
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<NioSocketChannel>() {
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    nioSocketChannel.pipeline().addLast(new StringEncoder());
                }
            })
            .connect(new InetSocketAddress("localhost", 8080));
        channelFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                channelFuture.channel().writeAndFlush("hello world");
            }
        });

        // 方法一:同步阻塞
        channelFuture.sync();
        Channel channel = channelFuture.channel();
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String line = scanner.nextLine();
                if ("q".equals(line)) {
                    // close方法也是异步的
                    channel.close();
                    // eventloop也需要关闭
                    group.shutdownGracefully();
                    break;
                }
                channel.writeAndFlush(line);
            }
        }, "input").start();

        log.info("close success");
    }

控制台
在这里插入图片描述
addListener()代码

    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        ChannelFuture channelFuture = new Bootstrap().group(group)
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<NioSocketChannel>() {
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    nioSocketChannel.pipeline().addLast(new StringEncoder());
                }
            })
            .connect(new InetSocketAddress("localhost", 8080));
        channelFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                channelFuture.channel().writeAndFlush("hello world");
            }
        });

        // 方法一:同步阻塞
//        channelFuture.sync();
//        Channel channel = channelFuture.channel();
//        new Thread(() -> {
//            Scanner scanner = new Scanner(System.in);
//            while (true) {
//                String line = scanner.nextLine();
//                if ("q".equals(line)) {
//                    // close方法也是异步的
//                    channel.close();
//                    // eventloop也需要关闭
//                    group.shutdownGracefully();
//                    break;
//                }
//                channel.writeAndFlush(line);
//            }
//        }, "input").start();

		// 方法二:异步监听
        ChannelFuture closeFuture = channelFuture.channel().close();
        closeFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                log.info("addListener close success");
                group.shutdownGracefully();
            }
        });
        log.info("close success");
    }

控制台
在这里插入图片描述

3.3 Future & Promise

在异步调用的时候,经常会用到这两个接口。首先要说明 netty 中的 Future 接口和 jdk 中的 Future 接口同名,但是是两个接口,netty中的Future接口继承自JDK中的Future,而Promise 又对 netty Future 进行了扩展。

  • jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
  • netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
  • netty Promise 不仅有 netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
功能/名称jdk Futurenetty FuturePromise
cancel取消任务--
isCanceled任务是否取消--
isDone任务是否完成,不能区分成功失败--
get获取任务结果,阻塞等待--
getNow-获取任务结果,非阻塞,还未产生结果时返回null-
await-等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断-
sync-等待任务结束,如果任务失败,抛出异常-
isSuccess-判断任务是否成功-
cause-获取失败信息,非阻塞,如果没有失败,返回null-
addLinstener-添加回调,异步接收结果-
setSuccess--设置成功结果
setFailure--设置失败结果

3.3.1 jdk Future

代码

@Slf4j
public class JdkFutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 1.创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 2.提交线程任务,使用callable,可以返回一个future对象,用于获取结果
        Future<Integer> future = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.info("执行任务");
                return 50;
            }
        });
        log.info("success");
        log.info("获取执行结果:{}",future.get());
    }
}

控制台
在这里插入图片描述

3.3.2 netty Future

代码

@Slf4j
public class NettyFutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        EventLoop eventLoop = eventLoopGroup.next();
        Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.info("执行任务");
                return 50;
            }
        });
        log.info("success");
//        log.info("获取执行结果:{}",future.get());

        // 相比jdk future,netty中的支持异步回调结果
        future.addListener(new GenericFutureListener<Future<? super Integer>>() {
            @Override
            public void operationComplete(Future<? super Integer> future) throws Exception {
                log.info("接收结果:{}",future.getNow());
            }
        });
        log.info("结果success");
    }
}

控制台
在这里插入图片描述

3.3.3 netty Promise

相比于前两者,promise对象是我们主动创建的,而且我们可以根据线程中执行的结果来返回success或者异常fail。

@Slf4j
public class PromiseTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        EventLoop eventLoop = eventLoopGroup.next();

        // DefaultPromise<Object> defaultPromise = new DefaultPromise<>(eventLoop);
        Promise<Integer> promise = eventLoop.newPromise();

        new Thread(() -> {
            log.info("执行业务");
            try {
                int a = 1 / 0;
                Thread.sleep(1000);
                promise.setSuccess(50);
            } catch (Exception e) {
                promise.setFailure(e);
            }
        }).start();
        log.info("等待结果...");
        log.info("结果{}", promise.get());
    }
}

控制台
在这里插入图片描述

3.4 Handler & Pipeline

ChannelHandler 用来处理 Channel 上的各种事件,分为入站,出站两种,所有ChannelHandler 被连成一串,就是Pipeline。

  • 入站处理器通常是 ChannelInboundHandlerAdapter的子类,主要用来读取客户端数据,写回数据
  • 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要用于对写回结果进行加工

3.4.1 InboundHandler 和 OutboundHandler

  • netty自身会在pipeline上初始两个处理器,分别是head和tail,我们自定义的处理器都是这两者之间的,类似双向链表。比如,我们用addLast方法依次添加三个入站处理器和三个出站处理器,此时类似于head -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 ->tail。
  • 读入的数据,只在入站处理器中处理,即数据流向 h1 -> h2 -> h3。
  • 写出的数据,只在出站处理器中处理,即数据流向 h6 -> h5 -> h4。
  • 调用 ctx.channel().write(msg) 的时候会从tail往前找出站处理器来处理。调用 ctx.write(msg) 会从当前处理器往前找出站处理器来处理

代码
下面代码中,在h2和h3中我们都调用了ctx.channel().writeAndFlush(“aaa”),只要我们在处理中写出数据,这时数据就会流转到出站处理器中,出站处理器都执行完后,再向下一级入站处理器中处理读入的数据。所以流程是h1->h2(写出数据)->h6->h5->h4->h3(写出数据)->h6->h5->h4

@Slf4j
public class HandlerTest {
    public static void main(String[] args) {
        new ServerBootstrap().group(new NioEventLoopGroup())
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast("h1", new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.info("h1");
                            super.channelRead(ctx, msg);
                        }
                    }).addLast("h2", new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.info("h2");
                            ctx.channel().writeAndFlush("aaa");
                            super.channelRead(ctx, msg);
                        }
                    }).addLast("h3", new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            log.info("h3");
                            ctx.channel().writeAndFlush("bbb");
                            super.channelRead(ctx, msg);
                        }
                    }).addLast("h4", new ChannelOutboundHandlerAdapter() {
                        @Override
                        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
                            throws Exception {
                            log.info("h4");
                            super.write(ctx, msg, promise);
                        }
                    }).addLast("h5", new ChannelOutboundHandlerAdapter() {
                        @Override
                        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
                            throws Exception {
                            log.info("h5");
                            super.write(ctx, msg, promise);
                        }
                    }).addLast("h6", new ChannelOutboundHandlerAdapter() {
                        @Override
                        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
                            throws Exception {
                            log.info("h6");
                            super.write(ctx, msg, promise);
                        }
                    });
                }
            })
            .bind(8080);
    }
}

控制台
在这里插入图片描述

3.4.2 EmbeddedChannel调试工具类

@Slf4j
public class EmbeddedTest {
    public static void main(String[] args) {
        ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.info("1");
                super.channelRead(ctx, msg);
            }
        };
        ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.info("2");
                super.channelRead(ctx, msg);
            }
        };
        ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter() {
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.info("3");
                super.write(ctx, msg, promise);
            }
        };
        ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter() {
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.info("4");
                super.write(ctx, msg, promise);
            }
        };

        EmbeddedChannel channel = new EmbeddedChannel(h1, h2, h3, h4);
        // 模拟入站
        channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes()));
        // 模拟出站
        channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world".getBytes()));
    }
}

控制台
在这里插入图片描述

3.5 ByteBuf

3.5.1 创建–直接内存 VS 堆内存

  • 初始容量,默认256字节。我们可以指定初始容量,如果写入字节超过容量,则自动扩容。

代码

public class ByteBufCreateTest {
    public static void main(String[] args) {

        // 不指定初始容量,默认256字节
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        log(buffer);

        // 指定初始容量,如果写入字节超过容量,则自动扩容
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16);
        log(buf);

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 17; i++) {
            sb.append("a");
        }
        buf.writeBytes(sb.toString().getBytes());
        log(buf);
    }

    /**
     * 打印日志工具,方便查看
     * 
     * @param buffer
     */
    private static void log(ByteBuf buffer) {
        int length = buffer.readableBytes();
        int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
        StringBuilder buf = new StringBuilder(rows * 80 * 2).append("read index:")
            .append(buffer.readerIndex())
            .append(" write index:")
            .append(buffer.writerIndex())
            .append(" capacity:")
            .append(buffer.capacity())
            .append(NEWLINE);
        appendPrettyHexDump(buf, buffer);
        System.out.println(buf.toString());
    }
}

控制台
在这里插入图片描述

  • 默认创建的ByteBuf是使用直接内存的,直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用。直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放。
  • 我们可以用ByteBufAllocator.DEFAULT.heapBuffer()指定使用堆内存
@Slf4j
public class ByteBufTest {
    public static void main(String[] args) {
        // 使用直接内存
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        log.info("{}",buffer);

        // 使用直接内存
        ByteBuf buffer2 = ByteBufAllocator.DEFAULT.directBuffer();
        log.info("{}",buffer2);

        // 使用堆内存
        ByteBuf buffer3 = ByteBufAllocator.DEFAULT.heapBuffer();
        log.info("{}",buffer3);
    }
}

控制台
在这里插入图片描述

3.5.2 创建–池化 VS 非池化

ByteBuf 中支持池化的管理,池化的最大意义在于可以重用 ByteBuf,对于那些创建比较慢的资源我们可以用池的资源进行优化,类似于线程池或者连接池。

  • 我们预先创建好多个 ByteBuf 实例, 在创建的时候就直接调用就行,这样的好处就是节省内存,因为 Java 中创建 ByteBuf 默认是直接内存,但是要知道直接内存是存在于操作系统中的,创建的代价是很高的,就算是堆内存,不断地创建 ByteBuf 实例也只会增添 GC 回收对象的压力。
  • 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
  • 高并发的时候采用池化技术更能节省内存,减少内存溢出的风险。

我们可以通过以下方式来决定是否开启池化功能:在 VM 参数中设置

-Dio.netty.allocator.type={unpooled|pooled}

在这里插入图片描述我们指定VM参数后,执行上面的ByteBufTest,控制台输出
在这里插入图片描述

3.5.3 组成

  • 四个元素,读指针、写指针、容量、最大容量(int的最大值)
  • 四个部分,废弃部分(已经读取了的部分)、可读部分(未读的部分)、可写部分(写指针与容量之间的部分)、可扩容部分
    在这里插入图片描述

3.5.4 写入

方法签名含义备注
writeBoolean(boolean value)写入 boolean 值用一字节 01\00 代表 true\false
writeByte(int value)写入 byte 值用一字节 01\00 代表 true\false
writeShort(int value)写入 short 值用一字节 01\00 代表 true\false
writeInt(int value)写入 int 值Big Endian(大端写入),即 0x250,写入后 00 00 02 50 ,低位靠后
writeIntLE(int value)写入 int 值Little Endian(低位写入),即 0x250,写入后 50 02 00 00 ,高位靠后
writeLong(long value)写入 long 值
writeChar(int value)写入 char 值
writeFloat(float value)写入 float 值
writeDouble(double value)写入 double 值
writeBytes(ByteBuf src)写入 netty 的 ByteBuf
writeBytes(byte[] src)写入 byte[]
writeBytes(ByteBuffer src))写入 nio 的 ByteBuffer
int writeCharSequence(CharSequence sequence, Charset charset)写入字符串
    public static void main(String[] args) {
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        buffer.writeBytes(new byte[]{1,2,3,4});
        log(buffer);

        buffer.writeInt(5);
        log(buffer);
        
        buffer.writeIntLE(5);
        log(buffer);
    }

控制台![

  • 有一类方法是 set 开头的一系列方法,也可以写入数据,但不会改变写指针位置。类似于redis中bitmap的使用。

3.5.5 扩容

扩容机制需要涉及到源码解析,这里不做研究。

    public static void main(String[] args) {
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(6);
        log(buffer);

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 7; i++) {
            sb.append("a");
        }
        buffer.writeBytes(sb.toString().getBytes());
        log(buffer);

        StringBuilder sb2 = new StringBuilder();
        for (int i = 6; i < 17; i++) {
            sb2.append("a");
        }
        buffer.writeBytes(sb2.toString().getBytes());
        log(buffer);

        StringBuilder sb3 = new StringBuilder();
        for (int i = 16; i < 65; i++) {
            sb3.append("a");
        }
        buffer.writeBytes(sb3.toString().getBytes());
        log(buffer);

        StringBuilder sb4 = new StringBuilder();
        for (int i = 65; i < 257; i++) {
            sb4.append("a");
        }
        buffer.writeBytes(sb4.toString().getBytes());
        log(buffer);
    }

控制台
在这里插入图片描述

3.5.6 读取

在我们读取的过程中,读指针会一直向后移动,读过的部分变成了废弃部分。

    public static void main(String[] args) {
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        buffer.writeBytes(new byte[]{1,2,3,4});
        buffer.writeInt(5);
        log(buffer);

        System.out.println(buffer.readByte()); // 1
        System.out.println(buffer.readByte()); // 2
        System.out.println(buffer.readByte()); // 3
        System.out.println(buffer.readByte()); // 4
        log(buffer);
    }

在这里插入图片描述
如果想要重复读取的话,可以像Nio里面的ByteBuffer那样设置一个标记位。

        // 标记读指针位置
        buffer.markReaderIndex();
        System.out.println(buffer.readInt());
        log(buffer);

        // 重置读指针位置
        buffer.resetReaderIndex();
        log(buffer);

在这里插入图片描述

  • 采用 get 开头的一系列方法,这些方法不会改变 read index,因为这类方法通常都是按照索引去获取的,所以不会改变读指针的位置

3.5.7 retain 和 release

  • UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可
  • UnpooledDirectByteBuf 使用的就是直接内存了,需要我们主动调用特殊的方法来回收内存,因为等到垃圾回收机制去主动回收的时候其实是不及时的
  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

对于回收实现 Netty 采用了引用计数法来控制回收内存,主要是每个ByteBuf 都实现了一个通用的接口 ReferenceCounted

  • 每个 ByteBuf 对象的初始计数为 1,表示有人在使用,不能回收
    调用 release 方法计数减 1,如果计数为 0,证明没有人用了,ByteBuf 内存被回收
  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用
  • 在 pineline中的双向存储结构中,head 和 tail的作用之一就是来释放掉入站或者出站的byteBuf。但是前提是,这个byteBuf传到head或者tail中,如果在中间的某一个hangler中执行完就不传递了,就需要我们手动调用release方法释放。 具体源码可以查看HeadContext和tailContext;

3.5.8 slice 和 Composite

【零拷贝】 的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针 。
在这里插入图片描述

public class SliceTest {
    public static void main(String[] args) {
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
        buffer.writeBytes(new byte[]{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'});
        log(buffer);

        // 进行切片,底层是同一块空间
        ByteBuf slice1 = buffer.slice(0, 5);
        ByteBuf slice2 = buffer.slice(5, 5);
        log(slice1);
        log(slice2);

        // 可以用set方法改变某字节的值,但是不能变更长度,比如写入新的字节,因为有其他对象引用
        slice1.setByte(0,'b');
        log(buffer);
    }

    private static void log(ByteBuf buffer) {
        int length = buffer.readableBytes();
        int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
        StringBuilder buf = new StringBuilder(rows * 80 * 2).append("read index:")
                .append(buffer.readerIndex())
                .append(" write index:")
                .append(buffer.writerIndex())
                .append(" capacity:")
                .append(buffer.capacity())
                .append(NEWLINE);
        appendPrettyHexDump(buf, buffer);
        System.out.println(buf.toString());
    }
}

在这里插入图片描述

当然,可以将多个 ByteBuf 合并为一个ByteBuf,这样也避免了一次拷贝。

    public static void main(String[] args) {
        ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
        buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
        ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
        buf2.writeBytes(new byte[]{6, 7, 8, 9, 10});

        // writeBytes是基于数据复制的,这里会发生两次数据复制,将buf1和buf2复制到buffer中
//        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
//        ByteBuf byteBuf = buffer.writeBytes(buf1).writeBytes(buf2);
        
        // 组合,避免复制。但是重新组合后,需要修改读写指针,第一个传参为true
        CompositeByteBuf byteBufs = ByteBufAllocator.DEFAULT.compositeBuffer();
        byteBufs.addComponents(true, buf1, buf2);
    }
  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值