You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python多包项目中gRPC proto文件的定义与组织方案问询

嘿,这个问题我刚好在单体仓库里处理过很多次,核心痛点就是不想重复复制proto内容,同时还要让生成的Python代码能被各个服务正确导入,还得保持仓库结构整洁。咱们一步步来搞定它:

第一步:规划合理的目录结构

先给你一个清晰的单体仓库结构参考,核心是把所有proto文件统一放在根目录的protos/下,按功能拆分子目录,避免和业务代码混在一起:

your-monorepo/
├── protos/                # 统一存放所有公共proto资源
│   ├── common/            # 公共消息、枚举、基础类型(全服务复用)
│   │   └── base.proto
│   └── my_service/        # 对应你的gRPC服务定义(包含接口声明)
│       └── service.proto
├── my_client_package/     # 客户端Python包
│   ├── __init__.py
│   ├── generated/         # 生成的gRPC客户端代码(自动生成)
│   └── client/
│       └── main.py
├── my_server_package/     # 服务端Python包
│   ├── __init__.py
│   ├── generated/         # 生成的gRPC服务端代码(自动生成)
│   └── server/
│       └── main.py
└── requirements.txt       # 依赖:grpcio、grpcio-tools、protobuf等
第二步:编写可复用的Proto文件

关键原则是公共内容抽离到common目录,业务服务只做导入和接口定义,彻底避免重复复制。

比如先写公共的protos/common/base.proto,放所有服务都会用到的消息类型:

syntax = "proto3";

// 定义package,避免命名冲突
package common;

// 公共用户信息消息
message UserInfo {
  string user_id = 1;
  string username = 2;
}

// 公共服务响应格式
message ServiceResponse {
  bool success = 1;
  string message = 2;
}

然后写服务定义protos/my_service/service.proto,直接导入公共proto,不用重复写消息:

syntax = "proto3";

package my_service;

// 导入公共proto,路径是相对于protos根目录的
import "common/base.proto";

// 定义gRPC服务接口
service MyService {
  rpc GetUserInfo(common.UserInfo) returns (common.ServiceResponse);
  rpc UpdateUser(common.UserInfo) returns (common.ServiceResponse);
}
第三步:生成Python gRPC代码

用官方的grpc_tools.protoc工具生成代码,重点是指定--proto_path(告诉protoc去哪里找导入的proto文件),以及输出目录。

比如给客户端生成代码的命令:

python -m grpc_tools.protoc \
  --proto_path=./protos \
  --python_out=./my_client_package/generated \
  --grpc_python_out=./my_client_package/generated \
  ./protos/common/base.proto \
  ./protos/my_service/service.proto

同样的命令给服务端生成(只需要改输出目录为./my_server_package/generated)。

如果你觉得手动敲命令麻烦,可以写个简单的Makefile或者shell脚本批量生成:

# Makefile示例
PROTOC_ROOT=./protos
CLIENT_OUT=./my_client_package/generated
SERVER_OUT=./my_server_package/generated

generate-all: generate-client generate-server

generate-client:
	python -m grpc_tools.protoc \
		--proto_path=$(PROTOC_ROOT) \
		--python_out=$(CLIENT_OUT) \
		--grpc_python_out=$(CLIENT_OUT) \
		$(shell find $(PROTOC_ROOT) -name "*.proto")

generate-server:
	python -m grpc_tools.protoc \
		--proto_path=$(PROTOC_ROOT) \
		--python_out=$(SERVER_OUT) \
		--grpc_python_out=$(SERVER_OUT) \
		$(shell find $(PROTOC_ROOT) -name "*.proto")

之后只需要运行make generate-all就能一键生成所有代码。

第四步:在服务中使用生成的代码

现在你可以在客户端和服务端直接导入生成的代码,没有路径问题。

比如服务端代码示例:

# my_server_package/server/main.py
import grpc
from concurrent import futures
from generated.common import base_pb2
from generated.my_service import service_pb2, service_pb2_grpc

# 实现gRPC服务接口
class MyServiceServicer(service_pb2_grpc.MyServiceServicer):
    def GetUserInfo(self, request, context):
        # request是base_pb2.UserInfo类型,直接用
        return base_pb2.ServiceResponse(
            success=True,
            message=f"Hello, {request.username}!"
        )

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    service_pb2_grpc.add_MyServiceServicer_to_server(MyServiceServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    print("Server started on port 50051")
    server.wait_for_termination()

if __name__ == "__main__":
    serve()

客户端代码示例:

# my_client_package/client/main.py
import grpc
from generated.common import base_pb2
from generated.my_service import service_pb2, service_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = service_pb2_grpc.MyServiceStub(channel)
        # 构造请求(用公共的UserInfo消息)
        request = base_pb2.UserInfo(user_id="123", username="Levi")
        response = stub.GetUserInfo(request)
        print(f"Server response: {response.success} - {response.message}")

if __name__ == "__main__":
    run()
一些实用的最佳实践
  • 给每个proto文件指定明确的package,避免不同服务的proto命名冲突
  • 保持proto目录结构和package名一致(比如protos/common对应package common),这样生成的Python代码结构也会对应,导入更清晰
  • 不要手动修改生成的Python代码,所有修改都应该在proto文件里完成,然后重新生成
  • 如果你的仓库用Poetry或者pipenv管理依赖,可以把generated目录加入源码路径,比如在pyproject.toml里添加:
    [tool.poetry]
    packages = [
        { include = "my_client_package" },
        { include = "my_server_package" },
        { include = "generated" }
    ]
    

内容的提问来源于stack exchange,提问作者Levi Noecker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 10:08:19