组件
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());
}
}
});
客户端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里面依次传递,执行对应的方法。
处理事件方法内部可以通过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中出入站的执行顺序
执行顺序:
入站是从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()); } });