C++仿RabbitMQ实现消息队列

前言

本项目将使用 C++Linux(CentOS 7.6) 环境下开发一个仿 RabbitMQ 的简易消息队列。

开发和调试环境如下:

  • 操作系统:Linux (CentOS 7.6)

  • 编辑器:Visual Studio Code / Vim

  • 编译器:g++(GNU Compiler Collection)

  • 调试工具:gdb

  • 构建工具:Makefile

一、技术栈介绍

开发主语言:C++

序列化框架:Protobuf 二进制序列化

  • 用于高效的二进制序列化和反序列化,适合网络传输,减少消息体积。

网络通信:自定义应用层协议+muduo库:

  • 对Tcp进行长连接封装,并且使用epoll的是将驱动模式,实现高并发服务器与客户端。

源数据信息数据库:SQLite3(轻量化数据库):

  • 直接嵌入进程序,只有一个小小的动态库

单元测试框架:Gtest

  • 使用Google出品的GTest框架进行单元测试,具备良好的C++支持、主流C++进行单元测试的工具

二、项目介绍

2.1背景介绍

曾经在学习生产消费者模型的时候,其中一种重要实现生产消费者模型的方式就是通过阻塞队列来进行实现,生产消费者模型常用于进行后端中的开发编程,分布式系统需要消息队列,主要得意于解耦合、支持并发、支持忙闲不均、削峰填谷等。

2.2消息队列概述

在后端的分布式系统的体系中,跨主机之间进行通讯页使用的是生产消费者模型,所以说将阻塞队列进行封装成一个独立的服务器程序,并且进行赋予丰富的功能,这样的服务器就是消息队列(Message Queue)简称MQ。市面上有很多成熟的消息队列RabbitMQ、Kafka、RoketMQ、ActiveMQ等等,其中RabbitMQ是一个非常有名并且功能强大的消息队列,我们就按照RabbitMQ来进行实现一个简易的消息队列。

三、CentOS环境的搭建

基本工具的安装

3.1、安装 wget

检查是否存在:

复制代码
wget --version

如果没有,执行安装:

复制代码
sudo yum install -y wget

3.2.、更换 yum 软件源

查看当前软件源:

复制代码
ls /etc/yum.repos.d/

备份存在的源配置:

复制代码
sudo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

下载阿里云源:

复制代码
sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

清理缓存,重构缓存:

复制代码
sudo yum clean all
sudo yum makecache

3.3、安装 SCL 和 EPEL 软件源

SCL(Software Collections) 用来安装高版本系统软件,如新版本 gcc。

安装 SCL:

复制代码
sudo yum install -y centos-release-scl centos-release-scl-rh

EPEL 是综合软件包扩展,提供更多软件。

安装 EPEL:

复制代码
sudo yum install -y epel-release

3.4、 安装常用工具

安装 lrzsz(文件传输工具):

复制代码
sudo yum install -y lrzsz

安装 Git:

复制代码
sudo yum install -y git

安装 CMake:

复制代码
sudo yum install -y cmake

3.5、 安装高版本 gcc/g++

通过 devtoolset-8 安装 GCC 8.3.1:

复制代码
sudo yum install -y devtoolset-8-gcc devtoolset-8-gcc-c++

立即生效:

复制代码
source /opt/rh/devtoolset-8/enable

为了每次打开终端自动生效,把命令写入 ~/.bashrc:

复制代码
echo "source /opt/rh/devtoolset-8/enable" >> ~/.bashrc

3.6、 基本工具的环境检查

检查 gcc:

复制代码
gcc --version

检查 git:

复制代码
git --version

检查 cmake:

复制代码
cmake --version

如果都有正确版本输出,那么恭喜你环境搭建完成了

从这里开始就是第三方库的安装了

3.7、安装protobuf

安装Protobuf依赖库:

bash 复制代码
sudo yum install autoconf automake libtool curl make gcc-c++ unzip

下载protobuf 包:

bash 复制代码
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.2/protobuf-all-3.20.2.tar.gz

3.8、安装muduo库

安装muduo库依赖的库

bash 复制代码
sudo yum install gcc-c++ cmake make zlib zlib-devel boost-devel

3.9、安装SQLite3数据库

这个数据库不是客户端服务器模式的,只是一个本地的数据库,只需要进行安装开发包即可

bash 复制代码
sudo yum install sqlite-devel

3.10、安装Gtest

安装epel软件源,上面我们已经进行安装过了

安装dnf工具

bash 复制代码
sudo yum install dnf

通过dnf进行安装

bash 复制代码
sudo dnf install dnf-plugins-core

安装Gtest和Gtest开发包

bash 复制代码
sudo dnf install gtest gtest-devel

检查Getest的安装

测试代码

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

void test()
{
    ASSERT_EQ(1,2);
}

int add(int a,int b)
{
    return a+b;
}

TEST(testCase,test1)
{
    EXPECT_EQ(add(2,3),5);
}
int main(int argc,char* argv[])
{
    testing::InitGoogleTest(&argc,argv);
    RUN_ALL_TESTS();
    test();
    return 0;
}

四、第三方库的使用和介绍

将环境进行搭建完后首先先进行介绍一下这个项目后续所要用到的第三方库,这个小节主要围绕以下三点进行展开,如何使用这些框架这些框架有什么功能如何进行使用这些第三方框架的API接口

4.1、protobuf

protobuf是什么?

Protobuf(Protocol Buffers)是Google开发的一种结构化数据序列化工具。

简单来说,它能把对象(比如一条消息)转成一串二进制字节,方便网络传输或者持久化存储,同时保持高效(体积小、解析快)。

为什么要用protobuf?

序列化工具有很多,例如JSON、XML等,但是protobuf相对于其他的序列化工具来说更高效、规范、跨平台

如何进行使用protobuf?

  • 编写proto文件

描述我们想要定义的结构化对象,描述对象中有什么样的成员,每个成员有什么样的属性,举个栗子:在protobuf中进行描述一个学生的结构:

姓名:一个字符串来表示 学号:用长整型来表示 年龄:整形来表示

cpp 复制代码
//声明语法版本
syntax = "proto3";

//声明代码的命名空间
package contacts;

//结构化对象的描述
message contact 
{
    //各个字段描述:  字段类型 字段名 = 字段唯一编号;
    uint64 sn = 1;
    string name = 2;
    float score = 3;
};

编写 .proto文件的规范

一般采用下划线命名法进行命名

指定proto3语法

  • 编译proto文件

针对proto文件中的描述,生成一份我们需要的对应的语言的结构化对象数据操作码,其中 .h 中定义了我们所描述的数据结构对象类; .cc 定义了实现结构化对象数据的访问、操作、序列化和反序列化

bash 复制代码
prtoc --cpp_out=. contacts.proto

通过执行编译命令会生成了两个文件,分别是contacts.pb.cc、contacts.pb.h

  • 使用
cpp 复制代码
class MessageLite {
public:
    // 序列化
    bool SerializeToOstream(ostream* output) const; // 将序列化后数据写入文件流
    bool SerializeToArray(void* data, int size) const;
    bool SerializeToString(string* output) const;

    // 反序列化
    bool ParseFromIstream(istream* input); // 从流中读取数据,再进行反序列化动作
    bool ParseFromArray(const void* data, int size);
    bool ParseFromString(const string& data);
};

MessageLite 是 protobuf 消息类的基类,其定义通常位于头文件(如 message_lite.h)中,而非 .cc 文件,所以说在contacts.pb.cc中进行查找是找不到的。

引入生成的头文件,在代码中根据需要进行使用即可

cpp 复制代码
#include <iostream>
#include "contacts.pb.h"

int main()
{
    contacts::contact  conn;
    conn.set_sn(10001);
    conn.set_name("小明");
    conn.set_score(60.5);

    //持久化的数据就放在str对象中,这时候可以对str进行持久化或网络传输
    std::string str = conn.SerializeAsString();


    contacts::contact stu;
    bool ret = stu.ParseFromString(str);
    if (ret == false) {
        std::cout << "反序列化失败!\n";
        return -1;
    }
    std::cout << stu.sn() << std::endl;
    std::cout << stu.name() << std::endl;
    std::cout << stu.score() << std::endl;
    return 0;
}

进行编译main.cc生成可执行文件

cpp 复制代码
g++ -o main main.cc contacts.pb.cc -lprotobuf -std=c++11

这里需要注意需要进行指定库文件的,否则会报错。

4.2、muduo

muduo库是什么?

Muduo 是一个基于 Reactor 模式 的 C++ 高性能网络库,由 陈硕大佬(@chenshuo) 开发并开源。它主要用于 Linux 平台,支持 TCP/UDP 网络编程,适用于构建高并发服务器程序。

Muduo库是基于主从 reactor 模型的高性能服务器框架,reactor模型就是基于事件触发模型(基于epoll进行IO事件的监控),主reactor模型的功能体现在只对新建立连接事件进行监控(保证不受IO阻塞影响实现高效进行建立新连接);从reactor模型:针对新建立的连接进行事件的监控(进行IO操作和业务处理),因此主从reactor必然是一个多执行流的并发方式,这种方式是核心是one thread one loop 即一个事件监控占据一个线程,进行事件监控。

muduo库的常见接口介绍

muduo::net::EventLoop类基础介绍

cpp 复制代码
class EventLoop : noncopyable
{
public:
    // 永久循环(必须在对象创建的同一线程中调用)
    void loop();

    // 退出循环(非线程安全)
    // 注意:通过裸指针调用时不保证线程安全,
    // 建议通过 shared_ptr<EventLoop> 调用以确保线程安全
    void quit();

    // 在指定时间运行回调函数
    TimerId runAt(Timestamp time, TimerCallback cb);

    // 延迟指定秒数后运行回调(支持跨线程调用)
    TimerId runAfter(double delay, TimerCallback cb);

    // 每隔指定秒数循环运行回调(支持跨线程调用)
    TimerId runEvery(double interval, TimerCallback cb);

    // 取消定时器(支持跨线程调用)
    void cancel(TimerId timerId);

private:
    std::atomic<bool> quit_;  // 原子操作的退出标志
    std::unique_ptr<Poller> poller_;  // 事件轮询器
    mutable MutexLock mutex_;  // 可变互斥锁
    std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);  // 待执行函数队列(受互斥锁保护)
};

muduo::net::TcpServer类基础介绍

cpp 复制代码
// Muduo库常见接口介绍

// 类型定义
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
typedef std::function<void (const TcpConnectionPtr&, Buffer*, Timestamp)> MessageCallback;    //服务端给客户端进行回消息的格式 一个连接、一个消息和一个时间

// InetAddress 类
class InetAddress : public muduo::copyable 
{
public:
    InetAddress(StringArg ip, uint16_t port, bool ipv6 = false);
};

// TcpServer 类
class TcpServer : noncopyable 
{
public:
    enum Option 
    {
        kNoReusePort,
        kReusePort,
    };

    TcpServer(EventLoop* loop,
              const InetAddress& listenAddr,
              const string& nameArg,
              Option option = kNoReusePort);

    void setThreadNum(int numThreads);
    void start();

    // 设置连接回调(非线程安全)
    void setConnectionCallback(const ConnectionCallback& cb) 
    { 
        connectionCallback_ = cb; 
    }

    // 设置消息回调(非线程安全)
    void setMessageCallback(const MessageCallback& cb) 
    { 
        messageCallback_ = cb; 
    }

private:
    ConnectionCallback connectionCallback_;
    MessageCallback messageCallback_;
};

成员函数的解析

这个类中有两个回调函数,其中setConnectionCallback用于当一个新连接进行建立时被调用,例如执行把建立连接的客户端的信息进行存储起来或者做一些其他的事情,举一个最好理解的例子:例如网络聊天室,当有好友进行上线(新连接建立成功),这是进行执行回调函数,这里回调函数的功能就是将这个进行好友上线这个消息进行通知所有好友。

setMessageCallback回调函数用于进行消息业务的处理,用于接到新连接消息的时候进行消息业务的处理。

setThreadNum

设置从属reactor数量的函数

构造函数

函数参数
EventLoop* loop :用于进行指定事件循环,每个 TcpServer 必须绑定到一个 EventLoop,负责处理 I/O 事件(如新连接、数据到达)
const InetAddress& listenAddr: 指定服务器监听的地址和端口。
const string& nameArg: 为服务器实例命名,用于日志和调试。
**Option option = kNoReusePort:**是否进行端口复用

muduo::net::Buffer类基础介绍

cpp 复制代码
class Buffer : public muduo::copyable
{
   public:
   static const size_t KCheapPrepend = 8;
   static const size_t kInitialSize = 1024;
   explicit Buffer(size_t initialSize = kInitialSize)
   : buffer_(KCheapPrepend + initialSize),
   readerIndex_(KCheapPrepend),
   writerIndex_(KCheapPrepend);
   void swap(Buffer& nbs)
   size_t readableBytes() const
   size_t writebleBytes() const
   const char* peek() const

   const char* findEDL() const
   const char* findEDL(const char* start) const
   void retrieve(size_t len)
   void retrieveInt64()
   void retrieveInt32()
   void retrieveInt16()
   void retrieveInt80()
   string retrieveAllAssFring()
   string retrieveAssFring(size_t len)
   void append(const StringPeace & str)
   void append(const char* /'restrict'/ data, size_t len)
   void append(const void# /'restrict'/ data, size_t len)
   char* beginWrite()
   const char* beginWrite() const
   void hasWritten(size_t len)
   void appendInt64(int64_t x)
   void appendInt32(int32_t x)
   void appendInt16(int16_t x)
   void appendInt8(int8 t x)
   int64_t readInt64()
   int32_t readInt32()
   int16_t readInt16()
   int8_t readInt8()
   int64_t peekInt64() const
   int32_t peekInt32() const
   int16_t peekInt16() const
   int8_t peekInt8() const 1
   void prependInt64(int64_t x)
   void prependInt32(int32_t x)
   void prependInt16(int16_t x)
   void prependInt8(int8_t x)
   void prepend(const void* /"restrict*/ data, size_t len)
   private:
   std::vector<char> buffer_
   size_t readerIndex_
   size_t writerIndex_
   static const char kcu[T]
}

muduo::net::TcpConnection类的基础介绍

cpp 复制代码
private:
    // 表示当前 TCP 连接的状态,用于状态机控制连接生命周期。
    // kDisconnected    - 已断开
    // kConnecting      - 正在连接中
    // kConnected       - 已连接
    // kDisconnecting   - 正在关闭连接(尚未完成关闭流程)
    enum StateE
    {
        kDisconnected,
        kConnecting,
        kConnected,
        kDisconnecting
    };

    // 指向所属的 EventLoop。每个 TcpConnection 必须绑定一个事件循环,
    // 所有 IO 事件的回调都必须在该 loop 所在线程中执行。
    EventLoop *loop_;

    // 用户设置的连接建立/断开时的回调函数。
    // 当连接建立或关闭时自动调用,可用于日志记录或资源管理等。
    ConnectionCallback connectionCallback_;

    // 用户设置的消息接收回调函数。
    // 当对端发送数据且读取成功后被调用,将消息数据传给用户逻辑处理。
    MessageCallback messageCallback_;

    // 写完成回调函数。
    // 当发送缓冲区的数据完全写入 socket 后调用,通常用于通知用户"可以继续发下一段数据"。
    WriteCompleteCallback writeCompleteCallback_;

    // 通用上下文数据,使用 boost::any 存储。
    // 用户可以在连接中绑定一些状态或会话信息(例如用户 ID、认证状态等)。
    boost::any context_;

muduo::net::TcpClient类基础介绍

cpp 复制代码
class TcpClient : noncopyable
{
public:
    // 构造函数(注释状态)
    // TcpClient(eventLoop* loop, const string& host, uint16 = port);
    // TcpClient(eventLoop* loop,
    //     const InetAddress& serverAddr,
    //     const string& namelet8);
    ~TcpClient(); // 强制外联析构,用于std::unique_ptr成员

    void connect();     // 连接服务器
    void disconnect();  // 断开连接
    void stop();        // 停止客户端

    // 获取当前连接(线程安全)
    TcpConnectionPtr connection() const
    {
        MutexLockGuard lock(mutex_);
        return connection_;
    }

    // 设置连接回调(非线程安全)
    void setConnectionCallback(ConnectionCallback cb)
    { connectionCallback_ = std::move(cb); }

    // 设置消息回调(非线程安全)
    void setMessageCallback(MessageCallback cb)
    { messageCallback_ = std::move(cb); }

private:
    EventLoop* loop_;
    ConnectionCallback connectionCallback_;
    MessageCallback messageCallback_;
    WriteCompleteCallback writeCompleteCallback_;
    TcpConnectionPtr connection_ GUARDED_BY(mutex_);  // 受互斥锁保护的连接对象
};

/* 
 * CountDownLatch 同步工具类
 * 备注:mutable库的服务器/客户端均为异步操作,
 * 客户端在连接未完全建立时发送数据,可通过此工具实现同步控制
 */
class CountDownLatch : noncopyable
{
public:
    explicit CountDownLatch(int count);  // 初始化计数器
    
    // 等待计数器归零
    void wait()
    {
        MutexLockGuard lock(mutex_);
        while (count_ > 0) {
            condition_.wait();
        }
    }
    
    // 计数器减1
    void countDown()
    {
        MutexLockGuard lock(mutex_);
        --count_;
        if (count_ == 0) {
            condition_.notifyAll();  // 唤醒所有等待线程
        }
    }
    
    int getCount() const;  // 获取当前计数值

private:
    mutable MutexLock mutex_;         // 可变互斥锁
    Condition condition_ GUARDED_BY(mutex_);  // 条件变量
    int count_ GUARDED_BY(mutex_);    // 计数器
};

contection 函数接口:获取客户端对应的通信连接

setConnectionCalback回调函数:连接服务器成功时的回调函数

setMessageCaback回调函数:收到服务器消息时的回调函数

connect():建立连接的函数,但是这个函数是非阻塞的,当进行调用该函数但是通信连接还没有建立成功时,容易进行崩溃。

CountDownLatch类 就是为了进行解决上面的问题的,通过类中的成员函数wait,在调用connect函数进行建立连接时先进行阻塞等待,countDown函数用于进行唤醒,将countDown函数进行定义到setConnectionCalback回调函数中,当调用setConnectionCalback回调函数时说明已经成功进行获取到客户端的通信的连接了,就可以进行解决由于connect进行连接时会出现崩溃的问题。

使用muduo库进行搭建服务器和客户端

以实现一个单词翻译为例进行使用muduo库进行搭建服务器和客户端

服务器的搭建

cpp 复制代码
#include<iostream>
#include "include/muduo/net/TcpServer.h"
#include "include/muduo/net/EventLoop.h"
#include<functional>
#include<string>
#include<unordered_map>

//muduo::net::TcpConnectionPtr  管理Tcp连接的只能指针类型
class TranslateServer
{
public:
    TranslateServer(int port)
    :_server(&_baseloop,muduo::net::InetAddress("0.0.0.0",port),\
    "TranslateServer",muduo::net::TcpServer::kReusePort)
    {
        //将我们类的成员函数进行设置成回调函数
       _server.setConnectionCallback(std::bind(&TranslateServer::onConnection,this,std::placeholders::_1)); 
       _server.setMessageCallback(std::bind(&TranslateServer::onMessage,this,std::placeholders::_1,\
       std::placeholders::_2,std::placeholders::_3));
    }
 
    //启动服务器
    void start()
    {
        //1、开始事件监听
        _server.start();
        //2、开始事件监控
        _baseloop.loop();
    }
private:
    //新连接建立成功时的回调函数
    void onConnection(const muduo::net::TcpConnectionPtr& conn)
    {
        if(conn->connected()==true)
        {
            std::cout<<"建立连接成功"<<std::endl;
        }
        else
        {
            std::cout<<"断开连接成功"<<std::endl;
        }
    }

    //进行处理消息业务的回调函数
    void onMessage(const muduo::net::TcpConnectionPtr& conn,muduo::net::Buffer* buf,muduo::Timestamp)
    {
        //1、从buf中进行读取数据
        std::string str=buf->retrieveAllAsString();
        //2、进行对应的数据业务处理
        std::string resp=translate(str);
        //3、将处理好的消息进行返回给客户端
        conn->send(resp);
    }

    std::string translate(const std::string& str)
    {
        //进行初始化哈希表
        static std::unordered_map<std::string,std::string> dirt={
            {"你好","hello"},
            {"hello","你好"}
        };
        auto it=dirt.find(str);
        if(it==dirt.end())
        {
            return "没有查找到目标string的译文";
        }
        else
        {
            return it->second;
        }
    }
private:
    muduo::net::EventLoop _baseloop;
    muduo::net::TcpServer _server;
};

int main()
{
    TranslateServer server(8085);
    server.start();
    return 0;
}

使用muduo进行搭建服务器的思路

1.先两个私有成员变量的设置

  • _server:

①用于进行绑定接收消息的回调函数和进行消息业务处理的回调函数

②进行服务器的启动

注意:在进行将成员函数进行绑定为回调函数时,由于成员函数是在类内,所以说成员函数的第一个参数默认是this 指针,但是我们回调函数在进行传参的时候是不希望进行额外将this 指针进行传入,所以通过C++11的bind 函数进行将this 指针进行绑定成为回调中的第一个参数。

  • _baseloop:

①用于_server的初始化绑定,进行IO事件的处理

②进行服务器的循环监控

2.进行成员函数的补充

  • 建立连接时的成员函数
  • 接收消息进行业务处理的函数
  • 构造函数
    • TcpServer对象的初始化(绑定EvenLoop对象,绑定服务器的IP和端口号,服务器的命名,服务器是否允许端口复用)
    • 将两个成员函数分别进行绑定成建立连接时的回调函数和接收消息进行业务处理的回调函数
  • 服务器启动函数
    • 开始事件监听
    • 开始事件监控

注意:

使用muduo库进行搭建服务器时,在进行编译的时候

客户端的搭建

cpp 复制代码
#include<iostream>
#include "include/muduo/net/TcpClient.h"
#include "include/muduo/net/EventLoopThread.h"
#include "include/muduo/base/CountDownLatch.h"
#include <functional>
#include<string>
//注意:muduo库中的操作都是异步操作
class TranslateClient
{
public:
    TranslateClient(const std::string& sip,int sport)
    :_latch(1)
    ,_client(_loopthread.startLoop(),muduo::net::InetAddress(sip,sport),"TranslateClient")
    {
        _client.setConnectionCallback(std::bind(&TranslateClient::onConnection,this,std::placeholders::_1));
        _client.setMessageCallback(std::bind(&TranslateClient::onMessage,this,std::placeholders::_1,\
       std::placeholders::_2,std::placeholders::_3));
    }

    //连接服务器
    void connect()
    {
        _client.connect();
        _latch.wait();
    }
    //向服务器进行发送消息
    bool send(const std::string& msg)
    {
        if(_conn->connected())
        {
            _conn->send(msg);
            return true;
        }
        else
        {
            return false;
        }
    }
private:
    //连接建立成功时的回调函数
    void onConnection(const muduo::net::TcpConnectionPtr& conn)
    {
        if(conn->connected())
        {
            _latch.countDown();
            _conn=conn;
        }
        else
        {
            _conn.reset();
        }
    }
    //收到消息时候的回调函数
    void onMessage(const muduo::net::TcpConnectionPtr& conn,muduo::net::Buffer* buf,muduo::Timestamp)
    {
        //翻译客户端
        std::cout<<"翻译结果:"<<buf->retrieveAllAsString()<<std::endl;
    }
private:
    muduo::CountDownLatch _latch;
    muduo::net::EventLoopThread _loopthread;    //直接就进行事件监控,无需进行调用
    muduo::net::TcpClient _client;
    muduo::net::TcpConnectionPtr _conn;
};

int main()
{
    TranslateClient client("127.0.0.1",8085);
    client.connect();
    while(1)
    {
        std::string buf;
        std::cin>>buf;
        client.send(buf);
    }
    return 0;
}

使用muduo库进行搭客户端的思路

1.私有成员变量的设置

  • muduo::CountDownLatch _latch:

用于控制主线程进行等待客户端连接完成的同步机制:

Muduo 是异步库,而 TcpClient.connect() 是非阻塞的,立即返回。但你想要在 main() 中等待连接建立之后再输入内容,因此需要一种机制来"阻塞"主线程直到连接建立。

初始化为 1(表示等待一个事件完成),connect() 调用后主线程阻塞在 _latch.wait()。当连接建立后,在 onConnection() 中调用 _latch.countDown(),唤醒主线程。

  • muduo::net::EventLoopThread _loopthread

创建并管理一个单独的 I/O 线程,用于执行 EventLoop:

Muduo 的网络事件(如连接建立、消息到达)都需要在一个 EventLoop(事件循环)中运行。EventLoopThread 封装了一个线程和它对应的 EventLoop

  • muduo::net::TcpClient _client

TCP 客户端对象,负责连接服务器、发送/接收数据:

封装了 socket 的连接逻辑、事件注册、消息发送等

  • muduo::net::TcpConnectionPtr _conn

当前Tcp连接的对象:

装了底层 socket 与 buffer,连接成功后,TcpClient 会通过回调把实际建立的连接传进来,程序可以通过这个连接对象进行数据读写。

2.成员函数的补充

  • 建立连接成功时的成员函数
  • 收到消息时的业务处理成员函数
  • 向服务器进行发送消息的函数
  • 构造函数
    • 控制主线程连接对象的初始化
    • 客户但对象的初始化(绑定_loopthread对象进行IO事件的处理,绑定服务端的IP和端口号,进行客户端的命名)
    • 将建立连接成功时的成员函数和接收消息时的业务处理函数进行设置成回调函数
  • 客户端启动函数
    • 进行连接的建立
    • 控制线程同步

使用muduo库实现protobuf协议的通信

ProtobufCodec类

ProtobufCodec 是 Muduo 网络库 中提供的一个封装类,用于支持 基于 Protobuf 的 TCP 消息编解码与处理。这个类在网络通信中起到了"协议适配器"的作用 ------ 它负责把原始的网络数据(字节流)转化成 Protobuf 定义的消息对象,也可以将 Protobuf 消息序列化成字节流发送出去,**通俗一点讲:**它的作用就像是一个翻译器:当你从网络上收到一串"看不懂"的二进制数据,ProtobufCodec 能把这些数据还原成你用 Protobuf 定义好的消息对象,就像把密文翻译成普通话;当你想把 Protobuf 消息发给别人时,它也能把你写好的消息对象转换成网络能传输的格式,再发出去,就像把你的话翻译成对方能听懂的语言。

cpp 复制代码
/*muduo-master/examples/protobuf/codec.h*/
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
 
class ProtobufCodec : muduo::noncopyable
{
public:
    enum ErrorCode
    {
        kNoError = 0,
        kInvalidLength,
        kCheckSumError,
        kInvalidNameLen,
        kUnknownMessageType,
        kParseError,
    };
    typedef std::function<void(const muduo::net::TcpConnectionPtr &, const MessagePtr &, muduo::Timestamp)> ProtobufMessageCallback;
 
    // 这⾥的messageCb是针对protobuf请求进⾏处理的函数,它声明在dispatcher.h中的
    ProtobufDispatcher类 explicit ProtobufCodec(const ProtobufMessageCallback &messageCb)
        : messageCallback_(messageCb), // 这就是设置的请求处理回调函数
          errorCallback_(defaultErrorCallback)
    {
    }
 
    // 它的功能就是接收消息,进⾏解析,得到了proto中定义的请求后调⽤设置的messageCallback_进⾏处理
    void onMessage(const muduo::net::TcpConnectionPtr &conn,
                   muduo::net::Buffer *buf,
                   muduo::Timestamp receiveTime);
 
    // 通过conn对象发送响应的接⼝
    void send(const muduo::net::TcpConnectionPtr &conn,
              const google::protobuf::Message &message)
    {
        // FIXME: serialize to TcpConnection::outputBuffer()
        muduo::net::Buffer buf;
        fillEmptyBuffer(&buf, message);
        conn->send(&buf);
    }
    static const muduo::string &errorCodeToString(ErrorCode errorCode);
    static void fillEmptyBuffer(muduo::net::Buffer *buf, const google::protobuf::Message &message);
    static google::protobuf::Message *createMessage(const std::string type_name);
    static MessagePtr parse(const char *buf, int len, ErrorCode *errorCode);
 
private:
    static void defaultErrorCallback(const muduo::net::TcpConnectionPtr &,
                                     muduo::net::Buffer *,
                                     muduo::Timestamp,
                                     ErrorCode);
    ProtobufMessageCallback messageCallback_;
    ErrorCallback errorCallback_;
    const static int kHeaderLen = sizeof(int32_t);
    const static int kMinMessageLen = 2 * kHeaderLen + 2; // nameLen + typeName +checkSum
    const static int kMaxMessageLen = 64 * 1024 * 1024;   // same as codec_stream.hkDefaultTotalBytesLimit
};

类定义与成员解释

类型定义:

typedef std::shared_ptr<google::protobuf::Message> MessagePtr;

typedef std::function<void(const TcpConnectionPtr&, const MessagePtr&, Timestamp)> ProtobufMessageCallback;

  • MessagePtr 是智能指针形式的 Protobuf 消息。
  • ProtobufMessageCallback 是应用层自定义的处理函数(回调),在成功解析 Protobuf 消息后调用。

构造函数

explicit ProtobufCodec(const ProtobufMessageCallback &messageCb)

设置消息处理回调函数。

errorCallback_ 默认为 defaultErrorCallback,可以处理错误情况。

编解码的核心方法

接收消息:onMessage()

void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp receiveTime);

当有数据到来时被调用。

  • 功能是:

从接收到的字节流中解析出 Protobuf 消息。

  • 调用过程:
  1. 从缓冲区读取消息长度。
  2. 验证校验和。
  3. 解析消息名,找到对应的消息类。
  4. 反序列化生成 Protobuf 对象。
  5. 调用 messageCallback_ 回调。

发送消息:send()

void send(const TcpConnectionPtr &conn, const google::protobuf::Message &message);

  • 功能是:

将 Protobuf 消息对象序列化为字节流,通过 conn->send() 发送出去。

内部使用 fillEmptyBuffer() 填充数据格式。

ProtobufDispatcher类

构建一个"Protobuf 消息 -> 用户处理函数"的映射体系,网络上收到消息后,已经被 ProtobufCodec 解码成了某种 google::protobuf::Message 类型。但这个消息到底是哪种?怎么调用你定义的对应业务逻辑?所以就需要这三类共同实现一个"类型安全、自动识别、统一接口调用"的分发系统。

cpp 复制代码
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
class Callback : muduo::noncopyable
{
public:
    virtual ~Callback() = default;
    virtual void onMessage(const muduo::net::TcpConnectionPtr &,
                           const MessagePtr &message,
                           muduo::Timestamp) const = 0;
};
 
// 这是⼀个对函数接⼝进⾏⼆次封装⽣成⼀个统⼀类型对象的类
template <typename T>
class CallbackT : public Callback
{
    static_assert(std::is_base_of<google::protobuf::Message, T>::value,
                  "T must be derived from gpb::Message.");
 
public:
    typedef std::function<void(const muduo::net::TcpConnectionPtr &,
                               const std::shared_ptr<T> &message,
                               muduo::Timestamp)>
        ProtobufMessageTCallback;
    CallbackT(const ProtobufMessageTCallback &callback)
        : callback_(callback)
    {
    }
    void onMessage(const muduo::net::TcpConnectionPtr &conn,
                   const MessagePtr &message,
                   muduo::Timestamp receiveTime) const override
    {
        std::shared_ptr<T> concrete = muduo::down_pointer_cast<T>(message);
        assert(concrete != NULL);
        callback_(conn, concrete, receiveTime);
    }
 
private:
    ProtobufMessageTCallback callback_;
};
// 这是⼀个protobuf请求分发器类,需要⽤⼾注册不同请求的不同处理函数,
// 注册完毕后,服务器收到指定请求就会使⽤对应接⼝进⾏处理
class ProtobufDispatcher
{
public:
    typedef std::function<void(const muduo::net::TcpConnectionPtr &,
                               const MessagePtr &message,
                               muduo::Timestamp)>
        ProtobufMessageCallback;
    // 构造对象时需要传⼊⼀个默认的业务处理函数,以便于找不到对应请求的处理函数时调⽤。
    explicit ProtobufDispatcher(const ProtobufMessageCallback &defaultCb)
        : defaultCallback_(defaultCb)
    {
    }
    // 这个是⼈家实现的针对proto中定义的类型请求进⾏处理的函数,内部会调⽤我们⾃⼰传⼊的业务处理函数
    void onProtobufMessage(const muduo::net::TcpConnectionPtr &conn,
                           const MessagePtr &message,
                           muduo::Timestamp receiveTime) const
    {
        CallbackMap::const_iterator it = callbacks_.find(message->GetDescriptor());
        if (it != callbacks_.end())
        {
            it->second->onMessage(conn, message, receiveTime);
        }
        else
        {
            defaultCallback_(conn, message, receiveTime);
        }
    }
    /*
    这个接⼝⾮常巧妙,基于proto中的请求类型将我们⾃⼰的业务处理函数与对应的请求给关联起来了
    相当于通过这个成员变量中的CallbackMap能够知道收到什么请求后应该⽤什么处理函数进⾏处理
    简单理解就是注册针对哪种请求--应该⽤哪个我们⾃⼰的函数进⾏处理的映射关系
    但是我们⾃⼰实现的函数中,参数类型都是不⼀样的⽐如翻译有翻译的请求类型,加法有加法请求类型
    ⽽map需要统⼀的类型,这样就不好整了,所以⽤CallbackT对我们传⼊的接⼝进⾏了⼆次封装。
    */
    template <typename T>
    void registerMessageCallback(const typename CallbackT<T>::ProtobufMessageTCallback &callback)
    {
        std::shared_ptr<CallbackT<T>> pd(new CallbackT<T>(callback));
        callbacks_[T::descriptor()] = pd;
    }
 
private:
    typedef std::map<const google::protobuf::Descriptor *, std::shared_ptr<Callback>> CallbackMap;
    CallbackMap callbacks_;
    ProtobufMessageCallback defaultCallback_;
};
  • 你(服务器)收到用户发来的信息(TCP 字节流)
  • ProtobufCodec 是前台接待员,把信息翻译成对应语言(Protobuf 消息)
  • ProtobufDispatcher 是调度员,根据语言类型派给不同的客服(你写的业务逻辑)
  • CallbackT<T> 是个"适配器耳机",让客服统一接收调度员的呼叫

基于muduo库,对于protobuf协议的处理代码,实现一个翻译+加法服务器与客户端

1、编写proto文件,生成相关结构代码

数据结构的proto文件

请求有请求的格式

响应有响应的格式

加法依旧如此

2、编写服务端代码,搭建服务器

客户端(发送 Protobuf 请求)

TCP层(Muduo TcpServer)

消息解码(ProtobufCodec)

请求分发(ProtobufDispatcher)

业务处理函数(onTranslate / onAdd)

构造响应 → ProtobufCodec → Tcp连接写回

cpp 复制代码
#include "../include/muduo/proto/codec.h"
#include "../include/muduo/proto/dispatcher.h"
#include "../include/muduo/base/Logging.h"
#include "../include/muduo/base/Mutex.h"
#include "../include/muduo/net/EventLoop.h"
#include "../include/muduo/net/TcpServer.h"

#include <iostream>
#include "request.pb.h"
#include <functional>

class Server
{
public:
    typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
    typedef std::shared_ptr<ys::TranslateRequest> TranslateRequestPtr;
    typedef std::shared_ptr<ys::AddRequest> AddRequestPtr;

    Server(int port)
        : _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "Server", muduo::net::TcpServer::kReusePort)
        , _dispatcher(std::bind(&Server::onUnknownMessage, this,
                std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
        ,_codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher,
                std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
    {
        // 注册消息类型和处理函数
        _dispatcher.registerMessageCallback<ys::TranslateRequest>(std::bind(&Server::onTranslate, this,
                std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        _dispatcher.registerMessageCallback<ys::AddRequest>(std::bind(&Server::onAdd, this,
                std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _server.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec,
                std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _server.setConnectionCallback(std::bind(&Server::onConnection, this, std::placeholders::_1));
    }

    void start()
    {
        _server.start();
        _baseloop.loop();
    }

private:
    std::string translate(const std::string &str)
    {
        // 进行初始化哈希表
        static std::unordered_map<std::string, std::string> dirt = {
            {"你好", "hello"},
            {"hello", "你好"}};
        auto it = dirt.find(str);
        if (it == dirt.end())
        {
            return "没有查找到目标string的译文";
        }
        else
        {
            return it->second;
        }
    }
    // 翻译功能
    void onTranslate(const muduo::net::TcpConnectionPtr &conn, const TranslateRequestPtr &message, muduo::Timestamp)
    {
        // 1. 提取message中的有效消息,也就是需要翻译的内容
        std::string req_msg = message->msg();
        // 2. 进行翻译,得到结果
        std::string rsp_msg = translate(req_msg);
        // 3. 组织protobuf的响应
        ys::TranslateResponse resp;
        resp.set_msg(rsp_msg);
        // 4. 发送响应
        _codec.send(conn, resp);
    }
    // 加法功能
    void onAdd(const muduo::net::TcpConnectionPtr &conn, const AddRequestPtr &message, muduo::Timestamp)
    {
        int num1 = message->num1();
        int num2 = message->num2();
        int result = num1 + num2;
        ys::AddResponse resp;
        resp.set_result(result);
        _codec.send(conn, resp);
    }
    // 未知消息处理
    void onUnknownMessage(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp)
    {
        LOG_INFO << "onUnknownMessage: " << message->GetTypeName();
        conn->shutdown();
    }
    // 连接事件处理
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if (conn->connected())
        {
            LOG_INFO << "新连接建立成功!";
        }
        else
        {
            LOG_INFO << "连接即将关闭!";
        }
    }

private:
    muduo::net::EventLoop _baseloop;
    muduo::net::TcpServer _server;
    ProtobufDispatcher _dispatcher; // 请求分发器对象
    ProtobufCodec _codec;           // protobuf协议处理器
};

protobuf为我们生成了请求结构类,我们进行使用的不直接是对象,而是智能指针

模仿server.cc

定义智能指针类型

两个回调哈数通过注册业务处理函数进行注册

收到不同的请求通过不同的函数进行处理

_codec专门的协议处理器

3、编写客户端代码,搭建客户端

cpp 复制代码
#include "muduo/proto/dispatcher.h"
#include "muduo/proto/codec.h"
#include "muduo/base/Logging.h"
#include "muduo/base/Mutex.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpClient.h"
#include "muduo/net/EventLoopThread.h"
#include "muduo/base/CountDownLatch.h"

#include "request.pb.h"
#include <iostream>

class Client
{
public:
    typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
    typedef std::shared_ptr<ys::AddResponse> AddResponsePtr;
    typedef std::shared_ptr<ys::TranslateResponse> TranslateResponsePtr;
    Client(const std::string &sip, int sport)
        : _latch(1), _client(_loopthread.startLoop(), muduo::net::InetAddress(sip, sport), "Client")
        ,_dispatcher(std::bind(&Client::onUnknownMessage, this, std::placeholders::_1,
            std::placeholders::_2, std::placeholders::_3))
        ,_codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher,
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
    {

        _dispatcher.registerMessageCallback<ys::TranslateResponse>(std::bind(&Client::onTranslate, this,
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        _dispatcher.registerMessageCallback<ys::AddResponse>(std::bind(&Client::onAdd, this,
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        _client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec,
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _client.setConnectionCallback(std::bind(&Client::onConnection, this, std::placeholders::_1));
    }
    void connect()
    {
        _client.connect();
        _latch.wait(); // 阻塞等待,直到连接建立成功
    }
    void Translate(const std::string &msg)
    {
        ys::TranslateRequest req;
        req.set_msg(msg);
        send(&req);
    }
    void Add(int num1, int num2)
    {
        ys::AddRequest req;
        req.set_num1(num1);
        req.set_num2(num2);
        send(&req);
    }

private:
    bool send(const google::protobuf::Message *message)
    {
        if (_conn->connected())
        { // 连接状态正常,再发送,否则就返回false
            _codec.send(_conn, *message);
            return true;
        }
        return false;
    }
    void onTranslate(const muduo::net::TcpConnectionPtr &conn, const TranslateResponsePtr &message, muduo::Timestamp)
    {
        std::cout << "翻译结果:" << message->msg() << std::endl;
    }
    void onAdd(const muduo::net::TcpConnectionPtr &conn, const AddResponsePtr &message, muduo::Timestamp)
    {
        std::cout << "加法结果:" << message->result() << std::endl;
    }
    void onUnknownMessage(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp)
    {
        LOG_INFO << "onUnknownMessage: " << message->GetTypeName();
        conn->shutdown();
    }
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if (conn->connected())
        {
            _latch.countDown(); // 唤醒主线程中的阻塞
            _conn = conn;
        }
        else
        {
            // 连接关闭时的操作
            _conn.reset();
        }
    }

private:
    muduo::CountDownLatch _latch;            // 实现同步的
    muduo::net::EventLoopThread _loopthread; // 异步循环处理线程
    muduo::net::TcpConnectionPtr _conn;      // 客户端对应的连接
    muduo::net::TcpClient _client;           // 客户端
    ProtobufDispatcher _dispatcher;          // 请求分发器
    ProtobufCodec _codec;                    // 协议处理器
};

int main()
{
    Client client("127.0.0.1",8085);
    client.connect();

    client.Translate("hello");
    client.Add(11,22);

    sleep(1);
    return 0;
}

4.3、SQLite3

SQLite3 是什么?

SQLite3 是一种轻量级的嵌入式关系型数据库管理系统(RDBMS),它的特点是 自给自足、无服务器、零配置。SQLite3 是 SQLite 的第 3 版,是目前最广泛使用的数据库之一,SQLite引擎也不是一个独立的进程,其可以按照应用程序的不同需求进行静态 / 动态连接,直接访问其存储文件。

为什么要用SQLite3 ?

1、SQLite是无服务器的,不需要一个单独的服务器进程 / 操作系统。

2、SQLite不需要配置,自给自足,不需要任何外部依赖。

3、一个完整的SQLite数据库存储在一个单一的、跨平台的磁盘文件上。

4、SQLite是一个轻量级数据库,省略可选功能配置时大小小于250KiB,即便完全配置,占据空间也不会大于400KIB。

5、SQLite事务是完全兼容ACID(数据库管理系统必须具备的四个特性:原子性、一致性、隔离性、持久性)的,允许从多个进程 / 线程安全访问。

6、SQLite支持SQL92(SQL2)标准的大多数数据库查询语言。

7、SQLite使用ANSI-C编写,提供的API简单且利于使用。

8、SQLite可以在UNIX(Linux、Mac OS-X,Android、iOS)和Windows(Win32、WinCE、WinRT)等多个平台运行,兼容性好。

如何进行使用SQLite3?

cpp 复制代码
1、查看当前数据库在编译阶段是否启动了线程安全(默认启用)
int sqlite3_threadsafe(); // 返回值:0-未启⽤; 1-启⽤
/*
需要注意的是SQLite3有三种安全等级:非线程安全模式(效率最高)、线程安全模式(不同的连接在不同的线程 进程是安全的,即一个句柄不能用于多线程之间)、串行化模式(可以在不同的线程,进程间使用同一个句柄)
*/
2、创建 / 打开数据库文件,并返回操作句柄
int sqlite3_open(const char *filename, sqlite3 **ppDb) // 成功返回SQLITE_OK
 
// 若在编译阶段启动了线程安全,则在程序运⾏阶段可以通过参数选择线程安全等级,
int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs );
/*
   const char *filename 
   要打开或创建的数据库文件路径。如果是 `":memory:"`,表示创建内存数据库。 
   sqlite3 **ppDb       
   输出参数,函数成功后将分配一个数据库句柄(连接指针)给它

   flags即文件打开方式,有四种
   1、SQLITE_OPEN_READWRITE -- 以可读可写⽅式打开数据库⽂件
   2、SQLITE_OPEN_CREATE -- 不存在数据库⽂件则创建
   3、SQLITE_OPEN_NOMUTEX--多线程模式,只要不同的线程使⽤不同的连接即可保证线程安全
   4、SQLITE_OPEN_FULLMUTEX--串⾏化模式
   3和4都是线程安全的设置方式;设置成功同样返回SQLITE_OK
*/
3、执行语句
int sqlite3_exec(sqlite3*, char *sql, int (*callback)(void*,int,char**,char**), void* arg,char **err)
/*
    参数含义
    sqlite3* db	已打开的数据库句柄。
    const char *sql	SQL 语句字符串,支持多条语句。
    int (*callback)(void*,int,char**,char**)	回调函数,处理查询结果;如果为 nullptr,则忽        略结果。
    void *arg	传给回调函数的用户数据,可以为 nullptr。
    char **errmsg	若不为 nullptr,将指向错误信息字符串(用完后需 sqlite3_free() 释放)。


    返回值:SQLITE_OK表示成功
    int (*callback)(void*,int,char**,char**)的介绍:
    void* : 是设置的在回调时传入的arg参数
    int:⼀⾏中数据的列数
    char**:存储⼀⾏数据的字符指针数组
    char**:每⼀列的字段名称
    这个回调函数有个int返回值,成功处理的情况下必须返回0,返回⾮0会触发ABORT退出程序
*/
4、销毁句柄
int sqlite3_close(sqlite3* db);          // 成功返回SQLITE_OK
int sqlite3_close_v2(sqlite3*);          // 推荐使⽤--⽆论如何都会返回SQLITE_OK

封装一个SQLite类,方便项目后面进行使用

cpp 复制代码
//封装实现SqliteHelper类,提供简单的sqlite数据库接口,完成数据基础的增删改查
//一般步骤
//1、创建/打开数据库
//2、针对打开数据库的操作(表的操作和数据的操作)
//3、关闭数据库

#include<iostream>
#include<sqlite3.h>
#include<string>

class SqliteHelper
{
public:
    typedef int (*SqliteCallback)(void*,int,char**,char**);
    SqliteHelper(const std::string& dbfile)
    :_handler(nullptr)
    ,_dbfile(dbfile)
    {

    }
    
    //1、创建打开数据库
    bool open(int safe_leve = SQLITE_OPEN_FULLMUTEX)
    {
        //safe_leve: 是可选参数,默认为 SQLITE_OPEN_FULLMUTEX,表示**线程安全(完全互斥)**的打开方式。
        //int sqlite3_open(const char *filename, sqlite3 **ppDb) // 成功返回SQLITE_OK
        //int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs );
        int ret=sqlite3_open_v2(_dbfile.c_str(),&_handler, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | safe_leve, nullptr);
        if(ret!=SQLITE_OK)
        {
            std::cout<<"创建/打开sqlite失败:";
            std::cout<<sqlite3_errmsg(_handler)<<std::endl;
            return false;
        }
        return true;
    }

    //2、针对打开的数据库进行执行操作
    bool exec(const std::string& sql,SqliteCallback cb,void* arg)
    {
        //int sqlite3_exec(sqlite3*, char *sql, int (*callback)(void*,int,char**,char**), void* arg,char **err)
        int ret=sqlite3_exec(_handler,sql.c_str(),cb,arg,nullptr);
        if (ret != SQLITE_OK) 
        {
            std::cout << sql << std::endl;
            std::cout << "执行语句失败: ";
            std::cout << sqlite3_errmsg(_handler) << std::endl;
            return false;
        }
        return true;
    }

    //3、进行关闭数据库
    void close()
    {
        //int sqlite3_close_v2(sqlite3*); 
        if(_handler)
        {
            sqlite3_close_v2(_handler);
        }
    }
private:
    std::string _dbfile;    //表示数据库文件路径,是 SQLite 要打开或创建的文件。
    sqlite3* _handler;      //指向 SQLite 的数据库句柄(sqlite3*),所有操作都基于此句柄进行。
};

4.4、gtest

gtest是什么?

gtest是Google进行开源的一个C++单元测试框架,进行编写和运行C++程序的自动化测试

为什么要使用gtest?

断言机制丰富:

  • 基本断言:EXPECT_EQ, EXPECT_NE, EXPECT_TRUE, EXPECT_FALSE 等。
  • 致命断言:ASSERT_EQ, ASSERT_TRUE 等(遇到失败就终止当前测试函数)。

测试结构清晰:

使用 TEST() 宏来定义测试用例,例如:

cpp 复制代码
TEST(MathTest, Addition)

{

EXPECT_EQ(2 + 2, 4);

}

支持测试夹具(Test Fixtures):

用于多个测试共享初始化代码,继承自 ::testing::Test。

兼容性好:

  • 跨平台(Linux、Windows、macOS)均支持。
  • 与 CMake 等构建系统兼容。

如何使用gtest?

宏断言

断言宏的认识

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

常见断言的认识

cpp 复制代码
// 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

断言宏的使用

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

/*
    断言宏的使用 
        ASSERT_  断言失败则退出
        EXPECT_  断言失败继续运行
    注意:
        断言宏,必须在单元测试宏函数中使用
*/

TEST(test,less_than)
{
    int age=18;
    ASSERT_GT(age,16);
    printf("OK");
}
TEST(test,great_than)
{
    int age=18;
    ASSERT_LT(age,20);
    printf("OK");
}
int main(int argc,char* argv[])
{
    //初始化
    testing::InitGoogleTest(&argc, argv);
    RUN_ALL_TESTS();
    return 0;
}

事件机制

测试套件的认识

测试中,可以有多个测试套件,测试套可以理解成一个测试环境,可以在单元测试之前进行测试环境的初始化,测试完毕后进行测试环境的清理

全局测试套件

在整体的测试中,只会进行初始化环境一次,在所有的测试用例完毕后才会进行清理,单元名称一般和测试套件名称相同

虚函数的重写

SetUp函数:在所有单元前执行的接口,常用于测试环境的初始化

TearDown函数:在所有单元测试运行完毕后执行的接口,完成测试环境的清理

全局套件的使用和认识

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

class MyEnvironment:public testing::Environment
{
public:
    virtual void SetUp() override
    {
        std::cout<<"单元测试执行前的环境初始化!!"<<std::endl;
    }
    virtual void TearDown() override
    {
        std::cout<<"单元执行完毕后的环境清理!!"<<std::endl;
    }
};



TEST(MyEnvironment,test1)
{
    std::cout<<"单元测试1"<<std::endl;
}

TEST(MyEnvironment,test2)
{
    std::cout<<"单元测试2"<<std::endl;
}


std::unordered_map<std::string, std::string> mymap;
class MyMapTest : public testing::Environment {
    public:
        virtual void SetUp() override 
        {
            std::cout << "单元测试执行前的环境初始化!!\n";
            mymap.insert(std::make_pair("hello", "你好"));
            mymap.insert(std::make_pair("bye", "再见"));
        }
        virtual void TearDown() override 
        {
            std::cout << "单元测试执行完毕后的环境清理!!\n";
            mymap.clear();
        }
};

TEST(MyMapTest, test1) 
{
    ASSERT_EQ(mymap.size(), 2);
    mymap.erase("hello");
}

TEST(MyMapTest, test2) 
{
    ASSERT_EQ(mymap.size(), 2);
}
int main(int argc,char* argv[])
{
    testing::InitGoogleTest(&argc,argv);
    testing::AddGlobalTestEnvironment(new MyEnvironment);
    testing::AddGlobalTestEnvironment(new MyMapTest);
    RUN_ALL_TESTS();
    return 0;
}

独立测试套件

在每次的单元测试中都活进行重新初始化测试环境,完毕后进行清理环境,用于解决多个全局套件依然会对同一个对象会有互相影响在里面

独立测试套件相对于全局测试套件的变化

  • 继承的类不同
  • 单元测试函数不同
  • 环境中的类可以进行自定义成员

独立套件的使用和认识

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

class MyTest : public testing::Test
{
public:
    // 对于整个测试类只会进行执行一次,用于初始化一次的大资源
    static void SetUpTestCase()
    {
        std::cout << "所有单元测试执行前,初始化总环境!!" << std::endl;
    }
    static void TearDownTestCase()
    {
        std::cout << "所有单元测试完毕后,清理总环境!!" << std::endl;
    }

    // 每个测试用例前进行调用一次,用于进行设置档期那测试的状态
    void SetUp() override
    {
        std::cout << "单元测试初始化!!\n";
        _mymap.insert(std::make_pair("hello", "你好"));
        _mymap.insert(std::make_pair("bye", "再见"));
    }
    void TearDown() override
    {
        _mymap.clear();
        std::cout << "单元测试清理!!\n";
    }

public:
    std::unordered_map<std::string, std::string> _mymap;
};

TEST_F(MyTest, insert_test) 
{
    _mymap.insert(std::make_pair("leihou", "你好"));
    ASSERT_EQ(_mymap.size(), 3);
    
}
TEST_F(MyTest, size_test) 
{
    ASSERT_EQ(_mymap.size(), 2);
}

int main(int argc, char *argv[])
{
    testing::InitGoogleTest(&argc, argv);
    RUN_ALL_TESTS();
    return 0;
}

4.5、future

future是什么?

在多线程的编程中,经常将任务进行交给其他线程进行异步执行,但是我们还希望在某个是间带你获取这个任务的执行结果,所以说需要一种机制来进行等待并获取结果,在学习linux的时候进行都是采用全局变量或者是线程间的通信进行解决等待并进行回去结果的,但是这种方式代码比较复杂,而且容易出现同步问题,C++11中引入的future模板类进行解决这个问题,它允许我们将一个异步任务的结果"托管"起来,稍后在主线程中获取这个结果。std::future 实际上维护了一个共享状态,当异步任务完成时,它会将结果写入这个状态。主线程通过调用 get() 来访问结果,必要时会阻塞直到结果就绪。

如何进行使用future?

关联组件

  • std::promise:用来设置值。
  • std::async:用来启动一个异步任务,并返回 std::future。
  • std::packaged_task:将函数封装为一个任务,可以异步执行并返回结果。

这三个组件的目的都是用来进行获取异步线程进行执行任务的结果,只是这个三个组件进行采用的方式不同

注意事项

  • future.get() 等待结果。
  • promise.set_value() 设置结果。
  • async 自动管理线程并返回 future。
  • 只允许一次 get(),但可用 shared_future 多次获取。

基于代码对于三大组件的理解

cpp 复制代码
#include<iostream>
#include<future>
#include<thread>
#include<chrono>


int Add(int num1, int num2) 
{
    std::cout << "加法!!1111\n";
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "加法!!2222\n";
    return num1 + num2;
}

int main()
{
    //std::async(func, ...)      std::async(policy, func, ...)
    std::cout << "--------1----------\n";
    //std::launch::deferred  在执行get获取异步结果的时候,才会执行异步任务
    //std::launch::async   内部会创建工作线程,异步的完成任务
    std::future<int> result = std::async(std::launch::deferred, Add, 11, 22);
    //std::future<int> result = std::async(std::launch::async, Add, 11, 22);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "--------2----------\n";
    int sum = result.get();
    std::cout << "--------3----------\n";
    std::cout << sum << std::endl;
    return 0;
}
cpp 复制代码
#include <iostream>
#include <thread>
#include <future>

// 通过在线程中对promise对象设置数据,其他线程中通过future获取设置数据的方式实现获取异步任务执行结果的功能
void Add(int num1, int num2, std::promise<int> &prom)
{
    std::this_thread::sleep_for(std::chrono::seconds(3));
    prom.set_value(num1 + num2);
    return;
}

int main()
{
    std::promise<int> prom; //创建promise对象,这个对象内部维护了一个共享状态

    std::future<int> fu = prom.get_future();    //prom获取一个绑定的future对象,promise 和 future 并不是直接传递值的,
                                                //而是通过一个内部的共享状态来通信的。

    std::thread thr(Add, 11, 22, std::ref(prom));   //按照引用进行传递prom,一个线程设置值,另一个线程可以获取这个值
    int res = fu.get();
    std::cout << "sum: " << res << std::endl;
    thr.join();
    return 0;
}
cpp 复制代码
#include <iostream>
#include <thread>
#include <future>
#include <memory>
// pakcaged_task的使用
//    pakcaged_task 是一个模板类,实例化的对象可以对一个函数进行二次封装,
// pakcaged_task可以通过get_future获取一个future对象,来获取封装的这个函数的异步执行结果



//package进行封装函数就是为了进行获取线程进行执行函数的结果
int Add(int num1, int num2)
{
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return num1 + num2;
}

int main()
{
    // std::packaged_task<int(int,int)> task(Add);
    // std::future<int> fu = task.get_future();

    // task(11, 22);  task可以当作一个可调用对象来调用执行任务
    // 但是它又不能完全的当作一个函数来使用
    // std::async(std::launch::async, task, 11, 22);
    // std::thread thr(task, 11, 22);

    // 但是我们可以把task定义成为一个指针,传递到线程中,然后进行解引用执行
    // 但是如果单纯指针指向一个对象,存在生命周期的问题,很有可能出现风险
    // 思想就是在堆上new对象,用智能指针管理它的生命周期
    auto ptask = std::make_shared<std::packaged_task<int(int, int)>>(Add);
    std::future<int> fu = ptask->get_future();
    std::thread thr([ptask]()
                    { (*ptask)(11, 22); });

    int sum = fu.get();
    std::cout << sum << std::endl;
    thr.join();
    return 0;
}

task 看起来和一个可调用对象一样,但是这又不是一个完完全全的可调用对象,也不能把他当成一个具体的函数来进行对待

进行使用时,通过智能指针进行进行指向task,通过解引用进行使用,所以说直接在堆上进行new空间,通过智能指针进行管理起来,为什么不使用指针呢?单纯指针指向一个对象,存在生命周期的问题,很可能出现风险

将ptask进行传入到线程中进行执行

C++线程池的实现

线程池实现的技术选择

线程池的实现选择的我们选择使用 std::packaged_task 配合 std::future 来获取任务结果,没有进行选择promise和async的原因是promise 需要进行将promise 当成参数进行传参,这就涉及到需要进行更改函数的参数,这显然是不合适的,async 内部直接自带线程进行执行也是不合适的,我们在进行调用异步线程进行执行的时候是自己手动进行创建线程的,更利于线程资源和任务调度。

线程池进行实现的核心

  • 线程池提供的操作
    • 入队任务:将需要进行执行的任务(函数操作)进行放入队列中
    • 停止运行:中止线程池
  • 线程池管理的资源
    • 线程池:用vector 进行维护的函数池子
    • 互斥锁和条件变量:保证线程安全,提高CPU资源的利用率
    • 一定数量的工作线程:获取任务和执行任务
    • 结束任务的标志:便于进行控制线程池的结束
  • 线程池的工作思想

用于进行传入要进行执行的函数于线程池中,由线程池进行分配线程池中的线程进行执行

线程池的设计

cpp 复制代码
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <future>
#include <vector>
#include <functional>

class threadpool
{
public:
    using Functor = std::function<void(void)>;
    threadpool(int thr_count = 1) // 线程的个数
        : _stop(false)
    {
        for (int i = 0; i < thr_count; i++)
        {
            // 创建线程对象
            // 当线程进行创建时直接进行调用this->entry(),即线程池对象的entry方法
            _threads.emplace_back(std::thread(&threadpool::entry, this));
        }
    }

    void stop()
    {
        if (_stop == true)
        {
            return;
        }
        _stop = true;
        _cv.notify_all();
        for (auto &thread : _threads)
        {
            thread.join();
        }
    }

    template <typename F, typename... Args>
    auto push(const F &&func, Args &&...args) -> std::future<decltype(func(args...))>
    {
        // 1、将传入的函数进行封装成packaged_task任务
        using return_type = decltype(func(args...));
        auto tmp_func = std::bind(std::forward<F>(func), std::forward<Args>(args)...);
        auto task = std::make_shared<std::packaged_task<return_type()>>(tmp_func);
        std::future<return_type> fu = task->get_future();
        // 2. 构造一个lambda匿名函数(捕获任务对象),函数内执行任务对象
        {
            std::unique_lock<std::mutex> lock(_mutex);
            // 3. 将构造出来的匿名函数对象,抛入到任务池中
            _taskpool.push_back([task]()
                                { (*task)(); });
            _cv.notify_one();
        }
        return fu;
    }

    ~threadpool()
    {
        stop();
    }

private:
    // 线程的入口函数 --- 内部不断从任务池中进行取出任务执行
    void entry()
    {
        std::vector<Functor> tmp_taskpool;
        {
            std::unique_lock<std::mutex> lock(_mutex); 
            _cv.wait(lock, [this]()
                     { return _stop || !_taskpool.empty(); }); 
            tmp_taskpool.swap(_taskpool);                      
        }
        // 执行所有任务
        for (auto &task : tmp_taskpool)
        {
            task();
        }
    }

private:
    std::mutex _mutex;
    std::condition_variable _cv;
    std::vector<std::thread> _threads;
    std::vector<Functor> _taskpool;
    std::atomic<bool> _stop;
};


int Add(int num1, int num2) 
{
    return num1 + num2;
}
int main()
{
    threadpool pool;
    for (int i = 0; i < 10; i++) {
        std::future<int> fu = pool.push(Add, 11, i);
        std::cout << fu.get() << std::endl;
    }
    pool.stop();
    return 0;
}

线程入口函数的设计的几点说明

线程入口函数entry中锁进行保护的是共享资源 标记位_stop和任务池_taskpool

线程仅在两种条件下被会进行继续执行

1、线程池被中止 2、任务队列中由其他待执行的任务

批量取出任务,减少锁持有的时间,增大效率
push 函数的设计思路解释

函数头的设计

用户将想让线程池中的线程进行执行的任务函数进行push到任务池中让对应的线程进行执行,并且获取线程进行执行的结果,由于不知道进行传入的函数的返回值和函数的参数类型和个数,只好通过模板和可变参数来进行实现,用户还需要进行获取线程执行的结果,但是future也是一个模板类,我们是不知道push的具体返回的结果的类型的,因此根据 decltype(func(args...)) 和auto进行使用进行类型推导。

函数体的设计

用户提交的任务可能是带有参数的函数(如 int foo(int x, int y)),但线程池的任务队列需要存储无参的可调用对象(统一为 std::function<void()>)。

std::bind 的作用:

将 func 和 args... 绑定为一个新的无参函数对象 tmp_func,调用 tmp_func() 等价于调用 func(args...)。

相关推荐
慧一居士21 分钟前
ShardingSphere-JDBC 与 Sharding-JDBC 的对比与区别
分布式·系统架构
隰有游龙3 小时前
hadoop集群启动没有datanode解决
大数据·hadoop·分布式
UCoding3 小时前
我们来学zookeeper -- 集群搭建
分布式·zookeeper
小马哥编程4 小时前
【ISAQB大纲解读】Kafka消息总线被视为“自下而上设计”?
分布式·kafka·系统架构·linq
帅气的小峰8 小时前
1-【源码剖析】kafka核心概念
分布式·kafka
xiaolin03338 小时前
【RabbitMQ】- Channel和Delivery Tag机制
分布式·rabbitmq
不吃饭的猪9 小时前
记一次运行spark报错
大数据·分布式·spark
qq_463944869 小时前
【Spark征服之路-2.1-安装部署Spark(一)】
大数据·分布式·spark
昭阳~10 小时前
Kafka深度技术解析:架构、原理与最佳实践
分布式·架构·kafka
ikun·11 小时前
Kafka 消息队列
分布式·kafka