目录
[1.3.1.构造ASRClient::ptr asr_client](#1.3.1.构造ASRClient::ptr asr_client)
[1.3.2.构造Registry::ptr reg_client](#1.3.2.构造Registry::ptr reg_client)
[1.3.3.构造std::shared_ptr server](#1.3.3.构造std::shared_ptr server)
还记得我们整个项目的服务架构吗?

当语音识别子服务启动后,其首要任务是与注册中心(etcd服务器)建立连接并完成服务注册。
这一过程涉及以下几个关键环节:
-
向注册中心注册 :子服务实例将自身提供的RPC服务信息 (包括服务名称、版本、协议、对外访问地址和端口)**通过API写入etcd。**为了确保注册信息的准确性和实时性,通常会设置一个租约(lease)并定期续约,这样一旦服务实例崩溃或网络分区,etcd能够自动将过期实例从注册表中移除。注册完成后,该服务实例便成为服务发现体系中的一个可用节点。
-
健康检查与保活:子服务后台会启动一个心跳任务,周期性地向etcd刷新租约,告知注册中心自己依然存活。同时,etcd可以配置健康检查机制,主动探测服务实例的状态,进一步增强系统的可靠性。
-
网关的服务发现:客户端发送语音识别请求到网关,网关作为统一的入口,需要知道应该将请求转发给哪个具体的服务实例。为此,网关通常会缓存一份服务注册表,或者实时向etcd查询可用的语音识别服务实例列表。当有多个实例时,网关可以根据负载均衡策略(如轮询、一致性哈希、最少连接数等)选择一个目标实例。
-
RPC调用与结果返回 :网关确定目标实例后,会发起RPC调用(基于brpc框架),将客户端的原始请求(包含语音数据)转发给语音识别子服务。子服务接收到请求后,调用百度AI SDK完成语音识别,将识别出的文字文本封装成RPC响应返回给网关。网关再将响应转换为客户端期望的格式(如HTTP JSON或WebSocket消息)返回给客户端。
一.实现过程
我们整个语音识别子服务的实现其实很简单

1.1.rpc服务的定义
我们的数据传输都是借助于我们的protobuf来进行序列化和反序列化
首先我们需要定义好,我们的语言识别子服务的speech.proto文件
cpp
syntax = "proto3";
package IMS;
option cc_generic_services = true;
message SpeechRecognitionReq {
string request_id = 1; //请求ID
bytes speech_content = 2; //语音数据
optional string user_id = 3; //用户ID , optional表示这个字段是可选的
optional string session_id = 4; //登录会话ID -- 网关进行身份鉴权 , optional表示这个字段是可选的
}
message SpeechRecognitionRsp {
string request_id = 1; //请求ID
bool success = 2; //请求处理结果标志
optional string errmsg = 3; //失败原因 , optional表示这个字段是可选的
optional string recognition_result = 4; //识别后的文字数据 , optional表示这个字段是可选的
}
//语音识别Rpc服务及接口的定义
service SpeechService {
rpc SpeechRecognition(SpeechRecognitionReq) returns (SpeechRecognitionRsp);
}
这里定义了好了我们的RPC服务接口。
如果有点忘记brpc和etcd怎么进行配合使用了,建议回到之前的文章来进行学习一下。
那么在写我们的RPC服务的接口之前,我们需要知道,我们这个RPC服务的核心服务是语音识别,那么语音识别需要什么样的组件?
按照我们之前的测试,肯定是需要一个语音识别客户端!!!那么我们就给这个RPC服务接口类安排一个语音识别客户端成员变量。
cpp
// SpeechServiceImpl 类:实现了 protobuf 定义的 SpeechService 服务接口
class SpeechServiceImpl : public IMS::SpeechService
{
public:
// 构造函数:接收一个 ASRClient 的智能指针,用于后续的语音识别调用
SpeechServiceImpl(const ASRClient::ptr &asr_client):
_asr_client(asr_client) {}
// 析构函数(默认)
~SpeechServiceImpl() {}
......
private:
ASRClient::ptr _asr_client; // 语音识别客户端对象的智能指针,用于调用语音识别功能
};
基于语音识别客户端成员变量,我们就能彻底实现我们的RPC服务接口了。
那么我们就很快就能写出下面这个RPC服务的接口了。
cpp
// SpeechServiceImpl 类:实现了 protobuf 定义的 SpeechService 服务接口
class SpeechServiceImpl : public IMS::SpeechService
{
public:
// 构造函数:接收一个 ASRClient 的智能指针,用于后续的语音识别调用
SpeechServiceImpl(const ASRClient::ptr &asr_client):
_asr_client(asr_client) {}
// 析构函数(默认)
~SpeechServiceImpl() {}
// SpeechRecognition 方法:处理语音识别请求,由 RPC 框架自动调用
// @param controller: RPC 控制器,用于控制 RPC 调用过程
// @param request: 语音识别请求对象,包含请求 ID 和语音数据
// @param response: 语音识别响应对象,用于填充识别结果或错误信息
// @param done: 完成回调,由框架提供,用于通知 RPC 调用结束
void SpeechRecognition(google::protobuf::RpcController* controller,
const ::IMS::SpeechRecognitionReq* request,
::IMS::SpeechRecognitionRsp* response,
::google::protobuf::Closure* done) {
// 记录调试日志,表示收到语音转文字请求
LOG_DEBUG("收到语音转文字请求!");
// 使用 ClosureGuard 确保在函数退出时自动调用 done->Run(),避免遗漏
brpc::ClosureGuard rpc_guard(done);
// 1. 取出请求中的语音数据(通过 request->speech_content() 获取)
// 2. 调用语音识别 SDK 模块进行语音识别,得到识别结果字符串
std::string err; // 用于接收语音识别过程中的错误信息
std::string res = _asr_client->recognize(request->speech_content(), err);
// 如果识别结果为空,表示识别失败
if (res.empty())
{
// 记录错误日志,包含请求 ID
LOG_ERROR("{} 语音识别失败!", request->request_id());
// 设置响应中的请求 ID
response->set_request_id(request->request_id());
// 设置成功标志为 false
response->set_success(false);
// 设置错误信息,包含 SDK 返回的错误描述
response->set_errmsg("语音识别失败:" + err);
return;
}
// 3. 识别成功,组织正常响应
response->set_request_id(request->request_id()); // 设置请求 ID
response->set_success(true); // 设置成功标志为 true
response->set_recognition_result(res); // 设置识别结果文本
}
private:
ASRClient::ptr _asr_client; // 语音识别客户端对象的智能指针,用于调用语音识别功能
};
这个写法可以说和我们之前的brpc的是一模一样。
注意啊,我们在构造它的时候需要传递一个ASRClient::ptr asr_client参数给它,这个ASRClient::ptr asr_client才能提供最终的语音识别服务。
1.2.SpeechServer类
那么我们到这里仅仅只是完成了我们RPC服务的定义,但是谁来维护这个语音识别子服务模块的运转呢?那么我们就引入了SpeechServer类,这个类的核心职责就是让语音识别子服务一直运行下去。
这个类是最终的语音识别子服务的服务器类
cpp
// SpeechServer 类:封装整个语音识别 RPC 服务器的运行管理
class SpeechServer
{
public:
// 智能指针类型别名
using ptr = std::shared_ptr<SpeechServer>;
// 构造函数:接收语音识别客户端、服务注册客户端和 RPC 服务器对象的智能指针
SpeechServer(const ASRClient::ptr asr_client,
const Registry::ptr ®_client,
const std::shared_ptr<brpc::Server> &server):
_asr_client(asr_client),
_reg_client(reg_client),
_rpc_server(server) {}
// 析构函数(默认)
~SpeechServer() {}
// start 方法:启动 RPC 服务器,进入事件循环,直到收到退出信号
void start()
{
_rpc_server->RunUntilAskedToQuit(); // 阻塞等待,直到服务器被要求退出
}
private:
ASRClient::ptr _asr_client; // 语音识别客户端对象
Registry::ptr _reg_client; // 服务注册客户端对象(用于向注册中心注册服务)
std::shared_ptr<brpc::Server> _rpc_server; // brpc 服务器对象
};
SpeechServer 类的主要作用是**聚合整个语音识别子服务所需的各个组件,并提供统一的启动入口。**它作为一个服务容器,将以下关键对象组合在一起:
- ASRClient:负责实际的语音识别功能(调用百度 AI SDK)。
- Registry:负责向注册中心(etcd)注册服务,以及维护服务实例的租约。
- brpc::Server:负责 RPC 服务的监听、请求调度和网络通信。
SpeechServer 提供了一个简单的 start() 方法,内部调用 _rpc_server->RunUntilAskedToQuit(),使服务器进入事件循环,开始处理 RPC 请求。这样,外部只需要获取 SpeechServer 对象并调用 start(),而无需关心内部组件的细节。
有人可能好奇,下面这2个成员我好像都没有用到它们的成员函数啊
- ASRClient:负责实际的语音识别功能(调用百度 AI SDK)。
- Registry:负责向注册中心(etcd)注册服务,以及维护服务实例的租约。
别慌,我们不是在这里使用它们的成员函数的。我们是在下面这个SpeechServerBuilder类里面使用它们的成员函数的。
1.3.SpeechServerBuilder类
这个类是辅助构造SpeechServer类的类
我们在构造这个SpeechServer 类的时候,怎么是使用了这样子的指针或者引用的参数来构造呢?
cpp
SpeechServer(const ASRClient::ptr asr_client,
const Registry::ptr ®_client,
const std::shared_ptr<brpc::Server> &server):
_asr_client(asr_client),
_reg_client(reg_client),
_rpc_server(server) {}
难道我们需要提前去创建这些对象,再将这些对象的引用和指针传递进去来构造我们的SpeechServer 类?
你说对了!!我们的SpeechServerBuilder类就是干这个事情的,它就干了下面这4件事情
- 构造ASRClient::ptr asr_client
- 构造Registry::ptr reg_client
- 构造std::shared_ptr<brpc::Server> server
- 构造SpeechServer 类对象,并返回
怎么样?思路是不是一下子就清晰了,那么你肯定知道它的成员变量是啥了。
cpp
// SpeechServerBuilder 类:用于构建 SpeechServer 对象的建造者类,简化配置和创建过程
class SpeechServerBuilder
{
......
private:
ASRClient::ptr _asr_client; // 语音识别客户端智能指针
Registry::ptr _reg_client; // 服务注册客户端智能指针
std::shared_ptr<brpc::Server> _rpc_server; // brpc 服务器智能指针
};
1.3.1.构造ASRClient::ptr asr_client
我们这个对象的核心作用就是提供语音识别服务的啊!!!
cpp
// make_asr_object:构造语音识别客户端对象
// @param app_id: 百度 AI 应用的 APP_ID
// @param api_key: 百度 AI 应用的 API_KEY
// @param secret_key: 百度 AI 应用的 SECRET_KEY
void make_asr_object(const std::string &app_id,
const std::string &api_key,
const std::string &secret_key)
{
_asr_client = std::make_shared<ASRClient>(app_id, api_key, secret_key);
}
这个就很简单了,我们只需要注意这个ASRClient::ptr _asr_client在构造的时候会干些什么事情即可。我们去看看
cpp
// 命名空间 IMS,用于封装即时通讯相关的功能
namespace IMS {
// ASRClient类:封装了百度AI的语音识别功能,提供语音识别服务
class ASRClient {
public:
// 智能指针类型别名,方便管理ASRClient对象的生命周期
using ptr = std::shared_ptr<ASRClient>;
// 构造函数:初始化百度AI语音识别客户端
// @param app_id: 百度AI应用的APP_ID
// @param api_key: 百度AI应用的API_KEY
// @param secret_key: 百度AI应用的SECRET_KEY
ASRClient(const std::string &app_id,
const std::string &api_key,
const std::string &secret_key)
: _client(app_id, api_key, secret_key) {} // 初始化内部aip::Speech对象
// recognize函数:对输入的语音数据进行识别
// @param speech_data: 语音数据(PCM格式)
// @param err: 输出参数,用于返回错误信息(如果识别失败)
// @return: 识别出的文本字符串,如果失败则返回空字符串
std::string recognize(const std::string &speech_data, std::string &err)
{
// 调用百度SDK的recognize方法进行语音识别
// 参数:语音数据、音频格式(pcm)、采样率(16000)、可选参数(这里使用aip::null)
Json::Value result = _client.recognize(speech_data, "pcm", 16000, aip::null);
// 检查识别结果中是否包含错误码(err_no不为0表示失败)
if (result["err_no"].asInt() != 0) {
// 记录错误日志,输出错误信息
LOG_ERROR("语音识别失败:{}", result["err_msg"].asString());
// 将错误信息赋值给输出参数err
err = result["err_msg"].asString();
// 返回空字符串表示识别失败
return std::string();
}
// 识别成功,返回识别结果中的第一个文本(result是一个数组,通常第一个元素即为识别结果)
return result["result"][0].asString();
}
private:
aip::Speech _client; // 百度AI语音识别客户端对象
};
好吧这里什么都没有干,它传递的这些参数都是为了后续我们进行语音识别子服务的时候使用的。
具体的使用还是在构造我们的RPC服务(构造SpeechServiceImp对象)的时候,需要传递一个ASRClient::ptr asr_client参数给它,这个ASRClient::ptr asr_client才能提供最终的语音识别服务。
就像下面这样子
cpp
// 创建 brpc 服务器对象
_rpc_server = std::make_shared<brpc::Server>();
// 创建服务实现对象,并将其添加到服务器中
SpeechServiceImpl *speech_service = new SpeechServiceImpl(_asr_client);//注意这个SpeechServiceImpl就是我们自己上面定义的
int ret = _rpc_server->AddService(speech_service,
brpc::ServiceOwnership::SERVER_OWNS_SERVICE);//添加服务到服务器,指定服务对象不由服务器管理(即生命周期由外部控制)
if (ret == -1) {
那么这里面构造SpeechServiceImp对象时传递的就是我们现在这个函数构造的。!!!
1.3.2.构造Registry::ptr reg_client
我们这里就是创建了一个服务注册者
cpp
// make_reg_object:构造服务注册客户端对象,并向注册中心注册服务
// @param reg_host: 注册中心(如 etcd)的主机地址
// @param service_name: 要注册的服务名称
// @param access_host: 外部访问该服务的主机地址(例如 "127.0.0.1:8080")
void make_reg_object(const std::string ®_host,
const std::string &service_name,
const std::string &access_host)
{
_reg_client = std::make_shared<Registry>(reg_host);
_reg_client->registry(service_name, access_host); // 执行注册
}
可以看到,我们这里指定了注册中心(etcd服务器)的IP地址创建了一个服务注册者,然后我们借助这个服务注册者来往我们的注册中心注册了服务名以及这个服务所在IP地址和端口号。
那么我们也可以去看看这个注册者内部的代码
cpp
// 服务注册客户端类
// 功能:向etcd注册服务实例信息,并通过租约保活机制维护服务在线状态
class Registry
{
public:
using ptr = std::shared_ptr<Registry>; // 智能指针类型别名
// 构造函数,初始化etcd客户端并创建租约保活对象
// host: etcd服务器地址(例如 "http://127.0.0.1:2379")
Registry(const std::string &host) : _client(std::make_shared<etcd::Client>(host)), // 创建etcd客户端
_keep_alive(_client->leasekeepalive(3).get()), // 创建租约保活对象,租约TTL为3秒,这里采用leasekeepalive,会自动续约
_lease_id(_keep_alive->Lease())
{
} // 获取租约ID
// 析构函数,取消租约保活,释放资源
~Registry()
{
_keep_alive->Cancel(); // 释放租约
}
// 注册服务信息到etcd
// key: 服务实例的键(通常包含服务名和实例标识,如 "/services/user/192.168.1.1:8080")
// val: 服务实例的值(如IP地址、端口等元数据)
// 返回值:注册成功返回true,失败返回false
bool registry(const std::string &key, const std::string &val)
{
// 向etcd写入键值对,并关联租约ID(租约到期后自动删除)
auto resp = _client->put(key, val, _lease_id).get(); // .get()同步等待结果,只有等到这个键值对真正写入这个etcd服务器后才会自动返回
if (resp.is_ok() == false)
{
LOG_ERROR("注册数据失败:{}", resp.error_message()); // 日志记录错误
return false;
}
return true;
}
private:
std::shared_ptr<etcd::Client> _client; // etcd客户端对象
std::shared_ptr<etcd::KeepAlive> _keep_alive; // 租约保活对象,负责自动续期,注意我们的_keep_alive的构造是依赖于_client,所以放在这个位置
uint64_t _lease_id; // 租约ID,用于关联注册的数据,注意我们的_lease_id的构造是依赖于_keep_alive,所以放在这个位置
};
可以看到注册服务本质就是往注册中心(etcd服务器)写入一个键值对
- 键:服务名称
- 值:服务的IP地址:端口号
这个就很明显了
1.3.3.构造std::shared_ptr<brpc::Server> server
brpc::Server对象有啥作用?
- brpc::Server 启动后持续监听指定端口,接收客户端发来的 RPC 请求。
- 它首先解析请求内容,提取出目标服务名、方法名以及序列化的请求数据。
- 随后,Server 依据内部维护的服务路由表(通过 AddService 注册的服务实例)快速定位到对应的服务实现对象(如 SpeechServiceImpl),并调用该对象中与请求方法名匹配的业务处理函数。
- 最终,请求数据被反序列化后传递给用户定义的逻辑,而响应结果则经 Server 封装后返回给客户端。
整个过程对开发者透明,只需专注于服务实现本身。
cpp
// make_rpc_server:构造并启动 RPC 服务器
// @param port: 服务器监听的端口号
// @param timeout: 空闲连接超时时间(秒)
// @param num_threads: 服务器的工作线程数
void make_rpc_server(uint16_t port, int32_t timeout, uint8_t num_threads)
{
// 检查语音识别客户端是否已初始化,因为我们的这个
if (!_asr_client)
{
LOG_ERROR("还未初始化语音识别模块!");
abort(); // 初始化失败,终止程序
}
// 创建 brpc 服务器对象
_rpc_server = std::make_shared<brpc::Server>();
// 创建服务实现对象,并将其添加到服务器中
SpeechServiceImpl *speech_service = new SpeechServiceImpl(_asr_client);//注意这个SpeechServiceImpl就是我们自己上面定义的
int ret = _rpc_server->AddService(speech_service,
brpc::ServiceOwnership::SERVER_OWNS_SERVICE);//添加服务到服务器,指定服务对象不由服务器管理(即生命周期由外部控制)
if (ret == -1) {
LOG_ERROR("添加Rpc服务失败!");
abort();
}
// 配置服务器选项
brpc::ServerOptions options;
options.idle_timeout_sec = timeout; // 空闲超时时间
options.num_threads = num_threads; // 工作线程数
// 启动服务器,监听指定端口
ret = _rpc_server->Start(port, &options);
if (ret == -1) {
LOG_ERROR("服务启动失败!");
abort();
}
}
这个就很常规了,没什么好说的。挺常规的
1.3.4.构造SpeechServer对象
这里就是集大成者了。我们将上面3个部分都整合到一个类里面,这样子就构成了一个完整的语音识别子服务。
SpeechServer 类的主要目的是将语音识别 RPC 服务的各个核心组件(语音识别客户端、服务注册客户端、RPC 服务器)整合成一个高内聚的单元,对外提供统一的启动接口。它的设计必要性体现在以下几个方面:
- **职责聚合与封装:**SpeechServer 把原本分散在 Builder 中的组件(_asr_client、_reg_client、_rpc_server)聚合在一起,使它们成为一个整体。这样,服务的运行状态、资源生命周期都由 SpeechServer 对象统一管理,外部只需通过一个简单的 start() 方法即可启动整个服务,无需关心内部复杂的协作细节。
- **生命周期管理:**由于各个组件都是以智能指针形式存放在 SpeechServer 内部,当 SpeechServer 对象析构时,这些组件也会自动释放,避免了资源泄漏。这为未来添加优雅退出、资源清理等操作提供了基础。
- **简化使用接口:**如果没有 SpeechServer,调用者(如 main 函数)就需要手动持有三个独立对象,并显式调用 _rpc_server->RunUntilAskedToQuit(),代码会变得松散且容易出错。SpeechServer 将启动逻辑封装成 start(),使服务的使用者只需关注"启动"这个单一动作,代码更加清晰。
- **模块化与可扩展性:**将服务封装为类,有利于后续功能的添加,例如增加 stop() 方法实现平滑退出、添加健康检查接口、集成监控等。这些扩展都可以在 SpeechServer 内部完成,不影响外部调用者。
- **与 Builder 模式协同:**SpeechServer 配合 SpeechServerBuilder,遵循了"构建与表示分离"的设计原则。Builder 负责灵活地配置和组装各个组件,而 SpeechServer 则作为最终产物,负责运行时的统一控制。这种结构使服务创建过程更加清晰,便于维护和测试。
总之,SpeechServer 类是对整个语音识别 RPC 服务的抽象与封装,它让服务的构建、启动和管理变得简单而统一,体现了面向对象设计中的高内聚、低耦合思想。
cpp
// build:构建最终的 SpeechServer 对象,返回其智能指针
// 在调用此方法前,必须确保所有必要的组件都已初始化
SpeechServer::ptr build()
{
// 检查语音识别客户端是否已初始化
if (!_asr_client) {
LOG_ERROR("还未初始化语音识别模块!");
abort();
}
// 检查服务注册客户端是否已初始化
if (!_reg_client) {
LOG_ERROR("还未初始化服务注册模块!");
abort();
}
// 检查 RPC 服务器是否已初始化
if (!_rpc_server) {
LOG_ERROR("还未初始化RPC服务器模块!");
abort();
}
// 创建 SpeechServer 对象并返回
SpeechServer::ptr server = std::make_shared<SpeechServer>(
_asr_client, _reg_client, _rpc_server);
return server;
}
这个就是为了构造出我们的SpeechServer 类对象,并返回这个对象,现在我们的语音识别子服务的结构就很清晰了。
1.3.5.完整代码
cpp
// SpeechServerBuilder 类:用于构建 SpeechServer 对象的建造者类,简化配置和创建过程
class SpeechServerBuilder
{
public:
// make_asr_object:构造语音识别客户端对象
// @param app_id: 百度 AI 应用的 APP_ID
// @param api_key: 百度 AI 应用的 API_KEY
// @param secret_key: 百度 AI 应用的 SECRET_KEY
void make_asr_object(const std::string &app_id,
const std::string &api_key,
const std::string &secret_key)
{
_asr_client = std::make_shared<ASRClient>(app_id, api_key, secret_key);
}
// make_reg_object:构造服务注册客户端对象,并向注册中心注册服务
// @param reg_host: 注册中心(如 etcd)的主机地址
// @param service_name: 要注册的服务名称
// @param access_host: 外部访问该服务的主机地址(例如 "127.0.0.1:8080")
void make_reg_object(const std::string ®_host,
const std::string &service_name,
const std::string &access_host)
{
_reg_client = std::make_shared<Registry>(reg_host);
_reg_client->registry(service_name, access_host); // 执行注册
}
// make_rpc_server:构造并启动 RPC 服务器
// @param port: 服务器监听的端口号
// @param timeout: 空闲连接超时时间(秒)
// @param num_threads: 服务器的工作线程数
void make_rpc_server(uint16_t port, int32_t timeout, uint8_t num_threads)
{
// 检查语音识别客户端是否已初始化,因为我们的这个
if (!_asr_client)
{
LOG_ERROR("还未初始化语音识别模块!");
abort(); // 初始化失败,终止程序
}
// 创建 brpc 服务器对象
_rpc_server = std::make_shared<brpc::Server>();
// 创建服务实现对象,并将其添加到服务器中
SpeechServiceImpl *speech_service = new SpeechServiceImpl(_asr_client);//注意这个SpeechServiceImpl就是我们自己上面定义的
int ret = _rpc_server->AddService(speech_service,
brpc::ServiceOwnership::SERVER_OWNS_SERVICE);//添加服务到服务器,指定服务对象不由服务器管理(即生命周期由外部控制)
if (ret == -1) {
LOG_ERROR("添加Rpc服务失败!");
abort();
}
// 配置服务器选项
brpc::ServerOptions options;
options.idle_timeout_sec = timeout; // 空闲超时时间
options.num_threads = num_threads; // 工作线程数
// 启动服务器,监听指定端口
ret = _rpc_server->Start(port, &options);
if (ret == -1) {
LOG_ERROR("服务启动失败!");
abort();
}
}
// build:构建最终的 SpeechServer 对象,返回其智能指针
// 在调用此方法前,必须确保所有必要的组件都已初始化
SpeechServer::ptr build()
{
// 检查语音识别客户端是否已初始化
if (!_asr_client) {
LOG_ERROR("还未初始化语音识别模块!");
abort();
}
// 检查服务注册客户端是否已初始化
if (!_reg_client) {
LOG_ERROR("还未初始化服务注册模块!");
abort();
}
// 检查 RPC 服务器是否已初始化
if (!_rpc_server) {
LOG_ERROR("还未初始化RPC服务器模块!");
abort();
}
// 创建 SpeechServer 对象并返回
SpeechServer::ptr server = std::make_shared<SpeechServer>(
_asr_client, _reg_client, _rpc_server);
return server;
}
private:
ASRClient::ptr _asr_client; // 语音识别客户端智能指针
Registry::ptr _reg_client; // 服务注册客户端智能指针
std::shared_ptr<brpc::Server> _rpc_server; // brpc 服务器智能指针
};
这个就很好理解了吧。
二.搭建语音识别子服务
有了上面这些打基础,我们
cpp
// 主要实现语音识别子服务的服务器的搭建
#include "speech_server.hpp"
// 定义命令行参数:程序的运行模式,false-调试模式,true-发布模式
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
// 定义命令行参数:发布模式下日志输出文件路径,默认为空(输出到控制台)
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
// 定义命令行参数:发布模式下日志输出等级,默认为0(通常表示最低级别)
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
// 定义命令行参数:服务注册中心(如 etcd)的地址,默认指向本机的2379端口
DEFINE_string(registry_host, "http://127.0.0.1:2379", "服务注册中心地址");
// 定义命令行参数:服务监控的根目录,用于在注册中心中组织服务
DEFINE_string(base_service, "/service", "服务监控根目录");
// 定义命令行参数:当前实例在注册中心中的具体名称,用于唯一标识该服务实例
DEFINE_string(instance_name, "/speech_service/instance", "当前实例名称");
// 定义命令行参数:当前实例的外部访问地址(IP:端口),供客户端或其他服务调用
DEFINE_string(access_host, "127.0.0.1:10001", "当前实例的外部访问地址");
// 定义命令行参数:语音识别 RPC 服务器监听的端口号,默认 10001
DEFINE_int32(listen_port, 10001, "语音识别Rpc服务器监听端口");
// 定义命令行参数:语音识别 RPC 调用超时时间(秒),-1 表示不超时
DEFINE_int32(rpc_timeout, -1, "语音识别Rpc调用超时时间");
// 定义命令行参数:语音识别 RPC 服务器的 IO 线程数量,默认为 1
DEFINE_int32(rpc_threads, 1, "语音识别Rpc的IO线程数量");
// 定义命令行参数:百度语音识别平台的应用 ID
DEFINE_string(app_id, "122418677", "语音平台应用ID");
// 定义命令行参数:百度语音识别平台的 API 密钥
DEFINE_string(api_key, "nQD0xImQ40B725DcrAEzAs66", "语音平台API密钥");
// 定义命令行参数:百度语音识别平台的加密密钥
DEFINE_string(secret_key, "RPCS6oZrefqA97eSMnrCe0xIKAYKUqeW", "语音平台加密密钥");
// 程序主函数
int main(int argc, char *argv[])
{
// 解析命令行参数,将识别的参数从 argc/argv 中移除
google::ParseCommandLineFlags(&argc, &argv, true);
// 初始化日志系统,根据运行模式、日志文件路径和日志等级进行设置
IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 创建语音识别服务器的建造者对象,用于逐步构建服务器组件
IMS::SpeechServerBuilder ssb;
// 构造语音识别客户端对象,传入从命令行参数获取的百度 AI 密钥
ssb.make_asr_object(FLAGS_app_id, FLAGS_api_key, FLAGS_secret_key);
// 构造并启动 RPC 服务器,指定监听端口、超时时间和线程数
ssb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);
// 构造服务注册客户端对象,并注册当前服务实例到注册中心
// 注册路径为 base_service + instance_name,外部访问地址为 access_host
ssb.make_reg_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
// 构建最终的 SpeechServer 对象,包含所有已初始化的组件
auto server = ssb.build();
// 启动服务器,进入事件循环,等待处理 RPC 请求
server->start();
// 服务器退出后返回 0
return 0;
}
到现在我们的语音识别子服务就算是搭建完成了。
特别需要注意:
- 我们的这个语音识别子服务是在127.0.0.1:10001进行监控的。
三.测试
现在我们就可以写一个测试代码了
cpp
//speech_server的测试客户端实现
//1. 进行服务发现--发现speech_server的服务器节点地址信息并实例化的通信信道
//2. 读取语音文件数据
//3. 发起语音识别RPC调用
// 包含必要的头文件
#include "aip-cpp-sdk/speech.h" // 百度AI语音SDK,用于读取语音文件
#include "etcd.hpp" // etcd服务发现客户端封装
#include "channel.hpp" // RPC通信信道管理
#include <gflags/gflags.h> // 命令行参数解析
#include <thread> // 线程支持,用于等待
#include "speech.pb.h" // 语音识别服务的protobuf协议定义
// 定义命令行参数
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");
DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(speech_service, "/service/speech_service", "语音识别服务在etcd中的监控目录");
int main(int argc, char *argv[])
{
// 解析命令行参数
google::ParseCommandLineFlags(&argc, &argv, true);
// 初始化日志系统(根据运行模式设置日志输出)
IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 1. 构造RPC信道管理对象,用于维护服务实例及其对应的RPC信道
auto sm = std::make_shared<IMS::ServiceManager>();
// 声明需要关注的服务名称(语音识别服务)
sm->declared(FLAGS_speech_service);//声明我们需要关注的服务是/service/speech_service。只有我们关注的服务在服务上下线时才会去调用我们的服务上线和下线时的回调函数
// 绑定服务上线和下线时的回调函数,当etcd中有服务节点变化时自动更新信道
auto put_cb = std::bind(&IMS::ServiceManager::onServiceOnline, sm.get(), std::placeholders::_1, std::placeholders::_2);
auto del_cb = std::bind(&IMS::ServiceManager::onServiceOffline, sm.get(), std::placeholders::_1, std::placeholders::_2);
// 2. 构造服务发现对象,连接etcd并监控服务目录的变化
//etcd服务器会持续监听/service目录的服务上下线情况,但是注意了,只有我们关注的服务/service/speech_service在服务上下线时才会去调用我们的服务上线和下线时的回调函数
IMS::Discovery::ptr dclient = std::make_shared<IMS::Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);
// 3. 通过Rpc信道管理对象,获取语音识别服务的一个可用信道(随机负载均衡)
auto channel = sm->choose(FLAGS_speech_service);//根据服务名称来获取对应的brpc::channel对象
if (!channel) {
// 如果没有可用服务,等待1秒后退出
std::this_thread::sleep_for(std::chrono::seconds(1));
return -1;
}
// 读取语音文件数据(16k采样率的PCM格式文件)
std::string file_content;
aip::get_file_content("16k.pcm", &file_content); // 使用百度AI SDK提供的文件读取函数
std::cout << file_content.size() << std::endl; // 打印文件大小,用于调试
// 4. 构造RPC请求并调用语音识别服务
//注意这个SpeechService_Stub是我们.proto文件编译形成的
IMS::SpeechService_Stub stub(channel.get()); // 创建RPC桩,绑定信道
IMS::SpeechRecognitionReq req; // 请求对象
req.set_speech_content(file_content); // 设置语音数据
req.set_request_id("111111"); // 设置请求ID,可用于追踪
// 创建RPC控制器和响应对象(使用堆内存,避免在调用返回前被释放)
brpc::Controller *cntl = new brpc::Controller();
IMS::SpeechRecognitionRsp *rsp = new IMS::SpeechRecognitionRsp();
// 发起同步RPC调用
stub.SpeechRecognition(cntl, &req, rsp, nullptr);
if (cntl->Failed() == true) {
// 调用失败(网络错误、超时等),打印错误信息并清理资源
std::cout << "Rpc调用失败:" << cntl->ErrorText() << std::endl;
delete cntl;
delete rsp;
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;
// 清理动态分配的对象
delete cntl;
delete rsp;
return 0;
}
注意:我们在包含这个aip-cpp-sdk/speech.h头文件的时候,一定需要将它放到最前面去
千万不能把这个aip-cpp-sdk/speech.h头文件放到后面去进行#include,否则编译的时候可能会出现下面这个问题:
这个问题就需要我们去修改aip-cpp-sdk里面的头文件了,这是我们不愿意看到的

我们直接编译运行看看
先启动我们的服务器,再启动我们的客户端。


特别需要注意的是:我们在运行客户端的时候,必须得在客户端的运行目录了提前搞好一个名字叫16k.pcm的声音源文件,声音源文件的要求和我们之前的是一模一样的。

可以看到,这个语音识别子服务一点问题都没有。

