grpc学习

rpc

remote procedure cal远程过程调用

rpc对应的是本地过程调用,函数调用就是常见的本地过程调用

远程调用会面临很多问题:

  1. cal的id映射
  2. 序列化和反序列化(编码解码协议)
  3. 网络传输(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
    布尔值,只有两个可能值:truefalse

字符串和字节类型

  • string
    用于存储 UTF-8 编码的文本。字符串在内部以长度前缀的方式存储。
  • bytes
    用于存储任意的二进制数据。与字符串不同,bytes 不会进行编码转换,适合存储图片、文件或其他非文本数据。

2. 复合类型(Composite Types)

复合类型用于构造更复杂的数据结构,允许多个标量或其他复合类型的组合。

消息类型(Message)

  • message
    是最常用的复合类型,用来定义一个结构化的数据对象。消息可以包含任意数量的字段,每个字段都有一个唯一的数字标识符。
    例如:

    1
    2
    3
    4
    5
    6
    message Person {
    string name = 1;
    int32 id = 2;
    string email = 3;
    }

    消息还可以嵌套其他消息,形成复杂的数据层次结构。

枚举类型(Enum)

  • enum
    用于定义一组具名的常量。枚举类型使数据表达更为直观,通常用来表示状态或选项。
    例如:
    1
    2
    3
    4
    5
    6
    enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
    }

Oneof 字段

  • oneof
    用于在消息中定义一组互斥的字段,即在同一时间内只能设置其中的一个字段。
    例如:

    1
    2
    3
    4
    5
    6
    7
    message Contact {
    oneof contact_info {
    string email = 1;
    int64 phone = 2;
    }
    }

    使用 oneof 可以节省内存,并确保数据的一致性。


3. 扩展与容器类型

Repeated 字段

  • repeated
    表示一个字段可以出现任意次,类似于数组或列表。
    例如:
    1
    2
    3
    4
    message Person {
    repeated string phone_numbers = 4;
    }

Map 字段

  • map
    用于定义键值对映射,其中键必须是标量类型(不能是浮点数),值可以是任意类型(标量或消息)。
    例如:
    1
    2
    3
    4
    message Dictionary {
    map<string, string> translations = 1;
    }


4. 其他注意事项

  • 默认值与零值
    在 proto3 中,所有字段都有默认的“零值”。例如,数字类型默认值为 0,布尔类型为 false,字符串为空字符串等。proto2 中可以定义 optionalrequired 字段,但 proto3 则只支持 optional(最新版本中也重新支持 optional 标记)。
  • Well-Known Types
    Protocol Buffers 提供了一些预定义的消息类型(例如 TimestampDurationAny 等),这些类型通常用于跨语言数据交换,并已在各语言中有相应的支持库。
  • 向后兼容性
    Protobuf 设计中非常注重向后兼容性,字段的数字标识符一旦定义后就不应随意更改。删除字段建议保留其标识符,但将其视为“废弃”。此外,新增字段不会破坏旧代码的解码行为,因为未识别的字段会被忽略。

参考资料

  • citedevelopers-google-com-protobuf-docs
  • citegrpc.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

func main() {
// 创建一个 metadata 对象
md := metadata.New(map[string]string{
"authorization": "Bearer token_value",
})
// 将 metadata 附加到上下文中
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 发起 RPC 调用时使用该上下文
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
// 假设 client 为生成的 gRPC 客户端
// client.SomeRPCMethod(ctx, request)
}

参考资料citegrpc.io-docs-what-is-grpc


2. 拦截器(Interceptors)

定义与作用:

  • 拦截器类似于 HTTP 中的中间件,可以在 RPC 方法调用之前或之后对请求和响应进行统一处理。
  • 主要用于日志记录、认证授权、请求验证、错误处理和指标统计等功能的统一实现。

分类:

  • 客户端拦截器(Client Interceptors): 在客户端调用前拦截请求或在收到响应后拦截处理,可以对请求进行预处理或添加统一的 header 信息。
  • 服务器拦截器(Server Interceptors): 在服务器端拦截进来的请求,可以在执行实际的 RPC 方法前后做一些处理,如统一认证、监控、限流等。

使用方法:

  • gRPC 提供了拦截器的接口,你可以编写符合接口的函数,然后在创建 gRPC 服务器或客户端时传入拦截器列表。
  • 例如,在 Go 语言中,使用 grpc.UnaryInterceptor() 添加单个拦截器,或者使用 grpc.ChainUnaryInterceptor() 组合多个拦截器。

示例代码(服务器端拦截器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)

func loggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
log.Printf("RPC 方法:%s, 请求:%v", info.FullMethod, req)
resp, err := handler(ctx, req)
if err != nil {
log.Printf("RPC 方法:%s, 错误:%v", info.FullMethod, err)
}
return resp, err
}

func main() {
s := grpc.NewServer(
grpc.UnaryInterceptor(loggingInterceptor),
)
// 注册服务并启动服务器...
}

参考资料citegrpc.io-docs-guides-overview


3. 验证器

概念说明:

  • gRPC 本身不内置消息数据验证功能,但通常在实际应用中需要对传入数据进行校验,比如检查字段是否为空、数据格式是否正确等。
  • 常见做法有两种:
    1. 自定义拦截器: 编写拦截器,在调用实际业务逻辑前对请求数据进行校验。
    2. 使用代码生成工具: 例如 protoc-gen-validate(PGV),在 proto 文件中通过注释或特定语法定义校验规则,生成代码后在服务实现中调用自动生成的验证函数。

示例(使用 protoc-gen-validate): 在 proto 文件中定义校验规则:

1
2
3
4
5
6
7
8
9
10
11
syntax = "proto3";

package example;

import "validate/validate.proto";

message CreateUserRequest {
string username = 1 [(validate.rules).string = {min_len: 3, max_len: 20}];
string email = 2 [(validate.rules).string = {email: true}];
}

生成代码后,在业务逻辑中调用验证器:

1
2
3
4
5
6
7
8
func (s *server) CreateUser(ctx context.Context, req *example.CreateUserRequest) (*example.UserResponse, error) {
// 调用自动生成的 Validate 方法
if err := req.Validate(); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
// 处理业务逻辑...
}

这种方式可以将校验规则与数据结构定义放在一起,提升代码的可维护性和一致性。

参考资料citedevelopers-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): 请求未认证或认证失败。

这些状态码在错误处理和重试策略中起到关键作用,客户端可以根据状态码采取不同的恢复策略。

参考资料citegrpc.github.io-grpc/core-md-statuscodes


5. 错误处理

基本思路:

  • 在 gRPC 中,错误通常通过状态码(codes)和错误信息(message)一起传递。
  • 使用 google.golang.org/grpc/status 包构造和解析错误,允许在错误中嵌入详细信息(例如通过 error details 扩展)。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func someRPCMethod(ctx context.Context, req *Request) (*Response, error) {
// 检查参数有效性
if req == nil {
return nil, status.Error(codes.InvalidArgument, "请求参数不能为空")
}
// 业务逻辑处理出错时,也可以返回相应的错误码
if err := process(req); err != nil {
return nil, status.Errorf(codes.Internal, "处理请求时发生错误: %v", err)
}
return &Response{}, nil
}

  • 通过 status.Error()status.Errorf() 构造错误,可以确保客户端收到的错误包含统一的结构化信息,便于后续处理和日志记录。

参考资料citegrpc.io-docs-what-is-grpc


6. 超时机制

概念与实现:

  • 超时机制通常通过设置 RPC 调用的 deadline(截止时间)来实现,保证客户端不会因为服务器响应延迟而长时间阻塞。
  • 在 Go 语言中,通常使用 context.Context 来传递超时信息,调用 context.WithTimeout()context.WithDeadline() 创建带有截止时间的上下文,然后传递给 RPC 调用。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import (
"context"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

func main() {
// 设置 5 秒超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
// 处理连接错误
}
client := NewYourServiceClient(conn)
// 发起 RPC 调用时传入带超时的 ctx
resp, err := client.SomeMethod(ctx, &SomeRequest{})
if err != nil {
// 如果超时,会返回 DeadlineExceeded 状态码
}
// 处理响应...
}

  • 如果 RPC 调用在截止时间前没有完成,gRPC 会自动取消调用,并返回 DeadlineExceeded 错误。

参考资料citegrpc.io-docs-architecture


7. 验证器(Validator,重复说明)

由于在业务中对请求数据的校验是必不可少的,我们前面已经提到两种常见方案:

  1. 自定义拦截器校验:
    在拦截器中统一对传入请求数据进行检查,不符合要求则直接返回错误,避免进入业务逻辑。
  2. 使用 protoc-gen-validate:
    利用 proto 文件中定义的规则生成验证代码,调用生成的 Validate() 方法检查数据合法性。

通过这种方式,可以确保进入业务逻辑的数据已经过预先校验,减少因数据问题导致的运行时错误和安全隐患。


总结

  • Metadata 为 gRPC 调用提供了传递额外上下文数据的能力,便于在不同服务间传递认证、日志和追踪信息。
  • 拦截器 是 gRPC 中实现横切关注点(如日志、认证、监控)的重要机制,支持客户端和服务器端的统一处理。
  • 验证器 虽然不是 gRPC 内置的功能,但常结合拦截器或代码生成工具(如 protoc-gen-validate)实现消息数据的校验。
  • 状态码 为 RPC 调用提供了标准化的错误描述,每个状态码对应特定的错误场景。
  • 错误处理 依赖于状态码与详细错误信息的传递,通过 status 包构造统一格式的错误返回。
  • 超时机制 通过上下文传递 deadline 控制调用时限,确保调用不会因服务端响应延迟而无限等待。

这些机制共同构成了 gRPC 的强大功能,使得其在高并发、分布式和跨语言的场景下表现出色。

参考资料citegrpc.io-docs-what-is-grpc、citegrpc.io-docs-guides-overview、citegrpc.github.io-grpc/core-md-statuscodes

grpc生成的代码内容

通过 gRPC 编译器(protoc)及其插件生成的代码主要分为两大部分:消息类型代码和服务代码,每一部分都有各自的功能和结构。下面详细讲解这些代码内容。


1. 消息类型代码

这部分代码主要由 protoc-gen-go 插件生成,用于表示和操作你在 .proto 文件中定义的消息。

  • 结构体定义
    每个在 .proto 文件中定义的 message 会生成一个对应的 Go 结构体,其中每个字段都映射到结构体中的一个成员。
    例如:

    1
    2
    3
    4
    5
    message Person {
    string name = 1;
    int32 age = 2;
    }

    生成的代码会包含:

    1
    2
    3
    4
    5
    6
    type 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 结构体
    • 序列化、反序列化、重置、字符串输出及反射支持等辅助方法
  • 服务代码
    • 服务器端:服务接口定义、未实现的默认实现(UnimplementedXXXServer)、以及服务注册函数
    • 客户端:客户端接口与具体实现、RPC 方法调用封装、以及对流式调用的支持

这种生成模式将数据结构与服务接口分离,既保证了代码的高效性,又方便了开发者在多语言环境下进行调用和扩展,同时也确保了向后兼容性和服务的灵活部署。

参考资料citegrpc.io-docs-what-is-grpc、citedevelopers-google-com-protobuf-docs

二、知识小结

知识点 核心内容 考试重点/易混淆点 难度系数
单体应用到微服务演变 单体应用结构介绍(以电商系统为例) 单体应用与微服务结构的区别 ★★★
- 网站与后台管理系统作为两个独立项目 - 单体应用:单一数据库,功能增多和并发增加导致问题
- 功能包括:登录注册、商品查询、下单(网站);商品添加、订单管理、用户管理(后台) - 微服务:解决功能增多、并发增加、代码复用、系统间调用等问题
微服务解决的问题 - 代码复用:如商品查询功能在多个服务中复用 - 微服务与单体应用的界限 ★★★★
- 系统间调用:如秒杀、下单查询商品信息 - 微服务并非万能,需根据实际需求选择
- 接口对内对外提供服务:如商品查询接口同时供浏览器和小程序使用
- 数据库性能问题:如数据分析功能对数据库的全数据查询影响
微服务带来的问题 - 数据库被多个服务依赖,无法拆分和升级 - 数据库依赖导致的升级困难、数据不一致等问题 ★★★★★
- 某个接口性能问题影响整体数据库性能 - 接口间的强依赖关系导致的测试、部署困难
- 开发测试部署困难:如修改代码需进行大量测试
下一步演进 如何在下一节课中进一步演进微服务架构 - 微服务架构的具体实现方式 ★★★★
- 微服务架构的优缺点分析
注:难度系数以五星为最高,表示该知识点或核心内容的难度和重要性。

grpc学习
http://example.com/2025/03/20/grpc学习/
作者
WoodQ
发布于
2025年3月20日
许可协议