80、gRPC:gRPC-Kotlin、Protocol Buffers、服务定义、流式通信
微服务之间怎么高效通信?这是每个后端工程师都绕不开的问题。RESTful API 虽然普及,但遇到高频调用、强类型约束、双向流场景时,就显得力不从心了。这时候,gRPC 就站了出来。
我个人习惯把 gRPC 看作「带类型检查的远程调用」。它不像 REST 那样靠 JSON 文本拼凑,而是用 Protocol Buffers 定义接口,生成强类型的客户端和服务端代码。说白了,你写 Kotlin 代码就像调用本地函数一样,背后却是跨网络的远程调用。
Protocol Buffers:接口的「契约」
gRPC 的核心是 Protocol Buffers(简称 protobuf)。它是一种语言无关的序列化格式,比 JSON 更小、更快、更严格。
你想想看,JSON 里字段名是字符串,每次传输都要带上,浪费带宽。protobuf 用数字编号代替字段名,体积直接缩小 3-10 倍。我在项目中遇到过,把一个 2MB 的 JSON 接口改成 protobuf 后,响应体降到了 200KB 左右,效果立竿见影。
先看一个简单的 protobuf 定义文件:
syntax = "proto3";
package com.example.grpc;
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc UpdateUser (stream UpdateUserRequest) returns (UpdateUserResponse);
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message GetUserRequest {
int32 user_id = 1;
}
message GetUserResponse {
int32 user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message UpdateUserRequest {
int32 user_id = 1;
string name = 2;
}
message ChatMessage {
string user = 1;
string content = 2;
int64 timestamp = 3;
}
注意看,每个字段后面都有一个数字编号。这就是 protobuf 的压缩秘诀——传输时只传编号和值,不传字段名。
服务定义:四种通信模式
gRPC 支持四种通信模式,我按使用频率排个序:
| 模式 | 描述 | 适用场景 |
|---|---|---|
| 一元 RPC | 客户端发送一个请求,服务端返回一个响应 | 常规 CRUD、查询接口 |
| 服务端流式 | 客户端发一个请求,服务端返回多个响应 | 日志推送、数据订阅、大文件分片 |
| 客户端流式 | 客户端发多个请求,服务端返回一个响应 | 批量上传、数据聚合 |
| 双向流式 | 双方同时发送和接收多个消息 | 实时聊天、协同编辑、游戏同步 |
嗯,这里要注意:流式通信是 gRPC 的杀手锏。REST 要实现类似效果,要么靠 WebSocket,要么靠轮询,复杂度高得多。
Kotlin 集成:从 protobuf 到代码
在 Kotlin 项目中使用 gRPC,需要先安装 protobuf 编译器,然后通过 Gradle 插件自动生成代码。
build.gradle.kts 配置示例:
plugins {
id("com.google.protobuf") version "0.9.4"
kotlin("jvm")
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.1"
}
plugins {
create("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.60.0"
}
create("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1"
}
}
generateProtoTasks {
all().forEach {
it.plugins {
create("grpc")
create("grpckt")
}
}
}
}
dependencies {
implementation("io.grpc:grpc-netty-shaded:1.60.0")
implementation("io.grpc:grpc-protobuf:1.60.0")
implementation("io.grpc:grpc-stub:1.60.0")
implementation("io.grpc:grpc-kotlin-stub:1.4.1")
}
配置完成后,运行 Gradle 构建,protobuf 编译器会自动生成 UserServiceGrpcKt 类。你只需要继承它,实现业务逻辑即可。
服务端实现:一元 RPC 示例
先看最简单的一元 RPC。服务端代码:
class UserServiceImpl : UserServiceGrpcKt.UserServiceCoroutineImplBase() {
override suspend fun getUser(request: GetUserRequest): GetUserResponse {
// 模拟从数据库查询
val user = findUserById(request.userId)
return GetUserResponse.newBuilder()
.setUserId(user.id)
.setName(user.name)
.setEmail(user.email)
.setAge(user.age)
.build()
}
}
客户端调用:
suspend fun fetchUser(userId: Int): GetUserResponse {
val channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build()
val stub = UserServiceGrpcKt.UserServiceCoroutineStub(channel)
val request = GetUserRequest.newBuilder().setUserId(userId).build()
return stub.getUser(request)
}
你看,客户端调用 getUser 就像调用本地 suspend 函数一样。这就是 gRPC 的魅力——网络细节被完全隐藏了。
流式通信:服务端推送
服务端流式通信,适合「一次请求,多次响应」的场景。比如分页查询用户列表:
override fun listUsers(request: ListUsersRequest): Flow<User> = flow {
val users = queryUsersByPage(request.page, request.pageSize)
for (user in users) {
emit(user)
}
}
客户端接收流:
val userFlow = stub.listUsers(ListUsersRequest.newBuilder()
.setPage(1)
.setPageSize(10)
.build())
userFlow.collect { user ->
println("收到用户: ${user.name}")
}
这里用了 Kotlin 的 Flow,天然支持异步流处理。我个人觉得这是 Kotlin 和 gRPC 最搭的地方——协程 + Flow 让流式编程变得极其自然。
双向流:实时聊天
双向流是最复杂的模式,但也是最强大的。双方可以同时发送和接收消息,互不阻塞。
override fun chat(requests: Flow<ChatMessage>): Flow<ChatMessage> = flow {
requests.collect { message ->
println("收到来自 ${message.user} 的消息: ${message.content}")
val reply = ChatMessage.newBuilder()
.setUser("Server")
.setContent("已收到: ${message.content}")
.setTimestamp(System.currentTimeMillis())
.build()
emit(reply)
}
}
客户端:
val chatFlow = stub.chat(
flow {
emit(ChatMessage.newBuilder().setUser("Alice").setContent("你好").build())
emit(ChatMessage.newBuilder().setUser("Alice").setContent("在吗?").build())
}
)
chatFlow.collect { reply ->
println("服务端回复: ${reply.content}")
}
核心知识体系
下面这张图总结了 gRPC 在 Kotlin 中的完整链路:
避坑总结
- 字段编号不可变:发布后修改编号会导致旧版本客户端解析错误。
- 流式通信必须消费:不消费流会导致背压,内存飙升。
- 连接复用:gRPC 基于 HTTP/2,一个连接可以处理多个请求,不要每次调用都新建连接。
- 错误处理:gRPC 使用 Status 对象传递错误码,不要用异常代替业务逻辑。
gRPC 在 Kotlin 生态中越来越成熟。如果你正在构建微服务架构,尤其是需要高性能、强类型、流式通信的场景,它绝对值得一试。从 protobuf 定义到 Kotlin 代码生成,再到四种通信模式,这套工具链已经非常完善了。
公众号:蓝海资料掘金营,微信deep3321