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

一、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
总结

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

浙公网安备 33010602011771号