grpc客户端优化

📚 使用须知

  • 本博客内容仅供学习参考
  • 建议理解思路后独立实现
  • 欢迎交流讨论

task : grpc客户端优化

故事的开始:固件升级流程

我们的任务是实现一个通过 gRPC 控制的固件升级流程。从逻辑上看,它非常直接:

  1. 客户端通过一个 gRPC 流式 (Streaming) RPC 将固件文件上传到服务器。
  2. 服务器接收完文件后,执行一个脚本(例如 upgrade-rootfs.sh)来应用更新。
  3. 该脚本在最后会重启服务器,以加载新的固件。
  4. 客户端在上传完成后,进入一个轮询 (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 解析:

  1. 创建 ClientContext:包含调用元数据、截止时间等
  2. 创建流式写入器ClientWriter<FirmwareUpdateRequest>>
  3. 写入请求writer->Write(request) 发送测试请求
  4. 结束写入writer->WritesDone() 表示流结束
  5. 获取状态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 解析:

  1. WritesDone():通知服务器客户端已写完所有消息
  2. Finish():等待服务器完成处理并返回最终状态
  3. 响应处理:检查服务器的 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 技术要点总结:

  1. 混合模式

    • 固件上传使用 客户端流式 RPC(streaming)
    • 状态查询使用 一元 RPC(unary)
  2. 流式传输特点

    • 单连接多消息传输
    • 支持大文件分块
    • 需要明确的流结束信号
  3. 错误处理

    • 检查 gRPC 状态码
    • 处理服务器业务逻辑错误码
    • 优雅的重试机制
  4. 资源管理

    • 使用智能指针管理 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 也能正确处理。
  • 每次循环:
    1. 从文件读取一块数据到 buffer
    2. 创建一个新的 FirmwareUpdateRequest 消息,并通过 mutable_content() 获取 SoftwareItemContent 子消息。
    3. 将读取到的数据块通过 set_data 放入消息。
    4. 调用 writer->Write(content_request) 发送该数据块。
    5. 累计已发送字节数,并定期输出上传进度(代码中省略了进度计算部分,但实际工程中会包含)。
  • 该循环持续进行,直到整个文件被分块发送完毕。

第 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 实现,它将一个大文件拆分为:

  1. 一个携带元数据的消息(文件信息、哈希值等)
  2. N 个携带实际数据的消息(文件分块)

通过单一的 gRPC 流按顺序发送,最后等待服务器的确认响应。其核心设计思想与 Python 版本(使用生成器 yield)完全一致,但 C++ 的 API 更加显式,需要手动控制 WriteWritesDoneFinish,体现了 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 流如何建立、网络错误如何处理、轮询超时如何计算——这些技术细节全部被封装在 UploadWithManualInfoWaitForDeviceAndGetSWInfo 这两个独立的方法中。
  • 它只关心高级业务流程:其唯一职责就是定义“一个完整的固件升级 = 成功上传 + 成功等待验证”,并按顺序编排这两个步骤。

这种设计带来的好处:

  • 代码清晰易读:函数体只有寥寥几行,但任何人一眼就能看懂固件升级的整体流程。
  • 易于维护:如果需要修改上传逻辑(例如改变分块大小),只需修改 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 分钟的轮询循环。每次循环:

  1. 发起 RPC 调用:通过 config_stub_->GetSWInfos 向服务器请求当前的软件信息。
  2. 检查成功条件:只有同时满足 gRPC 状态为 OK响应中包含至少一条软件信息 时,才认为设备已完全恢复。此时函数打印新固件详情并返回 true
  3. 静默忽略失败:在设备重启完成前,RPC 调用几乎必然失败(连接拒绝、超时、服务不可用等)。代码通过 try-catch 块捕获所有异常,并对 status 非 OK 的情况不做任何处理——所有失败都被刻意忽略。这不是疏忽,而是一种容错策略:它视失败为重启过程中的常态,唯一的追求是等待那个最终的成功。
  4. 等待后重试:每次尝试后,无论成功与否,线程都暂停 5 秒再进行下一轮尝试。

轮询参数

  • 重试间隔:5 秒(兼顾响应及时性与网络压力)
  • 最大尝试次数:通常为 120 次(10 分钟 ÷ 5 秒)。若 10 分钟后仍未成功,函数放弃并返回 false

功能总结:衔接上传与成功的桥梁

WaitForDeviceAndGetSWInfo 函数的核心价值在于解决了一个分布式系统中的经典问题:如何在一个不确定的时间窗口内,验证一个正在重启的远程服务是否已恢复正常

它通过以下设计完美地衔接了“固件上传”与“升级成功”这两个状态:

  • 容错性:无视中间的所有失败,只关注最终的成功。
  • 资源友好:初始盲等减少了无效请求,轮询间隔避免了高频冲击。
  • 确定性:最大等待时间(10 分钟)保证了流程不会无限期阻塞。

正是这种“耐心验证者”的角色,使得整个固件升级流程能够自动化、可靠地完成。如果没有它,上传固件后客户端将无法知晓何时该继续下一步(如触发测试或通知用户),整个端到端流程就会断裂。

gRPC python客户端

在这篇技术博文中,我们将深入剖析一个在实现 gRPC 固件升级流程中遇到的典型问题:当服务器被强制重启后,客户端为何无法通过轮询(Polling)重新建立联系? 我们不仅会揭示问题背后的 gRPC 连接管理机制,还将展示如何通过优秀的代码结构设计,将一个复杂、臃肿的函数重构为清晰、健壮、可维护的模块。

场景设定:固件升级

我们的目标是构建一个 Python 客户端,通过 gRPC 实现对嵌入式设备的固件升级。其核心流程分为两个主要阶段:

  1. 文件上传:客户端使用 gRPC 客户端流式(Client Streaming)RPC,将一个数 MB 大小的固件文件高效地传输到服务器。
  2. 状态验证:服务器在接收文件后会执行重启以应用更新。客户端必须进入一个轮询循环,使用一个一元(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. 混合了大量的异常处理

这种结构难以阅读、测试和维护。

重构后的结构:职责分明的“经理”与“专家”

我们将其重构为三个职责分明的函数,如同一个分工明确的团队:

  1. _send_firmware_stream (流上传专家)

    • 单一职责:专门负责处理 gRPC 客户端流式 RPC 的所有细节。
    • 实现:内部包含 request_iterator 生成器,管理文件块的读取和 tqdm 进度条的更新。它只关心如何高效地把文件发送出去,并返回一个简单的成功或失败标志。
  2. _wait_for_device_and_get_sw_info (轮询专家)

    • 单一职责:专门负责在服务器重启后,通过创建临时连接进行轮询,直到确认设备上线或超时。
    • 实现:包含了我们解决“僵尸连接”问题的核心逻辑。
  3. 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
    

通过这次重构,代码的可读性、可测试性和可维护性都得到了质的飞跃。

结论

本次实践带给我们两个核心启示:

  1. gRPC 连接管理:在涉及服务强制重启的场景下,必须对客户端持有的长连接状态保持警惕。最健壮的策略是在状态不可信时果断抛弃旧连接,并在需要时创建新的临时连接来完成任务。
  2. 代码结构设计:遵循单一职责原则进行重构,将一个复杂的流程拆分为多个职责单一的函数,是提升代码质量的必经之路。它不仅让代码更易于理解,也使得像“僵尸连接”这样的复杂问题更容易被定位和修复。

从一个棘手的 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. 指挥中心:mainargparse

这是用户与程序交互的入口。通过使用 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) {}
}

使用 protocgrpc_cpp_plugin 编译后,会生成四个核心类:

  • RouteGuide::Service(服务端需继承的基类)
  • RouteGuide::Stub(客户端存根)
  • 以及配套的 ServerReaderServerWriterClientReader 等流式辅助类 。

二、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 即完成
  }
};

特点:直接拿到 requestresponse 对象,填充后返回 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(&note)) {          // 不断读客户端发来的消息
    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(&note)) {
  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++ 实现这四种服务方式的套路非常统一:

  1. 服务端:根据类型选择 ServerReader/ServerWriter/ServerReaderWriter,在循环中 ReadWrite
  2. 客户端:根据类型选择 ClientReader/ClientWriter/ClientReaderWriter,操作完后调用 WritesDone()Finish()
  3. 状态处理:所有方法最终都返回 Status,检查 ok() 判断成功与否 。

这种显式控制的 API 虽然代码量稍多,但给开发者提供了最大的控制权,特别适合对性能有极致要求的场景。


gRPC 实战深潜

为何 C++ 能“死等”,而 Python 却要“另起炉灶”?

在使用 gRPC 构建分布式系统时,我们常常会遇到一个棘手的场景:客户端需要与一个可能会重启的服务进行通信。一个典型的例子就是固件升级:客户端上传完固件后,服务器需要重启以应用更新。此时,客户端如何优雅地等待服务器恢复并确认升级成功呢?

最近,在开发一个固件升级工具时,我们发现 C++ 和 Python 的 gRPC 客户端在处理这一场景时,采取了截然不同的策略,这背后揭示了 gRPC 在不同语言实现中的深层差异。

场景设定:等待重启的服务器

我们的业务流程如下:

  1. 客户端通过 gRPC 连接到设备(服务器)。
  2. 客户端通过客户端流式 RPC 上传固件文件。
  3. 服务器接收文件后,执行升级并重启
  4. 客户端需要轮询服务器,直到它重新上线,并通过一个一元 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 调用因为服务器不可达而失败(statusok),它也只是简单地忽略错误,然后继续下一次循环。它似乎坚信,只要坚持尝试,这个 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 客户端在每一次轮询尝试中,都会创建一个全新的 channelstub。它不信任旧的连接,而是选择在每次尝试时都“另起炉灶”,发起一次全新的连接请求。我们称之为“重新请求”策略。

为什么会存在这种差异?

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() 获取状态码(如 UNAVAILABLEDEADLINE_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__)。

让我们分解一下这个过程:

  1. 进入上下文 (__enter__):

    • grpc.insecure_channel(self.server_addr) 创建了一个gRPC通道(channel)对象。
    • with 语句接收到这个对象后,会调用该对象的 __enter__ 方法。
    • 这个 __enter__ 方法负责建立和准备资源(在这里就是网络连接),然后返回通道对象本身。
    • as channel__enter__ 方法返回的对象赋值给变量 channel
  2. 执行代码块:

    • 接下来,with 代码块内部的语句被执行:
      grpc.channel_ready_future(channel).result(...)
      temp_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(channel)
      response = temp_stub.GetSWInfos(request, timeout=2)
      
    • 在这期间,channel 变量是可用的,代表一个活跃的gRPC连接。
  3. 退出上下文 (__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 语句都保证在代码块结束时,这个网络连接会被立即、确定地关闭。这完美地解决了我们之前讨论的“僵尸连接”问题。
  • 垃圾回收 会在稍后某个不确定的时间点,回收 channeltemp_stubrequestresponse 这些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 参数。 通过 ClientContextset_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 分块上传大文件,结合盲等与轮询策略验证设备重启。理解这种跨语言实现,有助于你根据项目需求选择最合适的语言,并在不同技术栈间迁移时保持业务逻辑的清晰与可靠。

posted @ 2026-02-26 14:27  mo686  阅读(28)  评论(0)    收藏  举报