微服务即时通讯系统(服务端)——Speech 语音模块开发(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.hspeech.pb.cc,作为 RPC 接口的基础代码。

(2)RPC 接口实现:SpeechServiceImpl

SpeechServiceImpl继承自 protobuf 生成的SpeechService类,重写SpeechRecognition方法,处理核心业务逻辑:

  1. 请求参数校验 :获取请求中的speech_content(语音数据)与request_id(请求唯一标识)。
  2. 语音识别调用 :通过ASRTool::Recognize解析语音数据,获取识别结果。
  3. 响应构建:
    • 识别成功:设置success=truerecognition_result(识别文本)。
    • 识别失败:设置success=falseerrmsg(错误详情)。
  4. 异常处理 :捕获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)封装服务初始化流程,降低代码耦合,核心步骤:

  1. make_asr_object:初始化ASRTool::SpeechToText实例(传入 API 的 app_id、api_key 等鉴权信息)。
  2. make_regs_object:初始化RegisterEtcd实例,将服务注册到 etcd(设置租约 TTL=10s)。
  3. make_rpc_object:初始化 brpc 服务器,添加SpeechServiceImpl接口,绑定监听端口(如 10001),设置 IO 线程数与超时时间。
  4. 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联动:

  1. 客户端启动时,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
  1. 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)语音请求发起:
  1. 读取语音文件 :通过aip::get_file_content读取 PCM 格式语音文件(如16k.pcm)。
  2. 构建 RPC 请求 :设置request_id(如 "111111")与speech_content(文件内容)。
  3. 调用 RPC 接口:通过SpeechService_Stub发起SpeechRecognition请求,解析响应:
    • 请求失败:打印 brpc 错误信息(cntl.ErrorText())。
    • 识别失败:打印errmsg
    • 识别成功:打印recognition_result(语音转文字结果)。
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 管理编译流程,核心配置:

  1. protobuf 代码生成 :检测speech.proto,自动生成speech.pb.ccspeech.pb.h(指定--cpp_out-I参数)。
  2. 源码收集 :通过aux_source_directory收集服务端(source/)与客户端(test/)源码。
  3. 目标生成 :生成speechServer(服务端可执行文件)与testClient(客户端可执行文件)。
  4. 依赖链接:链接 protobuf、brpc、etcd-cpp-api、spdlog 等依赖库,确保编译通过。
  5. 安装配置 :将可执行文件安装到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. 异常防护:多层级错误处理

  • 工具类层ASRToolEtcdTool捕获 API 调用异常,返回错误信息,不向上抛出。
  • RPC 接口层SpeechServiceImpl捕获业务异常,构建错误响应,记录日志。
  • 客户端层 :检查 RPC 调用状态(cntl.Failed())与服务端响应(rsp.success()),优雅处理失败场景。

四、模块部署与测试

1. 部署流程

  1. 环境准备:安装 brpc、etcd、protobuf、etcd-cpp-api 等依赖库。
  2. 编译构建 :执行cmake .. && make,生成speechServertestClient
  3. 启动 etcd :启动 etcd 服务(默认地址http://127.0.0.1:2379)。
  4. 启动服务端 :运行speechServer,通过 gflags 参数指定监听端口、etcd 地址、语音 API 鉴权信息。
  5. 启动客户端 :运行testClient,指定 etcd 地址与语音文件路径,发起识别请求。

2. 测试验证

  • 功能验证:客户端输出 "收到响应:识别文本",表示语音识别成功。
  • 高可用验证:关闭一个服务实例,客户端自动切换到其他实例,请求正常处理。
  • 负载均衡验证:启动多个服务实例,客户端轮询调用不同实例,查看日志确认负载均衡生效。

五、总结与扩展

本文的 Speech 模块通过模块化设计、brpc 高性能 RPC 框架与 etcd 服务发现,实现了分布式环境下的语音识别能力,核心优势:

  • 低耦合:工具类(ASRTool、EtcdTool)与业务逻辑分离,便于替换第三方 API 或服务发现组件。
  • 高可用:etcd 租约与实时监听确保服务上下线感知,轮询策略实现负载均衡。
  • 易扩展:通过 Builder 模式简化服务初始化,新增 RPC 接口只需扩展 protobuf 与实现类。

后续可扩展方向:

  1. 语音格式支持:增加 WAV、MP3 等格式转 PCM 的逻辑,提升模块兼容性。
  2. 熔断降级:在客户端添加熔断机制,避免服务异常时大量无效请求。
  3. 监控告警:集成 Prometheus 与 Grafana,监控 RPC 调用量、延迟与错误率,触发异常告警。
相关推荐
观测云4 小时前
云原生架构下微服务接入 SkyWalking 最佳实践
微服务·云原生·架构·skywalking
小蜜蜂爱编程4 小时前
Ubuntu无法开机Failed to activate swap /swapfile
linux·运维·ubuntu
A接拉起0074 小时前
如何丝滑迁移 Mongodb 数据库
后端·mongodb·架构
阿巴~阿巴~4 小时前
CPU 指令集、权限与用户态内核态机制
linux·运维·服务器·指令集·权限·用户态内核态
小涵4 小时前
企业SRE/DevOps向的精通Linux课程培训课程
linux·运维·devops·1024程序员节
沐怡旸4 小时前
【穿越Effective C++】条款7:为多态基类声明virtual析构函数——C++多态资源管理的基石
c++·面试
航Hang*5 小时前
第1章:初识Linux系统——第8节:查看/修改权限控制和ACL
linux·运维·服务器·笔记·操作系统
Algo-hx5 小时前
C++编程基础(五):字符数组和字符串
开发语言·c++
无敌最俊朗@5 小时前
C++ STL中 std::list 的高频面试题与答案
开发语言·c++·list