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 怎么选
没有银弹,按场景取舍:
| 维度 | gRPC | REST / JSON | GraphQL |
|---|---|---|---|
| 传输协议 | HTTP/2 | HTTP/1.1 | HTTP/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 做向后兼容校验,避免上下游在不知不觉中断裂。




