欢迎体验这篇关于使用 gRPC 实现一个完整 Demo 的教学指南!如果你正在探索高性能的远程过程调用(RPC)框架,或者希望为你的项目构建高效的微服务,gRPC 是一个强大的选择。本文将以一个图书管理服务为例,带你从零开始掌握 gRPC 的核心概念、实现步骤,以及如何使用 Python 和 Go 两种语言设计和运行一个简单的客户端-服务器应用。无论你是初学者还是希望深入理解 gRPC 的开发者,这篇指南都将通过通俗易懂的讲解、详细的代码注释和原理分析,帮助你快速上手并灵活运用 gRPC。
1. 什么是 gRPC?
gRPC 是一个由 Google 开发的开源远程过程调用框架,基于 HTTP/2 协议和 Protocol Buffers(protobuf) 序列化格式。它旨在提供高性能、低延迟的跨语言通信,特别适合微服务架构。gRPC 的核心特点包括:
- 高性能:HTTP/2 支持多路复用,减少连接开销;protobuf 序列化比 JSON 更快。
- 跨语言:支持多种编程语言(如 Python、Go、Java),客户端和服务器可使用不同语言。
- 强类型:通过 protobuf 定义服务接口,生成类型安全的代码。
- 双向流:支持单向、服务器流、客户端流和双向流通信。
在本 Demo 中,我们将实现一个图书管理服务,包含以下功能:
- 添加图书(
AddBook
)
- 查询图书(
GetBook
)
- 列出所有图书(
ListBooks
)
我们将分别使用 Python 和 Go 实现,Python 因其语法简洁适合初学者,Go 因其高性能和并发支持适合生产环境。Demo 将包括服务端、客户端和必要的配置文件,逐步讲解每部分的实现和原理。
2. 准备工作
在开始编码之前,确保你的开发环境已准备好。以下是 Python 和 Go 实现所需的工具和步骤。
2.1 安装通用依赖
-
安装 Protocol Buffers 编译器(protoc):
- 在 Linux 上:
1
|
sudo apt-get install protobuf-compiler
|
- 在 macOS 上:
- 验证安装:
-
项目结构:
创建清晰的项目目录结构,支持 Python 和 Go 实现:
book_service/
├── proto/
│ └── book.proto # 服务定义文件
├── python/
│ ├── server/
│ │ └── book_server.py # Python 服务端
│ ├── client/
│ │ └── book_client.py # Python 客户端
│ └── generated/
│ └── book_pb2.py # Python 生成的 protobuf 代码
│ └── book_pb2_grpc.py # Python 生成的 gRPC 代码
├── go/
│ ├── server/
│ │ └── main.go # Go 服务端
│ ├── client/
│ │ └── main.go # Go 客户端
│ └── generated/
│ └── book/
│ └── book.pb.go # Go 生成的 protobuf 和 gRPC 代码
└── README.md # 项目说明
为什么要这样组织?
proto/
:存放 .proto
文件,统一服务定义,跨语言复用。
python/
和 go/
:隔离不同语言的实现,保持清晰。
generated/
:存放编译生成的代码,避免手动修改。
README.md
:记录项目说明,方便他人理解。
2.2 Python 环境准备
- 安装 Python:推荐 Python 3.8 或以上,确保
pip
可用。
- 安装 gRPC 工具:
1
|
pip install grpcio grpcio-tools
|
2.3 Go 环境准备
-
安装 Go:推荐 Go 1.18 或以上。下载并安装:https://golang.org/dl/
-
安装 gRPC 和 protobuf 工具:
1
2
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
确保 $GOPATH/bin
在你的 PATH 中:
1
|
export PATH=$PATH:$(go env GOPATH)/bin
|
-
验证安装:
1
2
|
protoc-gen-go --version
protoc-gen-go-grpc --version
|
3. 定义服务:编写 .proto
文件
3.1 什么是 Protocol Buffers?
Protocol Buffers(protobuf)是 gRPC 的核心,用于定义服务接口和数据结构。它是一种语言无关的序列化格式,生成强类型的代码,确保客户端和服务器的通信一致。
3.2 编写 book.proto
在 proto/book.proto
中定义图书管理服务(Python 和 Go 共用):
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
syntax = "proto3";
package book;
// 定义图书消息
message Book {
int32 id = 1; // 图书 ID
string title = 2; // 图书标题
string author = 3; // 作者
int32 publish_year = 4; // 出版年份
}
// 添加图书请求
message AddBookRequest {
Book book = 1; // 要添加的图书
}
// 添加图书响应
message AddBookResponse {
bool success = 1; // 添加是否成功
string message = 2; // 响应消息
}
// 查询图书请求
message GetBookRequest {
int32 id = 1; // 图书 ID
}
// 查询图书响应
message GetBookResponse {
Book book = 1; // 查找到的图书
}
// 列出图书请求
message ListBooksRequest {}
// 列出图书响应
message ListBooksResponse {
repeated Book books = 1; // 图书列表
}
// 定义图书管理服务
service BookService {
// 添加图书
rpc AddBook(AddBookRequest) returns (AddBookResponse);
// 查询图书
rpc GetBook(GetBookRequest) returns (GetBookResponse);
// 列出所有图书
rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);
}
|
代码解析:
syntax = "proto3"
:使用 protobuf 第 3 版,语法简洁。
package book
:定义命名空间,避免冲突。
message Book
:定义图书数据结构,包含 ID、标题、作者和出版年份。字段编号(如 1
、2
)用于序列化。
message AddBookRequest
和 AddBookResponse
:定义添加图书的请求和响应。
message GetBookRequest
和 GetBookResponse
:查询单本图书的请求和响应。
message ListBooksRequest
和 ListBooksResponse
:列出所有图书,repeated
表示列表。
service BookService
:定义服务,包含三个 RPC 方法。
为什么这样设计?
- 统一接口:Python 和 Go 共用同一
.proto
文件,确保接口一致。
- 简单性:功能聚焦于 CRUD 操作,便于教学。
- 可扩展性:字段编号支持未来扩展而不破坏兼容性。
3.3 编译 .proto
文件
3.3.1 为 Python 编译
1
|
python -m grpc_tools.protoc -Iproto --python_out=python/generated --grpc_python_out=python/generated proto/book.proto
|
生成的文件:
python/generated/book_pb2.py
:消息类(如 Book
、AddBookRequest
)。
python/generated/book_pb2_grpc.py
:gRPC 服务类(如 BookServiceServicer
)。
3.3.2 为 Go 编译
1
2
3
|
protoc --go_out=go/generated --go_opt=paths=source_relative \
--go-grpc_out=go/generated --go-grpc_opt=paths=source_relative \
-Iproto proto/book.proto
|
生成的文件:
go/generated/book/book.pb.go
:消息类和 gRPC 接口定义。
- 为什么要用
paths=source_relative
? 保持生成文件的路径与项目结构一致,便于导入。
为什么要分开编译?
- 不同语言的 gRPC 工具生成特定语言的代码,Python 和 Go 的实现方式不同。
- 分目录存储生成代码,保持项目清晰。
4. Python 实现
以下是使用 Python 实现的图书管理服务,包括服务端和客户端。
4.1 服务端:python/server/book_server.py
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
import grpc
from concurrent import futures
import time
# 导入生成的代码
from python.generated import book_pb2
from python.generated import book_pb2_grpc
# 模拟数据库:使用字典存储图书
class BookDatabase:
def __init__(self):
self.books = {} # key: book_id, value: Book
def add_book(self, book):
if book.id in self.books:
return False, "图书 ID 已存在"
self.books[book.id] = book
return True, "图书添加成功"
def get_book(self, book_id):
return self.books.get(book_id)
def list_books(self):
return self.books.values()
# 实现 BookServiceServicer
class BookServiceServicer(book_pb2_grpc.BookServiceServicer):
def __init__(self):
self.db = BookDatabase()
def AddBook(self, request, context):
book = request.book
success, message = self.db.add_book(book)
return book_pb2.AddBookResponse(success=success, message=message)
def GetBook(self, request, context):
book = self.db.get_book(request.id)
if book is None:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details("图书未找到")
return book_pb2.GetBookResponse()
return book_pb2.GetBookResponse(book=book)
def ListBooks(self, request, context):
books = self.db.list_books()
return book_pb2.ListBooksResponse(books=books)
# 启动 gRPC 服务器
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
book_pb2_grpc.add_BookServiceServicer_to_server(BookServiceServicer(), server)
server.add_insecure_port('[::]:50051')
print("Python gRPC 服务器启动于 localhost:50051")
server.start()
try:
while True:
time.sleep(86400)
except KeyboardInterrupt:
server.stop(0)
print("Python gRPC 服务器已停止")
if __name__ == '__main__':
serve()
|
代码解析:
- BookDatabase:使用字典模拟数据库,存储图书数据。方法
add_book
、get_book
和 list_books
实现核心逻辑。
- BookServiceServicer:继承生成的基类,实现
AddBook
、GetBook
和 ListBooks
方法。错误处理通过 context.set_code
提供 gRPC 标准错误。
- serve:创建 gRPC 服务器,使用线程池支持并发,监听
localhost:50051
。
- 为什么要用线程池? Python 的 gRPC 服务器通过线程池处理并发请求,适合中小规模服务。
设计理由:
- 简单存储:字典便于教学,易于替换为真实数据库。
- 错误处理:gRPC 错误码(如
NOT_FOUND
)增强客户端体验。
- 并发支持:线程池满足教学场景的并发需求。
4.2 客户端:python/client/book_client.py
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
import grpc
import time
from python.generated import book_pb2
from python.generated import book_pb2_grpc
def run():
with grpc.insecure_channel('localhost:50051') as channel:
stub = book_pb2_grpc.BookServiceStub(channel)
print("\n=== 测试用例 1:添加图书 ===")
book = book_pb2.Book(
id=1,
title="Python 编程入门",
author="Alice",
publish_year=2023
)
try:
response = stub.AddBook(book_pb2.AddBookRequest(book=book))
print(f"添加结果: {response.success}, 消息: {response.message}")
except grpc.RpcError as e:
print(f"添加失败: {e.details()}")
print("\n=== 测试用例 2:查询图书 ===")
try:
response = stub.GetBook(book_pb2.GetBookRequest(id=1))
if response.book.id:
print(f"找到图书: ID={response.book.id}, 标题={response.book.title}")
else:
print("图书未找到")
except grpc.RpcError as e:
print(f"查询失败: {e.details()}")
print("\n=== 测试用例 3:查询不存在的图书 ===")
try:
response = stub.GetBook(book_pb2.GetBookRequest(id=999))
print("图书未找到")
except grpc.RpcError as e:
print(f"查询失败: {e.details()}")
print("\n=== 测试用例 4:列出所有图书 ===")
try:
response = stub.ListBooks(book_pb2.ListBooksRequest())
for book in response.books:
print(f"图书: ID={book.id}, 标题={book.title}, 作者={book.author}")
except grpc.RpcError as e:
print(f"列出失败: {e.details()}")
if __name__ == '__main__':
run()
|
代码解析:
- 通道和存根:使用
insecure_channel
连接服务器,创建 BookServiceStub
调用 RPC 方法。
- 测试用例:覆盖添加、查询(成功和失败)、列出图书,展示 gRPC 的使用方式。
- 错误处理:捕获
RpcError
,打印详细错误信息。
- 为什么要用 with 语句? 自动管理通道资源,防止泄漏。
设计理由:
- 测试全面:用例覆盖常见场景,验证服务功能。
- 用户友好:清晰的输出和错误信息便于调试。
- 简洁逻辑:专注于 gRPC 调用,降低学习曲线。
5. Go 实现
以下是使用 Go 实现的图书管理服务,包括服务端和客户端。Go 的实现利用其强大的并发模型和高性能,适合生产环境。
5.1 初始化 Go 模块
在 go/
目录下初始化 Go 模块:
1
2
3
4
|
cd go
go mod init book_service
go get google.golang.org/grpc@v1.64.0
go get google.golang.org/protobuf@v1.34.2
|
为什么要初始化模块?
- Go 模块管理依赖,确保版本一致。
google.golang.org/grpc
和 google.golang.org/protobuf
是 gRPC 和 protobuf 的核心库。
5.2 服务端:go/server/main.go
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
package main
import (
"context"
"fmt"
"log"
"net"
"sync"
"book_service/go/generated/book"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// BookDatabase 模拟数据库,使用内存存储图书
type BookDatabase struct {
books map[int32]*book.Book
mu sync.RWMutex // 保护并发访问
}
func NewBookDatabase() *BookDatabase {
return &BookDatabase{
books: make(map[int32]*book.Book),
}
}
func (db *BookDatabase) AddBook(b *book.Book) (bool, string) {
db.mu.Lock()
defer db.mu.Unlock()
if _, exists := db.books[b.Id]; exists {
return false, "图书 ID 已存在"
}
db.books[b.Id] = b
return true, "图书添加成功"
}
func (db *BookDatabase) GetBook(id int32) (*book.Book, error) {
db.mu.RLock()
defer db.mu.RUnlock()
if book, exists := db.books[id]; exists {
return book, nil
}
return nil, status.Error(codes.NotFound, "图书未找到")
}
func (db *BookDatabase) ListBooks() []*book.Book {
db.mu.RLock()
defer db.mu.RUnlock()
books := make([]*book.Book, 0, len(db.books))
for _, b := range db.books {
books = append(books, b)
}
return books
}
// BookServer 实现 BookServiceServer 接口
type BookServer struct {
book.UnimplementedBookServiceServer
db *BookDatabase
}
func NewBookServer() *BookServer {
return &BookServer{
db: NewBookDatabase(),
}
}
func (s *BookServer) AddBook(ctx context.Context, req *book.AddBookRequest) (*book.AddBookResponse, error) {
success, message := s.db.AddBook(req.Book)
return &book.AddBookResponse{
Success: success,
Message: message,
}, nil
}
func (s *BookServer) GetBook(ctx context.Context, req *book.GetBookRequest) (*book.GetBookResponse, error) {
b, err := s.db.GetBook(req.Id)
if err != nil {
return nil, err
}
return &book.GetBookResponse{Book: b}, nil
}
func (s *BookServer) ListBooks(ctx context.Context, req *book.ListBooksRequest) (*book.ListBooksResponse, error) {
books := s.db.ListBooks()
return &book.ListBooksResponse{Books: books}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("无法监听端口: %v", err)
}
server := grpc.NewServer()
book.RegisterBookServiceServer(server, NewBookServer())
fmt.Println("Go gRPC 服务器启动于 localhost:50051")
if err := server.Serve(lis); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
}
|
代码解析:
-
BookDatabase:
- 使用
map[int32]*book.Book
存储图书,sync.RWMutex
保护并发访问。
- 方法
AddBook
、GetBook
和 ListBooks
实现图书操作,返回 gRPC 错误(如 codes.NotFound
)。
- 为什么要用 RWMutex? 支持读多写少的场景,Go 的并发模型高效处理多客户端请求。
-
BookServer:
- 实现生成的
BookServiceServer
接口,包含 AddBook
、GetBook
和 ListBooks
方法。
- 使用
context.Context
传递请求元数据,支持取消和超时。
- 为什么要实现接口? Go 的 gRPC 要求显式实现
.proto
定义的方法,确保类型安全。
-
main 函数:
- 创建 TCP 监听器,绑定
localhost:50051
。
- 初始化 gRPC 服务器,注册
BookServer
。
- 为什么要用 net.Listen? Go 提供细粒度的网络控制,适合高性能服务。
设计理由:
- 并发安全:
RWMutex
确保多协程访问数据库安全。
- 错误处理:使用
status.Error
返回 gRPC 标准错误码。
- 简洁高效:Go 的单文件结构适合教学,易于扩展。
5.3 客户端:go/client/main.go
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
package main
import (
"context"
"fmt"
"log"
"book_service/go/generated/book"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
func main() {
conn, err := grpc.NewClient("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("无法连接服务器: %v", err)
}
defer conn.Close()
client := book.NewBookServiceClient(conn)
ctx := context.Background()
// 测试用例 1:添加图书
fmt.Println("\n=== 测试用例 1:添加图书 ===")
bookToAdd := &book.Book{
Id: 1,
Title: "Go 编程实战",
Author: "Bob",
PublishYear: 2024,
}
resp, err := client.AddBook(ctx, &book.AddBookRequest{Book: bookToAdd})
if err != nil {
log.Printf("添加失败: %v", err)
} else {
fmt.Printf("添加结果: %v, 消息: %s\n", resp.Success, resp.Message)
}
// 测试用例 2:查询图书
fmt.Println("\n=== 测试用例 2:查询图书 ===")
getResp, err := client.GetBook(ctx, &book.GetBookRequest{Id: 1})
if err != nil {
log.Printf("查询失败: %v", err)
} else {
fmt.Printf("找到图书: ID=%d, 标题=%s\n", getResp.Book.Id, getResp.Book.Title)
}
// 测试用例 3:查询不存在的图书
fmt.Println("\n=== 测试用例 3:查询不存在的图书 ===")
_, err = client.GetBook(ctx, &book.GetBookRequest{Id: 999})
if err != nil {
if status.Code(err) == codes.NotFound {
fmt.Println("查询失败: 图书未找到")
} else {
log.Printf("查询失败: %v", err)
}
}
// 测试用例 4:列出所有图书
fmt.Println("\n=== 测试用例 4:列出所有图书 ===")
listResp, err := client.ListBooks(ctx, &book.ListBooksRequest{})
if err != nil {
log.Printf("列出失败: %v", err)
} else {
for _, b := range listResp.Books {
fmt.Printf("图书: ID=%d, 标题=%s, 作者=%s\n", b.Id, b.Title, b.Author)
}
}
}
|
代码解析:
-
连接服务器:
- 使用
grpc.NewClient
建立未加密连接,WithInsecure
适合本地测试。
- 为什么要用 defer Close? 确保连接在程序结束时释放。
-
客户端存根:
NewBookServiceClient
创建客户端,封装 RPC 方法。
- 为什么要用存根? 提供类型安全的接口,简化调用。
-
测试用例:
- 与 Python 客户端类似,覆盖添加、查询和列出图书。
- 使用
status.Code
检查 gRPC 错误码。
- 为什么要检查错误码? Go 的 gRPC 错误更细粒度,
codes.NotFound
明确表示未找到。
设计理由:
- 一致性:测试用例与 Python 客户端对齐,便于比较。
- Go 风格:使用 Go 的错误处理和上下文机制,符合语言习惯。
- 简洁清晰:专注于 gRPC 调用,降低学习难度。
6. 运行 Demo
6.1 Python 实现
-
编译 .proto
文件:
1
|
python -m grpc_tools.protoc -Iproto --python_out=python/generated --grpc_python_out=python/generated proto/book.proto
|
-
启动服务端:
1
|
python python/server/book_server.py
|
输出:
Python gRPC 服务器启动于 localhost:50051
-
运行客户端:
1
|
python python/client/book_client.py
|
预期输出:
=== 测试用例 1:添加图书 ===
添加结果: True, 消息: 图书添加成功
=== 测试用例 2:查询图书 ===
找到图书: ID=1, 标题=Python 编程入门
=== 测试用例 3:查询不存在的图书 ===
查询失败: 图书未找到
=== 测试用例 4:列出所有图书 ===
图书: ID=1, 标题=Python 编程入门, 作者=Alice
6.2 Go 实现
-
编译 .proto
文件:
1
2
3
|
protoc --go_out=go/generated --go_opt=paths=source_relative \
--go-grpc_out=go/generated --go-grpc_opt=paths=source_relative \
-Iproto proto/book.proto
|
-
启动服务端:
1
2
|
cd go/server
go run main.go
|
输出:
Go gRPC 服务器启动于 localhost:50051
-
运行客户端:
1
2
|
cd go/client
go run main.go
|
预期输出:
=== 测试用例 1:添加图书 ===
添加结果: true, 消息: 图书添加成功
=== 测试用例 2:查询图书 ===
找到图书: ID=1, 标题=Go 编程实战
=== 测试用例 3:查询不存在的图书 ===
查询失败: 图书未找到
=== 测试用例 4:列出所有图书 ===
图书: ID=1, 标题=Go 编程实战, 作者=Bob
6.3 跨语言测试
- Python 客户端可以连接 Go 服务端,反之亦然,因为
.proto
文件定义了统一的接口。
- 为什么要测试跨语言? 展示 gRPC 的跨语言兼容性,验证接口一致性。
7. 深入分析:Python vs Go 实现
7.1 实现差异
- 并发模型:
- Python:使用
ThreadPoolExecutor
处理并发,适合中小规模服务。
- Go:利用协程(goroutines)和
sync.RWMutex
,天然支持高并发,性能更高。
- 错误处理:
- Python:通过
context.set_code
设置 gRPC 错误。
- Go:使用
status.Error
返回标准错误码,更细粒度。
- 代码结构:
- Python:动态类型,代码更简洁,适合快速原型。
- Go:静态类型,显式接口实现,适合生产环境。
7.2 为什么提供两种实现?
- Python:语法简单,易于初学者理解,适合教学和快速开发。
- Go:高性能、并发模型强大,适合生产环境和性能敏感场景。
- 对比学习:通过两种语言,读者可以理解 gRPC 的语言无关性。
7.3 性能考虑
- Go 的服务端通常比 Python 更快,因为:
- 协程模型比线程池更轻量。
- 静态编译生成高效的二进制文件。
- Python 适合开发速度优先的场景,Go 适合高并发和低延迟。
8. 优化与扩展
8.1 添加 TLS 加密
生产环境应启用 TLS 加密。
Python 服务端:
1
2
3
4
|
server_credentials = grpc.ssl_server_credentials(
[(open('server.key', 'rb').read(), open('server.crt', 'rb').read())]
)
server.add_secure_port('[::]:50051', server_credentials)
|
Go 服务end:
1
2
3
4
5
|
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
log.Fatalf("加载 TLS 证书失败: %v", err)
}
server := grpc.NewServer(grpc.Creds(creds))
|
为什么要加密? 保护数据隐私,防止中间人攻击。
8.2 集成真实数据库
替换内存数据库为 PostgreSQL。
Go 示例:
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
29
30
31
32
33
34
|
import (
"database/sql"
_ "github.com/lib/pq"
)
type BookDatabase struct {
db *sql.DB
}
func NewBookDatabase() (*BookDatabase, error) {
db, err := sql.Open("postgres", "user=postgres password=password dbname=books_db host=localhost sslmode=disable")
if err != nil {
return nil, err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
publish_year INTEGER
)`)
if err != nil {
return nil, err
}
return &BookDatabase{db: db}, nil
}
func (db *BookDatabase) AddBook(b *book.Book) (bool, string) {
_, err := db.db.Exec("INSERT INTO books (id, title, author, publish_year) VALUES ($1, $2, $3, $4)",
b.Id, b.Title, b.Author, b.PublishYear)
if err != nil {
return false, "图书 ID 已存在"
}
return true, "图书添加成功"
}
|
为什么要用数据库? 持久化存储,适合生产环境。
8.3 添加流式 RPC
支持流式列出图书。
proto 文件:
1
|
rpc ListBooksStream(ListBooksRequest) returns (stream Book);
|
Go 服务端:
1
2
3
4
5
6
7
8
9
|
func (s *BookServer) ListBooksStream(req *book.ListBooksRequest, stream book.BookService_ListBooksStreamServer) error {
books := s.db.ListBooks()
for _, b := range books {
if err := stream CreedSend(b); err != nil {
return err
}
}
return nil
}
|
为什么要用流? 适合大数据量,逐个传输减少内存占用。
9. 常见问题与解决方案
9.1 服务端未启动
症状:客户端报 connection refused
。
解决方案:
- 确保服务端运行并监听
localhost:50051
。
- 检查防火墙或端口占用。
9.2 查询未找到图书
症状:返回 NotFound
错误。
解决方案:
9.3 Go 编译错误
症状:undefined: book.BookServiceServer
。
解决方案:
- 确保
protoc
生成了 book.pb.go
和 gRPC 代码。
- 检查 Go 模块依赖:
9.4 Python 客户端无法连接 Go 服务端
症状:UNAVAILABLE
错误。
解决方案:
- 确认 Go 服务端已启动。
- 检查
.proto
文件版本一致。
10. 总结与实践建议
通过本文,你构建了一个 gRPC 图书管理服务,掌握了 Python 和 Go 两种实现方式,覆盖了以下内容:
- 服务定义:使用
.proto
文件设计统一接口。
- Python 实现:快速原型,适合教学和开发。
- Go 实现:高性能,适合生产环境。
- 优化扩展:TLS、数据库和流式支持。
以下是一些实践建议:
- 比较语言:运行 Python 和 Go 实现,比较性能和并发能力。
- 扩展功能:添加
UpdateBook
或 DeleteBook
方法。
- 集成监控:使用 Prometheus 监控 gRPC 请求。
- 部署实践:将 Go 服务端部署到 Docker 或 Kubernetes。
动手实践:
- 运行 Python 和 Go 实现,交叉测试客户端和服务端。
- 添加 TLS 加密,验证安全连接。
- 集成 PostgreSQL,替换内存数据库。
- 实现
ListBooksStream
,测试流式传输。
希望这篇指南能帮助你深入理解 gRPC 并在项目中灵活应用!如果有任何疑问或需要进一步探讨,欢迎留言交流。
参考资料:
- gRPC 官方文档:https://grpc.io/docs/
- Protocol Buffers 文档:https://developers.google.com/protocol-buffers
- Go gRPC 教程:https://grpc.io/docs/languages/go/
评论 0