grpc客户端优化
📚 使用须知
- 本博客内容仅供学习参考
- 建议理解思路后独立实现
- 欢迎交流讨论
故事的开始:固件升级流程
我们的任务是实现一个通过 gRPC 控制的固件升级流程。从逻辑上看,它非常直接:
- 客户端通过一个 gRPC 流式 (
Streaming) RPC 将固件文件上传到服务器。 - 服务器接收完文件后,执行一个脚本(例如
upgrade-rootfs.sh)来应用更新。 - 该脚本在最后会重启服务器,以加载新的固件。
- 客户端在上传完成后,进入一个轮询 (
Polling) 循环,反复查询服务器的状态,直到服务器成功响应,确认新固件已在运行
C++
ezhuzix@CNNJ002931:/mnt/c/Users/ezhuzix/repo_o/nib_application/grpc$ ./../build/binaries/grpc_client fwup 192.168.2.61:50050 ./TIB3_rp
i_software_package.tar "CXP9024418/1" R1A
Firmware Update Client
Server: 192.168.2.61:50050
File: ./TIB3_rpi_software_package.tar
Product: CXP9024418/1
RState: R1A
------------------------
Firmware client connected to: 192.168.2.61:50050
Testing firmware connection...
[OK] Firmware connection test succeeded
Starting complete firmware upgrade process...
Step 1: Uploading firmware...
Testing upload: ./TIB3_rpi_software_package.tar
File size: 137553920 bytes
[OK] Software item sent
Progress: 7% (10485760/137553920 bytes)
Progress: 15% (20971520/137553920 bytes)
Progress: 22% (31457280/137553920 bytes)
Progress: 30% (41943040/137553920 bytes)
Progress: 38% (52428800/137553920 bytes)
Progress: 45% (62914560/137553920 bytes)
Progress: 53% (73400320/137553920 bytes)
Progress: 60% (83886080/137553920 bytes)
Progress: 68% (94371840/137553920 bytes)
Progress: 76% (104857600/137553920 bytes)
Progress: 83% (115343360/137553920 bytes)
Progress: 91% (125829120/137553920 bytes)
Progress: 99% (136314880/137553920 bytes)
Progress: 100% (137553920/137553920 bytes)
[OK] All data sent (132 chunks)
[OK] Firmware upload succeeded
Server response: (code: 0)
[OK] Firmware upload completed
Step 2: Waiting for device to restart and come back online...
Waiting for device to start upgrade (2 minutes)...
Still waiting for upgrade to start... (30/120 seconds)
Still waiting for upgrade to start... (60/120 seconds)
Still waiting for upgrade to start... (90/120 seconds)
Still waiting for upgrade to start... (120/120 seconds)
Starting to poll GetSWInfos every 5 seconds (max 10 minutes)...
[OK] Device is back online! New firmware is running.
Current software information:
- Name: TIB3
- Product Number: CXC1744343/2-R1E-0-g2aefebe
- R-State: R1E
- Comment: Build time: 2025-05-27 05:48:17+00:00
[OK] Firmware upgrade completed successfully!
[SUCCESS] Operation completed successfully!
以下是终端输出的对应代码段解析,按执行顺序排列:
1. 初始化输出
终端输出:
Firmware Update Client
Server: 192.168.2.61:50050
File: ./TIB3_rpi_software_package.tar
Product: CXP9024418/1
RState: R1A
------------------------
对应代码段 (grpc_client_main.cc):
std::cout << "Firmware Update Client" << std::endl;
std::cout << "Server: " << server_address << std::endl;
std::cout << "File: " << file_path << std::endl;
std::cout << "Product: "
<< (product_number.empty() ? "(empty)" : product_number)
<< std::endl;
std::cout << "RState: " << (rstate.empty() ? "(empty)" : rstate)
<< std::endl;
std::cout << "------------------------" << std::endl;
解析:
- 简单的控制台输出,显示命令行参数
- 使用
std::cout进行格式化输出
2. 客户端连接
终端输出:
Firmware client connected to: 192.168.2.61:50050
对应代码段 (simple_firmware_client.cc):
SimpleFirmwareClient::SimpleFirmwareClient(const std::string &server_address)
: BaseGrpcClient(server_address) {
auto channel = CreateChannel();
firmware_stub_ = proto::raptor2::ms::test_interface::
TestInterfaceFirmwareUpdateService::NewStub(channel);
config_stub_ = proto::raptor2::ms::test_interface::
TestInterfaceConfigurationService::NewStub(channel);
std::cout << "Firmware client connected to: " << server_address << std::endl;
}
解析:
- 构造函数中创建 gRPC 通道和 stub
CreateChannel():继承自BaseGrpcClient,创建 gRPC 通信通道NewStub(channel):生成 gRPC 客户端存根,用于调用远程方法
3. 连接测试
终端输出:
Testing firmware connection...
[OK] Firmware connection test succeeded
对应代码段 (simple_firmware_client.cc):
bool SimpleFirmwareClient::TestConnection() {
std::cout << "Testing firmware connection..." << std::endl;
ClientContext context;
ProductionStatus response;
FirmwareUpdateRequest request;
request.set_dut_position(0);
std::unique_ptr<ClientWriter<FirmwareUpdateRequest>> writer(
firmware_stub_->FirmwareUpdate(&context, &response));
bool write_ok = writer->Write(request);
writer->WritesDone();
Status status = writer->Finish();
return CheckStatus(status, "Firmware connection test") && write_ok;
}
gRPC 解析:
- 创建 ClientContext:包含调用元数据、截止时间等
- 创建流式写入器:
ClientWriter<FirmwareUpdateRequest>> - 写入请求:
writer->Write(request)发送测试请求 - 结束写入:
writer->WritesDone()表示流结束 - 获取状态:
writer->Finish()等待服务器响应
4. 固件升级开始
终端输出:
Starting complete firmware upgrade process...
Step 1: Uploading firmware...
Testing upload: ./TIB3_rpi_software_package.tar
File size: 137553920 bytes
对应代码段 (simple_firmware_client.cc):
bool SimpleFirmwareClient::FirmwareUpgrade(const std::string &file_path,
const std::string &product_number,
const std::string &rstate) {
std::cout << "Starting complete firmware upgrade process..." << std::endl;
std::cout << "Step 1: Uploading firmware..." << std::endl;
// 调用 UploadWithManualInfo
}
bool SimpleFirmwareClient::UploadWithManualInfo(...) {
std::cout << "Testing upload: " << file_path << std::endl;
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
size_t file_size = file.tellg();
std::cout << "File size: " << file_size << " bytes" << std::endl;
}
5. 创建软件项目信息
终端输出:
[OK] Software item sent
对应代码段 (simple_firmware_client.cc):
SoftwareItem software_item =
software_repository_test::SoftwareFileManager::CreateFromFile(
file_path, product_number, rstate, "Firmware update");
FirmwareUpdateRequest item_request;
item_request.set_dut_position(0);
*item_request.mutable_item() = software_item;
if (!writer->Write(item_request)) {
// 错误处理
}
std::cout << client_common::SUCCESS_SYMBOL << " Software item sent" << std::endl;
gRPC 解析:
- 使用 Protobuf 消息
SoftwareItem描述固件信息 mutable_item():获取 item 字段的可变引用并赋值writer->Write(item_request):发送描述信息的第一个消息
6. 分块传输数据
终端输出:
Progress: 7% (10485760/137553920 bytes)
...
Progress: 100% (137553920/137553920 bytes)
[OK] All data sent (132 chunks)
对应代码段 (simple_firmware_client.cc):
const size_t CHUNK_SIZE = client_common::FIRMWARE_CHUNK_SIZE;
std::vector<char> buffer(CHUNK_SIZE);
while (file.read(buffer.data(), CHUNK_SIZE) || file.gcount() > 0) {
size_t bytes_read = file.gcount();
FirmwareUpdateRequest content_request;
content_request.set_dut_position(0);
SoftwareItemContent *content = content_request.mutable_content();
content->set_swtype(software_item.sw_type());
content->set_data(buffer.data(), bytes_read);
if (!writer->Write(content_request)) {
// 错误处理
}
// 进度显示
if (chunk_count % client_common::PROGRESS_UPDATE_INTERVAL == 0 ||
total_sent == file_size) {
int progress = (file_size > 0) ? (total_sent * 100) / file_size : 0;
std::cout << "Progress: " << progress << "% (" << total_sent << "/"
<< file_size << " bytes)" << std::endl;
}
}
gRPC 解析:
- 流式传输:通过同一个 gRPC 流连续发送多个消息
- Chunked 传输:大文件分块发送,每块 1MB
- 数据消息:使用
SoftwareItemContent消息类型携带二进制数据
7. 传输完成
终端输出:
[OK] Firmware upload succeeded
Server response: (code: 0)
对应代码段 (simple_firmware_client.cc):
if (!writer->WritesDone()) {
std::cout << client_common::FAILURE_SYMBOL << " Failed to complete write stream" << std::endl;
return false;
}
Status status = writer->Finish();
if (!CheckStatus(status, "Firmware upload")) {
return false;
}
std::cout << "Server response: " << response.message()
<< " (code: " << response.code() << ")" << std::endl;
return response.code() == 0; // 0 means OK
gRPC 解析:
- WritesDone():通知服务器客户端已写完所有消息
- Finish():等待服务器完成处理并返回最终状态
- 响应处理:检查服务器的
ProductionStatus响应
8. 等待设备重启
终端输出:
[OK] Firmware upload completed
Step 2: Waiting for device to restart and come back online...
Waiting for device to start upgrade (2 minutes)...
Still waiting for upgrade to start... (30/120 seconds)
...
对应代码段 (simple_firmware_client.cc):
bool SimpleFirmwareClient::WaitForDeviceAndGetSWInfo(int max_retries) {
std::cout << "Waiting for device to start upgrade (2 minutes)..."
<< std::endl;
// Wait for device to start upgrade
for (int i = 0; i < client_common::DEVICE_RESTART_WAIT_SECONDS; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (i % 30 == 29) {
std::cout << "Still waiting for upgrade to start... (" << (i + 1) << "/"
<< client_common::DEVICE_RESTART_WAIT_SECONDS << " seconds)"
<< std::endl;
}
}
}
9. 轮询设备状态
终端输出:
Starting to poll GetSWInfos every 5 seconds (max 10 minutes)...
[OK] Device is back online! New firmware is running.
Current software information:
- Name: TIB3
- Product Number: CXC1744343/2-R1E-0-g2aefebe
- R-State: R1E
- Comment: Build time: 2025-05-27 05:48:17+00:00
对应代码段 (simple_firmware_client.cc):
for (int attempt = 1; attempt <= client_common::MAX_POLL_ATTEMPTS; ++attempt) {
try {
grpc::ClientContext context;
proto::raptor2::test_interface::GetSWInfoRequest request;
proto::raptor2::test_interface::SWInfos response;
grpc::Status status =
config_stub_->GetSWInfos(&context, request, &response);
if (status.ok() && response.sw_info_size() > 0) {
std::cout << client_common::SUCCESS_SYMBOL
<< " Device is back online! New firmware is running."
<< std::endl;
std::cout << "Current software information:" << std::endl;
for (const auto &sw_info : response.sw_info()) {
std::cout << " - Name: " << sw_info.sw_name() << std::endl;
std::cout << " - Product Number: " << sw_info.product_number()
<< std::endl;
std::cout << " - R-State: " << sw_info.product_rstate() << std::endl;
std::cout << " - Comment: " << sw_info.comment() << std::endl;
}
return true;
}
} catch (const std::exception &e) {
// Ignore exceptions, continue polling
}
}
gRPC 解析:
- Unary RPC 调用:
GetSWInfos是典型的请求-响应模式 - 使用 config_stub_:切换到了配置服务存根
- 错误处理:使用 try-catch 忽略异常,继续轮询
10. 最终成功输出
终端输出:
[OK] Firmware upgrade completed successfully!
[SUCCESS] Operation completed successfully!
对应代码段 (simple_firmware_client.cc):
std::cout << client_common::SUCCESS_SYMBOL
<< " Firmware upgrade completed successfully!" << std::endl;
对应代码段 (grpc_client_main.cc):
if (success) {
std::cout << std::endl
<< "[SUCCESS] Operation completed successfully!" << std::endl;
return 0;
}
gRPC 技术要点总结:
-
混合模式:
- 固件上传使用 客户端流式 RPC(streaming)
- 状态查询使用 一元 RPC(unary)
-
流式传输特点:
- 单连接多消息传输
- 支持大文件分块
- 需要明确的流结束信号
-
错误处理:
- 检查 gRPC 状态码
- 处理服务器业务逻辑错误码
- 优雅的重试机制
-
资源管理:
- 使用智能指针管理 stub 生命周期
- 正确关闭流式连接
- 合理的超时和重试配置
C++固件升级详解
TestConnection
TestConnection()
bool SimpleFirmwareClient::TestConnection() {
std::cout << "Testing firmware connection..." << std::endl;
ClientContext context;
ProductionStatus response;
FirmwareUpdateRequest request;
request.set_dut_position(0);
std::unique_ptr> writer(
firmware_stub_->FirmwareUpdate(&context, &response));
bool write_ok = writer->Write(request);
writer->WritesDone();
Status status = writer->Finish();
return CheckStatus(status, "Firmware connection test") && write_ok;
}
它的主要功能是:通过模拟一次“空”的固件升级操作,来测试客户端与服务器之间的 FirmwareUpdate 这个 gRPC 服务是否真正连通并且可用。
让我们逐行来解析它的行为:
bool SimpleFirmwareClient::TestConnection() {
// 1. 打印日志,表明测试开始
std::cout << "Testing firmware connection..." << std::endl;
// 2. 准备 gRPC 调用所需的上下文和响应对象
ClientContext context;
ProductionStatus response; // 这个对象将用来接收服务器最终的响应
// 3. 创建一个最简化的请求
FirmwareUpdateRequest request;
request.set_dut_position(0); // 甚至没有包含任何文件信息或数据块
// 4. 初始化一个客户端流式写入器 (ClientWriter)
std.unique_ptr<ClientWriter<FirmwareUpdateRequest>> writer(
firmware_stub_->FirmwareUpdate(&context, &response));
第 4 步是关键的开始。FirmwareUpdate 是一个客户端流式 (Client Streaming) RPC。调用它会返回一个 writer 对象,你可以通过这个 writer 对象向服务器发送一个或多个请求消息。
// 5. 发送这个“空”请求
bool write_ok = writer->Write(request);
这里,它向服务器发送了一个几乎不包含任何有效信息的请求。对于服务器来说,它收到了一个 FirmwareUpdate 的请求流,但这个流里只有一个非常简单的请求。
// 6. 通知服务器:“我的请求流已经发送完毕”
writer->WritesDone();
这一步非常重要。对于客户端流式 RPC,你必须显式地调用 WritesDone() 来告诉服务器,客户端这边不会再有新的请求消息了。服务器只有在收到这个信号后,才会开始处理已接收到的请求流,并准备返回它唯一的响应。
// 7. 等待并获取服务器的最终响应和状态
Status status = writer->Finish();
writer->Finish() 是一个阻塞操作。它会一直等待,直到服务器处理完所有请求并返回最终的 ProductionStatus 响应(该响应会被填充到第 2 步中创建的 response 对象里)以及一个 gRPC Status 对象。这个 Status 对象包含了这次 RPC 调用的最终状态(是否成功,如果失败了错误码和错误信息是什么)。
// 8. 返回最终的测试结果
return CheckStatus(status, "Firmware connection test") && write_ok;
}
最后,函数返回两个布尔值的“与” (&&) 结果:
CheckStatus(status, ...): 这是一个辅助函数(我们没有看到它的源码,但可以推断),它会检查 writer->Finish() 返回的 status 是否为 OK。如果 RPC 调用在网络层面或服务器处理层面失败,status 就不会是 OK。
write_ok: 检查 writer->Write(request) 是否成功。这可以捕获在写入流时发生的早期错误。
功能总结
所以,TestConnection 函数的整体逻辑是:
它打开一个到 FirmwareUpdate 服务的流。
它只往这个流里发送了一个最小化的、几乎为空的请求。
它立即关闭这个流 (WritesDone())。
它等待服务器对这个“空操作”的响应。
这个函数的核心目的,不是为了传输任何数据,而是为了验证从客户端到服务器的整个 gRPC 调用链路是通畅的。
如果这个函数能够返回 true,那就证明了:
客户端可以成功连接到服务器。
客户端有权限调用 FirmwareUpdate 服务。
服务器端的 FirmwareUpdate 服务正在正常运行,并且能够处理请求。
这是一种非常轻量级且可靠的“健康检查”或“连通性测试”方法,特别适用于验证流式 RPC 服务的可用性。
UploadWithManualInfo
UploadWithManualInfo
bool SimpleFirmwareClient::UploadWithManualInfo(
const std::string &file_path, const std::string &product_number,
const std::string &rstate) {
std::cout << "Testing upload: " << file_path << std::endl;
// Check file
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
std::cout << client_common::FAILURE_SYMBOL << " Cannot open file: " << file_path << std::endl;
return false;
}
size_t file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::cout << "File size: " << file_size << " bytes" << std::endl;
// Prepare software item info
SoftwareItem software_item =
software_repository_test::SoftwareFileManager::CreateFromFile(
file_path, product_number, rstate, "Firmware update");
if (software_item.hash().empty()) {
std::cout << client_common::FAILURE_SYMBOL << " Failed to create software item" << std::endl;
return false;
}
// Create gRPC stream
ClientContext context;
ProductionStatus response;
std::unique_ptr> writer(
firmware_stub_->FirmwareUpdate(&context, &response));
// 1. Send software item info
FirmwareUpdateRequest item_request;
item_request.set_dut_position(0);
*item_request.mutable_item() = software_item;
if (!writer->Write(item_request)) {
std::cout << client_common::FAILURE_SYMBOL << " Failed to send software item" << std::endl;
return false;
}
std::cout << client_common::SUCCESS_SYMBOL << " Software item sent" << std::endl;
// 2. Send file data
const size_t CHUNK_SIZE = client_common::FIRMWARE_CHUNK_SIZE;
std::vector buffer(CHUNK_SIZE);
size_t total_sent = 0;
size_t chunk_count = 0;
while (file.read(buffer.data(), CHUNK_SIZE) || file.gcount() > 0) {
size_t bytes_read = file.gcount();
FirmwareUpdateRequest content_request;
content_request.set_dut_position(0);
SoftwareItemContent *content = content_request.mutable_content();
content->set_swtype(software_item.sw_type());
content->set_data(buffer.data(), bytes_read);
if (!writer->Write(content_request)) {
std::cout << client_common::FAILURE_SYMBOL << " Failed to send chunk " << chunk_count << std::endl;
return false;
}
total_sent += bytes_read;
chunk_count++;
// Show progress
if (chunk_count % client_common::PROGRESS_UPDATE_INTERVAL == 0 ||
total_sent == file_size) {
int progress = (file_size > 0) ? (total_sent * 100) / file_size : 0;
std::cout << "Progress: " << progress << "% (" << total_sent << "/"
<< file_size << " bytes)" << std::endl;
}
}
std::cout << client_common::SUCCESS_SYMBOL << " All data sent (" << chunk_count << " chunks)" << std::endl;
// 3. Complete transmission
if (!writer->WritesDone()) {
std::cout << client_common::FAILURE_SYMBOL << " Failed to complete write stream" << std::endl;
return false;
}
Status status = writer->Finish();
if (!CheckStatus(status, "Firmware upload")) {
return false;
}
std::cout << "Server response: " << response.message()
<< " (code: " << response.code() << ")" << std::endl;
return response.code() == 0; // 0 means OK
}
在基于 gRPC 的固件升级系统中,客户端如何将一个大文件可靠地传输到服务器是核心挑战之一。客户端流式 RPC(Client Streaming RPC)为此提供了完美的解决方案:客户端通过一个流发送多个请求消息(文件分块),服务器接收完毕后返回一个响应。
函数签名与上下文
bool SimpleFirmwareClient::UploadWithManualInfo(
const std::string &file_path,
const std::string &product_number,
const std::string &rstate)
作用:接收本地文件路径及产品元信息,将文件分块并通过 gRPC 客户端流式 RPC 发送到服务器。
执行流程分解
第 1 步:文件准备与校验(前期工作)
// Check file
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
// ... 文件打开失败处理 ...
return false;
}
size_t file_size = file.tellg(); // 获取文件大小
file.seekg(0, std::ios::beg); // 将文件指针移回文件开头
// Prepare software item info
SoftwareItem software_item =
software_repository_test::SoftwareFileManager::CreateFromFile(
file_path, product_number, rstate, "Firmware update");
if (software_item.hash().empty()) {
// ... 创建软件元数据失败处理 ...
return false;
}
解析:
- 以二进制方式打开文件,
std::ios::ate使指针初始位于文件末尾,以便直接用tellg()获取文件大小。 - 获取大小后立即将指针
seekg回开头,为后续读取做准备。 - 调用
SoftwareFileManager::CreateFromFile创建SoftwareItem对象,该对象封装了固件的元数据:- 产品号(
product_number) - 版本(
rstate) - 文件名、大小、哈希值(MD5/SHA,用于服务器校验)
- 产品号(
- 若哈希值为空,说明元数据生成失败,直接返回。
第 2 步:初始化 gRPC 流(建立通信)
// Create gRPC stream
ClientContext context;
ProductionStatus response;
std::unique_ptr<ClientWriter<FirmwareUpdateRequest>> writer(
firmware_stub_->FirmwareUpdate(&context, &response));
解析:
ClientContext用于传递元数据、超时等 RPC 上下文信息。firmware_stub_->FirmwareUpdate是客户端流式 RPC 的入口,它返回一个ClientWriter智能指针。writer对象成为我们向服务器发送请求流的唯一通道,最终服务器返回的ProductionStatus会填充到response中。
第 3 步:发送元数据(流的第一个消息)
// 1. Send software item info
FirmwareUpdateRequest item_request;
item_request.set_dut_position(0);
*item_request.mutable_item() = software_item; // 将元数据对象放入请求中
if (!writer->Write(item_request)) {
// ... 发送失败处理 ...
return false;
}
解析:
- 创建第一个
FirmwareUpdateRequest消息,不携带文件数据,而是填充software_item元数据。 - 通过
writer->Write()发送该消息。 - 这一步至关重要:它提前通知服务器即将上传的文件的元信息,让服务器可以预分配资源、验证权限或拒绝不合规的上传。
第 4 步:循环发送文件数据块(流的核心)
// 2. Send file data
const size_t CHUNK_SIZE = client_common::FIRMWARE_CHUNK_SIZE;
std::vector<char> buffer(CHUNK_SIZE);
while (file.read(buffer.data(), CHUNK_SIZE) || file.gcount() > 0) {
size_t bytes_read = file.gcount();
FirmwareUpdateRequest content_request;
// ...
SoftwareItemContent *content = content_request.mutable_content();
content->set_data(buffer.data(), bytes_read); // 将读取到的数据块放入请求
if (!writer->Write(content_request)) {
// ... 发送失败处理 ...
return false;
}
// ... 更新并显示上传进度 ...
}
解析:
- 定义固定大小的缓冲区
CHUNK_SIZE(例如 64KB)。 while循环条件file.read(...) || file.gcount() > 0确保了即使最后一次读取不足CHUNK_SIZE也能正确处理。- 每次循环:
- 从文件读取一块数据到
buffer。 - 创建一个新的
FirmwareUpdateRequest消息,并通过mutable_content()获取SoftwareItemContent子消息。 - 将读取到的数据块通过
set_data放入消息。 - 调用
writer->Write(content_request)发送该数据块。 - 累计已发送字节数,并定期输出上传进度(代码中省略了进度计算部分,但实际工程中会包含)。
- 从文件读取一块数据到
- 该循环持续进行,直到整个文件被分块发送完毕。
第 5 步:结束流并获取响应(完成通信)
// 3. Complete transmission
if (!writer->WritesDone()) {
// ...
return false;
}
Status status = writer->Finish();
if (!CheckStatus(status, "Firmware upload")) {
return false;
}
// ...
return response.code() == 0; // 0 means OK
解析:
writer->WritesDone()通知服务器客户端已经发送完所有请求消息。此操作会发送一个半关闭信号,服务器据此知晓流已结束。- 调用
writer->Finish()阻塞等待服务器的最终响应。服务器在处理完所有数据块后,会返回一个ProductionStatus消息(填充到之前传入的response对象中)。 - 先检查 gRPC 层的状态
status是否 OK(例如网络错误、超时等),再检查服务器返回的业务逻辑状态码response.code()是否为 0(通常 0 表示成功)。 - 只有两者都成功,函数才返回
true,表示固件上传成功。
功能总结
UploadWithManualInfo 函数是一个典型的 gRPC 客户端流式 RPC 实现,它将一个大文件拆分为:
- 一个携带元数据的消息(文件信息、哈希值等)
- N 个携带实际数据的消息(文件分块)
通过单一的 gRPC 流按顺序发送,最后等待服务器的确认响应。其核心设计思想与 Python 版本(使用生成器 yield)完全一致,但 C++ 的 API 更加显式,需要手动控制 Write、WritesDone 和 Finish,体现了 C++ 注重细粒度控制的特点。
这种模式不仅适用于固件升级,也广泛用于日志收集、大数据上传、实时视频流传输等场景,是 gRPC 高性能通信能力的重要体现。
FirmwareUpgrade
FirmwareUpgrade
bool SimpleFirmwareClient::FirmwareUpgrade(const std::string &file_path,
const std::string &product_number,
const std::string &rstate) {
std::cout << "Starting complete firmware upgrade process..." << std::endl;
// 1. Execute firmware upload
std::cout << "Step 1: Uploading firmware..." << std::endl;
if (!UploadWithManualInfo(file_path, product_number, rstate)) {
PrintResult(false, "Firmware upload");
return false;
}
std::cout << client_common::SUCCESS_SYMBOL << " Firmware upload completed" << std::endl;
std::cout << "Step 2: Waiting for device to restart and come back online..."
<< std::endl;
// 2. Wait for device to reboot and come back online
if (!WaitForDeviceAndGetSWInfo()) {
std::cout
<< client_common::FAILURE_SYMBOL << " Device did not come back online or failed to get software info"
<< std::endl;
return false;
}
std::cout << client_common::SUCCESS_SYMBOL << " Firmware upgrade completed successfully!" << std::endl;
return true;
}
在构建一个完整的固件升级功能时,仅仅实现文件上传是不够的。上传完成后,设备需要重启并加载新固件,客户端必须等待设备恢复并验证新版本。如何将这两个核心步骤有序地串联起来,正是 FirmwareUpgrade 函数的职责。
函数签名与作用
bool SimpleFirmwareClient::FirmwareUpgrade(
const std::string &file_path,
const std::string &product_number,
const std::string &rstate)
作用:编排并执行一个完整的固件升级流程,确保“上传”和“重启后验证”这两个关键业务步骤按序、正确地完成。
执行步骤分解
第 1 步:执行固件上传
// 1. Execute firmware upload
std::cout << "Step 1: Uploading firmware..." << std::endl;
if (!UploadWithManualInfo(file_path, product_number, rstate)) {
PrintResult(false, "Firmware upload");
return false;
}
解析:
- 函数首先调用我们先前深入分析过的
UploadWithManualInfo方法,该方法封装了所有文件分块、gRPC 流式上传的细节。 - 目的:完成将固件文件从客户端可靠地传输到服务器。
- 流程控制:检查
UploadWithManualInfo的返回值。若上传失败(返回false),立即打印失败信息并返回false,整个升级流程中止。这体现了失败快速返回的原则。
第 2 步:等待设备重启并获取软件信息
// 2. Wait for device to reboot and come back online
if (!WaitForDeviceAndGetSWInfo()) {
std::cout
<< client_common::FAILURE_SYMBOL << " Device did not come back online or failed to get software info"
<< std::endl;
return false;
}
解析:
- 只有在第 1 步成功的前提下,程序才会执行到此。它调用
WaitForDeviceAndGetSWInfo方法,该方法负责轮询设备状态,直到设备重启完毕且能够成功获取新的软件信息。 - 目的:处理固件上传后最关键的等待与验证阶段,确保新固件已生效。
- 流程控制:检查
WaitForDeviceAndGetSWInfo的返回值。若在规定时间内设备未上线或信息获取失败(返回false),同样视为升级失败,函数返回false。
第 3 步:宣布成功
std::cout << client_common::SUCCESS_SYMBOL << " Firmware upgrade completed successfully!" << std::endl;
return true;
解析:
- 只有当“上传”和“等待验证”两个步骤都成功完成,才能执行到此。
- 目的:输出最终的成功信息,并返回
true给调用者,表明整个端到端的固件升级流程已圆满完成。
功能总结:关注点分离的设计典范
FirmwareUpgrade 函数完美体现了软件工程中关注点分离 (Separation of Concerns) 的设计原则:
- 它不关心底层细节:文件如何分块、gRPC 流如何建立、网络错误如何处理、轮询超时如何计算——这些技术细节全部被封装在
UploadWithManualInfo和WaitForDeviceAndGetSWInfo这两个独立的方法中。 - 它只关心高级业务流程:其唯一职责就是定义“一个完整的固件升级 = 成功上传 + 成功等待验证”,并按顺序编排这两个步骤。
这种设计带来的好处:
- 代码清晰易读:函数体只有寥寥几行,但任何人一眼就能看懂固件升级的整体流程。
- 易于维护:如果需要修改上传逻辑(例如改变分块大小),只需修改
UploadWithManualInfo;如果需要调整等待策略(例如轮询间隔),只需修改WaitForDeviceAndGetSWInfo,两者互不影响。 - 便于测试:可以分别针对上传、等待验证编写单元测试,然后针对流程编排编写集成测试。
WaitForDeviceAndGetSWInfo
WaitForDeviceAndGetSWInfo
bool SimpleFirmwareClient::WaitForDeviceAndGetSWInfo(int max_retries) {
std::cout << "Waiting for device to start upgrade (2 minutes)..."
<< std::endl;
// Wait for device to start upgrade
for (int i = 0; i < client_common::DEVICE_RESTART_WAIT_SECONDS; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (i % 30 == 29) {
std::cout << "Still waiting for upgrade to start... (" << (i + 1) << "/"
<< client_common::DEVICE_RESTART_WAIT_SECONDS << " seconds)"
<< std::endl;
}
}
std::cout << "Starting to poll GetSWInfos every 5 seconds (max 10 minutes)..."
<< std::endl;
// Poll GetSWInfos every 5 seconds for max 10 minutes
for (int attempt = 1; attempt <= client_common::MAX_POLL_ATTEMPTS;
++attempt) {
try {
grpc::ClientContext context;
proto::raptor2::test_interface::GetSWInfoRequest request;
proto::raptor2::test_interface::SWInfos response;
grpc::Status status =
config_stub_->GetSWInfos(&context, request, &response);
if (status.ok() && response.sw_info_size() > 0) {
std::cout << client_common::SUCCESS_SYMBOL << " Device is back online! New firmware is running."
<< std::endl;
std::cout << "Current software information:" << std::endl;
for (const auto &sw_info : response.sw_info()) {
std::cout << " - Name: " << sw_info.sw_name() << std::endl;
std::cout << " - Product Number: " << sw_info.product_number()
<< std::endl;
std::cout << " - R-State: " << sw_info.product_rstate() << std::endl;
std::cout << " - Comment: " << sw_info.comment() << std::endl;
}
return true;
}
} catch (const std::exception &e) {
// Ignore exceptions, continue polling
}
if (attempt % client_common::PROGRESS_LOG_INTERVAL == 0) {
std::cout << "Still waiting for device response... (attempt " << attempt
<< "/" << client_common::MAX_POLL_ATTEMPTS << ")" << std::endl;
}
std::this_thread::sleep_for(
std::chrono::seconds(client_common::POLLING_INTERVAL_SECONDS));
}
std::cout << client_common::FAILURE_SYMBOL << " Device did not respond after 10 minutes of polling"
<< std::endl;
return false;
}
C++ 客户端中的 WaitForDeviceAndGetSWInfo 函数正是为解决这一难题而生。它扮演着 “耐心验证者” 的角色,采用一种健壮的 “盲等 + 主动轮询” 策略,优雅地应对了设备重启的不确定性。
函数签名与作用
bool SimpleFirmwareClient::WaitForDeviceAndGetSWInfo()
作用:在固件上传成功后,持续等待设备完成重启,并通过获取软件信息的方式验证设备已恢复正常运行。若成功,则打印新固件信息并返回 true;若超时仍未成功,返回 false。
执行阶段分解
阶段一:初始盲等 (Giving the device a head start)
// Wait for device to start upgrade
for (int i = 0; i < client_common::DEVICE_RESTART_WAIT_SECONDS; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
// 可选的进度打印代码(省略)
}
行为:函数一开始便无条件暂停执行 2 分钟(DEVICE_RESTART_WAIT_SECONDS 通常为 120)。在这段时间内,客户端不做任何通信尝试,只是每秒休眠一次。
设计意图:这基于一个合理的业务假设——服务器在接收完固件后,需要时间完成内部准备工作(如校验文件、解压、写入存储)并触发重启流程。在这期间,服务器可能已经关闭了 gRPC 服务或网络栈,任何连接尝试都注定失败。与其徒劳地“撞墙”,不如主动让出时间窗口,给服务器留出充足的启动时间。这种“盲等”策略避免了不必要的错误日志和资源消耗。
阶段二:主动轮询 (Actively checking for signs of life)
// Poll GetSWInfos every 5 seconds for max 10 minutes
for (int attempt = 1; attempt <= client_common::MAX_POLL_ATTEMPTS; ++attempt) {
try {
grpc::ClientContext context;
proto::raptor2::test_interface::GetSWInfoRequest request;
proto::raptor2::test_interface::SWInfos response;
grpc::Status status =
config_stub_->GetSWInfos(&context, request, &response);
if (status.ok() && response.sw_info_size() > 0) {
std::cout << client_common::SUCCESS_SYMBOL
<< " Device is back online! New firmware is running."
<< std::endl;
// 打印新软件信息...
return true;
}
} catch (const std::exception &e) {
// 忽略异常,继续轮询
}
std::this_thread::sleep_for(std::chrono::seconds(5));
}
行为:2 分钟盲等结束后,函数进入一个最长持续 10 分钟的轮询循环。每次循环:
- 发起 RPC 调用:通过
config_stub_->GetSWInfos向服务器请求当前的软件信息。 - 检查成功条件:只有同时满足 gRPC 状态为 OK 且 响应中包含至少一条软件信息 时,才认为设备已完全恢复。此时函数打印新固件详情并返回
true。 - 静默忽略失败:在设备重启完成前,RPC 调用几乎必然失败(连接拒绝、超时、服务不可用等)。代码通过
try-catch块捕获所有异常,并对status非 OK 的情况不做任何处理——所有失败都被刻意忽略。这不是疏忽,而是一种容错策略:它视失败为重启过程中的常态,唯一的追求是等待那个最终的成功。 - 等待后重试:每次尝试后,无论成功与否,线程都暂停 5 秒再进行下一轮尝试。
轮询参数:
- 重试间隔:5 秒(兼顾响应及时性与网络压力)
- 最大尝试次数:通常为 120 次(10 分钟 ÷ 5 秒)。若 10 分钟后仍未成功,函数放弃并返回
false。
功能总结:衔接上传与成功的桥梁
WaitForDeviceAndGetSWInfo 函数的核心价值在于解决了一个分布式系统中的经典问题:如何在一个不确定的时间窗口内,验证一个正在重启的远程服务是否已恢复正常。
它通过以下设计完美地衔接了“固件上传”与“升级成功”这两个状态:
- 容错性:无视中间的所有失败,只关注最终的成功。
- 资源友好:初始盲等减少了无效请求,轮询间隔避免了高频冲击。
- 确定性:最大等待时间(10 分钟)保证了流程不会无限期阻塞。
正是这种“耐心验证者”的角色,使得整个固件升级流程能够自动化、可靠地完成。如果没有它,上传固件后客户端将无法知晓何时该继续下一步(如触发测试或通知用户),整个端到端流程就会断裂。
gRPC python客户端
在这篇技术博文中,我们将深入剖析一个在实现 gRPC 固件升级流程中遇到的典型问题:当服务器被强制重启后,客户端为何无法通过轮询(Polling)重新建立联系? 我们不仅会揭示问题背后的 gRPC 连接管理机制,还将展示如何通过优秀的代码结构设计,将一个复杂、臃肿的函数重构为清晰、健壮、可维护的模块。
场景设定:固件升级
我们的目标是构建一个 Python 客户端,通过 gRPC 实现对嵌入式设备的固件升级。其核心流程分为两个主要阶段:
- 文件上传:客户端使用 gRPC 客户端流式(Client Streaming)RPC,将一个数 MB 大小的固件文件高效地传输到服务器。
- 状态验证:服务器在接收文件后会执行重启以应用更新。客户端必须进入一个轮询循环,使用一个一元(Unary)RPC 反复查询设备状态,直到确认新固件已成功运行。
gRPC 核心组件回顾
在深入问题之前,让我们快速回顾一下本次场景中涉及的关键 gRPC 组件:
- Channel (
grpc.insecure_channel): 这是客户端与服务器端点之间的虚拟连接。它封装了底层的 TCP 连接、重连策略和所有复杂的网络细节。一个 Channel 对象可以被多个 Stub 复用,它的创建和销毁相对昂贵。 - Stub (
YourService_pb2_grpc.YourServiceStub): 这是从.proto文件自动生成的客户端代理。我们通过调用 Stub 对象上的方法,来发起对远端服务的 RPC 调用。Stub 本身是轻量级的,它必须依附于一个 Channel 才能工作。 - 客户端流式 RPC: 允许客户端向服务器发送一系列消息。在我们的案例中,客户端将固件文件切分成数据块(chunks),像流水一样持续发送给服务器。这通过在客户端提供一个迭代器(iterator) 来实现。
- 一元 RPC: 最简单的 RPC 类型,客户端发送一个请求,服务器返回一个响应。非常适合用于状态查询。
问题的浮现:无法唤醒的“僵尸连接”
我们的初步实现遵循了一个看似合理的逻辑:在客户端启动时创建一个长生命周期的 Channel 和 Stub,并在整个升级流程中复用它们。
然而,流程总是在第二阶段失败。尽管服务器已经重启完毕,但客户端的轮询请求永远得不到响应,最终因超时而失败。独立的测试脚本(每次都创建新连接)却能成功,这让我们将疑点锁定在了连接的复用上。
技术层面的根本原因:
当服务器执行 reboot 时,它粗暴地中断了底层的 TCP 连接,并不会发送一个标准的 FIN 报文来优雅地关闭连接。对于客户端的 gRPC Channel 来说,它持有的网络连接突然“人间蒸发”了。
此时,这个 Channel 对象就进入了一种不稳定的“僵尸状态”。它内部的状态机可能还认为连接是活跃的,或者正在尝试重连一个已经失效的旧套接字(socket)。当我们的轮询逻辑立即开始工作,并试图复用这个“僵尸 Channel”时,所有的 RPC 请求都注定失败,因为它们无法通过这个已经损坏的“桥梁”到达服务器。
解决方案:在轮询中创建“临时”连接
既然长生命周期的连接在服务器重启后变得不可信,那么最可靠的策略就是抛弃它。在每次需要确认服务器状态时,我们都应该创建一个全新的、短暂的连接。
这正是我们对轮询函数 _wait_for_device_and_get_sw_info 进行的核心改造:
# 优化后的轮询函数
def _wait_for_device_and_get_sw_info(self) -> bool:
# ... (其他逻辑) ...
for attempt in range(self.MAX_POLL_ATTEMPTS):
try:
# --- 核心修复:在每次循环中创建全新的临时 Channel 和 Stub ---
with grpc.insecure_channel(self.server_address) as channel:
temp_stub = configuration_service_pb2_grpc.TestInterfaceConfigurationServiceStub(channel)
request = empty_pb2.Empty()
# 使用这个全新的、干净的连接发起请求
response = temp_stub.GetSWInfos(request, timeout=self.DEFAULT_TIMEOUT)
self.logger.info("✓ Device is online and software info confirmed.")
return True
except grpc.RpcError:
# 在此阶段,连接失败是预期之中的,我们安静地等待下一次尝试
pass
time.sleep(self.POLL_INTERVAL_SECONDS)
# ... (超时逻辑) ...
with grpc.insecure_channel(...) 语句确保了在每次轮询尝试中,我们都建立了一个全新的 TCP 连接。请求结束后,该连接被立即、安全地关闭。这种“阅后即焚”的模式,完美地解决了“僵尸连接”问题。
从混乱到清晰:代码结构的重构之旅
在修复核心 Bug 之后,我们审视了代码结构,发现 firmware_upgrade 函数过于臃肿,违反了单一职责原则(Single Responsibility Principle)。一个好的函数应该只做一件事,并把它做好。
重构前的结构:一个“大泥球”
def firmware_upgrade(self, file_path: str) -> bool:
# 1. 打开文件,获取文件大小
# 2. 创建 tqdm 进度条
# 3. 定义一个嵌套的 request_iterator 函数用于 gRPC 流
# 4. 调用 gRPC 流式上传方法
# 5. 处理上传响应
# 6. 如果上传成功,调用轮询函数
# 7. 处理轮询结果
# 8. 返回最终状态
# 9. 混合了大量的异常处理
这种结构难以阅读、测试和维护。
重构后的结构:职责分明的“经理”与“专家”
我们将其重构为三个职责分明的函数,如同一个分工明确的团队:
-
_send_firmware_stream(流上传专家)- 单一职责:专门负责处理 gRPC 客户端流式 RPC 的所有细节。
- 实现:内部包含
request_iterator生成器,管理文件块的读取和tqdm进度条的更新。它只关心如何高效地把文件发送出去,并返回一个简单的成功或失败标志。
-
_wait_for_device_and_get_sw_info(轮询专家)- 单一职责:专门负责在服务器重启后,通过创建临时连接进行轮询,直到确认设备上线或超时。
- 实现:包含了我们解决“僵尸连接”问题的核心逻辑。
-
firmware_upgrade(项目经理)- 单一职责:作为高层协调器,编排整个升级流程。
- 实现:它的代码变得像一份清晰的计划书,极具可读性:
def firmware_upgrade(self, file_path: str) -> bool: self.logger.info(f"Starting firmware upgrade with file: {file_path}") try: # 步骤 1: 委托“上传专家”发送文件 upload_success = self._send_firmware_stream(file_path) if not upload_success: return False # 步骤 2: 委托“轮询专家”确认设备状态 self.logger.info("Upload complete. Waiting for device...") device_online = self._wait_for_device_and_get_sw_info() # 报告最终结果 if device_online: self.logger.info("Firmware upgrade completed successfully!") else: self.logger.error("Device did not come back online.") return device_online except Exception: # “经理”只处理流程中的顶层异常 self.logger.exception("An unexpected error occurred during the upgrade process.") return False
通过这次重构,代码的可读性、可测试性和可维护性都得到了质的飞跃。
结论
本次实践带给我们两个核心启示:
- gRPC 连接管理:在涉及服务强制重启的场景下,必须对客户端持有的长连接状态保持警惕。最健壮的策略是在状态不可信时果断抛弃旧连接,并在需要时创建新的临时连接来完成任务。
- 代码结构设计:遵循单一职责原则进行重构,将一个复杂的流程拆分为多个职责单一的函数,是提升代码质量的必经之路。它不仅让代码更易于理解,也使得像“僵尸连接”这样的复杂问题更容易被定位和修复。
从一个棘手的 Bug 出发,我们最终收获的不仅是一个能正常工作的功能,更是一套经过优化的、高质量的代码库,以及对 gRPC 客户端设计的更深理解。
ezhuzix@CNNJ002931:/mnt/c/Users/ezhuzix/repo_o/nib_application/grpc$ python3 simple_firmware_client.py upgrade 192.168.2.61:50050 TIB3_rpi_software_package.tar "CXC1744343/2" R1F
[SUCCESS] 导入 protobuf 模块成功
准备执行完整固件升级:
文件: TIB3_rpi_software_package.tar
大小: 137,553,920 字节
产品: CXC1744343/2
状态: R1F
位置: 0
最大轮询: 120次 (约10.0分钟)
确认执行完整升级流程? (y/N): y
[DEBUG] 连接到 192.168.2.61:50050
[OK] Firmware client connected to: 192.168.2.61:50050
[DEBUG] 测试固件连接...
[OK] Firmware connection test succeeded
Starting complete firmware upgrade process...
============================================================
Step 1: Uploading firmware...
============================================================
Testing upload: TIB3_rpi_software_package.tar
File size: 137553920 bytes
[OK] Software item sent
Progress: 0% (655360/137553920 bytes)
Progress: 0% (1310720/137553920 bytes)
Progress: 1% (1966080/137553920 bytes)
Progress: 1% (2621440/137553920 bytes)
Progress: 2% (3276800/137553920 bytes)
Progress: 2% (3932160/137553920 bytes)
Progress: 3% (4587520/137553920 bytes)
Progress: 3% (5242880/137553920 bytes)
Progress: 4% (5898240/137553920 bytes)
Progress: 4% (6553600/137553920 bytes)
Progress: 5% (7208960/137553920 bytes)
Progress: 5% (7864320/137553920 bytes)
Progress: 6% (8519680/137553920 bytes)
Progress: 6% (9175040/137553920 bytes)
Progress: 7% (9830400/137553920 bytes)
Progress: 7% (10485760/137553920 bytes)
Progress: 8% (11141120/137553920 bytes)
Progress: 8% (11796480/137553920 bytes)
Progress: 9% (12451840/137553920 bytes)
Progress: 9% (13107200/137553920 bytes)
Progress: 10% (13762560/137553920 bytes)
Progress: 10% (14417920/137553920 bytes)
Progress: 10% (15073280/137553920 bytes)
Progress: 11% (15728640/137553920 bytes)
Progress: 11% (16384000/137553920 bytes)
Progress: 12% (17039360/137553920 bytes)
Progress: 12% (17694720/137553920 bytes)
Progress: 13% (18350080/137553920 bytes)
Progress: 13% (19005440/137553920 bytes)
Progress: 14% (19660800/137553920 bytes)
Progress: 14% (20316160/137553920 bytes)
Progress: 15% (20971520/137553920 bytes)
Progress: 15% (21626880/137553920 bytes)
Progress: 16% (22282240/137553920 bytes)
Progress: 16% (22937600/137553920 bytes)
Progress: 17% (23592960/137553920 bytes)
Progress: 17% (24248320/137553920 bytes)
Progress: 18% (24903680/137553920 bytes)
Progress: 18% (25559040/137553920 bytes)
Progress: 19% (26214400/137553920 bytes)
Progress: 19% (26869760/137553920 bytes)
Progress: 20% (27525120/137553920 bytes)
Progress: 20% (28180480/137553920 bytes)
Progress: 20% (28835840/137553920 bytes)
Progress: 21% (29491200/137553920 bytes)
Progress: 21% (30146560/137553920 bytes)
Progress: 22% (30801920/137553920 bytes)
Progress: 22% (31457280/137553920 bytes)
Progress: 23% (32112640/137553920 bytes)
Progress: 23% (32768000/137553920 bytes)
Progress: 24% (33423360/137553920 bytes)
Progress: 24% (34078720/137553920 bytes)
Progress: 25% (34734080/137553920 bytes)
Progress: 25% (35389440/137553920 bytes)
Progress: 26% (36044800/137553920 bytes)
Progress: 26% (36700160/137553920 bytes)
Progress: 27% (37355520/137553920 bytes)
Progress: 27% (38010880/137553920 bytes)
Progress: 28% (38666240/137553920 bytes)
Progress: 28% (39321600/137553920 bytes)
Progress: 29% (39976960/137553920 bytes)
Progress: 29% (40632320/137553920 bytes)
Progress: 30% (41287680/137553920 bytes)
Progress: 30% (41943040/137553920 bytes)
Progress: 30% (42598400/137553920 bytes)
Progress: 31% (43253760/137553920 bytes)
Progress: 31% (43909120/137553920 bytes)
Progress: 32% (44564480/137553920 bytes)
Progress: 32% (45219840/137553920 bytes)
Progress: 33% (45875200/137553920 bytes)
Progress: 33% (46530560/137553920 bytes)
Progress: 34% (47185920/137553920 bytes)
Progress: 34% (47841280/137553920 bytes)
Progress: 35% (48496640/137553920 bytes)
Progress: 35% (49152000/137553920 bytes)
Progress: 36% (49807360/137553920 bytes)
Progress: 36% (50462720/137553920 bytes)
Progress: 37% (51118080/137553920 bytes)
Progress: 37% (51773440/137553920 bytes)
Progress: 38% (52428800/137553920 bytes)
Progress: 38% (53084160/137553920 bytes)
Progress: 39% (53739520/137553920 bytes)
Progress: 39% (54394880/137553920 bytes)
Progress: 40% (55050240/137553920 bytes)
Progress: 40% (55705600/137553920 bytes)
Progress: 40% (56360960/137553920 bytes)
Progress: 41% (57016320/137553920 bytes)
Progress: 41% (57671680/137553920 bytes)
Progress: 42% (58327040/137553920 bytes)
Progress: 42% (58982400/137553920 bytes)
Progress: 43% (59637760/137553920 bytes)
Progress: 43% (60293120/137553920 bytes)
Progress: 44% (60948480/137553920 bytes)
Progress: 44% (61603840/137553920 bytes)
Progress: 45% (62259200/137553920 bytes)
Progress: 45% (62914560/137553920 bytes)
Progress: 46% (63569920/137553920 bytes)
Progress: 46% (64225280/137553920 bytes)
Progress: 47% (64880640/137553920 bytes)
Progress: 47% (65536000/137553920 bytes)
Progress: 48% (66191360/137553920 bytes)
Progress: 48% (66846720/137553920 bytes)
Progress: 49% (67502080/137553920 bytes)
Progress: 49% (68157440/137553920 bytes)
Progress: 50% (68812800/137553920 bytes)
Progress: 50% (69468160/137553920 bytes)
Progress: 50% (70123520/137553920 bytes)
Progress: 51% (70778880/137553920 bytes)
Progress: 51% (71434240/137553920 bytes)
Progress: 52% (72089600/137553920 bytes)
Progress: 52% (72744960/137553920 bytes)
Progress: 53% (73400320/137553920 bytes)
Progress: 53% (74055680/137553920 bytes)
Progress: 54% (74711040/137553920 bytes)
Progress: 54% (75366400/137553920 bytes)
Progress: 55% (76021760/137553920 bytes)
Progress: 55% (76677120/137553920 bytes)
Progress: 56% (77332480/137553920 bytes)
Progress: 56% (77987840/137553920 bytes)
Progress: 57% (78643200/137553920 bytes)
Progress: 57% (79298560/137553920 bytes)
Progress: 58% (79953920/137553920 bytes)
Progress: 58% (80609280/137553920 bytes)
Progress: 59% (81264640/137553920 bytes)
Progress: 59% (81920000/137553920 bytes)
Progress: 60% (82575360/137553920 bytes)
Progress: 60% (83230720/137553920 bytes)
Progress: 60% (83886080/137553920 bytes)
Progress: 61% (84541440/137553920 bytes)
Progress: 61% (85196800/137553920 bytes)
Progress: 62% (85852160/137553920 bytes)
Progress: 62% (86507520/137553920 bytes)
Progress: 63% (87162880/137553920 bytes)
Progress: 63% (87818240/137553920 bytes)
Progress: 64% (88473600/137553920 bytes)
Progress: 64% (89128960/137553920 bytes)
Progress: 65% (89784320/137553920 bytes)
Progress: 65% (90439680/137553920 bytes)
Progress: 66% (91095040/137553920 bytes)
Progress: 66% (91750400/137553920 bytes)
Progress: 67% (92405760/137553920 bytes)
Progress: 67% (93061120/137553920 bytes)
Progress: 68% (93716480/137553920 bytes)
Progress: 68% (94371840/137553920 bytes)
Progress: 69% (95027200/137553920 bytes)
Progress: 69% (95682560/137553920 bytes)
Progress: 70% (96337920/137553920 bytes)
Progress: 70% (96993280/137553920 bytes)
Progress: 70% (97648640/137553920 bytes)
Progress: 71% (98304000/137553920 bytes)
Progress: 71% (98959360/137553920 bytes)
Progress: 72% (99614720/137553920 bytes)
Progress: 72% (100270080/137553920 bytes)
Progress: 73% (100925440/137553920 bytes)
Progress: 73% (101580800/137553920 bytes)
Progress: 74% (102236160/137553920 bytes)
Progress: 74% (102891520/137553920 bytes)
Progress: 75% (103546880/137553920 bytes)
Progress: 75% (104202240/137553920 bytes)
Progress: 76% (104857600/137553920 bytes)
Progress: 76% (105512960/137553920 bytes)
Progress: 77% (106168320/137553920 bytes)
Progress: 77% (106823680/137553920 bytes)
Progress: 78% (107479040/137553920 bytes)
Progress: 78% (108134400/137553920 bytes)
Progress: 79% (108789760/137553920 bytes)
Progress: 79% (109445120/137553920 bytes)
Progress: 80% (110100480/137553920 bytes)
Progress: 80% (110755840/137553920 bytes)
Progress: 80% (111411200/137553920 bytes)
Progress: 81% (112066560/137553920 bytes)
Progress: 81% (112721920/137553920 bytes)
Progress: 82% (113377280/137553920 bytes)
Progress: 82% (114032640/137553920 bytes)
Progress: 83% (114688000/137553920 bytes)
Progress: 83% (115343360/137553920 bytes)
Progress: 84% (115998720/137553920 bytes)
Progress: 84% (116654080/137553920 bytes)
Progress: 85% (117309440/137553920 bytes)
Progress: 85% (117964800/137553920 bytes)
Progress: 86% (118620160/137553920 bytes)
Progress: 86% (119275520/137553920 bytes)
Progress: 87% (119930880/137553920 bytes)
Progress: 87% (120586240/137553920 bytes)
Progress: 88% (121241600/137553920 bytes)
Progress: 88% (121896960/137553920 bytes)
Progress: 89% (122552320/137553920 bytes)
Progress: 89% (123207680/137553920 bytes)
Progress: 90% (123863040/137553920 bytes)
Progress: 90% (124518400/137553920 bytes)
Progress: 90% (125173760/137553920 bytes)
Progress: 91% (125829120/137553920 bytes)
Progress: 91% (126484480/137553920 bytes)
Progress: 92% (127139840/137553920 bytes)
Progress: 92% (127795200/137553920 bytes)
Progress: 93% (128450560/137553920 bytes)
Progress: 93% (129105920/137553920 bytes)
Progress: 94% (129761280/137553920 bytes)
Progress: 94% (130416640/137553920 bytes)
Progress: 95% (131072000/137553920 bytes)
Progress: 95% (131727360/137553920 bytes)
Progress: 96% (132382720/137553920 bytes)
Progress: 96% (133038080/137553920 bytes)
Progress: 97% (133693440/137553920 bytes)
Progress: 97% (134348800/137553920 bytes)
Progress: 98% (135004160/137553920 bytes)
Progress: 98% (135659520/137553920 bytes)
Progress: 99% (136314880/137553920 bytes)
Progress: 99% (136970240/137553920 bytes)
Progress: 100% (137553920/137553920 bytes)
[OK] All data sent (2099 chunks)
Server response: (code: 0)
[OK] Firmware upload completed in 7.34 seconds
============================================================
Step 2: Waiting for device to restart and come back online...
============================================================
Waiting for device to start upgrade (120 seconds)...
Still waiting for upgrade to start... (30/120 seconds)
Still waiting for upgrade to start... (60/120 seconds)
Still waiting for upgrade to start... (90/120 seconds)
Still waiting for upgrade to start... (120/120 seconds)
Starting to poll GetSWInfos every 5 seconds (max 120 attempts)...
Still waiting for device response... (attempt 12/120)
[OK] Device is back online! New firmware is running.
Current software information:
- Name: TIB3
Product Number: CXC1744343/2-R1E-0-g2aefebe
R-State: R1E
Comment: Build time: 2025-05-27 05:48:17+00:00
============================================================
[OK] Firmware upgrade completed successfully!
Total time: 281.93 seconds
============================================================
[DEBUG] 连接已关闭,总运行时间: 281.93 秒
simple_firmware_client.py
#!/usr/bin/env python3
"""
Firmware Update Client - 修复完整流程版本
支持真正的固件升级并正确获取升级后信息
"""
import argparse
import sys
import os
import time
import hashlib
import traceback
from typing import Generator, List, Optional, Dict, Any
from datetime import datetime
try:
import grpc
except ImportError:
print("[ERROR] 需要安装 grpc: pip3 install grpcio grpcio-tools")
sys.exit(1)
# 添加 proto_python 目录到 Python 路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(current_dir, 'proto_python'))
try:
import test_interface_pb2 as pb2
import service_ms_test_interface_pb2_grpc as pb2_grpc
import common_enums_pb2
import type_pb2
from google.protobuf.timestamp_pb2 import Timestamp
from google.protobuf import empty_pb2
print("[SUCCESS] 导入 protobuf 模块成功")
except ImportError as e:
print(f"[ERROR] 导入失败: {e}")
sys.exit(1)
class CompleteFirmwareClient:
"""完整固件升级客户端 - 修复版"""
# 常量定义(与C++版本保持一致)
FIRMWARE_CHUNK_SIZE = 64 * 1024 # 64KB
PROGRESS_UPDATE_INTERVAL = 10 # 每10个块更新一次进度
DEVICE_RESTART_WAIT_SECONDS = 120 # 修复:等待2分钟,与C++版本一致
MAX_POLL_ATTEMPTS = 120 # 最大轮询次数(10分钟,每次5秒)
POLLING_INTERVAL_SECONDS = 5 # 轮询间隔(秒)
PROGRESS_LOG_INTERVAL = 12 # 每12次轮询(即1分钟)打印一次日志
# 状态符号(与C++版本保持一致)
SUCCESS_SYMBOL = "[OK]"
FAILURE_SYMBOL = "[FAIL]"
DEBUG_SYMBOL = "[DEBUG]"
def __init__(self, server_addr: str, verbose: bool = False):
self.server_addr = server_addr
self.verbose = verbose
self.channel = None
self.firmware_stub = None
self.config_stub = None
self.start_time = None
self.connected = False
def connect(self) -> bool:
"""连接服务器并初始化两个存根"""
print(f"{self.DEBUG_SYMBOL} 连接到 {self.server_addr}")
# 重试连接
max_retries = 3
for retry in range(max_retries):
try:
self.channel = grpc.insecure_channel(
self.server_addr,
options=[
('grpc.max_send_message_length', 100 * 1024 * 1024),
('grpc.max_receive_message_length', 100 * 1024 * 1024),
('grpc.keepalive_time_ms', 10000),
('grpc.keepalive_timeout_ms', 5000),
('grpc.keepalive_permit_without_calls', 1),
('grpc.http2.max_pings_without_data', 0),
]
)
# 初始化两个存根
self.firmware_stub = pb2_grpc.TestInterfaceFirmwareUpdateServiceStub(self.channel)
self.config_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(self.channel)
# 测试通道
grpc.channel_ready_future(self.channel).result(timeout=10)
print(f"{self.SUCCESS_SYMBOL} Firmware client connected to: {self.server_addr}")
self.connected = True
# 测试固件服务连接
if self.test_connection():
return True
else:
print(f"{self.DEBUG_SYMBOL} 连接测试失败,重试 {retry+1}/{max_retries}")
except Exception as e:
print(f"{self.DEBUG_SYMBOL} 连接尝试 {retry+1}/{max_retries} 失败: {e}")
time.sleep(2)
print(f"{self.FAILURE_SYMBOL} 连接失败,尝试 {max_retries} 次后仍无法连接")
return False
def test_connection(self) -> bool:
"""测试固件服务连接"""
print(f"{self.DEBUG_SYMBOL} 测试固件连接...")
try:
# 创建测试请求生成器
def generate_test_request():
request = pb2.FirmwareUpdateRequest()
request.dut_position = 0
yield request
# 调用流式RPC测试
response = self.firmware_stub.FirmwareUpdate(
generate_test_request(),
timeout=10
)
print(f"{self.SUCCESS_SYMBOL} Firmware connection test succeeded")
return True
except grpc.RpcError as e:
print(f"{self.FAILURE_SYMBOL} Firmware connection test failed: {e.details()}")
return False
except Exception as e:
print(f"{self.FAILURE_SYMBOL} Firmware connection test error: {e}")
return False
def create_session(self) -> type_pb2.SessionId:
"""创建会话"""
session = type_pb2.SessionId()
session.device_id = "python_complete_client"
now = Timestamp()
now.GetCurrentTime()
session.start_time.CopyFrom(now)
return session
def calculate_md5_hash(self, filepath: str) -> str:
"""计算文件的MD5哈希值"""
md5 = hashlib.md5()
with open(filepath, 'rb') as f:
while chunk := f.read(8192):
md5.update(chunk)
return md5.hexdigest().lower()
def upload_with_manual_info(self, filepath: str, product_num: str,
rstate: str, dut_position: int = 0) -> bool:
"""上传固件"""
print(f"Testing upload: {filepath}")
# 检查文件
if not os.path.exists(filepath):
print(f"{self.FAILURE_SYMBOL} Cannot open file: {filepath}")
return False
filesize = os.path.getsize(filepath)
print(f"File size: {filesize} bytes")
try:
filename = os.path.basename(filepath)
filehash = self.calculate_md5_hash(filepath)
shared_session = self.create_session()
# 创建请求生成器
def generate_upload_requests():
# 1. 发送软件项信息
request1 = pb2.FirmwareUpdateRequest()
request1.session.CopyFrom(shared_session)
request1.dut_position = dut_position
# 创建SoftwareItem
item = pb2.SoftwareItem()
item.description = f"Firmware update"
item.product_number = product_num
item.rstate = rstate
item.sw_type = common_enums_pb2.SOFTWARE_TYPE_INITIAL_FLASH_IMAGE
item.filename = filename
item.hash = filehash
item.total_size = filesize
request1.item.CopyFrom(item)
yield request1
print(f"{self.SUCCESS_SYMBOL} Software item sent")
# 2. 发送文件数据
chunk_count = 0
total_sent = 0
with open(filepath, 'rb') as f:
while True:
chunk = f.read(self.FIRMWARE_CHUNK_SIZE)
if not chunk:
break
bytes_read = len(chunk)
request = pb2.FirmwareUpdateRequest()
request.session.CopyFrom(shared_session)
request.dut_position = dut_position
# 创建SoftwareItemContent
content = pb2.SoftwareItemContent()
content.swType = item.sw_type
content.data = chunk
request.content.CopyFrom(content)
total_sent += bytes_read
chunk_count += 1
# 显示进度
if (chunk_count % self.PROGRESS_UPDATE_INTERVAL == 0 or
total_sent == filesize):
if filesize > 0:
progress = (total_sent * 100) // filesize
else:
progress = 0
print(f"Progress: {progress}% ({total_sent}/{filesize} bytes)")
yield request
print(f"{self.SUCCESS_SYMBOL} All data sent ({chunk_count} chunks)")
# 调用流式RPC,设置较长超时时间
response = self.firmware_stub.FirmwareUpdate(
generate_upload_requests(),
timeout=600 # 10分钟超时
)
# 检查响应
if hasattr(response, 'code'):
print(f"Server response: {response.message} (code: {response.code})")
return response.code == 0
else:
print(f"{self.DEBUG_SYMBOL} Unexpected server response: {response}")
return True # 即使响应格式不同,也认为成功
except grpc.RpcError as e:
print(f"{self.FAILURE_SYMBOL} Upload failed: {e.details()}")
if self.verbose:
print(f"Error code: {e.code()}")
return False
except Exception as e:
print(f"{self.FAILURE_SYMBOL} Upload error: {e}")
if self.verbose:
traceback.print_exc()
return False
def get_sw_infos(self) -> Optional[object]:
"""获取设备软件信息。在轮询期间,此方法可能会因设备不可用而返回 None。"""
try:
request = empty_pb2.Empty()
# 直接调用已知的方法,超时时间应小于轮询间隔
response = self.config_stub.GetSWInfos(request, timeout=self.POLLING_INTERVAL_SECONDS - 1)
return response
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.UNAVAILABLE:
# 设备可能离线,这是轮询期间的正常情况
if self.verbose:
print(f"{self.DEBUG_SYMBOL} Device unavailable (expected during polling).")
else:
if self.verbose:
print(f"{self.DEBUG_SYMBOL} RPC error getting SW info: {e.details()}")
return None
except Exception as e:
if self.verbose:
print(f"{self.DEBUG_SYMBOL} Unexpected error getting SW info: {e}")
traceback.print_exc()
return None
def parse_sw_info_response(self, response) -> List[Dict[str, Any]]:
"""解析软件信息响应"""
sw_infos = []
try:
# 尝试不同的响应格式
if hasattr(response, 'sw_info'):
# 格式1: response.sw_info (列表)
for sw_info in response.sw_info:
info = {}
if hasattr(sw_info, 'sw_name'):
info['name'] = sw_info.sw_name
elif hasattr(sw_info, 'swName'):
info['name'] = sw_info.swName
if hasattr(sw_info, 'product_number'):
info['product_number'] = sw_info.product_number
elif hasattr(sw_info, 'productNumber'):
info['product_number'] = sw_info.productNumber
if hasattr(sw_info, 'product_rstate'):
info['rstate'] = sw_info.product_rstate
elif hasattr(sw_info, 'product_r_state'):
info['rstate'] = sw_info.product_r_state
elif hasattr(sw_info, 'rstate'):
info['rstate'] = sw_info.rstate
if hasattr(sw_info, 'comment'):
info['comment'] = sw_info.comment
sw_infos.append(info)
elif hasattr(response, 'software_info'):
# 格式2: response.software_info (列表)
for sw_info in response.software_info:
info = {}
if hasattr(sw_info, 'name'):
info['name'] = sw_info.name
if hasattr(sw_info, 'product'):
info['product_number'] = sw_info.product
if hasattr(sw_info, 'state'):
info['rstate'] = sw_info.state
if hasattr(sw_info, 'comment'):
info['comment'] = sw_info.comment
sw_infos.append(info)
elif hasattr(response, 'infos'):
# 格式3: response.infos (列表)
for sw_info in response.infos:
info = {}
# 尝试常见字段名
for attr in ['sw_name', 'name', 'software_name']:
if hasattr(sw_info, attr):
info['name'] = getattr(sw_info, attr)
break
for attr in ['product_number', 'product', 'product_id']:
if hasattr(sw_info, attr):
info['product_number'] = getattr(sw_info, attr)
break
for attr in ['product_rstate', 'rstate', 'state', 'revision']:
if hasattr(sw_info, attr):
info['rstate'] = getattr(sw_info, attr)
break
if hasattr(sw_info, 'comment'):
info['comment'] = sw_info.comment
sw_infos.append(info)
# 如果是单个对象而不是列表
elif hasattr(response, 'sw_name') or hasattr(response, 'product_number'):
info = {}
if hasattr(response, 'sw_name'):
info['name'] = response.sw_name
elif hasattr(response, 'name'):
info['name'] = response.name
if hasattr(response, 'product_number'):
info['product_number'] = response.product_number
elif hasattr(response, 'product'):
info['product_number'] = response.product
if hasattr(response, 'product_rstate'):
info['rstate'] = response.product_rstate
elif hasattr(response, 'rstate'):
info['rstate'] = response.rstate
if hasattr(response, 'comment'):
info['comment'] = response.comment
sw_infos.append(info)
except Exception as e:
print(f"{self.DEBUG_SYMBOL} 解析软件信息时出错: {e}")
if self.verbose:
traceback.print_exc()
return sw_infos
def wait_for_device_and_get_sw_info(self, max_retries: Optional[int] = None) -> bool:
"""等待设备重启并获取软件信息 - 与C++版本一致"""
if max_retries is None:
max_retries = self.MAX_POLL_ATTEMPTS
print(f"Waiting for device to start upgrade ({self.DEVICE_RESTART_WAIT_SECONDS} seconds)...")
# 等待设备开始升级(2分钟)
for i in range(self.DEVICE_RESTART_WAIT_SECONDS):
time.sleep(1)
if (i + 1) % 30 == 0:
print(f"Still waiting for upgrade to start... ({i+1}/{self.DEVICE_RESTART_WAIT_SECONDS} seconds)")
print(f"Starting to poll GetSWInfos every {self.POLLING_INTERVAL_SECONDS} seconds (max {max_retries} attempts)...")
# 轮询设备状态
for attempt in range(1, max_retries + 1):
response = None
try:
# --- 核心修复:在每次循环中创建全新的临时连接 ---
# 这可以避免因服务器重启导致的旧连接失效问题
with grpc.insecure_channel(self.server_addr) as channel:
# 等待通道就绪,超时时间要短于轮询间隔
grpc.channel_ready_future(channel).result(timeout=self.POLLING_INTERVAL_SECONDS - 1)
temp_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(channel)
request = empty_pb2.Empty()
# 使用一个较短的RPC超时
response = temp_stub.GetSWInfos(request, timeout=2)
except grpc.RpcError as e:
# 在轮询期间,UNAVAILABLE 和 DEADLINE_EXCEEDED 是正常现象
if e.code() not in (grpc.StatusCode.UNAVAILABLE, grpc.StatusCode.DEADLINE_EXCEEDED):
if self.verbose:
print(f"{self.DEBUG_SYMBOL} 轮询时发生非预期的RPC错误 (attempt {attempt}): {e.details()}")
except KeyboardInterrupt:
print("\n轮询被用户中断")
return False
except Exception as e:
# 捕获其他异常,例如 channel_ready_future 超时
if self.verbose:
print(f"{self.DEBUG_SYMBOL} 轮询异常 (attempt {attempt}): {e}")
# 检查是否成功获取响应
if response is not None:
print(f"{self.SUCCESS_SYMBOL} Device is back online! New firmware is running.")
# 解析软件信息
sw_infos = self.parse_sw_info_response(response)
if sw_infos:
print("Current software information:")
for info in sw_infos:
info_str = f" - Name: {info.get('name', 'Unknown')}"
if 'product_number' in info:
info_str += f"\n Product Number: {info['product_number']}"
if 'rstate' in info:
info_str += f"\n R-State: {info['rstate']}"
if 'comment' in info and info['comment']:
info_str += f"\n Comment: {info['comment']}"
print(info_str)
else:
print(f"{self.DEBUG_SYMBOL} 解析到响应但无软件信息,原始响应: {response}")
return True
# 设备尚未准备好,继续轮询
if attempt % self.PROGRESS_LOG_INTERVAL == 0:
print(f"Still waiting for device response... (attempt {attempt}/{max_retries})")
time.sleep(self.POLLING_INTERVAL_SECONDS)
print(f"{self.FAILURE_SYMBOL} Device did not respond after {max_retries * self.POLLING_INTERVAL_SECONDS} seconds of polling")
return False
def firmware_upgrade(self, filepath: str, product_num: str,
rstate: str, dut_position: int = 0,
max_poll_attempts: int = None) -> bool:
"""完整固件升级流程"""
self.start_time = time.time()
print("Starting complete firmware upgrade process...")
# 使用指定的轮询次数
if max_poll_attempts is not None:
self.MAX_POLL_ATTEMPTS = max_poll_attempts
# 1. 执行固件上传
print("\n" + "="*60)
print("Step 1: Uploading firmware...")
print("="*60)
if not self.upload_with_manual_info(filepath, product_num, rstate, dut_position):
print(f"{self.FAILURE_SYMBOL} Firmware upload failed")
return False
upload_time = time.time() - self.start_time
print(f"{self.SUCCESS_SYMBOL} Firmware upload completed in {upload_time:.2f} seconds")
# 2. 等待设备重启并重新上线
print("\n" + "="*60)
print("Step 2: Waiting for device to restart and come back online...")
print("="*60)
if not self.wait_for_device_and_get_sw_info():
print(f"{self.FAILURE_SYMBOL} Device did not come back online or failed to get software info")
return False
total_time = time.time() - self.start_time
print("\n" + "="*60)
print(f"{self.SUCCESS_SYMBOL} Firmware upgrade completed successfully!")
print(f"Total time: {total_time:.2f} seconds")
print("="*60)
return True
def verify_file(self, filepath: str) -> bool:
"""验证文件并计算哈希值"""
if not os.path.exists(filepath):
print(f"{self.FAILURE_SYMBOL} 文件不存在: {filepath}")
return False
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
# 计算所有哈希值
md5_hash = self.calculate_md5_hash(filepath)
print(f"\n{'='*60}")
print("文件验证报告")
print('='*60)
print(f"文件名: {filename}")
print(f"文件大小: {filesize:,} 字节 ({filesize/1024/1024:.2f} MB)")
print(f"修改时间: {datetime.fromtimestamp(os.path.getmtime(filepath))}")
print('='*60)
print("哈希值:")
print(f" MD5 (小写): {md5_hash}")
print('='*60)
print(f"\n{self.DEBUG_SYMBOL} 此服务器期望使用: MD5 (小写)")
return True
def close(self):
"""关闭连接"""
if self.channel:
self.channel.close()
if self.start_time:
elapsed_time = time.time() - self.start_time
print(f"{self.DEBUG_SYMBOL} 连接已关闭,总运行时间: {elapsed_time:.2f} 秒")
else:
print(f"{self.DEBUG_SYMBOL} 连接已关闭")
self.connected = False
def main():
parser = argparse.ArgumentParser(
description='完整固件升级客户端 - 修复版',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
%(prog)s test localhost:50050
%(prog)s upload localhost:50050 firmware.bin "CXP9024418/1" R1A
%(prog)s upgrade localhost:50050 firmware.bin "CXP9024418/1" R1A
%(prog)s verify firmware.bin
注意: 完整升级流程需要较长时间,建议增加轮询次数
'''
)
subparsers = parser.add_subparsers(dest='command', help='命令', required=True)
# test 命令
test_parser = subparsers.add_parser('test', help='测试服务器连接')
test_parser.add_argument('server', help='服务器地址 (主机:端口)')
test_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
# upload 命令
upload_parser = subparsers.add_parser('upload', help='上传固件(不等待重启)')
upload_parser.add_argument('server', help='服务器地址')
upload_parser.add_argument('file', help='固件文件路径')
upload_parser.add_argument('product', help='产品编号')
upload_parser.add_argument('rstate', help='状态代码')
upload_parser.add_argument('--position', '-p', type=int, default=0, help='DUT位置')
upload_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
# upgrade 命令
upgrade_parser = subparsers.add_parser('upgrade', help='完整固件升级(上传+等待重启)')
upgrade_parser.add_argument('server', help='服务器地址')
upgrade_parser.add_argument('file', help='固件文件路径')
upgrade_parser.add_argument('product', help='产品编号')
upgrade_parser.add_argument('rstate', help='状态代码')
upgrade_parser.add_argument('--position', '-p', type=int, default=0, help='DUT位置')
upgrade_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
upgrade_parser.add_argument('--max-poll', type=int, default=120, help='最大轮询次数(默认120,即10分钟)')
# verify 命令
verify_parser = subparsers.add_parser('verify', help='验证文件哈希值')
verify_parser.add_argument('file', help='文件路径')
verify_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
args = parser.parse_args()
client = None
try:
if args.command == 'test':
client = CompleteFirmwareClient(args.server, args.verbose)
if client.connect():
return 0
return 1
elif args.command == 'upload':
if not os.path.exists(args.file):
print(f"{CompleteFirmwareClient.FAILURE_SYMBOL} Error: File not found: {args.file}")
return 1
client = CompleteFirmwareClient(args.server, args.verbose)
if not client.connect():
return 1
success = client.upload_with_manual_info(
args.file, args.product, args.rstate, args.position
)
return 0 if success else 1
elif args.command == 'upgrade':
if not os.path.exists(args.file):
print(f"{CompleteFirmwareClient.FAILURE_SYMBOL} Error: File not found: {args.file}")
return 1
print(f"\n准备执行完整固件升级:")
print(f" 文件: {os.path.basename(args.file)}")
print(f" 大小: {os.path.getsize(args.file):,} 字节")
print(f" 产品: {args.product}")
print(f" 状态: {args.rstate}")
print(f" 位置: {args.position}")
print(f" 最大轮询: {args.max_poll}次 (约{args.max_poll * 5 / 60:.1f}分钟)")
confirm = input("\n确认执行完整升级流程? (y/N): ").strip().lower()
if confirm not in ['y', 'yes', '是']:
print(f"{CompleteFirmwareClient.DEBUG_SYMBOL} 操作已取消")
return 0
client = CompleteFirmwareClient(args.server, args.verbose)
if not client.connect():
return 1
success = client.firmware_upgrade(
args.file, args.product, args.rstate, args.position, args.max_poll
)
return 0 if success else 1
elif args.command == 'verify':
if not os.path.exists(args.file):
print(f"{CompleteFirmwareClient.FAILURE_SYMBOL} Error: File not found: {args.file}")
return 1
client = CompleteFirmwareClient("localhost:50050", args.verbose)
success = client.verify_file(args.file)
return 0 if success else 1
except KeyboardInterrupt:
print(f"\n{CompleteFirmwareClient.DEBUG_SYMBOL} 操作被用户中断")
return 130
except Exception as e:
print(f"\n{CompleteFirmwareClient.FAILURE_SYMBOL} 发生未预期错误: {e}")
traceback.print_exc()
return 1
finally:
if client:
client.close()
if __name__ == '__main__':
sys.exit(main())
深入剖析
gRPC 以其高性能和跨语言的特性,成为了现代微服务架构中的宠儿。然而,从掌握 gRPC 的基础概念到构建一个能够在真实世界中稳定运行的客户端,中间还有许多坑需要我们去填。
本文将以一个功能完善的 Python 固件升级客户端 simple_firmware_client.py 为例,深入剖析其代码结构、gRPC 通信模式以及在实战中遇到的挑战与解决方案。无论你是 gRPC 的初学者还是有一定经验的开发者,相信都能从中获得启发。
蓝图:一个优秀客户端的架构设计
一个好的架构是软件成功的基石。我们的固件升级客户端没有将所有逻辑杂糅在一起,而是采用了清晰的、面向对象的设计,将整个项目划分为三个层次分明的模块。
1. 启动与依赖:告别 ImportError
在任何复杂的 Python 项目中,模块导入都是一个潜在的痛点。我们的客户端通过一段精妙的代码,一劳永逸地解决了这个问题。
# 添加 proto_python 目录到 Python 路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(current_dir, 'proto_python'))
try:
# 现在可以安全地导入了
import test_interface_pb2 as pb2
import service_ms_test_interface_pb2_grpc as pb2_grpc
# ...
except ImportError:
print("[ERROR] Protobuf 模块未找到,请先编译 .proto 文件")
sys.exit(1)
核心思想:动态地将存放 Protobuf 生成代码的 proto_python 目录添加到 Python 解释器的搜索路径 sys.path 的最前端。这确保了无论你在哪个目录下运行脚本,它总能优先找到我们需要的模块,彻底告别因路径问题导致的 ImportError。
2. 核心引擎:CompleteFirmwareClient 类
这是整个客户端的心脏,一个封装了所有业务逻辑的类。它遵循单一职责原则,将复杂的固件升级流程拆解为一系列清晰、可维护的方法。
__init__(): 构造函数,初始化服务器地址、日志记录器等。connect(): 负责建立 gRPC 连接,并创建与远程服务通信的 Stub (存根)。upload_with_manual_info(): 负责上传固件,是 gRPC 客户端流式 RPC 的绝佳实践。wait_for_device_and_get_sw_info(): 负责轮询设备状态,是 gRPC 一元 RPC 和健壮性设计的典范。firmware_upgrade(): 流程编排方法。它像一位项目经理,按顺序调用上传和轮询方法,清晰地定义了整个业务流程。
3. 指挥中心:main 与 argparse
这是用户与程序交互的入口。通过使用 Python 内置的 argparse 库,我们为客户端提供了一个功能强大且用户友好的命令行接口 (CLI)。
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="gRPC Firmware Upgrade Client")
parser.add_argument("server", help="Server address (e.g., localhost:50051)")
parser.add_argument("-f", "--file", required=True, help="Firmware file path")
# ... 其他参数
args = parser.parse_args()
main(args)
用户只需在命令行中提供必要的参数,main 函数就会驱动 CompleteFirmwareClient 完成所有复杂的任务。
实战:驾驭 gRPC 的两种核心通信模式
理论总是枯燥的,让我们看看 gRPC 在实战中是如何施展拳脚的。
模式一:客户端流式 RPC (Client Streaming RPC) - 优雅地传输大文件
固件文件通常很大,一次性读入内存再发送是不可接受的。客户端流式 RPC 正是为此而生。
它的工作方式:客户端调用一个 RPC 方法时,传递的不是一个请求对象,而是一个生成器 (Generator)。
# 1. 定义一个生成器,它会“边读边传”
def generate_upload_requests(filepath, ...):
# 第一个请求:发送文件的元数据
yield create_metadata_request(...)
# 后续请求:循环读取文件块,并逐个发送
with open(filepath, 'rb') as f:
while chunk := f.read(CHUNK_SIZE):
yield create_data_chunk_request(chunk)
# 2. 在 RPC 调用时传入生成器
response = firmware_stub.FirmwareUpdate(
generate_upload_requests(filepath, ...),
timeout=600 # 设置一个较长的超时
)
优势:这种“流式”传输的模式,使得客户端的内存占用极低,可以轻松应对 GB 级别的超大文件传输,同时还能通过 yield 的间隙方便地实现进度报告。
模式二:一元 RPC (Unary RPC) - 经典的“请求-响应”
这是最简单、最常见的模式,就像一次普通的函数调用。我们用它来查询设备的软件信息。
request = empty_pb2.Empty()
# 发送一个请求,等待一个响应
response = config_stub.GetSWInfos(request, timeout=5)
简单、直接,非常适合状态查询、获取配置等操作。
挑战与超越:解决“僵尸连接”问题
在我们的项目中,最大的挑战来自于:固件升级后,服务器会重启。
问题:服务器重启后,客户端持有的旧 gRPC channel 实际上已经失效,但客户端本身可能并不知情。此时,任何基于这个旧 channel 的 RPC 调用都会失败或无限期等待,我们称之为“僵尸连接”。
解决方案:在轮询设备状态的循环中,我们采取了一个非常巧妙的策略——为每一次尝试都创建一个全新的、临时的 channel。
def wait_for_device_and_get_sw_info(self):
for attempt in range(MAX_POLL_ATTEMPTS):
try:
# 使用 'with' 语句确保临时 channel 被正确关闭
with grpc.insecure_channel(self.server_addr) as channel:
# 在这个全新的 channel 上创建临时 stub
temp_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(channel)
# 发起 RPC 调用
response = temp_stub.GetSWInfos(empty_pb2.Empty(), timeout=2)
# 如果调用成功,说明设备已上线,可以退出循环
print("✓ Device is back online!")
return True
except grpc.RpcError:
# 在设备重启期间,连接失败是正常现象,我们忽略它
print(f"Attempt {attempt+1}: Device not ready yet...")
time.sleep(POLLING_INTERVAL_SECONDS)
print("✗ Device did not respond in time.")
return False
通过在每次循环中都使用一个“一次性”的 channel,我们确保了每一次轮询都是一次全新的连接尝试,从而完美地绕过了服务器重启带来的连接状态不一致问题。这是一种在构建需要与可能重启的服务进行交互的客户端时,非常实用且健壮的设计模式。
结语
通过对 simple_firmware_client.py 的深度剖析,我们不仅学习了如何组织一个清晰、可维护的 gRPC 客户端项目,还掌握了如何运用不同的 RPC 模式来解决实际问题,尤其是如何通过创建临时 channel 的方式来处理棘手的“僵尸连接”问题。
gRPC 的强大之处不仅在于其性能,更在于它提供了一套完整的工具集,让我们能够构建出既优雅又健壮的分布式系统。希望本文的经验能为你未来的 gRPC 之旅扫清一些障碍。
C++ 实现 gRPC 四种服务方式的核心在于:统一的 .proto 定义 + 语言特定的 API 风格。与 Python 的生成器风格不同,C++ 提供了同步和异步两套 API。这里我基于最常用的同步 API(也就是你之前看的 C++ 固件客户端用的那种),并结合官方 RouteGuide 示例,为你详细拆解。
C++ 实现 gRPC 四种服务方式的核心在于:统一的 .proto 定义 + 语言特定的 API 风格。与 Python 的生成器风格不同,C++ 提供了同步和异步两套 API。
一、.proto 定义(与 Python 完全一致)
所有语言共享同一个接口定义。以 route_guide.proto 为例:
service RouteGuide {
// 一元 RPC
rpc GetFeature(Point) returns (Feature) {}
// 服务器端流式 RPC
rpc ListFeatures(Rectangle) returns (stream Feature) {}
// 客户端流式 RPC
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// 双向流式 RPC
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
使用 protoc 加 grpc_cpp_plugin 编译后,会生成四个核心类:
RouteGuide::Service(服务端需继承的基类)RouteGuide::Stub(客户端存根)- 以及配套的
ServerReader、ServerWriter、ClientReader等流式辅助类 。
二、C++ 服务端实现四种方式
2.1 一元 RPC:最简单
class RouteGuideImpl final : public RouteGuide::Service {
Status GetFeature(ServerContext* context, const Point* point,
Feature* feature) override {
feature->set_name(GetFeatureName(*point)); // 填充响应
feature->mutable_location()->CopyFrom(*point);
return Status::OK; // 返回 OK 即完成
}
};
特点:直接拿到 request 和 response 对象,填充后返回 Status。和 Python 一样直接。
2.2 服务器端流式 RPC:ServerWriter
Status ListFeatures(ServerContext* context, const Rectangle* rectangle,
ServerWriter<Feature>* writer) override {
// writer 是 gRPC 提供的写入器
for (const Feature& f : feature_list_) {
if (InRectange(f, *rectangle)) {
writer->Write(f); // 每调用一次 Write,就向客户端推送一条消息
}
}
return Status::OK; // 流结束,返回最终状态
}
与 Python 对比:Python 用 yield 隐式流,C++ 用显式的 writer->Write(),循环结束后通过 return Status 关闭流 。
2.3 客户端流式 RPC:ServerReader
Status RecordRoute(ServerContext* context, ServerReader<Point>* reader,
RouteSummary* summary) override {
Point point;
while (reader->Read(&point)) { // 循环读取客户端发来的流
// 处理每个 point
total_distance += CalcDistance(point);
}
// 客户端流结束(调用 WritesDone 后),填充最终响应
summary->set_distance(total_distance);
return Status::OK;
}
关键点:reader->Read() 返回 true 表示还有数据,返回 false 表示客户端流已关闭 。对应你之前 Python 固件上传中的 request_iterator。
2.4 双向流式 RPC:ServerReaderWriter
Status RouteChat(ServerContext* context,
ServerReaderWriter<RouteNote, RouteNote>* stream) override {
RouteNote note;
std::vector<RouteNote> received_notes;
while (stream->Read(¬e)) { // 不断读客户端发来的消息
for (const auto& n : received_notes) {
if (n.location() == note.location()) {
stream->Write(n); // 发现匹配就写回给客户端
}
}
received_notes.push_back(note);
}
return Status::OK;
}
特点:同一个 stream 对象既能 Read 又能 Write,读写独立进行 。
三、C++ 客户端调用四种方式
3.1 一元 RPC
Feature feature;
Status status = stub_->GetFeature(&context, point, &feature);
if (status.ok()) { /* 使用 feature */ }
同步调用,阻塞直到响应返回。
3.2 服务器端流式 RPC:ClientReader
std::unique_ptr<ClientReader<Feature>> reader(
stub_->ListFeatures(&context, rect));
Feature feature;
while (reader->Read(&feature)) { // 循环读流,直到服务器关闭
std::cout << feature.name() << std::endl;
}
Status status = reader->Finish(); // 获取最终状态
3.3 客户端流式 RPC:ClientWriter
std::unique_ptr<ClientWriter<Point>> writer(
stub_->RecordRoute(&context, &summary));
for (const Point& p : points_to_send) {
if (!writer->Write(p)) { // Write 失败表示流已中断
break;
}
}
writer->WritesDone(); // 显式关闭流,通知服务器
Status status = writer->Finish(); // 等待最终响应(RouteSummary)
这就是你之前分析的固件上传函数的底层实现:先 Write 多个块,然后 WritesDone(),最后 Finish() 获取响应 。
3.4 双向流式 RPC:ClientReaderWriter
std::shared_ptr<ClientReaderWriter<RouteNote, RouteNote>> stream(
stub_->RouteChat(&context));
// 启动写协程
std::thread writer([stream]() {
for (const auto& note : notes_to_send) {
stream->Write(note);
}
stream->WritesDone();
});
// 主线程读
RouteNote note;
while (stream->Read(¬e)) {
std::cout << "Got note: " << note.message() << std::endl;
}
writer.join();
Status status = stream->Finish();
四、与 Python 的核心差异对比
| 维度 | Python (同步) | C++ (同步) | C++ (异步/回调) |
|---|---|---|---|
| 流式请求 | 生成器 yield |
显式 Write 循环 |
使用 CompletionQueue 或回调 Reactor |
| 流式响应 | for 循环迭代 |
显式 Read 循环 |
同上 |
| 流结束信号 | 生成器结束自动发送 | 需手动调用 WritesDone() |
回调中处理 OnDone |
| 错误处理 | 异常 grpc.RpcError |
检查 Status 对象 |
检查 Status 或回调中的 ok 标志 |
| 并发模型 | 多线程 | 多线程 | 基于 CompletionQueue 的事件驱动 |
为什么 C++ 更“啰嗦”?
- RAII 与资源管理:需要显式管理
ClientWriter等对象的生命周期(智能指针)。 - 无生成器语法:C++ 没有类似 Python
yield的协程语法(C++20 才有),所以循环必须显式写。 - 异步高性能:为了极致性能,C++ 提供了
CompletionQueue异步模式,允许你复用线程,避免阻塞 。
五、总结
C++ 实现这四种服务方式的套路非常统一:
- 服务端:根据类型选择
ServerReader/ServerWriter/ServerReaderWriter,在循环中Read或Write。 - 客户端:根据类型选择
ClientReader/ClientWriter/ClientReaderWriter,操作完后调用WritesDone()和Finish()。 - 状态处理:所有方法最终都返回
Status,检查ok()判断成功与否 。
这种显式控制的 API 虽然代码量稍多,但给开发者提供了最大的控制权,特别适合对性能有极致要求的场景。
gRPC 实战深潜
为何 C++ 能“死等”,而 Python 却要“另起炉灶”?
在使用 gRPC 构建分布式系统时,我们常常会遇到一个棘手的场景:客户端需要与一个可能会重启的服务进行通信。一个典型的例子就是固件升级:客户端上传完固件后,服务器需要重启以应用更新。此时,客户端如何优雅地等待服务器恢复并确认升级成功呢?
最近,在开发一个固件升级工具时,我们发现 C++ 和 Python 的 gRPC 客户端在处理这一场景时,采取了截然不同的策略,这背后揭示了 gRPC 在不同语言实现中的深层差异。
场景设定:等待重启的服务器
我们的业务流程如下:
- 客户端通过 gRPC 连接到设备(服务器)。
- 客户端通过客户端流式 RPC 上传固件文件。
- 服务器接收文件后,执行升级并重启。
- 客户端需要轮询服务器,直到它重新上线,并通过一个一元 RPC (
GetSWInfos) 获取更新后的软件版本信息。
问题的核心在于第 4 步:客户端如何处理那个因服务器重启而失效的 gRPC 连接?
两种策略:C++ 的“原地死等” vs. Python 的“重新请求”
通过分析两个客户端的代码,我们发现了两种截然不同的实现模式。
C++ 客户端:执着的等待者
C++ 客户端的轮询逻辑大致如下:
// 在客户端对象的整个生命周期中,config_stub_ 是复用的
bool SimpleFirmwareClient::WaitForDeviceAndGetSWInfo() {
// ... 初始等待 ...
for (int attempt = 1; attempt <= MAX_POLL_ATTEMPTS; ++attempt) {
try {
// 在循环中,反复使用同一个 stub 对象
grpc::Status status =
config_stub_->GetSWInfos(&context, request, &response);
if (status.ok()) {
// 成功!服务器已上线
return true;
}
} catch (...) {
// 忽略所有异常,继续下一次尝试
}
std::this_thread::sleep_for(std::chrono::seconds(5));
}
return false;
}
核心行为:C++ 客户端在整个轮询过程中,始终复用同一个 stub 对象(它是在客户端初始化时创建的)。即使 RPC 调用因为服务器不可达而失败(status 非 ok),它也只是简单地忽略错误,然后继续下一次循环。它似乎坚信,只要坚持尝试,这个 stub 最终会重新连接成功。我们称之为“原地死等”策略。
Python 客户端:灵活的挑战者
与 C++ 不同,Python 客户端如果采用同样的策略,会遭遇“僵尸连接”问题——即 channel 在服务器重启后进入一个无法自动恢复的失败状态。因此,我们最终采取了另一种更健壮的策略:
# 在轮询的 for 循环内部
def _wait_for_device_and_get_sw_info(self):
for attempt in range(self.MAX_POLL_ATTEMPTS):
try:
# 关键:每次循环都创建一个全新的、临时的 channel
with grpc.insecure_channel(self.server_address) as channel:
# 基于这个新 channel 创建一个临时 stub
temp_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(channel)
# 发起调用
response = temp_stub.GetSWInfos(request, timeout=2)
# 如果能走到这里,说明连接成功了!
return True
except grpc.RpcError:
# 连接失败是预料之中的,忽略并等待下一次重试
pass
time.sleep(self.POLLING_INTERVAL_SECONDS)
return False
核心行为:Python 客户端在每一次轮询尝试中,都会创建一个全新的 channel 和 stub。它不信任旧的连接,而是选择在每次尝试时都“另起炉灶”,发起一次全新的连接请求。我们称之为“重新请求”策略。
为什么会存在这种差异?
C++ 客户端之所以能够“原地死等”,根源在于其 gRPC 实现拥有一个更强大、更持久的后台自动重连机制。
我们可以从以下几个层面来理解:
1. 底层核心库的差异
gRPC 的核心库 grpc-core 是用 C++ 编写的,它是所有语言实现的基础。C++ gRPC 客户端能够更直接、更深度地利用核心库的全部功能。这很可能包括一个更激进、更智能的后台重连策略。
当服务器断开,channel 进入 TRANSIENT_FAILURE (瞬时故障) 状态后,C++ 的底层实现可能会在后台持续、自动地尝试重连,并且其重连的退避算法(backoff strategy)被设计为可以覆盖长达数分钟的中断窗口。当上层应用发起 RPC 调用时,这个调用会被挂起,直到后台重连成功。
相比之下,Python 的 gRPC 库(作为 C++ 核心库的封装)在处理长时间中断时,其默认的重连行为可能相对“保守”,在几次失败后可能就停止尝试,导致 channel 永久失效。
2. 默认 Channel 配置的不同
gRPC channel 的行为可以通过大量参数进行微调。C++ 版本的 gRPC 库可能拥有更有利于长时间重连的默认参数。例如,它可能默认启用了无限重试或一个非常长的重试超时。而 Python 的默认配置可能更倾向于“快速失败”,以避免应用程序被长时间阻塞。
3. 一个形象的比喻
让我们用一个等电梯的比喻来理解这两种策略:
-
C++ 客户端(执着的员工):他走到电梯前,发现电梯正在维修(服务器重启)。他没有离开,而是选择在原地一直等待。他背后的智能管家(gRPC 核心库)会持续监控电梯状态,一旦电梯恢复,立刻通知他。他的策略是“原地死等”,依赖于强大的底层支持。
-
Python 客户端(灵活的员工):他发现电梯在维修,等了一小会儿后,决定先去干点别的事(
time.sleep)。过了一会儿,他重新走到电梯前,像第一次来一样,重新按了一遍按钮。他的策略是“放弃等待,重新发起请求”,通过主动的、应用层的重试来确保成功。
Python gRPC固件升级客户端解析
基于一个完整的 Python gRPC 固件升级客户端代码,深入解析其在 gRPC 使用上的关键模式,包括客户端流式 RPC、错误处理、超时控制等,并对照我们之前讨论的 C++ 客户端实现,展示两种语言在 gRPC 编程中的异同。
1. 环境与准备工作
1.1 依赖与导入
import grpc
import test_interface_pb2 as pb2
import service_ms_test_interface_pb2_grpc as pb2_grpc
grpcio:Python gRPC 核心库。- 生成的 protobuf 模块:通过
grpcio-tools从.proto文件编译得到,包含消息类和服务存根。
1.2 关键常量定义
FIRMWARE_CHUNK_SIZE = 64 * 1024 # 64KB 分块大小
INITIAL_WAIT_SECONDS = 120 # 上传后盲等 2 分钟
MAX_POLL_ATTEMPTS = 120 # 最多轮询 120 次(10分钟)
POLLING_INTERVAL_SECONDS = 5 # 轮询间隔 5 秒
这些常量与 C++ 版本中的定义完全一致,体现了跨语言业务逻辑的统一。
2. gRPC 客户端核心实现
2.1 连接服务器与创建存根
self.channel = grpc.insecure_channel(
self.server_addr,
options=[
('grpc.max_send_message_length', 100 * 1024 * 1024),
('grpc.max_receive_message_length', 100 * 1024 * 1024),
...
]
)
self.firmware_stub = pb2_grpc.TestInterfaceFirmwareUpdateServiceStub(self.channel)
self.config_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(self.channel)
- 通道选项:设置最大消息长度,支持大文件传输,与 C++ 中的
ChannelArguments类似。 - 存根:每个服务对应一个存根,用于调用 RPC 方法。C++ 中也是通过
NewStub创建智能指针。
2.2 测试连接
def generate_test_request():
request = pb2.FirmwareUpdateRequest()
request.dut_position = 0
yield request
response = self.firmware_stub.FirmwareUpdate(generate_test_request(), timeout=10)
- 使用生成器产生一个请求消息,发起客户端流式 RPC。即使只有一个请求,也必须通过生成器传入。
timeout参数:设置整个 RPC 的超时时间,与 C++ 中通过ClientContext设置deadline等效。
2.3 核心:客户端流式上传 upload_with_manual_info
2.3.1 生成器函数
def generate_upload_requests():
# 1. 发送元信息
request1 = pb2.FirmwareUpdateRequest()
request1.item.CopyFrom(item) # 填充 SoftwareItem
yield request1
# 2. 分块发送文件数据
with open(filepath, 'rb') as f:
while True:
chunk = f.read(self.FIRMWARE_CHUNK_SIZE)
if not chunk: break
request = pb2.FirmwareUpdateRequest()
request.content.data = chunk
yield request
- 生成器即请求流:每次
yield一个FirmwareUpdateRequest对象,gRPC 运行时自动将其序列化并发送。 - 第一个消息:携带元数据,与 C++ 中先
Write(item_request)对应。 - 后续消息:携带数据块,与 C++ 循环
Write(content_request)对应。
2.3.2 调用流式 RPC
response = self.firmware_stub.FirmwareUpdate(
generate_upload_requests(),
timeout=600 # 10分钟超时
)
- 隐式流结束:生成器耗尽时,gRPC 自动发送半关闭信号,无需显式调用
WritesDone。 - 响应获取:
response直接是服务器返回的最终ProductionStatus,无需手动调用Finish。
2.3.3 错误处理
except grpc.RpcError as e:
self.logger.error(f"Upload failed: {e.details()}")
return False
- 使用异常捕获 gRPC 错误,可通过
e.code()获取状态码(如UNAVAILABLE、DEADLINE_EXCEEDED)。 - 与 C++ 检查
writer->Finish()返回的Status对象本质相同,但 Python 使用异常更符合语言习惯。
2.4 等待设备重启与轮询 wait_for_device_and_get_sw_info
2.4.1 盲等阶段
for i in range(self.INITIAL_WAIT_SECONDS):
time.sleep(1)
- 与 C++ 相同:先无条件等待 2 分钟,避免过早轮询。
2.4.2 主动轮询
for attempt in range(1, max_retries + 1):
with grpc.insecure_channel(self.server_addr) as channel:
grpc.channel_ready_future(channel).result(timeout=self.POLLING_INTERVAL_SECONDS - 1)
temp_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(channel)
response = temp_stub.GetSWInfos(empty_pb2.Empty(), timeout=2)
if response is not None:
return True
time.sleep(self.POLLING_INTERVAL_SECONDS)
设计亮点:
- 每次轮询使用全新的临时通道:避免因服务器重启导致的旧连接失效问题(C++ 中通常复用同一个
ClientContext,但也可采用类似策略)。 - 短超时:RPC 调用设置 2 秒超时,防止阻塞整个轮询循环。
- 异常忽略:
try-except捕获所有异常,仅当成功获取响应时才视为成功。这与 C++ 中静默忽略失败的设计完全一致。
触及了Python语言中资源管理和内存管理的两个核心概念。我们来详细解读您选中的这段代码,并阐述清楚
with语句和垃圾回收(Garbage Collection, GC)是如何协同工作以减少手动资源释放的。
with 语句:上下文管理器 (Context Manager)
您看到的 with grpc.insecure_channel(...) as channel: 写法,是Python中一个非常强大且推荐使用的语法结构,称为上下文管理协议。
它的核心思想是:为一个代码块的执行准备一个“上下文”(__enter__),并在代码块执行完毕后自动进行“清理”(__exit__)。
让我们分解一下这个过程:
-
进入上下文 (
__enter__):grpc.insecure_channel(self.server_addr)创建了一个gRPC通道(channel)对象。with语句接收到这个对象后,会调用该对象的__enter__方法。- 这个
__enter__方法负责建立和准备资源(在这里就是网络连接),然后返回通道对象本身。 as channel将__enter__方法返回的对象赋值给变量channel。
-
执行代码块:
- 接下来,
with代码块内部的语句被执行:grpc.channel_ready_future(channel).result(...) temp_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(channel) response = temp_stub.GetSWInfos(request, timeout=2) - 在这期间,
channel变量是可用的,代表一个活跃的gRPC连接。
- 接下来,
-
退出上下文 (
__exit__):- 当代码块执行结束时(无论是正常完成还是因为发生异常而中断),
with语句会自动调用通道对象的__exit__方法。 - 这个
__exit__方法是关键,它的职责是执行清理工作。对于gRPC通道来说,就是安全地关闭网络连接,释放所有相关的底层资源。
- 当代码块执行结束时(无论是正常完成还是因为发生异常而中断),
为什么这能“减少手动释放代码”?
如果没有 with 语句,您必须手动编写 try...finally 结构来确保资源一定被释放,代码会变得冗长且容易出错:
# 不使用 with 的等效代码
channel = None
try:
# 1. 手动创建资源
channel = grpc.insecure_channel(self.server_addr)
# 2. 使用资源
grpc.channel_ready_future(channel).result(...)
temp_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(channel)
response = temp_stub.GetSWInfos(request, timeout=2)
# ...
finally:
# 3. 手动确保资源被释放
if channel:
channel.close() # gRPC channel 需要被关闭
with 语句将这个 try...finally 模式优雅地封装了起来,让代码更简洁、更安全、意图更明确。
垃圾回收 (Garbage Collection, GC)
垃圾回收是Python自动内存管理的一部分。它的工作是回收不再被使用的对象所占用的内存。Python主要使用“引用计数”作为GC机制:当一个对象的引用计数变为0时(即没有任何变量指向它),它所占用的内存就会被释放。
with 语句与垃圾回收的关系与区别
这是问题的核心:with 和 GC 是两个不同层面的机制。
with语句管理的是“资源”:它关心的是像文件句柄、网络连接、数据库会话、线程锁这类需要显式打开和关闭的资源。它的清理时机是确定的、即时的——就在代码块结束的那一刻。- 垃圾回收管理的是“内存”:它关心的是对象占用的内存空间。它的回收时机是不确定的。你不知道一个对象的内存具体会在哪个时刻被GC回收,可能是在它不再被引用后的下一秒,也可能是几分钟后。
关键区别:我们不能依赖垃圾回收来关闭gRPC通道这类资源。
为什么?想象一下,如果我们在循环中创建了很多gRPC通道,但不使用
with或手动close()。这些通道对象在循环结束后可能没有引用了,但GC可能还没来得及运行。结果就是,大量的网络连接会保持打开状态,耗尽操作系统的资源(如文件描述符),最终导致程序崩溃。
with语句 确保了每次轮询尝试时创建的grpc.insecure_channel都是一个临时的、用完即弃的资源。无论GetSWInfos调用成功、失败还是超时,with语句都保证在代码块结束时,这个网络连接会被立即、确定地关闭。这完美地解决了我们之前讨论的“僵尸连接”问题。- 垃圾回收 会在稍后某个不确定的时间点,回收
channel、temp_stub、request、response这些Python对象所占用的内存。
简单来说:with 负责及时关门(释放资源),GC 负责打扫房间(回收内存)。两者各司其职,共同保证了程序的健壮和高效。
2.4.3 响应解析
def parse_sw_info_response(self, response):
# 尝试多种可能的字段名,适应不同 proto 版本
- 这种兼容性处理在 C++ 中通常通过访问具体字段实现,但 Python 的动态特性使字段探测更灵活。
2.5 完整升级流程 firmware_upgrade
if not self.upload_with_manual_info(...):
return False
if not self.wait_for_device_and_get_sw_info(...):
return False
return True
- 关注点分离:只编排业务顺序,不关心底层实现,与 C++ 的
FirmwareUpgrade函数如出一辙。
3. Python vs C++ gRPC 客户端对比
| 维度 | Python | C++ |
|---|---|---|
| 请求流实现 | 生成器(Generator):通过 yield 产生消息,隐式控制流。 |
显式 ClientWriter:手动调用 Write()、WritesDone()、Finish()。 |
| 代码风格 | 简洁、声明式,符合 Python 习惯。 | 控制精细、显式,符合 C++ RAII 理念。 |
| 错误处理 | 主要使用异常(grpc.RpcError),通过 e.code() 获取状态码。 |
检查每个 Write 返回值,通过 Finish() 返回的 Status 判断。 |
| 超时设置 | 在 stub 方法调用时直接传入 timeout 参数。 |
通过 ClientContext 的 set_deadline() 设置。 |
| 资源管理 | 生成器内的 with 自动管理文件;通道可作为上下文管理器使用。 |
智能指针管理 ClientWriter,RAII 管理资源。 |
| 轮询策略 | 可为每次轮询新建临时通道,避免连接复用问题。 | 通常复用同一个 ClientContext,但需注意服务器重启后的连接恢复。 |
| 动态特性 | 可灵活探测响应对象的字段,兼容多种 proto 版本。 | 字段访问在编译时确定,类型安全但灵活性较低。 |
3.1 相同点
- 底层均基于 gRPC C Core,网络传输、流控、序列化机制一致。
- 业务逻辑流程(上传 + 等待验证)完全相同。
- 超时、重试、错误码等概念通用。
3.2 Python 独特优势
- 开发效率高:生成器简化流式代码,异常处理更直观。
- 动态调试:可在运行时探测消息字段,适配不同 proto 版本。
- 资源管理简洁:
with语句和垃圾回收减少手动释放代码。
3.3 C++ 独特优势
- 性能极致:零成本抽象,适合对延迟敏感的场景。
- 类型安全:编译时检查字段存在性,避免运行时错误。
- 细粒度控制:可干预流的状态(如检查每个
Write是否成功)。
4. 总结
本 Python 客户端完整实现了与 C++ 版本相同的固件升级功能,但采用了更符合 Python 习惯的生成器、异常和动态特性。两者虽 API 风格迥异,但设计思想高度统一:通过客户端流式 RPC 分块上传大文件,结合盲等与轮询策略验证设备重启。理解这种跨语言实现,有助于你根据项目需求选择最合适的语言,并在不同技术栈间迁移时保持业务逻辑的清晰与可靠。

浙公网安备 33010602011771号