.proto 文件
.proto 文件 是 gRPC 和 Protocol Buffers 的接口定義文件,它描述了:
- 要傳遞什么數據(也就是消息體 message)。
- 要暴露什么接口(也就是服務 service 和它們的 方法)。
也就是一份規范文件,讓客戶端和服務端能按照相同的約定相互通信。
my_service.proto
syntax = "proto3"; // 指定使用 proto3 語法版本package demo; // 包名,方便生成代碼時分模塊service MyService {// 定義 RPC 服務rpc Predict(PredictRequest) returns (PredictResponse) {}
}message PredictRequest {string prompt = 1;
}message PredictResponse {string result = 1;
}
- 用 protoc 自動生成客戶端/服務端代碼
這里使用 python 來進行操作
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. my_service.proto
- I. 指定 proto 搜索路徑為當前目錄
- -python_out=. 生成 my_service_pb2.py(定義消息類型)
- -grpc_python_out=. 生成 my_service_pb2_grpc.py(定義服務類接口)
因為 gRPC 要跨語言,所以必須有語言無關的描述文件:
-
.proto 就是統一規范
-
Protocol Buffers 定義數據傳輸結構(比 JSON 輕量、高速、強類型)
-
gRPC 框架用它生成客戶端/服務端、自動序列化、自動網絡傳輸
-
protoc 會以輸入文件 my_service.proto 的文件名為基準自動生成對應的 Python 文件。
-
它沒有單獨的參數去自定義生成文件名,所以默認就是:
? my_service_pb2.py:
- 包含你 .proto 中定義的 message 和 enum 類型對應的 Python 類。
? my_service_pb2_grpc.py:
- 包含你 .proto 中定義的 service 和 RPC 方法對應的客戶端和服務端樁代碼。
<proto文件名>_pb2.py
<proto文件名>_pb2_grpc.py
文件 | 包含內容 | 用途 |
---|---|---|
my_service_pb2.py | 消息類型類(message、enum) | 序列化/反序列化數據 |
my_service_pb2_grpc.py | 服務類和客戶端/服務端接口 | 實現服務邏輯/調用遠程服務 |
- pb2.py → 負責數據模型
- pb2_grpc.py → 負責服務調用邏輯(RPC 接口)
服務端實現
from concurrent import futures
import grpc
import my_service_pb2
import my_service_pb2_grpc# 實現 RPC 服務
class MyService(my_service_pb2_grpc.MyServiceServicer):
# 是生成的服務端基類,這里我們繼承它并實現 Predict 方法邏輯。def Predict(self, request, context):print(f"Received prompt: {request.prompt}")return my_service_pb2.PredictResponse(result=f"Hello, {request.prompt}!")# 啟動 gRPC 服務器
def serve():server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))my_service_pb2_grpc.add_MyServiceServicer_to_server(MyService(), server)server.add_insecure_port("[::]:50051")server.start()print("gRPC Server running at 0.0.0.0:50051...")server.wait_for_termination()if __name__ == "__main__":serve()# python -m grpc_tools.protoc -I. --pytheon_out=. --grpc_python_out=. my_service.proto
? grpc.server(…) 創建 gRPC 服務器實例,并給它分配一個線程池(能同時處理多個請求)。
? add_MyServiceServicer_to_server(MyService(), server) 注冊我們自己實現的服務邏輯。
? server.add_insecure_port(“[::]:50051”) 打開監聽端口。
? server.start() 啟動服務,wait_for_termination() 讓服務持續監聽,不會自動結束。
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'my_service_pb2', _globals)
- 動態注冊生成消息類。
也就是:
- PredictRequest、PredictResponse 這些類在導入時動態創建,IDE 靜態分析器(比如 PyCharm)無法提前知道它們存在,因此你用 IDE 查看時找不到定義。
- 但是程序運行時,這些類會存在,并能正常使用。
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10my_service.proto\x12\x04\x64\x65mo\" \n\x0ePredictRequest\x12\x0e\n\x06prompt\x18\x01 \x01(\t\"!\n\x0fPredictResponse\x12\x0e\n\x06result\x18\x01 \x01(\t2E\n\tMyService\x12\x38\n\x07Predict\x12\x14.demo.PredictRequest\x1a\x15.demo.PredictResponse\"\x00\x62\x06proto3')
- AddSerializedFile(…) 把你 .proto 定義編譯成的二進制描述數據注冊進 DescriptorPool。
- 也就是把整個 my_service.proto 的結構信息(服務、消息類型、字段等)塞入一個描述池中。
客戶端實現
import grpc
import my_service_pb2
import my_service_pb2_grpcdef run_client():channel = grpc.insecure_channel("localhost:50051")stub = my_service_pb2_grpc.MyServiceStub(channel)response = stub.Predict(my_service_pb2.PredictRequest(prompt="World111"))print(f"Server response: {response.result}")if __name__ == "__main__":run_client()
客戶端主要做三件事:
- 建立 gRPC 連接(channel)。
- 使用自動生成的 stub 來調用服務端方法。
- 接收服務端返回的響應并打印。
代碼部分 | 解釋 |
---|---|
grpc.insecure_channel() | 使用未加密通道(適合本地測試,不推薦生產環境直接用這個,要用 TLS 版 secure_channel()) |
MyServiceStub(channel) | 這是 gRPC 自動生成的客戶端類,內部已經幫你實現好了 RPC 序列化、反序列化邏輯 |
PredictRequest() | 使用自動生成的類構造 RPC 請求對象,這個類對應你的 proto 中定義的消息類型 |
stub.Predict(…) | 相當于遠程調用服務端的 Predict(),gRPC 會自動序列化請求、網絡傳輸、反序列化響應 |
response.result | 服務端返回的 PredictResponse 消息對象,取它的 result 字段就是你服務返回的內容 |
🚀 客戶端 vs 服務端對應關系
服務端:
class MyService(my_service_pb2_grpc.MyServiceServicer):def Predict(self, request, context):return my_service_pb2.PredictResponse(result=f"Hello, {request.prompt}!")
客戶端:
response = stub.Predict(my_service_pb2.PredictRequest(prompt="World111")
)
客戶端調用 Predict() 時:
- 底層幫你序列化 prompt 參數并傳給服務端
- 服務端執行對應邏輯并返回 result 給客戶端
- 客戶端拿到結果打印出來 🎉
也可以使用 go 來客戶端,注意先
protoc --go_out=. --go-grpc_out=. my_service.proto
go
package mainimport ("context""log""time""google.golang.org/grpc"pb "path/to/generated/my_service" // 引入生成的包
)func main() {// 1?? 建立 gRPC 連接conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil {log.Fatalf("連接失敗: %v", err)}defer conn.Close()// 2?? 創建客戶端存根client := pb.NewMyServiceClient(conn)// 3?? 調用服務ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)defer cancel()resp, err := client.Predict(ctx, &pb.PredictRequest{Prompt: "Hello Go!"})if err != nil {log.Fatalf("調用錯誤: %v", err)}log.Printf("服務返回: %s\n", resp.Result)
}
總結
無論是 gRPC、騰訊的 tRPC,甚至國內很多 RPC 框架(例如阿里 HSF、螞蟻 SOFA RPC),它們背后的實現思路都是一樣的👇:
🔄共同點:
-
先定義接口協議(IDL)
用 proto、.thrift、.proto3 或者類似 IDL 文件描述你的服務、方法、消息。
-
代碼生成器生成對應語言的客戶端/服務端存根
protoc、trpcproto 或者對應語言插件 → 自動生成:
- 服務接口類(Stub / Service 接口)
- 消息類(序列化 / 反序列化邏輯)
-
服務端實現 Service 類邏輯
你只需繼承生成的服務接口,然后實現方法邏輯即可。
-
客戶端用生成的存根調用方法
就像調用本地函數一樣,不需要手動拼接網絡數據、序列化二進制流。
.proto 文件↓
[生成工具] 自動生成代碼↓
服務端: 實現 Service 類
客戶端: 使用 Client Stub 調用↓
底層負責網絡傳輸、序列化、負載均衡、錯誤處理...
? 減少重復代碼 — 不用你手寫網絡序列化部分
? 強類型檢查 — 調用方/服務方都能知道數據結構
? 方便跨語言調用 — 一份協議,不同語言都能生成對應客戶端、服務端
? 減少網絡細節關注 — 把網絡部分封裝起來