4. gRPC 服务定义:service 关键字与四种 RPC 模式

在领域驱动设计中,gRPC 服务定义是契约边界的核心载体。通过 service 关键字,我们将领域能力暴露为远程可调用的方法集合,并严格定义请求与响应的消息结构。gRPC 支持四种 RPC 模式,分别对应不同的领域交互场景。

4.1 service 关键字:定义领域服务契约

在 .proto 文件中,service 关键字用于声明一个 gRPC 服务。每个服务包含一组 RPC 方法,每个方法指定输入消息类型和输出消息类型。这是领域服务在分布式系统中的显式边界。

syntax = "proto3";

package order.v1;

// 订单领域服务 —— 定义订单相关的远程调用契约
service OrderService {
    // 创建订单(一元 RPC)
    rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);

    // 查询订单列表(服务端流式 RPC)
    rpc ListOrders(ListOrdersRequest) returns (stream Order);

    // 批量提交订单(客户端流式 RPC)
    rpc BatchSubmitOrders(stream Order) returns (BatchSubmitResponse);

    // 实时订单状态跟踪(双向流式 RPC)
    rpc TrackOrderStatus(stream OrderStatusRequest) returns (stream OrderStatusEvent);
}

message CreateOrderRequest {
    string user_id = 1;
    repeated OrderItem items = 2;
}

message CreateOrderResponse {
    string order_id = 1;
    OrderStatus status = 2;
}

message Order {
    string order_id = 1;
    string user_id = 2;
    repeated OrderItem items = 3;
    OrderStatus status = 4;
}

message OrderItem {
    string product_id = 1;
    int32 quantity = 2;
    double price = 3;
}

message ListOrdersRequest {
    string user_id = 1;
    int32 page_size = 2;
    string page_token = 3;
}

message BatchSubmitResponse {
    int32 success_count = 1;
    int32 fail_count = 2;
    repeated string failed_order_ids = 3;
}

message OrderStatusRequest {
    string order_id = 1;
}

message OrderStatusEvent {
    string order_id = 1;
    OrderStatus new_status = 2;
    string timestamp = 3;
}

enum OrderStatus {
    ORDER_STATUS_UNSPECIFIED = 0;
    ORDER_STATUS_PENDING = 1;
    ORDER_STATUS_CONFIRMED = 2;
    ORDER_STATUS_SHIPPING = 3;
    ORDER_STATUS_DELIVERED = 4;
    ORDER_STATUS_CANCELLED = 5;
}
领域设计提示:每个 service 应映射一个限界上下文中的聚合根或领域服务。例如 OrderService 对应订单上下文,PaymentService 对应支付上下文。避免将多个上下文的 RPC 方法混入同一个 service 定义中。

4.2 四种 RPC 模式详解

gRPC 基于 HTTP/2 的流式能力,提供了四种通信模式。下表对比了它们的核心特征与适用场景:

模式 关键字 请求 响应 典型领域场景
一元 RPC rpc Method(Request) returns (Response) 单个消息 单个消息 创建订单、查询用户信息、支付确认
服务端流式 RPC rpc Method(Request) returns (stream Response) 单个消息 消息流 订单列表分页、日志导出、数据同步
客户端流式 RPC rpc Method(stream Request) returns (Response) 消息流 单个消息 批量上传、文件分片上传、传感器数据聚合
双向流式 RPC rpc Method(stream Request) returns (stream Response) 消息流 消息流 实时聊天、订单状态推送、协同编辑

4.3 四种 RPC 模式的通信流程

下图展示了四种 RPC 模式在客户端与服务端之间的消息交互时序:

四种 gRPC RPC 模式通信流程 四种 gRPC RPC 模式通信流程 一元 RPC 客户端 → 服务端 单个请求 单个响应 请求-响应 1:1 如:CreateOrder 服务端流式 RPC 客户端 → 服务端 单个请求 请求-响应 1:N 如:ListOrders 客户端流式 RPC 客户端 → 服务端 多个请求 请求-响应 N:1 如:BatchSubmitOrders 双向流式 RPC 客户端 ↔ 服务端 请求-响应 N:M 如:TrackOrderStatus 模式选择与领域设计原则 • 一元 RPC:适用于命令和查询操作,请求与响应均为单条消息,语义清晰。 • 服务端流式 RPC:适用于数据集合或事件流的推送,减少客户端轮询。 • 客户端流式 RPC:适用于批量提交或数据聚合场景,降低网络往返次数。 • 双向流式 RPC:适用于实时双向通信,如聊天、协同、状态跟踪。 领域设计原则:优先使用一元 RPC 保持简单;当数据量或实时性要求超出单次消息承载时, 再升级为流式模式。流式模式会增加客户端和服务端的复杂度,需权衡收益。

4.4 一元 RPC:最基础的领域操作

一元 RPC 是最简单的模式,客户端发送一个请求消息,服务端返回一个响应消息。它天然适合领域驱动设计中的命令(Command)和查询(Query)操作。

  • 命令操作:如 CreateOrder、CancelOrder,请求包含执行命令所需的参数,响应返回执行结果。
  • 查询操作:如 GetOrder、GetUserProfile,请求包含查询条件,响应返回查询结果。
Go 服务端实现示例:
func (s *orderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
    // 领域逻辑:创建订单聚合根
    order, err := s.domainService.CreateOrder(ctx, req.UserId, req.Items)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "创建订单失败: %v", err)
    }
    return &pb.CreateOrderResponse{
        OrderId: order.ID,
        Status:  pb.OrderStatus_ORDER_STATUS_PENDING,
    }, nil
}

4.5 服务端流式 RPC:推送数据集合

当服务端需要返回大量数据或持续推送事件时,服务端流式 RPC 是理想选择。客户端发送一个请求,服务端通过流多次发送响应消息。

  • 分页查询:如 ListOrders,服务端逐页返回订单数据,客户端按需消费。
  • 事件推送:如 SubscribeOrderEvents,服务端持续推送订单状态变更事件。
领域设计建议:服务端流式 RPC 适合读模型的批量返回。在 DDD 中,这通常对应查询模型(Query Model)或领域事件(Domain Event)的订阅。注意流式响应应支持取消和超时控制。

4.6 客户端流式 RPC:批量提交与聚合

客户端流式 RPC 允许客户端发送多条消息,服务端在接收完所有消息后返回一个聚合响应。这适用于批量操作场景。

  • 批量上传:如 BatchSubmitOrders,客户端逐条发送订单,服务端批量处理并返回统计结果。
  • 数据聚合:如 CollectSensorData,客户端持续发送传感器数据,服务端聚合后返回分析结果。
注意:客户端流式 RPC 要求服务端在接收完所有消息前不能发送响应。如果处理时间较长,建议在响应中包含进度信息,或考虑使用双向流式 RPC 实现更灵活的交互。

4.7 双向流式 RPC:实时双向通信

双向流式 RPC 允许客户端和服务端独立地发送和接收消息流。双方可以按任意顺序读写,实现真正的全双工通信。

  • 实时状态跟踪:如 TrackOrderStatus,客户端发送订单 ID 流,服务端返回状态变更事件流。
  • 聊天系统:客户端和服务端互相发送消息,实现实时对话。
  • 协同编辑:多个客户端通过服务端同步编辑操作。
Go 双向流服务端实现示例:
func (s *orderServer) TrackOrderStatus(stream pb.OrderService_TrackOrderStatusServer) error {
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        // 模拟状态变更事件推送
        for _, event := range s.domainService.GetStatusEvents(req.OrderId) {
            if err := stream.Send(event); err != nil {
                return err
            }
        }
    }
}

4.8 模式选择决策树

在领域驱动设计中,选择正确的 RPC 模式直接影响契约边界的清晰度和系统性能。以下决策树可辅助选择:

gRPC RPC 模式选择决策树 gRPC RPC 模式选择决策树 选择 RPC 模式 单次请求-响应? 需要流式通信? 一元 RPC 谁发送流? 服务端流式 RPC 双向流式 RPC 客户端流式 RPC 适用于 客户端发送流、服务端单次响应

4.9 流式 RPC 的 Go 实现要点

在 Go 中实现流式 RPC 时,需要注意以下关键点:

模式 服务端接口 客户端调用 关键方法
服务端流式 ServerStreamingServer ClientStreamingClient Send() / Recv()
客户端流式 ClientStreamingServer ClientStreamingClient Recv() / SendAndClose()
双向流式 BidiStreamingServer BidiStreamingClient Recv() / Send()
流式 RPC 避坑指南:
  • 服务端流式 RPC 中,Send() 可能阻塞,需考虑背压(backpressure)控制。
  • 客户端流式 RPC 中,客户端必须调用 CloseAndRecv() 告知服务端流结束。
  • 双向流式 RPC 中,Recv() 和 Send() 可以在不同 goroutine 中并发执行,但需注意同步。
  • 所有流式 RPC 都应处理 context.Context 的超时和取消信号。

4.10 领域驱动设计中的模式映射

将四种 RPC 模式映射到 DDD 战术模式,可以更清晰地表达领域意图:

  • 一元 RPC → 领域服务方法:对应聚合根上的命令方法或查询方法,如 Order.Create()。
  • 服务端流式 RPC → 领域事件订阅:对应领域事件的发布-订阅模式,服务端作为事件源推送事件。
  • 客户端流式 RPC → 批量领域命令:对应批量处理多个领域命令,如批量创建订单。
  • 双向流式 RPC → 领域事件流:对应实时事件流处理,客户端订阅特定事件,服务端持续推送。
总结:选择 RPC 模式时,应从领域交互的语义出发,而非技术偏好。一元 RPC 是最简单的契约,应作为默认选择;流式 RPC 仅在数据量、实时性或批量处理需求明确时引入。清晰的契约边界是领域驱动设计与 gRPC 结合的核心价值。