Server端grpc调用client,client回调是怎么实现的

Snail AI 的 Server ↔ Client 双向 gRPC 通信机制
项目采用 Server-Client 双向 gRPC 通信 架构。Agent(Client)同时扮演两个角色:
gRPC 客户端:主动回调 Server(如创建对话、保存消息、RAG 搜索)
gRPC 服务端:被动接收 Server 的聊天分发请求

整体通信拓扑:

image

一、Server 主动调用 Client(聊天分发)
1. Server 端发起 gRPC Streaming 调用
在 LlmCallHandler 中,Server 通过 GrpcChannelUtil.sendServerStreaming() 将聊天请求推送给 Client:

// LlmCallHandler.java (Server 端, @Order(80))
GrpcChannelUtil.sendServerStreaming(
    ctx.getTargetClient().getChannel(),   // 连接到 Client 的 gRPC channel
    UriConstants.CHAT_DISPATCH,            // URI = "/chat/dispatch"
    dispatchBody,                          // JSON 格式的 ChatDispatchRequest
    Map.of(),
    new ChatStreamObserver(ctx, chatResultPersistService, shortTermWindow)); // 流式回调

2. Client 端接收并分发请求
Client 内嵌了一个 ClientGrpcServer(监听 1790 端口),注册了 Unary 和 Streaming 两种服务:

// ClientGrpcServer.java (Client 端)
public void start() throws Exception {
    server = NettyServerBuilder.forPort(port)
        .addService(GrpcServiceDefinitionBuilder.createUnaryServiceDefinition(unaryHandler))
        .addService(GrpcServiceDefinitionBuilder.createServerStreamingServiceDefinition(streamingHandler))
        .maxInboundMessageSize(10 * 1024 * 1024)
        .build()
        .start();
}

Streaming 请求到达后,由 ClientRequestDispatcher 分发:

// ClientRequestDispatcher.java (Client 端)
public ServerCalls.ServerStreamingMethod<GrpcSnailAiRequest, GrpcSnailAiResult> streamingHandler() {
    return (request, observer) -> {
        String uri = request.getMetadata().getUri();
        Map<String, String> headers = request.getMetadata().getHeadersMap();
        grpcRequestDispatcher.dispatchStreaming(
            uri, headers, request.getBody(), request.getReqId(), observer);
    };
}

3. GrpcRequestDispatcher 按 URI 路由到 Handler

// GrpcRequestDispatcher.java (共用)
public void dispatchStreaming(String uri, Map<String, String> headers, String body,
                              long reqId, StreamObserver<GrpcSnailAiResult> observer) {
    for (GrpcStreamingRequestHandler handler : streamingHandlers) {
        if (handler.supports(uri)) {
            handler.handle(request, observer);
            return;
        }
    }
}

4. ChatDispatchStreamingHandler 处理聊天请求

// ChatDispatchStreamingHandler.java (Client 端)
public void handle(GrpcHandlerRequest request, StreamObserver<GrpcSnailAiResult> observer) {
    ChatDispatchRequest dispatchRequest = parseDispatchRequest(request.getBody());

    chatSessionRuntime.execute(ChatSessionRequest.builder()
        .dispatchRequest(dispatchRequest)
        .textConsumer(text -> handleTextChunk(reqId, sid, text, observer))      // 流式返回文本
        .thinkingConsumer(thinking -> handleThinkingChunk(reqId, sid, thinking, observer)) // 思考过程
        .completionConsumer(completion -> handleCompletion(reqId, ...))          // 完成
        .errorConsumer(error -> handleError(reqId, sid, error, observer))        // 错误
        .build());
}

每个文本 chunk 通过 observer.onNext() 流式推送回 Server:

private void handleTextChunk(long reqId, String sid, String text, StreamObserver<GrpcSnailAiResult> observer) {
    ChatStreamResponse response = ChatStreamResponse.text(sid, text);
    String data = JsonUtil.toJsonString(response);
    observer.onNext(GrpcSnailAiResult.newBuilder()
        .setReqId(reqId)
        .setStatus(1)
        .setData(data)
        .build());
}

5. Server 端 ChatStreamObserver 接收流式结果

// ChatStreamObserver.java (Server 端)
public void onNext(GrpcSnailAiResult result) {
    ChatStreamResponse data = JsonUtil.parseObject(result.getData(), ChatStreamResponse.class);
    switch (data.getType()) {
        case ChatStreamResponse.TYPE_TEXT -> handleText(data);       // 转发给前端
        case ChatStreamResponse.TYPE_THINKING -> handleThinking(data);
        case ChatStreamResponse.TYPE_COMPLETION -> handleCompletion(data); // 持久化
    }
}

private void handleText(ChatStreamResponse data) {
    String json = JsonUtil.toJsonString(ChatStreamResponse.text(data.getSid(), data.getContent()));
    context.getStreamWriter().send(json + "\n");  // 写入 HTTP SSE 流
}

二、Client 回调 Server(核心问题)
Client 需要回调 Server 的场景:创建对话、保存消息记录、加载短期记忆、获取 Skill 内容、RAG 搜索。
1. RpcClient 接口定义(Client 端)
通过 @Mapping 注解声明每个回调方法对应的 URI

// RpcClient.java (Client 端接口)
public interface RpcClient {

    @Mapping(path = UriConstants.CALLBACK_CONVERSATION_CREATE)    // "/callback/conversation/create"
    void createConversation(ConversationCreateRequest request);

    @Mapping(path = UriConstants.CALLBACK_CONVERSATION_RECORD)    // "/callback/conversation/record"
    void saveRecord(ConversationRecordRequest request);

    @Mapping(path = UriConstants.CALLBACK_MEMORY_SHORT_TERM)      // "/callback/memory/short-term"
    List<Map<String, Object>> loadShortTermHistory(ShortTermMemoryRequest request);

    @Mapping(path = UriConstants.CALLBACK_SKILL_CONTENT)          // "/callback/skill/content"
    SkillContentResponse fetchSkillContent(SkillContentRequest request);

    @Mapping(path = UriConstants.CALLBACK_RAG_SEARCH, timeout = 120_000) // "/callback/rag/search"
    RagSearchResponse searchRag(RagSearchRequest request);
}

2. JDK 动态代理生成实现
Spring 容器中通过 Proxy.newProxyInstance() 创建代理实例:

// SnailAiAgentAutoConfiguration.java (Client 端)
@Bean
public RpcClient serverCallback(GrpcChannelProvider channelProvider, SnailAiAgentProperties properties) {
    InvocationHandler handler = new GrpcClientInvokeHandler(channelProvider, properties);
    return (RpcClient) Proxy.newProxyInstance(
        RpcClient.class.getClassLoader(),
        new Class<?>[]{RpcClient.class},
        handler
    );
}

3. GrpcClientInvokeHandler — 回调核心执行逻辑
当 Client 业务代码调用 rpcClient.createConversation(req) 时:

// GrpcClientInvokeHandler.java (Client 端)
public Object invoke(Object proxy, Method method, Object[] args) {
    Mapping mapping = method.getAnnotation(Mapping.class);
    String uri = mapping.path();      // 如 "/callback/conversation/create"
    long timeout = mapping.timeout();
    return executeWithRetry(callbackName, uri, method, timeout, args, returnType);
}

doInvoke() 方法执行实际的 gRPC 调用:

private Object doInvoke(String uri, Method method, long timeout, Object[] args, Class<?> returnType) {
    // 1. 从 GrpcChannelProvider 获取连接到 Server 的 channel
    ManagedChannel channel = channelProvider.getChannel();

    // 2. 设置超时
    CallOptions options = CallOptions.DEFAULT.withDeadlineAfter(timeout, TimeUnit.MILLISECONDS);

    // 3. 通过 GrpcChannelUtil.sendUnary() 发送 Unary 请求到 Server
    GrpcSnailAiResult result = GrpcChannelUtil.sendUnary(
        channel, uri, JsonUtil.toJsonString(params),
        channelProvider.getHeaders(), options
    );

    // 4. 检查响应状态
    if (result == null || result.getStatus() != 1) {
        throw new CallbackServerErrorException("Server error: " + result.getMessage());
    }

    // 5. 解析返回数据
    return JsonUtil.parseObject(result.getData(), returnType);
}

底层 GrpcChannelUtil.sendUnary() 构建通用信封消息:

// GrpcChannelUtil.java
public static GrpcSnailAiResult sendUnary(ManagedChannel channel, String uri, String body,
                                           Map<String, String> headers, CallOptions callOptions) {
    // 构建统一信封:reqId + metadata(uri, headers) + body(JSON)
    GrpcSnailAiRequest request = GrpcSnailAiRequest.newBuilder()
        .setReqId(REQ_ID_GEN.incrementAndGet())
        .setMetadata(Metadata.newBuilder().setUri(uri).putAllHeaders(headers).build())
        .setBody(body)
        .build();

    // 同步阻塞调用
    return ClientCalls.blockingUnaryCall(channel.newCall(md, callOptions), request);
}

4. Server 端接收回调请求
回调请求到达 Server 的 SnailAiServerGrpcServer(监听 1789 端口):

// SnailAiServerGrpcServer.java (Server 端)
private void handleUnaryRequest(GrpcSnailAiRequest request, StreamObserver<GrpcSnailAiResult> observer) {
    String uri = request.getMetadata().getUri();
    Map<String, String> headers = request.getMetadata().getHeadersMap();

    GrpcSnailAiResult result = grpcRequestDispatcher.dispatchUnary(
        request.getReqId(), uri, headers, request.getBody());
    observer.onNext(result);
    observer.onCompleted();
}

GrpcRequestDispatcher 按 URI 路由到对应的 CallbackHandler:

// GrpcRequestDispatcher.java
public GrpcSnailAiResult dispatchUnary(long reqId, String uri, Map<String, String> headers, String body) {
    for (GrpcRequestHandler handler : unaryHandlers) {
        if (handler.supports(uri)) {
            return handler.handle(request).toBuilder().setReqId(reqId).build();
        }
    }
    return GrpcDispatchResults.unknownUri(reqId, uri);
}

5. 具体 CallbackHandler 处理(以创建对话为例)

// ConversationCreateCallbackHandler.java (Server 端)
@Component
public class ConversationCreateCallbackHandler implements GrpcRequestHandler {

    public boolean supports(String uri) {
        return UriConstants.CALLBACK_CONVERSATION_CREATE.equals(uri);
    }

    public GrpcSnailAiResult handle(GrpcHandlerRequest request) {
        ConversationCreateRequest req = JsonUtil.parseObject(request.getBody(), ConversationCreateRequest.class);

        // 创建对话(如不存在)
        long count = conversationMapper.selectCount(...);
        if (count == 0) {
            conversationMapper.insert(AgentConversationPO.builder()
                .agentId(req.getAgentId()).userId(req.getUserId())
                .conversationId(req.getConversationId()).title(title)
                .build());
        }

        // 保存用户消息
        recordMapper.insert(AgentConversationRecordPO.builder()
            .agentId(req.getAgentId()).conversationId(req.getConversationId())
            .role(ConversationRoleEnum.USER.getValue())
            .content(req.getUserMessage())
            .build());

        return GrpcSnailAiResult.newBuilder().setStatus(1).setMessage("OK").build();
    }
}

三、完整调用链路示例(以一次聊天为例)

用户 → [HTTP SSE] → Server → [责任链处理]
                              ↓
                    LlmCallHandler (Order 80)
                              ↓
              GrpcChannelUtil.sendServerStreaming()
              URI="/chat/dispatch" → Client:1790
                              ↓
                    ClientGrpcServer 接收
                              ↓
                    ClientRequestDispatcher
                              ↓
                    GrpcRequestDispatcher.dispatchStreaming()
                              ↓
                    ChatDispatchStreamingHandler
                              ↓
                    调用 LLM,流式返回 chunks
                              ↓
              ┌─ 过程中回调 Server ──────────────────────┐
              │  rpcClient.createConversation(req)        │
              │  → GrpcClientInvokeHandler (JDK Proxy)    │
              │  → GrpcChannelUtil.sendUnary()            │
              │  → URI="/callback/conversation/create"    │
              │  → Server:1789 接收                       │
              │  → ConversationCreateCallbackHandler      │
              │  → 创建对话 + 保存用户消息                  │
              │  → 返回 GrpcSnailAiResult(status=1)       │
              └───────────────────────────────────────────┘
                              ↓
              observer.onNext() 流式 chunks 回传 Server
                              ↓
                    ChatStreamObserver.onNext()
                              ↓
                    context.getStreamWriter().send() → 前端 SSE

 

 

总结

image

关键设计点:
通用信封协议:所有请求统一用 GrpcSnailAiRequest,通过 metadata.uri 路由
JDK 动态代理:RpcClient 接口方法上的 @Mapping 注解自动翻译为 gRPC 调用,业务代码无感知
重试机制:GrpcClientInvokeHandler.executeWithRetry() 内置重试 + 超时控制
Channel 管理:GrpcChannelProvider 管理 Client→Server 的连接,由心跳任务维护更新

 

posted @ 2026-09-22 18:08  雨也飘柔  阅读(6)  评论(0)    收藏  举报