写在前面
最近看了不少开源项目的源码,发现到处都是 gRPC:SkyWalking 用它上报数据,Nacos 2.0 用它做服务注册,Dubbo 也支持 gRPC 协议。
既然这么多项目在用,我就花了点时间把 gRPC 这个东西搞明白。结果发现,之前对 gRPC 的理解有不少误区。
这篇文章就是把我搞明白的东西写下来,顺便帮自己理清思路。
我之前的几个误解
误解1:gRPC 是 Go 专用的
我一开始真这么想,毕竟名字里有个 "g"。而且看了几个 Go 写的微服务项目,发现它们服务间通信基本都用 gRPC,就更坚定了我的想法。
后来才知道这个 g 是 Google,不是 Go。gRPC 确实跟 Go 很搭(都是 Google 出品,生态契合度高),但不是 Go 专用的。
gRPC 支持一大堆语言:Java、Python、C++、Go、C#、Node.js、PHP、Ruby... 基本上主流语言都支持。Java 生态里 Dubbo、Spring Cloud 也都支持 gRPC。这篇文章用的就是 Java。
误解2:gRPC 直接基于 TCP
我以为 gRPC 是直接在 TCP 上搞的,后来发现不对。gRPC 的层次是这样的:
应用代码 → gRPC → HTTP/2 → TCP
中间有层 HTTP/2。为啥不直接用 TCP?后来我想明白了:
- HTTP/2 已经帮你搞定了流控制、多路复用、头部压缩
- 防火墙和负载均衡器都认识 HTTP,不会被拦
- TLS 加密也是现成的
- 标准协议,生态成熟
误解3:用 gRPC 要自己写 Netty
这个误解最离谱。我看到 gRPC 底层用 Netty,就以为要自己写 Netty 代码。
实际上 gRPC 官方库已经把 Netty 封装好了:
Server server = ServerBuilder.forPort(50051)
.addService(new GreeterServiceImpl())
.build()
.start();
就这么简单。底层的网络 I/O、连接池、线程管理全帮你搞定了。你完全不用碰 Netty。
误解4:Spring Boot 集成 gRPC 性能很差
我之前担心 Spring Boot 框架重,集成 gRPC 会有性能问题。
后来测试发现,确实有损耗,但也就 5-15%。而实际项目里,你的瓶颈往往是数据库、Redis、外部接口,不是框架本身。
除非你 QPS 上几万,否则 Spring Boot 集成完全够用。
gRPC 是个啥
说白了,gRPC 就是让不同机器上的程序互相调用,像调本地方法一样。比 REST API 快,比自己写 Socket 省事。
核心就三点:
- 用 HTTP/2 传输(快、支持流)
- 用 Protobuf 序列化(比 JSON 省空间)
- 自动生成代码(写个 .proto 文件,代码自动生成)
实战:写个 gRPC 服务
别光说不练,咱们写个真实的项目。
第一步:Maven 配置
这是重点,很多人卡在这一步。
<properties>
<!-- Java 17,别用 8 了,新特性真香 -->
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<grpc.version>1.60.0</grpc.version>
<protobuf.version>3.25.1</protobuf.version>
</properties>
<dependencies>
<!-- gRPC 核心三件套 -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
</dependencies>
<build>
<!-- 这个扩展很重要,不加会报错 -->
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<!-- 这个插件自动生成代码 -->
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<!-- protoc 编译器 -->
<protocArtifact>
com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
</protocArtifact>
<!-- gRPC 插件 -->
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
重点说明:
grpc-netty-shaded是推荐版本,避免 Netty 版本冲突os-maven-plugin必须加,不然 protoc 下载会出错protobuf-maven-plugin会自动生成代码到target/generated-sources/
关键:生成的代码不用 copy 到 src/main/java
很多人会纠结要不要把生成的代码复制到 src 目录。答案是:不用!
Maven 会自动识别 target/generated-sources/ 目录,IDE 也会自动识别。而且生成的代码不应该提交到 Git,每次 mvn compile 都会重新生成。
第二步:定义协议
在 src/main/proto/greeter.proto 写协议定义:
syntax = "proto3";
option java_package = "com.example.grpc";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1; // 数字是字段编号,不能改
int32 age = 2;
}
message HelloReply {
string message = 1;
}
注意:
- 字段编号一旦定了就别改,影响向后兼容
- 1-15 的编号 Tag 只占 1 字节,常用字段用这些编号
- 16-2047 的编号 Tag 占 2 字节
第三步:生成代码
mvn clean compile
这一步会生成三个关键文件:
HelloRequest.java- 请求对象HelloReply.java- 响应对象GreeterGrpc.java- 服务类和客户端 Stub
你会在 target/generated-sources/protobuf/java/ 和 target/generated-sources/protobuf/grpc-java/ 看到它们。
IDEA 会把这些目录标记成蓝色(Generated Sources Root),可以直接用。
第四步:写服务端
// 服务实现
public class GreeterServiceImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest request,
StreamObserver<HelloReply> responseObserver) {
String message = "Hello " + request.getName() +
", you are " + request.getAge() + " years old";
HelloReply reply = HelloReply.newBuilder()
.setMessage(message)
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
// 启动服务器
public class GreeterServer {
public static void main(String[] args) throws Exception {
Server server = ServerBuilder.forPort(50051)
.addService(new GreeterServiceImpl())
.build()
.start();
System.out.println("服务器启动,监听 50051");
// 优雅关闭
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.shutdown();
}));
server.awaitTermination();
}
}
就这么简单。不需要写任何 Netty 代码,不需要处理网络连接,不需要管理线程池。
第五步:写客户端
public class GreeterClient {
public static void main(String[] args) {
// 创建连接
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 50051)
.usePlaintext() // 不用 TLS,测试环境
.build();
try {
// 创建客户端
GreeterBlockingStub stub = GreeterGrpc.newBlockingStub(channel);
// 发请求
HelloRequest request = HelloRequest.newBuilder()
.setName("张三")
.setAge(25)
.build();
HelloReply response = stub.sayHello(request);
System.out.println("响应: " + response.getMessage());
} finally {
channel.shutdown();
}
}
}
运行
# 终端1:启动服务端
mvn exec:java -Dexec.mainClass="com.example.grpc.server.GreeterServer"
# 终端2:运行客户端
mvn exec:java -Dexec.mainClass="com.example.grpc.client.GreeterClient"
gRPC 消息是怎么传的
很多人好奇 gRPC 底层到底传了啥。简单说就是:5 字节头 + Protobuf 数据。
完整消息:
00 00 00 00 09 0A 05 41 6C 69 63 65 10 19
│ │
│ └─ Protobuf 数据(9字节)
└─ 5字节头部
5 字节头部:
- 第 1 字节:压缩标志(0x00 = 不压缩)
- 第 2-5 字节:消息长度(大端序)
Protobuf 数据:
0A 05 41 6C 69 63 65 10 19
解释:
0A = Tag (字段1,类型是字符串)
05 = 长度 5
41 6C 69 63 65 = "Alice" 的 UTF-8
10 = Tag (字段2,类型是整数)
19 = 值 25
Protobuf 用的是 Tag-Length-Value 编码:
- Tag 包含字段编号和类型
- Length 对于字符串类型才有
- Value 就是实际值
比 JSON 省空间多了:
JSON: {"name":"Alice","age":25} // 26 字节
Protobuf: 0A 05 41 6C 69 63 65 10 19 // 9 字节
拦截器:看清楚每个字节
我写了个拦截器,可以打印出每个请求的详细字节分析。这对理解 gRPC 很有帮助。
public class LoggingServerInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
System.out.println("收到请求: " + call.getMethodDescriptor().getFullMethodName());
// 这里可以拦截请求和响应
// 打印字节、记录日志、做权限校验等
return next.startCall(call, headers);
}
}
加到服务器:
Server server = ServerBuilder.forPort(50051)
.addService(new GreeterServiceImpl())
.intercept(new LoggingServerInterceptor()) // 加这一行
.build()
.start();
我项目里的拦截器会打印详细的字节分析,运行后输出是这样的:
========== 收到 gRPC 请求 ==========
方法: greeter.Greeter/SayHello
请求消息:
name: "Alice"
age: 25
Protobuf 字节数: 9
十六进制: 0A 05 41 6C 69 63 65 10 19
详细解析:
[0] 0x0A = Tag (字段1,字符串)
[1] 0x05 = 长度 5
[2-6] "Alice"
[7] 0x10 = Tag (字段2,整数)
[8] 0x19 = 值 25
这对调试和学习 gRPC 协议非常有用。
纯 gRPC vs Spring Boot
纯 gRPC(我这个项目的方式)
代码:
Server server = ServerBuilder.forPort(50051)
.addService(new GreeterServiceImpl())
.build()
.start();
优点:
- 完全掌控,想怎么配置就怎么配置
- 轻量,启动快(毫秒级)
- 性能最好
- 适合学习和理解原理
缺点:
- 要自己管理服务器生命周期
- 没有依赖注入
- 不能用 Spring 那一套(事务、缓存、安全等)
适合:
- 学习 gRPC
- 独立的微服务
- 性能要求极高的场景
- 不需要 Spring 生态
Spring Boot 集成
依赖:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>
代码:
@GrpcService // 一个注解搞定
public class GreeterServiceImpl extends GreeterGrpc.GreeterImplBase {
@Autowired
private UserService userService; // 依赖注入
@Override
public void sayHello(HelloRequest request,
StreamObserver<HelloReply> responseObserver) {
// 可以用 Spring 的所有东西
}
}
配置(application.yml):
grpc:
server:
port: 9090
优点:
- 自动配置,省事
- 依赖注入,代码简洁
- 可以用 Spring 全家桶
- 开发效率高
缺点:
- 启动慢(Spring 容器初始化)
- 内存占用多
- 性能有 5-15% 的损耗
适合:
- 企业级应用
- 团队熟悉 Spring
- 需要 Spring 生态
- 对性能要求不是特别极致
怎么选
QPS < 10K,团队用 Spring → Spring Boot 集成
QPS > 50K,性能敏感 → 纯 gRPC
学习和研究 → 纯 gRPC
说实话,大部分项目用 Spring Boot 集成完全够用。那点性能损耗真的不是瓶颈。
实际使用场景
SkyWalking:Agent 上报数据
SkyWalking 的 Agent 用 gRPC 向 OAP 服务器上报追踪数据。为啥用 gRPC?
- Agent 要高频上报(每秒几千次)
- gRPC 二进制协议省带宽
- 支持流式上报
- 对应用性能影响小
Nacos 2.0:服务注册
Nacos 2.0 把 HTTP 轮询改成了 gRPC 长连接。好处:
- 配置变更实时推送(不用轮询了)
- 心跳用双向流(省连接)
- 减少服务器压力
微服务内部调用
订单服务调用库存服务、支付服务,用 gRPC:
- 类型安全(.proto 定义接口)
- 比 REST 快
- 自动生成客户端代码
- 支持流式(批量操作)
常见问题
Q:如何调试 gRPC?
方法1:用拦截器(我项目里就是这么干的)
方法2:用 grpcurl
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"name":"张三","age":25}' \
localhost:50051 greeter.Greeter/SayHello
方法3:用 Postman(新版本支持 gRPC)
Q:生产环境怎么办?
- 开启 TLS
- 加认证(Metadata 里传 Token)
- 限流(拦截器里做)
- 监控(集成 Prometheus)
Q:能用于对外 API 吗?
不太推荐。gRPC 更适合内部微服务。对外 API 还是用 REST,理由:
- REST 文档友好(Swagger)
- 浏览器直接支持
- 调试方便
- 生态成熟
如果非要用,可以加个网关,外面 REST,里面 gRPC。
Q:性能到底怎么样?
跟 REST 比,gRPC 一般快 3-5 倍。但说实话,你的瓶颈往往不在这:
- 数据库查询:10-50ms
- Redis 读取:1-5ms
- 外部接口:50-500ms
- gRPC vs REST:差 1-2ms
所以别过度优化。除非你 QPS 真的很高。
总结
gRPC 其实没那么复杂:
- 不是 Go 专用的,Java 也能用
- 不用自己写 Netty,官方库帮你封装好了
- Maven 自动生成代码,不用手动 copy
- 用 HTTP/2 + Protobuf,比 REST 快
- Spring Boot 集成有损耗,但大部分场景够用
我的建议:
- 先用纯 gRPC 学习,理解原理
- 生产环境看情况,能用 Spring Boot 就用
- 别过度优化,先跑起来再说