Netty实战应用学习笔记-中级拓展篇(七)

🔄 项目七:请求响应同步通信(RPC基础)

项目概述

项目名称:itstack-demo-netty-2-07
核心功能:实现类似RPC的同步请求-响应通信模式
技术要点:Future模式、CountDownLatch、请求ID映射、超时控制

为什么需要这个项目?

实际问题

  • Netty默认是异步通信,无法像HTTP一样同步等待响应
  • RPC框架需要同步调用(发送请求→等待响应→返回结果)
  • 多个请求同时发送,响应可能乱序返回
  • 需要超时控制,避免无限等待

解决方案

  • ✅ Future模式:异步转同步,阻塞等待响应
  • ✅ CountDownLatch:线程阻塞和唤醒机制
  • ✅ 请求ID映射:准确匹配请求和响应
  • ✅ 超时控制:避免无限等待,提升系统稳定性

适用场景

  • RPC框架(Dubbo、gRPC的核心原理)
  • 同步调用远程服务
  • 需要等待响应结果的场景
  • 分布式事务(需要确认操作结果)

核心问题

为什么需要同步通信?

普通的Socket通信是异步的(像QQ聊天,发送消息后不需要立即等待回复),但RPC框架需要同步通信(像HTTP调用,必须等待响应返回或超时)。

异步通信(Netty默认):
客户端发送消息 → 继续执行其他代码 → 某个时刻收到响应

同步通信(RPC需要):
客户端发送消息 → 阻塞等待 → 收到响应或超时 → 继续执行

核心知识点

1. Future模式实现同步等待

WriteFuture接口

public interface WriteFuture<T> extends Future<T> {
    Throwable cause();                    // 获取异常
    void setCause(Throwable cause);       // 设置异常
    boolean isWriteSuccess();             // 是否发送成功
    void setWriteResult(boolean result);  // 设置发送结果
    String requestId();                   // 获取请求ID
    T response();                         // 获取响应
    void setResponse(Response response);  // 设置响应
    boolean isTimeout();                  // 是否超时
}

SyncWriteFuture实现类

public class SyncWriteFuture implements WriteFuture<Response> {
    private CountDownLatch latch = new CountDownLatch(1);  // 核心:阻塞工具
    private final long begin = System.currentTimeMillis();
    private long timeout;
    private Response response;
    private final String requestId;
    private boolean writeResult;
    private Throwable cause;
    private boolean isTimeout = false;
    
    // 阻塞等待响应
    public Response get(long timeout, TimeUnit unit) {
        if (latch.await(timeout, unit)) {  // 阻塞等待
            return response;
        }
        return null;  // 超时返回null
    }
    
    // 设置响应,唤醒等待线程
    public void setResponse(Response response) {
        this.response = response;
        latch.countDown();  // 唤醒等待的线程
    }
}

CountDownLatch工作原理

线程A:发送请求 → latch.await() → 阻塞等待
                                    ↓
线程B:收到响应 → setResponse() → latch.countDown() → 唤醒线程A
2. 请求ID映射机制

全局映射表

public class SyncWriteMap {
    // 存储 requestId → Future 的映射关系
    public static Map<String, WriteFuture> syncKey = 
        new ConcurrentHashMap<>();
}

为什么需要请求ID?

在Netty中,多个请求可能同时发送,响应返回的顺序可能与发送顺序不同。通过请求ID可以准确匹配请求和响应。

客户端发送:
请求1(ID=uuid-001) → 服务器
请求2(ID=uuid-002) → 服务器
请求3(ID=uuid-003) → 服务器

服务器响应(可能乱序):
响应2(ID=uuid-002) → 客户端(通过ID找到请求2的Future)
响应1(ID=uuid-001) → 客户端(通过ID找到请求1的Future)
响应3(ID=uuid-003) → 客户端(通过ID找到请求3的Future)
3. 完整通信流程

SyncWrite.java - 发送请求并等待响应

public class SyncWrite {
    public Response writeAndSync(Channel channel, Request request, 
                                long timeout) throws Exception {
        // 1. 生成唯一请求ID
        String requestId = UUID.randomUUID().toString();
        request.setRequestId(requestId);
        
        // 2. 创建Future并存入全局Map
        WriteFuture<Response> future = new SyncWriteFuture(requestId);
        SyncWriteMap.syncKey.put(requestId, future);
        
        // 3. 发送请求并阻塞等待响应
        Response response = doWriteAndSync(channel, request, timeout, future);
        
        // 4. 清理映射关系
        SyncWriteMap.syncKey.remove(requestId);
        return response;
    }
    
    private Response doWriteAndSync(Channel channel, Request request, 
                                    long timeout, WriteFuture<Response> writeFuture) {
        // 发送请求,添加监听器
        channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture future) {
                writeFuture.setWriteResult(future.isSuccess());
                writeFuture.setCause(future.cause());
                // 发送失败,移除映射
                if (!writeFuture.isWriteSuccess()) {
                    SyncWriteMap.syncKey.remove(writeFuture.requestId());
                }
            }
        });
        
        // 阻塞等待响应
        Response response = writeFuture.get(timeout, TimeUnit.MILLISECONDS);
        if (response == null) {
            if (writeFuture.isTimeout()) {
                throw new TimeoutException();
            } else {
                throw new Exception(writeFuture.cause());
            }
        }
        return response;
    }
}

服务端Handler - 处理请求并返回响应

@Override
public void channelRead(ChannelHandlerContext ctx, Object obj) {
    Request msg = (Request) obj;
    
    // 构建响应,保持相同的requestId(关键!)
    Response response = new Response();
    response.setRequestId(msg.getRequestId());
    response.setParam(msg.getResult() + " 请求成功");
    
    // 发送响应
    ctx.writeAndFlush(response);
}

客户端Handler - 接收响应并唤醒等待线程

@Override
public void channelRead(ChannelHandlerContext ctx, Object obj) {
    Response msg = (Response) obj;
    String requestId = msg.getRequestId();
    
    // 根据requestId找到对应的Future
    SyncWriteFuture future = (SyncWriteFuture) SyncWriteMap.syncKey.get(requestId);
    if (future != null) {
        future.setResponse(msg);  // 设置响应,唤醒等待线程
    }
}
4. 完整时序图
客户端线程                    Netty IO线程                  服务端
   |                              |                           |
   |--1.生成requestId------------>|                           |
   |--2.创建Future并存入Map------->|                           |
   |--3.发送请求----------------->|--4.网络传输-------------->|
   |--5.阻塞等待(latch.await)     |                           |
   |                              |                           |--6.处理请求
   |                              |<--7.返回响应(带requestId)--|
   |                              |                           |
   |<-8.根据requestId找到Future---|                           |
   |<-9.setResponse唤醒线程-------|                           |
   |--10.返回结果                 |                           |

项目总结

核心技术

  • ✅ CountDownLatch:实现线程阻塞和唤醒
  • ✅ 请求ID映射:准确匹配请求和响应
  • ✅ Future模式:异步转同步
  • ✅ 超时控制:避免无限等待

适用场景

  • RPC框架(Dubbo、gRPC)
  • 同步调用远程服务
  • 需要等待响应结果的场景

关键点

  • 每个请求必须有唯一ID
  • 服务端必须原样返回requestId
  • 使用ConcurrentHashMap保证线程安全
  • 及时清理映射关系,避免内存泄漏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值