文章目录
- [微服务即时通讯系统(服务端)------Speech 语音模块开发:从架构到落地](#微服务即时通讯系统(服务端)——Speech 语音模块开发:从架构到落地)
-
- 一、模块整体架构:三层核心设计
- 二、核心组件开发:从基础到实现
-
- [1. 基础依赖封装:ASRTool 与 EtcdTool](#1. 基础依赖封装:ASRTool 与 EtcdTool)
- [2. RPC 服务端开发:brpc 框架落地](#2. RPC 服务端开发:brpc 框架落地)
-
- [(1)RPC 接口定义:protobuf 协议](#(1)RPC 接口定义:protobuf 协议)
- [(2)RPC 接口实现:SpeechServiceImpl](#(2)RPC 接口实现:SpeechServiceImpl)
- (3)服务构建与启动:SpeechServerBuilder
- [3. RPC 客户端开发:服务发现与请求发起(附带简略版本代码)](#3. RPC 客户端开发:服务发现与请求发起(附带简略版本代码))
-
- [(1)服务发现:依托 brpcChannelTool](#(1)服务发现:依托 brpcChannelTool)
- (2)语音请求发起:
- [4. 构建脚本:CMake 配置](#4. 构建脚本:CMake 配置)
- 三、关键问题与解决方案
-
- [1. 服务高可用:etcd 租约与监听](#1. 服务高可用:etcd 租约与监听)
- [2. 负载均衡:轮询策略](#2. 负载均衡:轮询策略)
- [3. 异常防护:多层级错误处理](#3. 异常防护:多层级错误处理)
- 四、模块部署与测试
-
- [1. 部署流程](#1. 部署流程)
- [2. 测试验证](#2. 测试验证)
- 五、总结与扩展
微服务即时通讯系统(服务端)------Speech 语音模块开发:从架构到落地
在分布式服务架构中,语音识别模块需要兼顾高可用、可扩展与低延迟,本文将以实际代码为例,拆解基于 brpc 框架、etcd 服务发现与第三方语音 API 的 Speech 模块开发流程,详解核心组件设计与关键逻辑实现。
一、模块整体架构:三层核心设计
Speech 语音模块采用 "服务端 - 客户端 - 中间件" 三层架构,通过模块化拆分降低耦合,同时依托 etcd 实现服务注册发现,确保分布式环境下的稳定性。整体架构如下:
| 层级 | 核心组件 | 功能职责 |
|---|---|---|
| 服务端 | SpeechServer、SpeechServiceImpl | 提供 RPC 语音识别接口,处理客户端请求 |
| 客户端 | TestClient | 发起语音识别请求,解析服务端响应 |
| 中间件 | ASRTool、EtcdTool、brpcChannelTool | 封装第三方语音 API、etcd 服务注册发现、RPC 信道管理 |
核心流程为:客户端通过 etcd 发现语音服务实例 → 发起 RPC 请求(携带 PCM 语音数据)→ 服务端调用 ASRTool 解析语音 → 返回识别结果给客户端。
二、核心组件开发:从基础到实现
1. 基础依赖封装:ASRTool 与 EtcdTool
(1)ASRTool:语音识别能力封装
模块依赖第三方语音 API(如百度智能云语音)实现语音转文字,ASRTool::SpeechToText类封装了 API 调用逻辑,对外暴露Recognize接口,简化服务端调用:
- 入参:PCM 格式语音数据(字符串)、错误信息引用(用于传出错误详情)
- 出参:识别结果字符串(空串表示识别失败)
- 核心逻辑:处理 API 鉴权、请求格式转换、响应解析,将第三方接口差异屏蔽在工具类内部。
cpp
#include "LogTool.hpp"
#include <aip-cpp-sdk/base/http.h>
#include <aip-cpp-sdk/speech.h>
#include <curl/curl.h>
#include <jsoncpp/json/json.h>
#include <memory>
#include <stdexcept>
#include <string>
namespace ASRTool
{
class SpeechToText
{
public:
SpeechToText(const std::string& app_id
, const std::string& api_key
, const std::string& secret_key )
{
LogModule::Log::Init();
app_id_ = app_id;
api_key_ = api_key;
secret_key_ = secret_key;
client_ = std::make_unique<aip::Speech>(app_id_, api_key_, secret_key_); // 若失败直接抛异常
if (!IsValid())
{
throw std::runtime_error("Speech client initialize fail!");
}
}
std::string Recognize(const std::string& file_content
, std::string& err)
{
if (!IsValid())
{
throw std::runtime_error("Speech client not initialized");
}
Json::Value result;
TryCatch([&](){
result = client_->recognize(file_content, "pcm", 16000, aip::null);
});
if(result["err_no"].asInt() != 0)
{
//throw std::runtime_error(result["err_msg"].asString());
LOG_ERROR("解析语音失败:{}", result["err_msg"].asString());
err = result["err_msg"].asString();
}
else
{
return result["result"][0].asString();
}
return std::string();
}
// 检查客户端是否初始化成功
bool IsValid() const
{
return client_ != nullptr;
}
~SpeechToText()
{}
private:
// 改进 TryCatch:增加参数控制是否传播异常
template<typename T = std::exception>
bool TryCatch(std::function<void ()> func, bool propagate = false)
{
try
{
func();
return true; // 执行成功
}
catch (const T& e)
{
LOG_ERROR("Error Get:{}", e.what());
if (propagate) {
throw e; // 向上传播异常
}
return false; // 执行失败
}
}
private:
std::string app_id_;
std::string api_key_;
std::string secret_key_;
std::unique_ptr<aip::Speech> client_;
};
};
(2)EtcdTool:服务注册与发现
基于 etcd-cpp-api 封装RegisterEtcd(服务注册)与MonitorEtcd(服务监听),解决分布式环境下的服务定位问题:
- RegisterEtcd :服务端启动时,将服务实例(如
/service/speech_service/instance)与访问地址(如127.0.0.1:10001)注册到 etcd,并通过租约(Lease)保活,避免无效实例残留。 - MonitorEtcd :客户端启动时,监听 etcd 中语音服务根路径(如
/service/speech_service),实时感知服务实例上线 / 下线,并触发Online/Offline回调更新本地信道列表。
2. RPC 服务端开发:brpc 框架落地
服务端核心是SpeechServiceImpl(RPC 接口实现)与SpeechServer(服务管理),通过 Builder 模式(SpeechServerBuilder)简化初始化流程。
(1)RPC 接口定义:protobuf 协议
首先通过speech.proto定义 RPC 接口与数据结构,确保客户端与服务端数据交互一致性:
protobuf
syntax = "proto3";
package bite_im;
option cc_generic_services = true;
message SpeechRecognitionReq {
string request_id = 1; //请求ID
bytes speech_content = 2; //语音数据
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; //失败原因
optional string recognition_result = 4; //识别后的文字数据
}
//语音识别Rpc服务及接口的定义
service SpeechService {
rpc SpeechRecognition(SpeechRecognitionReq) returns (SpeechRecognitionRsp);
}
通过 protoc 编译生成speech.pb.h与speech.pb.cc,作为 RPC 接口的基础代码。
(2)RPC 接口实现:SpeechServiceImpl
SpeechServiceImpl继承自 protobuf 生成的SpeechService类,重写SpeechRecognition方法,处理核心业务逻辑:
- 请求参数校验 :获取请求中的
speech_content(语音数据)与request_id(请求唯一标识)。 - 语音识别调用 :通过
ASRTool::Recognize解析语音数据,获取识别结果。 - 响应构建:
- 识别成功:设置
success=true与recognition_result(识别文本)。 - 识别失败:设置
success=false与errmsg(错误详情)。
- 识别成功:设置
- 异常处理 :捕获
std::exception,避免服务崩溃,同时记录错误日志。
cpp
class SpeechServiceImpl : public bite_im::SpeechService
{
public:
SpeechServiceImpl(std::shared_ptr<ASRTool::SpeechToText>& asr)
{
asr_ = asr;
}
void SpeechRecognition(google::protobuf::RpcController* ctl
, const SpeechRecognitionReq* req
, SpeechRecognitionRsp* rep
, google::protobuf::Closure* done) override
{
try
{
brpc::ClosureGuard done_guard(done);
std::string err;
std::string res = asr_->Recognize(req->speech_content(), err);
if(res.empty())
{
rep->set_request_id(req->request_id());
rep->set_success(false);
rep->set_errmsg(err);
}
else
{
rep->set_request_id(req->request_id());
rep->set_success(true);
rep->set_recognition_result(res);
}
}
catch (const std::exception& e)
{
rep->set_request_id(req->request_id());
rep->set_success(false);
rep->set_errmsg(e.what());
LOG_ERROR("SpeechServiceImpl Recognize error:{}", e.what());
}
}
~SpeechServiceImpl()
{}
private:
std::shared_ptr<ASRTool::SpeechToText> asr_;
};
(3)服务构建与启动:SpeechServerBuilder
采用 Builder 模式(SpeechServerBuilder)封装服务初始化流程,降低代码耦合,核心步骤:
make_asr_object:初始化ASRTool::SpeechToText实例(传入 API 的 app_id、api_key 等鉴权信息)。make_regs_object:初始化RegisterEtcd实例,将服务注册到 etcd(设置租约 TTL=10s)。make_rpc_object:初始化 brpc 服务器,添加SpeechServiceImpl接口,绑定监听端口(如 10001),设置 IO 线程数与超时时间。build:校验所有组件初始化完成,生成SpeechServer实例,调用start()启动服务(RunUntilAskedToQuit阻塞等待停止信号)。
cpp
class SpeechServerBuilder
{
public:
SpeechServerBuilder()
{}
void make_asr_object(const std::string& app_id
, const std::string& api_key
, const std::string& secret_key)
{
asr_ = std::make_shared<ASRTool::SpeechToText>(app_id, api_key, secret_key);
}
void make_regs_object(const std::string& reghost
, const std::string &service_name
, const std::string &access_host)
{
regs_ = std::make_shared<Etcd_Tool::RegisterEtcd>(reghost);
Etcd_Tool::resinfo_t resinfo;
resinfo.service_name_ = service_name;
resinfo.service_addr_ = access_host;
resinfo.ttl_ = 10;
regs_->Register(resinfo);
}
void make_rpc_object(in_port_t port
, int timeout_sec
, size_t threadnums)
{
if(!asr_)
{
LOG_ERROR("asr需要优先实例化!");
abort();
}
brpcServer_ = std::make_shared<brpc::Server>();
brpc::ServerOptions op;
op.idle_timeout_sec = timeout_sec;
op.num_threads = threadnums;
SpeechServiceImpl *speech_service = new SpeechServiceImpl(asr_);
int ret = brpcServer_->AddService(speech_service,
brpc::ServiceOwnership::SERVER_OWNS_SERVICE);
if (ret == -1)
{
LOG_ERROR("添加Rpc服务失败!");
abort();
}
// 启动服务
if (brpcServer_->Start(port, &op) != 0)
{
LOG_ERROR("启动服务失败!");
abort();
}
}
std::shared_ptr<SpeechServer> build()
{
if(!asr_)
{
LOG_ERROR("asr未实例化!");
abort();
}
if(!regs_)
{
LOG_ERROR("regs未实例化!");
abort();
}
if(!brpcServer_)
{
LOG_ERROR("rpc未实例化!");
abort();
}
return std::make_shared<SpeechServer>(asr_, regs_, brpcServer_);
}
~SpeechServerBuilder()
{}
private:
std::shared_ptr<ASRTool::SpeechToText> asr_;
std::shared_ptr<Etcd_Tool::RegisterEtcd> regs_;
std::shared_ptr<brpc::Server> brpcServer_;
};
3. RPC 客户端开发:服务发现与请求发起(附带简略版本代码)
客户端(TestClient)的核心是通过 etcd 发现服务实例,发起 RPC 请求,核心流程:
(1)服务发现:依托 brpcChannelTool
brpcChannelTool::ServiceManager管理 RPC 信道,与MonitorEtcd联动:
- 客户端启动时,
MonitorEtcd监听 etcd 中语音服务路径,触发Online回调将服务实例添加到ServiceManager。
cpp
#pragma once
#include <cstdint>
#include <etcd/Client.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/KeepAlive.hpp>
#include <mutex>
#include <string>
#include <functional>
#include <memory>
#include <unordered_map>
#include <vector>
#include "LogTool.hpp"
namespace Etcd_Tool
{
// 1. 服务注册信息结构体
typedef struct ResgisterInfo
{
std::string service_name_; // 服务名
std::string service_addr_; // 服务地址(ip:port)
int ttl_ = 30; // 租约时间(默认30秒)
} resinfo_t;
// 2. 服务注册类(核心:服务注册、注销、租约保活)
class RegisterEtcd
{
public:
// 构造:初始化etcd客户端并验证连接
RegisterEtcd(const std::string& serverinfo)
{
LogModule::Log::Init();
try
{
etcd_client_ = std::make_shared<etcd::Client>(serverinfo);
// 连接测试
auto test_res = etcd_client_->get("/").get();
if (test_res.is_ok() || test_res.error_code() == 100)
{
LOG_TRACE("etcd连接成功: {}", serverinfo);
}
else
{
LOG_ERROR("etcd连接失败: {}", test_res.error_message());
etcd_client_.reset();
abort();
}
}
catch (...) { LOG_ERROR("创建etcd客户端异常"); }
}
// 核心:注册服务(绑定租约)
bool Register(const resinfo_t& info)
{
std::lock_guard<std::mutex> lock(mutex_);
try
{
// 避免重复注册
if (keepalives_.count(info.service_name_))
{
LOG_DEBUG("服务已注册: {}", info.service_name_);
return false;
}
// 创建租约保活
auto keepalive = etcd_client_->leasekeepalive(info.ttl_).get();
if (!keepalive) { LOG_ERROR("创建租约失败"); return false; }
// 注册服务(绑定租约ID)
auto res = etcd_client_->put(info.service_name_, info.service_addr_, keepalive->Lease()).get();
if (!res.is_ok()) { LOG_ERROR("注册失败: {}", res.error_message()); return false; }
// 本地缓存
keepalives_[info.service_name_] = keepalive;
services_.push_back(info);
LOG_INFO("服务注册成功: {}", info.service_name_);
return true;
}
catch (...) { LOG_ERROR("注册服务异常"); return false; }
}
// 核心:注销服务(释放租约+删除键)
bool UnRegister(const std::string& service_name)
{
std::lock_guard<std::mutex> lock(mutex_);
try
{
auto it = keepalives_.find(service_name);
if (it == keepalives_.end()) { LOG_DEBUG("服务不存在: {}", service_name); return false; }
// 释放租约+删除etcd键
it->second.reset();
auto res = etcd_client_->rm(service_name).get();
if (!res.is_ok()) { LOG_ERROR("注销失败: {}", res.error_message()); return false; }
// 清理本地缓存
keepalives_.erase(it);
services_.erase(std::remove_if(services_.begin(), services_.end(),
[&](const resinfo_t& e) { return e.service_name_ == service_name; }), services_.end());
LOG_INFO("服务注销成功: {}", service_name);
return true;
}
catch (...) { LOG_ERROR("注销服务异常"); return false; }
}
// 批量清理所有服务
bool Clear()
{
std::vector<std::string> service_names;
{
std::lock_guard<std::mutex> lock(mutex_);
for (auto& [name, _] : keepalives_) service_names.push_back(name);
}
bool all_ok = true;
for (auto& name : service_names)
{
if (!UnRegister(name)) all_ok = false;
}
return all_ok;
}
private:
std::shared_ptr<etcd::Client> etcd_client_; // etcd客户端
std::unordered_map<std::string, std::shared_ptr<etcd::KeepAlive>> keepalives_; // 租约缓存
std::vector<resinfo_t> services_; // 已注册服务列表
mutable std::mutex mutex_; // 线程安全锁
};
// 3. 监视相关定义
enum class filetype { DIR, FILE }; // 监视类型:目录/文件
using callbackfunc = std::function<void (const std::string&, const std::string&)>; // 回调(键+值)
// 监视信息结构体
typedef struct Monitorinfo
{
filetype type_; // 监视类型
std::string monitor_path_; // 监视路径
callbackfunc put_on_; // 上线回调(PUT事件)
callbackfunc put_off_; // 下线回调(DELETE事件)
} monitor_t;
// 4. 服务监视类(核心:监听etcd键变化)
class MonitorEtcd
{
public:
// 构造:初始化etcd客户端
MonitorEtcd(const std::string& serverinfo)
{
LogModule::Log::Init();
try
{
etcd_Client_ = std::make_shared<etcd::Client>(serverinfo);
// 连接测试
auto test_res = etcd_Client_->get("/").get();
if (test_res.is_ok() || test_res.error_code() == 100)
{
LOG_TRACE("etcd监视客户端连接成功: {}", serverinfo);
}
else
{
LOG_ERROR("etcd监视客户端连接失败: {}", test_res.error_message());
etcd_Client_.reset();
}
}
catch (...) { LOG_ERROR("创建监视客户端异常"); }
}
// 核心:添加监视任务
bool PushMonitor(const monitor_t& info)
{
if (!etcd_Client_) { LOG_ERROR("客户端未初始化"); return false; }
std::lock_guard<std::mutex> lock(mtx_);
try
{
// 创建监听器(true=监视目录,false=监视单个键)
auto watcher = std::make_unique<etcd::Watcher>(
*etcd_Client_,
info.monitor_path_,
std::bind(&MonitorEtcd::bk, this, info, std::placeholders::_1),
(info.type_ == filetype::DIR)
);
// 初始加载现有数据
auto rsp = etcd_Client_->ls(info.monitor_path_).get();
if (rsp.is_ok())
{
for (int i = 0; i < rsp.keys().size(); i++)
{
info.put_on_(rsp.key(i), rsp.value(i).as_string());
}
}
// 本地缓存监听器
monitoring_set_[info.monitor_path_] = {std::move(watcher), info};
LOG_INFO("添加监视成功: {}", info.monitor_path_);
return true;
}
catch (...) { LOG_ERROR("添加监视异常"); return false; }
}
// 核心:取消监视任务
bool CancelMonitor(const std::string& monitor_path)
{
if (!etcd_Client_) { LOG_ERROR("客户端未初始化"); return false; }
std::lock_guard<std::mutex> lock(mtx_);
auto it = monitoring_set_.find(monitor_path);
if (it == monitoring_set_.end()) { LOG_DEBUG("监视路径不存在: {}", monitor_path); return false; }
try
{
it->second.first->Cancel(); // 取消监听
monitoring_set_.erase(it);
LOG_INFO("取消监视成功: {}", monitor_path);
return true;
}
catch (...) { LOG_ERROR("取消监视异常"); return false; }
}
private:
// 内部:事件回调处理(转发到用户自定义回调)
void bk(monitor_t t, const etcd::Response& res)
{
try
{
for (auto& e : res.events())
{
if (e.event_type() == etcd::Event::EventType::PUT)
{
auto v = e.kv();
LOG_INFO("服务上线: {}-{}", v.key(), v.as_string());
if (t.put_on_) t.put_on_(v.key(), v.as_string());
}
else if (e.event_type() == etcd::Event::EventType::DELETE_)
{
auto v = e.prev_kv();
LOG_INFO("服务下线: {}-{}", v.key(), v.as_string());
if (t.put_off_) t.put_off_(v.key(), v.as_string());
}
}
}
catch (...) { LOG_ERROR("事件处理异常"); }
}
private:
std::shared_ptr<etcd::Client> etcd_Client_; // etcd客户端
// 监视任务缓存:路径 -> (监听器, 监视信息)
std::unordered_map<std::string, std::pair<std::unique_ptr<etcd::Watcher>, monitor_t>> monitoring_set_;
mutable std::mutex mtx_; // 线程安全锁
};
}; // namespace Etcd_Tool
ServiceManager通过轮询(RR) 策略从可用信道列表中选择一个实例(Choose方法),确保负载均衡。
cpp
#pragma once
#include <atomic>
#include <brpc/channel.h>
#include <cstddef>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "LogTool.hpp"
// brpc通道工具命名空间,封装服务通道管理相关功能
namespace brpcChannelTool
{
// 定义brpc::Channel的智能指针类型,简化指针管理
using channelptr = std::shared_ptr<brpc::Channel>;
/**
* @brief 服务通道类,管理单个服务的多个实例通道
* 负责维护服务对应的多个后端实例连接,提供添加、删除实例及轮询选择通道的功能
*/
class ServiceChannel
{
public:
/**
* @brief 构造函数
* @param service_name 服务名称
*/
ServiceChannel(std::string service_name)
: service_name_(service_name)
, index_(0)
{}
/**
* @brief 检查是否没有可用通道
* @return 无通道时返回true,否则返回false
*/
bool empty()
{
return channels_.empty();
}
/**
* @brief 添加服务实例通道
* @param host 实例地址(格式: ip:port)
* @return 添加成功返回true,失败返回false
*/
bool append(std::string host)
{
try
{
// 创建通道智能指针并初始化
channelptr ptr = std::make_shared<brpc::Channel>();
brpc::ChannelOptions op;
setChannelop(op); // 设置通道参数
// 初始化通道失败则返回
if(ptr->Init(host.c_str(), &op) != 0)
{
LOG_ERROR("{}-{} init fail!", service_name_, host);
return false;
}
std::unique_lock<std::mutex> ulock(mutex_); // 加锁保证线程安全
// 避免重复添加同一实例
if(hostTochannel_.find(host) != hostTochannel_.end())
{
LOG_INFO("{}-{} already append!", service_name_, host);
return false;
}
// 添加通道到容器
channels_.push_back(ptr);
hostTochannel_[host] = ptr;
LOG_TRACE("{}-{} append sucess!", service_name_, host);
return true;
}
catch (const std::exception& e)
{
LOG_ERROR("{}-exception error!", e.what());
}
catch(...)
{
LOG_ERROR("{}-Unknown error!", service_name_);
}
return false;
}
/**
* @brief 移除服务实例通道
* @param host 实例地址(格式: ip:port)
* @return 移除成功返回true,失败返回false
*/
bool remove(std::string host)
{
std::unique_lock<std::mutex> ulock(mutex_); // 加锁保证线程安全
try
{
auto it = hostTochannel_.find(host);
if(it == hostTochannel_.end())
{
LOG_WARN("{}-{} cannot find to remove!", service_name_, host);
return false;
}
// 从通道列表中移除对应实例
for(auto cit = channels_.begin(); cit != channels_.end();cit++)
{
if(*cit == it->second)
{
channels_.erase(cit);
break;
}
}
// 从地址映射中移除
hostTochannel_.erase(it);
LOG_TRACE("{}-{} remove sucess!", service_name_, host);
return true;
}
catch (const std::exception& e)
{
LOG_ERROR("{}-exception error!", e.what());
}
catch(...)
{
LOG_ERROR("{}-Unknown error!", service_name_);
}
return false;
}
/**
* @brief 轮询选择一个服务通道
* @return 选中的通道智能指针,无可用通道时返回空指针
*/
channelptr choose()
{
std::unique_lock<std::mutex> ulock(mutex_); // 加锁保证线程安全
try
{
if(channels_.empty())
{
LOG_WARN("{}-No available channel!", service_name_);
return channelptr();
}
// 轮询算法实现:取模计算当前索引,然后索引自增
index_ %= channels_.size();
return channels_[index_++];
}
catch (const std::exception& e)
{
LOG_ERROR("{}-exception error!", e.what());
}
catch(...)
{
LOG_ERROR("{}-Unknown error!", service_name_);
}
return channelptr();
}
private:
/**
* @brief 设置brpc通道参数
* @param op 通道参数对象引用
*/
void setChannelop(brpc::ChannelOptions& op)
{
op.timeout_ms = -1; // 不设置超时时间
op.connect_timeout_ms = -1; // 不设置连接超时时间
op.connection_type = "single"; // 单连接模式
op.protocol = "baidu_std"; // 使用baidu_std协议
op.max_retry = 3; // 最大重试次数3次
}
private:
size_t index_; // 轮询索引
std::mutex mutex_; // 互斥锁,保证线程安全
std::string service_name_; // 服务名称
std::vector<channelptr> channels_; // 服务实例通道列表
std::unordered_map<std::string, channelptr> hostTochannel_; // 地址到通道的映射
};
// 定义ServiceChannel的智能指针类型
using service_channel_ptr = std::shared_ptr<ServiceChannel>;
/**
* @brief 服务管理器类,管理多个服务的通道集合
* 负责维护服务的上下线、通道选择,支持服务过滤功能
*/
class ServiceManager
{
public:
/**
* @brief 默认构造函数,初始化为跟踪所有服务模式
*/
ServiceManager() : is_follow(true)
{}
/**
* @brief 带单个服务参数的构造函数
* @param service 要跟踪的服务名称
* 注意:使用explicit禁止隐式类型转换
*/
explicit ServiceManager(std::string service)
{
services_follow_.insert(service);
is_follow = true;
}
/**
* @brief 带服务列表参数的构造函数
* @param services 要跟踪的服务名称列表
*/
ServiceManager(std::vector<std::string> services)
{
services_follow_.insert(services.begin(), services.end());
is_follow = true;
}
/**
* @brief 设置是否启用服务过滤
* @param trueorfalse true:只处理跟踪的服务 false:处理所有服务
*/
void set_is_follow(bool trueorfalse)
{
is_follow = trueorfalse;
}
/**
* @brief 添加要跟踪的服务
* @param service 服务名称
* @return 添加成功返回true
*/
bool follow(std::string service)
{
try
{
std::unique_lock<std::mutex> ulock(mutex_);
services_follow_.insert(service);
return true;
}
catch (const std::exception& e)
{
LOG_ERROR("{}-{}-exception error!", e.what(), service);
}
catch(...)
{
LOG_ERROR("follow {}-Unknown error!", service);
}
return false;
}
/**
* @brief 服务实例上线处理
* @param instacne_name 实例名称(格式: 服务名/实例标识)
* @param host 实例地址(格式: ip:port)
* @return 处理成功返回true
*/
bool Online(std::string instacne_name, std::string host)
{
try
{
// 从实例名称中解析出服务名
std::string name = getServiceName(instacne_name);
std::unique_lock<std::mutex> ulock(mutex_);
// 如果启用过滤且服务不在跟踪列表中,则忽略
auto it = services_follow_.find(name);
if(it == services_follow_.end() && is_follow)
{
LOG_INFO("{}-{} online, but manager overlooked it!", name, host);
return false;
}
// 若服务通道不存在则创建
if(manager_.find(name) == manager_.end())
{
manager_[name] = std::make_shared<ServiceChannel>(name);
}
// 解锁后添加实例通道(减少锁持有时间)
auto scptr = manager_[name];
ulock.unlock();
scptr->append(host);
LOG_TRACE("{}-{} Online sucess!", name, host);
return true;
}
catch (const std::exception& e)
{
LOG_ERROR("Online {}-{}-{}-exception error!", instacne_name, host, e.what());
}
catch(...)
{
LOG_ERROR("Online {}-{}-Unknown error!", instacne_name, host);
}
return false;
}
/**
* @brief 服务实例下线处理
* @param instance_name 实例名称(格式: 服务名/实例标识)
* @param host 实例地址(格式: ip:port)
* @return 处理成功返回true
*/
bool Offline(std::string instance_name, std::string host)
{
try
{
// 从实例名称中解析出服务名
std::string name = getServiceName(instance_name);
std::unique_lock<std::mutex> ulock(mutex_);
// 服务不存在则直接返回
auto it = manager_.find(name);
if(it == manager_.end())
{
LOG_INFO("{}-{}, manager cannot find it!", name, host);
return false;
}
// 移除实例通道
bool success = manager_[name]->remove(host);
// 若服务已无可用实例,则移除服务通道
if(success && manager_[name]->empty())
{
manager_.erase(it);
}
if(!success)
{
LOG_ERROR("manager_{}->remove{} fail", name, host);
return false;
}
LOG_TRACE("{}-{} Offline sucess!", name, host);
return true;
}
catch (const std::exception& e)
{
LOG_ERROR("Offline {}-{}-{}-exception error!", instance_name, host, e.what());
}
catch(...)
{
LOG_ERROR("Offline {}-{}-Unknown error!", instance_name, host);
}
return false;
}
/**
* @brief 选择服务的一个实例通道
* @param name 服务名称
* @return 选中的通道智能指针,无可用通道时返回空指针
*/
channelptr Choose(std::string name)
{
try
{
std::unique_lock<std::mutex> ulock(mutex_);
auto it = manager_.find(name);
// 服务不存在则返回空
if(it == manager_.end())
{
LOG_INFO("{}, manager cannot find it!", name);
return channelptr();
}
// 解锁后选择通道(减少锁持有时间)
service_channel_ptr scptr = manager_[name];
ulock.unlock();
channelptr ptr = scptr->choose();
if(!ptr)
{
LOG_INFO("{}, prt is nullprt!", name);
return ptr;
}
LOG_TRACE("{}-Chosse sucess!", name);
return ptr;
}
catch (const std::exception& e)
{
LOG_ERROR("Choose {}-{}-exception error!", name, e.what());
}
catch(...)
{
LOG_ERROR("Choose {}-Unknown error!", name);
}
return channelptr();
}
private:
/**
* @brief 从实例名称中解析服务名
* @param service_instance 实例名称(格式: 服务名/实例标识)
* @return 解析出的服务名
*/
std::string getServiceName(const std::string &service_instance)
{
auto pos = service_instance.find_last_of('/');
if (pos == std::string::npos) return service_instance;
return service_instance.substr(0, pos);
}
private:
std::mutex mutex_; // 互斥锁,保证线程安全
std::atomic<bool> is_follow; // 是否启用服务过滤
std::unordered_set<std::string> services_follow_; // 跟踪的服务集合
std::unordered_map<std::string, service_channel_ptr> manager_; // 服务到通道管理器的映射
};
};
(2)语音请求发起:
- 读取语音文件 :通过
aip::get_file_content读取 PCM 格式语音文件(如16k.pcm)。 - 构建 RPC 请求 :设置
request_id(如 "111111")与speech_content(文件内容)。 - 调用 RPC 接口:通过
SpeechService_Stub发起SpeechRecognition请求,解析响应:- 请求失败:打印 brpc 错误信息(
cntl.ErrorText())。 - 识别失败:打印
errmsg。 - 识别成功:打印
recognition_result(语音转文字结果)。
- 请求失败:打印 brpc 错误信息(
cpp
#include "etcdTool.hpp"
#include "brpcChannelTool.hpp"
#include "speech.pb.h"
#include <aip-cpp-sdk/speech.h>
#include <brpc/controller.h>
#include <chrono>
#include <functional>
#include <gflags/gflags.h>
#include <butil/logging.h>
#include <memory>
#include <spdlog/common.h>
#include <thread>
DEFINE_bool(log_mode, false, "日志模式: false(调试模式,默认), true(发布模式)");
DEFINE_string(log_file, "app.log", "发布模式下日志名称");
DEFINE_int32(log_level, 2, "发布模式下日志等级设置");
DEFINE_string(registry_host, "http://127.0.0.1:2379", "etcd服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(speech_host, "/service/speech_service", "语音服务根目录");
int main(int argc, char* argv[])
{
//参数解析
gflags::ParseCommandLineFlags(&argc, &argv, true);
//brpc自带日志系统输出等级设置
logging::SetMinLogLevel(4);
//自定义日志模块初始化
LogModule::LogConfig config;
config.output_mode = FLAGS_log_mode ? LogModule::OutputMode::FileOnly : LogModule::OutputMode::ConsoleOnly;
config.level = static_cast<spdlog::level::level_enum>(FLAGS_log_level);
config.enable_debug = !FLAGS_log_mode;
LogModule::Log::Init(config);
//rpc信道管理器
std::shared_ptr<brpcChannelTool::ServiceManager> manager = std::make_shared<brpcChannelTool::ServiceManager>();
manager->set_is_follow(false); // 关闭关注模式,处理所有服务
//etcd监视器设置
Etcd_Tool::MonitorEtcd m(FLAGS_registry_host);
Etcd_Tool::monitor_t info;
info.monitor_path_ = FLAGS_speech_host;
info.put_on_ = std::bind(&brpcChannelTool::ServiceManager::Online, manager.get(), std::placeholders::_1, std::placeholders::_2);
info.put_off_ = std::bind(&brpcChannelTool::ServiceManager::Offline, manager.get(), std::placeholders::_1, std::placeholders::_2);
info.type_ = Etcd_Tool::DIR;
m.PushMonitor(info);
// 等待初始发现完成
if (!m.WaitForInitialDiscovery())
{
LOG_ERROR("服务发现初始化失败");
return -1;
}
//选择信道
brpcChannelTool::channelptr ptr = manager->Choose(FLAGS_speech_host);
while(!ptr)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
ptr = manager->Choose(FLAGS_speech_host);
}
//读取语音文件数据
std::string file_content;
aip::get_file_content("16k.pcm", &file_content);
if(file_content.size() <= 0)
{
LOG_ERROR("file_content.size() is zero!");
return -1;
}
//调用rpc
bite_im::SpeechService_Stub stub(ptr.get());
brpc::Controller cntl;
bite_im::SpeechRecognitionReq req;
req.set_speech_content(file_content);
req.set_request_id("111111");
bite_im::SpeechRecognitionRsp rsp;
stub.SpeechRecognition(&cntl, &req, &rsp, nullptr);
if (cntl.Failed() == true) {
std::cout << "Rpc调用失败:" << cntl.ErrorText() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
return -1;
}
if (rsp.success() == false) {
std::cout << rsp.errmsg() << std::endl;
return -1;
}
std::cout << "收到响应: " << rsp.request_id() << std::endl;
std::cout << "收到响应: " << rsp.recognition_result() << std::endl;
return 0;
}
4. 构建脚本:CMake 配置
通过 CMake 管理编译流程,核心配置:
- protobuf 代码生成 :检测
speech.proto,自动生成speech.pb.cc与speech.pb.h(指定--cpp_out与-I参数)。 - 源码收集 :通过
aux_source_directory收集服务端(source/)与客户端(test/)源码。 - 目标生成 :生成
speechServer(服务端可执行文件)与testClient(客户端可执行文件)。 - 依赖链接:链接 protobuf、brpc、etcd-cpp-api、spdlog 等依赖库,确保编译通过。
- 安装配置 :将可执行文件安装到
bin目录,方便部署。
cmake
# 1. 添加cmake版本说明
cmake_minimum_required(VERSION 3.1.3)
# 2. 声明工程名称
project(speechServer)
# 3. 检测并生成proto框架代码
# 1. 添加所需的proto映射代码文件名称
set(proto_path ${CMAKE_CURRENT_SOURCE_DIR}/../proto)
set(proto_files speech.proto)
# 2. 检测框架代码文件是否已经生成
set(proto_h "")
set(proto_cc "")
set(proto_srcs "")
foreach(proto_file ${proto_files})
# 3. 如果没有生成,则预定义生成指令 -- 用于在构建项目之间先生成框架代码
string(REPLACE ".proto" ".pb.h" proto_h ${proto_file})
string(REPLACE ".proto" ".pb.cc" proto_cc ${proto_file})
if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc})
add_custom_command(
PRE_BUILD
COMMAND protoc
ARGS
--cpp_out=${CMAKE_CURRENT_BINARY_DIR}
-I ${proto_path}
--experimental_allow_proto3_optional
${proto_path}/${proto_file}
DEPENDS ${proto_path}/${proto_file}
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc}
COMMENT "生成proto框架代码文件:" ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc}
)
endif()
# 4. 将所有生成的框架源码文件名称保存起来 student.pb.cc classes.pb.cc
list(APPEND proto_srcs ${CMAKE_CURRENT_BINARY_DIR}/${proto_cc})
endforeach()
# 4. 获取源码目录下的所有源码文件
set(src_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files)
set(test_src_files "")
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/test test_src_files)
# 5. 声明目标及依赖
set(target "speechServer")
set(test_client "testClient")
add_executable(${target} ${src_files} ${proto_srcs})
add_executable(${test_client} ${test_src_files} ${proto_srcs})
# 6. 设置头文件默认搜索路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/source)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../common)
# 7. 设置需要连接的库
target_link_libraries(${target} -lprotobuf
-lgflags -lspdlog -lbrpc -lpthread -lssl -lleveldb -lcrypto
-letcd-cpp-api -lcpprest -lcurl -lfmt -ljsoncpp -L/usr/local/lib)
target_link_libraries(${test_client} -lprotobuf
-lgflags -lspdlog -lbrpc -lpthread -lssl -lleveldb -lcrypto
-letcd-cpp-api -lcpprest -lcurl -lfmt -ljsoncpp -L/usr/local/lib)
#8. 设置安装路径
INSTALL(TARGETS ${target} RUNTIME DESTINATION bin)
INSTALL(TARGETS ${test_client} RUNTIME DESTINATION bin).
三、关键问题与解决方案
1. 服务高可用:etcd 租约与监听
- 服务注册保活 :
RegisterEtcd通过 etcd 租约(Lease)定期续约,服务下线后租约过期,etcd 自动删除实例信息,避免客户端调用无效服务。 - 实时服务发现 :
MonitorEtcd监听 etcd 节点变化,客户端实时更新可用服务列表,无需重启即可感知服务上下线。
2. 负载均衡:轮询策略
brpcChannelTool::ServiceChannel采用轮询(RR)策略选择 RPC 信道,确保多个服务实例均匀分担请求压力,避免单点过载。
3. 异常防护:多层级错误处理
- 工具类层 :
ASRTool与EtcdTool捕获 API 调用异常,返回错误信息,不向上抛出。 - RPC 接口层 :
SpeechServiceImpl捕获业务异常,构建错误响应,记录日志。 - 客户端层 :检查 RPC 调用状态(
cntl.Failed())与服务端响应(rsp.success()),优雅处理失败场景。
四、模块部署与测试
1. 部署流程
- 环境准备:安装 brpc、etcd、protobuf、etcd-cpp-api 等依赖库。
- 编译构建 :执行
cmake .. && make,生成speechServer与testClient。 - 启动 etcd :启动 etcd 服务(默认地址
http://127.0.0.1:2379)。 - 启动服务端 :运行
speechServer,通过 gflags 参数指定监听端口、etcd 地址、语音 API 鉴权信息。 - 启动客户端 :运行
testClient,指定 etcd 地址与语音文件路径,发起识别请求。
2. 测试验证
- 功能验证:客户端输出 "收到响应:识别文本",表示语音识别成功。
- 高可用验证:关闭一个服务实例,客户端自动切换到其他实例,请求正常处理。
- 负载均衡验证:启动多个服务实例,客户端轮询调用不同实例,查看日志确认负载均衡生效。
五、总结与扩展
本文的 Speech 模块通过模块化设计、brpc 高性能 RPC 框架与 etcd 服务发现,实现了分布式环境下的语音识别能力,核心优势:
- 低耦合:工具类(ASRTool、EtcdTool)与业务逻辑分离,便于替换第三方 API 或服务发现组件。
- 高可用:etcd 租约与实时监听确保服务上下线感知,轮询策略实现负载均衡。
- 易扩展:通过 Builder 模式简化服务初始化,新增 RPC 接口只需扩展 protobuf 与实现类。
后续可扩展方向:
- 语音格式支持:增加 WAV、MP3 等格式转 PCM 的逻辑,提升模块兼容性。
- 熔断降级:在客户端添加熔断机制,避免服务异常时大量无效请求。
- 监控告警:集成 Prometheus 与 Grafana,监控 RPC 调用量、延迟与错误率,触发异常告警。