背景前提:
我们能不能编写一个qq的自定义客户端/某游戏的客户端/xxx自定义客户端
不能,因为他们没有公开自己使用的自定义协议,虽然没公开,但是网络上有些大神(开源项目),实现了自定制的qq客户端,(可以通过一些抓包/逆向手段,拆测qq的应用层协议是啥样)
这个过程全靠猜,取决于水平+运气

开发客户端和开发服务器的人都清楚的知道协议的细节
如果我们想要开发一个redis客户端,也需要知道redis的应用层协议,这里redis官网上就有
RESP协议

resp讲这个主要是希望大家能够了解redis底层的通信原理,即使不了解,也没关系,不影响后续代码的编写
优点:
1.简单好实现
2.快速进行解析
3.肉眼可读
传输层这里基于tcp,但是和tcp又没有强耦合
请求和响应之间的通信模型是一问一答的形式,客户端给服务器一个请求,服务器返回一个响应


通过字符串一开始的标识符确定是什么数据类型,RESP 是 Redis 客户端和服务器之间通信所使用的协议,这样的设计让数据类型的识别简单高效,方便客户端和服务器对数据进行解析和处理。
因此,redis客户端要做的工作
1.按照上述格式,构造出字符串,往socket中写入
2.从socket中读取字符串,按照上述格式解析
一会写代码,不是真的需要按照上诉的协议,解析/构造字符串
已经有很多大佬,实现了这套协议的解析/构造
只要使用这些大佬提供的库,就可以比较简单方便的来完成和redis服务器通信的操作了
使用redis-plus-plus库
c++操作redis的库有很多,这里使用redis-plus-plus(github上有)
虽然库多,但是大同小异

对于库的安装,不同的环境可能有差异,自行去查找资料进行安装
redis的通用命令的使用
set命令



第一个参数是key
第二个参数是val,类型就是使用了c++17中的string_view,是一种轻量级的、非拥有型的字符串视图,常用于提高字符串操作的效率和安全性。
第三个参数是ttl,就是一个过期时间,默认是一个0,不过期,如果是keepttl就是表示是否保留原来的过期时间,主要是当key存在的时候采用
第四个参数是type,默认值为UpdateType::ALWAYS,表示默认情况按照always这种更新策略操作,always就是你传入的值来更新键
这两个重载函数通过不同的参数设计,提供了更灵活的方式来设置 Redis 键值对以及对过期时间进行处理。
OptionalString补充讲解
![]()
![]()
也就是OptionalString是一个std::optional<string>类的具体化
#include <sw/redis++/redis++.h>
#include <iostream>
int main() {
sw::redis::Redis redis("tcp://127.0.0.1:6379");
// 调用 GET 命令,返回值类型为 OptionalString
sw::redis::OptionalString val = redis.get("mykey");
if (val.has_value()) { // 检查是否有值(即键存在)
std::cout << "Value: " << val.value() << std::endl; // 获取字符串值
} else {
std::cout << "Key does not exist" << std::endl; // 无值(键不存在)
}
return 0;
}
这个类是用来返回值的,里面有个方法,
has_value():返回 bool,表示是否包含有效字符串(true 表示有值,false 表示 nil)。
value():返回包含的 std::string(若 has_value() 为 false,调用此方法会抛出 std::bad_optional_access 异常)。
value_or(default_val):若有值则返回该值,否则返回指定的默认字符串(安全获取值的方式,避免异常)。
OptionalString 是 redis-plus-plus 中处理 “可能为 nil 的字符串结果” 的关键类型,通过它可以清晰地区分 Redis 命令返回的 “有效字符串” 和 “nil”(键不存在),避免了使用 nullptr 或特殊字符串(如空字符串)来表示 “不存在” 时的歧义,是一种更类型安全的设计。避免用 nullptr 或空字符串等模糊的方式表示 “值不存在”,从而消除歧义。
get命令

![]()
所以这个get返回的是OptionalString类型,所以我们使用get的时候,要判断返回类型,使用has_value,如果是true,在打印 value



这里可以看到我们已经存入了key1-value1
注意如果我们刚刚返回值为false,我们还调用了value就会抛异常,抛异常两种办法
1.要么捕捉try catch(这个会有性能开销,对于追求极致性能不太友好)
2.要么就是判断为true,在调用value
exists命令

第二个是迭代器版本,第三个是多个key的版本(C++11之后的语法,支持传入多个参数,允许用逗号分隔的列表直接初始化容器或作为函数参数,本质上是一个临时的 “元素集合”。)
返回值long long,0表示都不存在,返回几就表示有多少个存在
和其他 Redis 操作一样,exists 命令在执行过程中可能会遇到网络问题、Redis 服务不可用等情况,此时会抛出 sw::redis::Error 异常或者其他 std::exception 派生类的异常。因此,需要使用 try-catch 块来捕获并处理这些异常,以保证程序的稳定性。
flushall执行的顺序(补充)
有时候我们在进行一小部分测试的时候,为了避免数据干扰,会执行flushall
但是执行flushall是应该放在小测试test的前面呢?还是后面?
选择放在前面,因为如果这个测试中间,有个exists命令之类的会抛出异常,就会导致代码异常终止,然后放在后面的话就清理不了数据,就会对别的测试造成数据污染,把清理操作放在测试开始时,测试执行过程中产生的数据会保留到测试结束。这方便开发者人工检查 Redis 中的数据内容,验证测试执行后的数据是否符合预期
简单来说,核心是通过 “前置清理”,既保证每次测试有干净的初始环境(避免历史残留干扰),又能在测试后保留数据供人工验证,同时还能规避 “异常导致清理失败、数据残留” 的问题。
keys命令

![]()
两个版本的区别就是第一个需要在内部构造,然后返回出去还有拷贝一遍,第二个直接你输入输出型参数,这样能够减少性能消耗
redis-plus-plus 中 keys 命令的重载函数明确要求第二个参数是输出迭代器(Output Iterator)
如果你传入一个普通的容器是无法保存的,编译器会提示 “没有匹配的函数重载”
一般来说使用第二种,在c++中一般有五种迭代器,输入、输出、前向、双向、随机访问
这里output我们使用的是插入迭代器(插入是一种特殊输出迭代器),插入迭代器就是代表了一个位置+动作
插入迭代器:front_insert_iterator、back_insert_iterator、insert_iterator
这里我们使用back_insert_iterator(),相当于传入一个容器+push_back()
//key的第二个参数是一个“插入迭代器”,需要先准备好一个保存结果的容器
//接下来再创建一个插入迭代器指向容器的位置,
//就可以把keys获取到的结果依次通过刚才的插入迭代器插入到容器的指定位置中
//void keys<Output>(const sw::redis::StringView &pattern, Output output)
vector<string> result;
auto it = std::back_insert_iterator(result);
redis.keys("*",it);
for(const auto& elem:result{
std::cout<<elem<<std::endl;
}
思考:为什么不直接传入一个容器?让keys内部直接操作容器就行,为什么还要传插入迭代器?
为了解耦合,如果你固定了写法vector,那用户就只能传入vector,就不能传入list了
所以为了用户传入什么类型都能接受,使用插入迭代器来解耦合
expire命令

- 第一个函数使用原始的长整型数值,单位固定为秒;
- 第二个函数使用
std::chrono::seconds类型,类型更明确,在代码可读性和类型安全性上更有优势,尤其是在与其他使用std::chrono时间类型的代码交互时,更加统一和方便。
type命令

判断一个key的类型
string类型命令

list类型的命令




OptionalStringPair是optional<std::pair<std::string,std::string>>的实例化
也就是我们正常删除的时候会返回删除的list和对应的元素,也就是两个string,所以这里内部使用了两个string来表示

set类型的命令

这里的back_insert_iterator迭代器是绑定了push_back的,只有容器有push_back才能用,但是set又没有,所以这里采用了insert_iterator,这里相当于调用insert而不是push_back
std::set<std::string> my_set;
// insert_iterator 绑定到 my_set,并指定插入位置(这里用 begin() 作为提示)
auto it = std::insert_iterator<std::set<std::string>>(my_set, my_set.begin());
*it = "apple"; // 等价于 my_set.insert("apple");
*it = "banana"; // 等价于 my_set.insert("banana");
hash类型的命令

zset类型的命令

注意:redis-plus-plus中的命令基本都是统一的形式,提供的和命令行的基本都是一致的,遇到不懂的去查文档即可,不必每个都进行记忆,把常用的操作多敲几遍自然会记住
2262

被折叠的 条评论
为什么被折叠?



