微服务即时通讯系统的实现(服务端)----(1)

目录

  • [1. 项目介绍和服务器功能设计](#1. 项目介绍和服务器功能设计)
  • [2. 基础工具安装](#2. 基础工具安装)
  • [3. gflags的安装与使用](#3. gflags的安装与使用)
    • [3.1 gflags的介绍](#3.1 gflags的介绍)
    • [3.2 gflags的安装](#3.2 gflags的安装)
    • [3.3 gflags的认识](#3.3 gflags的认识)
    • [3.4 gflags的使用](#3.4 gflags的使用)
  • [4. gtest的安装与使用](#4. gtest的安装与使用)
    • [4.1 gtest的介绍](#4.1 gtest的介绍)
    • [4.2 gtest的安装](#4.2 gtest的安装)
    • [4.3 gtest的使用](#4.3 gtest的使用)
  • [5 Spdlog日志组件的安装与使用](#5 Spdlog日志组件的安装与使用)
    • [5.1 Spdlog的介绍](#5.1 Spdlog的介绍)
    • [5.2 Spdlog的安装](#5.2 Spdlog的安装)
    • [5.3 Spdlog的认识](#5.3 Spdlog的认识)
    • [5.4 Spdlog的使用](#5.4 Spdlog的使用)
    • [5.5 对Spdlog的进行封装](#5.5 对Spdlog的进行封装)
    • [5.6 spdlog 与 glog 组件对比](#5.6 spdlog 与 glog 组件对比)
  • [6. etcd的安装与使用](#6. etcd的安装与使用)
    • [6.1 etcd的介绍](#6.1 etcd的介绍)
    • [6.2 etcd的安装](#6.2 etcd的安装)
    • [6.3 客户端类的接口介绍](#6.3 客户端类的接口介绍)
    • [6.4 客户端类的使用](#6.4 客户端类的使用)
    • [6.5 封装服务发现与注册功能](#6.5 封装服务发现与注册功能)
  • [7 brpc的安装与使用](#7 brpc的安装与使用)
    • [7.1 brpc的介绍](#7.1 brpc的介绍)
    • [7.2 brpc的安装](#7.2 brpc的安装)
    • [7.3 brpc的介绍](#7.3 brpc的介绍)
    • [7.4 brpc的使用](#7.4 brpc的使用)
    • [7.5 brpc的封装](#7.5 brpc的封装)
  • [8. ES的安装与使用](#8. ES的安装与使用)
    • [8.1 ES的介绍](#8.1 ES的介绍)
    • [8.2 ES和kibana的安装](#8.2 ES和kibana的安装)
    • [8.3 ES客户端的安装](#8.3 ES客户端的安装)
    • [8.4 ES的核心概念](#8.4 ES的核心概念)
    • [8.4 ES的封装](#8.4 ES的封装)
  • [9. cpp-httplib的安装与使用](#9. cpp-httplib的安装与使用)
    • [9.1 cpp-httplib的介绍](#9.1 cpp-httplib的介绍)
    • [9.2 cpp-httplib的安装及接口介绍](#9.2 cpp-httplib的安装及接口介绍)
    • [9.3 cpp-httplib的使用](#9.3 cpp-httplib的使用)
  • [10. websocketpp的安装与使用](#10. websocketpp的安装与使用)
    • [10.1 websocketpp的介绍和原理](#10.1 websocketpp的介绍和原理)
    • [10.2 websocketpp的安装](#10.2 websocketpp的安装)
    • [10.3 websocketpp的使用](#10.3 websocketpp的使用)
  • [11. redis的安装与使用](#11. redis的安装与使用)
    • [11.1 redis的介绍](#11.1 redis的介绍)
    • [11.2 redis的安装](#11.2 redis的安装)
    • [11.3 redis的部分接口介绍](#11.3 redis的部分接口介绍)
    • [11.4 redis的使用](#11.4 redis的使用)
  • [12. ODB安装与使用](#12. ODB安装与使用)
    • [12.1 ODB的介绍](#12.1 ODB的介绍)
    • [12.2 ODB的安装](#12.2 ODB的安装)
  • [13. RabbitMq的安装和使用](#13. RabbitMq的安装和使用)
    • [13.1 RabbitMq的介绍](#13.1 RabbitMq的介绍)
    • [13.2 RabbitMq的安装](#13.2 RabbitMq的安装)
    • [13.3 AMQP-CPP 库的简单使用](#13.3 AMQP-CPP 库的简单使用)
    • [13.4 RabbitMQ的二次封装](#13.4 RabbitMQ的二次封装)
  • [14. 短信验证码SDK](#14. 短信验证码SDK)
    • [15.1 短信验证码的安装](#15.1 短信验证码的安装)
    • [14.2 接口的使用](#14.2 接口的使用)
    • [14.3 短信接口的封装](#14.3 短信接口的封装)
  • [15. 语音识别SDK](#15. 语音识别SDK)
    • [15.1 语音识别的安装](#15.1 语音识别的安装)
    • [15.2 语音识别的简单使用](#15.2 语音识别的简单使用)
    • [15.3 语音识别的封装](#15.3 语音识别的封装)
  • [16. cmake搭建过程和介绍精简版](#16. cmake搭建过程和介绍精简版)

1. 项目介绍和服务器功能设计

(1)项目简介见微服务即时通讯系统的实现(客户端)博客: https://blog.csdn.net/m0_65558082/article/details/143168742?spm=1001.2014.3001.5502

(2)在本项目当中服务器的主要包含了以下功能:

  1. 用户注册:用户输入用户名(昵称),以及密码进行用户名的注册。
  2. 用户登录:用户通过用户名和密码进行登录。
  3. 短信验证码获取:当用户通过手机号注册或登录的时候,需要获取短信验证码。
  4. 手机号注册:用户输入手机号和短信验证码进行手机号的用户注册。
  5. 手机号登录:用户输入手机号和短信验证码进行手机号的用户登录。
  6. 用户信息获取:当用户登录之后,获取个人信息进行展示。
  7. 头像修改:设置用户头像。
  8. 昵称修改:设置用户昵称。
  9. 签名修改:设置用户签名。
  10. 手机号修改:修改用户的绑定手机号。
  11. 好友列表的获取:当用户登录成功之后,获取自己好友列表进行展示。
  12. 申请好友:搜索用户之后,点击申请好友,向对方发送好友申请。
  13. 待处理申请的获取:当用户登录成功之后,会获取离线的好友申请请求以待处理。
  14. 好友申请的处理:针对收到的好友申请进行同意/拒绝的处理。
  15. 删除好友:删除当前好友列表中的好友。
  16. 用户搜索:可以进行用户的搜索用于申请好友。
  17. 聊天会话列表的获取:每个单人/多人聊天都有一个聊天会话,在登录成功后可以获取聊天会话,查看历史的消息以及对方的各项信息。
  18. 多人聊天会话的创建:单人聊天会话在对方同意好友时创建,而多人会话需要调用该接口进行手动创建。
  19. 聊天成员列表的获取:多人聊天会话中,可以点击查看群成员按钮,查看群成员信息。
  20. 发送聊天消息:在聊天框输入内容后点击发送,则向服务器发送消息聊天请求。
  21. 获取历史消息
    • 获取最近 N 条消息:用于登录成功后,点击对方头像打开聊天框时显示最近的消息。
    • 获取指定时间段内的消息:用户可以进行聊天消息的按时间搜索。
  22. 消息搜索:用户可以进行聊天消息的关键字搜索。
  23. 文件的上传
    • 单个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储。
    • 多个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储。
  24. 文件的下载
    • 单个文件的下载:在后台用于获取用户头像文件数据,以及客户端用于获取文件/语音/图片消息的文件数据。
    • 多个文件的下载:在后台用于大批量获取用户头像数据(比如获取用户列表的时候),以及前端的批量文件下载。
  25. 语音消息的文字转换:客户端进行语音消息的文字转换。

(3)除了以上的与客户端之间交互的功能之外,还包含一些服务器后台内部所需的功能:

  1. 消息的存储:用于将文本消息进行存储起来,以便于进行消息的搜索,以及离线消息的存储。
  2. 文件的存储:用于存储用户的头像文件,以及消息中的文件/图片/语音文件数据。
  3. 各项用户,好友,会话数据的存储管理。

2. 基础工具安装

(1)编辑器安装:

powershell 复制代码
C++
[xiaomaker@alibaba ~]$ sudo apt-get install vim

(2)编译器安装:

powershell 复制代码
C++
[xiaomaker@alibaba ~]$ sudo apt-get install gcc g++

(3)调试器安装:

powershell 复制代码
C++
[xiaomaker@alibaba ~]$ sudo apt-get install gdb

(4)项目构建工具安装:

powershell 复制代码
C++
[xiaomaker@alibaba ~]$ sudo apt-get install make cmake

(5)传输工具安装:

powershell 复制代码
C++
[xiaomaker@alibaba ~]$ sudo apt-get install lrzsz

(6)版本管理工具安装:

powershell 复制代码
C++
[xiaomaker@alibaba ~]$ sudo apt-get install git

3. gflags的安装与使用

3.1 gflags的介绍

gflags是Google 开发的一个开源库,用于 C++ 应用程序中命令行参数的声明、定义和解析。gflags 库提供了一种简单的方式来添加、解析和文档化命令行标志(flags),使得程序可以根据不同的运行时配置进行调整。它具有如下几个特点:

  • 易于使用 :gflags 提供了一套简单直观的 API 来定义和解析命令行标志,使得开
    发者可以轻松地为应用程序添加新的参数。
  • 自动帮助和文档 :gflags 可以自动生成每个标志的帮助信息和文档,这有助于用
    户理解如何使用程序及其参数。
  • 类型安全 :gflags 支持多种数据类型的标志,包括布尔值、整数、字符串等,并
    且提供了类型检查和转换。
  • 多平台支持:gflags 可以在多种操作系统上使用,包括 Windows、Linux 和macOS。
  • 可扩展性:gflags 允许开发者自定义标志的注册和解析逻辑,提供了强大的扩展性。

官方文档https://gflags.github.io/gflags/
代码仓库https://github.com/gflags/gflags.git

3.2 gflags的安装

(1)直接命令行安装:

powershell 复制代码
C++
[xiaomaker@alibaba gflags]$ sudo apt-get install libgflags-dev

(2)原码安装:

powershell 复制代码
# 下载源码
git clone https://github.com/gflags/gflags.git
# 切换目录
cd gflags/
# 创建build并进入
mkdir build && cd build/
# 生成 Makefile
cmake ..
# 编译代码
make
# 安装
make install

3.3 gflags的认识

(1)使用 gflags 库来定义/解析命令行参数必须包含如下头文件:

cpp 复制代码
C++
#include <gflags/gflags.h>

(2)定义参数:

  • 利用 gflag 提供的宏定义来定义参数。该宏的 3 个参数分别为命令行参数名,参数默认值,参数的帮助信息。
cpp 复制代码
C++
DEFINE_bool(reuse_addr, true, "是否开始网络地址重用选项");
DEFINE_int32(log_level, 1, "日志等级:1-DEBUG, 2-WARN, 3-ERROR");
DEFINE_string(log_file, "stdout", "日志输出位置设置,默认为标准输出");
  • gflags支持定义多种类型的宏函数:
cpp 复制代码
C++
DEFINE_bool
DEFINE_int32
DEFINE_int64
DEFINE_uint64
DEFINE_double
DEFINE_string

(3)访问参数:

  • 我们可以在程序中通过 FLAGS_name 像正常变量一样访问标志参数。比如在上面的例子中,我们可以通过 FLAGS_big_menu 和 FLAGS_languages 变量来访问命令行参数。

(4)不同文件访问参数:

  • 如果想再另外一个文件访问当前文件的参数,以参数 FLAGS_big_menu 为例,我们可以使用用宏 DECLARE_bool(big_menu)来声明引入这个参数。
  • 其实这个宏就相当于做了 extern FLAGS_big_menu, 定义外部链接属性。

(5)初始化所有参数:

  • 当我们定义好参数后,需要告诉可执行程序去处理解析命令行传入的参数,使得FLAGS_*变量能得到正确赋值。我们需要在 main 函数中,调用下面的函数来解决命令行传入的所有参数。
cpp 复制代码
C++
google::ParseCommandLineFlags(&argc, &argv, true);
  • 参数解析:
    • argc 和 argv 就是 main 的入口参数
    • 第三个参数被称为 remove_flags。如果它为 true, 表示

ParseCommandLineFlags 会从 argv 中移除标识和它们的参数,相应减少 argc 的值。如果它为 false,ParseCommandLineFlags 会保留 argc 不变,但将会重新调整它们的顺序,使得标识再前面。

(6)运行参数设置:flags 为我们提供了多种命令行设置参数的方式。

  • string 和 int 设置参数
powershell 复制代码
Shell
exec --log_file="./main.log"
exec -log_file="./main.log"
exec --log_file "./main.log"
exec -log_file "./main.log"
  • bool 设置参数
powershell 复制代码
Shell
exec --reuse_addr
exec --noreuse_addr
exec --reuse_addr=true
exec --reuse_addr=false

符号- -将会终止标识的处理。比如在 exec -f1 1 -- -f2 2 中, f1 被认为是一个标

识,但 f2 不会

(7)配置文件的使用:

  • 配置文件的使用,其实就是为了让程序的运行参数配置更加标准化,不需要每次运行的时候都手动收入每个参数的数值,而是通过配置文件,一次编写,永久使用。需要注意的是,配置文件中选项名称必须与代码中定义的选项名称一致。样例:
cpp 复制代码
C++
-reuse_addr=true, 
-log_level=3
-log_file=./log/main.log

(8)特殊参数标识:

  • gflags 也默认为我们提供了几个特殊的标识。
powershell 复制代码
Shell
--help # 显示文件中所有标识的帮助信息
--helpfull # 和-help 一样, 帮助信息更全面一些
--helpshort # 只显示当前执行文件里的标志
--helpxml # 以 xml 方式打印,方便处理
--version # 打印版本信息,由 google::SetVersionString()设定
--flagfile -flagfile=f #从文件 f 中读取命令行参数

3.4 gflags的使用

(1)编写样例代码:main.cpp

cpp 复制代码
#include <iostream>
#include <gflags/gflags.h>

DEFINE_string(ip, "127.0.0.1", "这是服务器的监听IP地址,格式:127.0.0.1");
DEFINE_int32(port, 8080, "这是服务器的监听端口, 格式: 8080");
DEFINE_bool(debug_enable, true, "是否启用调试模式, 格式:true/false");

int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);

    std::cout << FLAGS_ip << std::endl;
    std::cout << FLAGS_port << std::endl;
    std::cout << FLAGS_debug_enable << std::endl;
    
    return 0;
}

(2)配置文件编写:main.conf

cpp 复制代码
-ip="192.168.2.2"
-port=10001
-debug_enable=true

(3)Makefile 编写:

cpp 复制代码
main:main.cpp
	g++ $^ -o $@ -std=c++17 -lgflags

.PHONY:clean
clean:
	rm -f main

(4)运行结果:

4. gtest的安装与使用

4.1 gtest的介绍

gtest是一个跨平台的 C++单元测试框架,由 google 公司发布。gtest 是为了在不同平台上为编写 C++单元测试而生成的。它提供了丰富的断言、致命和非致命判断、参数化等等测试所需的宏,以及全局测试,单元测试组件。

4.2 gtest的安装

(1)命令行安装:

powershell 复制代码
C++
[xiaomaker@alibaba ~]$ sudo apt-get install libgtest-dev

4.3 gtest的使用

(1)头文件包含:

cpp 复制代码
C++
#include <gtest/gtest.h>

(2)框架初始化接口:

cpp 复制代码
C++
testing::InitGoogleTest(&argc, argv);

(3)调用测试样例:

cpp 复制代码
C++
RUN_ALL_TESTS(); 

(4)TEST 宏:

cpp 复制代码
C++
//这里不需要双引号,且同测试下多个测试样例不能同名
TEST(测试名称, 测试样例名称) 
TEST_F(test_fixture,test_name)
  • TEST:主要用来创建一个简单测试, 它定义了一个测试函数, 在这个函数中可以使用任何 C++代码并且使用框架提供的断言进行检查。
  • TEST_F:主要用来进行多样测试,适用于多个测试场景如果需要相同的数据配置的情况, 即相同的数据测不同的行为。

(5)GTest 中的断言的宏可以分为两类:

  • ASSERT_系列:如果当前点检测失败则退出当前函数
  • EXPECT_系列:如果当前点检测失败则继续往下执行

下面是经常使用的断言介绍

cpp 复制代码
C++
// bool 值检查
ASSERT_TRUE(参数),期待结果是 true
ASSERT_FALSE(参数),期待结果是 false
//数值型数据检查
ASSERT_EQ(参数 1,参数 2),传入的是需要比较的两个数 equal
ASSERT_NE(参数 1,参数 2),not equal,不等于才返回 true
ASSERT_LT(参数 1,参数 2),less than,小于才返回 true
ASSERT_GT(参数 1,参数 2),greater than,大于才返回 true
ASSERT_LE(参数 1,参数 2),less equal,小于等于才返回 true
ASSERT_GE(参数 1,参数 2),greater equal,大于等于才返回 true

(6)测试样例:

cpp 复制代码
#include <iostream>
#include <gtest/gtest.h>

int Add(int num1, int num2)
{
    return num1 + num2;
}

TEST(测试名称, 加法用例名称1)
{
    ASSERT_EQ(Add(10, 20), 30);
    ASSERT_LT(Add(20, 20), 50);
}

TEST(测试名称, 字符串比较测试)
{
    std::string str = "Hello";
    EXPECT_EQ(str, "hello");
    printf("断言失败后的打印\n");
    EXPECT_EQ(str, "Hello");
}

int main(int argc, char *argv[])
{
    //单元测试框架的初始化
    testing::InitGoogleTest(&argc, argv);
    
    //开始所有的单元测试
    return RUN_ALL_TESTS();
}

(7)运行结果:

5 Spdlog日志组件的安装与使用

5.1 Spdlog的介绍

(1)spdlog 是一个高性能、超快速、零配置的 C++ 日志库,它旨在提供简洁的 API 和丰富的功能,同时保持高性能的日志记录 。它支持多种输出目标、格式化选项、线程安全以及异步日志记录。以下是对 spdlog 的详细介绍和使用方法。github链接:https://github.com/gabime/spdlog

(2)Spdlog的特点:

  • 高性能:spdlog 专为速度而设计,即使在高负载情况下也能保持良好的性能。
  • 零配置:无需复杂的配置,只需包含头文件即可在项目中使用。
  • 异步日志:支持异步日志记录,减少对主线程的影响。
  • 格式化:支持自定义日志消息的格式化,包括时间戳、线程 ID、日志级别等。
  • 多平台:跨平台兼容,支持 Windows、Linux、macOS 等操作系统。
  • 丰富的 API:提供丰富的日志级别和操作符重载,方便记录各种类型的日志。

5.2 Spdlog的安装

(1)命令行安装:

powershell 复制代码
C++
[xiaomaker@alibaba spdlog]$ sudo apt-get install libspdlog-dev

(2)原码安装:

powershell 复制代码
Bash
[xiaomaker@alibaba spdlog]$ git clone https://github.com/gabime/spdlog.git
[xiaomaker@alibaba spdlog]$ cd spdlog/
[xiaomaker@alibaba spdlog]$ mkdir build && cd build
[xiaomaker@alibaba spdlog]$ cmake -DCMAKE_INSTALL_PREFIX=/usr ..
[xiaomaker@alibaba spdlog]$ make && sudo make install

5.3 Spdlog的认识

(1)在你的 C++ 源文件中包含 spdlog 的头文件:

cpp 复制代码
C++
#include <spdlog/spdlog.h>

(2)日志输出等级枚举:

cpp 复制代码
C++
namespace level
{
    enum level_enum : int
    {
        trace = SPDLOG_LEVEL_TRACE,
        debug = SPDLOG_LEVEL_DEBUG,
        info = SPDLOG_LEVEL_INFO,
        warn = SPDLOG_LEVEL_WARN,
        err = SPDLOG_LEVEL_ERROR,
        critical = SPDLOG_LEVEL_CRITICAL,
        off = SPDLOG_LEVEL_OFF,
        n_levels
    };
}

(3)日志输出可以自定义日志消息的格式:

cpp 复制代码
C++
logger->set_pattern("%Y-%m-%d %H:%M:%S [%t] [%-7l] %v");
%t - 线程 ID(Thread ID)。
%n - 日志器名称(Logger name)。
%l - 日志级别名称(Level name),如 INFO, DEBUG, ERROR 等。
%v - 日志内容(message)。
比特就业课
%Y - 年(Year)。
%m - 月(Month)。
%d - 日(Day)。
%H - 小时(24-hour format)。
%M - 分钟(Minute)。
%S - 秒(Second)。

(4)日志记录器类:创建一个基本的日志记录器,并设置日志级别和输出模式:

cpp 复制代码
C++
namespace spdlog
{
    class logger
    {
        logger(std::string name);
        logger(std::string name, sink_ptr single_sink);
        logger(std::string name, sinks_init_list sinks);
        void set_level(level::level_enum log_level);
        void set_formatter(std::unique_ptr<formatter> f);
        template <typename... Args>
        void trace(fmt::format_string<Args...> fmt, Args &&...args);
        template <typename... Args>
        void debug(fmt::format_string<Args...> fmt, Args &&...args);
        template <typename... Args>
        void info(fmt::format_string<Args...> fmt, Args &&...args); 
        template <typename... Args>
        void warn(fmt::format_string<Args...> fmt, Args &&...args); 
        template <typename... Args>
        void error(fmt::format_string<Args...> fmt, Args &&...args); 
        template <typename... Args>
        void critical(fmt::format_string<Args...> fmt, Args &&...args);

        void flush(); // 刷新日志
        // 策略刷新--触发指定等级日志的时候立即刷新日志的输出
        void flush_on(level::level_enum log_level);
    };
}

(5)异步日志记录类:为了异步记录日志,可以使用 spdlog::async_logger:

cpp 复制代码
C++
class async_logger final : public logger
{
    async_logger(std::string logger_name,
                 sinks_init_list sinks_list,
                 std::weak_ptr<details::thread_pool> tp,
                 async_overflow_policy overflow_policy =
                     async_overflow_policy::block);
    async_logger(std::string logger_name,
                 sink_ptr single_sink,
                 std::weak_ptr<details::thread_pool> tp,
                 async_overflow_policy overflow_policy =
                     async_overflow_policy::block);

    // 异步日志输出需要异步工作线程的支持,这里是线程池类
    class SPDLOG_API thread_pool
    {
        thread_pool(size_t q_max_items,
                    size_t threads_n,
                    std::function<void()> on_thread_start,
                    std::function<void()> on_thread_stop);
        thread_pool(size_t q_max_items, size_t threads_n,
                    std::function<void()> on_thread_start);
        thread_pool(size_t q_max_items, size_t threads_n);
    };
} 
std::shared_ptr<spdlog::details::thread_pool> thread_pool()
{
    return details::registry::instance().get_tp();
}

// 默认线程池的初始化接口
inline void init_thread_pool(size_t q_size, size_t thread_count) auto async_logger = spdlog::async_logger_mt("async_logger",
                                                                                                             "logs/async_log.txt");
async_logger->info("This is an asynchronous info message");

(6)日志记录器工厂类:

cpp 复制代码
C++
using async_factory = async_factory_impl<async_overflow_policy::block>;
template <typename Sink, typename... SinkArgs>
inline std::shared_ptr<spdlog::logger> create_async(
    std::string logger_name,
    SinkArgs &&...sink_args)
    // 创建一个彩色输出到标准输出的日志记录器,默认工厂创建同步日志记录器
    template <typename Factory = spdlog::synchronous_factory>
    std::shared_ptr<logger> stdout_color_mt(
        const std::string &logger_name,
        color_mode mode = color_mode::automatic);
// 标准错误
template <typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> stderr_color_mt(
    const std::string &logger_name,
    color_mode mode = color_mode::automatic);
// 指定文件
template <typename Factory = spdlog::synchronous_factory>
std::shared_ptr<logger> basic_logger_mt(
    const std::string &logger_name,
    const filename_t &filename,
    bool truncate = false,
    const file_event_handlers &event_handlers = {})
    // 循环文件
    template <typename Factory = spdlog::synchronous_factory>
    std::shared_ptr<logger> rotating_logger_mt(
        const std::string &logger_name,
        const filename_t &filename,
        size_t max_file_size,
        size_t max_files,
        bool rotate_on_open = false)
		...

(7)日志落地类:

cpp 复制代码
C++
namespace spdlog
{
    namespace sinks
    {
        class SPDLOG_API sink
        {
        public:
            virtual ~sink() = default;
            virtual void log(const details::log_msg &msg) = 0;

            virtual void flush() = 0;
            virtual void set_pattern(const std::string &pattern) = 0;
            virtual void
            set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) = 0;

            void set_level(level::level_enum log_level);
        };

        using stderr_color_sink_mt;
        using stderr_sink_mt;
        using stdout_color_sink_mt;
        using stdout_sink_mt;
        // 滚动日志文件-超过一定大小则自动重新创建新的日志文件
        sink_ptr rotating_file_sink(filename_t base_filename,
                                    std::size_t max_size,
                                    std::size_t max_files,
                                    bool rotate_on_open = false,
                                    const file_event_handlers &event_handlers =
                                        {});
        using rotating_file_sink_mt = rotating_file_sink<std::mutex>;
        // 普通的文件落地对啊 ing
        sink_ptr basic_file_sink(const filename_t &filename,
                                 bool truncate = false,
                                 const file_event_handlers &event_handlers =
                                     {});
        using basic_file_sink_mt = basic_file_sink<std::mutex>;

        using kafka_sink_mt = kafka_sink<std::mutex>;
        using mongo_sink_mt = mongo_sink<std::mutex>;
        using tcp_sink_mt = tcp_sink<std::mutex>;
        using udp_sink_mt = udp_sink<std::mutex>;
        .....
        //*_st:单线程版本,不用加锁,效率更高。
        //*_mt:多线程版本,用于多线程程序是线程安全的。
    }
}

(8)全局接口:

cpp 复制代码
C++
//输出等级设置接口
void set_level(level::level_enum log_level);
//日志刷新策略-每隔 N 秒刷新一次
void flush_every(std::chrono::seconds interval)
//日志刷新策略-触发指定等级立即刷新
void flush_on(level::level_enum log_level);

(9)使用日志记录器记录不同级别的日志:

cpp 复制代码
C++
logger->trace("This is a trace message");
logger->debug("This is a debug message");
logger->info("This is an info message");
logger->warn("This is a warning message");
logger->error("This is an error message");
logger->critical("This is a critical message");

5.4 Spdlog的使用

(1)同步日志器sync.cpp:

cpp 复制代码
#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>

int main()
{
    //设置全局的刷新策略
    //每秒刷新
    spdlog::flush_every(std::chrono::seconds());

    //遇到debug以上等级的日志立即刷新
    spdlog::flush_on(spdlog::level::level_enum::debug);
    
    //设置全局的日志输出等级 -- 无所谓 --每个日志器可以独立进行设置
    spdlog::set_level(spdlog::level::level_enum::debug);

    //创建同步日志器(标准输出/文件) -- 工厂接口默认创建的就是同步日志器
    auto logger = spdlog::stdout_color_mt("default-logger");
    //auto logger = spdlog::basic_logger_mt("file-logger", "sync.log");

    //设置日志器的刷新策略,以及设置日志器的输出等级
    // logger->flush_on(spdlog::level::level_enum::debug);
    // logger->set_level(spdlog::level::level_enum::debug);
    //设置日志输出格式
    logger->set_pattern("[%n][%H:%M:%S][%t][%-8l] %v");

    //进行简单的日志输出
    logger->trace("你好!{}", "小明");
    logger->debug("你好!{}", "小明");
    logger->info("你好!{}", "小明");
    logger->warn("你好!{}", "小明");
    logger->error("你好!{}", "小明");
    logger->critical("你好!{}", "小明");
    std::cout << "日志输出演示完毕!\n";

    return 0;
}

(2)运行结果:

(3)异步日志输出器async.cpp:

cpp 复制代码
#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/async.h>

int main()
{
    //设置全局的刷新策略
    //每秒刷新
    spdlog::flush_every(std::chrono::seconds(1));
    spdlog::flush_on(spdlog::level::level_enum::debug);
    spdlog::set_level(spdlog::level::level_enum::debug);

    //初始化异步日志输出线程配置
    //void init_thread_pool(size_t q_size, size_t thread_count)
    spdlog::init_thread_pool(3072, 1);
    //创建异步日志器(标准输出/文件) -- 工厂接口默认创建的就是同步日志器
    auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("async-logger");
    //设置日志输出格式
    logger->set_pattern("[%n][%H:%M:%S][%t][%-8l] %v");
    //进行简单的日志输出
    logger->trace("你好!{}", "小明");
    logger->debug("你好!{}", "小明");
    logger->info("你好!{}", "小明");
    logger->warn("你好!{}", "小明");
    logger->error("你好!{}", "小明");
    logger->critical("你好!{}", "小明");
    std::cout << "日志输出演示完毕!\n";
    return 0;
}

(4)运行结果:

5.5 对Spdlog的进行封装

(1)因为 spdlog 的日志输出对文件名和行号并不是很友好(也有可能是调研不到位...),以及因为 spdlog 本身实现了线程安全,如果使用默认日志器每次进行单例获取,效率会有降低,因此进行二次封装,简化使用。日志的初始化封装接口以及日志的输出接口封装宏。

(2)封装成logger.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/async.h>

// mode - 运行模式: true-发布模式; false调试模式
std::shared_ptr<spdlog::logger> g_default_logger;
void init_logger(bool mode, const std::string &file, int32_t level)
{
    if(mode == false) 
    {
        //如果是调试模式,则创建标准输出日志器,输出等级为最低
        g_default_logger = spdlog::stdout_color_mt("default-logger");
        g_default_logger->set_level(spdlog::level::level_enum::trace);
        g_default_logger->flush_on(spdlog::level::level_enum::trace);
    }
    else 
    {
        //否则是发布模式,则创建文件输出日志器,输出等级根据参数而定
        g_default_logger = spdlog::basic_logger_mt("default-logger", file);
        g_default_logger->set_level((spdlog::level::level_enum)level);
        g_default_logger->flush_on((spdlog::level::level_enum)level);
    }
    
    g_default_logger->set_pattern("[%n][%H:%M:%S][%t][%-8l]%v");
}

#define LOG_TRACE(format, ...) g_default_logger->trace(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_DEBUG(format, ...) g_default_logger->debug(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_INFO(format, ...) g_default_logger->info(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_WARN(format, ...) g_default_logger->warn(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) g_default_logger->error(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_FATAL(format, ...) g_default_logger->critical(std::string("[{}:{}] ") + format, __FILE__, __LINE__, ##__VA_ARGS__)

(3)封装测试:

cpp 复制代码
#include <gflags/gflags.h>
#include "logger.hpp"

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);
    LOG_DEBUG("你好:{}", "小明");
    LOG_INFO("你好:{}", "小明");
    LOG_WARN("你好:{}", "小明");
    LOG_ERROR("你好:{}", "小明");
    LOG_FATAL("你好:{}", "小明");
    LOG_DEBUG("这是一个测试");

    return -1;
}

(4)运行结果:


(5)整体Makefile:

cpp 复制代码
all:sync async main
main:main.cpp
	g++ $^ -o $@ -std=c++17 -lspdlog -lfmt -lgflags -lpthread
sync:sync.cpp
	g++ $^ -o $@ -std=c++17 -lspdlog -lfmt -lpthread
async:async.cpp
	g++ $^ -o $@ -std=c++17 -lspdlog -lfmt -lpthread

.PHONY:clean
clean:
	rm -f main sync async

5.6 spdlog 与 glog 组件对比

(1)glog 和 spdlog 都是流行的 C++ 日志库,它们各自具有不同的特点和优势。以下是对这两个库的对比分析,包括性能测试的结果和使用场景的考量。

(2)使用场景的考量:

  • glog 是由 Google 开发的一个开源 C++ 日志库,它提供了丰富的日志功能,包括多种日志级别、条件日志记录、日志文件管理、信号处理、自定义日志格式等。glog 默认情况下是同步记录日志的,这意味着每次写日志操作都会阻塞直到日志数据被写入磁盘。
  • spdlog 是一个开源的、高性能的 C++ 日志库,它支持异步日志记录,允许在不影响主线程的情况下进行日志写入。spdlog 旨在提供零配置的用户体验,只需包含头文件即可使用。它还支持多种输出目标、格式化选项和线程安全。

(3)性能方面:

  • 根据性能对比测试分析,glog 在同步调用的场景下的性能较 spdlog 慢。在一台低配的服务器上,glog 耗时 1.027 秒处理十万笔日志数据,而在固态硬盘上的耗时为 0.475 秒。
  • 在同样的性能测试中,spdlog 在同步调用的场景下比 glog 快。在低配服务器上的耗时为 0.135 秒,而在固态硬盘上的耗时为 0.057 秒。此外,spdlog 还提供了异步日志记录的功能,其简单异步模式的耗时为 0.158 秒。

(4)对比总结:

  • 性能:从性能测试结果来看,spdlog 在同步调用场景下的性能优于 glog。当涉及到大量日志数据时,spdlog 显示出更快的处理速度。
  • 异步日志:spdlog 支持异步日志记录,这在处理高负载应用程序时非常有用,可以减少日志操作对主线程的影响。
  • 易用性:spdlog 提供了更简单的集成和配置方式,只需包含头文件即可使用,而glog 可能需要额外的编译和配置步骤。
  • 功能:glog 提供了一些特定的功能,如条件日志记录和信号处理,这些在某些场景下可能非常有用。
  • 使用场景:glog 可能更适合那些对日志性能要求不是特别高,但需要一些特定功能的场景。而 spdlog 则适合需要高性能日志记录和异步日志能力的应用程序。

在选择日志库时,开发者应根据项目的具体需求和性能要求来决定使用哪个库。如果项目对日志性能有较高要求,或者需要异步日志记录来避免阻塞主线程,spdlog 可能是更好的选择。如果项目需要一些特定的日志功能,或者已经在使用 glog 且没有显著的性能问题,那么继续使用 glog 也是合理的。

(5)总结:

  • spdlog 是一个功能强大且易于使用的 C++ 日志库,它提供了丰富的功能和高性能的日志记录能力。通过简单的 API,开发者可以快速地在项目中实现日志记录,同时保持代码的清晰和可维护性。无论是在开发阶段还是生产环境中,spdlog 都能提供稳定和高效的日志服务。

6. etcd的安装与使用

6.1 etcd的介绍

Etcd 是一个 golang 编写的分布式、高可用的一致性键值存储系统,用于配置共享和服务发现等。它使用 Raft 一致性算法来保持集群数据的一致性,且客户端通过长连接watch 功能,能够及时收到数据变化通知,相较于 Zookeeper 框架更加轻量化。以下是关于 etcd 的安装与使用方法的详细介绍。

6.2 etcd的安装

(1)首先需要在你的系统中安装 Etcd。Etcd 是一个分布式键值存储,通常用于服务发现和配置管理。以下是在 Linux 系统上安装 Etcd 的基本步骤:

  1. 安装 Etcd:
powershell 复制代码
Bash
sudo apt-get install etcd

当安装的时候系统显示找不到etcd的时候可以分别安装etcd-client、etcd-discovery 和 etcd-server。因为它们都是 etcd 项目的一部分。系统可能将etcd拆分成这三个,只需要依次安装即可。

  1. 启动 Etcd 服务:
powershell 复制代码
Bash
sudo systemctl start etcd
  1. 设置 Etcd 开机自启:
powershell 复制代码
Bash
sudo systemctl enable etcd

(2)节点配置:如果是单节点集群其实就可以不用进行配置,默认 etcd 的集群节点通信端口为 2380,客户端访问端口为 2379。若需要修改,则可以配置:/etc/default/etcd

powershell 复制代码
Bash
#节点名称,默认为 "default"
ETCD_NAME="etcd1"
#数据目录,默认为 "${name}.etcd"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
#用于客户端连接的 URL。
ETCD_LISTEN_CLIENT_URLS="http://192.168.65.132:2379,http://127.0.0.1:2379"
#用于客户端访问的公开,也就是提供服务的 URL
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.65.132:2379,http://127.
0.0.1:2379"
#用于集群节点间通信的 URL。
ETCD_LISTEN_PEER_URLS="http://192.168.65.132:2380"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.65.132:2380"
#心跳间隔时间-毫秒
ETCD_HEARTBEAT_INTERVAL=100
#选举超时时间-毫秒
ETCD_ELECTION_TIMEOUT=1000
#以下为集群配置,若无集群则需要注销
#初始集群状态和配置--集群中所有节点
#ETCD_INITIAL_CLUSTER="etcd1=http://192.168.65.132:2380,etcd2=http
://192.168.65.132:2381,etcd3=http://192.168.65.132:2382"
#初始集群令牌-集群的 ID
#ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
#ETCD_INITIAL_CLUSTER_STATE="new"
#以下为安全配置,如果要求 SSL 连接 etcd 的话,把下面的配置启用,并修改文件
路径
#ETCD_CERT_FILE="/etc/ssl/client.pem"
#ETCD_KEY_FILE="/etc/ssl/client-key.pem"
#ETCD_CLIENT_CERT_AUTH="true"
#ETCD_TRUSTED_CA_FILE="/etc/ssl/ca.pem"
#ETCD_AUTO_TLS="true"
#ETCD_PEER_CERT_FILE="/etc/ssl/member.pem"
#ETCD_PEER_KEY_FILE="/etc/ssl/member-key.pem"
#ETCD_PEER_CLIENT_CERT_AUTH="false"
#ETCD_PEER_TRUSTED_CA_FILE="/etc/ssl/ca.pem"
#ETCD_PEER_AUTO_TLS="true"

(3)单节点运行示例:

powershell 复制代码
Bash
etcd --name etcd1 --initial-advertise-peer-urls 
http://192.168.65.132:2380 \ 
 --listen-peer-urls http://192.168.65.132:2380 \ 
 --listen-client-urls http://192.168.65.132:2379 \ 
 --advertise-client-urls http://192.168.65.132:2379 \
 --initial-cluster-token etcd-cluster \ 
 --initial-cluster 
etcd1=http://192.168.65.132:2380,etcd2=http://192.168.65.132:2381,
etcd3=http://192.168.65.132:2382 \ 
 --initial-cluster-state new &> nohup1.out &

(4)运行验证:

powershell 复制代码
C++
etcdctl put mykey "this is awesome"
  • 如果出现报错:
powershell 复制代码
Bash
[xiaomaker@alibaba ~]$ etcdctl put mykey "this is awesome" No help topic for 'put'
  • 则 sudo vi /etc/profile 在末尾声明环境变量 ETCDCTL_API=3 以确定 etcd 版本。
powershell 复制代码
Bash
export ETCDCTL_API=3
  • 完毕后,加载配置文件,并重新执行测试指令:
powershell 复制代码
Bash
[xiaomaker@alibaba ~]$ source /etc/profile
[xiaomaker@alibaba ~]$ etcdctl put mykey "this is awesome"
OK
[xiaomaker@alibaba ~]$ etcdctl get mykey
mykey
this is awesome
[xiaomaker@alibaba ~]$ etcdctl del mykey
  • 在命令行中输入以下命令来停止etcd服务:
powershell 复制代码
bash
[xiaomaker@alibaba ~]$ sudo systemctl stop etcd
  • 为了确保etcd服务已经成功停止,可以运行以下命令来检查etcd服务的状态:
powershell 复制代码
bash
[xiaomaker@alibaba ~]$ sudo systemctl status etcd

(5)搭建服务注册发现中心:

  • 使用 Etcd 作为服务注册发现中心,你需要定义服务的注册和发现逻辑。这通常涉及到以下几个操作:
    1. 服务注册:服务启动时,向 Etcd 注册自己的地址和端口。
    2. 服务发现:客户端通过 Etcd 获取服务的地址和端口,用于远程调用。
    3. 健康检查:服务定期向 Etcd 发送心跳,以维持其注册信息的有效性。

etcd 采用 golang 编写,v3 版本通信采用 grpc API,即(HTTP2+protobuf);

官方只维护了 go 语言版本的 client 库,因此需要找到 C/C++ 非官方的 client 开发库。

  • etcd-cpp-apiv3:

  • 依赖安装:

powershell 复制代码
C++
sudo apt-get install libboost-all-dev libssl-dev
sudo apt-get install libprotobuf-dev protobuf-compiler-grpc
sudo apt-get install libgrpc-dev libgrpc++-dev 
sudo apt-get install libcpprest-dev
  • etcd-cpp-apiv3框架安装:
powershell 复制代码
Bash
# 1. 克隆官方库
git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git
# 2. 进入目录
cd etcd-cpp-apiv3
# 3. 创建build目录
mkdir build && cd build
# 4. 构建make
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
# 5. 运行安装
make -j$(nproc) && sudo make install

6.3 客户端类的接口介绍

(1)客户端类简单的接口展示:

cpp 复制代码
// pplx::task 并行库异步结果对象
// 阻塞方式 get(): 阻塞直到任务执行完成,并获取任务结果
// 非阻塞方式 wait(): 等待任务到达终止状态,然后返回任务状态
namespace etcd
{
    class Value
    {
        bool is_dir();                    //判断是否是一个目录
        std::string const &key();       // 键值对的 key 值
        std::string const &as_string(); // 键值对的 val 值

        int64_t lease(); // 用于创建租约的响应中,返回租约 ID
    };
        // etcd 会监控所管理的数据的变化,一旦数据产生变化会通知客户端
        // 在通知客户端的时候,会返回改变前的数据和改变后的数据
    class Event
    {
        enum class EventType
        {
            PUT,     // 键值对新增或数据发生改变
            DELETE_, // 键值对被删除
            INVALID,
        };
        enum EventType event_type();
        const Value &kv();
        const Value &prev_kv();
    }; 
    class Response
    {
        bool is_ok();
        std::string const &error_message();
        Value const &value();            // 当前的数值 或者 一个请求的处理结果
        Value const &prev_value();           // 之前的数值
        Value const &value(int index);       //
        std::vector<Event> const &events(); // 触发的事件
    };
    
    class KeepAlive
    {
        KeepAlive(Client const &client, int ttl, int64_t lease_id = 0);
        // 返回租约 ID
        int64_t Lease();
        // 停止保活动作
        void Cancel();
    }; 
    
    class Client
    {
        // etcd_url: "http://127.0.0.1:2379"
        Client(std::string const &etcd_url,
               std::string const &load_balancer = "round_robin");
        // Put a new key-value pair 新增一个键值对
        pplx::task<Response> put(std::string const &key,
                                 std::string const &value);
        // 新增带有租约的键值对 (一定时间后,如果没有续租,数据自动删除)
        pplx::task<Response> put(std::string const &key,
                                 std::string const &value,
                                 const int64_t leaseId);
        // 获取一个指定 key 目录下的数据列表
        pplx::task<Response> ls(std::string const &key);

        // 创建并获取一个存活 ttl 时间的租约
        pplx::task<Response> leasegrant(int ttl);
        // 获取一个租约保活对象,其参数 ttl 表示租约有效时间
        pplx::task<std::shared_ptr<KeepAlive>> leasekeepalive(int
                                                                  ttl);
        // 撤销一个指定的租约
        pplx::task<Response> leaserevoke(int64_t lease_id);
        // 数据锁
        pplx::task<Response> lock(std::string const &key);
    };

    class Watcher
    {
        Watcher(Client const &client,
                std::string const &key,                 // 要监控的键值对 key
                std::function<void(Response)> callback, // 发生改变后的回调
                bool recursive = false);                // 是否递归监控目录下的所有数据改变
        Watcher(std::string const &address,
                std::string const &key,
                std::function<void(Response)> callback,
                bool recursive = false);
        // 阻塞等待,直到监控任务被停止
        bool Wait();
        bool Cancel();
    };
}

6.4 客户端类的使用

(1)服务注册:

cpp 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <thread>

int main(int argc, char *argv[])
{
    std::string etcd_host = "http://127.0.0.1:2379";
    
    // 实例化客户端对象
    etcd::Client client(etcd_host);

    // 获取租约保活对象--伴随着创建一个指定有效时长的租约
    auto keep_alive = client.leasekeepalive(3).get();

    // 获取租约ID
    auto lease_id = keep_alive->Lease();

    // 向etcd新增数据
    auto resp1 = client.put("/service/user", "127.0.0.1:8080", lease_id).get();
    if (resp1.is_ok() == false)
    {
        std::cout << "新增数据失败:" << resp1.error_message() << std::endl;
        return -1;
    }

    auto resp2 = client.put("/service/friend", "127.0.0.1:9090").get();
    if (resp2.is_ok() == false)
    {
        std::cout << "新增数据失败:" << resp2.error_message() << std::endl;
        return -1;
    }

    std::this_thread::sleep_for(std::chrono::seconds(10));
    return 0;
}

(2)服务发现:

cpp 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <thread>

void callback(const etcd::Response &resp)
{
    if (resp.is_ok() == false)
    {
        std::cout << "收到一个错误的事件通知:" << resp.error_message() << std::endl;
        return;
    }

    for (auto const &ev : resp.events())
    {
        if (ev.event_type() == etcd::Event::EventType::PUT)
        {
            std::cout << "服务信息发生了改变:\n";
            std::cout << "当前的值:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
            std::cout << "原来的值:" << ev.prev_kv().key() << "-" << ev.prev_kv().as_string() << std::endl;
        }
        else if (ev.event_type() == etcd::Event::EventType::DELETE_)
        {
            std::cout << "服务信息下线被删除:\n";
            std::cout << "当前的值:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
            std::cout << "原来的值:" << ev.prev_kv().key() << "-" << ev.prev_kv().as_string() << std::endl;
        }
    }
}

int main(int argc, char *argv[])
{
    std::string etcd_host = "http://127.0.0.1:2379";

    // 实例化客户端对象
    etcd::Client client(etcd_host);

    // 获取指定的键值对信息
    auto resp = client.ls("/service").get();
    if (resp.is_ok() == false)
    {
        std::cout << "获取键值对数据失败: " << resp.error_message() << std::endl;
        return -1;
    }

    int sz = resp.keys().size();
    for (int i = 0; i < sz; ++i)
    {
        std::cout << resp.value(i).as_string() << "可以提供" << resp.key(i) << "服务\n";
    }

    // 实例化一个键值对事件监控对象
    etcd::Watcher watcher(client, "/service", callback, true);
    watcher.Wait();
    return 0;
}

(3)Makefile:

cpp 复制代码
all:put get
put:put.cpp
	g++ -std=c++17 $^ -o $@ -letcd-cpp-api -lcpprest
get:get.cpp
	g++ -std=c++17 $^ -o $@ -letcd-cpp-api -lcpprest

.PHONY:clean
clean:
	rm -rf put get

(4)运行结果:

6.5 封装服务发现与注册功能

(1)服务注册:

  • 主要是在 etcd 服务器上存储一个租期 ns 的保活键值对,表示所能提供指定服务的节点主机,比如 /service/user/instance-1 的 key,且对应的 val 为提供服务的主机节点地址:<key, val> -- < /service/user/instance-1, 127.0.0.1:9000>

    • /service 是主目录,其下会有不同服务的键值对存储
    • /user 是服务名称,表示该键值对是一个用户服务的节点
    • /instance-1 是节点实例名称,提供用户服务可能会有很多节点,每个节点都应该有自己独立且唯一的实例名称。
  • 当这个键值对注册之后,服务发现方可以基于目录进行键值对的发现。且一旦注册节点退出,保活失败,则 3s 后租约失效,键值对被删除,etcd 会通知发现方数据的失效,进而实现服务下线通知的功能。

(2)服务发现:

  • 服务发现分为两个过程:

    • 刚启动客户端的时候,进行 ls 目录浏览,进行/service 路径下所有键值对的获取。
    • 对关心的服务进行 watcher 观测,一旦数值发生变化(新增/删除),收到通知进行节点的管理。
  • 如果 ls 的路径为/service,则会获取到 /service/user, /service/firend, ...等其路径下的所有能够提供服务的实例节点数据。

  • 如果 ls 的路径为 /service/user, 则会获取到 /service/user/instancd-1,

    /service/user/instance-2,...等所有提供用户服务的实例节点数据。

  • 客户端可以将发现的所有<实例 - 地址>管理起来,以便于进行节点的管理:

    • 收到新增数据通知,则向本地管理添加新增的节点地址 -- 服务上线。
    • 收到删除数据通知,则从本地管理删除对应的节点地址 -- 服务下线。
  • 因为管理了所有的能够提供服务的节点主机的地址,因此当需要进行 rpc 调用的时候,则根据服务名称,获取一个能够提供服务的主机节点地址进行访问就可以了,而这里的获取策略,我们采用 RR 轮转策略。

(3)封装思想:

  • 将 etcd 的操作全部封装起来,也不需要管理数据,只需要向外四个基础操作接口:
    • 进行服务注册,也就是向 etcd 添加 <服务-主机地址>的数据
    • 进行服务发现,获取当前所有能提供服务的信息
    • 设置服务上线的处理回调接口
    • 设置服务下线的处理回调接口

这样封装之后,外部的 rpc 调用模块,可以先获取所有的当前服务信息,建立通信连接进行 rpc 调用,也能在有新服务上线的时候新增连接,以及下线的时候移除连接。

(4)具体实现:

cpp 复制代码
#pragma once
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <thread>
#include <memory>
#include "logger.hpp"

namespace Test
{
    // 服务注册客户端类
    class Registry
    {
    public:
        using ptr = std::shared_ptr<Registry>;

        Registry(const std::string &host)
            :_client(std::make_shared<etcd::Client>(host))
            ,_keep_clive(_client->leasekeepalive(3).get())
            ,_lease_id(_keep_clive->Lease())
        {}

        bool registry(const std::string &key, const std::string &val)
        {
            auto resp = _client->put(key, val, _lease_id).get();
            if(resp.is_ok() == false)
            {
                LOG_ERROR("注册数据失败:{}", resp.error_message());
                return false;
            }

            return true;
        }

    private:
        std::shared_ptr<etcd::Client> _client;
        std::shared_ptr<etcd::KeepAlive> _keep_clive;
        uint64_t _lease_id;
    };

    //服务发现客户端类
    class Discovery 
    {
    public:
        using ptr = std::shared_ptr<Discovery>;
        using NotifyCallback = std::function<void(std::string, std::string)>;

        Discovery(const std::string &host, 
            const std::string &basedir,
            const NotifyCallback &put_cb,
            const NotifyCallback &del_cb)
            :_client(std::make_shared<etcd::Client>(host))
            ,_put_cb(put_cb)
            ,_del_cb(del_cb)
        {
            //先进行服务发现,先获取到当前已有的数据
            auto resp = _client->ls(basedir).get();
            if(resp.is_ok() == false) 
            {
                LOG_ERROR("获取服务信息数据失败:{}", resp.error_message());
            }

            int sz = resp.keys().size();
            for(int i = 0; i < sz; ++i) 
            {
                if (_put_cb) _put_cb(resp.key(i), resp.value(i).as_string());
            }

            //然后进行事件监控,监控数据发生的改变并调用回调进行处理
            _watcher = std::make_shared<etcd::Watcher>(*_client.get(), basedir,
                std::bind(&Discovery::callback, this, std::placeholders::_1), true);
        }

    private:
        void callback(const etcd::Response &resp)
        {
            if(resp.is_ok() == false)
            {
                LOG_ERROR("收到一个错误的事件通知: {}", resp.error_message());
                return;
            }

            for(auto const &ev : resp.events())
            {
                if(ev.event_type() == etcd::Event::EventType::PUT)
                {
                    if (_put_cb)
                        _put_cb(ev.kv().key(), ev.kv().as_string());
                    LOG_DEBUG("新增服务:{}-{}", ev.kv().key(), ev.kv().as_string());
                }
                else if(ev.event_type() == etcd::Event::EventType::DELETE_)
                {
                    if(_del_cb)
                    {
                        _del_cb(ev.prev_kv().key(), ev.prev_kv().as_string());
                    }

                    LOG_DEBUG("下线服务:{}-{}", ev.prev_kv().key(), ev.prev_kv().as_string());
                }
            }
        }

    private:
        NotifyCallback _put_cb;
        NotifyCallback _del_cb;
        std::shared_ptr<etcd::Client> _client;
        std::shared_ptr<etcd::Watcher> _watcher;
    };
}

7 brpc的安装与使用

7.1 brpc的介绍

(1)brpc 是用 c++语言编写的工业级 RPC 框架,常用于搜索、存储、机器学习、广告、推荐等高性能系统。你可以使用它:

  • 搭建能在一个端口支持多协议的服务, 或访问各种服务
    • restful http/https, h2/gRPC。使用 brpc 的 http 实现比 libcurl 方便多了。从其他语言通过 HTTP/h2+json 访问基于 protobuf 的协议。
    • redis 和 memcached线程安全,比官方 client 更方便。
    • rtmp/flv/hls可用于搭建流媒体服务。
    • 支持 thrift 线程安全,比官方 client 更方便。
    • 各种百度内使用的协议: baidu_std,streaming_rpc,hulu_pbrpc,sofa_pbrpc,nova_pbrpc,public_pbrpc,ubrpc 和使用 nshead 的各种协议。
    • 基于工业级的 RAFT 算法实现搭建高可用分布式系统,已在 braft 开源。
  • Server 能同步或异步处理请求。
  • Client 支持同步、异步、半同步,或使用组合 channels 简化复杂的分库或并发访问。
  • 通过 http 界面调试服务, 使用 cpu,heap,contention profilers。
  • 获得更好的延时和吞吐。
  • 把你组织中使用的协议快速地加入 brpc,或定制各类组件,包括命名服务 (dns, zk, etcd),负载均衡 (rr、random、consistent hashing)。

7.2 brpc的安装

(1)先安装依赖:

powershell 复制代码
C++
[xiaomaker@alibaba brpc]$ sudo apt-get install -y git g++ make libssl-dev libprotobuf-dev libprotoc-dev protobuf-compiler libleveldb-dev

(2)安装 brpc:

powershell 复制代码
C++
[xiaomaker@alibaba brpc]$ git clone https://github.com/apache/brpc.git
[xiaomaker@alibaba brpc]$ cd brpc/
[xiaomaker@alibaba brpc]$ mkdir build && cd build
[xiaomaker@alibaba build]$ cmake -DCMAKE_INSTALL_PREFIX=/usr .. && cmake --build . -j6
[xiaomaker@alibaba build]$ make && sudo make install

7.3 brpc的介绍

(1)日志输出类与接口:

  • 包含头文件: #include <butil/logging.h>
    日志输出这里,本质上我们其实用不着 brpc 的日志输出,因此在这里主要介绍如何关闭日志输出。
cpp 复制代码
C++
namespace logging
{
    enum LoggingDestination
    {
        LOG_TO_NONE = 0
    };
    
    struct BUTIL_EXPORT LoggingSettings
    {
        LoggingSettings();
        LoggingDestination logging_dest;
    };

    bool InitLogging(const LoggingSettings &settings);
}

(2)protobuf 类与接口:

cpp 复制代码
C++
namespace google
{
    namespace protobuf
    {
        class PROTOBUF_EXPORT Closure
        {
        public:
            Closure() {}
            virtual ~Closure();
            virtual void Run() = 0;
        };
        inline Closure *NewCallback(void (*function)());
        class PROTOBUF_EXPORT RpcController
        {
            bool Failed();
            std::string ErrorText();
        }
    }
}

(3)服务端类与接口:这里只介绍主要用到的成员与接口。

cpp 复制代码
C++
namespace brpc
{
    struct ServerOptions
    {
        // 无数据传输,则指定时间后关闭连接
        int idle_timeout_sec; // Default: -1 (disabled)
        int num_threads;      // Default: #cpu-cores
                              //....
    };

    enum ServiceOwnership 
    {
        // 添加服务失败时,服务器将负责删除服务对象
        SERVER_OWNS_SERVICE,
        // 添加服务失败时,服务器也不会删除服务对象
        SERVER_DOESNT_OWN_SERVICE
    };

    class Server
    {
        int AddService(google::protobuf::Service *service,
                       ServiceOwnership ownership);
        int Start(int port, const ServerOptions *opt);
        int Stop(int closewait_ms /*not used anymore*/);
        int Join();
        // 休眠直到 ctrl+c 按下,或者 stop 和 join 服务器
        void RunUntilAskedToQuit();
    };
    
    class ClosureGuard
    {
        explicit ClosureGuard(google::protobuf::Closure *done);
        ~ClosureGuard()
        {
            if (_done)
                _done->Run();
        }
    }; 
    class HttpHeader
    {
        void set_content_type(const std::string &type);
        const std::string *GetHeader(const std::string &key);
        void SetHeader(const std::string &key, const std::string &value);                                                       
        const URI &uri() const { return _uri; }
        HttpMethod method() const { return _method; }
        void set_method(const HttpMethod method); 
        int status_code(); 
        void set_status_code(int status_code);
    };

    class Controller : public google::protobuf::RpcController
    {
        void set_timeout_ms(int64_t timeout_ms);
        void set_max_retry(int max_retry);
        google::protobuf::Message *response();
        HttpHeader &http_response();
        HttpHeader &http_request();
        bool Failed();
        std::string ErrorText();

        using AfterRpcRespFnType = std::function<void(Controller *cntl, const google::protobuf::Message *req, const google::protobuf::Message *res)>;

        void set_after_rpc_resp_fn(AfterRpcRespFnType &&fn);
    };
}

(4)客户端类与接口:

cpp 复制代码
C++
namespace brpc
{
    struct ChannelOptions
    {
        // 请求连接超时时间
        int32_t connect_timeout_ms; // Default: 200 (milliseconds)
        // rpc 请求超时时间
        int32_t timeout_ms; // Default: 500 (milliseconds)
        // 最大重试次数
        int max_retry; // Default: 3
        // 序列化协议类型 options.protocol = "baidu_std";
        AdaptiveProtocolType protocol;
        //....
    };
    
    class Channel : public ChannelBase
    {
        // 初始化接口,成功返回 0;
        int Init(const char *server_addr_and_port,
                 const ChannelOptions *options);
    };
}

7.4 brpc的使用

(1)同步调用:

  • 同步调用是指客户端会阻塞收到 server 端的响应或发生错误。
  • 创建 proto 文件 - main.proto
cpp 复制代码
syntax="proto3";

package example;

option cc_generic_services = true;

message EchoRequest {
    string message = 1;
}

message EchoResponse {
    string message = 1;
}

service EchoService {
    rpc Echo(EchoRequest) returns (EchoResponse);
}
  • 之后运行如下命令(也可以放入Makefile当中):
bash 复制代码
protoc --cpp_out=./ main.proto
  • 创建服务端代码server.cpp:
cpp 复制代码
#include <brpc/server.h>
#include <butil/logging.h>
#include "main.pb.h"

// 1. 继承于EchoService创建一个子类,并实现rpc调用的业务功能
class EchoServiceImpl : public example::EchoService
{
public:
    EchoServiceImpl()
    {}

    void Echo(google::protobuf::RpcController* controller,
                const ::example::EchoRequest* request,
                ::example::EchoResponse* response,
                ::google::protobuf::Closure* done)
    {
        brpc::ClosureGuard rpc_guard(done);
        std::cout << "收到消息:" << request->message() << std::endl;

        std::string str = request->message() + "--这是响应!!";
        response->set_message(str);
    }

    ~EchoServiceImpl()
    {}
};

int main(int argc, char *argv[])
{
    // 关闭brpc的默认日志输出
    logging::LoggingSettings settings;
    settings.logging_dest = logging::LoggingDestination::LOG_TO_NONE;
    logging::InitLogging(settings);

    // 2. 构造服务器对象
    brpc::Server server;

    // 3. 向服务器对象中,新增EchoService服务
    EchoServiceImpl echo_service;
    int ret = server.AddService(&echo_service, brpc::ServiceOwnership::SERVER_DOESNT_OWN_SERVICE);
    if(ret == -1)
    {
        std::cout << "添加Rpc服务失败!\n";
        return -1;
    }

    // 4. 启动服务器
    brpc::ServerOptions options;
    options.idle_timeout_sec = -1;  //连接空闲超时时间-超时后连接被关闭
    options.num_threads = 1;        // IO线程数量
    ret = server.Start(8080, &options);
    if(ret == -1)
    {
        std::cout << "启动服务器失败!\n";
        return -1;
    }

    server.RunUntilAskedToQuit();//修改等待运行结束
    return 0;
}
  • 创建客户端源码client.cpp:
cpp 复制代码
#include <brpc/channel.h>
#include <thread>
#include "main.pb.h"

int main()
{
    //1. 构造Channel信道,连接服务器
    brpc::ChannelOptions options;
    options.connect_timeout_ms = -1;// 连接等待超时时间,-1表示一直等待
    options.timeout_ms = -1; //rpc请求等待超时时间,-1表示一直等待
    options.max_retry = 3;//请求重试次数
    options.protocol = "baidu_std"; //序列化协议,默认使用baidu_std

    brpc::Channel channel;
    int ret = channel.Init("127.0.0.1:8080", &options);
    if(ret == -1)
    {
        std::cout << "初始化信道失败!\n";
        return -1;
    }

    //2. 构造EchoService_Stub对象,用于进行rpc调用
    example::EchoService_Stub stub(&channel);

    //3. 进行Rpc调用/
    example::EchoRequest req;
    req.set_message("你好~小明~!");

    brpc::Controller *cntl = new brpc::Controller();
    example::EchoResponse *rsp = new example::EchoResponse();
    stub.Echo(cntl, &req, rsp, nullptr);
    if (cntl->Failed() == true) {
        std::cout << "Rpc调用失败:" << cntl->ErrorText() << std::endl;
        return -1;
    }
    std::cout << "收到响应: " << rsp->message() << std::endl;
    delete cntl;
    delete rsp;
    std::cout << "异步调用结束!\n";
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 0;
}
  • 编写Makefile:
cpp 复制代码
all:server client
server:server.cpp main.pb.cc
	g++ $^ -o $@ -std=c++17 -lbrpc -lgflags -lssl -lcrypto -lprotobuf -lleveldb
client:client.cpp main.pb.cc
	g++ $^ -o $@ -std=c++17 -lbrpc -lgflags -lssl -lcrypto -lprotobuf -lleveldb

.PHONY:clean
clean:
	rm -rf server client
  • 运行结果:

(2)异步调用:

  • 异步调用是指客户端注册一个响应处理回调函数, 当调用一个 RPC 接口时立即返回,不会阻塞等待响应, 当 server 端返回响应时会调用传入的回调函数处理响应。
  • 具体的做法:给 CallMethod 传递一个额外的回调对象 done,CallMethod 在发出request 后就结束了,而不是在 RPC 结束后。当 server 端返回 response 或发生错误(包括超时)时,done->Run()会被调用。对 RPC 的后续处理应该写在 done->Run()里,而不是 CallMethod 后。由于 CallMethod 结束不意味着 RPC 结束,response/controller 仍可能被框架及 done->Run()使用,它们一般得创建在堆上,并在 done->Run()中删除。如果提前删除了它们,那当 done->Run()被调用时,将访问到无效内存。
cpp 复制代码
#include <brpc/channel.h>
#include <thread>
#include "main.pb.h"

void callback(brpc::Controller* cntl, ::example::EchoResponse* response)
{
    std::unique_ptr<brpc::Controller> cntl_guard(cntl);
    std::unique_ptr<example::EchoResponse> resp_guard(response);
    if(cntl->Failed() == true)
    {
        std::cout << "Rpc调用失败:" << cntl->ErrorText() << std::endl;
        return;
    }

    std::cout << "收到响应: " << response->message() << std::endl;
}

int main()
{
    //1. 构造Channel信道,连接服务器
    brpc::ChannelOptions options;
    options.connect_timeout_ms = -1;// 连接等待超时时间,-1表示一直等待
    options.timeout_ms = -1; //rpc请求等待超时时间,-1表示一直等待
    options.max_retry = 3;//请求重试次数
    options.protocol = "baidu_std"; //序列化协议,默认使用baidu_std

    brpc::Channel channel;
    int ret = channel.Init("127.0.0.1:8080", &options);
    if(ret == -1)
    {
        std::cout << "初始化信道失败!\n";
        return -1;
    }

    //2. 构造EchoService_Stub对象,用于进行rpc调用
    example::EchoService_Stub stub(&channel);

    //3. 进行Rpc调用/
    example::EchoRequest req;
    req.set_message("你好~小明~!");

    brpc::Controller *cntl = new brpc::Controller();
    example::EchoResponse *rsp = new example::EchoResponse();
    auto clusure = google::protobuf::NewCallback(callback, cntl, rsp);
    stub.Echo(cntl, &req, rsp, clusure); //异步调用
    std::cout << "异步调用结束!\n";
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 0;
}

7.5 brpc的封装

(1)封装思想:

  • rpc 调用这里的封装,因为不同的服务调用使用的是不同的 Stub,这个封装起来的意义不大,因此我们只需要封装通信所需的 Channel 管理即可,这样当需要进行什么样的服务调用的时候,只需要通过服务名称获取对应的 channel,然后实例化 Stub 进行调用即可。
  • 封装 Channel 的管理,每个不同的服务可能都会有多个主机提供服务,因此一个服务可能会对应多个 Channel,需要将其管理起来,并提供获取指定服务 channel 的接口。进行 rpc 调用时,获取 channel,目前以 RR 轮转的策略选择 channel。
  • 提供进行服务声明的接口,因为在整个系统中,提供的服务有很多,但是当前可能并不一定会用到所有的服务,因此通过声明来告诉模块哪些服务是自己关心的,需要建立连接管理起来,没有添加声明的服务即使上线也不需要进行连接的建立。
  • 提供服务上线时的处理接口,也就是新增一个指定服务的 channel。
  • 提供服务下线时的处理接口,也就是删除指定服务下的指定 channel。

(2)具体实现channel.hpp:

cpp 复制代码
#pragma once
#include <brpc/channel.h>
#include <string>
#include <vector>
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <mutex>
#include "logger.hpp"

namespace Test
{
    // 封装单个服务的信道管理类:
    class ServiceChannel
    {
    public:
        using ChannelPtr = std::shared_ptr<brpc::Channel>;
        using Ptr = std::shared_ptr<ServiceChannel>;


        ServiceChannel(const std::string &name)
            :_service_name(name)
            ,_index(0)
        {}

        // 服务上线了一个节点,则调用append新增信道
        void append(const std::string &host)
        {
            auto channel = std::make_shared<brpc::Channel>();
            brpc::ChannelOptions options;
            options.connect_timeout_ms = -1;
            options.timeout_ms = -1;
            options.max_retry = 3;
            options.protocol = "baidu_std";
            int ret = channel->Init(host.c_str(), &options);
            if(ret == -1)
            {
                LOG_ERROR("初始化{}-{}信道失败!", _service_name, host);
                return;
            }

            std::unique_lock<std::mutex> lock(_mutex);
            _hosts.insert(std::make_pair(host, channel));
            _channels.push_back(channel);
        }

        // 服务下线了一个节点,则调用remove释放信道
        void remove(const std::string &host)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto iter = _hosts.find(host);
            if(iter == _hosts.end())
            {
                LOG_WARN("{}-{}节点删除信道时,没有找到信道信息!", _service_name, host);
                return;
            }

            for(auto vit = _channels.begin(); vit != _channels.end(); ++vit)
            {
                if (*vit == iter->second)
                {
                    _channels.erase(vit);
                    break;
                }
            }

            _hosts.erase(iter);
        }

        // 通过RR轮转策略,获取一个Channel用于发起对应服务的Rpc调用
        ChannelPtr choose()
        {
            std::unique_lock<std::mutex> lock(_mutex);
            if(_channels.size() == 0)
            {
                LOG_ERROR("当前没有能够提供 {} 服务的节点!", _service_name);
                return ChannelPtr();
            }

            int32_t idx = _index++ % _channels.size();
            return _channels[idx];
        }

    private:
        std::mutex _mutex;
        int32_t _index;                                     // 当前轮转下标计数器
        std::string _service_name;                          // 服务名称
        std::vector<ChannelPtr> _channels;                  // 当前服务对应的信道集合
        std::unordered_map<std::string, ChannelPtr> _hosts; // 主机地址与信道映射关系
    };

    // 总体的服务信道管理类
    class ServiceManager
    {
    public:
        using Ptr = std::shared_ptr<ServiceManager>;

        ServiceManager()
        {}

        // 获取指定服务的节点信道
        ServiceChannel::ChannelPtr choose(const std::string &service_name)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto iter = _services.find(service_name);
            if(iter == _services.end())
            {
                LOG_ERROR("当前没有能够提供 {} 服务的节点!", service_name);
                return ServiceChannel::ChannelPtr();
            }

            return iter->second->choose();
        }

        // 先声明,我关注哪些服务的上下线,不关心的就不需要管理了
        void declared(const std::string &service_name)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _follow_services.insert(service_name);
        }

        // 服务上线时调用的回调接口,将服务节点管理起来
        void onServiceOnline(const std::string &service_instance, const std::string &host)
        {
            std::string service_name = getServiceName(service_instance);
            ServiceChannel::Ptr service;
            {
                std::unique_lock<std::mutex> lock(_mutex);
                auto fit = _follow_services.find(service_name);
                if(fit == _follow_services.end())
                {
                    LOG_DEBUG("{}-{} 服务上线了,但是当前并不关心!", service_name, host);
                    return;
                }

                // 先获取管理对象,没有则创建,有则添加节点
                auto sit = _services.find(service_name);
                if(sit == _services.end())
                {
                    service = std::make_shared<ServiceChannel>(service_name);
                    _services.insert(std::make_pair(service_name, service));
                }
                else
                {
                    service = sit->second;
                }
            }

            if(!service) 
            {
                LOG_ERROR("新增 {} 服务管理节点失败!", service_name);
                return;
            }

            service->append(host);
            LOG_DEBUG("{}-{} 服务上线新节点,进行添加管理!", service_name, host);
        }

        // 服务下线时调用的回调接口,从服务信道管理中,删除指定节点信道
        void onServiceOffline(const std::string &service_instance, const std::string &host)
        {
            std::string service_name = getServiceName(service_instance);
            ServiceChannel::Ptr service;
            {
                std::unique_lock<std::mutex> lock(_mutex);
                auto fit = _follow_services.find(service_name);
                if(fit == _follow_services.end())
                {
                    LOG_DEBUG("{}-{} 服务下线了,但是当前并不关心!", service_name, host);
                    return;
                }

                // 先获取管理对象,没有则创建,有则添加节点
                auto sit = _services.find(service_name);
                if(sit == _services.end())
                {
                    LOG_WARN("删除{}服务节点时,没有找到管理对象", service_name);
                    return;
                }

                service = sit->second;
            }

            service->remove(host);
            LOG_DEBUG("{}-{} 服务下线节点,进行删除管理!", service_name, host);
        }

    private:
        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::unordered_set<std::string> _follow_services;
        std::unordered_map<std::string, ServiceChannel::Ptr> _services;
    };
}

8. ES的安装与使用

8.1 ES的介绍

  • Elasticsearch, 简称 ES,它是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful 风格接口,多数据源,自动搜索负载等。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。es 也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。
  • Elasticsearch 是面向文档(document oriented)的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在 Elasticsearch 中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。

8.2 ES和kibana的安装

(1)安装ES:

shell 复制代码
Shell
# 添加仓库秘钥
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - 
# 上边的添加方式会导致一个 apt-key 的警告,如果不想报警告使用下边这个
curl -s https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --no-default-keyring --keyring gnupgring:/etc/apt/trusted.gpg.d/icsearch.gpg --import
# 可以添加镜像源仓库
echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elasticsearch.list
# 更新软件包列表
sudo apt update
# 安装 es
sudo apt-get install elasticsearch=7.17.21
# 启动 es
sudo systemctl start elasticsearch
# 安装 ik 分词器插件
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/7.17.21
  • 启动 es:
powershell 复制代码
Shell
sudo systemctl start elasticsearch
  • 启动 es 的时候报错:

(2)解决办法:

powershell 复制代码
Shell
#调整 ES 虚拟内存,虚拟内存默认最大映射数为 65530,无法满足 ES 系统要求,需要调整为 262144 以上
sysctl -w vm.max_map_count=262144
#增加虚拟机内存配置
vim /etc/elasticsearch/jvm.options
#新增如下内容
-Xms512m
-Xmx512m
  • es 服务的状态:
powershell 复制代码
Shell
sudo systemctl status elasticsearch.service
  • 验证 es 是否安装成功:
powershell 复制代码
Shell
curl -X GET "http://localhost:9200/"
  • 设置外网访问:如果新配置完成的话,默认只能在本机进行访问。
powershell 复制代码
Shell
# 打开如下文件
vim /etc/elasticsearch/elasticsearch.yml
# 新增以下配置
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]

(3)安装 kibana:

powershell 复制代码
Shell
# 使用 apt 命令安装 Kibana。
sudo apt install kibana
# 配置 Kibana(可选):根据需要配置 Kibana。配置文件通常位于 /etc/kibana/kibana.yml。可能需要设置如服务器地址、端口、Elasticsearch URL 等。
sudo vim /etc/kibana/kibana.yml 
# 例如,你可能需要设置 Elasticsearch 服务的 URL: 大概 32 行左右elasticsearch.host: "http://localhost:9200"
# 启动 Kibana 服务:
sudo systemctl start kibana
# 设置开机自启(可选):如果你希望 Kibana 在系统启动时自动启动,可以使用以下命令来启用自启动。
sudo systemctl enable kibana
# 验证安装:使用以下命令检查 Kibana 服务的状态。
sudo systemctl status kibana
# 访问 Kibana:在浏览器中访问 Kibana,通常是 http://<your-ip>:5601

8.3 ES客户端的安装

代码:https://github.com/seznam/elasticlient

官网:https://seznam.github.io/elasticlient/index.html

  • ES C++的客户端选择并不多, 我们这里使用 elasticlient 库, 下面进行安装。
powershell 复制代码
Shell
# 克隆代码
git clone https://github.com/seznam/elasticlient
# 切换目录
cd elasticlient
# 更新子模块
git submodule update --init --recursive
# 编译代码
mkdir build && cd build
# cmake生成Makefile
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
# 安装
make && make install
  • cmake 生成 makefile 的过程会遇到一个问题:
  • 需要安装 MicroHTTPD 库:
powershell 复制代码
Shell
sudo apt-get install libmicrohttpd-dev

8.4 ES的核心概念

(1)索引(Index):

  • 一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母的),并且当我们要对应于这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。

(2)类型(Type):

  • 在一个索引中,你可以定义一种或多种类型。一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。比如说,我们假设你运营一个博客平台并且将你所有的数据存储到一个索引中。在这个索引中,你可以为用户数据定义一个类型,为博客数据定义另一个类型,为评论数据定义另一个类型...

(3)字段(Field):

  • 字段相当于是数据表的字段,对文档数据根据不同属性进行的分类标识。
分类 类型 备注
字符串 text, keyword text 会被分词生成索引;keyword 不会被分词生成索引,只能精确值搜索
整形 integer, long, short, byte
浮点 double, float
逻辑 boolean true 或 false
日期 date, date_nanos "2018-01-13" 或 "2018-01-13 12:10:30"或者时间戳,即 1970 到现在的秒数/毫秒数
二进制 binary 二进制通常只存储,不索引
范围 range

(4)映射(mapping):

  • 映射是在处理数据的方式和规则方面做一些限制,如某个字段的数据类型、默认值、分析器、是否被索引等等,这些都是映射里面可以设置的,其它就是处理 es 里面数据的一些使用规则设置也叫做映射,按着最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。
名称 数值 备注
enabled true(默认) false 是否仅作存储,不做搜索和分析
index true(默认) false 是否构建倒排索引(决定了是否分词,是否被索引)
index_option
dynamic true(缺省) false 控制 mapping 的自动更新
doc_value true(默认) false 是否开启 doc_value,用户聚合和排序分析,分词字段不能使用
fielddata fielddata": {"format": "disabled"} 是否为 text 类型启动 fielddata,实现排序和聚合分析针对分词字段,参与排序或聚合时能提高性能,不分词字段统一建议使用 doc_value
store true false(默认) 是否单独设置此字段的是否存储而从_source 字段中分离,只能搜索,不能获取值
coerce true(默认) false 是否开启自动数据类型转换功能,比如:字符串转数字,浮点转整型
analyzer "analyzer": "ik" 指定分词器,默认分词器为 standard analyzer
boost "boost": 1.23 字段级别的分数加权,默认值是 1.0
fields "fields": {"raw": {"type": "text", "index": "not_analyzed"}} 对一个字段提供多种索引模式,同一个字段的值,一个分词,一个不分词
data_detection true(默认) false 是否自动识别日期类型

(5)文档 (document):

  • 一个文档是一个可被索引的基础信息单元。比如,你可以拥有某一个客户的文档,某一个产品的一个文档或者某个订单的一个文档。文档以 JSON(Javascript Object Notation)格式来表示,而 JSON 是一个到处存在的互联网数据交互格式。在一个index/type 里面,你可以存储任意多的文档。一个文档必须被索引或者赋予一个索引的 type。
  • Elasticsearch 与传统关系型数据库相比如下:

8.4 ES的封装

(1)封装客户端 api 主要是因为,客户端只提供了基础的数据存储获取调用功能,无法根据我们的思想完成索引的构建,以及查询正文的构建,需要使用者自己组织好 json 进行序列化后才能作为正文进行接口的调用。

(2)而封装的目的就是简化用户的操作,将索引的 json 正文构造,以及查询搜索的正文构造操作给封装起来,使用者调用接口添加字段就行,不用关心具体的 json 数据格式。所以封装的主要内容:

  • 索引构造过程的封装:

    • 索引正文构造过程,大部分正文都是固定的,唯一不同的地方是各个字段不同的名称以及是否只存储不索引这些选项,因此重点关注以下几个点即可:

      • 字段类型:type : text / keyword (目前只用到这两个类型)。
      • 是否索引:enable : true/false。
      • 索引的话分词器类型: analyzer : ik_max_word / standard。
  • 新增文档构造过程的封装:

    • 新增文档其实在常规下都是单条新增,并非批量新增,因此直接添加字段和值就行
  • 文档搜索构造过程的封装:

    • 搜索正文构造过程,我们默认使用条件搜索,我们主要关注的以下几点:
      • 应该遵循的条件是什么:should 中有什么。
      • 条件的匹配方式是什么:match 还是 term/terms,还是 wildcard。
      • 过滤的条件字段是什么:must_not 中有什么。
      • 过滤的条件字段匹配方式是什么:match 还是 wildcard,还是 term/terms。

整个封装的过程其实就是对 Json::Value 对象的一个组织的过程。

(3)具体封装结果:

cpp 复制代码
#include "icsearch.hpp"
#include "user.hxx"
#include "message.hxx"

namespace Test
{
    class ESClientFactory
    {
    public:
        static std::shared_ptr<elasticlient::Client> create(const std::vector<std::string> host_list)
        {
            return std::make_shared<elasticlient::Client>(host_list);
        }
    };
    class ESUser
    {
    public:
        using ptr = std::shared_ptr<ESUser>;
        ESUser(const std::shared_ptr<elasticlient::Client> &client) : _es_client(client) {}
        bool createIndex()
        {
            bool ret = ESIndex(_es_client, "user")
                           .append("user_id", "keyword", "standard", true)
                           .append("nickname")
                           .append("phone", "keyword", "standard", true)
                           .append("description", "text", "standard", false)
                           .append("avatar_id", "keyword", "standard", false)
                           .create();
            if (ret == false)
            {
                LOG_INFO("用户信息索引创建失败!");
                return false;
            }
            LOG_INFO("用户信息索引创建成功!");
            return true;
        }
        bool appendData(const std::string &uid,
                        const std::string &phone,
                        const std::string &nickname,
                        const std::string &description,
                        const std::string &avatar_id)
        {
            bool ret = ESInsert(_es_client, "user")
                           .append("user_id", uid)
                           .append("nickname", nickname)
                           .append("phone", phone)
                           .append("description", description)
                           .append("avatar_id", avatar_id)
                           .insert(uid);
            if (ret == false)
            {
                LOG_ERROR("用户数据插入/更新失败!");
                return false;
            }
            LOG_INFO("用户数据新增/更新成功!");
            return true;
        }
        std::vector<User> search(const std::string &key, const std::vector<std::string> &uid_list)
        {
            std::vector<User> res;
            Json::Value json_user = ESSearch(_es_client, "user")
                                        .append_should_match("phone.keyword", key)
                                        .append_should_match("user_id.keyword", key)
                                        .append_should_match("nickname", key)
                                        .append_must_not_terms("user_id.keyword", uid_list)
                                        .search();
            if (json_user.isArray() == false)
            {
                LOG_ERROR("用户搜索结果为空,或者结果不是数组类型");
                return res;
            }
            int sz = json_user.size();
            LOG_DEBUG("检索结果条目数量:{}", sz);
            for (int i = 0; i < sz; i++)
            {
                User user;
                user.user_id(json_user[i]["_source"]["user_id"].asString());
                user.nickname(json_user[i]["_source"]["nickname"].asString());
                user.description(json_user[i]["_source"]["description"].asString());
                user.phone(json_user[i]["_source"]["phone"].asString());
                user.avatar_id(json_user[i]["_source"]["avatar_id"].asString());
                res.push_back(user);
            }
            return res;
        }

    private:
        // const std::string _uid_key = "user_id";
        // const std::string _desc_key = "user_id";
        // const std::string _phone_key = "user_id";
        // const std::string _name_key = "user_id";
        // const std::string _avatar_key = "user_id";
        std::shared_ptr<elasticlient::Client> _es_client;
    };

    class ESMessage
    {
    public:
        using ptr = std::shared_ptr<ESMessage>;
        ESMessage(const std::shared_ptr<elasticlient::Client> &es_client) : _es_client(es_client) {}
        bool createIndex()
        {
            bool ret = ESIndex(_es_client, "message")
                           .append("user_id", "keyword", "standard", false)
                           .append("message_id", "keyword", "standard", false)
                           .append("create_time", "long", "standard", false)
                           .append("chat_session_id", "keyword", "standard", true)
                           .append("content")
                           .create();
            if (ret == false)
            {
                LOG_INFO("消息信息索引创建失败!");
                return false;
            }
            LOG_INFO("消息信息索引创建成功!");
            return true;
        }
        bool appendData(const std::string &user_id,
                        const std::string &message_id,
                        const long create_time,
                        const std::string &chat_session_id,
                        const std::string &content)
        {
            bool ret = ESInsert(_es_client, "message")
                           .append("message_id", message_id)
                           .append("create_time", create_time)
                           .append("user_id", user_id)
                           .append("chat_session_id", chat_session_id)
                           .append("content", content)
                           .insert(message_id);
            if (ret == false)
            {
                LOG_ERROR("消息数据插入/更新失败!");
                return false;
            }
            LOG_INFO("消息数据新增/更新成功!");
            return true;
        }
        bool remove(const std::string &mid)
        {
            bool ret = ESRemove(_es_client, "message").remove(mid);
            if (ret == false)
            {
                LOG_ERROR("消息数据删除失败!");
                return false;
            }
            LOG_INFO("消息数据删除成功!");
            return true;
        }
        std::vector<bite_im::Message> search(const std::string &key, const std::string &ssid)
        {
            std::vector<bite_im::Message> res;
            Json::Value json_user = ESSearch(_es_client, "message")
                                        .append_must_term("chat_session_id.keyword", ssid)
                                        .append_must_match("content", key)
                                        .search();
            if (json_user.isArray() == false)
            {
                LOG_ERROR("用户搜索结果为空,或者结果不是数组类型");
                return res;
            }
            int sz = json_user.size();
            LOG_DEBUG("检索结果条目数量:{}", sz);
            for (int i = 0; i < sz; i++)
            {
                bite_im::Message message;
                message.user_id(json_user[i]["_source"]["user_id"].asString());
                message.message_id(json_user[i]["_source"]["message_id"].asString());
                boost::posix_time::ptime ctime(boost::posix_time::from_time_t(
                    json_user[i]["_source"]["create_time"].asInt64()));
                message.create_time(ctime);
                message.session_id(json_user[i]["_source"]["chat_session_id"].asString());
                message.content(json_user[i]["_source"]["content"].asString());
                res.push_back(message);
            }
            return res;
        }

    private:
        std::shared_ptr<elasticlient::Client> _es_client;
    };
}

9. cpp-httplib的安装与使用

9.1 cpp-httplib的介绍

(1)C++ HTTP 库(cpp-httplib)是一个轻量级的 C++ HTTP 客户端/服务器库,它提供了简单的 API 来创建 HTTP 服务器和客户端,支持同步和异步操作。以下是一些关于cpp-httplib 的主要特点:

  1. 轻量级:cpp-httplib 的设计目标是简单和轻量,只有一个头文件包含即可,不依赖于任何外部库。
  2. 跨平台:它支持多种操作系统,包括 Windows、Linux 和 macOS。
  3. 同步和异步操作:库提供了同步和异步两种操作方式,允许开发者根据需要选择。
  4. 支持 HTTP/1.1:它实现了 HTTP/1.1 协议,包括持久连接和管道化。
  5. Multipart form-data:支持发送和接收 multipart/form-data 类型的请求,这对于文件上传非常有用。
  6. SSL/TLS 支持:通过使用 OpenSSL 或 mbedTLS 库,cpp-httplib 支持 HTTPS 和 WSS。
  7. 简单易用:API 设计简洁,易于学习和使用。
  8. 性能:尽管是轻量级库,但性能表现良好,适合多种应用场景。
  9. 社区活跃:cpp-httplib 有一个活跃的社区,不断有新的功能和改进被加入。

9.2 cpp-httplib的安装及接口介绍

(1)cpp-httplib安装:

powershell 复制代码
C++
[xiaomaker@alibaba ~]$ git clone https://github.com/yhirose/cpp-httplib.git

而且我们只需要cpp-httplib目录下的httplib.h头文件就可以,将此头文件移动其它目录下即可将cpp-httplib目录删除。

(2)类与接口的介绍:

powershell 复制代码
namespace httplib
{
    struct Request
    {
        std::string method;
        std::string path;
        Headers headers;
        std::string body;
        Params params;
    }; 

    struct Response
    {
        std::string version;
        int status = -1;
        std::string reason;
        Headers headers;
        std::string body;
        void set_content(const std::string &s,
                         const std::string &content_type);
        void set_header(const std::string &key,
                        const std::string &val);
    };

    class Server
    {
        using Handler = std::function<void(const Request &, Response
                                                                &)>;
        Server &Get(const std::string &pattern, Handler handler);
        Server &Post(const std::string &pattern, Handler handler);
        Server &Put(const std::string &pattern, Handler handler);
        Server &Delete(const std::string &pattern, Handler handler);
        bool listen(const std::string &host, int port);
    };
    
    class Client
    {
        explicit Client(const std::string &host, int port);
        Result Get(const std::string &path, const Headers &headers);
        Result Post(const std::string &path, const std::string &body,
                    const std::string &content_type);
        Result Put(const std::string &path, const std::string &body,
                   const std::string &content_type);
        Result Delete(const std::string &path, const std::string &body,
                      const std::string &content_type);
    };
}

9.3 cpp-httplib的使用

(1)main.cpp:

powershell 复制代码
#include <iostream>
#include "../../third/include/httplib.h"

int main()
{
    // 1. 实例化服务器对象
    httplib::Server server;

    // 2. 注册回调函数   void(const httplib::Request &, httplib::Response &)
    server.Get("/hi", [](const httplib::Request &req, httplib::Response &rsp)
    {
        std::cout << req.method << std::endl;
        std::cout << req.path << std::endl;
        for(auto it : req.headers) 
        {
            std::cout << it.first << ": " << it.second << std::endl;
        }

        std::string body = "<html><body><h1>Hello World</h1></body></html>";
        rsp.set_content(body, "text/html");
        rsp.status = 200;
    });

    // 3. 启动服务器
    server.listen("0.0.0.0", 8080);

    return 0;
}

(2)运行结果:


(3)服务端结果:

10. websocketpp的安装与使用

10.1 websocketpp的介绍和原理

(1)WebSocketpp 是一个跨平台的开源(BSD 许可证)头部专用 C++库,它实现了RFC6455(WebSocket 协议)和 RFC7692(WebSocketCompression Extensions)。它允许将 WebSocket 客户端和服务器功能集成到 C++程序中。在最常见的配置中,全功能网络 I/O 由 Asio 网络库提供。WebSocketpp 的主要特性包括:

  • 事件驱动的接口。
  • 支持 HTTP/HTTPS、WS/WSS、IPv6。
  • 灵活的依赖管理 --- Boost 库/C++11 标准库。
  • 可移植性:Posix/Windows、32/64bit、Intel/ARM。
  • 线程安全。

(2)WebSocketpp 同时支持 HTTP 和 Websocket 两种网络协议, 比较适用于我们本次的项目, 所以我们选用该库作为项目的依赖库用来搭建 HTTP 和WebSocket 服务器。下面是该项目的一些常用网站:

(3)Websocket 协议介绍:

  • WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的 消息推送机制。
  • 传统的 web 程序都是属于 "一问一答" 的形式,即客户端给服务器发送了一个HTTP 请求,服务器给客户端返回一个 HTTP 响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应
  • 像网页即时聊天或者我们做的五子棋游戏这样的程序都是非常依赖 "消息推送" 的,即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送一般需要通过 "轮询" 的方式实现, 而轮询的成本比较高并且也不能及时的获取到消息的响应。
  • 基于上述两个问题, 就产生了 WebSocket 协议。WebSocket 更接近于 TCP 这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。

(4)原理解析:

  • WebSocket 协议本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程并升级协议的过程。
  • 具体协议升级的过程如下:
  • 报文格式:


报文字段比较多,我们重点关注这几个字段:

  • FIN:WebSocket 传输数据以消息为概念单位,一个消息有可能由一个或多个帧组成,FIN 字段为 1 表示末尾帧。
  • RSV1~3:保留字段,只在扩展时使用,若未启用扩展则应置 1,若收到不全为 0的数据帧,且未协商扩展则立即终止连接。
  • opcode : 标志当前数据帧的类型。
    • 0x0: 表示这是个延续帧,当 opcode 为 0 表示本次数据传输采用了数据分片,当前收到的帧为其中一个分片:
    • 0x1: 表示这是文本帧。
    • 0x2: 表示这是二进制帧。
    • 0x3-0x7: 保留,暂未使用。
    • 0x8: 表示连接断开。
    • 0x9: 表示 ping 帧。
    • 0xa: 表示 pong 帧。
    • 0xb-0xf: 保留,暂未使用。
  • mask:表示 Payload 数据是否被编码,若为 1 则必有 Mask-Key,用于解码Payload 数据。仅客户端发送给服务端的消息需要设置。
  • Payload length:数据载荷的长度,单位是字节, 有可能为 7 位、7+16 位、7+64位。假设 Payload length = x
    • x 为 0~126:数据的长度为 x 字节。
    • x 为 126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度。
    • x 为 127:后续 8 个字节代表一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。
  • Mask-Key:当 mask 为 1 时存在,长度为 4 字节,解码规则: DECODED[i] = ENCODED[i] ^ MASK[i % 4]。
  • Payload data: 报文携带的载荷数据。

10.2 websocketpp的安装

(1)安装如下:

cpp 复制代码
sudo apt-get install libboost-dev libboost-system-dev libwebsocketpp-dev

(2)安装完毕后,若在 /usr/include 下有了 websocketpp 目录就表示安装成功了。

10.3 websocketpp的使用

(1)main.cpp:

cpp 复制代码
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include <iostream>

// 定义server类型
typedef websocketpp::server<websocketpp::config::asio> server_t;

void onOpen(websocketpp::connection_hdl hdl)
{
    std::cout << "websocket长连接建立成功!\n";
}

void onClose(websocketpp::connection_hdl hdl) 
{
    std::cout << "websocket长连接断开!\n";
}

void onMessage(server_t *server, websocketpp::connection_hdl hdl, server_t::message_ptr msg)
{
    //1. 获取有效消息载荷数据,进行业务处理
    std::string body = msg->get_payload();
    std::cout << "收到消息:" << body << std::endl;
    //2. 对客户端进行响应
    //获取通信连接
    auto conn = server->get_con_from_hdl(hdl);

    //发送数据
    conn->send(body + "-Hello!", websocketpp::frame::opcode::value::text);
}

int main()
{
    // 1. 实例化服务器对象
    server_t server;
    // 2. 初始化日志输出 --- 关闭日志输出
    server.set_access_channels(websocketpp::log::alevel::none);
    // 3. 初始化asio框架
    server.init_asio();
    // 4. 设置消息处理/连接握手成功/连接关闭回调函数
    server.set_open_handler(onOpen);
    server.set_close_handler(onClose);
    auto msg_hadler = std::bind(onMessage, &server, std::placeholders::_1, std::placeholders::_2);
    server.set_message_handler(msg_hadler);
    // 5. 启用地址重用
    server.set_reuse_addr(true);
    // 5. 设置监听端口
    server.listen(9090);
    // 6. 开始监听
    server.start_accept();
    // 7. 启动服务器
    server.run();
    return 0;
}

(2)运行结果:


(3)服务端结果:

11. redis的安装与使用

11.1 redis的介绍

Redis(Remote Dictionary Server)是一个开源的高性能键值对(key-value)数据库。它通常用作数据结构服务器,因为除了基本的键值存储功能外,Redis 还支持多种类型的数据结构,如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)以及范围查询、位图、超日志和地理空间索引等。以下是 Redis 的一些主要特性:

  1. 内存中数据库:Redis 将所有数据存储在内存中,这使得读写速度非常快。
  2. 持久化:尽管 Redis 是内存数据库,但它提供了持久化选项,可以将内存中的数据保存到磁盘上,以防系统故障导致数据丢失。
  3. 支持多种数据结构:Redis 不仅支持基本的键值对,还支持列表、集合、有序集合等复杂的数据结构。
  4. 原子操作:Redis 支持原子操作,这意味着多个操作可以作为一个单独的原子步骤执行,这对于并发控制非常重要。
  5. 发布/订阅功能:Redis 支持发布订阅模式,允许多个客户端订阅消息,当消息发布时,所有订阅者都会收到消息。
  6. 高可用性:通过 Redis 哨兵(Sentinel)和 Redis 集群,Redis 可以提供高可用性和自动故障转移。
  7. 复制:Redis 支持主从复制,可以提高数据的可用性和读写性能。
  8. 事务:Redis 提供了事务功能,可以保证一系列操作的原子性执行。
  9. Lua 脚本:Redis 支持使用 Lua 脚本进行复杂的数据处理,可以在服务器端执行复杂的逻辑。
  10. 客户端库:Redis 拥有丰富的客户端库,支持多种编程语言,如 Python、Ruby、Java、C# 等。
  11. 性能监控:Redis 提供了多种监控工具和命令,可以帮助开发者监控和优化性能。
  12. 易于使用:Redis 有一个简单的配置文件和命令行界面,使得设置和使用变得容易。Redis 广泛用于缓存、会话存储、消息队列、排行榜、实时分析等领域。由于其高性能和灵活性,Redis 成为了现代应用程序中非常流行的数据存储解决方案之一。

11.2 redis的安装

(1)使用 apt 安装:

powershell 复制代码
Shell
apt install redis -y

(2)支持远程连接:

  • 修改 /etc/redis/redis.conf
    • 修改 bind 127.0.0.1 为 bind 0.0.0.0
    • 修改 protected-mode yes 改为 protected-mode no
powershell 复制代码
Plain Text
# By default, if no "bind" configuration directive is specified, Redis listens
# for connections from all the network interfaces available on the server.
# It is possible to listen to just one or multiple selected interfaces using
# the "bind" configuration directive, followed by one or more IP addresses.
# 
# Examples:
# 
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
# 
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 loopback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
# 
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# bind 127.0.0.1 # 注释掉这行
bind 0.0.0.0 # 添加这行
protected-mode no # 把 yes 改成 no

(3)Redis的相关服务:

  • 启动Redis服务
powershell 复制代码
Plain Text
service redis-server start
  • 停止Redis服务
powershell 复制代码
Plain Text
service redis-server stop
  • 重启Redis服务
powershell 复制代码
Plain Text
service redis-server restart

(4)安装 hiredis:

  • C++ 操作 redis 的库有很多。本项目使用 redis-plus-plus这个库的功能强大,使用简单。Github 地址:https://github.com/sewenew/redis-plus-plus
  • redis-plus-plus是基于 hiredis 实现的。
  • hiredis 是一个 C 语言实现的 redis 客户端。
  • 因此需要先安装 hiredis,直接使用包管理器安装即可。
powershell 复制代码
Plain Text
apt install libhiredis-dev

(5)安装redis-plus-plus:

powershell 复制代码
Bash
# 下载 redis-plus-plus 源码
git clone https://github.com/sewenew/redis-plus-plus.git
# 编译/安装 redis-plus-plus
# 进入redis-plus-plus目录
cd redis-plus-plus
# 创建build并进入
mkdir build && cd build
# cmake进行构建
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
# 编译并安装
make && sudo make install

构建成功后, 会在 /usr/include/ 中多出 sw 目录,并且内部包含 redis-plus-plus的一系列头文件。

11.3 redis的部分接口介绍

(1)redis 本身支持很多数据类型的键值对,但是在聊天室项目中只涉及到了字符串键值对的操作,因此这里主要介绍字符串键值对的基础操作。

cpp 复制代码
namespace sw
{
    namespace redis
    {
        struct ConnectionOptions
        {
            std::string host;
            int port = 6379;

            std::string path;
            std::string user = "default";
            std::string password;
            int db = 0; // 默认 0 号库
            bool keep_alive = false;
        };
        
        struct ConnectionPoolOptions
        {
            std::size_t size = 1; // 最大连接数量
        };
        
        class Redis
        {
            // uri e.g 'tcp://127.0.0.1:6379'
            explicit Redis(const std::string &uri);
            explicit Redis(const ConnectionOptions &connection_opts,
                        const ConnectionPoolOptions &pool_opts = {});

            // 删除当前库中所有数据
            void flushdb(bool async = false);
            // 删除指定键值对
            long long del(const StringView &key);
            // 判断指定键值对是否存在
            long long exists(const StringView &key);
            // 获取一个 string 键值对
            OptionalString get(const StringView &key);
            // 存放一个 string 键值对,且设置过期时间-毫秒
            bool set(const StringView &key,
                     const StringView &val,
                     const std::chrono::milliseconds &ttl = std::chrono::milliseconds(0), // 0 表示不设置超时
                     UpdateType type = UpdateType::ALWAYS);

            void setex(const StringView &key,
                       long long ttl,
                       const StringView &val);

            // 向一个列表中尾插/头插 string 键值对
            long long rpush(const StringView &key, const StringView &val);
            long long lpush(const StringView &key, const StringView &val);
            long long rpush(const StringView &key, Input first, Input last);

            // std::vector<std::string> elements;
            // redis.lrange("list", 0, -1, std::back_inserter(elements));
            void lrange(const StringView &key, long long start, long long stop, Output output);
        };
    }
}

11.4 redis的使用

(1)这里只进行字符串键值对的增删改查操作以及数据的生命周期设置:

cpp 复制代码
#include <iostream>
#include <sw/redis++/redis.h>
#include <gflags/gflags.h>
#include <thread>

DEFINE_string(ip, "127.0.0.1", "这是服务器的IP地址,格式:127.0.0.1");
DEFINE_int32(port, 6379, "这是服务器的端口, 格式: 8080");
DEFINE_int32(db, 0, "库的编号:默认0号");
DEFINE_bool(keep_alive, true, "是否进行长连接保活");

void print(sw::redis::Redis &client) 
{
    auto user1 = client.get("会话ID1");
    if (user1) std::cout << *user1 << std::endl;
    auto user2 = client.get("会话ID2");
    if (user2) std::cout << *user2 << std::endl;
    auto user3 = client.get("会话ID3");
    if (user3) std::cout << *user3 << std::endl;
    auto user4 = client.get("会话ID4");
    if (user4) std::cout << *user4 << std::endl;
    auto user5 = client.get("会话ID5");
    if (user5) std::cout << *user5 << std::endl;
}

void add_string(sw::redis::Redis &client)
{
    client.set("会话ID1", "用户ID1");
    client.set("会话ID2", "用户ID2");
    client.set("会话ID3", "用户ID3");
    client.set("会话ID4", "用户ID4");
    client.set("会话ID5", "用户ID5");

    client.del("会话ID3");

    client.set("会话ID5", "用户ID555");  //数据已存在则进行修改,不存在则新增

    print(client);
}

void expired_test(sw::redis::Redis &client)
{
    //这次的新增,数据其实已经有了,因此本次是修改
    //不仅仅修改了val,而且还给键值对新增了过期时间
    client.set("会话ID1", "用户ID1111", std::chrono::milliseconds(1000));

    print(client);
    std::cout << "------------休眠2s-----------\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    print(client);
}

void list_test(sw::redis::Redis &client)
{
    client.rpush("群聊1", "成员1");
    client.rpush("群聊1", "成员2");
    client.rpush("群聊1", "成员3");
    client.rpush("群聊1", "成员4");
    client.rpush("群聊1", "成员5");

    std::vector<std::string> users;
    client.lrange("群聊1", 0, -1, std::back_inserter(users));

    for(auto user : users) 
    {
        std::cout << user << std::endl;
    }
}

int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);
    //功能接口演示中:
    //1. 构造连接选项,实例化Redis对象,连接服务器
    sw::redis::ConnectionOptions opts;
    opts.host = FLAGS_ip;
    opts.port = FLAGS_port;
    opts.db = FLAGS_db;
    opts.keep_alive = FLAGS_keep_alive;
    sw::redis::Redis client(opts);
    //2. 添加字符串键值对,删除字符串键值对,获取字符串键值对
    add_string(client);

    //3. 实践控制数据有效时间的操作
    expired_test(client);

    //4. 列表的操作,主要实现数据的右插,左获取
    std::cout << "--------------------------\n";
    list_test(client);

    return 0;
}

(2)运行结果:

(3)Makefile编译:

cpp 复制代码
main:main.cpp
	g++ -std=c++17 $^ -o $@ -lhiredis -lredis++ -lgflags
.PHONY:clean
clean:
	rm -rf main

12. ODB安装与使用

12.1 ODB的介绍

(1)ODB是一种面向对象的数据库管理系统,它支持面向对象的编程语言,允许用户通过面向对象的语言和工具来操作数据库。主要特性:

  • 面向对象:ODB使用对象来存储和管理数据,这些对象具有属性、方法和事件等特性,使得数据的表示和管理更加直观和灵活。
  • 可扩展性:ODB可以通过添加新的类和对象来实现系统的扩展,以适应不断变化的需求。
  • 交互性:ODB支持事件驱动机制,使得应用程序可以与数据库进行实时交互。
  • 平台独立性:ODB可以运行在多种平台上,如Windows、Linux、Mac OS等,具有广泛的平台兼容性。
  • 多语言支持:ODB可以支持多种面向对象的编程语言,如Java、C++、Python等。

(2)主要作用:

  • 存储和管理数据:ODB的主要作用是存储和管理面向对象数据,提供数据访问、查询、更新等操作的功能。
  • 确保数据一致性:ODB提供了事务管理和并发控制机制,确保在多用户环境下数据的一致性和完整性。
  • 支持数据完整性:ODB支持数据完整性的定义和检查,如约束、触发器等,以确保数据的准确性。
  • 保证数据持久性:ODB将数据存储在内存和磁盘上,即使在系统崩溃或电源中断的情况下,也能保证数据的完整性。

(3)应用领域:

  • 物联网:ODB可以用于存储物联网设备的实时数据,如传感器数据、位置信息等,实现智能决策和优化控制。
  • 大规模数据管理:与传统的关系型数据库相比,ODB采用面向对象的数据模型,能够更好地适应复杂的数据结构和关系。在处理大规模数据时,ODB可以提供更高的性能和可伸缩性。
  • 嵌入式系统:ODB通过提供高效的数据存储和访问方式,帮助嵌入式系统实时地处理数据,并减少对存储和计算资源的需求。
  • 软件开发:对于基于对象的软件开发,ODB提供了更加高效和灵活的数据管理方式,简化了数据访问的过程,减少了开发工作的重复性和复杂度。

12.2 ODB的安装

(1)安装 build2:

  • 因为 build2 安装时,有可能会版本更新,从 16 变成 17,或从 17 变 18,因此注意,先从 build2 官网查看安装步骤:https://build2.org/install.xhtml#unix
powershell 复制代码
PowerShell
[xiaomaker@alibaba build2]$ curl -sSfO https://download.build2.org/0.17.0/build2-install-0.17.0.sh
[xiaomaker@alibaba build2]$ sh build2-install-0.17.0.sh
  • 安装中因为网络问题,超时失败,解决:将超时时间设置的更长一些
powershell 复制代码
Shell
[xiaomaker@alibaba build2]$ sh build2-install-0.17.0.sh --timeout 1800

(2)安装odb-compiler:

powershell 复制代码
[xiaomaker@alibaba ~]$ #注意这里的 gcc-11 需要根据你自己版本而定
[xiaomaker@alibaba ~]$ sudo apt-get install gcc-11-plugin-dev
[xiaomaker@alibaba ~]$ mkdir odb-build && cd odb-build
[xiaomaker@alibaba odb-build]$ bpkg create -d odb-gcc-N cc \
 config.cxx=g++ \
 config.cc.coptions=-O3 \
 config.bin.rpath=/usr/lib \
 config.install.root=/usr/ \
 config.install.sudo=sudo
[xiaomaker@alibaba odb-build]$ cd odb-gcc-N
[xiaomaker@alibaba odb-gcc-N]$ bpkg build odb@https://pkg.cppget.org/1/beta
[xiaomaker@alibaba odb-gcc-N]$ bpkg test odb   # 输出以下内容
test odb-2.5.0-b.25+1/tests/testscript{testscript}
tested odb/2.5.0-b.25+1
[xiaomaker@alibaba odb-gcc-N]$ bpkg install odb
[xiaomaker@alibaba odb-gcc-N]$ odb --version
bash: /usr/bin/odb: No such file or directory
#如果报错了,找不到 odb,那就在执行下边的命令
[xiaomaker@alibaba odb-gcc-N]$ sudo echo 'export PATH=${PATH}:/usr/local/bin' >> ~/.bashrc 
[xiaomaker@alibaba odb-gcc-N]$ export PATH=${PATH}:/usr/local/bin
[xiaomaker@alibaba odb-gcc-N]$ odb --version
ODB object-relational mapping (ORM) compiler for C++ 2.5.0-b.25
Copyright (c) 2009-2023 Code Synthesis Tools CC.
This is free software; see the source for copying conditions. 
There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR 
PURPOSE.

(3)安装 ODB 运行时库:

powershell 复制代码
[xiaomaker@alibaba odb-gcc-N]$ cd ..
[xiaomaker@alibaba odb-build]$ bpkg create -d libodb-gcc-N cc \
 config.cxx=g++ \
 config.cc.coptions=-O3 \
 config.install.root=/usr/ \
 config.install.sudo=sudo
[xiaomaker@alibaba odb-build]$ cd libodb-gcc-N
[xiaomaker@alibaba libodb-gcc-N]$ bpkg add https://pkg.cppget.org/1/beta
[xiaomaker@alibaba libodb-gcc-N]$ bpkg fetch
[xiaomaker@alibaba libodb-gcc-N]$ bpkg build libodb
[xiaomaker@alibaba libodb-gcc-N]$ bpkg build libodb-mysql

(4)安装 mysql 和客户端开发包:

powershell 复制代码
# 安装 mysql:
sudo apt install mysql-server
sudo apt install -y libmysqlclient-dev

# 配置 mysql:
sudo vim /etc/my.cnf 或者 /etc/mysql/my.cnf # 有哪个修改哪个就行
# 添加以下内容
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
character-set-server=utf8
bind-address = 0.0.0.0

# 修改 root 用户密码:
dev@bite:~$ sudo cat /etc/mysql/debian.cnf
# Automatically generated for Debian scripts. DO NOT TOUCH!
[client]
host = localhost
user = debian-sys-maint
password = UWcn9vY0NkrbJMRC
socket = /var/run/mysqld/mysqld.sock
[mysql_upgrade]
host = localhost
user = debian-sys-maint
password = UWcn9vY0NkrbJMRC
socket = /var/run/mysqld/mysqld.sock
dev@bite:~$ sudo mysql -u debian-sys-maint -p
Enter password: #这里输入上边第 6 行看到的密码
mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH
mysql_native_password BY 'xxxxxx';
Query OK, 0 rows affected (0.01 sec)
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.01 sec)
mysql> quit

# 重启 mysql,并设置开机启动:
sudo systemctl restart mysql
sudo systemctl enable mysql

(5)安装 boost profile 库:

powershell 复制代码
[xiaomaker@alibaba libodb-gcc-N]$ bpkg install --all --recursive

(6)总体卸载:

powershell 复制代码
[xiaomaker@alibaba libodb-gcc-N]$ bpkg uninstall --all --recursive

(7)总体升级:

powershell 复制代码
[xiaomaker@alibaba libodb-gcc-N]$ bpkg fetch
[xiaomaker@alibaba libodb-gcc-N]$ bpkg status
[xiaomaker@alibaba libodb-gcc-N]$ bpkg uninstall --all --recursive
[xiaomaker@alibaba libodb-gcc-N]$ bpkg build --upgrade --recursive
[xiaomaker@alibaba libodb-gcc-N]$ bpkg install --all --recursive

13. RabbitMq的安装和使用

13.1 RabbitMq的介绍

(1)核心特性:

  • 可靠性:RabbitMQ基于AMQP协议,提供了持久化、可靠的消息传递机制。它确保消息能够在发送和接收之间进行可靠地传输,即使在出现故障的情况下也能保证消息的安全性。RabbitMQ使用消息确认机制(acknowledgment),确保消息被消费者正确接收并处理。此外,RabbitMQ还提供了镜像队列(mirrored queues)功能,可以在集群中的多个节点上复制队列,以确保消息的持久化和可靠性。
  • 灵活性:RabbitMQ支持多种消息传递模式,包括点对点、发布/订阅、请求/响应等。它允许开发人员根据应用程序的需求来选择合适的消息模式,实现灵活的消息传递。
  • 可扩展性:RabbitMQ通过使用可扩展的消息队列和集群功能,能够轻松地处理大量的消息传递。它支持水平扩展,可以在需要时添加更多的节点来处理更多的消息。此外,RabbitMQ还支持集群和负载均衡,确保在高并发或故障场景下服务的可用性。

(2)核心概念:

  • 队列(Queue):RabbitMQ中使用队列来存储和转发消息。每个队列都是一个消息缓冲区,用于在消息生产者和消费者之间进行异步通信。队列具有先进先出(FIFO)的特性,即先进入队列的消息会先被消费者取出。此外,队列还可以设置其他属性,如是否独占、是否自动删除等。
  • 交换机(Exchange):交换机是RabbitMQ中实现消息路由的核心组件。生产者将消息发送到交换机,交换机根据路由键和绑定规则将消息分发到不同的队列中。RabbitMQ提供了多种类型的交换机,包括直接交换机(Direct)、扇出交换机(Fanout)、主题交换机(Topic)和消息头交换机(Headers)等。
  • 路由键(Routing Key):路由键是交换机在路由消息时使用的关键字。生产者发送消息时可以指定路由键,交换机根据路由键和绑定规则将消息分发到相应的队列中。
  • 绑定(Binding):绑定是将交换机和队列按照路由规则进行关联的过程。通过绑定,交换机可以将消息路由到指定的队列中。绑定关系可以是一对一、一对多或多对多的。
  • 生产者(Producer):生产者是消息的发送方,它将消息发送到RabbitMQ的交换机中。生产者可以指定消息的路由键和交换机的名称,以控制消息的路由和分发。
  • 消费者(Consumer):消费者是消息的接收方,它从RabbitMQ的队列中获取并处理消息。消费者可以监听一个或多个队列,当队列中有新消息时,消费者会将其取出并处理。处理完成后,消费者可以选择发送确认消息给RabbitMQ服务器,以表示消息已被成功处理。

13.2 RabbitMq的安装

(1)安装RabbitMq:

powershell 复制代码
sudo apt install rabbitmq-server

# 启动服务
sudo systemctl start rabbitmq-server.service
# 查看服务状态
sudo systemctl status rabbitmq-server.service
# 安装完成的时候默认有个用户 guest ,但是权限不够,要创建一个
administrator 用户,才可以做为远程登录和发表订阅消息:
 #添加用户
sudo rabbitmqctl add_user root 123456
 #设置用户 tag
sudo rabbitmqctl set_user_tags root administrator
 #设置用户权限
sudo rabbitmqctl set_permissions -p / root "." "." ".*"
# RabbitMQ 自带了 web 管理界面,执行下面命令开启
sudo rabbitmq-plugins enable rabbitmq_management
  • 查看 rabbitmq-server 的状态:
  • 访问 webUI 界面,默认端口为 15672:
  • 取消rabbitmq-server开机自启状态如下命令:
powershell 复制代码
sudo systemctl disable rabbitmq-server

(2)安装 RabbitMQ 的 C++客户端库:

我们这里使用 AMQP-CPP 库来编写客户端程序。

  • 安装 AMQP-CPP:
powershell 复制代码
sudo apt install libev-dev # libev 网络库组件
git clone https://github.com/CopernicaMarketingSoftware/AMQP-CPP.git
cd AMQP-CPP/
make 
make install

13.3 AMQP-CPP 库的简单使用

(1)publish.cpp:

cpp 复制代码
#include <ev.h>
#include <amqpcpp.h>
#include <amqpcpp/libev.h>
#include <openssl/ssl.h>
#include <openssl/opensslv.h>

int main()
{
    //1. 实例化底层网络通信框架的I/O事件监控句柄
    auto *loop = EV_DEFAULT;
    //2. 实例化libEvHandler句柄 --- 将AMQP框架与事件监控关联起来
    AMQP::LibEvHandler handler(loop);
    //2.5. 实例化连接对象
    AMQP::Address address("amqp://root:123456@127.0.0.1:5672/");
    AMQP::TcpConnection connection(&handler, address);

    //3. 实例化信道对象
    AMQP::TcpChannel channel(&connection);
    //4. 声明交换机
    AMQP::Deferred &deferred = channel.declareExchange("test-exchange", AMQP::ExchangeType::direct); 
    
    deferred.onError([](const char *message)
    {
        std::cout << "声明交换机失败:" << message << std::endl;
        exit(0);
    });

    deferred.onSuccess([]()
    {
        std::cout << "test-exchange 交换机创建成功!" << std::endl;
    });

    //5. 声明队列
    AMQP::DeferredQueue &deferredQueue = channel.declareQueue("test-queue");

    deferredQueue.onError([](const char *message)
    {
        std::cout << "声明队列失败:" << message << std::endl;
        exit(0);
    });

    deferredQueue.onSuccess([]()
    {
        std::cout << "test-queue 队列创建成功!" << std::endl;
    });


    //6. 针对交换机和队列进行绑定
    auto &binding_deferred = channel.bindQueue("test-exchange", "test-queue", "test-queue-key");

    binding_deferred.onError([](const char *message) 
    {
        std::cout << "test-exchange - test-queue 绑定失败:" << message << std::endl;
        exit(0);
    });
       
    binding_deferred.onSuccess([]()
    {
        std::cout << "test-exchange - test-queue 绑定成功!" << std::endl;
    });

    //7. 向交换机发布消息
    for(int i = 0; i < 10; i++) 
    {
        std::string msg = "Hello World-" + std::to_string(i);
        bool ret = channel.publish("test-exchange", "test-queue-key", msg);
        if(ret == false) 
        {
            std::cout << "publish 失败!\n";
        }
    }

    //启动底层网络通信框架--开启I/O
    ev_run(loop, 0);
    return 0;
}

(2)consume.cpp:

cpp 复制代码
#include <ev.h>
#include <amqpcpp.h>
#include <amqpcpp/libev.h>
#include <openssl/ssl.h>
#include <openssl/opensslv.h>

// 消息回调处理函数的实现
void MessageCb(AMQP::TcpChannel *channel, const AMQP::Message &message, uint64_t deliveryTag, bool redelivered)
{
    std::string msg;
    msg.assign(message.body(), message.bodySize());
    std::cout << msg << std::endl;
    channel->ack(deliveryTag); // 对消息进行确认
}

int main()
{
    // 1. 实例化底层网络通信框架的I/O事件监控句柄
    auto *loop = EV_DEFAULT;
    // 2. 实例化libEvHandler句柄 --- 将AMQP框架与事件监控关联起来
    AMQP::LibEvHandler handler(loop);
    // 2.5. 实例化连接对象
    AMQP::Address address("amqp://root:123456@127.0.0.1:5672/");
    AMQP::TcpConnection connection(&handler, address);

    // 3. 实例化信道对象
    AMQP::TcpChannel channel(&connection);
    
    // 4. 声明交换机
    AMQP::Deferred &deferred = channel.declareExchange("test-exchange", AMQP::ExchangeType::direct);
    deferred.onError([](const char *message)
    {
        std::cout << "声明交换机失败:" << message << std::endl;
        exit(0); 
    });

    deferred.onSuccess([]()
    { 
        std::cout << "test-exchange 交换机创建成功!" << std::endl; 
    });

    // 5. 声明队列
    AMQP::DeferredQueue &deferredQueue = channel.declareQueue("test-queue");
    deferredQueue.onError([](const char *message)
    {
        std::cout << "声明队列失败:" << message << std::endl;
        exit(0); 
    });

    deferredQueue.onSuccess([]() 
    { 
        std::cout << "test-queue 队列创建成功!" << std::endl; 
    });

    // 6. 针对交换机和队列进行绑定
    auto &binding_deferred = channel.bindQueue("test-exchange", "test-queue", "test-queue-key");
    binding_deferred.onError([](const char *message)
    {
        std::cout << "test-exchange - test-queue 绑定失败:" << message << std::endl;
        exit(0); 
    });

    binding_deferred.onSuccess([]()
    { 
        std::cout << "test-exchange - test-queue 绑定成功!" << std::endl; 
    });
    
    // 7. 订阅队列消息 -- 设置消息处理回调函数
    auto callback = std::bind(MessageCb, &channel, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
    channel.consume("test-queue", "consume-tag") // 返回值 DeferredConsumer
    .onReceived(callback)
    .onError([](const char *message)
    {
        std::cout << "订阅 test-queue 队列消息失败:" << message << std::endl;
        exit(0); 
    }); // 返回值是 AMQP::Deferred

    // 8. 启动底层网络通信框架--开启I/O
    ev_run(loop, 0);
    return 0;
}

(3)运行结果:

13.4 RabbitMQ的二次封装

(1)在项目中使用 rabbitmq 的时候,我们目前只需要交换机与队列的直接交换,实现一台主机将消息发布给另一台主机进行处理的功能,因此在这里可以对 mq 的操作进行简单的封装,使 mq 的操作在项目中更加简便。封装一个 MQClient:

  • 提供声明指定交换机与队列,并进行绑定的功能。
  • 提供向指定交换机发布消息的功能。
  • 提供订阅指定队列消息,并设置回调函数进行消息消费处理的功能。

(2)具体实现:

cpp 复制代码
#pragma once
#include <ev.h>
#include <amqpcpp.h>
#include <amqpcpp/libev.h>
#include <openssl/ssl.h>
#include <openssl/opensslv.h>
#include <iostream>
#include <functional>
#include "logger.hpp"

namespace Test
{
    class MQClient
    {
    public:
        using MessageCallback = std::function<void(const char*, size_t)>;
        using ptr = std::shared_ptr<MQClient>;

        MQClient(const std::string &user, const std::string &passwd, const std::string &host)
        {
            _loop = EV_DEFAULT;
            _handler = std::make_unique<AMQP::LibEvHandler>(_loop);
            std::string url = "amqp://" + user + ":" + passwd + "@" + host + "/";
            AMQP::Address address(url);
            _connection = std::make_unique<AMQP::TcpConnection>(_handler.get(), address);
            _channel = std::make_unique<AMQP::TcpChannel>(_connection.get());

            _loop_thread = std::thread([this]() 
            {
                ev_run(_loop, 0);
            });
        }

        void declareComponents(const std::string &exchange,
            const std::string &queue,
            const std::string &routing_key = "routing_key",
            AMQP::ExchangeType echange_type = AMQP::ExchangeType::direct)
        {
            AMQP::Deferred &deferred = _channel->declareExchange(exchange, echange_type);

            deferred.onError([](const char *message)
            {
                LOG_ERROR("声明交换机失败:{}", message);
                exit(0); 
            });

            deferred.onSuccess([exchange]()
            { 
                LOG_ERROR("{} 交换机创建成功!", exchange);
            });

            // 5. 声明队列
            AMQP::DeferredQueue &deferredQueue = _channel->declareQueue(queue);

            deferredQueue.onError([](const char *message)
            {
                LOG_ERROR("声明队列失败:{}", message);
                exit(0); 
            });

            deferredQueue.onSuccess([queue]()
            { 
                LOG_ERROR("{} 队列创建成功!", queue);
            });

            // 6. 针对交换机和队列进行绑定
            auto &binding_deferred = _channel->bindQueue(exchange, queue, routing_key);

            binding_deferred.onError([exchange, queue](const char *message)
            {
                LOG_ERROR("{} - {} 绑定失败:", exchange, queue);
                exit(0); 
            });

            binding_deferred.onSuccess([exchange, queue, routing_key]()
            { 
                LOG_ERROR("{} - {} - {} 绑定成功!", exchange, queue, routing_key);
            });
        }

        bool publish(const std::string &exchange, 
            const std::string &msg, 
            const std::string &routing_key = "routing_key")
        {
            LOG_DEBUG("向交换机 {}-{} 发布消息!", exchange, routing_key);
            bool ret = _channel->publish(exchange, routing_key, msg);
            if(ret == false) 
            {
                LOG_ERROR("{} 发布消息失败:", exchange);
                return false;
            }

            return true;
        }

        void consume(const std::string &queue, const MessageCallback &cb) 
        {
            LOG_DEBUG("开始订阅 {} 队列消息!", queue);
            auto &Consume = _channel->consume(queue, "consume-tag"); // 返回值 DeferredConsumer
            Consume.onReceived([this, cb](const AMQP::Message &message,
                                          uint64_t deliveryTag,
                                          bool redelivered)
            {
                cb(message.body(), message.bodySize());
                _channel->ack(deliveryTag);
            });

            Consume.onError([queue](const char *message)
            {
                LOG_ERROR("订阅 {} 队列消息失败: {}", queue, message);
                exit(0); 
            });
        }

        ~MQClient() 
        {
            ev_async_init(&_async_watcher, watcher_callback);
            ev_async_start(_loop, &_async_watcher);
            ev_async_send(_loop, &_async_watcher);
            _loop_thread.join();
            _loop = nullptr;
        }

    private:
        static void watcher_callback(struct ev_loop *loop, ev_async *watcher, int32_t revents) 
        {
            ev_break(loop, EVBREAK_ALL);
        }

    private:
        struct ev_async _async_watcher;
        struct ev_loop *_loop;
        std::unique_ptr<AMQP::LibEvHandler> _handler;
        std::unique_ptr<AMQP::TcpConnection> _connection;
        std::unique_ptr<AMQP::TcpChannel> _channel;
        std::thread _loop_thread;
    };
}

14. 短信验证码SDK

15.1 短信验证码的安装

(1)首先注册百度云后点击如下操作:


(2)点击免费开通:

(3)扫码领取:

(4)进行一系列的信息验证:

(5)身份验证:


(6)获取API发送短信:


(7)获取密钥:

(8)安装环境:

powershell 复制代码
# 安装依赖
sudo apt-get install libcurl4-openssl-dev libssl-dev uuid-dev libjsoncpp-dev
# 克隆代码
git clone https://github.com/aliyun/aliyun-openapi-cpp-sdk.git
# 进入目录
cd aliyun-openapi-cpp-sdk
# 一键编译安装
sudo sh easyinstall.sh core

14.2 接口的使用

(1)test.cpp:

cpp 复制代码
#include <cstdlib>
#include <iostream>
#include <alibabacloud/core/AlibabaCloud.h>
#include <alibabacloud/core/CommonRequest.h>
#include <alibabacloud/core/CommonClient.h>
#include <alibabacloud/core/CommonResponse.h>

using namespace std;
using namespace AlibabaCloud;

int main(int argc, char **argv)
{
    AlibabaCloud::InitializeSdk();
    AlibabaCloud::ClientConfiguration configuration("cn-shanghai");
    // specify timeout when create client.
    configuration.setConnectTimeout(1500);
    configuration.setReadTimeout(4000);
    std::string access_key = "***********";
    std::string access_key_secret = "***********";
    AlibabaCloud::Credentials credential(access_key, access_key_secret);
    /* use STS Token
    credential.setSessionToken( getenv("ALIBABA_CLOUD_SECURITY_TOKEN") );
    */
    AlibabaCloud::CommonClient client(credential, configuration);
    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", "微服务");
    request.setQueryParameter("TemplateCode", "SMS_475320595");
    request.setQueryParameter("PhoneNumbers", "***********");
    request.setQueryParameter("TemplateParam", "{\"code\":\"1234\"}");

    auto response = client.commonResponse(request);
    if (response.isSuccess())
    {
        printf("request success.\n");
        printf("result: %s\n", response.result().payload().c_str());
    }
    else
    {
        printf("error: %s\n", response.error().errorMessage().c_str());
        printf("request id: %s\n", response.error().requestId().c_str());
    }

    AlibabaCloud::ShutdownSdk();
    return 0;
}

(2)运行结果:

14.3 短信接口的封装

(1)在项目中,我们实际上并不关心如何调用阿里云的 SDK,我只是期望有一个接口能够设定手机号和验证码之后,调用就进行发送,因此我们可以将 SDK 中使用到的接口和对象进行二次封装,简化项目中的使用:

  • 将 SDK 中的 client 对象管理起来。
  • 向外提供一个向指定手机号发送指定短信的接口。

(2)具体实现:

cpp 复制代码
#pragma once
#include <cstdlib>
#include <iostream>
#include <memory>
#include <alibabacloud/core/AlibabaCloud.h>
#include <alibabacloud/core/CommonRequest.h>
#include <alibabacloud/core/CommonClient.h>
#include <alibabacloud/core/CommonResponse.h>
#include "logger.hpp"

namespace Test
{
    class DMSClient 
    {
    public:
        using ptr = std::shared_ptr<DMSClient>;
        DMSClient(const std::string &access_key_id,
                  const std::string &access_key_secret)
        {
            AlibabaCloud::InitializeSdk();
            AlibabaCloud::ClientConfiguration configuration("cn-shanghai");
            configuration.setConnectTimeout(1500);
            configuration.setReadTimeout(4000);
            AlibabaCloud::Credentials credential(access_key_id, access_key_secret);
            _client = std::make_unique<AlibabaCloud::CommonClient>(credential, configuration);
        }

        bool send(const std::string &phone, const std::string &code)
        {
            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", "微服务");
            request.setQueryParameter("TemplateCode", "SMS_475320595");
            request.setQueryParameter("PhoneNumbers", phone);
            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;
        }

        ~DMSClient() 
        { 
            AlibabaCloud::ShutdownSdk(); 
        }

    private:
        std::unique_ptr<AlibabaCloud::CommonClient> _client;
    };
}

15. 语音识别SDK

15.1 语音识别的安装

(1)首先注册百度云后点击如下操作:

(2)下拉点击如下操作:

(3)点击去领取:


(4)点击领取:


(5)领取完成创建应用:


(6)创建完成:


(7)下载SDK:



(8)安装 sdk 所需依赖:

powershell 复制代码
Shell
# 安装 jsoncpp
sudo apt install libjsoncpp-dev
# 安装 libcurl
sudo apt install curl
# 安装 openssl
# ubuntu 22.04 默认安装了

(9)将上述下载的SDK下载解压即可。

15.2 语音识别的简单使用

(1)代码实现:

cpp 复制代码
#include "../../third/include/aip-cpp-sdk/speech.h"

void asr(aip::Speech &client)
{
    std::string file_content;
    aip::get_file_content("16k.pcm", &file_content);

    Json::Value result = client.recognize(file_content, "pcm", 16000, aip::null);
    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()
{
    // 设置APPID/AK/SK
    std::string app_id = "116303252";
    std::string api_key = "l6iGtYI7zvQNXrj2wHYAeudC";
    std::string secret_key = "zpYM0mBwuSajh6bpxMcUbhIjOEh30RKw";

    aip::Speech client(app_id, api_key, secret_key);

    asr(client);
    return 0;
}

(2)运行结果:

15.3 语音识别的封装

(1)封装结果:

powershell 复制代码
#pragma once
#include "../third/include/aip-cpp-sdk/speech.h"
#include "logger.hpp"

namespace Test
{
    class ASRClient
    {
    public:
        using ptr = std::shared_ptr<ASRClient>;

        ASRClient(const std::string &app_id,
            const std::string &api_key,
            const std::string &secret_key)
            :_client(app_id, api_key, secret_key)
        {}

        std::string recognize(const std::string &speech_data, std::string &err)
        {
            Json::Value result = _client.recognize(speech_data, "pcm", 16000, aip::null);
            if(result["err_no"].asInt() != 0) 
            {
                LOG_ERROR("语音识别失败:{}", result["err_msg"].asString());
                err = result["err_msg"].asString();
                return std::string();
            }

            return result["result"][0].asString();
        }

    private:
        aip::Speech _client;
    };
}

16. cmake搭建过程和介绍精简版

(1)设置 cmake 所需版本号:

cpp 复制代码
CMake
cmake_minimum_required(VERSION 3.1.3)

(2)设置项目工程名称:

cpp 复制代码
CMake
project(name)

(3)普通变量定义及内容设置:

cpp 复制代码
set(variable content1 content2 ...)
set(CMAKE_CXX_STANDARD 17) //设置 C++特性标准

(4)列表定义添加数据:

cpp 复制代码
CMake
set(variable_name "")
list(APPEND variable_name content)

(5)预定义变量:

预定义变量名称 备注
CMAKE_CXX_STANDARD c++特性标准
CMAKE_CURRENT_BINARY_DIR cmake 执行命令时所在的工作路径
CMAKE_CURRENT_SOURCE_DIR CMakeLists.txt 所在目录
CMAKE_INSTALL_PREFIX 默认安装路径

(6)字符串内容替换:

cpp 复制代码
CMake
string(REPLACE ".old" ".new" dest src)

(7)添加头文件路径:

cpp 复制代码
CMake
include_directories(path)

(8)添加链接库:

CMake

target_link_libraries(target lib1 lib2 ...)
(9)添加生成目标:

cpp 复制代码
CMake
add_executable(target srcfiles1 srcfile2 ...)

(10)错误或提示打印:

cpp 复制代码
CMake
message(FATAL_ERROR/STATUS content)

(11)查找源码文件:

cpp 复制代码
CMake
aux_source_directory(< dir > < variable >)

(12)判断文件是否存在:

cpp 复制代码
CMake
if (NOT EXISTS file)
endif()

(13)循环遍历:

cpp 复制代码
CMake
foreach(val vals)
endforeach()

(14)执行外部指令:

cpp 复制代码
CMake
add_custom_command(
 	PRE_BUILD //表示在所有其他步骤之前执行自定义命令
 	COMMAND //要执行的指令名称
 	ARGS //要执行的指令运行参数选项
 	DEPENDS //指定命令的依赖项
 	OUTPUT //指定要生成的目标名称
 	COMMENT //执行命令时要打印的内容
)

(15)添加嵌套子 cmake 目录:

cpp 复制代码
CMake
add_subdirectory(dir)

(16)设置安装路径:

cpp 复制代码
CMake
INSTALL(TARGETS ${target_name} RUNTIME DESTINATION bin)

详细的cmake介绍见博客:(未完成)。

现在已经完成服务器的环境搭建,现在进行各个子模块的服务实现,见博客:(未完成)。

客户端整体代码链接:(未完成)。

相关推荐
DN金猿1 小时前
git命令恢复/还原某个文件、删除远程仓库中的文件
git
别NULL3 小时前
机试题——疯长的草
数据结构·c++·算法
DWei_GaGa4 小时前
Git:查看分支、创建分支、合并分支
git
sdaxue.com4 小时前
帝国CMS:如何去掉帝国CMS登录界面的认证码登录
数据库·github·网站·帝国cms·认证码
CYBEREXP20084 小时前
MacOS M3源代码编译Qt6.8.1
c++·qt·macos
m0_748247554 小时前
github webhooks 实现网站自动更新
github
yuanbenshidiaos4 小时前
c++------------------函数
开发语言·c++
yuanbenshidiaos5 小时前
C++----------函数的调用机制
java·c++·算法
tianmu_sama5 小时前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
羚羊角uou5 小时前
【C++】优先级队列以及仿函数
开发语言·c++