英雄联盟网站设计,阿里的6家外包公司名单,seo需要什么技术,宁波百度快照优化排名本篇摘要
本篇是c中的一个仿RabbitMQ实现消息队列项目项目的开篇#xff0c;在本篇我们将介绍这四个好用的“神器”的用法#xff0c;方便之后#xff0c;实现后面的项目做铺垫。 欢迎拜访#xff1a; 点击进入博主主页 本篇主题#xff1a; SQLite3、Protobuf、gtest、…本篇摘要
本篇是c中的一个仿RabbitMQ实现消息队列项目项目的开篇在本篇我们将介绍这四个好用的“神器”的用法方便之后实现后面的项目做铺垫。 欢迎拜访 点击进入博主主页 本篇主题 SQLite3、Protobuf、gtest、muduo 简单科普 制作日期 2025.08.19 隶属专栏 点击进入所属仿RabbitMQ实现消息队列项目专栏 一.SQlite3介绍及简单使用
SQLite简介
定义SQLite 是一个进程内的轻量级数据库它实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它是一个零配置的数据库这意味着与其他数据库不一样我们不需要在系统中配置。像其他数据库 SQLite 引擎不是一个独立的进程可以按应用程序需求进行静态或动态连接 SQLite 直接访问其存储文件。特点: 无需单独服务器进程或操作系统支持。无需配置。整个数据库存储为单一跨平台磁盘文件。体积小完全配置小于400KiB省略可选功能小于250KiB。自给自足无外部依赖。事务兼容ACID支持多进程或线程安全访问。支持SQL92SQL2标准多数查询语言功能。用ANSI - C编写API简单易用。可在UNIXLinux、Mac OS - X、Android、iOS和WindowsWin32、WinCE、WinRT运行。
安装SQlite3
这段内容展示了在 Linux 系统从命令风格看大概率是 CentOS/RHEL 类系统上安装 SQLite 开发包并验证安装结果的步骤总结如下
1. 安装 SQLite 开发包
要编译/开发依赖 SQLite 的程序比如用 C/C 操作 SQLite 数据库通常需要先安装 sqlite-devel 这个开发包它包含了头文件、库文件等开发所需资源。 在终端中执行
sudo yum install sqlite-develsudo以管理员root权限执行命令因为安装软件包需要系统级权限。yum installCentOS/RHEL 系统下使用 yum 包管理器来安装软件包。sqlite-devel就是 SQLite 的开发包名称。
2. 验证安装是否成功
安装完成后可以通过查看 SQLite 命令行工具的版本来确认开发包以及 SQLite 本身是否正常安装。在终端执行
sqlite3 --version如果能看到类似下面的输出版本号、编译时间等信息就说明安装成功了
3.7.17 2013-05-20 00:56:22
118a3b35693b134d56ebd780123b7fd6f1497668简单来说整个流程就是用 yum 安装 sqlite-devel → 执行 sqlite3 --version 验证以此完成 SQLite 开发环境的搭建与验证(ubuntu就是apt)。
接口解释及使用
这个 我们要使用的Sqlite 类做了三件事
打开/创建一个 SQLite 数据库文件执行一条 SQL 语句比如建表、插入数据、查询等关闭数据库连接
SQLite3 函数介绍通俗版
1. sqlite3_open_v2(...) 作用打开或创建一个 SQLite 数据库文件 原型简化理解
int sqlite3_open_v2(const char *filename, // 数据库文件名比如 test.dbsqlite3 **ppDb, // 输出参数返回数据库句柄操作数据库的“钥匙”int flags, // 打开方式读写、创建、线程模式等const char *zVfs // 一般用 nullptr表示默认文件系统
);通俗解释 你想操作一个数据库文件比如 test.db这个函数会 如果文件存在 → 打开它如果文件不存在 → 创建一个新的数据库文件 成功后会返回一个“句柄”_handler后续所有操作都要通过这个句柄来进行 如果失败比如路径不对、权限不够等会返回错误码你可以通过 sqlite3_errmsg() 查看错误信息。 常用标志位你一定要了解的
标志常量含义是否常用SQLITE_OPEN_READWRITE以读写方式打开数据库可执行 INSERT/UPDATE 等✅ 常用SQLITE_OPEN_READONLY以只读方式打开数据库只能查询不能修改按需使用SQLITE_OPEN_CREATE如果文件不存在则创建新数据库文件✅通常要加SQLITE_OPEN_FULLMUTEX以全线程安全模式打开所有操作自动加锁适合多线程✅推荐用于多线程SQLITE_OPEN_SHAREDCACHE启用共享缓存模式多个连接共享缓存一般不用特殊场景使用SQLITE_OPEN_PRIVATECACHE每个连接独立缓存默认行为一般不用显式设置默认即可
使用如 int ret sqlite3_open_v2(_filename.c_str(), _handler, SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);2. sqlite3_exec(...) 作用执行一条 SQL 语句比如建表、插入、查询等 原型简化理解
int sqlite3_exec(sqlite3 *db, // 数据库句柄就是之前打开数据库拿到的 _handlerconst char *sql, // 你要执行的 SQL 语句字符串比如 CREATE TABLE xxxint (*callback)(void*,int,char**,char**), // 回调函数查询时用来处理结果可选void *arg, // 传给回调函数的参数一般用不到可以填 nullptrchar **errmsg // 如果出错这里会返回错误信息字符串
);通俗解释
你想让 SQLite 执行一句 SQL比如创建表、插入数据、查询等这个函数可以执行 绝大多数 SQL 命令如果是查询语句SELECT通常需要搭配回调函数来获取返回的数据如果执行失败会返回错误码并可通过 errmsg 获取错误信息不过你代码里这个参数传的是 nullptr所以看不到具体错误信息建议后续改进
使用如 int ret sqlite3_exec(_handler, sql.c_str(), cb, nullptr, nullptr);这里需要注意的就是这个回调函数
int callback(void *NotUsed, int argc, char **argv, char **azColName);这个接口最终必须返回0不然出错。 当我们exc执行完对应sql语句后拿到的串就会调用这个函数一般我们把对应的对象传进去然后等查询完就调用这个函数完成想要的操作。
现在根据这个框架解释下 static int SelectCallback(void *arg, int colnum, char **rowarr, char **segname){return 0; // 注意返回0}一般只有进行读取操作才用到。首先读出一行数据就去调用它。第一个参数就是我们调用SQlite3_exec()传进来的对应的参数。第二个参数就是有多少列。第三个参数就是当前行的字符串数组。第四个参数就是每个字段名字构成的数组。
假设我们读出数据有5行相当于遍历走五遍这个回调函数后面项目会用到。
3. sqlite3_close_v2(...) 作用安全地关闭数据库连接释放资源 原型简化理解
int sqlite3_close_v2(sqlite3 *db);通俗解释
当你用完数据库比如程序退出、不再需要操作数据库时必须调用这个函数来关闭数据库连接它会安全释放所有相关资源防止内存泄漏或数据库文件损坏传入的参数就是之前 sqlite3_open_v2() 返回的那个数据库句柄 _handler
总结这几个函数是干嘛的
SQLite 函数作用你的封装函数是否必须sqlite3_open_v2()打开/创建一个 SQLite 数据库文件返回一个句柄用于后续操作bool Open(...)✅ 必须第一步sqlite3_exec()执行 SQL 语句建表、插入、查询等bool Exec(...)✅ 必须核心功能sqlite3_close_v2()关闭数据库连接释放资源void Close()✅ 必须最后一步
封装Sqlite3类
#include iostream
#include string
#include vector
#include sqlite3.h// 模拟封装个简单的sqliteclass Sqlite
{public:using callback int (*)(void *, int, char **, char **);// typedef int (*callback)(void*,int,char**,char**);Sqlite(const std::string name) : _filename(name) {}bool Open(int level SQLITE_OPEN_FULLMUTEX){int ret sqlite3_open_v2(_filename.c_str(), _handler, level | SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);if (ret ! SQLITE_OK){std::cout 创建/打开sqlite数据库失败: ;std::cout sqlite3_errmsg(_handler) std::endl;return false;}elsereturn true;}bool Exec(const std::string sql, callback cb, void *arg){int ret sqlite3_exec(_handler, sql.c_str(), cb, nullptr, nullptr);if (ret ! SQLITE_OK){std::cout sql std::endl;std::cout 执行语句失败: ;std::cout sqlite3_errmsg(_handler) std::endl;return false;}return true;}void Close(){if (_handler)sqlite3_close_v2(_handler);}private:std::string _filename;sqlite3 *_handler;
};基于封装SQlite3类的测试
下面执行这两条语句
string sql_create create table if not exists stu( num int primary key , name varchar(32),age int);;string sql_addinsert into stu values (0,张三,18),(1,李四,11) ,(2,王五,10);;sq.Exec(sql_create,nullptr,nullptr);sq.Exec(sql_add,nullptr,nullptr);编译链接下
g test.cc -lsqlite3运行后可以看到创建成功 然后我们打开对应的SQlite3对应的文件sqlite3 test.db3 内部使用语法基本和mysql一致但是查询比如是.tables,退出是.exit然后就是这类语句都没有分号而一些如select创建等操作都要有分号
效果
roothsyq:/home/youxing/cpp_project-2/message_queue_project/ready_demo/sqlite# sqlite3 test.db3
SQLite version 3.37.2 2022-01-06 13:25:41
Enter .help for usage hints.
sqlite .tables
stu
sqlite select * from stu;
0|张三|18
1|李四|11
2|王五|10
sqlite 下面执行
string sql_deldelete from stu where num0;
string sql_upupdate stu set name篱笆where num1;;
sq.Exec(sql_del,nullptr,nullptr);
sq.Exec(sql_up,nullptr,nullptr);效果
SQLite version 3.37.2 2022-01-06 13:25:41
Enter .help for usage hints.
sqlite select * from stu;
1|篱笆|11
2|王五|10
sqlite .exit这里就先使用到这毕竟后面项目用到的也就这些。
二. Protobuf的介绍及简单使用
安装Protobuf
1. 安装依赖库适用于 C
sudo yum install -y autoconf automake libtool curl make gcc-c unzip2. 下载 Protobuf 包
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.2/protobuf-all-3.20.2.tar.gz
# 若 GitHub 下载慢改用
# wget https://gitee.com/qigezi/bitmq/blob/master/mqthird/protobuf-all-3.20.2.tar.gz3. 编译安装
tar -zxf protobuf-all-3.20.2.tar.gz \
cd protobuf-3.20.2 \
./autogen.sh \
./configure \
make -j$(nproc) \
sudo make install \
protoc --version说明
-j$(nproc)多核加速编译若仅需特定语言支持如 Python/Java可跳过 autogen.sh 步骤安装路径默认为 /usr/local/可通过 ./configure --prefix/your/path 自定义
基于Protobuf的简单介绍
1. Protobuf 是什么
ProtobufProtocol Buffers 是 Google 开发的一种高效的数据序列化格式用于结构化数据存储和网络通信。 简单来说它就像一个更小、更快、更省空间的 JSON/XML 替代品但比它们更高效
对比理解
格式特点适用场景JSON/XML可读性好但体积大、解析慢网页 API、配置文件Protobuf二进制、体积小、速度快、跨语言网络通信、游戏、微服务、存储2·Protobuf 的核心优势
体积小二进制格式比 JSON/XML 小很多 速度快解析和序列化比 JSON 快得多 跨语言支持 C、Java、Python、Go、C# 等 强类型定义数据结构.proto 文件编译后生成代码 兼容性好可以向后兼容旧代码能读新数据新代码也能读旧数据 3. Protobuf 基本使用步骤 步骤 1定义数据结构.proto 文件
先写一个 .proto 文件定义你要传输的数据格式例如
示例person.proto
syntax proto3; // 使用 Protobuf 3 语法// 定义一个消息类似 C 的 struct / Python 的 class
message Person {string name 1; // 字段名 字段编号唯一int32 id 2; // 编号不能重复string email 3;
}syntax proto3表示使用 Protobuf 3 语法推荐。message类似于 C 的 struct / Python 的 class用于定义数据结构。string name 1; name 是字段名string 是类型1 是唯一编号不能重复。编号1, 2, 3… 用于二进制编码不能改但可以改字段名
步骤 2编译 .proto 文件生成代码
用 protocProtobuf 编译器 把 .proto 文件编译成 C/Python/Java 等代码。
安装 protocLinux/macOS
# 下载 protoc以 3.20.2 为例
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.2/protobuf-all-3.20.2.tar.gz
tar -xzf protobuf-all-3.20.2.tar.gz
cd protobuf-3.20.2
./configure
make -j$(nproc)
sudo make install
protoc --version # 检查是否安装成功编译 .proto 文件生成 C 代码
protoc --cpp_out. person.proto--cpp_out.生成 C 代码.pb.h 和 .pb.cc 文件其他语言 --python_out. → Python--java_out. → Java--go_out. → Go
编译后会生成
person.pb.h头文件person.pb.cc实现文件C
4. Protobuf 语法详解
基本数据类型
Protobuf 类型C 类型Python 类型说明int32 / int64int32_t / int64_tint整数uint32 / uint64uint32_t / uint64_tint无符号整数floatfloatfloat单精度浮点数doubledoublefloat双精度浮点数boolboolbool布尔值stringstd::stringstr字符串bytesstd::stringbytes二进制数据
字段规则
规则说明required必须赋值Protobuf 3 已废弃不推荐用optional可选字段默认值0/false/“”repeated数组/列表类似 std::vector
示例带 repeated
message Person {string name 1;repeated int32 phone_numbers 2; // 可以存多个电话号码
}如
Person person;
person.add_phone_numbers(123456789); // 添加一个号码
person.add_phone_numbers(987654321); // 再添加一个5.常用函数接口
功能接口说明创建对象Person person;实例化 Protobuf 消息设置字段set_xxx(value)设置普通字段如 set_name(Alice)add_xxx(value)添加 repeated 字段如 add_phone_numbers(123)读取字段xxx()读取普通字段如 person.name()xxx(index)读取 repeated 字段的某个元素如 person.phone_numbers(0)序列化SerializeToString(str)转成二进制字符串SerializeToArray(buffer, size)转成二进制数组反序列化ParseFromString(str)从二进制字符串恢复ParseFromArray(buffer, size)从二进制数组恢复清空字段clear_xxx()清除某个字段
6. Protobuf vs JSON
对比项ProtobufJSON格式二进制文本体积更小更大速度更快较慢可读性不可读二进制可读文本跨语言支持支持适用场景网络通信、存储、RPCAPI、配置文件
基于Protobuf的简单测试
首先写一个proto后缀的文件按照上面的规则
syntaxproto3;//规定语法
package test;//声明作用域//probuf对象类
message stu{int32 age1;string name2;//每个类唯一编码float socre3;
}然后对应命令编译
protoc --cpp_out. test.proto然后得到两个文件 一个是放着一些对应函数接口的.h文件和它对应调用的.c文件。我们只需要包含对应.h然后运行的时候连同.cc一起编译链接即可。
测试代码
#includeiostream
#includetest.pb.hint main(){test::stu s;s.set_age(1);s.set_socre(99.5);s.set_name(马得);std::string str s.SerializeAsString();std::cout序列化结果是: strstd::endl;int rets.ParseFromString(str);if(ret-1) {std::cout反序列化失败std::endl;return -1;}std::couts.name()std::endl;std::couts.socre()std::endl;std::couts.age()std::endl;
}下面编译链接并运行下 简单使用到此为止用到的也就这些。
三.gtest的介绍即简单应用
安装步骤
# 步骤1安装EPEL软件源扩展
sudo yum install epel-release# 步骤2安装DNF包管理器
sudo yum install dnf# 步骤3安装DNF核心插件集
sudo dnf install dnf-plugins-core# 步骤4安装Google Test及其开发包
sudo dnf install gtest gtest-devel基于gtest用法简单介绍
GTest是什么
GTest是Google发布的一个跨平台C单元测试框架。用于在不同平台上编写C单元测试提供丰富断言、致命/非致命判断、参数化等功能对应头文件#includegtest/gtest.h 。
GTest使用 - TEST宏
TEST(test_case_name, test_name)创建简单测试定义测试函数可在其中用C代码和框架断言检查。TEST_F(test_fixture,test_name)用于多样测试当多个测试场景需相同数据配置相同数据测不同行为时使用。
断言
断言宏分两类 ASSERT_系列检测失败则退出当前函数。EXPECT_系列检测失败则继续往下执行。 常用断言介绍 bool值检查 ASSERT_TRUE(参数)期待结果为true。ASSERT_FALSE(参数)期待结果为false。 数值型数据检查 ASSERT_EQ(参数1, 参数2)传入两数比较相等返回true。ASSERT_NE(参数1, 参数2)不相等返回true。ASSERT_LT(参数1, 参数2)小于返回true。ASSERT_GT(参数1, 参数2)大于返回true。ASSERT_LE(参数1, 参数2)小于等于返回true。ASSERT_GE(参数1, 参数2)大于等于返回true。 自定义错误信息若对自动输出错误信息不满意可通过operator在失败时打印自定义日志。
它是基于一套测试用例可以有许多测试用例组 测试程序一个测试程序只有一个 main 函数也可以说是一个可执行程序是一个测试程序。该级别的事件机制是在程序的开始和结束执行测试套件代表一个测试用例的集合体该级别的事件机制是在整体的测试案例开始和结束执行测试用例该级别的事件机制是在每个测试用例开始和结束都执行每个TEST
下面简单的test测试
源码
#includeiostream
#includegtest/gtest.h int abs(int x)
{ return x 0 ? x : -x;
} TEST(abs_test, test1)
{ ASSERT_TRUE(abs(1) 1) abs(1)1; ASSERT_TRUE(abs(-1) 1); ASSERT_FALSE(abs(-2) -2); ASSERT_EQ(abs(1),abs(-1)); ASSERT_NE(abs(-1),0); ASSERT_LT(abs(-1),2); ASSERT_GT(abs(-1),0); ASSERT_LE(abs(-1),2); ASSERT_GE(abs(-1),2);
} int main(int argc,char *argv[])
{ //将命令行参数传递给gtest testing::InitGoogleTest(argc, argv); // 运行所有测试案例 return RUN_ALL_TESTS();
} 运行结果:
可以看到也是非常人性化的 gtest使用的三种情况
1·全局事件
简单来说
定义一个类继承全局类class Globaltest : public testing::Environment实现对应虚函数SetUpTearDown大环境之前调用SetUp函数测试用例组结束后也就是大环境准备析构调用TearDown。main函数中先拿参数初始化gtest然后new下Environment对象再跑测试。
基于Global下的测试
源码
#include iostream
#include gtest/gtest.h
using namespace std;class Globaltest : public testing::Environment
{public:virtual void SetUp() override{std::cout 测试前:提前准备数据!!\n;}virtual void TearDown() override{std::cout 测试结束后:清理数据!!\n;}static size_t cnt;
};size_t Globaltest::cnt 1;TEST(test1, cnt1)
{ASSERT_EQ(Globaltest::cnt, 1);
}TEST(test1, cnt2)
{Globaltest::cnt 2;ASSERT_EQ(Globaltest::cnt, 2);
}int main(int argc, char *argv[])
{testing::AddGlobalTestEnvironment(new Globaltest() );testing::InitGoogleTest(argc, argv);return RUN_ALL_TESTS();
}测试结果 这里可以发现多个测试用例全局值初始化一次环境。
2·testsuite事件
针对一个个测试套件。测试套件的事件机制我们同样需要去创建 一个类继承自 testing::Test实现两个静态函数 SetUpTestCase 和TearDownTestCase测试套件的事件机制不需要像全局事件机制一样在 main 注册而是需要将我们平时使用的 TEST 宏改为 TEST_F 宏然后第一个参数传递我们子类第二个就是每个测试用例名称还有就是不需要new大环境对象了。
基于testsuite下的测试
源码
#include iostream
#include gtest/gtest.h
using namespace std;class Suite : public testing::Test
{public:static void SetUpTestCase(){std::cout 测试前:提前准备数据!!\n;}static void TearDownTestCase(){std::cout 测试结束后:清理数据!!\n;}static size_t cnt;
};size_t Suite::cnt 1;TEST_F(Suite, cnt1)
{ASSERT_EQ(Suite::cnt, 1);cnt;
}TEST_F(Suite, cnt2)
{ASSERT_EQ(Suite::cnt, 2);
}int main(int argc, char *argv[])
{testing::InitGoogleTest(argc, argv);return RUN_ALL_TESTS();
}测试结果 可以发现是在两个测试用例都结束后调用的静态的那俩函数。这样使用可以如上面那样把一个用例对全局的改变让另一个用例看到。 3.testcase事件
TestCase 事件: 针对一个个测试用例。测试用例的事件机制的创建和测试套件的基本一样不同地方在于测试用例实现的两个函数分别是 SetUp 和 TearDown, 这两个函数也不是静态函数。
简单来说就是和testsuite其他地方相同但是比它多了个继承重写的SetUp和TearDown函数然后这俩是每个测试开始前和结束调用而对应之前的SetUpTestCase与TearDownTestCase这俩仍是大环境开始和结束调用了。
基于testcase下的测试
源码
#include iostream
#include gtest/gtest.h
using namespace std;class TestCase : public testing::Test
{public:virtual void SetUp() override{std::cout 每组测试用例前:提前准备数据!!\n;cnt;}virtual void TearDown() override{std::cout 每组测试用例测试结束后:清理数据!!\n;}static void SetUpTestCase(){std::cout 测试前:提前准备数据!!\n;}static void TearDownTestCase(){std::cout 测试结束后:清理数据!!\n;}static size_t cnt;
};size_t TestCase::cnt 0;TEST_F(TestCase, cnt1)
{ASSERT_EQ(TestCase::cnt, 1);
}TEST_F(TestCase, cnt2)
{ASSERT_EQ(TestCase::cnt, 2);
}int main(int argc, char *argv[])
{testing::InitGoogleTest(argc, argv);return RUN_ALL_TESTS();
}测试结果 基于testsuite和testcase总结下
其实这两个用法都是一样的对应SetUpTestCase TearDownTestCase是对于大环境而SetUp TearDown是针对每个测试用例而言只不过我们用testsuite的时候其实就是对整套进行的故只使用SetUpTestCase TearDownTestCase但是当testcase明显用到了测试用例故使用SetUp TearDown但是大环境也要用故其他俩也要用到。
即 testsuite模式只用SetUpTestCase TearDownTestCase来全局初始和清理而testcase 既要用它还要用SetUp TearDown来对每个测试用例进行初始化和销毁。
四.muduo库的介绍及简单使用
Muduo库是陈硕开发的一个用于C的高性能网络编程库主要用于构建高并发的TCP网络应用。以下是对Muduo库的简单介绍
1. 基本定义与设计思想
基于非阻塞IO与事件驱动Muduo采用非阻塞I/O模型配合事件驱动机制能高效处理大量并发连接避免传统阻塞I/O中“一个连接对应一个线程”带来的资源浪费与性能瓶颈。主从Reactor模式它借鉴了Reactor设计模式并做了扩展采用“主从Reactor”架构。主Reactor负责监听新连接当有客户端连接到来时把连接分发给子Reactor子Reactor则专门处理已建立连接的读写事件。
2. 线程模型one loop per threadepoll那里提到过 Muduo的核心线程模型是 “one loop per thread”意思是
每个线程有且只有一个EventLoop事件循环这个EventLoop负责响应定时器事件和I/O事件比如套接字的读、写就绪。一个文件描述符对应一个TCP连接只会由一个线程来读写。也就是说某个TCP连接从建立到断开全程由某一个EventLoop某个线程来管理避免了多线程同时操作同一个连接带来的竞态问题。
3. 核心组件与工作流程
Acceptor负责监听端口、接受新连接。当有客户端connect过来时Acceptor会创建一个新的TcpConnection对象并把这个连接“交给”某个子Reactor去管理。EventLoop每个线程的事件循环中枢不断轮询注册在其上的I/O事件读、写、定时器等一旦有事件就绪就回调对应的处理函数。TcpConnection封装了一条TCP连接的状态与操作比如读数据、写数据、连接建立/断开回调等。每个TcpConnection会绑定到某一个EventLoop上由该EventLoop负责它的读写事件分发。
4. 为什么选择Muduo
高性能非阻塞I/O 事件驱动 主从Reactor one loop per thread这些设计让它在高并发场景下比如成千上万的TCP连接依然能保持较低的延迟与较高的吞吐量。线程安全与易用性Muduo通过合理的线程模型一个连接只由一个线程管理减少了多线程同步的复杂度同时提供了简洁的接口让开发者可以更专注于业务逻辑而非底层网络细节。
5. 简单应用场景
Muduo非常适合用来开发高并发的服务器程序例如
即时通讯服务器IM游戏服务器分布式系统中的RPC框架各种需要处理大量长连接/短连接的网络服务
总之Muduo通过精心设计的线程模型与事件驱动机制在C生态里为高并发TCP网络编程提供了一套高效、易用的解决方案让开发者能以相对低的成本写出性能强劲的网络服务端程序。 6.认识下基于muduo库的server和client端大致流程博主亲自手画通俗易懂
server端流程手绘 client端流程手绘 基于muduo库简单使用
流程如上下面看测试效果模拟实现的业务加法与翻译
client的请求 - 对话结果 符合预期
以上就是对muduo库的简单应用项目用到的也就只有这些把上面的模版搞定后面项目的使用没丝毫难度如果没看懂请看上图秒懂
五.这里追增下关于异步操作的一些知识
std::future
1.与 std::async 配合使用最常用
作用 异步启动一个任务并通过 std::future 获取它的返回值。 用法
用 std::async(std::launch::......, 函数) 启动一个异步任务返回一个 std::futureT 对象。用 future.get() 获取任务结果会阻塞直到结果返回且只能调用一次。 适用场景 想简单异步执行一个函数并获取结果时用。
而它由分为三种选择情况async deferred 不选。
默认不选择选项这里
这里就是直接就是主线程去执行再去获取没有异步线程的意思。
代码如下
#include thread
#include future
#include chrono
#include iostream
int Add(int num1, int num2)
{//std::this_thread::sleep_for(std::chrono::seconds(1));std::cout 加法1111\n;// std::this_thread::sleep_for(std::chrono::seconds(5)); //无std::cout 加法2222\n;return num1 num2;
}int main()
{std::cout --------1----------\n;std::futureint result std::async(Add, 11, 22); // 派线程去执行并把结果保存到result里(但是不能保证访问这个函数是异步安全的)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;测试效果 async选项
派线程去执行并把结果保存到result里(但是不能保证访问这个函数是异步安全的)。
代码如下
#include thread
#include future
#include chrono
#include iostream
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()
{////// async: 可以发现这个函数是立刻被执行而不是等到调用get的时候std::cout --------1----------\n;std::futureint result std::async(std::launch::async, Add, 11, 22); // 派异步线程去执行并把结果保存到result里立刻执行std::this_thread::sleep_for(std::chrono::seconds(2)); // 主线程休眠std::cout --------2----------\n;int sum result.get(); // std::cout --------3----------\n;std::cout sum std::endl;测试效果 异步进行符合预期。
deferred选项
保存起这个任务到result里面等到外界调用get才去执行也就是这个异步线程只有主线程get后才去执行任务。
测试代码
#include thread
#include future
#include chrono
#include iostream
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()
{////deferredstd::cout --------1----------\n;std::futureint result std::async(std::launch::deferred, Add, 11, 22); // 保存起这个任务到result里面等到外界调用get才去执行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;
}测试效果 符合预期。 2.与 std::promise 配合使用
作用 手动设置一个值让其他线程或代码通过 std::future 获取这个值。 用法
创建一个 std::promiseT 对象。通过 promise.set_value(值) 设置结果。用 promise.get_future() 得到关联的 std::futureT之后用 future.get() 获取值。 适用场景 当你在一个线程中计算或得到某个值想手动“传递”给另一个线程时用。 也就是说让一个future的对象保存promise对象未来的值然后promise对象只要设置了后就能被future对象get到。 测试代码
#include iostream
#include thread
#include future// 通过在线程中对promise对象设置数据其他线程中通过future获取设置数据的方式实现获取异步任务执行结果的功能
void Add(int num1, int num2, std::promiseint prom)
{std::this_thread::sleep_for(std::chrono::seconds(3));prom.set_value(num1 num2);//设置对应的值方便fu提取return;
}int main()
{ //操作 fu里面保存prom里面设置的值到时候fu调用get如果prom还没设置就等待设置完就得到std::promiseint prom;std::futureint fu prom.get_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;
}测试效果 可以发现是被设置完成之后才能get到对应的值。 3.与 std::packaged_task 配合使用
作用 把一个函数打包成一个任务异步执行后通过 std::future 获取返回值。 用法
把某个函数比如普通函数、lambda包装进 std::packaged_taskT(参数类型...)。调用 packaged_task(参数) 执行任务并通过 get_future() 得到 std::futureT。之后用 future.get() 获取返回值。 适用场景 想把某个函数变成可异步调用的“任务”并获取其结果时用。 同样这里是把对应的函数封装成task对象然后能直接调用还是通过future对象获得结果。 但是这样需要注意如果传递的是线程执行线程执行的是函数因此需要把task封装成lamda等。
测试代码
#include iostream
#include thread
#include future
#include memory
//pakcaged_task的使用
// pakcaged_task 是一个模板类实例化的对象可以对一个函数进行二次封装
//pakcaged_task可以通过get_future获取一个future对象来获取封装的这个函数的异步执行结果 --只是可调用对象不嫩当做普通函数使用int Add(int num1, int num2) {std::this_thread::sleep_for(std::chrono::seconds(3));return num1 num2;
}int main(){std::packaged_taskint(int,int) pt(Add);std::futureint f pt.get_future();pt(1,2);//派异步线程去执行std::coutf.get()std::endl;//但是不能直接当成函数传给线程只能封装auto ptask std::make_sharedstd::packaged_taskint(int,int) (Add);std::futureint ff ptask-get_future();// std::thread t((*(Add))(1,1));std::thread t([ptask](){ (*ptask)(1,2);//类似穿了个普通函数然后调用的可调用对象});std::coutff.get()std::endl;t.join();return 0;
}测试效果 符合预期但是如果把这个task对象直接传给线程呢 发现这里是不允许的因此需要封装一下。 总结一句话 std::future 就像一张“取货单”你可以用它从别的地方比如另一个线程取回一个结果具体怎么“送货”有三种方式std::async自动送、std::promise手动送、std::packaged_task把函数打包送。
4.基于future的packaged_task的用法实现多参线程池
实现要点
多线程基于互斥锁与条件变量的帮助下实现同步从任务队列提取任务。基于future的packaged_task的用法以及decltype实现任务push操作。执行任务的时候一个线程进入后把所有任务都拿走然后解锁去执行自己的任务采取数组swap方法线程局部存储的应用来实现。其他详细见代码。
操作流程 代码实现
#include iostream
#include functional
#include memory
#include thread
#include future
#include mutex
#include condition_variable
#include vector
#include atomicclass Threadpool
{public:using func std::functionvoid(void);void Stop() // 如果外界直接调用stop还会进行析构就会出问题需要避免{if (_stop true) // 可以保证里面线程安全return;_stop true;_con.notify_all();for (auto thread : _threads){thread.join();}}Threadpool(int thr_count 1) : _stop(false){for (int i 0; i thr_count; i){_threads.emplace_back(Threadpool::Entry, this);}}~Threadpool(){Stop();}template typename pfunc, typename... Args//自动推导类型保证完美转发的成功auto Push(pfunc pf, Args ... args) - std::futuredecltype(pf(args...)){auto bind_task std::bind(std::forwardpfunc(pf), std::forward Args(args)...);using re_type decltype(pf(args...));auto ptask std::make_sharedstd::packaged_taskre_type(void)(bind_task);std::futurere_type fu ptask-get_future();//保存对应的可调用对象调用完后的返回值{std::unique_lockstd::mutex lock(_mutex);_tasks.push_back([ptask](){ (*ptask)(); });_con.notify_one();}return fu;}private:void Entry(){//这里让每个线程全部取出当前任务数组的任务去执行无论多少全部取完while (!_stop){std::vectorfunc tmp;//这里是个声明{std::unique_lockstd::mutex lock(_mutex);_con.wait(lock, [this](){ return _stop || !_tasks.empty(); });tmp.swap(_tasks);//交换后当线程1在进行对应任务的时候放入进_tasks任务不会落空}//因为tmp是线程局部存储所以多线程遍历的时候不会出问题拿到的都是自己的for (auto task : tmp){task();}}}std::atomicbool _stop;std::mutex _mutex;std::condition_variable _con;std::vectorfunc _tasks;std::vectorstd::thread _threads;
};int Add(int num1, int num2)
{return num1 num2;
}int main()
{Threadpool pool(5);for (int i 0; i 10; i){std::futureint fu pool.Push(Add, 11, i);std::cout fu.get() std::endl;}pool.Stop();return 0;
}简单测试
十个线程放出去执行加法任务符合预期。
六.关于项目通用类的设计
1·字符串分割操作
因为在进行路由是否匹配过程需要
如下
class Split
{public:static size_t split(std::string s, std::string sep, std::vectorstd::string res){int idx 0, pos 0;while (idx s.size()){pos s.find(sep.c_str(), idx);// 如果找不到就是到头了:if (pos std::string::npos){res.push_back(s.substr(idx));break;}else{// 防止找到重复的sep并跳过sep长度if (pos idx){idx pos sep.size();continue;}else{res.push_back(s.substr(idx, pos - idx));idx pos sep.size();}}}return res.size();}
};
2.UUID生成
定义与组成UUIDUniversally Unique Identifier即通用唯一识别码通常由32位16进制数字字符组成。标准型式标准形式包含32个16进制数字字符以连字号分为五段形式为8 - 4 - 4 - 4 - 12的32个字符例如550e8400 - e29b - 41d4 - a716 - 446655440000。生成方式及目的通过生成8个随机数字十六进制则十六个和8字节序号共16字节数组来生成32位16进制字符组合形式既确保全局唯一又能够根据序号分辨数据。这里我们采取C相关生成库和对应IO流完成详细见代码
代码生成
class Uuid
{public:static std::string uuid(){std::random_device r; // 生成一个机器随机数效率较低std::mt19937_64 mt(r()); // 通过梅森旋转算法生成一个伪随机数(把种子穿进去)std::uniform_int_distributionint d(0, 255); // 把随机数区间确定std::stringstream ss;// 进行前16个数生成for (int i 0; i 7; i){ss std::setw(2) std::setfill(0) std::hex d(mt);if (i 3 || i 5 || i 7){ss -;}}//统计个数十六进制static std::atomicsize_t cnt (1);//保证原子性 线程安全 64位也就是16个16进制size_t num cnt.fetch_add(1);for (int i 7; i 0; i--){//0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1000 0001---这里采取每次取前八位转成16进制形式。ss std::setw(2) std::setfill(0) std::hex ((num (i * 8)) 0xff);if (i 6)ss -;}return ss.str();}
};3.文件操作
对应接口功能
代码实现
class File
{public:File(std::string name) : _file(name) {}bool Exists(){ //既能判断目录又能判断文件struct stat st;return (stat(_file.c_str(), st) 0);}size_t Size(){struct stat st;int ret stat(_file.c_str(), st);if (ret 0){return 0;}return st.st_size;}bool Read(char *buff, size_t offset, size_t num){std::ifstream ifs;ifs.open(_file, std::ios::binary | std::ios::in);if (ifs.is_open() false){Elog(%s 文件打开失败, _file.c_str());return false;}ifs.seekg(offset, std::ios::beg);ifs.read(buff, num);if (ifs.good() false){Elog(%s 文件读取数据失败, _file.c_str());ifs.close();return false;}ifs.close();return true;}// 一口气全部读出bool Read(std::string buff){size_t size this-Size();// 及时更新buff对应的尺寸buff.resize(size);return Read(buff[0], 0, size);}bool Write(const char *buff, size_t offset, size_t num){std::fstream ios;//偏移文件指针必须有读权限ios.open(_file, std::ios::binary | std::ios::in | std::ios::out);if (ios.is_open() false){Elog(%s 文件打开失败, _file.c_str());return false;}ios.seekp(offset, std::ios::beg);ios.write(buff, num);if (ios.good() false){Elog(%s 文件写入数据失败, _file.c_str());ios.close();return false;}ios.close();return true;}bool Write(const std::string buff){return Write(buff.c_str(), 0, buff.size());}static bool CreateFile(const std::string name){std::fstream ios;ios.open(name.c_str(), std::ios::binary | std::ios::out);if (ios.is_open() false){Elog(%s 文件打开失败, name.c_str());return false;}ios.close();return true;}static std::string GetDir(const std::string filename){// /aaa/bb/ccc/ddd/test.txtsize_t pos filename.find_last_of(/);if (pos std::string::npos){// test.txtreturn ./;}std::string path filename.substr(0, pos);return path;}static bool CreateDir(const std::string path)// /aa/bb/ccc/ddd{size_t idx0, pos 0;while (idx path.size()){pos path.find(/, idx);// 找到最后一个了if (pos std::string::npos){return (mkdir(path.c_str(), 0775) 0);}std::string tmp path.substr(0, pos);int ret mkdir(tmp.c_str(), 0775);// 有可能当前目录存在此时ret也返回非0但是error会被标记EEXISTif (!ret errno ! EEXIST){Elog(创建目录 %s 失败: %s, path.c_str(), strerror(errno));return false;}idx pos 1;}return true;}bool Rename(const std::string nname){return (::rename(_file.c_str(), nname.c_str()) 0);}static bool RemoveFile(const std::string filename){return (::remove(filename.c_str()) 0);}static bool RemoveDir(const std::string dir){const std::string s rm -rf dir;return (system(s.c_str()) ! -1);}private:std::string _file; // 传入完整文件名字
};4·SQlite3相关操作
之前封装过这里就不再多拿了。
七.本篇小结
本篇介绍了基于消息队列实现的时候用到的四个库和一些新知识通过对他们的基本了解大致工作流程已经有了一定认识下篇就开始来正式实现仿RabbitMQ实现消息队列。