一、Redis的自定义网络协议
1.1 为什么可以编写出一个自定义的Redis客户端
为什么我们可以编写出一个自定义的Redis客户端?因为Redis公开了自己的自定义协议。而对于一些其他软件的客户端,我们无法编写出一个自定义的Redis客户端,因为他们没有公开自己的自定义协议,但是我们可以通过一些抓包/逆向的手段猜测其应用层协议是什么样子的!!
在网络通信过程中,会用到很多的“协议”,比如数据链路层的以太网协议,网络层的IP协议,传输层的TCP/UDP协议,应用层的协议更多!!虽然业界中有很多成熟的应用层协议:HTTP等~~但是此处更多的时候,都会“自定义”应用层协议,Redis此处的应用层协议就是自定义的协议(传输层还是基于TCP)~~
1.2 RESP protocol spec
RESP 协议的优点:
- 简单好实现
- 快速进行解析
- 肉眼可读
传输层这里基于TCP,但是又和TCP没有强耦合。
请求和响应之间的通信模型是一问一答的形式~~(客户端给服务器发送一个请求,服务器返回一个响应)
我们需要根据上述规则进行字符串的编写,然后将这个字符串写入到 tcp socket 中。因此,redis客户端服务器要做的工作是:
- 按照上述格式,构造出字符串,往 socket 中写入
- 从 socket 中读取字符串,按照上述格式解析
二、安装 redis-plus-plus
由于 redis-plus-plus 依赖了 hiredis(C语言版本的redis客户端库),所以我们要先下载 hiredis 库(可以直接使用包管理器来安装)。
但是 redis-plus-plus 本体,只能编译安装了。如果是编译安装,使用 Ubuntu 比 使用 centos 简单很多。redis-plus-plus 本身功能比较简陋,比较原始,写起来也比较麻烦,实际开发中很少会手写 makefile。通过程序来生成 makefile,cmake就是一个生成 makefile 的工具。
先来看一下如何使用 CMake 编译程序:
- 创建一个 build 目录是习惯做法,并非是必须,目的是为了让编译生成的临时文件都放到build下面,避免污染源代码目录~
- cmake .. 这个操作是生成 makefile,此处的 .. 指向的是刚才 CMakeLists.txt 文件所在的目录~~
- make 进行编译
- make install 把刚才的库拷贝到系统目录
三、进行 ping 命令验证
#include <sw/redis++/redis++.h>
1.包含 redis-plus-plus 的头文件,如果我们不知道将这个库下载到哪里,我们可以使用 find 命令进行查找,命令如下所示:
find /XXX(路径) -name XXX
2.创建了一个 Redis 对象
sw::redis::Redis redis("tcp://127.0.0.1:6379);
3.进行 ping 命令
4.使用 Makefile 编译程序
编译程序的时候,需要引入一些库文件(需要知道这些库文件的目录):
- redis++ 自己的静态库
- hiredis 的静态库
- 线程库
四、Redis 的通用命令的使用
一览整个Redis通用命令的使用:
get/set
exists
del
keys
expire/ttl
type
4.1 get/set
在 C++ 中,std::string 是可以修改,既能读,也能写。但是 StringView 是只读的(不能修改),针对只读操作,做很多的优化工作,效率比 std::string 更高。在 C++17 标准库中,也提供了一个 std::string_view。这里是为了兼容 C++11,14,17,所以自己封装了一个类型。StringView 中的各个操作和string类似,只不过只是包含了一些只读方法。
在 Java 中的 String 就是类似于 StringView 只读的,Java 中要想使用可修改的字符串,要使用 StringBuilder 或者 StringBuffer。
对于 get 来说,有可能获取不到元素,这时应该返回什么类型呢??
如果直接使用 std::string 来表示,不方便来表现这个 nil(无效值),如果使用 std::string* 来表示,是可以使用 nullptr 表示无效的,但是返回指针又涉及到内存归谁管~~
因此,作者就自己封装了一个类型,此处的 Optional 可以表示 “非法值” 或者 “无效值”。在 Boost 中,很早就引入了 optional 类型,C++14版本中,就正式归纳标准库了。
在使用 Optional 类型的时候,有可能出现以下这个错误:
此处不需要给这个 Optional 类型搞一个 << 重载,只需要把 Optional 里面包含的元素取出来即可~~
void test1(sw::redis::Redis& redis) {
std::cout << "get 和 set 的使用" << std::endl;
// 清空一下数据库, 避免之前残留的数据有干扰.
redis.flushall();
// 使用 set 设置 key
redis.set("key1", "111");
redis.set("key2", "222");
redis.set("key3", "333");
// 使用 get 获取到 key 对应的 value
auto value1 = redis.get("key1");
// optional 可以隐式转成 bool 类型, 可以直接在 if 中判定. 如果是无效元素, 就是返回 false
if (value1) {
std::cout << "value1=" << value1.value() << std::endl;
}
auto value2 = redis.get("key2");
if (value2) {
std::cout << "value2=" << value2.value() << std::endl;
}
auto value3 = redis.get("key3");
if (value3) {
std::cout << "value3=" << value3.value() << std::endl;
}
auto value4 = redis.get("key4");
if (value4) {
std::cout << "value4=" << value4.value() << std::endl;
}
}
4.2 exists
void test2(sw::redis::Redis& redis) {
std::cout << "exists" << std::endl;
redis.flushall();
redis.set("key", "111");
redis.set("key3", "111");
auto ret = redis.exists("key");
std::cout << ret << std::endl;
ret = redis.exists("key2");
std::cout << ret << std::endl;
ret = redis.exists({"key", "key2", "key3"});
std::cout << ret << std::endl;
}
对于 exists 命令来说,我们可以一次性查看多个键值,我们可以使用初始化列表传参,代码如下:
redis.exists({"key", "key2", "key3"});
4.3 del
void test3(sw::redis::Redis& redis) {
std::cout << "del" << std::endl;
// 清除库非常必要的!
redis.flushall();
redis.set("key", "111");
redis.set("key2", "111");
// redis.del("key");
auto ret = redis.del({"key", "key2", "key3"});
std::cout << ret << std::endl;
ret = redis.exists({"key", "key2"});
std::cout << ret << std::endl;
}
4.4 keys
keys 命令不可以随便使用,否则会影响其他命令的执行,因为 Redis 是单线程。keys 的返回值有多个。其返回值类型为:
这是插入迭代器,插入迭代器的本质是一种“输出迭代器”,通常,一个输出迭代器主要表示一个位置。插入迭代器,则是“位置” + “动作”。 插入迭代器总共有三种类型:
这里直接使用容器作为参数,keys内部直接操作容器,进行插入不是更好吗,为什么要通过迭代器呢??
因为可以解耦合。
void test4(sw::redis::Redis& redis) {
std::cout << "keys" << std::endl;
redis.flushall();
redis.set("key", "111");
redis.set("key2", "222");
redis.set("key3", "333");
redis.set("key4", "444");
redis.set("key5", "555");
redis.set("key6", "666");
// keys 的第二个参数, 是一个 "插入迭代器". 咱们需要先准备好一个保存结果的容器.
// 接下来再创建一个插入迭代器指向容器的位置. 就可以把 keys 获取到的结果依次通过刚才的插入迭代器插入到容器的指定位置中了.
vector<string> result;
auto it = std::back_inserter(result);
redis.keys("*", it);
printContainer(result);
}
4.5 expire/ttl
void test5(sw::redis::Redis& redis) {
using namespace std::chrono_literals;
std::cout << "expire and ttl" << std::endl;
redis.flushall();
redis.set("key", "111");
// 10s => std::chrono::seconds(10)
redis.expire("key", 10s);
std::this_thread::sleep_for(3s);
auto time = redis.ttl("key");
std::cout << time << std::endl;
}
在使用睡眠函数的时候,由于不同系统之间的单位不同,我们更好的选择是使用线程库中的睡眠函数:sleep_for。
Linux的sleep和Windows的Sleep,都属于系统函数,是和系统相关的,同样的功能,在不同系统中可能是完全不同的函数~
4.6 type
void test6(sw::redis::Redis& redis) {
std::cout << "type" << std::endl;
redis.flushall();
redis.set("key", "111");
string result = redis.type("key");
std::cout << "key: " << result << std::endl;
redis.lpush("key2", "111");
result = redis.type("key2");
std::cout << "key2: " << result << std::endl;
redis.hset("key3", "aaa", "111");
result = redis.type("key3");
std::cout << "key3: " << result << std::endl;
redis.sadd("key4", "aaa");
result = redis.type("key4");
std::cout << "key4: " << result << std::endl;
redis.zadd("key5", "吕布", 99);
result = redis.type("key5");
std::cout << "key5: " << result << std::endl;
}
五、string类型的操作
5.1 get/set
void test1(Redis& redis) {
std::cout << "get 和 set" << std::endl;
redis.flushall();
redis.set("key", "111");
auto value = redis.get("key");
if (value) {
std::cout << "value: " << value.value() << std::endl;
}
redis.set("key", "222");
value = redis.get("key");
if (value) {
std::cout << "value: " << value.value() << std::endl;
}
}
5.2 set带有超时时间
void test2(Redis& redis) {
std::cout << "set 带有超时时间" << std::endl;
redis.flushall();
redis.set("key", "111", 10s);
std::this_thread::sleep_for(3s);
long long time = redis.ttl("key");
std::cout << "time: " << time << std::endl;
}
5.3 set NX/XX
void test3(Redis& redis) {
std::cout << "set NX 和 XX" << std::endl;
redis.flushall();
redis.set("key", "111");
// set 的重载版本中, 没有单独提供 NX 和 XX 的版本, 必须搭配过期时间的版本来使用.
redis.set("key", "222", 0s, sw::redis::UpdateType::EXIST);
auto value = redis.get("key");
if (value) {
std::cout << "value: " << value.value() << std::endl;
} else {
std::cout << "key 不存在!" << std::endl;
}
}
5.4 mset
void test4(Redis& redis) {
std::cout << "mset" << std::endl;
redis.flushall();
// 第一种写法, 使用初始化列表描述多个键值对
// redis.mset({ std::make_pair("key1", "111"), std::make_pair("key2", "222"), std::make_pair("key3", "333") });
// 第二种写法, 可以把多个键值对提前组织到容器中. 以迭代器的形式告诉 mset
vector<std::pair<string, string>> keys = {
{"key1", "111"},
{"key2", "222"},
{"key3", "333"}
};
redis.mset(keys.begin(), keys.end());
auto value = redis.get("key1");
if (value) {
std::cout << "value: " << value.value() << std::endl;
}
value = redis.get("key2");
if (value) {
std::cout << "value: " << value.value() << std::endl;
}
value = redis.get("key3");
if (value) {
std::cout << "value: " << value.value() << std::endl;
}
}
5.5 mget
void test5(Redis& redis) {
std::cout << "mget" << std::endl;
redis.flushall();
vector<std::pair<string, string>> keys = {
{"key1", "111"},
{"key2", "222"},
{"key3", "333"}
};
redis.mset(keys.begin(), keys.end());
vector<sw::redis::OptionalString> result;
auto it = std::back_inserter(result);
redis.mget({"key1", "key2", "key3", "key4"}, it);
printContainerOptional(result);
}
5.6 getrange/setrange
void test6(Redis& redis) {
std::cout << "getrange 和 setrange" << std::endl;
redis.flushall();
redis.set("key", "abcdefghijk");
string result = redis.getrange("key", 2, 5);
std::cout << "result: " << result << std::endl;
redis.setrange("key", 2, "xyz");
auto value = redis.get("key");
std::cout << "value: " << value.value() << std::endl;
}
5.7 incr/decr
incr 和 decr 得到的是 long long 类型(使用这个更多一些),get 得到的是 OptionalString 类型,需要手动转成数字~C++中把字符串转成数字,也有很多种方法。
void test7(Redis& redis) {
std::cout << "incr 和 decr" << std::endl;
redis.flushall();
redis.set("key", "100");
long long result = redis.incr("key");
std::cout << "result: " << result << std::endl;
auto value = redis.get("key");
std::cout << "value: " << value.value() << std::endl;
result = redis.decr("key");
std::cout << "result: " << result << std::endl;
value = redis.get("key");
std::cout << "value: " << value.value() << std::endl;
}
六、list类型的操作
6.1 lpush/prange
void test1(Redis& redis) {
std::cout << "lpush 和 lrange" << std::endl;
redis.flushall();
// 插入单个元素
redis.lpush("key", "111");
// 插入一组元素, 基于初始化列表
redis.lpush("key", {"222", "333", "444"});
// 插入一组元素, 基于迭代器
vector<string> values = {"555", "666", "777"};
redis.lpush("key", values.begin(), values.end());
// lrange 获取到列表中的元素
vector<string> results;
auto it = std::back_inserter(results);
redis.lrange("key", 0, -1, it);
printContainer(results);
}
6.2 rpush
void test2(Redis& redis) {
std::cout << "rpush" << std::endl;
redis.flushall();
// 插入单个元素
redis.rpush("key", "111");
// 插入多个元素, 基于初始化列表
redis.rpush("key", {"222", "333", "444"});
// 插入多个元素, 基于容器
vector<string> values = {"555", "666", "777"};
redis.rpush("key", values.begin(), values.end());
// 使用 lrange 获取元素
vector<string> results;
auto it = std::back_inserter(results);
redis.lrange("key", 0, -1, it);
printContainer(results);
}
6.3 lpop/rpop
void test3(Redis& redis) {
std::cout << "lpop 和 rpop" << std::endl;
redis.flushall();
// 构造一个 list
redis.rpush("key", {"1", "2", "3", "4"});
auto result = redis.lpop("key");
if (result) {
std::cout << "lpop: " << result.value() << std::endl;
}
result = redis.rpop("key");
if (result) {
std::cout << "rpop: " << result.value() << std::endl;
}
}
6.4 blpop
void test4(Redis& redis) {
using namespace std::chrono_literals;
std::cout << "blpop" << std::endl;
redis.flushall();
auto result = redis.blpop({"key", "key2", "key3"}, 10s);
if (result) {
std::cout << "key:" << result->first << std::endl;
std::cout << "elem:" << result->second << std::endl;
} else {
std::cout << "result 无效!" << std::endl;
}
}
6.5 llen
void test5(Redis& redis) {
std::cout << "llen" << std::endl;
redis.flushall();
redis.lpush("key", {"111", "222", "333", "444"});
long long len = redis.llen("key");
std::cout << "len: " << len << std::endl;
}
对于 redis-plus-plus 这个库来说,接口风格的设计是非常统一的。
- 当一个函数参数需要传递多个值的时候,往往都是支持初始化列表或者一对迭代器的方式来进行实现的
- 当一个函数的返回值需要表示多个数据的时候,也往往会借助插入迭代器来实现往一个容器中添加元素的效果
- 当某些场景涉及到无效值的时候,往往会搭配 std::optional 来使用
七、set类型的操作
7.1 sadd/smembers
void test1(Redis& redis) {
std::cout << "sadd 和 smembers" << std::endl;
redis.flushall();
// 一次添加一个元素
redis.sadd("key", "111");
// 一次添加多个元素(使用初始化列表)
redis.sadd("key", {"222", "333", "444"});
// 一次添加多个元素(使用迭代器)
set<string> elems = {"555", "666", "777"};
redis.sadd("key", elems.begin(), elems.end());
// 获取到上述元素
// 此处用来保存 smembers 的结果, 使用 set 可能更合适.
vector<string> result;
// auto it = std::back_inserter(result);
// 由于此处 set 里的元素顺序是固定的. 指定一个 result.end() 或者 result.begin() 或者其他位置的迭代器, 都无所谓~~
auto it = std::inserter(result, result.end());
redis.smembers("key", it);
printContainer(result);
}
7.2 sismember
void test2(Redis& redis) {
std::cout << "sismember" << std::endl;
redis.flushall();
redis.sadd("key", {"111", "222", "333", "444"});
bool result = redis.sismember("key", "555");
std::cout << "result: " << result << std::endl;
}
7.3 scrad
void test3(Redis& redis) {
std::cout << "scard" << std::endl;
redis.flushall();
redis.sadd("key", {"111", "222", "333"});
long long result = redis.scard("key");
std::cout << "result: " << result << std::endl;
}
7.4 spop
void test4(Redis& redis) {
std::cout << "spop" << std::endl;
redis.flushall();
redis.sadd("key", {"111", "222", "333", "444"});
auto result = redis.spop("key");
if (result) {
std::cout << "result: " << result.value() << std::endl;
} else {
std::cout << "result 无效!" << std::endl;
}
}
7.5 sinter
void test5(Redis& redis) {
std::cout << "sinter" << std::endl;
redis.flushall();
redis.sadd("key1", {"111", "222", "333"});
redis.sadd("key2", {"111", "222", "444"});
set<string> result;
auto it = std::inserter(result, result.end());
redis.sinter({"key1", "key2"}, it);
printContainer(result);
}
7.6 sinterstore
void test6(Redis& redis) {
std::cout << "sinterstore" << std::endl;
redis.flushall();
redis.sadd("key1", {"111", "222", "333"});
redis.sadd("key2", {"111", "222", "444"});
long long len = redis.sinterstore("key3", {"key1", "key2"});
std::cout << "len: " << len << std::endl;
set<string> result;
auto it = std::inserter(result, result.end());
redis.smembers("key3", it);
printContainer(result);
}
八、hash类型的操作
8.1 hset/hget
void test1(Redis& redis) {
std::cout << "hset 和 hget" << std::endl;
redis.flushall();
redis.hset("key", "f1", "111");
redis.hset("key", std::make_pair("f2", "222"));
// hset 能够一次性插入多个 field-value 对!!
redis.hset("key", {
std::make_pair("f3", "333"),
std::make_pair("f4", "444")
});
vector<std::pair<string, string>> fields = {
std::make_pair("f5", "555"),
std::make_pair("f6", "666")
};
redis.hset("key", fields.begin(), fields.end());
auto result = redis.hget("key", "f3");
if (result) {
std::cout << "result: " << result.value() << std::endl;
} else {
std::cout << "result 无效!" << std::endl;
}
}
8.2 hexists
void test2(Redis& redis) {
std::cout << "hexits" << std::endl;
redis.flushall();
redis.hset("key", "f1", "111");
redis.hset("key", "f2", "222");
redis.hset("key", "f3", "333");
bool result = redis.hexists("key", "f4");
std::cout << "result: " << result << std::endl;
}
8.3 hdel
void test3(Redis& redis) {
std::cout << "hdel" << std::endl;
redis.flushall();
redis.hset("key", "f1", "111");
redis.hset("key", "f2", "222");
redis.hset("key", "f3", "333");
long long result = redis.hdel("key", "f1");
std::cout << "result: " << result << std::endl;
result = redis.hdel("key", {"f2", "f3"});
std::cout << "result: " << result << std::endl;
long long len = redis.hlen("key");
std::cout << "len: " << len << std::endl;
}
8.4 hkeys/hvals
void test4(Redis& redis) {
std::cout << "hkeys 和 hvals" << std::endl;
redis.flushall();
redis.hset("key", "f1", "111");
redis.hset("key", "f2", "222");
redis.hset("key", "f3", "333");
vector<string> fields;
auto itFields = std::back_inserter(fields);
redis.hkeys("key", itFields);
printContainer(fields);
vector<string> values;
auto itValues = std::back_inserter(values);
redis.hvals("key", itValues);
printContainer(values);
}
8.5 hmget/hmset
void test5(Redis& redis) {
std::cout << "hmget 和 hmset" << std::endl;
redis.flushall();
redis.hmset("key", {
std::make_pair("f1", "111"),
std::make_pair("f2", "222"),
std::make_pair("f3", "333")
});
vector<std::pair<string, string>> pairs = {
std::make_pair("f4", "444"),
std::make_pair("f5", "555"),
std::make_pair("f6", "666")
};
redis.hmset("key", pairs.begin(), pairs.end());
vector<string> values;
auto it = std::back_inserter(values);
redis.hmget("key", {"f1", "f2", "f3"}, it);
printContainer(values);
}
九、zset类型的操作
void test1(Redis& redis) {
std::cout << "zadd 和 zrange" << std::endl;
redis.flushall();
redis.zadd("key", "吕布", 99);
redis.zadd("key", {
std::make_pair("赵云", 98),
std::make_pair("典韦", 97)
});
vector<std::pair<string, double>> members = {
std::make_pair("关羽", 95),
std::make_pair("张飞", 93)
};
redis.zadd("key", members.begin(), members.end());
// zrange 支持两种主要的风格:
// 1. 只查询 member, 不带 score
// 2. 查询 member 同时带 score
// 关键就是看插入迭代器指向的容器的类型.
// 指向的容器只是包含一个 string, 就是只查询 member
// 指向的容器包含的是一个 pair, 里面有 string 和 double, 就是查询 member 同时带有 score
vector<string> memberResults;
auto it = std::back_inserter(memberResults);
redis.zrange("key", 0, -1, it);
printContainer(memberResults);
vector<std::pair<string, double>> membersWithScore;
auto it2 = std::back_inserter(membersWithScore);
redis.zrange("key", 0, -1, it2);
printContainerPair(membersWithScore);
}
void test2(Redis& redis) {
std::cout << "zcard" << std::endl;
redis.flushall();
redis.zadd("key", "zhangsan", 90);
redis.zadd("key", "lisi", 91);
redis.zadd("key", "wangwu", 92);
redis.zadd("key", "zhaoliu", 93);
long long result = redis.zcard("key");
std::cout << "result: " << result << std::endl;
}
void test3(Redis& redis) {
std::cout << "zrem" << std::endl;
redis.flushall();
redis.zadd("key", "zhangsan", 90);
redis.zadd("key", "lisi", 91);
redis.zadd("key", "wangwu", 92);
redis.zadd("key", "zhaoliu", 93);
redis.zrem("key", "zhangsan");
long long result = redis.zcard("key");
std::cout << "result: " << result << std::endl;
}
void test4(Redis& redis) {
std::cout << "zscore" << std::endl;
redis.flushall();
redis.zadd("key", "zhangsan", 90);
redis.zadd("key", "lisi", 91);
redis.zadd("key", "wangwu", 92);
redis.zadd("key", "zhaoliu", 93);
auto score = redis.zscore("key", "zhangsan");
if (score) {
std::cout << "score: " << score.value() << std::endl;
} else {
std::cout << "score 无效" << std::endl;
}
}
void test5(Redis& redis) {
std::cout << "zrank" << std::endl;
redis.flushall();
redis.zadd("key", "zhangsan", 90);
redis.zadd("key", "lisi", 91);
redis.zadd("key", "wangwu", 92);
redis.zadd("key", "zhaoliu", 93);
auto rank = redis.zrank("key", "zhaoliu");
if (rank) {
std::cout << "rank: " << rank.value() << std::endl;
} else {
std::cout << "rank 无效" << std::endl;
}
}