云计算百科
云计算领域专业知识百科平台

grpcurl 实战指南:深入解析 bankdemo 示例 gRPC 服务器——双向流、Header 鉴权与内存持久化

  • CLI
  • 开发工具

【免费下载链接】grpcurl

Like cURL, but for gRPC: Command-line tool for interacting with gRPC servers

项目地址:
https://gitcode.com/gh_mirrors/gr/grpcurl

点击查看 免费下载

bankdemo 是 grpcurl 仓库内置的一个示例 gRPC 服务器,曾在 GopherCon 2018 上用于现场演示 grpcurl 的各项能力。本文以 bankdemo 官方说明 为骨架,结合其源码(main.go、bank.go、chat.go、db.go、auth.go)逐层拆解:你将掌握如何用 go run 一键启动它、如何利用 server reflection 让 grpcurl 免 proto 文件直接探索与调用其服务、如何通过 authorization 请求头模拟"客户/客服"两种身份、以及如何用 grpcurl 实操一元 RPC、服务端流和全双工双向流三种调用模式。

bankdemo 是什么:为演示 grpcurl 而生的 gRPC 服务器

按 README 的定位,bankdemo 是一个示例性 gRPC 服务器,专门用来演示 grpcurl。它演示了构建 gRPC 服务端时几个非常有意思的概念,其中最具代表性的是聊天功能——依赖全双工双向流(full-duplex bidirectional streams)。文档同时明确提醒:这段代码是为"有趣、具体"的演示而写的,不应被视为任何意义上的生产级代码,阅读时请关注其教学价值而非工程完备性。

它演示的核心特性可归纳为三条:

  • 纯内存数据模型:账户、交易、余额全部保存在内存中,不依赖外部数据库;
  • 周期性 + 优雅退出双重持久化:每几秒、以及在收到 SIGTERM/SIGINT 优雅关闭时,把状态写入 accounts.json,进程重启后可恢复;
  • 聊天会话追踪:除银行账户数据外,服务端还维护"聊天会话",用于演示客户与客服之间基于双向流实现的实时对话应用。

仓库内还有另一个更朴素的测试服务器 testserver(见 testserver/README.md),其 RPC 接口定义在 example.proto 中,用于日常单元测试;而文档建议"想要更有趣的测试服务器,请看 bankdemo"——因为它提供了更具体、更贴近真实业务的 RPC 接口,包括全双工双向流方法。因此 bankdemo 是仓库内学习 grpcurl 与 gRPC 服务端交互的最佳实操样本。

快速启动 bankdemo

bankdemo 没有提供独立的 shell 脚本,直接在仓库根目录用 Go 运行即可:

# 在 grpcurl 仓库根目录执行
go run ./internal/testing/cmd/bankdemo

启动时支持两个命令行参数,定义在 main.go:

参数默认值说明
-port 12345 bankdemo gRPC 服务监听的端口
-datafile accounts.json 银行账户数据保存/加载的文件路径与文件名

例如换端口并指定数据文件:

go run ./internal/testing/cmd/bankdemo -port 54321 -datafile ./mybank.json

启动流程(main.go)依次为:加载既有数据文件 → 在 127.0.0.1:<port> 上监听 TCP → 创建 gRPC 服务器并注册 Bank、Support 两个服务 → 启动后台定时保存协程 → 注册 server reflection → 等待并处理 SIGINT/SIGTERM。启动成功后会在日志中输出 server starting, listening on 127.0.0.1:12345 之类的信息。

值得注意的细节:

  • 默认只绑定回环地址 127.0.0.1,即仅本机可访问,适合本地演示;
  • 服务端代码通过 //go:generate protoc –go_out=. –go-grpc_out=. bank.proto support.proto(main.go)声明了 .proto 的代码生成方式,生成的桩代码见 bank.pb.go、bank_grpc.pb.go、support.pb.go、support_grpc.pb.go;
  • 最关键的一点:main 中显式调用了 reflection.Register(grpcSvr)(main.go)。这意味着 grpcurl 无需任何 .proto 源文件或 protoset 文件,仅靠 server reflection 就能列出服务、查看 schema 并动态调用所有方法。

用 grpcurl 探索 bankdemo 的 Schema

由于服务端开启了 reflection,最简单的探索方式就是直接连过去。注意 bankdemo 走的是明文 HTTP/2(无 TLS),因此所有 grpcurl 命令都需要 -plaintext 标志。

列出服务器暴露的所有服务:

grpcurl -plaintext 127.0.0.1:12345 list

预期输出包含 Bank 与 Support 两个服务(以及 reflection 相关的 grpc.reflection.v1alpha.ServerReflection 等)。

查看某个服务的全部方法:

grpcurl -plaintext 127.0.0.1:12345 list Bank
grpcurl -plaintext 127.0.0.1:12345 list Support

查看具体符号的定义(grpcurl 会以等价的 proto 源片段形式打印描述):

grpcurl -plaintext 127.0.0.1:12345 describe Bank
grpcurl -plaintext 127.0.0.1:12345 describe Bank.GetTransactions
grpcurl -plaintext 127.0.0.1:12345 describe Support.ChatCustomer
grpcurl -plaintext 127.0.0.1:12345 describe bankdemo.Account

这些能力来自 grpcurl 对 server reflection 的内置支持——README 中明确说明,不带额外标志时 grpcurl 默认优先使用 reflection 获取 RPC schema(相关标志定义见 cmd/grpcurl/grpcurl.go)。

Bank 服务:一元 RPC 与流式 RPC 的完整示例

Bank 服务定义在 bank.proto 中,接口模型是"为当前已认证用户操作银行账户",鉴权身份通过 authorization 请求头传递(后文详述)。它同时包含一元 RPC和服务端流式 RPC两种形态,非常适合对照学习:

方法请求响应形态语义
OpenAccount OpenAccountRequest Account 一元 以指定类型和初始存款开户
CloseAccount CloseAccountRequest google.protobuf.Empty 一元 关闭账户(余额必须为零)
GetAccounts google.protobuf.Empty GetAccountsResponse 一元 列出当前客户的全部账户
GetTransactions GetTransactionsRequest stream Transaction 服务端流 按条件流式返回交易记录
Deposit DepositRequest BalanceResponse 一元 存款并返回新余额
Withdraw WithdrawRequest BalanceResponse 一元 取款并返回新余额
Transfer TransferRequest TransferResponse 一元 账户间转账,支持本行/外部 ACH 账户

几个值得关注的 schema 设计:

  • 金额全部用"分"(cents)表示整数,如 initial_deposit_cents、amount_cents、balance_cents,避免浮点误差——服务端格式化函数 dollars()(db.go)把分值转成 $xx.xx 展示;
  • Account 内嵌了 Type 枚举:UNKNOWN、CHECKING、SAVING、MONEY_MARKET、LINE_OF_CREDIT、LOAN、EQUITIES;
  • DepositRequest 内嵌了入金来源枚举 Source:CASH、CHECK、ACH、WIRE;
  • TransferRequest 通过两个 oneof 表达"源/目的账户",既可以填本行账号(source_account_number / dest_account_number),也可以填外部 ACH 账户(ach_routing_number + ach_account_number 构成的 ExternalAccount 嵌套消息);
  • 时间类型使用标准 Well-Known Type google.protobuf.Timestamp(start、end、Transaction.date)。

服务端实现(bank.go)展示了 gRPC 服务端的典型错误处理模式,全部通过 status.Errorf 返回标准 gRPC 错误码:

  • Unauthenticated:请求头缺少或无效的客户身份;
  • InvalidArgument:开户初始存款非法(如 LINE_OF_CREDIT/LOAN/EQUITIES 必须为零初始存款、存款金额非正、取款金额非负、未知账户类型、未知入金来源、外部 ACH 账号/路由号为零);
  • NotFound:客户名下不存在指定账号;
  • FailedPrecondition:余额非零时关户、取款超余额(insufficient funds)。

用 grpcurl 调用 Bank 服务

首先用一个"客户身份"创建账户(-H 用于附加请求头,用法见 cmd/grpcurl/grpcurl.go):

grpcurl -plaintext -H 'authorization: token 1' \\
-d '{"initial_deposit_cents": 10000, "type": "CHECKING"}' \\
127.0.0.1:12345 Bank.OpenAccount

注意:grpcurl 的所有 -H、-d 等参数必须放在服务器地址与方法名之前。响应类似:

{
"accountNumber": "1",
"type": "CHECKING",
"balanceCents": 10000
}

再存一笔、查一下账户列表:

grpcurl -plaintext -H 'authorization: token 1' \\
-d '{"account_number": 1, "amount_cents": 5000, "source": "CASH", "desc": "coffee money"}' \\
127.0.0.1:12345 Bank.Deposit

grpcurl -plaintext -H 'authorization: token 1' \\
-d '{}' 127.0.0.1:12345 Bank.GetAccounts

GetAccounts 的请求类型是 google.protobuf.Empty,JSON 中传 {} 即可。若不带 authorization 头调用,服务端会返回 Unauthenticated 错误,这正是下一节要讲的鉴权模型。

基于 Header 的鉴权模型:如何区分客户与客服

bankdemo 的鉴权是一个刻意简化的教学示例,实现在 auth.go 中:它把 gRPC 元数据里的 authorization 请求头当作身份凭据,格式为:

authorization: token <credential>

解析规则(auth.go):

  • 从 gRPC incoming context 中取 authorization 元数据,必须恰好一个值;
  • 将值按空格拆成两段,前半段必须是小写的 token;
  • 后半段即凭据内容,代码"把 token 直接当作 ID 使用"。

身份的"客户/客服"区分靠前缀约定(auth.go):

  • getCustomer(ctx):凭据不以 agent 开头 → 视为客户 ID(如 token 1、token 42);
  • getAgent(ctx):凭据必须以 agent 开头 → 视为客服 ID(如 token agent1、token agent2);
  • 前缀不符的会被相应接口拒绝,例如用 agent 开头的 token 调银行接口,会得到 Unauthenticated。

因此用 grpcurl 模拟不同身份非常简单:

# 以"客户 1"身份调用银行接口
grpcurl -plaintext -H 'authorization: token 1' … Bank.OpenAccount

# 以"客服 agent1"身份接入客服聊天
grpcurl -plaintext -H 'authorization: token agent1' … Support.ChatAgent

从 grpcurl 侧看,-H 附加的请求头在反射查询和 RPC 调用中都会携带(-H 的说明见 cmd/grpcurl/grpcurl.go),所以带鉴权头调用时反射与真正的方法调用都能通过服务端校验。

服务端流实战:GetTransactions 的流式返回

Bank.GetTransactions 是典型的**服务端流(server-streaming)**方法:客户端发一个请求,服务端通过流连续返回多条 Transaction 消息。请求支持三个字段(bank.proto):

  • account_number:必填,查询哪个账户;
  • start:起始时间(google.protobuf.Timestamp),不填则从"时间起点"算起;
  • end:结束时间,不填则包含到"当前/9999 年"为止的全部记录。

服务端逻辑(bank.go)会校验时间戳有效性(CheckValid),遍历账户的全部交易,把落在 [start, end] 区间内的记录逐条 stream.Send 出去。

调用示例(流式响应会逐条打印):

grpcurl -plaintext -H 'authorization: token 1' \\
-d '{"account_number": 1}' \\
127.0.0.1:12345 Bank.GetTransactions

如果账户 1 已有两条交易(开户初始存款 + 一笔入金),输出将是两条独立的 JSON 消息,各自带 date、amountCents、desc、seqNumber 字段。

全双工双向流:Support 聊天服务的状态机

Support 服务是 bankdemo 最精彩的部分,定义在 support.proto 中,用来演示全双工双向流:客户与客服各自维持一条双向流,消息可同时双向流动。它包含两个方法,两个都是 stream … returns (stream …):

  • ChatCustomer(stream ChatCustomerRequest) returns (stream ChatCustomerResponse):仅客户(非 agent 前缀 token)可调用;客户负责发起/终止会话;
  • ChatAgent(stream ChatAgentRequest) returns (stream ChatAgentResponse):仅客服(agent 前缀 token)可调用;客服可"接单"并回复消息。

proto 注释(support.proto)说明了核心抽象:一条流是一个有状态的"会话连接"。流初始处于"未连接"状态;必须先通过 init(客户)或 accept(客服)接入会话;要接入新会话,必须先通过 hang_up(客户)或 leave_session(客服)断开当前会话。

客户侧请求协议(oneof req)

字段类型语义
init InitiateChat 发起新会话;若带 resume_session_id 则尝试恢复指定会话
msg string 向会话中的客服发送消息
hang_up Void 终止会话;断流但未终止时,可另开新流用 init 恢复

客服侧请求协议(oneof req)

字段类型语义
accept AcceptChat 接入会话;session_id 为空则从**等待队列(FIFO)**取等待最久的会话,也可指定 ID 接入;多个客服可同时接入同一会话
msg string 发送消息给客户及其他客服
leave_session Void 退出当前会话(之后可随时重新 accept 该会话)

服务端把会话建模为"客户 + 若干客服"的订阅关系,核心实现在 chat.go:

  • newSession(chat.go):客户发起新会话,生成 %06d 格式的会话 ID,并加入 chatsAwaitingAgent 待接队列;
  • resumeSession(chat.go):恢复会话有严格限制——会话必须仍 active、发起者必须是同一客户、且该会话当前没有其他客户流占用;
  • acceptSession(chat.go):客服接单,默认从 FIFO 队列头部取单,多个客服可并存于同一 sess.agents map;
  • closeSession(chat.go):客户挂断后,向所有客服推送 session_ended 通知并清理队列与会话表;
  • 消息广播通过带缓冲的 channel + listener 实现(chat.go),send 在 channel 满或流已结束时优雅放弃,避免阻塞。

用 grpcurl 实操一次双向流聊天

双向流需要交互式输入:grpcurl 支持用 -d @ 从 stdin 读取请求体,交互式终端下可持续输入多条消息(-d 标志说明见 cmd/grpcurl/grpcurl.go)。终端 A 先以客户身份接入并开一个会话:

grpcurl -plaintext -H 'authorization: token 1' -d @ \\
127.0.0.1:12345 Support.ChatCustomer

随后在 stdin 中输入(每条一行 JSON):

{"init": {}}
{"msg": "hi, I need help with my account"}

init 的响应会返回 Session(含 sessionId、customerName、history)。注意保持该终端不退出(双向流持续在线)。

终端 B 以客服身份接单:

grpcurl -plaintext -H 'authorization: token agent1' -d @ \\
127.0.0.1:12345 Support.ChatAgent

stdin 中输入(先不填 session_id 走 FIFO 队列,也可填 "accept": {"session_id": "000001"} 指定会话):

{"accept": {}}
{"msg": "hello, how can I help?"}

此时终端 A 会实时收到客服消息(agentMsg),终端 B 会收到客户消息(ChatEntry)。客户想结束会话时发送 {"hang_up": "VOID"}(Void 是仅含 VOID=0 的枚举,JSON 中用字符串名表示),客服端会收到 session_ended 通知。若客户直接断开流而未 hang_up,客服仍在会话中时,客户可新开流发送 {"init": {"resume_session_id": "000001"}} 恢复原会话——这正是 proto 注释里描述的"断流不终止会话"语义。

由于双向流是长时间挂起的调用,实操时建议参考 README 对 grpcurl 流式能力的说明:流式方法完全被支持,交互式终端下用 stdin 作为请求体即可持续操作。

内存数据库与 JSON 持久化:accounts.json 的读写链路

bankdemo 的全部银行数据保存在一个"纯内存数据库"中,结构定义在 db.go:

字段类型作用
AccountNumbersByCustomer map[string][]uint64 客户 → 其名下账号列表
AccountsByNumber map[uint64]*account 账号 → 账户详情(含交易流水)
AccountNumbers []uint64 全部账号(供遍历/关闭时清理)
Customers []string 全部客户
LastAccountNum uint64 自增账号号段生成器
mu sync.RWMutex 读写锁,保护并发访问

并发安全是该实现的重点:openAccount、closeAccount 用 Lock() 独占写,getAccount、getAllAccounts 用 RLock() 共享读;单个账户的交易追加同样用账户级互斥锁保护(db.go),取款时还会校验余额不足并返回 FailedPrecondition。

持久化机制:定时 + 优雅关闭双保险

按 README 的说明,"每几秒、以及在收到 SIGTERM/SIGINT 等信号优雅关闭时"会把状态保存到 accounts.json。源码印证了这一设计:

  • 定时保存:bgSaver() 协程用 time.NewTicker(5 * time.Second) 每 5 秒调用一次 flush()(main.go);
  • 优雅关闭保存:main 中 defer s.flush() 保证进程退出前落盘(main.go),同时 signal.Notify 捕获 SIGINT/SIGTERM 后调用 grpcSvr.GracefulStop()(main.go);
  • 启动恢复:load() 在服务启动前读取 accounts.json,文件不存在则初始化空数据库(main.go)。

flush() 的实现也体现了并发考量:先对内存数据做一次 clone() 快照再 json.Marshal 写盘(main.go),避免持锁写文件拖慢 RPC。此外,由于 Transaction 包含 google.protobuf.Timestamp,db.go 为它实现了基于 protojson 的自定义 MarshalJSON / UnmarshalJSON(db.go),保证交易记录能正确序列化进 JSON 文件并在重启后还原。

这也带来一个实用的测试技巧:重启 go run ./internal/testing/cmd/bankdemo 后,之前创建的所有账户、交易(甚至未挂断的聊天历史之外的银行数据)都会从 accounts.json 恢复,方便反复演示。

服务端拦截器与可复用的教学点

除了业务逻辑,bankdemo 在 main.go 中还注册了一元拦截器(UnaryInterceptor)和流拦截器(StreamInterceptor):每次请求都会用原子计数器生成 request N,记录 FullMethod 与对端地址,请求结束后打印 gRPC 状态码与消息。这让 grpcurl 的每次调用在服务端日志中都有迹可循,调试时非常直观。从源码结构看,这个模式完全可以作为自建 gRPC 服务的日志/监控骨架。

总结 bankdemo 值得迁移到真实项目的设计点:

  • 金额整数化:所有货币字段用"分"存储,杜绝浮点误差;
  • 标准错误码:统一通过 status.Errorf 返回 Unauthenticated/InvalidArgument/NotFound/FailedPrecondition,语义清晰;
  • 反射开箱即用:注册 server reflection 后,grpcurl 无需任何 proto 文件即可完成 list、describe 与动态调用;
  • 双保险持久化:定时快照 + 优雅退出落盘 + 启动加载,且写盘前先克隆快照避免长锁;
  • 状态机化的双向流:把"一条流 = 一个会话连接"作为抽象,配合 oneof 消息协议,把全双工双向流的正确用法讲得清清楚楚。
  • 结语

    bankdemo 虽然只有少量源文件,却是理解 grpcurl 与 gRPC 服务端交互的最短路径:从 grpcurl -plaintext 127.0.0.1:12345 list 开始,到一元 RPC 的增删改查,再到服务端流式拉取交易、用两条终端双向流模拟一次真实的客户-客服聊天,整个链路全部可以在本仓库内完成验证。配合 README.md 中 grpcurl 的完整用法(-d、-H、list、describe、-protoset 等),读者可以把这个演示服务器当作 grpcurl 的"练习靶场",快速上手所有主流调用形态。

    赞

    分享

    • CLI
    • 开发工具

    【免费下载链接】grpcurl

    Like cURL, but for gRPC: Command-line tool for interacting with gRPC servers

    项目地址:
    https://gitcode.com/gh_mirrors/gr/grpcurl

    点击查看 免费下载

    上一篇:
    如何用CogVideoX轻松生成高质量视频:从文本到动态影像的完整指南

    下一篇:
    CogVideoX多模态融合引擎:重塑视频生成技术新边界

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » grpcurl 实战指南:深入解析 bankdemo 示例 gRPC 服务器——双向流、Header 鉴权与内存持久化
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!