加群联系作者vx:xiaoda0423
仓库地址:https://webvueblog.github.io/JavaPlusDoc/
阶段 | 说明 | 涉及数据结构 |
---|---|---|
创建 bossGroup、workerGroup | 分别管理接入连接和后续 IO 读写 | NioEventLoopGroup (内部有多线程执行) |
配置 serverBootstrap | 设置各种 Netty 参数,组装 ServerBootstrap | ServerBootstrap |
注册 childHandler | xxServerChInit 用于初始化每条新的连接 ChannelPipeline |
ChannelInitializer<SocketChannel> |
调用 bind(port) | 把服务绑定到 9000 端口,监听 TCP 连接 | ChannelFuture |
客户端连接到服务器 | bossGroup 接收连接,workerGroup 负责后续的数据通信 | NioServerSocketChannel 、NioSocketChannel |
连接初始化 | xxServerChInit 中添加的 Handler 被执行 |
ChannelPipeline (责任链模式) |
读写数据处理 | 自定义 Handler 处理接收到的数据 | 自定义消息类(比如 JT808 报文、定制指令) |
服务关闭 | 关闭所有资源,释放端口 | shutdownGracefully |
主要涉及的数据结构变化
变化点 | 变化描述 |
---|---|
ServerBootstrap -> ChannelFuture |
通过 bind 端口后,返回 ChannelFuture 表示异步绑定结果。 |
NioEventLoopGroup -> NioEventLoop |
创建的 boss/worker 组内部是多个 NioEventLoop (线程 + Selector)。 |
SocketChannel -> ChannelPipeline |
每次有连接建立时,初始化 ChannelPipeline ,里面串联各种 Handler。 |
AdaptiveRecvByteBufAllocator |
动态分配读缓冲区大小(避免内存浪费或过小导致频繁扩容)。 |
go
@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接收连接,调用 initChannel 方法 |
SocketChannel |
创建责任链(Pipeline) | pipeline() 返回一个空白链表结构 |
DefaultChannelPipeline |
加入 IdleStateHandler | 监测180秒内是否有读写,否则触发 IdleStateEvent |
IdleStateHandler |
加入解码器 xxxDecoder | 收到数据时,自动从 ByteBuf 解析成业务 POJO | xxDecoder |
加入编码器 xxEncoder | 发送数据时,把业务对象转成 ByteBuf | xxEncoder |
加入业务处理器 xxxServerHandle | 处理 decode后的业务消息,执行业务逻辑 | xxServerHandle |
主要涉及的数据结构变化
阶段 | 变化描述 |
---|---|
SocketChannel.pipeline() |
初始化一个新的责任链 DefaultChannelPipeline |
addLast 操作 |
往 Pipeline 中按顺序插入 Handler,形成链表 |
ByteBuf -> BusinessMessage |
通过 Decoder 把原始字节流反序列化成对象 |
BusinessMessage -> ByteBuf |
通过 Encoder 把业务对象编码成字节流 |
IdleStateEvent |
超时后通过 pipeline 触发一个 userEventTriggered |
JVM 调优:
1. 垃圾回收器选择
-
建议:使用 G1 GC
-XX:+UseG1GC
-
特点:适合高并发、低延迟场景,GC暂停时间可控。
2. 堆内存配置
-
根据实际连接数量和消息量,合理设置堆大小:
go-Xms2g -Xmx2g
(初始和最大一致,避免动态扩容带来的卡顿)
3. Netty内存管理优化
-
Netty 默认用 PooledByteBufAllocator,确保启用池化:
gobootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
-
减少小对象频繁分配带来的GC压力。
4. 线程数配置
-
bossGroup 和 workerGroup 线程数可以根据 CPU 核心数调整:
gonew 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 一致性 |
类型安全 | 泛型丢失 | 用 T 泛型 |
并发性能 | 粗锁 | 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 把响应写回客户端
-
-
完成一次完整的异步请求-响应!
-
示意伪代码:
go// 收到请求 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发送场景:
go// 发送消息 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
gopublic 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消息)
gopublic void asyncBizProcess(Request req) { // 发送消息到 Kafka,或者其他异步系统 sendKafkaMsg(req.getId(), buildKafkaMessage(req)); }
4. 异步发送 Kafka 消息,继续挂 Future 等待回调
gopublic 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
go@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)
把最终响应写回客户端 -
完成整个闭环
🔥【完整时序图概览】
go客户端 -> 服务端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
是这样实现的:gopublic void signal(T object) { this.object = object; lock.lock(); try { condition.signal(); // 唤醒一个等待的线程 } finally { lock.unlock(); } }
流程:
-
this.object = object;
👉 先把结果保存。 -
condition.signal();
👉 再用Condition
把await()
里卡住的线程唤醒。 -
被唤醒的线程,就能拿到结果,继续干活了。
🌟 举个小例子串起来理解
- Netty业务线程,收到客户端请求时:
goCondFuture<Response> future = container.getFuture(req.getId()); Response resp = future.await(30, TimeUnit.SECONDS); // 这里阻塞等待
它在
await
,等某人唤醒它。- 异步处理线程,比如 Kafka 返回 ACK:
gocontainer.setSyncFuture(req.getId(), response);
内部调用了
future.signal(response)
,把结果塞进去并唤醒
。- 于是第1步里挂起的线程醒来,拿到
resp
,继续往客户端写回响应。
🧠 核心理解一句话
signal 就是把异步返回结果通知到同步等待的线程,打通整个流程的关键。
🔥 总结口诀
步骤 动作 线程1 await()
等待别人唤醒线程2 业务处理完, signal(结果)
唤醒线程1 醒了,拿到结果继续跑 -