🎬 博主简介:
文章目录
- 前言:
- 一、DMS 短信验证码平台集成
- 1.1 业务流程与平台介绍
- 1.2 阿里云短信 SDK 安装
- 安装系统依赖
- 克隆并编译 SDK
- 1.3 SDK 二次封装:DMSClient 类
- 头文件定义
- 实现代码
- 1.4 功能测试
- 二、语音识别平台集成
- 2.1 应用场景与注意事项
- 音频格式要求(高频踩坑点)
- 2.2 语音识别 SDK 基础使用
- 2.3 SDK 二次封装:ASRClient 类
- 2.4 功能测试
- 三、CMake 构建系统入门与实战
- 3.1 CMake 基础介绍
- 主要特点
- 最小示例
- 3.2 核心语法速览
- 变量与列表
- 常用预定义变量
- 字符串替换
- 头文件与链接库
- 消息打印
- 查找源码文件
- 条件判断
- 自定义构建命令
- 子目录与安装
- 3.3 实战:ODB 项目的 CMake 构建
- 项目目录结构
- 完整 CMakeLists.txt
- 3.4 多目录嵌套与安装配置
- 四、核心要点总结
- 结尾:
前言:
做一套完整的即时通讯系统,光有网络通信和业务逻辑还不够,很多能力直接自研成本太高、周期太长,接入成熟的第三方云服务是更高效的选择。比如手机号注册登录需要短信验证码,语音消息需要语音转文字,这些都可以直接对接大厂的开放平台。与此同时,随着项目模块越来越多,手写 Makefile 维护成本会急剧上升,引入 CMake 做跨平台构建管理是工程化的必经之路。本文就顺着项目落地的顺序,依次拆解三个基建组件:阿里云短信验证码服务的接入与封装、百度语音识别服务的接入与封装,以及 CMake 构建系统的基础用法和实战案例。从 SDK 安装、接口封装到工程化落地,全程附带源码和踩坑记录。
一、DMS 短信验证码平台集成
1.1 业务流程与平台介绍
手机号快捷登录注册,是现在 APP 的标配功能。它的完整业务流程是这样的:
- 客户端点击「获取验证码」,向服务端提交手机号
- 服务端生成随机验证码和唯一 ID,把手机号 + 验证码发送给短信平台
- 短信平台向用户手机下发短信
- 用户输入验证码,带着 ID 提交给服务端
- 服务端比对 ID、手机号、验证码,一致则校验通过
我们选用阿里云的短信服务(Dysmsapi),稳定性高,到达率有保障,C++ SDK 也比较完善。
1.2 阿里云短信 SDK 安装
安装系统依赖
sudo apt-get install libcurl4-openssl-dev libssl-dev uuid-dev libjsoncpp-dev克隆并编译 SDK
git clone [https://github.com/aliyun/aliyun-openapi-cpp-sdk.git](https://github.com/aliyun/aliyun-openapi-cpp-sdk.git) # 一键编译安装 core 核心模块 sudo sh easyinstall.sh core安装完成后,系统会生成libalibabacloud-sdk-core.so动态库和对应的头文件,后续业务代码直接链接即可。
安全提醒:AccessKey ID 和 AccessKey Secret 是访问云 API 的密钥,权限很高。生产环境绝对不要硬编码在代码里,建议通过环境变量或者配置文件读取,并且定期轮换。
1.3 SDK 二次封装:DMSClient 类
原生 SDK 的调用步骤比较繁琐,每次都要手动构造请求、设置一堆参数。我们把它封装成一个DMSClient类,业务层只需要传手机号和验证码就能发送,屏蔽底层细节。
头文件定义
#pragma once #include <string> #include <memory> #include <alibabacloud/core/AlibabaCloud.h> #include <alibabacloud/core/CommonClient.h> #include "logger.hpp" class DMSClient { public: DMSClient(const std::string& access_key_id, const std::string& access_key_secret); ~DMSClient(); // 发送短信验证码,成功返回true bool send(const std::string& phone, const std::string& code); private: std::unique_ptr<AlibabaCloud::CommonClient> _client; };实现代码
DMSClient::DMSClient(const std::string& access_key_id, const std::string& access_key_secret) { // 初始化SDK AlibabaCloud::InitializeSdk(); // 配置客户端:地域、超时时间 AlibabaCloud::ClientConfiguration configuration("cn-chengdu"); configuration.setConnectTimeout(1500); configuration.setReadTimeout(4000); // 设置凭证 AlibabaCloud::Credentials credential(access_key_id, access_key_secret); _client = std::make_unique<AlibabaCloud::CommonClient>(credential, configuration); } DMSClient::~DMSClient() { // 析构时关闭SDK,释放资源 AlibabaCloud::ShutdownSdk(); } bool DMSClient::send(const std::string& phone, const std::string& code) { // 构造RPC模式的通用请求 AlibabaCloud::CommonRequest request( AlibabaCloud::CommonRequest::RequestPattern::RpcPattern); request.setHttpMethod(AlibabaCloud::HttpRequest::Method::Post); request.setDomain("dysmsapi.aliyuncs.com"); request.setVersion("2017-05-25"); // 设置业务参数 request.setQueryParameter("Action", "SendSms"); request.setQueryParameter("SignName", "bitejiuyeke"); // 短信签名 request.setQueryParameter("TemplateCode", "SMS_465324787"); // 模板CODE request.setQueryParameter("PhoneNumbers", phone); // 目标手机号 // 模板参数,JSON格式 std::string param_code = "{\"code\":\"" + code + "\"}"; request.setQueryParameter("TemplateParam", param_code); // 发起请求 auto response = _client->commonResponse(request); if (!response.isSuccess()) { LOG_ERROR("短信验证码请求失败: {}", response.error().errorMessage()); return false; } return true; }代码解读:
- 构造函数里做 SDK 初始化、客户端配置,析构函数做资源释放,遵循 RAII 原则,业务层不用关心初始化和清理
- 使用
CommonClient通用客户端,通过 RPC 模式调用短信 API,这种方式比专用客户端更灵活,对接其他阿里云服务也能用 - 所有参数通过
setQueryParameter设置,签名、模板码这些固定配置可以抽成配置项,不用硬编码
1.4 功能测试
写一个简单的测试程序,结合 gflags 传入密钥参数:
#include "../common/dms.hpp" #include <gflags/gflags.h> DEFINE_string(key_id, "", "平台访问密钥ID"); DEFINE_string(key_secret, "", "平台访问密钥"); DEFINE_bool(run_mode, false, "运行模式:false调试,true发布"); DEFINE_string(log_file, "", "日志文件路径"); DEFINE_int32(log_level, 0, "日志级别"); int main(int argc, char* argv[]) { google::ParseCommandLineFlags(&argc, &argv, true); init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level); DMSClient client(FLAGS_key_id, FLAGS_key_secret); client.send("15929917272", "5678"); return 0; }编译命令:
test: test.cc g++ -std=c++17 $^ -o $@ -lalibabacloud-sdk-core -ljsoncpp -lcurl -lssl -lcrypto -luuid二、语音识别平台集成
2.1 应用场景与注意事项
在聊天系统里,语音消息是很常用的功能,但纯语音不方便检索、也不方便用户快速浏览。我们在服务端接入语音识别能力,把语音消息自动转成文字,既可以做语音搜索,也能让用户直接看到文字预览。
我们选用百度智能云的语音识别服务,中文识别准确率高,C++ SDK 也比较成熟。
音频格式要求(高频踩坑点)
百度语音识别对音频参数有严格要求,不符合的话识别结果会完全错乱,比如全是“嗯嗯嗯”“嗨嗨嗨”:
- 采样率:仅支持 16000Hz
- 位深:16 bits 小端序(s16le)
- 声道:仅支持单声道
- 格式:推荐 pcm 原始格式
如果用户上传的是 mp3、amr 等其他格式,必须先用 ffmpeg 转码:
ffmpeg -i input.mp3 -acodec pcm_s16le -f s16le -ac 1 -ar 16000 output.pcm这行命令就是把任意音频转成 16k 单声道 16 位 pcm 格式,符合接口要求。
2.2 语音识别 SDK 基础使用
百度的 C++ SDK 叫 aip-cpp-sdk,核心类是aip::Speech,直接调用recognize方法就能识别。
基础调用示例:
#include "aip-cpp-sdk/speech.h" void asr(aip::Speech& client) { // 读取音频文件 std::string file_content; aip::get_file_content("16k.pcm", &file_content); // 可选参数,指定识别模型 std::map<std::string, std::string> options; options["dev_pid"] = "1537"; // 普通话输入法模型 // 发起识别请求 Json::Value result = client.recognize(file_content, "pcm", 16000, options); // err_no为0表示成功 if (result["err_no"].asInt() != 0) { std::cout << "识别失败: " << result["err_msg"].asString() << std::endl; return; } // 取第一条识别结果 std::cout << result["result"][0].asString() << std::endl; } int main() { std::string app_id = "你的APPID"; std::string api_key = "你的API_KEY"; std::string secret_key = "你的SECRET_KEY"; aip::Speech client(app_id, api_key, secret_key); asr(client); return 0; }2.3 SDK 二次封装:ASRClient 类
同样做一层封装,业务层只需要传入语音数据,直接拿到识别结果字符串,出错返回空。
#pragma once #include <string> #include "aip-cpp-sdk/speech.h" #include "logger.hpp" class ASRClient { public: ASRClient(const std::string& app_id, const std::string& api_key, const std::string& secret_key) : _client(app_id, api_key, secret_key) {} // 语音识别,输入pcm音频数据,返回识别后的文字,失败返回空 std::string recognize(const std::string& speech_data) { Json::Value result = _client.recognize(speech_data, "pcm", 16000, aip::null); if (result["err_no"].asInt() != 0) { LOG_ERROR("语音识别失败: {}", result["err_msg"].asString()); return std::string(); } return result["result"][0].asString(); } private: aip::Speech _client; };代码解读:
- 封装后业务层完全不用关心 aip 库的接口、参数、JSON 解析,只需要知道「传音频进,出文字」
- 出错统一打日志并返回空字符串,上层业务判断空就知道识别失败了,处理逻辑非常简洁
2.4 功能测试
#include "../../common/asr.hpp" #include <gflags/gflags.h> DEFINE_string(app_id, "", "语音平台应用ID"); DEFINE_string(api_key, "", "语音平台API密钥"); DEFINE_string(secret_key, "", "语音平台加密密钥"); DEFINE_bool(run_mode, false, "运行模式"); DEFINE_string(log_file, "", "日志文件"); DEFINE_int32(log_level, 0, "日志级别"); int main(int argc, char* argv[]) { google::ParseCommandLineFlags(&argc, &argv, true); init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level); ASRClient client(FLAGS_app_id, FLAGS_api_key, FLAGS_secret_key); std::string file_content; aip::get_file_content("16k.pcm", &file_content); std::string res = client.recognize(file_content); if (res.empty()) { return -1; } std::cout << "识别结果: " << res << std::endl; return 0; }编译命令:
test: test.cc g++ -std=c++17 $^ -o $@ -lfmt -lspdlog -lgflags -lcurl -lcrypto /usr/lib/x86_64-linux-gnu/libjsoncpp.so.1.8.4三、CMake 构建系统入门与实战
3.1 CMake 基础介绍
CMake 是一个开源、跨平台的构建系统,它不直接编译代码,而是根据CMakeLists.txt配置文件,生成对应平台的构建文件:Linux 下生成 Makefile,Windows 下生成 VS 工程,macOS 下生成 Xcode 工程。
它的核心价值就是一次编写,到处构建,不用为每个平台单独维护一套构建脚本。
主要特点
- 跨平台:一套配置适配所有主流系统和编译器
- 管理大型项目:支持多目录、多模块、嵌套构建
- 高效率:比 autotools 这类老工具构建速度快很多
- 可扩展:可以编写自定义模块,支持自定义编译步骤
- 语法简洁:比手写 Makefile 简单直观,维护成本低
最小示例
最简单的 CMakeLists.txt 只需要三行:
# 要求的最低CMake版本 cmake_minimum_required(VERSION 3.0.0) # 项目名称 project(test) # 生成可执行文件 add_executable(main main.cc)标准构建流程:
mkdir build && cd build cmake .. make推荐用 build 目录做外编译,生成的中间文件都在 build 里,不会污染源码目录。
3.2 核心语法速览
给大家整理最常用的语法,日常项目基本够用:
变量与列表
# 定义变量 set(variable_name "value") # 列表追加元素 set(src_files "") list(APPEND src_files main.cc child.cc)常用预定义变量
| 变量名 | 说明 |
|---|---|
CMAKE_CXX_STANDARD | C++ 标准版本,比如 17 |
CMAKE_CURRENT_SOURCE_DIR | 当前 CMakeLists.txt 所在的源码目录 |
CMAKE_CURRENT_BINARY_DIR | 当前执行 cmake 命令的构建目录 |
CMAKE_INSTALL_PREFIX | make install 的默认安装路径 |
重点区分:
SOURCE_DIR是源码路径,BINARY_DIR是构建产物路径。外编译模式下这俩是不一样的,生成文件、找资源的时候千万别搞混。
字符串替换
# 把src_files里的.cc后缀替换成.o,存到obj_files string(REPLACE ".cc" ".o" obj_files ${src_files})头文件与链接库
# 添加头文件搜索路径 include_directories(/usr/include/xxx) # 给目标添加链接库 target_link_libraries(main spdlog fmt)消息打印
# 普通提示 message(STATUS "这是普通提示信息") # 致命错误,直接终止cmake message(FATAL_ERROR "这是致命错误")查找源码文件
# 把src_dir目录下所有源码文件都加到src_files里 aux_source_directory(src_dir src_files)条件判断
if (NOT EXISTS 文件路径) # 文件不存在时的逻辑 endif()自定义构建命令
add_custom_command( PRE_BUILD # 编译前执行 COMMAND odb ... # 要执行的命令 DEPENDS student.hxx # 依赖的文件,依赖变了才重新执行 OUTPUT student-odb.cxx # 生成的输出文件 COMMENT "生成ODB框架代码" # 执行时打印的提示 )这个非常有用,比如生成 protobuf 代码、ODB 代码、flex/bison 代码,都可以用它自动完成。
子目录与安装
# 添加子目录,子目录里也要有CMakeLists.txt add_subdirectory(test2) # 设置安装规则,运行make install时把main安装到bin目录 INSTALL(TARGETS main RUNTIME DESTINATION bin)3.3 实战:ODB 项目的 CMake 构建
光看语法太抽象,我们拿 ODB 项目练手,这是一个稍微复杂点的场景:需要先通过 ODB 工具生成框架代码,再和业务源码一起编译。
项目目录结构
test2/ ├── CMakeLists.txt ├── entity/ │ └── student.hxx # ODB实体定义 └── source/ └── main.cc # 业务源码完整 CMakeLists.txt
# 1. 最低版本要求 cmake_minimum_required(VERSION 3.1.3) # 2. 项目名称 project(odb-test) # 3. ODB代码自动生成逻辑 set(odb_path ${CMAKE_CURRENT_SOURCE_DIR}/entity) set(odb_files student.hxx) set(odb_srcs "") foreach(odb_file ${odb_files}) # 替换出对应的生成文件名 string(REPLACE ".hxx" "-odb.hxx" odb_hxx ${odb_file}) string(REPLACE ".hxx" "-odb.cxx" odb_cxx ${odb_file}) # 如果生成文件不存在,就添加自定义命令预先生成 if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx}) add_custom_command( PRE_BUILD COMMAND odb -d mysql --std c++11 --generate-query --generate-schema --profile boost/date-time ${odb_path}/${odb_file} DEPENDS ${odb_path}/${odb_file} OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx} COMMENT "生成ODB框架代码: ${odb_cxx}" ) endif() # 把生成的源码加入列表 list(APPEND odb_srcs ${CMAKE_CURRENT_BINARY_DIR}/${odb_cxx}) endforeach() # 4. 收集业务源码 set(src_files "") aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files) # 5. 声明生成目标 add_executable(main ${src_files} ${odb_srcs}) # 6. 添加头文件路径 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/entity) include_directories(${CMAKE_CURRENT_BINARY_DIR}) # 7. 链接依赖库 target_link_libraries(main odb-mysql odb odb-boost gflags) # 8. 设置安装规则 INSTALL(TARGETS main RUNTIME DESTINATION bin)代码解读:
- 用
foreach遍历所有实体文件,自动生成对应的 ODB 代码,新增实体文件只需要往列表里加名字就行,不用改构建逻辑 PRE_BUILD保证代码生成在编译之前执行,并且只有依赖文件变了才会重新生成,增量编译效率高- 生成的文件放在构建目录里,不污染源码目录,符合外编译的最佳实践
3.4 多目录嵌套与安装配置
如果项目有多个模块,可以在上层放一个总控 CMakeLists.txt,用add_subdirectory管理所有子模块:
cmake_minimum_required(VERSION 3.1.3) project(all-test) # 把安装前缀设为构建目录,方便测试 set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR}) # 添加所有子模块 add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/test2) # add_subdirectory(user) # add_subdirectory(message) # ...构建流程:
mkdir build && cd build cmake .. make # 编译所有子模块 make install # 把所有产物安装到指定目录这样不管项目有多少个子服务,一条 cmake + make 就能全部编译完成,非常方便。
四、核心要点总结
- 第三方 SDK 一定要做二次封装:不要把平台相关的调用散落在业务代码里。封装成独立的类,对外只暴露业务语义的接口,以后换平台、升级 SDK,只需要改封装类,业务代码不用动。
- 语音识别的坑在音频格式:识别不准、结果乱码,90% 的情况不是平台的问题,是音频参数不符合要求。接入前一定要先把音频转码流程跑通,16k 单声道 16 位 pcm 是硬要求。
- CMake 的核心价值是自动化:它不只是替代手写 Makefile,更重要的是能把代码生成、资源编译、多模块管理、安装打包这些流程全部自动化,工程化程度越高,后期维护越省心。
- 外编译是最佳实践:永远不要在源码目录直接执行 cmake,单独建 build 目录,生成产物和源码分离,清理方便,也不会误提交生成文件。
- 自定义命令是进阶利器:
add_custom_command可以把任何外部工具集成到构建流程里,protobuf、ODB、Qt moc 这些代码生成场景都能用,彻底告别手动执行脚本。
结尾:
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!结语:到这里,短信验证码、语音识别两个第三方服务,以及 CMake 构建系统这三项基建就全部落地了。短信服务支撑手机号注册登录,语音识别提升语音消息的使用体验,CMake 则让整个项目的构建管理更规范、更高效。接下来我们就可以基于这些基建,开始逐个实现各个业务子服务了。后续也会继续分享更多 C++ 微服务、即时通讯系统的实战内容,欢迎关注交流。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど