项目学习总结(2)

概述

今天的项目中使用到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 个关键问题:

  1. 解决 “异步操作结果的暂存与传递”
    Netty 的connect、writeAndFlush都是异步的,调用后不知道结果何时返回。CompletableFuture相当于一个 “结果容器”:

异步操作完成前:容器是空的,调用get()会阻塞等待。
异步操作完成后:通过complete或completeExceptionally把结果(或异常)放入容器,唤醒等待的线程。

  1. 实现 “异步转同步” 的优雅衔接
    RPC 调用需要 “像本地方法一样等待结果”(同步),但 Netty 操作是异步的。CompletableFuture的get()方法正好实现这种转换:

异步操作(如连接、发请求)通过监听器 “填充”CompletableFuture。
调用方通过get()阻塞等待,直到结果被填充 —— 既利用了 Netty 异步 IO 的高效,又满足了 RPC 调用的同步需求。

  1. 方便处理异常传递
    网络操作容易出问题(如连接超时、发送失败),CompletableFuture的completeExceptionally可以把异常 “封装” 到 Future 中,调用get()时会自动抛出异常,避免了手动处理异常的繁琐。

CompletableFuture的核心机制总结
从代码中可以看出,它的工作原理依赖三个核心部分:

状态管理
内部维护三种状态:
未完成(Incomplete):初始状态,结果为空。
已完成(Completed):通过complete(result)更新,保存正常结果。
异常完成(CompletedExceptionally):通过completeExceptionally(ex)更新,保存异常。
状态一旦更新就不可变(类似 “一次性开关”)。

结果容器
本质是一个 “线程安全的结果盒子”:
异步操作完成后,通过complete/completeExceptionally向盒子里 “放结果”。
调用get()的线程从盒子里 “拿结果”,如果没放进去就阻塞等待。

回调与通知
支持两种处理结果的方式:
阻塞等待:通过get()让当前线程阻塞,直到结果就绪(适合需要同步获取结果的场景,如你的 RPC 调用)。
异步回调:通过whenComplete/thenAccept等方法注册回调函数,结果就绪时自动执行(比如不需要阻塞,只想在结果出来后做后续处理)。

总结

今天项目主要复习了netty的工作流程以及如何使用工具类完成异步操作的请求处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值