跳到主要内容
C++ 分布式语音识别服务实践 | 极客日志
C++ AI 算法
C++ 分布式语音识别服务实践 分布式语音识别服务基于 C++ 结合 brpc 框架、etcd 注册中心及百度 AI SDK,实现了高可用的 RPC 接口。服务端负责语音识别请求处理与 ASR 调用,客户端通过服务发现与负载均衡发起请求。文章详细解析了 Protobuf 接口定义、ASR 封装、服务注册发现及信道管理模块的核心代码,并总结了编译期类型歧义、文件路径权限及 etcd watcher 生命周期等常见问题的排查与解决方案,为 C++ 分布式服务开发提供实践参考。
机器人 发布于 2026/3/21 更新于 2026/9/11 60 浏览基于 brpc+etcd 与百度 AI SDK 的分布式语音识别服务实践
一、项目背景与核心功能
本项目基于 C++ 构建了一个分布式语音识别子服务,核心目标是提供高可用的 RPC 接口,支持客户端上传 PCM 音频文件并返回识别结果。技术栈选型如下:
RPC 框架 :brpc(百度开源高性能 RPC 框架,支持多种协议);
数据序列化 :Protobuf(定义 RPC 接口和数据结构);
服务注册与发现 :etcd(分布式键值存储,实现服务上下线感知);
语音识别能力 :百度 AI 语音 SDK(提供成熟的 PCM 音频转文字能力);
日志与配置 :spdlog(高性能日志库)、gflags(命令行参数解析)。
项目分为服务端和客户端两部分:
服务端:实现 RPC 服务、注册到 etcd、封装百度 AI SDK 调用;
客户端:通过 etcd 发现服务、读取音频文件、发起 RPC 请求。
二、核心代码架构解析
为了保证代码的可扩展性和可维护性,采用模块化 + Builder 模式设计,各组件职责单一,解耦清晰。
1. 整体架构概览
语音识别服务
├─ 服务端(speech_server)
│ ├─ RPC 服务实现(SpeechServiceImpl):处理语音识别请求
│ ├─ 服务构建器(SpeechServerBuilder):组装各模块(ASR、注册、RPC)
│ ├─ 语音识别封装(ASRClient):调用百度 AI SDK
│ ├─ 服务注册(Registry):将服务节点注册到 etcd
│ └─ 日志配置:初始化 spdlog 日志
└─ 客户端(speech_client)
├─ 服务发现(Discovery):从 etcd 获取服务节点
├─ 信道管理(ServiceManager):RR 轮询负载均衡
└─ 音频读取:调用百度 AI SDK 工具函数读取 PCM 文件
2. 关键模块代码解析
(1)RPC 接口定义(Protobuf)
首先通过 speech.proto 定义 RPC 服务和数据结构,明确请求(音频数据)和响应(识别结果)格式:
syntax = "proto3";
package zrt; // 命名空间,避免类名冲突
option cc_generic_services = true; // 生成 C++ RPC 服务代码
// 语音识别请求
message SpeechRecognitionReq {
string request_id = 1; // 请求 ID(用于追踪)
bytes speech_content = 2; // 核心:PCM 音频数据(二进制)
optional string user_id = 3; // 可选:用户 ID
optional string session_id = 4; // 可选:会话 ID(鉴权用)
}
// 语音识别响应
message SpeechRecognitionRsp {
string request_id = 1; // 对应请求的 ID
bool success = 2; // 识别是否成功
optional string errmsg = 3; // 失败原因(success=false 时必选)
optional string recognition_result = 4; // 识别结果(success=true 时必选)
}
// RPC 服务定义
service SpeechService {
rpc SpeechRecognition(SpeechRecognitionReq) returns (SpeechRecognitionRsp);
}
通过 protoc 编译生成 speech.pb.cc 和 speech.pb.h,为 RPC 服务提供基础代码。
(2)语音识别封装(ASRClient)
封装百度 AI SDK 的调用逻辑,对外提供简洁的 recognize 接口,隐藏 SDK 细节:
#pragma once
#
zrt {
{
:
ptr = std::shared_ptr<ASRClient>;
( std::string &app_id, std::string &api_key, std::string &secret_key)
: _client(app_id, api_key, secret_key) {}
{
Json::Value result = _client. (speech_data, , , aip::null);
(result[ ]. () != ) {
( , result[ ]. ());
err = result[ ]. ();
;
}
result[ ][ ]. ();
}
:
aip::Speech _client;
};
}
include
"../third/include/aip-cpp-sdk/speech.h"
#include "logger.hpp"
namespace
class
ASRClient
public
using
ASRClient
const
const
const
std::string recognize (const std::string &speech_data, std::string &err)
recognize
"pcm"
16000
if
"err_no"
asInt
0
LOG_ERROR
"语音识别失败:{}"
"err_msg"
asString
"err_msg"
asString
return
""
return
"result"
0
asString
private
(3)RPC 服务实现(SpeechServiceImpl) 继承 Protobuf 生成的服务基类,实现 SpeechRecognition 接口,处理客户端请求:
class SpeechServiceImpl : public zrt::SpeechService {
public :
SpeechServiceImpl (const ASRClient::ptr &asr_client) : _asr_client(asr_client) {}
void SpeechRecognition (google::protobuf::RpcController* controller,
const ::zrt::SpeechRecognitionReq* request,
::zrt::SpeechRecognitionRsp* response,
::google::protobuf::Closure* done) {
LOG_DEBUG ("收到语音转文字请求!request_id: {}" , request->request_id ());
brpc::ClosureGuard rpc_guard (done) ;
std::string err;
std::string res = _asr_client->recognize (request->speech_content (), err);
response->set_request_id (request->request_id ());
if (res.empty ()) {
response->set_success (false );
response->set_errmsg ("语音识别失败:" + err);
return ;
}
response->set_success (true );
response->set_recognition_result (res);
}
private :
ASRClient::ptr _asr_client;
};
(4)服务注册与发现(etcd 集成)
服务注册(Registry) :将服务节点注册到 etcd,并通过 lease(租约)维持节点存活,服务下线时自动删除;
服务发现(Discovery) :监听 etcd 的服务目录,感知服务上下线,并回调更新信道。
class Discovery {
public :
using ptr = std::shared_ptr<Discovery>;
using NotifyCallback = std::function<void (std::string, std::string)>;
Discovery (const std::string &host, const std::string &basedir,
const NotifyCallback &put_cb, const NotifyCallback &del_cb)
: _client(std::make_shared <etcd::Client>(host)), _put_cb(put_cb), _del_cb(del_cb) {
auto resp = _client->ls (basedir).get ();
if (!resp.is_ok ()) {
LOG_ERROR ("获取服务列表失败:{}" , resp.error_message ());
return ;
}
for (int i = 0 ; i < resp.keys ().size (); ++i) {
if (_put_cb) _put_cb(resp.key (i), resp.value (i).as_string ());
}
_watcher = std::make_shared <etcd::Watcher>(*_client.get (), basedir,
std::bind (&Discovery::callback, this , std::placeholders::_1),
true );
}
private :
void callback (const etcd::Response &resp) {
if (!resp.is_ok ()) {
LOG_ERROR ("etcd 事件错误:{}" , resp.error_message ());
return ;
}
for (auto &ev : resp.events ()) {
if (ev.event_type () == etcd::Event::PUT) {
LOG_DEBUG ("服务上线:{}-{}" , ev.kv ().key (), ev.kv ().as_string ());
if (_put_cb) _put_cb(ev.kv ().key (), ev.kv ().as_string ());
} else if (ev.event_type () == etcd::Event::DELETE_) {
LOG_DEBUG ("服务下线:{}-{}" , ev.prev_kv ().key (), ev.prev_kv ().as_string ());
if (_del_cb) _del_cb(ev.prev_kv ().key (), ev.prev_kv ().as_string ());
}
}
}
private :
NotifyCallback _put_cb;
NotifyCallback _del_cb;
std::shared_ptr<etcd::Client> _client;
std::shared_ptr<etcd::Watcher> _watcher;
};
(5)信道管理与负载均衡(ServiceManager) 客户端通过 ServiceManager 管理 RPC 信道,采用 RR(Round-Robin)轮询策略实现负载均衡,避免单节点压力过大:
class ServiceManager {
public :
using ptr = std::shared_ptr<ServiceManager>;
void declared (const std::string &service_name) {
std::unique_lock<std::mutex> lock (_mutex) ;
_follow_services.insert (service_name);
}
void onServiceOnline (const std::string &service_instance, const std::string &host) {
std::string service_name = getServiceName (service_instance);
if (_follow_services.count (service_name) == 0 ) {
LOG_DEBUG ("{}服务上线,无需关注" , service_name);
return ;
}
auto service = getOrCreateServiceChannel (service_name);
service->append (host);
}
ServiceChannel::ChannelPtr choose (const std::string &service_name) {
std::unique_lock<std::mutex> lock (_mutex) ;
auto it = _services.find (service_name);
if (it == _services.end ()) {
LOG_ERROR ("无{}服务的可用节点" , service_name);
return nullptr ;
}
return it->second->choose ();
}
private :
std::string getServiceName (const std::string &service_instance) {
auto pos = service_instance.find_last_of ('/' );
return pos == std::string::npos ? service_instance : service_instance.substr (0 , pos);
}
private :
std::mutex _mutex;
std::unordered_set<std::string> _follow_services;
std::unordered_map<std::string, ServiceChannel::ptr> _services;
};
三、核心问题与解决方案(问题排查) 在项目开发过程中,遇到了多个编译期和运行期问题,以下是关键问题的排查过程和解决方案,均为 C++ 分布式服务开发中的常见坑。
1. 编译期:百度 AI SDK 的 toupper 重载歧义
问题现象 编译客户端时,报 std::transform 调用 toupper 的重载歧义错误:
error: no matching function for call to
note: couldn
原因分析 C++ 中有两个 toupper 版本,编译器无法确定使用哪个:
<cctype> 中的 int toupper(int c):处理单个字符,参数为 int(兼容 EOF);
<locale> 中的 template <class charT> charT toupper(charT c, const locale& loc):带本地化参数的模板函数。
百度 AI SDK 的 utils.h 中直接调用 std::transform(..., toupper),未明确版本,导致歧义。
解决方案 用 lambda 表达式显式指定 toupper 版本,消除歧义,并处理 char 类型转换(避免负数问题):
std::transform (src.begin (), src.end (), src.begin (), toupper);
std::transform (src.begin (), src.end (), src.begin (), [](unsigned char c) {
return static_cast <char >(std::toupper (c));
});
关键思路 :lambda 作为'中间层',明确参数类型和函数版本,让编译器无需猜测。
2. 编译期:函数漏写 return 语句的警告
问题现象 修改 utils.h 后,编译报'无返回语句'警告:
warning : no return statement in function returning non-void [-Wreturn -type ]
原因分析 to_upper/to_lower 函数声明返回 std::string,但修改时不小心删除了 return src; 语句,导致函数无返回值(C++ 中属于未定义行为,编译器宽容处理为警告,但运行时可能返回随机值)。
解决方案 补全 return 语句,确保函数返回处理后的字符串:
std::string aip::to_upper (std::string src) {
std::transform (...);
return src;
}
3. 运行期:音频文件读取失败(invalid audio length)
问题现象 客户端运行时,输出 file_content.size() = 0,百度 AI SDK 返回'invalid audio length':
0 语音识别失败:invalid audio length
原因分析
路径错误 :客户端用相对路径 "16k.pcm",但运行目录(如 build/)下无此文件;
文件权限 :文件存在但无读权限;
格式错误 :文件不是百度 SDK 要求的'16kHz 采样率、16 位深度、单声道'PCM。
解决方案
使用绝对路径 :明确指定文件位置,避免相对路径陷阱:
aip::get_file_content ("16k.pcm" , &file_content);
aip::get_file_content ("/home/zrt/workspace/16k.pcm" , &file_content);
ls -l 16k.pcm
chmod +r 16k.pcm
验证 PCM 格式 :用 ffmpeg 转换为标准格式:
ffmpeg -i test.wav -ar 16000 -ac 1 -sample_fmt s16le 16k.pcm
4. 运行期:etcd watcher 警告(watcher doesn't exit normally)
问题现象 程序退出时,报 watcher doesn't exit normally 警告。
原因分析 Discovery 的 watcher 线程未正常停止,程序退出时强制终止线程导致警告。
解决方案 在 Discovery 析构函数中主动取消 watcher:
~Discovery () {
_watcher->Cancel ();
}
四、项目运行流程
./speech_server --etcd_host=http://127.0.0.1:2379 --port=8080
./speech_client --etcd_host=http://127.0.0.1:2379 --speech_service=/service/speech_service
12345
收到响应:111111
收到响应:你好,世界
五、总结与经验
第三方 SDK 踩坑 :第三方库代码可能不严谨(如百度 SDK 的 toupper 歧义),需针对性修改,修改时注意保留原功能;
分布式服务核心 :服务注册发现(etcd)和负载均衡(RR)是分布式服务的基石,需保证高可用和线程安全;
C++ 编译问题 :编译错误需重点看 error: 前的具体代码行,尤其是模板推导失败(如重载歧义),可通过显式类型或 lambda 解决;
路径与权限 :文件操作尽量用绝对路径,避免运行目录依赖;权限问题在 Linux 下容易被忽略,需提前验证。
该项目不仅实现了语音识别的核心功能,更重要的是梳理了 C++ 分布式服务的开发流程和问题排查思路,后续可扩展多节点部署、熔断降级等高级特性。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
随机西班牙地址生成器 随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online