grpc学习
rpc
remote procedure cal远程过程调用
rpc对应的是本地过程调用,函数调用就是常见的本地过程调用
远程调用会面临很多问题:
- cal的id映射
- 序列化和反序列化(编码解码协议)
- 网络传输(http2.0协议)
rpc架构要点
其实rpc就像mcp一样,自己写好工具函数,然后通过某种约定的协议,发起请求之后调用工具函数
protobuf
数据类型
Protocol Buffers(protobuf)是一种语言无关、平台无关、可扩展的序列化结构化数据的方法,其数据类型可以分为两大类:标量类型(Scalar Types)和复合类型(Composite Types),另外还有一些扩展类型。下面详细介绍这些数据类型。
1. 标量类型(Scalar Types)
标量类型是最基本的数据类型,直接对应于单一的值。
整数类型
- int32 / int64
有符号整数,采用变长编码(varint)存储。当数值较小时可以节省空间,但负数的编码可能稍微占用更多字节。 - uint32 / uint64
无符号整数,也采用 varint 编码,适用于只需要正数的场景。 - sint32 / sint64
有符号整数,采用 ZigZag 编码,可以更有效地编码负数,避免了普通 varint 在负数上的低效性。 - fixed32 / fixed64
固定长度的无符号整数,分别总是占用 4 字节和 8 字节,适用于数值范围固定且对性能要求较高的场景。 - sfixed32 / sfixed64
固定长度的有符号整数,同样分别占用 4 字节和 8 字节,用于保证数值总是以固定空间存储,适用于频繁解码操作的场景。
浮点类型
- float
32 位单精度浮点数。 - double
64 位双精度浮点数。
布尔类型
- bool
布尔值,只有两个可能值:true
或false
。
字符串和字节类型
- string
用于存储 UTF-8 编码的文本。字符串在内部以长度前缀的方式存储。 - bytes
用于存储任意的二进制数据。与字符串不同,bytes 不会进行编码转换,适合存储图片、文件或其他非文本数据。
2. 复合类型(Composite Types)
复合类型用于构造更复杂的数据结构,允许多个标量或其他复合类型的组合。
消息类型(Message)
message
是最常用的复合类型,用来定义一个结构化的数据对象。消息可以包含任意数量的字段,每个字段都有一个唯一的数字标识符。
例如:1
2
3
4
5
6message Person {
string name = 1;
int32 id = 2;
string email = 3;
}消息还可以嵌套其他消息,形成复杂的数据层次结构。
枚举类型(Enum)
- enum
用于定义一组具名的常量。枚举类型使数据表达更为直观,通常用来表示状态或选项。
例如:1
2
3
4
5
6enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
Oneof 字段
oneof
用于在消息中定义一组互斥的字段,即在同一时间内只能设置其中的一个字段。
例如:1
2
3
4
5
6
7message Contact {
oneof contact_info {
string email = 1;
int64 phone = 2;
}
}使用 oneof 可以节省内存,并确保数据的一致性。
3. 扩展与容器类型
Repeated 字段
- repeated
表示一个字段可以出现任意次,类似于数组或列表。
例如:1
2
3
4message Person {
repeated string phone_numbers = 4;
}
Map 字段
- map
用于定义键值对映射,其中键必须是标量类型(不能是浮点数),值可以是任意类型(标量或消息)。
例如:1
2
3
4message Dictionary {
map<string, string> translations = 1;
}
4. 其他注意事项
- 默认值与零值
在 proto3 中,所有字段都有默认的“零值”。例如,数字类型默认值为 0,布尔类型为 false,字符串为空字符串等。proto2 中可以定义optional
和required
字段,但 proto3 则只支持optional
(最新版本中也重新支持optional
标记)。 - Well-Known Types
Protocol Buffers 提供了一些预定义的消息类型(例如Timestamp
、Duration
、Any
等),这些类型通常用于跨语言数据交换,并已在各语言中有相应的支持库。 - 向后兼容性
Protobuf 设计中非常注重向后兼容性,字段的数字标识符一旦定义后就不应随意更改。删除字段建议保留其标识符,但将其视为“废弃”。此外,新增字段不会破坏旧代码的解码行为,因为未识别的字段会被忽略。
参考资料
- citedevelopers-google-com-protobuf-docs
- citegrpc.io-protobuf-types-docs
以上详细介绍了 Protocol Buffers 中的主要数据类型及其使用场景,希望对你理解和使用 protobuf 有所帮助。
proto文件同步的坑
对于服务端和客户端的proto来说,如果序号搞反了就会出问题
protobuf的编号类似于
16gopher,序号长度内容
但是如果序号搞反了,服务端解码就会出现错误
编号可以不完全都有,但是一定不能编号搞错
各种机制
下面详细讲解一下 gRPC 中关于 metadata、拦截器、验证器、状态码、错误处理和超时机制的各个机制与实现方式。
1. Metadata
定义与作用:
- Metadata 是一组键值对信息,用于在客户端和服务器之间传递额外的数据。它相当于 HTTP/2 的 header 信息,常用于传递认证令牌、调用链追踪 ID、版本信息等上下文数据。
- 在 gRPC 中,metadata 分为请求元数据和响应尾部元数据,支持在 RPC 调用的各个阶段附加信息。
使用方法:
- 客户端可以在发起 RPC 调用前,通过
metadata.New()
创建一份 metadata,然后利用grpc.NewOutgoingContext()
将其附加到上下文中传递给服务端。 - 服务端则可以通过
metadata.FromIncomingContext()
从上下文中提取出 metadata。 - 同时,还可以在服务端利用
grpc.SetHeader()
或grpc.SetTrailer()
向响应中添加 metadata。
示例代码(客户端添加 metadata):
1 |
|
参考资料citegrpc.io-docs-what-is-grpc
2. 拦截器(Interceptors)
定义与作用:
- 拦截器类似于 HTTP 中的中间件,可以在 RPC 方法调用之前或之后对请求和响应进行统一处理。
- 主要用于日志记录、认证授权、请求验证、错误处理和指标统计等功能的统一实现。
分类:
- 客户端拦截器(Client Interceptors): 在客户端调用前拦截请求或在收到响应后拦截处理,可以对请求进行预处理或添加统一的 header 信息。
- 服务器拦截器(Server Interceptors): 在服务器端拦截进来的请求,可以在执行实际的 RPC 方法前后做一些处理,如统一认证、监控、限流等。
使用方法:
- gRPC 提供了拦截器的接口,你可以编写符合接口的函数,然后在创建 gRPC 服务器或客户端时传入拦截器列表。
- 例如,在 Go 语言中,使用
grpc.UnaryInterceptor()
添加单个拦截器,或者使用grpc.ChainUnaryInterceptor()
组合多个拦截器。
示例代码(服务器端拦截器):
1 |
|
参考资料citegrpc.io-docs-guides-overview
3. 验证器
概念说明:
- gRPC 本身不内置消息数据验证功能,但通常在实际应用中需要对传入数据进行校验,比如检查字段是否为空、数据格式是否正确等。
- 常见做法有两种:
- 自定义拦截器: 编写拦截器,在调用实际业务逻辑前对请求数据进行校验。
- 使用代码生成工具: 例如 protoc-gen-validate(PGV),在 proto 文件中通过注释或特定语法定义校验规则,生成代码后在服务实现中调用自动生成的验证函数。
示例(使用 protoc-gen-validate): 在 proto 文件中定义校验规则:
1 |
|
生成代码后,在业务逻辑中调用验证器:
1 |
|
这种方式可以将校验规则与数据结构定义放在一起,提升代码的可维护性和一致性。
参考资料citedevelopers-google-com-protobuf-docs
4. 状态码(Status Codes)
概念与作用:
- gRPC 定义了一套标准的状态码,用于表示 RPC 调用结果的各种状态。这些状态码不仅可以描述成功与失败,还能表达失败的原因,便于客户端进行相应处理。
常用状态码:
- OK (0): 调用成功,没有错误。
- Canceled (1): 操作被取消,通常由客户端取消或超时导致。
- Unknown (2): 未知错误。
- InvalidArgument (3): 客户端传入参数无效。
- DeadlineExceeded (4): 超出截止时间。
- NotFound (5): 未找到资源。
- AlreadyExists (6): 资源已存在。
- PermissionDenied (7): 没有权限。
- ResourceExhausted (8): 资源耗尽(例如配额耗尽)。
- FailedPrecondition (9): 请求未满足执行前提条件。
- Aborted (10): 操作中止,通常用于并发冲突。
- OutOfRange (11): 数值超出范围。
- Unimplemented (12): 功能未实现。
- Internal (13): 内部错误。
- Unavailable (14): 服务暂时不可用。
- DataLoss (15): 不可恢复的数据丢失。
- Unauthenticated (16): 请求未认证或认证失败。
这些状态码在错误处理和重试策略中起到关键作用,客户端可以根据状态码采取不同的恢复策略。
参考资料citegrpc.github.io-grpc/core-md-statuscodes
5. 错误处理
基本思路:
- 在 gRPC 中,错误通常通过状态码(codes)和错误信息(message)一起传递。
- 使用
google.golang.org/grpc/status
包构造和解析错误,允许在错误中嵌入详细信息(例如通过 error details 扩展)。
示例代码:
1 |
|
- 通过
status.Error()
和status.Errorf()
构造错误,可以确保客户端收到的错误包含统一的结构化信息,便于后续处理和日志记录。
参考资料citegrpc.io-docs-what-is-grpc
6. 超时机制
概念与实现:
- 超时机制通常通过设置 RPC 调用的 deadline(截止时间)来实现,保证客户端不会因为服务器响应延迟而长时间阻塞。
- 在 Go 语言中,通常使用
context.Context
来传递超时信息,调用context.WithTimeout()
或context.WithDeadline()
创建带有截止时间的上下文,然后传递给 RPC 调用。
示例代码:
1 |
|
- 如果 RPC 调用在截止时间前没有完成,gRPC 会自动取消调用,并返回
DeadlineExceeded
错误。
参考资料citegrpc.io-docs-architecture
7. 验证器(Validator,重复说明)
由于在业务中对请求数据的校验是必不可少的,我们前面已经提到两种常见方案:
- 自定义拦截器校验:
在拦截器中统一对传入请求数据进行检查,不符合要求则直接返回错误,避免进入业务逻辑。 - 使用 protoc-gen-validate:
利用 proto 文件中定义的规则生成验证代码,调用生成的Validate()
方法检查数据合法性。
通过这种方式,可以确保进入业务逻辑的数据已经过预先校验,减少因数据问题导致的运行时错误和安全隐患。
总结
- Metadata 为 gRPC 调用提供了传递额外上下文数据的能力,便于在不同服务间传递认证、日志和追踪信息。
- 拦截器 是 gRPC 中实现横切关注点(如日志、认证、监控)的重要机制,支持客户端和服务器端的统一处理。
- 验证器 虽然不是 gRPC 内置的功能,但常结合拦截器或代码生成工具(如 protoc-gen-validate)实现消息数据的校验。
- 状态码 为 RPC 调用提供了标准化的错误描述,每个状态码对应特定的错误场景。
- 错误处理 依赖于状态码与详细错误信息的传递,通过
status
包构造统一格式的错误返回。 - 超时机制 通过上下文传递 deadline 控制调用时限,确保调用不会因服务端响应延迟而无限等待。
这些机制共同构成了 gRPC 的强大功能,使得其在高并发、分布式和跨语言的场景下表现出色。
参考资料citegrpc.io-docs-what-is-grpc、citegrpc.io-docs-guides-overview、citegrpc.github.io-grpc/core-md-statuscodes
grpc生成的代码内容
通过 gRPC 编译器(protoc)及其插件生成的代码主要分为两大部分:消息类型代码和服务代码,每一部分都有各自的功能和结构。下面详细讲解这些代码内容。
1. 消息类型代码
这部分代码主要由 protoc-gen-go 插件生成,用于表示和操作你在 .proto 文件中定义的消息。
结构体定义
每个在 .proto 文件中定义的message
会生成一个对应的 Go 结构体,其中每个字段都映射到结构体中的一个成员。
例如:1
2
3
4
5message Person {
string name = 1;
int32 age = 2;
}生成的代码会包含:
1
2
3
4
5
6type Person struct {
Name string
Age int32
// 内部用于反射和缓存的数据...
}序列化与反序列化方法
自动生成的方法(如Marshal()
,Unmarshal()
,Reset()
,String()
等)用于支持二进制序列化、反序列化和调试输出。反射支持
生成的代码实现了 Protobuf 的反射接口(如ProtoReflect()
),方便在运行时对消息结构进行动态处理或诊断。辅助方法
包括获取消息描述符、校验消息合法性(如果有手动集成验证工具)等方法,便于与其他工具链协同工作。
2. 服务代码
这部分代码主要由 protoc-gen-go-grpc 插件生成,专门用于 gRPC 服务相关的功能,包含客户端和服务器两部分的代码。
服务器端代码
- 服务接口
对应 .proto 中定义的每个service
,生成一个服务器端接口(例如GreeterServer
),接口中包含所有 RPC 方法的签名。
其中还会包含一个额外方法(如mustEmbedUnimplementedGreeterServer()
),目的是迫使开发者在实现接口时嵌入默认的未实现结构体,以确保未来添加新方法时能够向后兼容。 - 未实现的服务结构体
生成一个UnimplementedGreeterServer
结构体,该结构体默认实现了所有服务接口方法(一般返回“未实现”的错误)。
你的实际服务实现可以将它嵌入到自己的结构体中,这样当未来增加新方法时,编译器不会因为缺少实现而报错。 - 注册函数
自动生成RegisterGreeterServer(s *grpc.Server, srv GreeterServer)
函数,用于将你实现的服务注册到 gRPC 服务器实例中。这个函数会将你的服务和 gRPC 底层的路由机制绑定在一起。
客户端代码
- 客户端接口和结构体
为每个 service 生成一个客户端接口(如GreeterClient
),以及一个具体实现该接口的客户端结构体。
这些代码封装了底层的 gRPC 调用细节,每个 RPC 方法在客户端中对应一个函数调用,通过传入上下文(context)、请求数据等参数,调用远程服务并返回响应或错误。 - 调用方法的实现
客户端代码实现了与服务器进行通信的逻辑,内部利用 HTTP/2 连接发送请求和接收响应,并进行序列化与反序列化处理。 - 流式调用支持
如果服务定义中有流式 RPC(无论是客户端、服务器还是双向流),生成的代码还会包括相应的流接口,帮助你在客户端和服务器之间实现数据流的发送和接收。
3. 总结
通过 gRPC 生成的代码内容主要包括:
- 消息类型代码:
- 对应 .proto 中每个
message
生成 Go 结构体 - 序列化、反序列化、重置、字符串输出及反射支持等辅助方法
- 对应 .proto 中每个
- 服务代码:
- 服务器端:服务接口定义、未实现的默认实现(UnimplementedXXXServer)、以及服务注册函数
- 客户端:客户端接口与具体实现、RPC 方法调用封装、以及对流式调用的支持
这种生成模式将数据结构与服务接口分离,既保证了代码的高效性,又方便了开发者在多语言环境下进行调用和扩展,同时也确保了向后兼容性和服务的灵活部署。
参考资料citegrpc.io-docs-what-is-grpc、citedevelopers-google-com-protobuf-docs
二、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
单体应用到微服务演变 | 单体应用结构介绍(以电商系统为例) | 单体应用与微服务结构的区别 | ★★★ |
- 网站与后台管理系统作为两个独立项目 | - 单体应用:单一数据库,功能增多和并发增加导致问题 | ||
- 功能包括:登录注册、商品查询、下单(网站);商品添加、订单管理、用户管理(后台) | - 微服务:解决功能增多、并发增加、代码复用、系统间调用等问题 | ||
微服务解决的问题 | - 代码复用:如商品查询功能在多个服务中复用 | - 微服务与单体应用的界限 | ★★★★ |
- 系统间调用:如秒杀、下单查询商品信息 | - 微服务并非万能,需根据实际需求选择 | ||
- 接口对内对外提供服务:如商品查询接口同时供浏览器和小程序使用 | |||
- 数据库性能问题:如数据分析功能对数据库的全数据查询影响 | |||
微服务带来的问题 | - 数据库被多个服务依赖,无法拆分和升级 | - 数据库依赖导致的升级困难、数据不一致等问题 | ★★★★★ |
- 某个接口性能问题影响整体数据库性能 | - 接口间的强依赖关系导致的测试、部署困难 | ||
- 开发测试部署困难:如修改代码需进行大量测试 | |||
下一步演进 | 如何在下一节课中进一步演进微服务架构 | - 微服务架构的具体实现方式 | ★★★★ |
- 微服务架构的优缺点分析 | |||
注:难度系数以五星为最高,表示该知识点或核心内容的难度和重要性。 |