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}")
}
⚠️ 注意:双向流中,两端必须同时消费和发送。如果一端只发不收,会导致背压问题。我曾经在生产环境遇到过,客户端只发不收,服务端内存暴涨,最后 OOM 了。解决方案是确保两端都开启协程并发处理。

核心知识体系

下面这张图总结了 gRPC 在 Kotlin 中的完整链路:

gRPC-Kotlin 核心架构 Protocol Buffers 定义 .proto 文件 → 服务接口 + 消息结构 代码生成 protoc 编译器 → Kotlin Stub + gRPC 基类 四种通信模式 一元 RPC | 服务端流 | 客户端流 | 双向流 HTTP/2 传输 二进制帧 | 多路复用 | 头部压缩 | 流控

避坑总结

  • 字段编号不可变:发布后修改编号会导致旧版本客户端解析错误。
  • 流式通信必须消费:不消费流会导致背压,内存飙升。
  • 连接复用:gRPC 基于 HTTP/2,一个连接可以处理多个请求,不要每次调用都新建连接。
  • 错误处理:gRPC 使用 Status 对象传递错误码,不要用异常代替业务逻辑。

gRPC 在 Kotlin 生态中越来越成熟。如果你正在构建微服务架构,尤其是需要高性能、强类型、流式通信的场景,它绝对值得一试。从 protobuf 定义到 Kotlin 代码生成,再到四种通信模式,这套工具链已经非常完善了。

📌 一句话总结:gRPC + Kotlin = 强类型 + 高性能 + 流式通信,是微服务间通信的「最佳拍档」。

公众号:蓝海资料掘金营,微信deep3321