前言

redis客户端与redis服务器通信是属于CS通讯模型:客户发送一个请求给服务器,然后客户端等待服务器的返回;服务器收到请求后,解析请求并返回客户端想要的数据或者返回一个错误。

通常来说,客户端与服务器通讯都会遵守一定的规则,这个规则又称为协议。协议有公开有协议,也有私有的协议。著名的公开协议有HTTP、FTP等,它们都是构建在TCP层之上的应用层协议。而redis客户端与redis服务器主要使用RESP 2(redis6之前的版本)和RESP 3(redis6)协议来进行通讯。

目前个人已经开源了一个redis C++客户端:github地址

项目介绍

1. 项目主要在windows平台使用vs2019进行开发,github上的代码可以直接下载下来打开解决方案并进行编译调试。在linux下,可以使用cmake进行编译,CMakeLists.txt同时也已经提供。

2. 项目中并没有使用任何的网络库,也没有其他第三方依赖,所以编译将非常简单

3. 仅仅要求使用C++11

简单使用

1. 连接redis服务器

std::string ip("81.71.11.65");
int port = 6379;

auto client = std::make_shared<RClient>(ip, port);
int ret = client->connect("123456");
if (ret != 0)
{
	std::cout << "Err: " << client->strerror() << std::endl;
}
else
{
	std::cout << "connect successfully..." << std::endl;
}

2. 执行redis命令

std::string ip("81.71.11.65");
int port = 6379;

auto client = std::make_shared<RClient>(ip, port);
int ret = client->connect("123456");
if (ret != 0)
{
	std::cout << "Err: " << client->strerror() << std::endl;
}
else
{
	std::cout << "connect successfully..." << std::endl;
}

//transfer to RESP 3
ret = client->use_resp3();
if (ret != 0)
{
	return 1;
}
std::string cmd = "sadd test_set 101 102\r\n";
int written_len = client->command(cmd.c_str(), cmd.size());
if (written_len != cmd.size())
{
	//send error
	return 1;
}
int ret_code = 0;
auto result_ptr = client->get_results(ret_code);
if (result_ptr)
{
	if (result_ptr->value_type() == ParserType::Number)
	{
		auto ptr = std::dynamic_pointer_cast<RedisValue>(result_ptr);
		std::cout << "result : " << ptr->u.int_val_ << std::endl;
	}
}
else
{
	std::cout << "get results error: " << ret_code << std::endl;
}

3. 使用高级数据结构

std::string ip("81.71.77.77");
	int port = 6379;

	auto client = std::make_shared<RClient>(ip, port);
	int ret = client->connect("123456");
	if (ret != 0)
	{
		std::cout << "Err: " << client->strerror() << std::endl;
		return;
	}

	ret = client->use_resp3();
	if (ret != 0)
	{
		return;
	}
	auto set_client = std::make_shared<RdSet>(client);
	int count = set_client->sadd("test_set", { 10, 9, 8 });
	std::cout << "set count: " << count << std::endl;
	auto results = set_client->smembers("test_set");
	if (results)
	{
		if (results->value_type() != ParserType::Set
			&& results->value_type() != ParserType::Array)
		{
			return;
		}
		std::cout << "values:";
		auto ptr = std::dynamic_pointer_cast<RedisComplexValue>(results);
		auto it = ptr->results.begin();
		for (; it != ptr->results.end(); ++it)
		{
			if ((*it)->is_string())
			{
				auto ptr = std::dynamic_pointer_cast<RedisValue>(*it);
				std::cout << " " << ptr->str_val_;
			}
		}
		std::cout << std::endl;
	}
	bool flag = set_client->is_member("test_set", 2);
	std::cout << "flag:" << flag << std::endl;

教程(原理)

使用rclientpp的核心是理解client->get_results(ret_code)的返回,接口定义如下:

class RClient
{
public:
	RClient(const std::string& ipstr, int port);
    ...
	std::shared_ptr<BaseValue> get_results(int& ret_code);
};

get_results将返回一个BaseValue的shared_ptr, BaseValue的定义如下:

class BaseValue
{
public:
	BaseValue(ParserType type)
		:type_(type)
	{}

	ParserType value_type() const
	{
		return type_;
	}
public:
	ParserType type_;
};

class RedisValue : public BaseValue
{
public:
	RedisValue(ParserType type)
		:BaseValue(type)
	{
	}
	RedisValue(int64_t val)
		:BaseValue(ParserType::Number)
	{
		u.int_val_ = val;
	}
	RedisValue(long double val)
		:BaseValue(ParserType::Double)
	{
		u.d_val_ = val;
	}

	RedisValue(bool val)
		:BaseValue(ParserType::Boolean)
	{
		u.boolean_val_ = val;
	}

	RedisValue(const std::string& val, ParserType type)
		:BaseValue(type),
		str_val_(val)
	{
	}

	RedisValue(const std::string&& val, ParserType type)
		:BaseValue(type),
		str_val_(val)
	{
	}

	virtual bool is_ok()
	{
		if (value_type() == ParserType::SimpleString)
		{
			if (str_val_ == std::string("OK"))
			{
				return true;
			}
		}
		return false;
	}

public:
	union
	{
		int64_t int_val_;
		long double d_val_;
		bool boolean_val_;
	} u;
	std::string str_val_;
};

class RedisComplexValue : public BaseValue
{
public:
	RedisComplexValue(ParserType type)
		:BaseValue(type)
	{
	}
	std::list<std::shared_ptr<BaseValue>> results;
	int count{0};
};

可以看到,RedisValue与RedisComplexValue都继承自BaseValue,就是说get_results()返回的将会是RedisValue或者RedisComplexValue,BaseValue是作为抽象基类存在的,永远不会创建一个BaseValue,而只会创建它的子类。

RedisValue类包装了C++基础的数据类型(POD+string),如int, long long, double, bool, string,它代表着redis服务器返回的单值。

RedisComplexValue类是一个复合的数据类型,它由一系列的BaseValue组成,而每一个BaseValue又会是RedisValue或者RedisComplexValue,这是由我们Redis命令返回的结果决定的。

特别注意

对于一些redis命令,其返回多条平行的数据类型,如:

SUBSCRIBE chat_msg chat_msg2\r\n

返回:

*3\r\n$9\r\nsubscribe\r\n$8\r\nchat_msg\r\n:1\r\n*3\r\n$9\r\nsubscribe\r\n$9\r\nchat_msg2\r\n:2\r\n

从上面可以看到,返回是两个数组,它们并没有谁包含谁,是平行的关系,这时候,get_results()只会返回第一个数组,后面的不会返回。文件subscriber.cpp提供了处理这种情况的范例。

好了,核心的内容已经介绍完了。