一、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 設置 keyredis.set("key1", "111");redis.set("key2", "222");redis.set("key3", "333");// 使用 get 獲取到 key 對應的 valueauto value1 = redis.get("key1");// optional 可以隱式轉成 bool 類型, 可以直接在 if 中判定. 如果是無效元素, 就是返回 falseif (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") });// 第二種寫法, 可以把多個鍵值對提前組織到容器中. 以迭代器的形式告訴 msetvector<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();// 構造一個 listredis.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 同時帶有 scorevector<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;}
}