导读
在这篇文章中,你能学会 RPC、 GRPC 是什么,protobuf 是什么,怎么定义,以及如何在 Android开发中使用。
gRPC 是什么
RPC是什么
RPC (Remote Procedure Call,远程过程调用) 是一种计算机通信协议。它的核心目标是:让调用远程服务器上的函数,看起来像调用本地函数一样简单。
如果没有 RPC,你需要自己处理网络连接、序列化数据(把对象转成字节流)、发送请求、等待响应等极其繁琐的步骤。而 RPC 框架 把这些底层的复杂逻辑封装好了,让开发者只需关注业务代码。
kotlin
// ----使用传统的方式请求接口----
val params: Params = Params("admin", "1234")
// 1. 需要知道请求地址,发送的参数需要转成 json 字符串
val res = await request("/login", toJson(params))
// 2. 解析返回的数据,又要做一次反序列化
println(fromJson<Data>(res.body()))
// ----使用 RPC 的方式请求接口----
// 直接调用生成的方法,获取结果
val params: Params = Params("admin", "1234")
val data: Data = await LoginStub.Login(params)
println(data)
这里可以看出来几个优点:
- 不用记接口的地址,直接调用本地函数
- 不用每次请求都重复写序列化和反序列代码
- 不用定义 Params 和 Data(定义完 proto 后会自动生成)
简单来讲,简化了接口调用。
相比 REST 的优势
既然 REST (HTTP/JSON) 已经统一了互联网,为什么在大厂(字节、阿里、腾讯、Google)的内部,微服务之间却要"多此一举"去搞一套 RPC 框架。
其实,REST 是为了"通用性"设计的,而 RPC 是为了"极致性能"设计的。
在大规模分布式系统中,微服务之间的调用频率可能达到每秒 千万级。这时,REST 的一些特性就变成了"负担"。
性能:二进制 vs 文本
- REST (JSON) :JSON 是文本格式。如果你传一个数字
12345,JSON 会把它当成 5 个字符(5 个字节)传输。传输前要序列化,接收后要解析字符串。 - RPC (Protobuf) :使用二进制编码。同样的数字
12345在二进制中可能只占 2 个字节。- 结果 :RPC 的数据包更小,解析速度快 5-10 倍,极大地节省了 CPU 和带宽。
效率:HTTP/2 vs HTTP/1.1
虽然现在的 REST 也可以跑在 HTTP/2 上,但大多数传统的 REST API 仍停留在 HTTP/1.1。
- REST (1.1) :每次请求通常都要经历"建立连接 -> 发送 -> 关闭"或者复杂的长连接管理。头部信息(Header)重复传输,浪费严重。
- RPC (gRPC/HTTP2) :原生支持多路复用。在一个 TCP 连接上可以同时发出一千个调用,互不干扰。此外,HTTP/2 还会压缩 Header,效率提升巨大。
开发体验:强类型契约 vs 弱类型文档
这是前后端分离开发最感同身受的一点:
- REST:后端写完接口,得写 Swagger 文档,前端/其他后端再照着文档手动写代码。如果后端改了字段名没通知,程序运行时就会直接报错(报错还不一定好找)。
- RPC :代码即文档 。
- 你定义了一个
.proto文件,执行编译。 - 客户端和服务端自动生成对应的类和方法。
- 如果你改了字段名,对方的代码在编译阶段就会报错。这种"编译期发现错误"的能力在大型工程中价值连城。
- 你定义了一个
💡形象的比喻
- REST 就像是邮寄信件: 格式是标准化的(信封、邮编),全世界谁都能寄,谁都能收,但你得手动拆信、读信。
- RPC 就像是三体人: 两个大脑直接通过电信号同步信息,意念相同。速度极快,不需要语言转化,但前提是两个大脑必须接入同一套协议(三体人基因)。
传输成本:不仅仅是数据
在一个复杂的微服务链路中(比如 A 调 B,B 调 C,C 调 D),如果每一层都用 JSON 序列化和反序列化,累积的延迟(Latency)会非常恐怖。
gRPC是什么
gRPC (gRPC Remote Procedure Call) 是一个由 Google 开发的高性能、开源的通用 RPC 框架。
gRPC 的核心特点
- 高效的数据传输 :不同于 REST 使用文本格式(JSON/XML),gRPC 默认使用 Protocol Buffers (protobuf) 。这是一种二进制序列化格式,体积更小、速度更快。
- 基于 HTTP/2:利用 HTTP/2 的多路复用、头部压缩和双向流特性,极大地提升了通信效率 。
- 跨语言支持:你可以用 Python 写客户端,用 Go 或 Java 写服务端,它们之间可以无缝通信 。
- 强类型契约 :在编写代码前,需要先定义
.proto文件,明确服务接口和消息结构,这减少了前后端对接时的沟通成本。
Protocol Buffers
Protocol Buffers (简称 Protobuf, 文件后缀 .proto)是 gRPC 的灵魂。它是 Google 开发的一种语言无关、平台无关、可扩展的序列化结构数据的方法。
Protobuf 是二进制格式 ,直接映射到内存地址,解析速度比 JSON 快 5 到 10 倍。如果把 REST 比作发送"散装的快递",那么 Protobuf 就是将物品"真空压缩"后的"标准集装箱"。
定义
一个简单的消息类型(message)定义,里面有 3 个字段:
Protobuf
syntax = "proto3"; // 使用 proto3 语法
message Player {
int32 id = 1; // 这里的 1, 2, 3 是"字段编号",不是值
string name = 2;
bool is_online = 3;
}
编译完成后大概会生成这样的代码:
kotlin
// 1. 消息类(生成的实体)
class Player : GeneratedMessageLite<...> {
val id: Int // 对应 int32
val name: String // 对应 string
val isOnline: Boolean // 对应 bool (自动转为驼峰命名)
// 还有一系列判断字段是否存在、序列化、反序列化的方法
}
// 2. 伴生对象或扩展方法提供的 DSL
inline fun player(block: PlayerKt.Dsl.() -> Unit): Player { ... }
注意:编译完成不会是简单的 data class,因为 gRPC 还要做一系列的序列化反序列化以及兼容性的处理。
字段类型
在前面的例子Player消息中,所有字段都是标量类型:1 个整数(id)、 1 个字符串(name)和 1 个布尔类型(is_online)。还可以定义枚举类型和使用其他消息作为类型。
字段编号
每个字段必须要有一个编号,编号满足下列要求:
- 给定的编号必须 在该消息的所有字段中是唯一的。
- 字段编号
19000到19999为 Protocol Buffers 实现保留。如果你在消息中使用这些保留的字段编号,protocol buffer 编译器会报错。 - 不能使用任何先前保留(
reserved) 的字段编号,保留字段编号会在删除字段的时候用到。
重点 :字段序号(Field Tags)的唯一性约束仅限于同一个 message 内部 。不同的 message(无论是否在同一个 .proto 文件中)之间,序号是互不干扰的。
1 . 同一文件,不同 Message(序号可重复)
Protobuf
message User {
int32 id = 1; // 这里的 1 没问题
string name = 2;
}
message Order {
int32 id = 1; // 这里的 1 也是合法的,完全独立
double price = 2;
}
- 不同文件(序号可重复)
这是最常见的场景。比如你在 User.proto 里定义了序号 1,在 Product.proto 里也定义了序号1,它们在编译后生成的二进制数据中,会通过 Message 类型 来区分,而不仅仅靠号。
- 嵌套 Message(序号可重复)
Protobuf
message SearchResponse {
message Result {
string url = 1; // 嵌套内的 1
string title = 2;
}
repeated Result results = 1; // 外层的 1,与内部的 1 不冲突
int32 total_pages = 2;
}
repeated 指的是可重复字段,即是个列表,repeated 字段永远不会是 null。如果没有传值,你拿到的是一个空的 List (emptyList())
⚠️ 真正不能重复的地方
Protobuf
message BadUser {
int32 id = 1;
string email = 1; // ❌ 错误!编译不会通过
}
💡 进阶小知识:序号范围的秘密
虽然你可以随便用序号,但 Protobuf 的序号其实是有"成本"的:
- 1 到 15 :占用 1 个字节(包含序号和字段类型)。
- 16 到 2047 :占用 2 个字节。
建议: 把 1 到 15 这些"黄金序号"留给那些最频繁出现、数据量最大的字段,这样可以进一步压榨性能,减小包体积。
删除字段
如果你要删除一个字段,首先从客户端代码中删除所有对该字段的引用,然后从消息中删除改字段定义。
但是,你必须保留已删除的字段的编号,因为你不保留该字段编号,其他开发人员可能会重复使用编号,导致程序出错。
因为你无法保证客户端和服务器同时更新,所以不能直接删除。
Protobuf
message Foo{
string url = 1;
string title = 2; // 如果现在要删除title 字段
}
// ❌ 直接删除 title 字段和它的字段编号
message Foo{
string url = 1;
int code = 2; // 可能后续会被其他开发用在其他字段上,导致数据错误
}
// ✅ 删除字段,但是保留编号,避免重复使用
message Foo{
reserved 2; // 保留编号
string url = 1;
}
保留字段
相当于禁用,后续不能使用,可以在一行里面写禁用多个字段编号:
Protobuf
message Foo {
reserved 2, 15, 9 to 11;
reserved "title"; // 也可以禁用字段名称
}
这里9 to 11 相当于写 9, 10, 11 。
💡 核心:在 Protobuf 中,字段名只是给开发者看的"外号",字段编号才是数据在网络中穿梭的"唯一身份证"。身份证一旦注销,终身不得重新启用。
如果不遵守规则会发生什么?
- 数据错乱 :如果旧版 App 还在发
email (编号2),你把编号2改成了phone,新版服务器会把用户的 Email 当成电话号码存进数据库。 - 解析失败 :如果类型从
string改成了int32,Protobuf 可能会解析出一堆乱码,导致业务逻辑出现无法预知的else分支。
服务
在 gRPC 的世界里,如果说 message(消息)定义的是数据的"长相" ,那么 service (服务)定义的就是数据的"动向"。
简单来说,service 定义的是一套远程调用的接口契约(Interface Contract) 。它规定了服务端可以提供哪些功能,以及客户端可以如何调用这些功能。
1. 核心定义:它是"动作"的集合
在 .proto 文件中,service 块包含了若干个 rpc 方法。每一个 rpc 方法都像是一个跨越网络的函数声明。
Protobuf
message OrderRequest {...}
message OrderResponse {...}
service OrderService {
// 1. 定义方法名:CreateOrder
// 2. 定义输入:OrderRequest (必须是 message 类型)
// 3. 定义输出:OrderResponse (必须是 message 类型)
rpc CreateOrder (OrderRequest) returns (OrderResponse);
}
2. Service 定义的四种通信模式
这是 service 最强大的地方。它不仅能定义"一问一答",还能定义"流式通讯":
| 模式 | 定义方式 | 描述 | 场景举例 |
|---|---|---|---|
| 简单 RPC (Unary) | rpc Method(Req) returns (Res); |
客户端发一个请求,服务端回一个响应。 | 登录、查询余额 |
| 服务端流 (Server Streaming) | rpc Method(Req) returns (stream Res); |
客户端发一个请求,服务端连续返回多个响应。 | 股票行情、进度条更新 |
| 客户端流 (Client Streaming) | rpc stream Method(Req) returns (Res); |
客户端连续发多个请求,服务端最后回一个响应。 | 上传大文件、物联网轨迹上报 |
| 双向流 (Bi-directional) | rpc stream Method(Req) returns (stream Res); |
双方同时发送和接收数据,互不等待。 | 实时聊天、多人协作编辑 |
实践-在 Android 中使用 gRPC
1. 配置 build.gradle
Groovy
apply plugin: 'com.google.protobuf'
dependencies {
// gRPC核心组件
api "io.grpc:grpc-okhttp:1.78.0" // gRPC的OkHttp传输实现,负责HTTP/2通信
api "io.grpc:grpc-protobuf-lite:1.78.0" // Protocol Buffers轻量级序列化支持
api "io.grpc:grpc-stub:1.78.0" // gRPC客户端和服务端存根生成器
api "io.grpc:grpc-kotlin-stub:1.5.0" // Kotlin语言的gRPC存根支持
// 序列化支持
implementation "com.google.protobuf:protobuf-kotlin-lite:4.33.5" // Protocol Buffers的Kotlin轻量级支持
}
// Protobuf编译器配置
protobuf {
// 基础设置
protoc {
// 指定编译器版本
artifact = "com.google.protobuf:protoc:3.25.1"
}
// 定义了三个代码生成插件:
plugins {
// java插件:生成Java代码
java {
artifact = 'io.grpc:protoc-gen-grpc-java:1.63.0'
}
// grpc插件:生成gRPC Java服务代码
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.63.0'
}
// grpckt插件:生成gRPC Kotlin代码
grpckt {
artifact = 'io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar'
}
}
// 代码生成任务配置
generateProtoTasks {
all().each { task ->
task.builtins {
// 生成轻量级Java代码
java {
option 'lite'
}
// 生成轻量级Kotlin代码
kotlin {
option 'lite'
}
}
task.plugins {
// 生成轻量级gRPC代码
grpc {
option 'lite'
}
// 生成轻量级gRPC Kotlin代码
grpckt {
option 'lite'
}
}
}
}
}
主要功能:
- proto文件编译 :将Protocol Buffers定义文件(
.proto)编译成各种语言的代码 - gRPC服务生成:自动生成gRPC客户端和服务端代码
- 多语言支持 :同时支持
Java和Kotlin代码生成 - 轻量级优化 :使用
'lite'选项减少生成代码的体积 - 自动化集成 :与
Gradle构建系统无缝集成
完成后要执行 Sync Gradle。
2. 编写 proto 文件
这里在指定目录的文件夹下创建 proto文件,如果没有 proto 文件夹,先建一个文件夹,不然编译的时候会找不到 proto 文件。
bash
MyAndroidApp/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/ # 你的 Kotlin/Java 代码
│ │ │ ├── proto/ # 👈 必须放在这里!请手动创建这个文件夹(如果不存在的话)。
│ │ │ │ ├── auth.proto
│ │ │ │ └── player.proto
│ │ │ └── AndroidManifest.xml
│ └── build.gradle.kts
如果想修改默认扫码的 proto 文件夹路径,可以去 build.gradle 里面设置,但是一般没必要改。
新建一个 auth.proto 文件,内容如下:
Protobuf
syntax = "proto3";
package auth;
option java_package = "com.demo.android_client.proto";
option java_multiple_files = true;
// 1. 定义登录请求
message LoginRequest {
string username = 1;
string password = 2;
}
// 2. 定义登录响应
message LoginResponse {
bool success = 1;
string token = 2; // 登录成功后的令牌
string message = 3; // 错误信息或提示
}
// 3. 定义服务
service AuthService {
// 简单 RPC:发送一个请求,得到一个响应
rpc Login (LoginRequest) returns (LoginResponse);
}
这里有一些新字段,补充一下 proto 配置的说明:
package:包名,用于解决不同proto文件里面有相同message或service出现冲突的问题,必须写。option java_package:用于配置包名,默认用包名,最好设置一下,符合 Android 的规范。option java_multiple_files:默认值false,表示要不要将message编译成独立的文件,建议设为 true,避免定义的消息多了,打包一个很大的类出来。
编写完成后要点击编译按钮进行编译,编译完成后就会看到以下文件:

3. 修改 proto 文件
如果修改了 proto 文件,一定要重新编译,最好先 clean build 一下,不然会有缓存。
4. 编写测试代码
新建一个 GrpcViewModel.kt 文件,用于连接 gRPC,和实现登录方法:
kotlin
package com.demo.android_client
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.demo.android_client.proto.AuthServiceGrpcKt
import com.demo.android_client.proto.loginRequest
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class GrpcViewModel : ViewModel() {
// 如果是 Android 模拟器访问电脑本机,通常用 10.0.2.2,端口号在服务端配置
val channel: ManagedChannel = ManagedChannelBuilder.forAddress("10.0.2.2", 50051)
.usePlaintext() // 开发环境通常关闭 TLS 加密
.build()
// 创建支持协程的 Stub
val stub = AuthServiceGrpcKt.AuthServiceCoroutineStub(channel)
private val _toastMessage = MutableStateFlow<String?>(null)
val toastMessage = _toastMessage.asStateFlow()
fun doLogin() {
// 在协程作用域中启动
viewModelScope.launch {
try {
// 构造请求对象
val request = loginRequest {
username = "admin"
password = "123"
}
// 发起异步请求
val response = stub.login(request)
// 处理结果
if (response.success) {
_toastMessage.value = "登录成功,Token: ${response.token}"
println("登录成功,Token: ${response.token}")
} else {
_toastMessage.value = "登录失败: ${response.message}"
println("登录失败: ${response.message}")
}
} catch (e: Exception) {
_toastMessage.value = "网络错误: ${e.message}"
println("网络错误: ${e.message}")
}
}
}
}
这里需要注意的是:message 编译完成生成 kotlin 代码(loginRequest),要用 DSL 语法进行初始化,而不是传统的 new 一个类的写法。当然,你也可以用 build 的方式进行赋值:
kotlin
// 传统写法
val request = LoginRequest.newBuilder()
.setUsername("admin")
.setPassword("123")
.build()
// 优雅写法
val request = loginRequest {
username = "admin"
password = "123"
}
再创建一个 Activity 用于触发登录逻辑:
kotlin
package com.demo.android_client
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: GrpcViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
// 初始化 ViewModel
viewModel = ViewModelProvider(this)[GrpcViewModel::class.java]
// 设置按钮点击事件
findViewById<Button>(R.id.button_login).setOnClickListener {
viewModel.doLogin()
Toast.makeText(this, "正在尝试登录...", Toast.LENGTH_SHORT).show()
}
// 观察 Toast 消息
lifecycleScope.launch {
viewModel.toastMessage.collect { message ->
message?.let {
Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show()
}
}
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="登录"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_text" />
<TextView
android:id="@+id/main_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="gRPC Login Demo"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/button_login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
5. 验证
在模拟器运行,点击登录:

看到 toast,证明成功获取到服务端的返回🎉。这就是一个 gRPC 在 Android 端应用最简单的 demo。
详细代码见文末链接。
完整 demo
内容包含一个 Android 写的客户端和 go 写的服务端,使用 gRPC 进行通讯。