概述
今天的项目中使用到netty进行网络通信以及使用CompletableFuture对异步操作结果进行处理,虽然之前学习过netty但大部分已经忘了,因此今天主要结合netty的使用及异步操作的处理复盘代码
netty
netty的基本工作流程
Netty 核心组件与餐厅角色类比表
由于我之前学过 Netty 的工作流程及计网,这里做个复习总结:Netty 的核心逻辑本质是高效实现 “客户端 - 服务端的请求响应”,结合计网知识很容易理解,这里我先简单概括在结合餐厅工作类比一下 :
通信的基础是 “连接”:Netty 通过 BossGroup 接收 TCP 连接请求,建立连接后交给 WorkerGroup,后续的 IO 操作全由 WorkerGroup 负责,分工解耦保证高并发;
数据传输靠 “Channel”:每个连接对应一个 Channel,它既是双向的数据传输管道,也管理着连接的生命周期(如存活、关闭);
数据处理靠 “Handler 流水线”:Channel 绑定的 ChannelPipeline 中,多个 Handler 按顺序协作 —— 入站时先解码(二进制→Java 对象)、再处理业务,出站时先编码(Java 对象→二进制)、再传输,确保数据处理的正确性;
关键前提是 “编解码”:因为网络传输只能是二进制字节流,而业务层用的是 Java 对象,所以必须通过编解码器完成格式转换,适配跨端通信的协议(如JSON)。
Netty 核心组件 | 餐厅对应角色 | 核心作用 |
---|---|---|
BossGroup(老板组) | 餐厅前台接待员 | 只负责 “接客”:接收客户端的连接请求(比如 RPC 客户端连服务端),接完后交给 WorkerGroup |
WorkerGroup(员工组) | 餐厅服务员 | 只负责 “服务”:处理已连接的客户端数据(读请求、写响应),不负责接新客 |
Channel | 餐桌(每个餐桌对应 1 个客人) | 代表一个网络连接(客户端 - 服务端的双向通道),数据通过 Channel 传输 |
ChannelPipeline | 餐桌的 “服务流水线” | 每个 Channel 都有一个流水线,里面串着多个 “Handler”(工人),数据按顺序经过每个 Handler |
Handler | 流水线工人 | 负责具体工作:比如 “编码工人”(把 Java 对象转成网络能传的字节)、“业务工人”(处理 RPC 调用)、“解码工人”(把字节转成 Java 对象) |
Bootstrap/ServerBootstrap | 餐厅开业前的 “准备流程” | 初始化 Netty 的核心组件(Boss/Worker、Channel 类型),相当于 “餐厅装修 + 招人” |
netty在代码中的使用
这里我们先来看下netty客户端发送请求的整体逻辑及代码:
public class NettyClient {
// 用CountDownLatch实现“发请求后等结果”(因为Netty是异步的)
private CountDownLatch countDownLatch;
private RpcResponse response; // 保存服务端返回的结果
// 发送RPC请求,返回结果
public RpcResponse sendRequest(RpcRequest request, String host, int port) {
countDownLatch = new CountDownLatch(1); // 计数器初始为1
// 1. 初始化WorkerGroup(客户端只有Worker,没有Boss,因为不用接连接)
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 2. 客户端启动器
Bootstrap bootstrap = new Bootstrap();
bootstrap
.group(workerGroup)
.channel(NioSocketChannel.class) // 客户端用NioSocketChannel
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 1. 编码器:把RpcRequest → 字节数组(发请求用)
pipeline.addLast(new RpcEncoder(RpcRequest.class));
// 2. 解码器:把字节数组 → RpcResponse(收结果用)
pipeline.addLast(new RpcDecoder(RpcResponse.class));
// 3. 客户端Handler:接收服务端的响应
pipeline.addLast(new NettyClientHandler(NettyClient.this));
}
});
// 3. 连接服务端(异步操作,但用sync()阻塞等连接成功)
ChannelFuture connectFuture = bootstrap.connect(host, port).sync();
// 4. 获取Channel,发送RPC请求(异步操作)
Channel channel = connectFuture.channel();
channel.writeAndFlush(request);
// 5. 阻塞等待结果(CountDownLatch:等服务端返回后,countDown()才继续)
countDownLatch.await();
// 6. 返回结果(此时response已经被客户端Handler赋值)
return response;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
}
return null;
}
// 供客户端Handler调用:收到结果后,唤醒等待的线程
public void setResponse(RpcResponse response) {
this.response = response;
countDownLatch.countDown(); // 计数器减为0,await()会继续
}
}
ChannelFuture connectFuture = bootstrap.connect(host, port).sync();
这里我们重点理解下这段代码:(首先我们要理解netty的异步模型,调用connect后并不会立刻连接,但我们不想同步阻塞因此先执行下去,当异步结果处理完后会修改ChannelFuture的结果,如果添加了监听器就可以在这里执行一些操作成功后的回调逻辑。下面我们具体来看下)
Netty 是异步非阻塞框架,所有 I/O 操作(如连接、读写、关闭)都是异步执行的 —— 调用 bootstrap.connect(…) 时,Netty 不会立刻阻塞等待连接完成,而是立即返回一个 ChannelFuture 对象,用它来 “占位” 后续的连接结果。
我们可以把 ChannelFuture 理解为一个 “未来结果的凭证”:
调用 connect(…) 的瞬间,连接可能还在建立中(比如 TCP 三次握手还没完成),此时 ChannelFuture 处于 “未完成” 状态;
当连接成功建立(TCP 握手完成)或连接失败(如目标主机不可达)时,Netty 会自动 “填充” 这个 ChannelFuture 的结果,使其变为 “已完成” 状态。
它的核心作用总结为 3 点:
**获取连接结果:**判断连接是成功还是失败,成功时获取建立好的 Channel(网络连接通道),失败时获取异常原因;
**监听连接状态:**通过添加 Listener(监听器),在连接完成时自动触发回调逻辑(无需主动轮询);
**控制异步流程:**通过 sync()/await() 等方法,将异步操作转为 “同步等待”(按需使用,避免阻塞线程池)。
**sync() 的作用:**阻塞当前线程,将异步连接转为同步等待,确保连接完成后再执行后续逻辑
接下来看下netty服务端的整体逻辑及代码:
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
// 当收到客户端数据时,自动触发这个方法(异步回调!)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 1. msg已经被解码器转成RpcRequest对象了
RpcRequest request = (RpcRequest) msg;
System.out.println("服务端收到请求:" + request.getInterfaceName() + "#" + request.getMethodName());
// 2. 调用本地服务方法(RPC的核心:找到服务实现类,反射调用)
// (这里的ServiceProvider是之前写的,保存“接口→实现类”的映射)
Object serviceImpl = ServiceProvider.getService(request.getInterfaceName());
// 反射获取方法并调用
Method method = serviceImpl.getClass().getMethod(
request.getMethodName(),
request.getParameterTypes()
);
Object result = method.invoke(serviceImpl, request.getParameters());
// 3. 封装结果为RpcResponse
RpcResponse response = new RpcResponse();
response.setRequestId(request.getRequestId()); // 用请求ID对应请求和响应
response.setResult(result);
// 4. 写回结果给客户端(Netty异步操作:writeAndFlush不阻塞)
ctx.writeAndFlush(response);
}
// 发生异常时触发
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close(); // 关闭连接
}
}
简单来说就是接受请求找到具体的方法执行并写回结果。了解了大体流程后学习今天的代码就很容易了:
public void start() {
EventLoopGroup boss = new NioEventLoopGroup(2);
EventLoopGroup worker = new NioEventLoopGroup(10);
try {
//2.需要一个服务器引导程序
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//核心部分,需要添加很多入站和出战的handler
socketChannel.pipeline().addLast(new SimpleChannelInboundHandler<>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
log.info("bytebuf--> {}",byteBuf.toString(Charset.defaultCharset()));
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("htrpc".getBytes()));
}
});
}
});
//4.绑定端口
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
}catch (InterruptedException e){
}finally {
try {
boss.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
下面结合刚刚服务端的整体逻辑来详细讲解下这段代码:
1. 初始化 Netty 线程组:BossGroup(老板组)和 WorkerGroup(员工组)
EventLoopGroup boss = new NioEventLoopGroup(2);
EventLoopGroup worker = new NioEventLoopGroup(10);
EventLoopGroup:Netty 的 “线程池”,管理多个EventLoop(单个线程),负责处理网络事件(连接、读 / 写数据)。
NioEventLoopGroup:基于 NIO(非阻塞 IO)的线程组,是 Netty 高效的核心 —— 支持 “一个线程处理多个连接”,避免传统 IO 的 “一个连接一个线程” 浪费资源。
参数含义:
boss = new NioEventLoopGroup(2):创建 2 个线程的 Boss 组(“2 个前台接待员”)。
作用:只负责接收客户端的连接请求(比如 RPC 客户端要连服务端,先找 Boss 组),接完后把连接交给 Worker 组处理,自己不碰数据。
worker = new NioEventLoopGroup(10):创建 10 个线程的 Worker 组(“10 个服务员”)。
作用:处理已连接的客户端数据(读客户端发的请求、写服务端的响应),不负责接收新连接。
为什么分 Boss 和 Worker?
类比餐厅:前台(Boss)只带客入座,服务员(Worker)只负责上菜 —— 分工明确,避免 “前台既要带客又要上菜” 导致效率低。
2. 创建服务端引导程序:ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
ServerBootstrap:Netty 服务端的 “启动配置器”,相当于餐厅开业前的 “准备清单”。
作用:把后续要配置的 “线程组、Channel 类型、Handler 流水线” 等组件整合起来,最终通过它启动服务。
3. 配置服务端核心参数:serverBootstrap.group(…) 链式调用
这部分是服务端的 “核心配置”,用链式调用把关键组件绑定到引导程序上:
serverBootstrap
// 3.1 绑定线程组:告诉服务端用哪个Boss和Worker
.group(boss, worker)
// 3.2 指定Channel类型:NioServerSocketChannel
.channel(NioServerSocketChannel.class)
// 3.3 配置“新连接的流水线”:每个客户端连接都会走这个流水线
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 给当前客户端连接的Channel添加Handler(流水线工人)
socketChannel.pipeline().addLast(new SimpleChannelInboundHandler<>() {
// 3.3.1 当收到客户端数据时,自动触发的方法(入站Handler)
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// ① 把收到的原始数据(msg)转成Netty的ByteBuf(字节容器)
ByteBuf byteBuf = (ByteBuf) msg;
// ② 打印收到的客户端数据(按UTF-8转成字符串)
log.info("bytebuf--> {}", byteBuf.toString(Charset.defaultCharset()));
// ③ 给客户端回写响应:发送"htrpc"字节(Unpooled是Netty的字节工具类)
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("htrpc".getBytes()));
}
});
}
});
我们拆解每个子步骤的作用:
3.1 .group(boss, worker)
作用:把之前创建的 Boss 组和 Worker 组 “交给” 引导程序,告诉服务端:“用这 2 个前台接客,10 个服务员干活”。
必须传两个线程组(Boss 在前,Worker 在后),服务端才能正常分工。
3.2 .channel(NioServerSocketChannel.class)
NioServerSocketChannel:Netty 服务端的 “连接监听通道”,相当于餐厅的 “大门”—— 专门用来监听客户端的连接请求。
作用:告诉服务端 “用 NIO 模式的通道来监听端口”,是 Netty 非阻塞 IO 的关键。
对比:客户端用的是NioSocketChannel(相当于 “顾客的餐盘”,用来传数据),服务端用NioServerSocketChannel(相当于 “餐厅大门”,用来接客)。
3.3 .childHandler(…):配置 “客户端连接的流水线”
这是最核心的部分,需要重点理解:
ChannelInitializer:一个 “通道初始化器”——每有一个新客户端连接服务端,就会创建一个新的SocketChannel(代表这个连接),并执行initChannel方法给这个 Channel 配置流水线。
类比:每来一个顾客,就给这个顾客分配一张新餐桌(SocketChannel),并给这张餐桌安排专属的 “服务流水线”(Handler)。
socketChannel.pipeline():获取当前连接的 “流水线”(ChannelPipeline),所有数据都会按顺序经过流水线上的 Handler。
addLast(new SimpleChannelInboundHandler<>()):给流水线添加一个 “入站 Handler”(处理客户端发过来的数据):
SimpleChannelInboundHandler:Netty 提供的 “简化版入站 Handler”,专门用来处理 “接收客户端数据” 的逻辑,自动帮我们做了 “数据类型转换” 和 “资源释放”(比原始的ChannelInboundHandlerAdapter更易用)。
channelRead0方法:入站 Handler 的核心回调方法 ——当服务端通过这个 Channel 收到客户端数据时,Netty 会自动调用这个方法(由 Worker 线程执行,不阻塞主线程)。
逻辑拆解:
ByteBuf byteBuf = (ByteBuf) msg:Netty 接收的原始数据是Object类型,实际是ByteBuf(Netty 的 “字节容器”,比 Java 原生的ByteBuffer好用),所以需要强转。
ctx.channel().writeAndFlush(…):给客户端回写响应 ——
Unpooled.copiedBuffer(“htrpc”.getBytes()):用 Netty 的Unpooled工具类,把字符串 “htrpc” 转成ByteBuf(网络传输只能传字节)。
writeAndFlush:“写数据并刷出”—— 把ByteBuf写到 Channel(连接)里,发给客户端(异步操作,调用后立刻返回,不用等客户端收到)。
ctx.channel():通过ChannelHandlerContext获取当前的连接 Channel(相当于 “当前顾客的餐桌”),确保响应发回给对应的客户端。
4. 绑定端口并启动服务:serverBootstrap.bind(port).sync()
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
bind(port):让服务端 “监听指定端口”(比如你 RPC 服务的端口 8088),相当于餐厅 “开门营业,挂出营业时间牌”。
这是一个异步操作,调用后立刻返回ChannelFuture(相当于 “营业准备的回执单”),表示 “绑定端口的操作正在进行中”,还没完成。
.sync():让当前线程(主线程)阻塞等待,直到 “绑定端口” 操作完成(成功或失败)。
为什么要加sync()?因为如果不加,主线程会直接往下走,可能服务还没绑定好端口就执行closeFuture().sync(),导致服务启动失败。
类比:餐厅开门前,老板(主线程)要等前台(Boss 组)确认 “大门已打开,能接客”,才继续往后安排。
ChannelFuture:Netty 异步操作的 “结果载体”—— 可以通过它判断操作是否成功,或添加回调(比如channelFuture.addListener(…))在操作完成后执行逻辑。
5. 阻塞等待服务关闭:channelFuture.channel().closeFuture().sync()
channelFuture.channel().closeFuture().sync();
channelFuture.channel():获取服务端的 “监听通道”(ServerChannel,即NioServerSocketChannel),相当于餐厅的 “大门”。
closeFuture():获取这个监听通道的 “关闭未来”—— 一个ChannelFuture,表示 “通道关闭的操作”(比如服务端主动关闭、被强制关闭)。
这个操作也是异步的,closeFuture()只是获取 “关闭的回执单”,不是立刻关闭。
.sync():让主线程一直阻塞,直到服务端的监听通道被关闭(比如你手动停服务、断电)。
作用:防止主线程执行完所有代码后退出,导致 Netty 的 Boss/Worker 线程也跟着退出,服务直接停止。
类比:餐厅老板(主线程)站在门口,一直等到 “打烊时间”,确认大门关闭后才离开。
6. 优雅关闭线程组:finally块
finally {
try {
boss.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
shutdownGracefully():Netty 提供的 “优雅关闭” 方法 (感觉很像四次挥手)
先停止接收新的网络事件(比如 Boss 组不再接新连接,Worker 组不再处理新数据);
等待已有的事件处理完成(比如 Worker 组把正在处理的客户端请求响应完);
最后关闭线程,释放资源。
对比 “强制关闭”(shutdown()):优雅关闭不会丢数据,强制关闭可能导致正在处理的请求中断。
.sync():阻塞等待线程组完全关闭,确保资源释放干净后,主线程再退出。
接下来重点看下客户端的这段代码:
首先我们需要知道整段代码的大致逻辑是:找服务(服务端会把服务注册到注册中心的节点上,我这用的是zookeeper);建立连接;发请求;等结果,接下来我们就里面的每一步详细介绍
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("hello proxy");
//1.发现服务:从注册中心中寻找一个可用的服务
//传入服务的名字,返回ip+端口
//todo 每次调用相关方法时都需要去注册中心拉取相关服务列表吗? 如何合理选择一个可用的服务?而不是只获取第一个?
InetSocketAddress address = registry.lookup(interfaceRef.getName());
if(log.isDebugEnabled()){
log.debug("服务调用方,发现了服务【{}】的可用主机【{}】",interfaceRef.getName()
,address);
}
//尝试获取通道
Channel channel = getAvailableChannel(address);
if(log.isDebugEnabled()){
log.debug("获取和【{}】建立的通道",address);
}
//2.先从全局缓存中获取通道
/**同步解决策略
ChannelFuture channelFuture = channel.writeAndFlush(new Object());
if(channelFuture.isDone()){
Object object = channelFuture.getNow();
} else if (!channelFuture.isSuccess()) {
//有问题,需要捕获异常
Throwable cause = channelFuture.cause();
throw new RuntimeException(cause);
}
**/
//写出报文
CompletableFuture<Object> completableFuture = new CompletableFuture<>();
// 需要将completabaFuture暴露出去
htrpcBootstrap.PENDING_REQUEST.put(1l,completableFuture);
channel.writeAndFlush(Unpooled.copiedBuffer("hello".getBytes())).addListener((ChannelFutureListener) promise -> {
// if(promise.isDone()){
// completableFuture.complete(promise.getNow());
// }
if (!promise.isSuccess()) {
completableFuture.completeExceptionally(promise.cause());
}
});
//获得结果
return completableFuture.get(10,TimeUnit.SECONDS);
}
/**
* 根据地址获取一个可用通道
* @param address
* @return
*/
private Channel getAvailableChannel(InetSocketAddress address) {
//1.先从缓存中获取
Channel channel = htrpcBootstrap.CHANNEL_CACHE.get(address);
//2.拿不到就建立连接
if(channel == null){
//await方法会阻塞,会等待连接成功在返回(同步)
// channel = NettyBootstrapInitilizer.getBootstrap()
// .connect(address).await().channel();
//异步操作
CompletableFuture<Channel> channelFuture = new CompletableFuture<>();
NettyBootstrapInitilizer.getBootstrap().connect(address).addListener((ChannelFutureListener) promise -> {
if(promise.isDone()){
if(log.isDebugEnabled()){
log.debug("已经和[{}]建立连接",address);
}
channelFuture.complete(promise.channel());
}else if(!promise.isSuccess()){
channelFuture.completeExceptionally(promise.cause());
}
});
//阻塞获取channel
try {
channel = channelFuture.get(3, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
log.error("获取通道时发生异常",e);
throw new DiscoveryException(e);
}
//缓存
htrpcBootstrap.CHANNEL_CACHE.put(address,channel);
}
if(channel == null){
log.error("获取【{}】的通道时发生异常",address);
throw new NetworkException("获取通道时发生了异常");
}
return channel;
}
1. 找服务:从注册中心获取服务端地址
InetSocketAddress address = registry.lookup(interfaceRef.getName());
作用:通过注册中心(如 ZooKeeper)查询HelloHtrpc接口对应的服务端 IP 和端口(比如192.168.1.100:8088)。这里的具体逻辑没列出来
类比:你想给朋友打电话,先从通讯录(注册中心)找到他的电话号码(IP + 端口)。
2. 建连接 / 拿通道:获取与服务端的网络连接(Channel)
Channel channel = getAvailableChannel(address);
Channel是 Netty 中 “客户端与服务端的双向连接通道”,相当于 “电话线路”—— 所有数据通过它发送和接收。
getAvailableChannel方法逻辑(这个等下具体讲):
① 先查缓存(htrpcBootstrap.CHANNEL_CACHE):如果之前连过这个服务端,直接复用已有的 Channel(避免重复建连接,节省资源)。
② 缓存没有则新建连接:通过 Netty 的Bootstrap连接服务端,拿到新的 Channel 后存入缓存。
3. 发请求:把调用信息发给服务端
// 创建一个CompletableFuture,用于接收响应结果
CompletableFuture<Object> completableFuture = new CompletableFuture<>();
// 把Future存入全局缓存,用请求ID关联(这里简化用1l,实际应是唯一ID)
htrpcBootstrap.PENDING_REQUEST.put(1l, completableFuture);
// 发送数据(这里简化发"hello",实际上应发RpcRequest对象(今天还没做):接口名、方法名、参数等)
channel.writeAndFlush(Unpooled.copiedBuffer("hello".getBytes()))
.addListener((ChannelFutureListener) promise -> {
// 发送操作的回调:如果发送失败,用异常完成Future
if (!promise.isSuccess()) {
completableFuture.completeExceptionally(promise.cause());
}
});
作用:通过 Channel 把请求数据发给服务端,并准备接收响应。
关键:用CompletableFuture暂存 “等待响应的状态”(这个等下重点讲),并关联到全局缓存(PENDING_REQUEST),方便后续收到响应时 “找到对应的请求”。
4. 等结果:阻塞等待服务端响应
return completableFuture.get(10, TimeUnit.SECONDS);
作用:当前线程(调用sayHi的线程)阻塞等待 10 秒,直到服务端返回结果或超时。
类比:打完电话(发请求)后,拿着听筒等对方回应(响应),期间啥也干不了(阻塞)。
接下来重点看下getAvailableChannel这个方法中是怎样异步处理的:
// 异步建立连接:调用connect后不阻塞,通过监听器处理结果
CompletableFuture<Channel> channelFuture = new CompletableFuture<>();
NettyBootstrapInitilizer.getBootstrap().connect(address)
.addListener((ChannelFutureListener) promise -> {
if(promise.isDone()){
// 连接成功,用Channel完成Future
channelFuture.complete(promise.channel());
}else if(!promise.isSuccess()){
// 连接失败,用异常完成Future
channelFuture.completeExceptionally(promise.cause());
}
});
// 阻塞获取结果(异步操作转同步等待)
channel = channelFuture.get(3, TimeUnit.SECONDS);
核心:
① 调用 Netty 的connect(异步操作),立刻返回ChannelFuture,不阻塞。
② 给ChannelFuture加监听器(addListener):当连接完成(成功 / 失败)时,自动触发回调,通过CompletableFuture的complete或completeExceptionally记录结果。
为什么需要用 CompletableFuture?
CompletableFuture是 Java 中处理 “异步操作” 的工具类,在这段代码中解决了 3 个关键问题:
- 解决 “异步操作结果的暂存与传递”
Netty 的connect、writeAndFlush都是异步的,调用后不知道结果何时返回。CompletableFuture相当于一个 “结果容器”:
异步操作完成前:容器是空的,调用get()会阻塞等待。
异步操作完成后:通过complete或completeExceptionally把结果(或异常)放入容器,唤醒等待的线程。
- 实现 “异步转同步” 的优雅衔接
RPC 调用需要 “像本地方法一样等待结果”(同步),但 Netty 操作是异步的。CompletableFuture的get()方法正好实现这种转换:
异步操作(如连接、发请求)通过监听器 “填充”CompletableFuture。
调用方通过get()阻塞等待,直到结果被填充 —— 既利用了 Netty 异步 IO 的高效,又满足了 RPC 调用的同步需求。
- 方便处理异常传递
网络操作容易出问题(如连接超时、发送失败),CompletableFuture的completeExceptionally可以把异常 “封装” 到 Future 中,调用get()时会自动抛出异常,避免了手动处理异常的繁琐。
CompletableFuture的核心机制总结
从代码中可以看出,它的工作原理依赖三个核心部分:
状态管理
内部维护三种状态:
未完成(Incomplete):初始状态,结果为空。
已完成(Completed):通过complete(result)更新,保存正常结果。
异常完成(CompletedExceptionally):通过completeExceptionally(ex)更新,保存异常。
状态一旦更新就不可变(类似 “一次性开关”)。
结果容器
本质是一个 “线程安全的结果盒子”:
异步操作完成后,通过complete/completeExceptionally向盒子里 “放结果”。
调用get()的线程从盒子里 “拿结果”,如果没放进去就阻塞等待。
回调与通知
支持两种处理结果的方式:
阻塞等待:通过get()让当前线程阻塞,直到结果就绪(适合需要同步获取结果的场景,如你的 RPC 调用)。
异步回调:通过whenComplete/thenAccept等方法注册回调函数,结果就绪时自动执行(比如不需要阻塞,只想在结果出来后做后续处理)。
总结
今天项目主要复习了netty的工作流程以及如何使用工具类完成异步操作的请求处理