用 Claude Code 开发 gRPC:Protobuf、流式传输与运维
用 Claude Code 构建可运行的 gRPC 服务,涵盖 Protobuf、期限、流式、认证和监控。
先写清契约,再让 Claude Code 写实现
gRPC 是一种远程过程调用框架。客户端像调用本地函数一样调用另一个服务的方法,但真正的通信发生在网络上。Protocol Buffers 负责定义契约:服务、方法、请求、响应和字段编号。Claude Code 可以很快写出服务器和客户端,但如果契约不清楚,后面的生成代码、错误处理、测试和文档都会一起偏掉。
工作时请以官方资料为准:入门看 gRPC Introduction 和 Core concepts,Node 实现看 gRPC Node basics,期限看 Deadlines,错误看 Status Codes,认证看 Authentication,监控看 OpenTelemetry Metrics,字段演进看 Protocol Buffers proto3 guide。
相关的站内内容可以继续读:生产级 API 开发、Claude Code 微服务设计、API 版本策略 和 测试策略。
适合 gRPC 的实际场景
不要只因为“性能好”就选择 gRPC。更好的判断是:这个通信是否需要强契约、明确的失败语义和跨语言一致性。
| 使用场景 | 为什么适合 gRPC | 交给 Claude Code 的任务 |
|---|---|---|
| 订单、库存、账单等内部服务 | 类型化契约能减少团队间的接口漂移 | 编写 .proto、服务器、客户端和状态码表 |
| 大量导出或同步数据 | 服务端流式传输可以分批返回 | 设计 returns (stream Item) 并处理取消 |
| Go、Node、Python 混合团队 | 同一个契约可以生成多语言客户端 | 整理生成命令、目录结构和 CI 检查 |
| 内部工具或 AI 代理 | 明确方法比自由文本调用更可控 | 加示例客户端、认证元数据、期限和日志 |
Masa 在小型服务里的经验是,过早做一个万能的 Search 方法会让后续演进变难。读、写、流式导出最好分开,让 Claude Code 先审查 .proto,再改实现。
可直接运行的 Node gRPC 示例
这个示例使用 @grpc/proto-loader 动态读取 .proto,第一次本地验证不需要安装 protoc。
mkdir claude-grpc-demo
cd claude-grpc-demo
npm init -y
npm install @grpc/grpc-js @grpc/proto-loader
mkdir proto
package.json:
{
"type": "commonjs",
"scripts": {
"server": "node server.js",
"client": "node client.js"
},
"dependencies": {
"@grpc/grpc-js": "latest",
"@grpc/proto-loader": "latest"
}
}
proto/task.proto:
syntax = "proto3";
package tasks.v1;
service TaskService {
rpc CreateTask(CreateTaskRequest) returns (Task);
rpc GetTask(GetTaskRequest) returns (Task);
rpc ListTasks(ListTasksRequest) returns (stream Task);
}
message Task {
string id = 1;
string title = 2;
string status = 3;
int64 created_at_unix = 4;
}
message CreateTaskRequest {
string title = 1;
}
message GetTaskRequest {
string id = 1;
}
message ListTasksRequest {
int32 limit = 1;
}
server.js:
const path = require("node:path");
const { randomUUID } = require("node:crypto");
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const PROTO_PATH = path.join(__dirname, "proto", "task.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: false,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const taskProto = grpc.loadPackageDefinition(packageDefinition).tasks.v1;
const token = process.env.DEMO_TOKEN || "dev-token";
const tasks = new Map();
function grpcError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}
function assertAuthenticated(call) {
const value = call.metadata.get("authorization")[0];
if (value !== `Bearer ${token}`) {
throw grpcError(grpc.status.UNAUTHENTICATED, "UNAUTHENTICATED");
}
}
function createTask(call, callback) {
try {
assertAuthenticated(call);
const title = String(call.request.title || "").trim();
if (!title) {
return callback(grpcError(grpc.status.INVALID_ARGUMENT, "INVALID_ARGUMENT: title"));
}
const task = {
id: randomUUID(),
title,
status: "OPEN",
createdAtUnix: String(Math.floor(Date.now() / 1000)),
};
tasks.set(task.id, task);
callback(null, task);
} catch (error) {
callback(error);
}
}
function getTask(call, callback) {
try {
assertAuthenticated(call);
const task = tasks.get(call.request.id);
if (!task) {
return callback(grpcError(grpc.status.NOT_FOUND, "NOT_FOUND: task"));
}
callback(null, task);
} catch (error) {
callback(error);
}
}
function listTasks(call) {
try {
assertAuthenticated(call);
const limit = Math.min(Math.max(Number(call.request.limit) || 10, 1), 100);
for (const task of Array.from(tasks.values()).slice(0, limit)) {
call.write(task);
}
call.end();
} catch (error) {
call.destroy(error);
}
}
const server = new grpc.Server();
server.addService(taskProto.TaskService.service, { createTask, getTask, listTasks });
server.bindAsync("127.0.0.1:50051", grpc.ServerCredentials.createInsecure(), (error, port) => {
if (error) throw error;
console.log(`TaskService listening on ${port}`);
});
client.js:
const path = require("node:path");
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const PROTO_PATH = path.join(__dirname, "proto", "task.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: false,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const taskProto = grpc.loadPackageDefinition(packageDefinition).tasks.v1;
const client = new taskProto.TaskService("127.0.0.1:50051", grpc.credentials.createInsecure());
const metadata = new grpc.Metadata();
metadata.set("authorization", `Bearer ${process.env.DEMO_TOKEN || "dev-token"}`);
function deadline(ms) {
return new Date(Date.now() + ms);
}
function createTask(title) {
return new Promise((resolve, reject) => {
client.createTask({ title }, metadata, { deadline: deadline(1000) }, (error, task) => {
if (error) return reject(error);
resolve(task);
});
});
}
function getTask(id) {
return new Promise((resolve, reject) => {
client.getTask({ id }, metadata, { deadline: deadline(1000) }, (error, task) => {
if (error) return reject(error);
resolve(task);
});
});
}
function listTasks(limit) {
return new Promise((resolve, reject) => {
const rows = [];
const stream = client.listTasks({ limit }, metadata, { deadline: deadline(1000) });
stream.on("data", (task) => rows.push(task));
stream.on("error", reject);
stream.on("end", () => resolve(rows));
});
}
async function main() {
const created = await createTask("Claude Code gRPC");
const fetched = await getTask(created.id);
const rows = await listTasks(10);
console.log(JSON.stringify({ created, fetched, streamed: rows.length }, null, 2));
client.close();
}
main().catch((error) => {
console.error(error.code, error.details || error.message);
client.close();
process.exitCode = 1;
});
一个终端启动服务器:
npm run server
另一个终端运行客户端:
npm run client
架构演进与常见失败
Protocol Buffers 的重点是字段编号。已经发布的编号不要改,也不要复用。删除字段时,用 reserved 保留编号和名称;新增字段时使用新编号;如果要区分“未设置”和“空值”,考虑 optional。
message Task {
string id = 1;
string title = 2;
string status = 3;
int64 created_at_unix = 4;
optional string assignee_email = 5;
reserved 6, 7;
reserved "owner_email";
}
期限也必须明确。官方文档说明,如果客户端不设置期限,调用可能一直等待。示例里的 1 秒只适合本地验证,生产环境要根据延迟、处理时间和重试策略调整。
流式传输适合导出和进度推送,但不要先把所有数据放进内存再写出。要处理客户端取消、网络中断和部分失败。认证方面,示例中的 createInsecure() 只能用于本地。生产环境应使用 TLS:服务端换成 grpc.ServerCredentials.createSsl(...),客户端换成 grpc.credentials.createSsl(rootCert),再通过元数据传令牌。
监控要能回答“哪个方法、哪个状态、耗时多少、是否超时、是否取消”。OpenTelemetry 的 gRPC 指标可以作为基线。错误码不要全部写成 UNKNOWN,而要区分 INVALID_ARGUMENT、NOT_FOUND、UNAUTHENTICATED、DEADLINE_EXCEEDED、UNAVAILABLE 和 RESOURCE_EXHAUSTED。写入类 RPC 只有在有幂等键时才适合自动重试。
给 Claude Code 的提示词
实现 gRPC TaskService。
要求:
- 先创建 proto/task.proto
- 使用 Node.js、@grpc/grpc-js、@grpc/proto-loader
- 实现 CreateTask、GetTask、ListTasks 服务端流式传输
- 每个客户端 RPC 都要设置 deadline
- 校验 authorization metadata
- 说明生产环境要把 createInsecure 换成 TLS
- 运行 npm run client 并报告输出
可编辑文件:
- proto/task.proto
- server.js
- client.js
- package.json
审查时再发:
请按 findings first 审查这个 gRPC 实现。
重点检查字段编号复用、schema evolution、deadline、cancellation、
status code、streaming 内存行为、TLS/auth、observability。
按严重程度排序,并给出具体文件位置。
变现导线与验证结果
个人学习可以从免费 Claude Code 速查表开始,把这个示例改成一次性练习项目。团队如果需要 .proto 管理、CI 关卡、审查提示词、TLS 上线和监控设计,可以查看 Claude Code 培训与咨询。需要可复用资料时,再看 ClaudeCodeLab 产品。
本次改写中,示例已在临时目录用 Node v24.14.1 和 npm 11.11.0 实际运行:npm install、node server.js、node client.js 都通过,客户端输出包含 created、fetched 和 streamed: 1。这台机器没有 go、protoc 和 Go 的 Protobuf 插件,所以本文选择了可验证的 Node 动态加载路径。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。