加群联系作者vx:xiaoda0423
仓库地址:https://webvueblog.github.io/JavaPlusDoc/
https://1024bat.cn/
阶段 | 说明 | 涉及数据结构 |
---|---|---|
创建 bossGroup、workerGroup | 分别管理接入连接和后续 IO 读写 | NioEventLoopGroup (内部有多线程执行) |
配置 serverBootstrap | 设置各种 Netty 参数,组装 ServerBootstrap | ServerBootstrap |
注册 childHandler | xxServerChInit 用于初始化每条新的连接 ChannelPipeline | ChannelInitializer<SocketChannel> |
调用 bind(port) | 把服务绑定到 9000 端口,监听 TCP 连接 | ChannelFuture |
客户端连接到服务器 | bossGroup 接收连接,workerGroup 负责后续的数据通信 | NioServerSocketChannel 、 |
连接初始化 | xxServerChInit 中添加的 Handler 被执行 | ChannelPipeline (责任链模式) |
读写数据处理 | 自定义 Handler 处理接收到的数据 | 自定义消息类(比如 JT808 报文、定制指令) |
服务关闭 | 关闭所有资源,释放端口 | shutdownGracefully |
主要涉及的数据结构变化
变化点 | 变化描述 |
---|---|
ServerBootstrap -> | 通过 |
NioEventLoopGroup -> | 创建的 boss/worker 组内部是多个 |
SocketChannel -> | 每次有连接建立时,初始化 |
AdaptiveRecvByteBufAllocator | 动态分配读缓冲区大小(避免内存浪费或过小导致频繁扩容)。 |
@Component
public class xxServerChInit extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 这里可以添加编解码器、业务处理器等
pipeline.addLast(new FrameDecoder()); // 粘包拆包处理器
pipeline.addLast(new ProtocolDecoder()); // 协议解码
pipeline.addLast(new BusinessHandler()); // 具体业务逻辑
}
}
这里的 pipeline 就像一条责任链,负责把客户端发过来的数据一层层加工和处理。
阶段 | 说明 | 涉及类/数据结构 |
---|---|---|
握手建立连接 | Netty接收连接,调用 | SocketChannel |
创建责任链(Pipeline) | pipeline() 返回一个空白链表结构 | DefaultChannelPipeline |
加入 IdleStateHandler | 监测180秒内是否有读写,否则触发 | IdleStateHandler |
加入解码器 xxxDecoder | 收到数据时,自动从 ByteBuf 解析成业务 POJO | xxDecoder |
加入编码器 xxEncoder | 发送数据时,把业务对象转成 ByteBuf | xxEncoder |
加入业务处理器 xxxServerHandle | 处理 decode后的业务消息,执行业务逻辑 | xxServerHandle |
主要涉及的数据结构变化
阶段 | 变化描述 |
---|---|
SocketChannel.pipeline() | 初始化一个新的责任链 |
addLast 操作 | 往 Pipeline 中按顺序插入 Handler,形成链表 |
ByteBuf -> BusinessMessage | 通过 |
BusinessMessage -> ByteBuf | 通过 |
IdleStateEvent | 超时后通过 pipeline 触发一个 |
JVM 调优:
1. 垃圾回收器选择
建议:使用 G1 GC
-XX:+UseG1GC
特点:适合高并发、低延迟场景,GC暂停时间可控。
2. 堆内存配置
根据实际连接数量和消息量,合理设置堆大小:
-Xms2g -Xmx2g
(初始和最大一致,避免动态扩容带来的卡顿)
3. Netty内存管理优化
Netty 默认用 PooledByteBufAllocator,确保启用池化:
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
减少小对象频繁分配带来的GC压力。
4. 线程数配置
bossGroup 和 workerGroup 线程数可以根据 CPU 核心数调整:
new NioEventLoopGroup(cpu核心数 * 2)
例子:8核机器可以用
new NioEventLoopGroup(16)
5. 防止内存泄漏
Handler 一定要释放资源,比如 ByteBuf 的 release()。
监控日志中是否有 Netty 报出的
LEAK: ByteBuf.release()
警告。保活(180s心跳超时检测)
解码(字节 -> 业务对象)
编码(业务对象 -> 字节)
核心业务处理(执行业务逻辑)
阶段 | 主要处理 |
---|---|
设备连接 | channelActive:记录Channel |
设备首次发消息 | channelRead0解析 |
后续收消息 | 更新心跳、解析处理业务逻辑、回写结果 |
设备掉线 | channelInactive清除映射关系 |
空闲超时 | userEventTriggered主动关闭超时连接 |
基于 ReentrantLock + Condition
支持异步线程之间的通知/等待。
支持超时控制。
手动
signal
触发返回结果。兼容
java.util.concurrent.Future
接口规范。
Condition
适合这种低开销、少量同步的场景,比如一个Future小对象,不走重型同步器(像 CompletableFuture
里面是用 Unsafe)。
存储一批等待异步完成的
CondFuture
。通过
id
来匹配具体的 Future。控制最大Future数量,防止内存爆掉。
定期清理超时未完成的 Future。
提供 setSignal(
setSyncFuture
)去异步唤醒 Future。
非常适合应用在 客户端请求异步等待场景,比如:
请求下发一个任务,客户端阻塞等待响应结果,超时自动清理。
用
ConcurrentHashMap
保证多线程安全。futureCount
作为保护阈值,避免堆积太多 Future。invalidTime
检查超时的 Future,主动释放资源。支持动态唤醒(
setSyncFuture
)指定 Future。保持了轻量,没引入复杂调度任务,适合高性能环境。
项目 | 现状 | 改进 |
---|---|---|
线程安全 | 基本ok | 加强 remove/signal 一致性 |
类型安全 | 泛型丢失 | 用 |
并发性能 | 粗锁 | double-check 减少锁 |
日志 | 频繁 | 只打异常日志 |
清理机制 | 嵌套 | 抽出 cleanExpiredFutures |
补充:这个容器典型应用场景
Netty长连接服务器(客户端发起请求,需要服务器应答)
Kafka异步投递任务回调
RPC调用 Future 同步等待返回
IoT设备异步响应
微服务之间的异步请求-响应
举个小例子,比如 Netty 收到一条业务请求,存个 CondFuture,异步发出去,下次 channelRead 回来,再通过 id 取出 Future,signal 一下返回响应,非常常见!
场景 | 说明 | 举例 |
---|---|---|
Netty长连接服务器 | 请求-响应模式,需要在异步事件驱动下,临时保存请求上下文。 | 客户端发指令(比如充值请求),服务器先缓存 Future,异步处理完后 signal 返回结果。 |
Kafka异步投递回调 | 生产者发送消息后,不是立即确认,需要等待消费端处理回调。 | Kafka 生产发出去后,异步监听回调 ack,等拿到ack再 signal Future。 |
RPC调用同步等待返回 | 微服务之间异步RPC调用,client端需要同步等待结果。 | A系统请求B系统处理数据,不阻塞主线程,通过 Future await 等待异步回包。 |
IoT设备异步响应 | 大量设备与云端交互,请求和响应间隔时间不确定。 | IoT终端发上传指令,云端收到保存 Future,等设备异步回执 signal。 |
微服务异步API网关 | 网关收到异步任务,需要同步返回响应,需要短暂缓存请求。 | API网关请求分发到后端异步处理,Future await直到有结果,返回给前端。 |
延迟消息确认 | 某些系统需要发送消息后等待业务确认或补充处理。 | 下单后,发“支付成功消息”,等用户支付异步回调,signal Future并继续业务。 |
链路追踪跨服务调用 | 需要追踪一条链路的请求链,保持一个临时状态直到结束。 | 一个调用链起点保存 Future,链路回调后 signal 记录埋点结束。 |
(Netty里的超典型用法)
比如:
客户端发送一条请求,带
reqId=12345
服务器 Netty
channelRead
收到请求后:
CondFutureContainer.getFuture("12345")
保存一个
CondFuture
继续异步向业务层派发请求
业务处理需要异步,比如访问数据库、下游系统、或者Kafka投递
后来异步事件返回,比如数据库处理好了/下游响应回来了
业务代码调用
CondFutureContainer.setSyncFuture("12345", 响应内容)
唤醒 Future,Netty 把响应写回客户端
完成一次完整的异步请求-响应!
示意伪代码:
// 收到请求 public void handleClientRequest(ChannelHandlerContext ctx, Request req) { CondFuture<Response> future = container.getFuture(req.getId()); asyncBizProcess(req); // 异步处理 Response response = future.await(30, TimeUnit.SECONDS); ctx.writeAndFlush(response); // 结果回来再响应客户端 } // 异步处理完成后 public void asyncBizCallback(String id, Response result) { container.setSyncFuture(id, result); }
⚡ 另外你提到 Kafka异步投递场景
也可以,比如:
生产者投递消息
但 Kafka ack确认需要异步监听
可以用 Future 容器 await
监听器回调 signalFuture
Kafka producer发送场景:
// 发送消息 public Response sendKafkaMsg(String id, Message msg) { CondFuture<Boolean> future = container.getFuture(id); kafkaTemplate.send("topic", msg); Boolean success = future.await(10, TimeUnit.SECONDS); return success ? Response.success() : Response.fail(); } // Kafka 监听ack @KafkaListener(...) public void onKafkaAck(ProducerRecord record) { String id = record.key(); container.setSyncFuture(id, true); }
【使用 CondFuture 异步请求-响应】
使用
CondFutureContainer
管理异步请求-响应的标准流程
1. 客户端发送请求
客户端(比如 Web、APP、IoT设备)发来一个请求,请求携带一个全局唯一的 ID(reqId)。
2. 服务端 Netty 收到请求,准备 Future
public void handleClientRequest(ChannelHandlerContext ctx, Request req) { // 2.1 从容器中根据请求ID拿到一个CondFuture(如果不存在就新建) CondFuture<Response> future = container.getFuture(req.getId()); // 2.2 异步处理请求(比如下游数据库、Kafka、其他微服务) asyncBizProcess(req); // 2.3 当前线程阻塞等待 future 被 signal,超时时间设定,比如30秒 Response response = future.await(30, TimeUnit.SECONDS); // 2.4 Future被唤醒或者超时,发送响应回客户端 ctx.writeAndFlush(response); }
3. 异步业务处理(比如发Kafka消息)
public void asyncBizProcess(Request req) { // 发送消息到 Kafka,或者其他异步系统 sendKafkaMsg(req.getId(), buildKafkaMessage(req)); }
4. 异步发送 Kafka 消息,继续挂 Future 等待回调
public Response sendKafkaMsg(String id, Message msg) { // 再次确保有Future存在(可以视业务而定) CondFuture<Boolean> future = container.getFuture(id); // 异步发送 Kafka 消息 kafkaTemplate.send("topic", msg); // 等待异步Kafka确认回调 Boolean success = future.await(10, TimeUnit.SECONDS); // 10秒超时 return success ? Response.success() : Response.fail(); }
5. Kafka异步回调,唤醒对应 Future
@KafkaListener(topics = "topic") public void onKafkaAck(ProducerRecord record) { // 获取发送时设置的 ID String id = record.key(); // 唤醒对应 Future,传递回调结果 container.setSyncFuture(id, true); }
6. 服务端继续处理(被 signal 唤醒)
await()
被 signal 成功唤醒取到回调的数据(比如 Response)
通过 Netty
ctx.writeAndFlush(response)
把最终响应写回客户端完成整个闭环
🔥【完整时序图概览】
客户端 -> 服务端Netty -> Future挂起 服务端异步处理 -> Kafka投递 Kafka发送成功 -> Kafka回调 Kafka回调监听器 -> signal Future Netty线程唤醒 -> 响应客户端
🌟【一整套关键点总结】
关键动作
说明
container.getFuture(id)
新建或获取对应的 Future 实例
future.await(timeout)
阻塞等待异步结果,设置超时时间
asyncBizProcess(req)
异步处理逻辑,不要阻塞主线程
container.setSyncFuture(id, result)
异步回调后,唤醒对应 Future
ctx.writeAndFlush(response)
收到回调后,将结果响应给客户端
超时处理 如果超时,返回超时错误,保护服务线程不会死等
容器清理机制 定期清理过期的 Future,防止内存泄漏
🛠️【注意点和最佳实践】
req.getId()
必须保证全局唯一,不要冲突!future.await(timeout)
记得设置超时时间,防止永远挂死。Kafka、MQ等异步系统,需要能正确拿到回调的
id
。CondFutureContainer
要考虑过期数据清理(可以加定时器清理,保证健壮性)。容器最大数量(futureCount)可以根据业务并发量调整。
Signal的时候可以带业务处理结果对象(比如 Response 或者处理状态)。
高性能
可扩展
异步超时可控
防止内存泄露
支持高并发请求 的 异步请求-响应框架了!
而且可以随意套用在 Netty长连接、Kafka消息投递、IoT云端响应、RPC调用 等各种场景!
带超时保护和定时清理 Future的优化版 CondFutureContainer
支持超时保护 + 定时清理过期 Future + 自动降级防爆内存
📌 signal的作用
在你这个
CondFuture
里面,signal(T object)
方法的意思是:✅ 把【异步处理好的结果】塞进去(设置到
object
字段),
✅ 然后【唤醒】之前在await()
方法里因为等待而挂起的线程。简单说:通知“兄弟,结果来了,醒醒,继续跑吧!”
📦 源码回顾一下
比如你的
CondFuture
类里,signal
是这样实现的:public void signal(T object) { this.object = object; lock.lock(); try { condition.signal(); // 唤醒一个等待的线程 } finally { lock.unlock(); } }
流程:
this.object = object;
👉 先把结果保存。condition.signal();
👉 再用Condition
把await()
里卡住的线程唤醒。被唤醒的线程,就能拿到结果,继续干活了。
🌟 举个小例子串起来理解
Netty业务线程,收到客户端请求时:
CondFuture<Response> future = container.getFuture(req.getId()); Response resp = future.await(30, TimeUnit.SECONDS); // 这里阻塞等待
它在
await
,等某人唤醒它。异步处理线程,比如 Kafka 返回 ACK:
container.setSyncFuture(req.getId(), response);
内部调用了
future.signal(response)
,把结果塞进去并唤醒
。于是第1步里挂起的线程醒来,拿到
resp
,继续往客户端写回响应。
🧠 核心理解一句话
signal 就是把异步返回结果通知到同步等待的线程,打通整个流程的关键。
🔥 总结口诀
步骤
动作
线程1
await()
等待别人唤醒
线程2
业务处理完,
signal(结果)
唤醒线程1
醒了,拿到结果继续跑