0%

Redis C++客户端—Hiredis

Hiredis是Redis数据库一个轻量的C语言客户端库。它只是简单的提供了对redis操作语句支持的接口,并没有实现具体的操作语句的功能,因此可以很容易的使用该库和redis数据库进行交互。本文主要翻译自官方说明文档。

1. Hiredis编译

Hiredis是用C写的Redis客户端,对Redis协议进行了简单的封装。除了支持发送命令和接收应答外,Hiredis还提供了独立于I/O的数据流解析操作,用于解析应答数据。在Windows平台上使用Hiredis,一般需要将源码编译生成库文件然后进行调用。在Visual Studio上编译Hiredis项目的时候会出现无法生成lib的问题,其Github库的issues#687提供了一种解决方案,测试可行。

同步连接:服务器与第一个请求建立连接并通信以后,第二个请求会被阻塞。
异步连接:服务器可以同时响应多个请求

Hiredis提供了同步、异步以及回复解析三种API。

2. 同步API

常用的同步API的函数有:

1
2
3
redisContext *redisConnect(const char *ip, int port);
void *redisCommand(redisContext *c, const char *format, ...);
void freeReplyObject(void *reply);

2.1 连接

Hiredis通过redisConnect创建一个redisContext来实现与Redis进行连接,context中包含了连接的信息。redisContext中包含有一个整形的err变量和一个字符类型的errstr变量,当创建连接失败,err为非零值,errstr为错误的表述。当使用redisConnect创建连接后,应该检查err参数以判断连接是否成功。

1
2
3
4
5
6
7
8
9
10
11
12
redisContext* context = redisConnect("127.0.0.1",6379)
if(context == NULL || context->err)
{
if(context)
{
printf("Connection Error: %s\n", context->errstr);
}
else
{
printf("Can't allocate redis context\n");
}
}

NOTE:redisContext是线程不安全的。

线程安全:多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全:不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

2.2 发送命令

有多种方法可以向Redis发送命令,这里介绍redisCommand,其采用的是类似于printf的格式。

1
2
3
4
5
6
7
8
redisReply* reply;
redisCommand(context,"SET key value");
redisCommand(context,"SET key %s",value)
redisCommand(context,"SET key:%s %s",mykey,value);
// 传输二进制安全命令,使用`%b`进行格式化
redisCommand(context,"SET key %b",value,(size_t)value_size);

reply = redisCommand(context,"GET key");

NOTE:Windows下redisCommand返回的reply与用redisReply声明的的类型会不匹配,需要进行类型转换,reply = static_cast<redisReply*>(redisCommand(context, "GET key"));

2.3 处理应答

redisCommand执行命令成功以后,会返回一个redisReply类型的返回值reply。当发生错误的时候,reply为NULL且context中的err的值会被改变。NOTE:一旦发生了错误,context就不能被重用,需要重新建立连接。
reply对象中有一个type属性来标识不同的错误类型:

  • REDIS_REPLY_STATUS:返回执行结果的状态,reply->str获取状态的描述信息,reply->len得到信息的长度。
  • REDIS_REPLY_ERROR:返回错误,通过reply->str获取错误的描述信息。
  • REDIS_REPLY_INTEGER:返回整形标识,通过reply->integer获得类型为long long的整型值。
  • REDIS_REPLY_NIL:返回NIL对象,说明不存在要访问的数据。
  • REDIS_REPLY_STRING:返回字符串标识,reply->str获取返回的字符串的值,reply->len得到字符串的长度。
  • REDIS_REPLY_ARRAY:返回包含多个reply的数据集,通过reply->elements获取reply的个数,每一个reply可以通过reply->element[..index..]索引得到。

NOTE:在执行完命令之后,必须要通过freeReplyObject()函数将reply对象释放掉。对于数组或者嵌套数组中的sub-reply,不需要进行嵌套释放。

2.4 清理连接

断开连接并释放context:

1
void redisFree(redisContext* context);

redisFree函数会关闭网络套接字并且释放所有在创建连接时分配的资源。

2.5 发送多个命令参数

redisCommandArgv函数可以用于传输多个命令参数,其函数原型为:

1
void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);

argc表示参数的个数,argv是存储命令字符串的数组,argvlen为数组中每个元素的长度。为了方便可以将argvlen设置为NULL,函数将会使用strlen()函数来判断每个参数的长度。为了保证参数的二进制安全,还是应该提供argvlen的值。

2.6 管线

如果使用redisCommand函数发送多次命令,需要每次发送后等待结果返回才能进行下一次发送。Redis的管线机制允许客户端一次性向服务端发送多个命令,Redis在接收到这些命令后按顺序进行处理,然后将请求的处理结果一次性返回给客户端。管线可以减少客户端与服务端之间的网络通信次数来提升Redis客户端发送多个命令时的性能。
redisCommand函数被调用,Hiredis先将命令格式化,被格式化的命令放入context的输出缓冲区(命令缓冲区),然后发送到Redis执行,并将结果返回到输入缓冲区(结果缓冲区)。Hiredis提供了redisAppendCommand()函数来实现管线的命令发送方案。输出缓冲区是动态的,可以容纳任意数量的命令。

1
2
void redisAppendCommand(redisContext *c, const char *format, ...);
void redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);

redisAppendCommand()函数被执行后,命令缓存到context的输出缓冲区,并不会立刻发送到Redis执行。当redisGetReply()被调用时,才会将输出缓冲区的命令一次性发送到Redis,并返回第一条命令的应答结果。redisGetReply()的返回值为REDIS_ERRREDIS_OKredisGetReply()的执行方式有两种:

  1. 输入缓冲区非空:
    • 从出入缓冲区中解析一个reply并返回
    • 如果没有reply可以被解析,执行2
  2. 输入缓冲区为空:
    • 将整个输出缓冲区写入socket
    • 从socket中读取命令直到有一个reply可以被解析

以下为一个简单的管线使用例程:

1
2
3
4
5
6
7
redisReply *reply;
redisAppendCommand(context,"SET foo bar");
redisAppendCommand(context,"GET foo");
redisGetReply(context,&reply); // reply for SET
freeReplyObject(reply);
redisGetReply(context,&reply); // reply for GET
freeReplyObject(reply);

redisGetReply()也可以用来实现一个阻塞的订阅:

1
2
3
4
5
6
7
reply = redisCommand(context,"SUBSCRIBE foo");
freeReplyObject(reply);
while(redisGetReply(context,&reply) == REDIS_OK)
{
// consume message
freeReplyObject(reply);
}

NOTE:调用redisAppendCommand()函数的次数需要与调用redisGetReply()的次数要一致,否则会出现获取的Redis处理结果跟预期不一致的情况。

2.7 错误处理

当函数调用不成功时,将会返回NULLREDIS_ERR,context中的err字段会设置为以下常量之一:

  • REDIS_ERR_IOI/O错误,发生在创建连接时(尝试写入或者读取socket)。通过包含errno.h可以获取详细的错误信息。
  • REDIS_ERR_EOF:服务端关闭了连接,导致读取为空。
  • REDIS_ERR_PROTOCOL:服务端解析协议时出错。
  • REDIS_ERR_OTHER:其他错误类型,仅在无法解析连接目标主机名时使用。

3. 异步API

// TODO

4. 回复解析API

Hiredis提供了回复解析API,可以轻松的与高级语言绑定。回复解析API函数有:

1
2
3
4
redisReader *redisReaderCreate(void);
void redisReaderFree(redisReader *reader);
int redisReaderFeed(redisReader *reader, const char *buf, size_t len);
int redisReaderGetReply(redisReader *reader, void **reply);

//TODO

5. SSL/TLS支持

SSL(Secure Socket Layer,完全套接字层):用以保障在Internet上数据传输之安全,利用数据加密(Encryption)技术,可确保数据在网络上之传输过程中不会被截取。SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。
SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

TLS(Transport Layer Security,传输层安全协议),用于两个应用程序之间提供保密性和数据完整性。
TLS协议由两层组成:TLS记录协议(TLS Record)和TLS握手协议(TLS Handshake)。较低的层为TLS 记录协议,位于某个可靠的传输协议(例如TCP)上面。

参考文献 & 资源链接