gRPC 实战:从 Proto 定义到四种通信模式落地

gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 与 Protocol Buffers(Protobuf)。相比传统 REST/JSON,它在内部微服务之间提供了更小的包体、更强的类型约束和原生的流式能力。本文从 .proto 定义讲起,手把手落地一元、服务端流、客户端流与双向流四种通信模式。

一、为什么需要 gRPC

在微服务架构里,服务间调用如果全用 REST + JSON,会面临几个痛点:JSON 文本体积大、字段靠约定容易写错、缺少统一的接口契约、流式推送要自己叠 WebSocket/SSE。gRPC 用 Protobuf 定义接口与消息,由 protoc 一键生成多语言客户端/服务端桩代码,接口即契约,类型在编译期就被校验。

如果你的服务已经在做注册发现与熔断限流,gRPC 可以无缝接入同一套治理体系(参见《Spring Cloud 微服务治理:注册发现与熔断限流》);在 Istio 服务网格中,gRPC 流量还能被 mTLS 加密并做精细的流量切分(参见《Istio 服务网格入门》)。

二、核心概念:Proto 与代码生成

gRPC 的一切都从 .proto 文件开始。下面定义一个订单服务,刻意把四种通信模式都放进同一个 service,方便对照。

syntax = "proto3";

package demo.order;

option java_multiple_files = true;
option java_package = "com.fsdata.grpc.order";

// 订单服务:演示 gRPC 四种通信模式
service OrderService {
  // 1. 一元 RPC:一次请求一次响应(如下单)
  rpc PlaceOrder(OrderRequest) returns (OrderResponse);
  // 2. 服务端流式:请求一次,返回多条
  rpc ListOrdersByUser(UserRequest) returns (stream OrderResponse);
  // 3. 客户端流式:发多条,返回一次汇总
  rpc UploadOrders(stream OrderRequest) returns (SummaryResponse);
  // 4. 双向流式:双向持续推送
  rpc WatchOrders(stream OrderRequest) returns (stream OrderStatus);
}

message OrderRequest  { string order_id = 1; string user_id = 2; int64 amount = 3; }
message OrderResponse { string order_id = 1; bool success = 2; string message = 3; }
message UserRequest   { string user_id = 1; }
message SummaryResponse { int32 count = 1; int64 total_amount = 2; }
message OrderStatus  { string order_id = 1; string status = 2; }

定义好之后,用 protoc 配合对应语言的插件生成桩代码。生成的是强类型类,调用方拿到的就是 OrderRequest、OrderResponse 这样的对象,而不是自己解析 JSON。

# 生成 Java 桩代码(需先装 protoc-gen-grpc-java)
protoc --java_out=./gen \
       --grpc-java_out=./gen \
       -I ./proto \
       ./proto/order.proto

# 同样的 .proto 也能生成 Go / Python / TS,实现多语言互通
protoc --go_out=./gen --go-grpc_out=./gen -I ./proto ./proto/order.proto

三、四种通信模式逐个落地

3.1 一元 RPC(Unary)

最常用的”一问一答”,语义与一次 HTTP 请求一致。服务端继承生成的 ImplBase,在方法里用 StreamObserver 的 onNext 返回结果、onCompleted 结束。

public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {

    @Override
    public void placeOrder(OrderRequest req, StreamObserver<OrderResponse> resp) {
        boolean ok = req.getAmount() > 0;
        resp.onNext(OrderResponse.newBuilder()
                .setOrderId(req.getOrderId())
                .setSuccess(ok)
                .setMessage(ok ? "下单成功" : "金额非法")
                .build());
        resp.onCompleted();           // 必须调用,否则客户端一直阻塞
    }
}

客户端使用阻塞 Stub,调用方式与本地方法几乎一样:

ManagedChannel channel = Grpc.newChannelBuilder(
        "localhost:8080", InsecureChannelCredentials.create()).build();
OrderServiceGrpc.OrderServiceBlockingStub stub =
        OrderServiceGrpc.newBlockingStub(channel);

OrderResponse r = stub.placeOrder(OrderRequest.newBuilder()
        .setOrderId("O1001").setUserId("U1").setAmount(19900).build());
System.out.println(r.getMessage());

3.2 服务端流式(Server streaming)

客户端发一次请求,服务端用 onNext 多次推送、最后 onCompleted。适合”查历史订单””订阅日志流”这类场景。服务端实现只需循环推送:

@Override
public void listOrdersByUser(UserRequest req, StreamObserver<OrderResponse> resp) {
    orderRepo.findByUser(req.getUserId()).forEach(o ->
        resp.onNext(toResponse(o)));   // 逐条推送
    resp.onCompleted();
}

3.3 客户端流式(Client streaming)

反向:客户端持续发送多条,服务端收齐后返回一次汇总。适合”批量上报””大文件分片上传”。服务端在 onCompleted 时给出最终响应。

@Override
public StreamObserver<OrderRequest> uploadOrders(StreamObserver<SummaryResponse> resp) {
    List<OrderRequest> buf = new ArrayList<>();
    return new StreamObserver<>() {
        public void onNext(OrderRequest r) { buf.add(r); }
        public void onError(Throwable t) { t.printStackTrace(); }
        public void onCompleted() {
            long total = buf.stream().mapToLong(OrderRequest::getAmount).sum();
            resp.onNext(SummaryResponse.newBuilder()
                    .setCount(buf.size()).setTotalAmount(total).build());
            resp.onCompleted();
        }
    };
}

3.4 双向流式(Bidirectional)

双方各自独立地持续收发,是实时性最强的一种。典型用途是”实时订单状态推送”。客户端拿到请求流后随时 onNext,服务端边收边推状态。

OrderServiceGrpc.OrderServiceStub asyncStub = OrderServiceGrpc.newStub(channel);
StreamObserver<OrderRequest> reqStream = asyncStub.watchOrders(
    new StreamObserver<OrderStatus>() {
        public void onNext(OrderStatus s)  { System.out.println(s.getStatus()); }
        public void onError(Throwable t)    { t.printStackTrace(); }
        public void onCompleted()           { System.out.println("done"); }
    });
reqStream.onNext(OrderRequest.newBuilder().setOrderId("O1001").build());
reqStream.onCompleted();

四、与 Spring Boot 集成

实际项目里 gRPC 服务常和 Spring Boot 应用共存。可引入 net.devh:grpc-server-spring-boot-starter,把实现类标注为 @GrpcService 即可自动注册到指定端口。gRPC 一般走独立端口(如 9090),REST 接口仍走 8080,两者互不干扰。升级到 Spring Boot 3 的注意事项(Jakarta 命名空间、Security 6 变更)可参考《Spring Boot 3 升级踩坑实录》。

跨服务的事务一致性是另一类难题:下单成功后要同步扣减库存、发消息,可结合《Seata 分布式事务实战》的 TCC/AT 模式保证最终一致。若服务端用 Java 21 处理阻塞逻辑,务必了解《Java 21 虚拟线程》对吞吐的影响,避免把虚拟线程绑死在同步 I/O 上。

五、gRPC vs REST vs GraphQL 怎么选

没有银弹,按场景取舍:

维度gRPCREST / JSONGraphQL
传输协议HTTP/2HTTP/1.1HTTP/1.1 / 2
序列化Protobuf(二进制)JSON(文本)JSON
代码生成一等公民弱 / 手写中
流式原生 4 种模式需 SSE/WebSocket订阅
浏览器直连需 grpc-web 网关天然天然
典型场景内部微服务/高并发公开 API聚合查询 / BFF

经验法则:内部服务间、对性能与契约要求高选 gRPC;对外公开、浏览器直连多选 REST;前端要按需聚合、避免过度/不足拉取选 GraphQL。

六、常见坑与最佳实践

1. 忘记调用 onCompleted。 流式接口若遗漏 onCompleted,客户端会一直阻塞等待,表现为”调用卡死”。务必在成功/异常分支都收尾。

2. 包体过大触发 HTTP/2 限制。 默认单消息有大小上限(常见 4MB),大批量数据传输应改成分片流式,而非一次性塞一个大对象。

3. 浏览器不能直接调。 前端直连 gRPC 需走 grpc-web 并配 Envoy 这类代理转换。纯前端场景别强行上 gRPC,保留 REST/BFF 更省事。

4. 超时与重试要显式设置。 gRPC 自带 deadline 机制,调用方应传 withDeadlineAfter,并配合服务治理层的熔断,避免级联雪崩。

七、拦截器:统一鉴权与链路追踪

随着服务增多,把鉴权、限流、日志与链路追踪塞进每个方法既不优雅也难维护。gRPC 提供 ServerInterceptor / ClientInterceptor,可以在不侵入业务逻辑的前提下统一横切。下面这个服务端拦截器在每次调用前后打印方法名与耗时:

public class LogInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call, Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        long start = System.nanoTime();
        String method = call.getMethodDescriptor().getFullMethodName();
        ServerCall.Listener<ReqT> listener = next.startCall(call, headers);
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(listener) {
            @Override public void onComplete() {
                System.out.printf("%s 耗时 %d ms%n", method,
                        (System.nanoTime() - start) / 1_000_000);
                super.onComplete();
            }
        };
    }
}

注册时把它加到 Server 的拦截器链即可;同理可在一个拦截器里统一校验 Token、注入 traceId。结合 OpenTelemetry 还能把调用链直接接入分布式追踪体系,无需改业务代码。调试阶段则推荐 grpcurl(类似 curl 的命令行工具)直接发请求验证接口,省去临时写客户端:

# 列出服务下的所有方法
grpcurl -plaintext localhost:9090 list demo.order.OrderService
# 调用一元方法(无需编译客户端)
grpcurl -plaintext -d '{"order_id":"O1","user_id":"U1","amount":19900}' \
        localhost:9090 demo.order.OrderService/PlaceOrder

八、小结

gRPC 用”接口即契约 + 强类型代码生成 + HTTP/2 流式”三件套,解决了内部微服务调用的性能与一致性痛点。掌握一元、服务端流、客户端流、双向流四种模式,你就覆盖了绝大多数服务间通信需求。下一步可以把它接入服务网格做统一治理,或结合网关对外暴露为 REST,兼顾性能与易用。团队落地时建议先把 .proto 契约纳入版本库统一管理,并配合 CI 做向后兼容校验,避免上下游在不知不觉中断裂。

上一篇 Linux 性能调优实战:CPU、内存与 I/O 瓶颈定位
下一篇 微服务陷阱:分布式架构下 7 个最常见的坑与避坑清单
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]