Netty组件

28 阅读12分钟

组件

Event Group

建立连接后的channel会经过pipline中handler处理,在handler里面进行处理数据,如读/写channel中数据。每个channel都会绑定到一个一个eventLoopGroup的eventLoop,和其相关的操作(这些Handler默认由Channel绑定的EventLoop执行)都会交由固定eventLoop去完成。Handler也可以单独指定一个eventLoopGroup,从而不用默认绑定的group。这样能够实现Handler的处理流程是在不同从线程中从而提高性能,netty会保证同一个channel的相同handler处理流程固定在某个线程内完成。

实现类有:

  • NioEventLoopGroup - 基于 Java NIO 的 EventLoopGroup 实现
  • OioEventLoopGroup - 基于传统阻塞 I/O 的 EventLoopGroup 实现
  • EpollEventLoopGroup - 基于 Linux Epoll 的高性能实现
ch.pipeline().addLast(hEventGroup,"handler3", // hEventGroup是额外定义的group
              new ChannelInboundHandlerAdapter() {
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) {
                    ByteBuf byteBuf = msg instanceof ByteBuf ? ((ByteBuf) msg) : null;
                    if (byteBuf != null) {
                        byte[] buf = new byte[16];
                        ByteBuf len = byteBuf.readBytes(buf, 0, byteBuf.readableBytes());
                    }
                }
            });

image-20251026095820337.png

客户端A和服务端建立连接后得到,服务器生成对应的channelA

  • Handler3处理流程初始时被绑定了H_EventLoop1,channelA的Handler3会一直交由H_EventLoop1处理。
  • 其他handler由于没有指定专用group,会使用默认的group,初始绑定EventLoop1,channelA的这些handler会一直交给EventLoop1处理。

EventLoopGroup实现了JDK的ScheduledExecutorService。ScheduledExecutorService是 Java 并发包中的一个接口,它扩展了 ExecutorService 接口(线程池相关接口),提供了任务调度执行的功能。ThreadPoolExecutor(JDK线程池)也扩展了 ExecutorService 接口,EventLoopGroup就是一个线程池。

  • ScheduledExecutorService:用于实现任务调度,如延迟或者周期执行某些任务。JDK中ScheduledThreadPoolExecutor 就实现了这个接口。从而EventLoopGroup也有类似功能。

EventLoopGroup的api

  • register(Channel channel) : 将 Channel 注册到 EventLoopGroup 中的一个 EventLoop 上
  • next() : 返回 EventLoopGroup 中的下一个 EventLoop 实例,用于轮询分配
  • iterator() :返回 EventLoopGroup 中所有 EventLoop 的迭代器
  • isShuttingDown() :检查 EventLoopGroup 是否正在关闭过程中
  • shutdownGracefully():优雅地关闭 EventLoopGroup,返回 Future 对象用于监听关闭完成状态
  • submit(Runnable task):提交任务到 EventLoopGroup 执行 submit(Callable task) 提交有返回值的任务到 EventLoopGroup 执行
  • schedule(Runnable command, long delay, TimeUnit unit) :在指定延迟后执行一次性任务
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

Channel

Channel 是 Netty 中网络通信的抽象,代表一个网络连接,实现类有:

  • NioSocketChannel - 基于 NIO 的 TCP 客户端 Channel
  • NioServerSocketChannel - 基于 NIO 的 TCP 服务端 Channel
  • NioDatagramChannel - 基于 NIO 的 UDP Channel
  • EmbeddedChannel - 用于测试的嵌入式 Channel 不用开启客户端和服务端进行测试pipeline

channel的生命周期

  • unregistered - Channel 未注册到 EventLoop
  • registered - Channel 已注册到 EventLoop ==>会触发对应注册事件,对应的handler会处理。
  • active - Channel 处于活动状态,可以接收和发送数据
  • inactive - Channel 处于非活动状态,无法进行 I/O 操作
  • unregistered - Channel 从 EventLoop 取消注册

Channel一些API

生命周期管理相关

  • isOpen() - 检查 Channel 是否打开
  • isActive() - 检查 Channel 是否处于活动状态
  • isWritable() - 检查 Channel 是否可写
  • close() - 关闭 Channel
  • closeFuture() - 返回 Channel 关闭后的 Future

网络配置相关

  • localAddress() - 获取本地地址
  • remoteAddress() - 获取远程地址
  • config() - 获取 ChannelConfig 配置对象
  • pipeline() - 获取 ChannelPipeline 处理链

数据读写操作相关

  • write(Object msg) - 写入数据到 Channel
  • writeAndFlush(Object msg) - 写入数据并刷新到网络
  • flush() - 刷新缓冲区数据到网络
  • read() - 从 Channel 读取数据

事件处理相关

  • bind(SocketAddress localAddress) - 绑定地址
  • connect(SocketAddress remoteAddress) - 连接到远程地址
  • disconnect() - 断开连接
  • deregister() - 取消注册

Handler

Netty 通过适配器模式简化了 ChannelHandler 的实现,避免开发者必须实现所有接口方法。你需要什么类型的Handler就实现对应适配器即可,常用Handler就出站入站两种。

如何理解出站和入站?

如读取数据

  • 第一阶段:调用 read() 方法 - 发出读请求,准备接收数据==>出站
  • 第二阶段:数据到达后 - 触发 channelRead 事件,真正处理接收到的数据==>入站

可以理解为发送相关请求的操作是出站,响应到达的操作为入站操作,从而出站也有相关的read。

相关接口

ChannelHandler - 所有 Handler 的基础接口,定义了 Handler 的基本生命周期方法=>实现类ChannelHandlerAdapter

  • handlerAdded(ChannelHandlerContext ctx) - 当 Handler 被添加到 Pipeline 时调用
  • handlerRemoved(ChannelHandlerContext ctx) - 当 Handler 从 Pipeline 移除时调用
  • exceptionCaught(ChannelHandlerContext ctx, Throwable cause) - 处理异常事件

ChannelInboundHandlerAdapter : 入站处理器适配器

ChannelOutboundHandlerAdapter : 出站相关处理器适配器

其他实现类

  • ChannelDuplexHandler : 能同时处理出入站的适配器

根据需要实现对应的适配器即可。

ChannelHandlerContext

控制事件在 ChannelPipeline 中的传播,执行 I/O 操作(读写数据),访问 Channel 和相关组件,管理 ChannelHandler 的生命周期和状态

事件的传播方法

  • fireChannelRegistered() - 传播 channelRegistered 事件到下一个入站 ChannelHandler
  • fireChannelUnregistered() - 传播 channelUnregistered 事件到下一个入站 ChannelHandler
  • fireChannelActive() - 传播 channelActive 事件到下一个入站 ChannelHandler
  • fireChannelInactive() - 传播 channelInactive 事件到下一个入站 ChannelHandler
  • fireChannelRead(Object msg) - 传播 channelRead 事件到下一个入站 ChannelHandler
  • fireChannelReadComplete() - 传播 channelReadComplete 事件到下一个入站 ChannelHandler
  • fireExceptionCaught(Throwable cause) - 传播异常事件到下一个入站 ChannelHandler
  • fireUserEventTriggered(Object event) - 传播用户自定义事件到下一个入站 ChannelHandler
  • fireChannelWritabilityChanged() - 传播可写性改变事件到下一个入站 ChannelHandler

I/O 操作方法

  • bind(SocketAddress localAddress) - 绑定地址
  • connect(SocketAddress remoteAddress) - 连接到远程地址
  • connect(SocketAddress remoteAddress, SocketAddress localAddress) - 连接并绑定本地地址
  • disconnect() - 断开连接
  • close() - 关闭 Channel
  • deregister() - 取消注册
  • read() - 从 Channel 读取数据
  • write(Object msg) - 写入数据到 Channel
  • flush() - 刷新写队列中的数据
  • writeAndFlush(Object msg) - 写入数据并立即刷新

访问器方法

  • channel() - 获取关联的 Channel 实例
  • executor() - 获取事件执行器
  • name() - 获取 ChannelHandler 的名称
  • handler() - 获取关联的 ChannelHandler 实例
  • pipeline() - 获取关联的 ChannelPipeline 实例
  • alloc() - 获取 ByteBufAllocator 用于分配缓冲区
  • attr(AttributeKey key) - 获取/设置属性
  • hasAttr(AttributeKey key) - 检查是否存在指定属性

入站 ChannelInboundHandlerAdapter

入站handler在pipeline里面按照链表形式组织,需要实现不同事件处理方法,当对应事件触发时,事件会在pipeline里面依次传递,执行对应的方法。

image-20251026123340414.png

处理事件方法内部可以通过ChannelHandlerContext调整事件传递,通常来说A事件会传递给下一个Handler的A事件处理方法进行处理。

具体应用

  • 消息解码: 将接收到的字节流转换为应用程序可用的对象
  • 业务逻辑处理: 处理客户端发送的请求数据

api

ChannelInboundHandler - 处理入站事件的接口=>实现类ChannelInboundHandlerAdapter

  • channelRegistered(ChannelHandlerContext ctx) - Channel 注册到 EventLoop 时调用
  • channelActive(ChannelHandlerContext ctx) - Channel 处于活动状态时调用
  • channelRead(ChannelHandlerContext ctx, Object msg) - 读取到数据时调用
  • channelReadComplete(ChannelHandlerContext ctx) - 读取完成时调用,所有的handler的读流程都过一遍后。
  • channelInactive(ChannelHandlerContext ctx) - Channel 失去连接时调用
  • channelUnregistered(ChannelHandlerContext ctx) - Channel 从 EventLoop 取消注册时调用
  • userEventTriggered(ChannelHandlerContext ctx, Object evt) - 用户自定义事件触发时调用
  ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                @Override
                public void channelRead(ChannelHandlerContext ctx, Object msg) {
                    System.out.println("解码器!");
                    ctx.fireChannelRead(msg); // 传递给下一个inbound
                    // 传递给下个一个inbound,msg可以是任意类型
                    // super.channelRead(ctx,msg);和ctx.fireChannelRead一样
                }
            });

SimpleChannelInboundHandler : channelRead0 方法直接接收指定泛型类型 I 的参数,不需要关注ByteBuf对象的引用释放。这里指定类型的转换是发生在解码器之后的,解码器解码数据,构造指定类型后往下一个handler传递。

ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8)); // netty内置的字符串解码器
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        // 直接处理字符串消息,无需类型转换
        System.out.println("接收到字符串: " + msg);
    }
});

没有解码器,msg就是一个ByteBuf对象,需要自己处理。

ch.pipeline().addLast(new SimpleChannelInboundHandler<ByteBuf>() {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
        // msg 是从网络读取的原始字节数据
        // 需要手动处理字节数据的解析
        byte[] bytes = new byte[msg.readableBytes()];
        msg.readBytes(bytes);
        String message = new String(bytes);
    }
});

出站 ChannelOutboundHandlerAdapter

能干什么?

  • 编码器实现: 作为自定义编码器的基础类,在数据发送前将其转换为字节流
  • 日志记录: 记录所有出站操作和发送的数据内容
  • 安全处理: 实现数据加密、签名等安全相关操作
  • 性能监控: 监控数据发送的性能指标和统计信息
  • 协议封装: 实现出站协议规范,对发送的数据进行协议封装

需要主动往channel中写数据,才会触发写事件,然后传递到出站处理器

ctx.channel().write(msg);  // 在入站读中触发出站,一般来说触发后,后续相关入站handler就不会再执行

api

ChannelOutboundHandler - 处理出站事件的接口==>实现类ChannelOutboundHandlerAdapter

  • bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) - 处理绑定请求,正式绑定前的一个处理
  • connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) - 处理连接请求
  • disconnect(ChannelHandlerContext ctx, ChannelPromise promise) - 处理断开连接请求
  • close(ChannelHandlerContext ctx, ChannelPromise promise) - 处理关闭请求
  • deregister(ChannelHandlerContext ctx, ChannelPromise promise) - 处理取消注册请求
  • read(ChannelHandlerContext ctx) - 处理读请求
  • write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) - 处理写请求 ) ,处理数据写入操作,在数据真正发送到网络之前可以进行修改或拦截
  • flush(ChannelHandlerContext ctx) - 处理刷新请求
// 自定义出站处理器
class SimpleOutboundHandler extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        // 在数据发送前进行处理
        System.out.println("Sending message: " + msg);
        // 可以修改要发送的消息
        String modifiedMsg = "[" + System.currentTimeMillis() + "] " + msg;
        // 继续传递给下一个出站处理器
        ctx.write(modifiedMsg, promise);
    }
    
    @Override
    public void flush(ChannelHandlerContext ctx) {
        System.out.println("Flushing data...");
        ctx.flush();
    }
}

Pipeline中出入站的执行顺序

执行顺序:

image-20251026160656593.png

入站是从head其开始执行直到主动触发出站事件或者达到tail,出站则相反。

ChannelPipeline 采用双向链表结构组织 ChannelHandler,头节点,负责实际的I/O操作,尾节点,负责最终的事件处理。DefaultChannelPipeline是ChannelPipeline的主要实现类。

ByteBuff

创建方法:ByteBufAllocator

// ctx.alloc() : 返回与当前 ChannelHandlerContext 关联的 ByteBufAllocator
ctx.alloc().buffer(1024);
​
// ByteBufAllocator.DEFAULT: 返回全局默认的 ByteBufAllocator 实例
 ByteBufAllocator.DEFAULT.buffer(1024);
// buffer方法得到可能是堆内存也可能是直接内存// heapBuffer(10) 堆内存
// directBuffer(10) 直接内存

常用api

  • writeBytes
  • set开头的方法:设置数据,不改变写指针
  • readByte: read会改变读指针
  • getByte: 不会改变读指针
  • markReaderIndex() : 做标记,调用resetReaderIndex可以重置会标记位置。

大小端: 内存中存储数据时如何组织人正常看到数值,如数值0x7c00

  • 大端:高字节保存在内存的低地址中, 就是人正常看到存储

     | 0字节|  1 字节|  
     | 7c  |  00    |
    
  • 小端:低字节保存在内存的低地址中

     | 0字节|  1字节 |  
     | 00  |  7c    |
    

池化: 提前分配内存,用完归还,而不是直接GC

配置池化,添加JVM启动参数

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

查看分配的是池化的,直接看ByteBufAllocator的类型即可。

扩容规则

  • 没有超过512就选恰好能存放数据的16倍数的数值为容量
  • 超过512,选 恰好能2^n能存放数据的数值为容量
  • 超过最大容量,报错。

内存释放

每个 ByteBuf 都实现了 ReferenceCounted 接口,计数器减为0就会被回收。

  • release :-1
  • retain: +1

原始的ByteBuf(从网络中读到数据),如果传递到TailContext 节点或者HeadContext,则会自己释放。如果中途进行消息转换,则需要在断开传递的位置进行释放。

  • 异常时,一定要通过循环取释放buff,release会返回true

slice

  • 原始 ByteBuf 进行切片,和对应buff共享内存
  • buffer.slice() : 对有效数据部分进行切片(没有读的那部分),切片后silce的指针是独立的

duplicate

  • 和原buff共享一片内存,得到的buff是不会再扩容

buff扩容机制是什么样的?

        // 原始 ByteBuf
        ByteBuf original = Unpooled.buffer(16);
        original.writeBytes("Hello".getBytes());
​
        // 创建 duplicate
        ByteBuf duplicate = original.duplicate();
        // 输出原始 ByteBuf 内容
        System.out.println("Original: " + original.toString(Charset.defaultCharset()));
        //  输出 duplicate 内容
        System.out.println("Duplicate: " + duplicate.toString(Charset.defaultCharset()));
        // 打印扩容前的内存地址
        System.out.println("Before expand - original.array(): " + System.identityHashCode(original.array()));
        System.out.println("Before expand - duplicate.array(): " + System.identityHashCode(duplicate.array()));
​
        // 触发扩容
        original.writeBytes(" World - expanded content".getBytes());
        // 打印扩容后的内存地址
        System.out.println("Before expand - original.array(): " + System.identityHashCode(original.array()));
        System.out.println("Before expand - duplicate.array(): " + System.identityHashCode(duplicate.array()));
​
        // 验证: 通过 duplicate 修改数据也不会影响扩容后的 original
        duplicate.setByte(0, (byte)'h');
        // original 中的 "Hello" 不会变成 "hello"
        // 输出原始 ByteBuf 内容
        System.out.println("Original: " + original.toString(Charset.defaultCharset()));
        //  输出 duplicate 内容
        System.out.println("Duplicate: " + duplicate.toString(Charset.defaultCharset()));

我机器输出结果:

Original: Hello
Duplicate: Hello
Before expand - original.array(): 4182120
Before expand - duplicate.array(): 4182120
Before expand - original.array(): 9823079
Before expand - duplicate.array(): 9823079
Original: hello World - expanded content
Duplicate: hello

上面输出的结果,扩容后数组hash变了,duplicate的hash也跟着变化了。很合理,但是又很让人匪夷所思,如何做到同时修改duplicate的arr的?

copy

  • 会将底层内存数据进行深拷贝,因此无论读写都与原始 ByteBuf 无关

CompositeByteBuf

  • 可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免拷贝

Unpooled

非池化工具类

  • wrappedBuffer,可以用来包装 ByteBuf,多个buf时底层采用CompositeByteBuf

Future和Promise

Netty中异步处理时常用到两个接口,单线程异步处理,能够最大化利用cpu。

Future:只读

  • getNow : 获取任务结果,非阻塞,还未产生结果时返回 null
  • await 等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断
  • sync 等待任务结束,如果任务失败,抛出异常
  • isSuccess
  • cause 获取失败信息,非阻塞,如果没有失败,返回null
  • addLinstener 添加回调,异步接收结果

Future基本上都是Netty的api去设置结果。

Promise:可以写结果,通常由自己结合业务去编程设置结果。

  • setSuccess 设置成功结果
  • setFailure 设置失败结果

Promise应用场景

  • 如何查询数据库成功与失败和netty的eventLoop结合的场景

    public Promise<User> fetchUserAsync(String userId) {
        // 1. 在 I/O 线程中创建 promise
        final EventExecutor executor = channel.eventLoop();
        final DefaultPromise<User> promise = new DefaultPromise<>(executor);
        // 2. 提交到业务线程池,业务线程池执行,然后设置promise
        businessExecutor.submit(() -> {
            try {
                User user = databaseService.getUser(userId);
                promise.setSuccess(user); // 设置成功结果
            } catch (Exception e) {
                promise.setFailure(e); // 设置失败原因
            }
        });
        
        return promise; // 立即返回 promise
    }
    ​
    // 在handler调用fetchUserAsync,并且给promise设置监听器。
    fetchUserAsync("123").addListener(future -> {
        if (future.isSuccess()) {
            channel.writeAndFlush(future.getNow());
        }
    });