redis/hiredis

GitHub: redis/hiredis

Redis 官方维护的极简 C 客户端库,提供同步/异步 API 与独立的 RESP 协议回复解析器,兼容 Redis 1.2 及以上版本。

Stars: 6657 | Forks: 1849

[![构建状态](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/9847002115201549.svg)](https://github.com/redis/hiredis/actions/workflows/build.yml) **本 README 反映了 master 分支的最新更改。有关最新发布版本的 README 和文档,请参阅 [v1.0.0](https://github.com/redis/hiredis/tree/v1.0.0)([API/ABI 历史](https://abi-laboratory.pro/?view=timeline&l=hiredis))。** # HIREDIS Hiredis 是一个用于 [Redis](https://redis.io/) 数据库的轻量级 C 客户端库。 它之所以轻量,是因为它仅提供了对协议的最基本支持,但 与此同时,它使用了类似于 `printf` 的高层级 API,这使得 相较于其极简的代码库以及缺乏对每个 Redis 命令的显式绑定所暗示的程度,它的实际抽象层级要高得多。 除了支持发送命令和接收回复外,它还附带了一个 与 I/O 层解耦的回复解析器。它是一个 专为易于复用而设计的流解析器,例如,它可以用于高层级的语言绑定中,以实现高效的回复解析。 Hiredis 仅支持二进制安全的 Redis 协议,因此你可以将其与任何 Redis 版本 >= 1.2.0 配合使用。 该库提供了多种 API。包括 *同步 API*、*异步 API* 和 *回复解析 API*。 ## 升级到 > 1.2.0 (**预发布版本**) * 在 v1.2.0 之后,我们修改了调用 `poll(2)` 等待连接完成的方式,这样如果 调用被信号中断,我们将重试该调用,直到: a) 连接成功或失败。 b) 达到总体的连接超时时间。 在以前的版本中,被中断的 `poll(2)` 调用会导致连接失败, 此时 `c->err` 会被设置为 `REDIS_ERR_IO`,而 `c->errstr` 会被设置为 `poll(2): Interrupted system call`。 ## 升级到 `1.1.0` 几乎所有用户只需要针对较新版本的 hiredis 重新编译他们的应用程序即可。 **注意**:Hiredis 现在除了可以在 `REDIS_REPLY_DOUBLE` 中返回 `-inf` 和 `inf` 外,还可以返回 `nan`。 处理 `RESP3` 双精度浮点数的应用程序应确保将这种情况考虑在内。 ## 升级到 `1.0.2` 注意:v1.0.1 错误地提升了 SONAME,这就是为什么在这里跳过它的原因。 版本 1.0.2 仅仅是带有 [CVE-2021-32765](https://github.com/redis/hiredis/security/advisories/GHSA-hfm9-39pp-55p2) 修复的 1.0.0。除此之外,它们是完全相同的。 ## 升级到 `1.0.0` 版本 1.0.0 标志着 Hiredis 的第一个稳定版本发布。 它包含了一些微小的破坏性变更,主要是为了使暴露的 API 更加统一和易于理解。 它还捆绑了更新后的 `sds` 库,以与上游和 Redis 保持同步。 有关代码变更,请参阅 [更新日志](CHANGELOG.md)。 _注意:如下所述,一些成员名称已更改,但大多数应用程序应该能够通过微小的代码修改和重新编译来完成升级。_ ## 重要:从 `0.14.1` -> `1.0.0` 的破坏性变更 * `redisContext` 增加了两个成员(`free_privdata` 和 `privctx`)。 * `redisOptions.timeout` 已被重命名为 `redisOptions.connect_timeout`,并且我们添加了 `redisOptions.command_timeout`。 * `redisReplyObjectFunctions.createArray` 的长度参数现在使用 `size_t` 而不是 `int`。 ## 重要:从 0.13.x -> 0.14.x 升级时的破坏性变更 小于 -1 或大于 `LLONG_MAX` 的批量回复和多批量回复长度现在 被视为协议错误。这与 RESP 规范保持一致。在 32 位 平台上,上限被降低到了 `SIZE_MAX`。 将 `redisReply.len` 更改为 `size_t`,因为它表示字符串的大小。 用户代码也应该将其与 `size_t` 值进行比较。如果之前被用于 与其他值进行比较,则可能需要进行类型转换;如果之前应用了 类型转换,则可以将其移除。 ## 从 `<0.9.0` 升级 版本 0.9.0 是 hiredis 在各个方面的一次大修。然而,升级现有 使用 hiredis 的代码应该不会太痛苦。升级时需要记住的关键点是,hiredis >= 0.9.0 使用 `redisContext*` 来保持状态,这与 无状态的 0.0.1(只有一个文件描述符可供使用)形成对比。 ## 同步 API 要使用同步 API,只需要了解几个函数调用: ``` redisContext *redisConnect(const char *ip, int port); void *redisCommand(redisContext *c, const char *format, ...); void freeReplyObject(void *reply); ``` ### 连接 函数 `redisConnect` 用于创建所谓的 `redisContext`。该 上下文是 Hiredis 为连接保持状态的地方。`redisContext` 结构体有一个整数的 `err` 字段,当连接处于 错误状态时,该字段为非零值。`errstr` 字段将包含一个描述该 错误的字符串。有关错误的更多信息,请参阅 **错误** 部分。 在尝试使用 `redisConnect` 连接到 Redis 后,你应该 检查 `err` 字段以查看建立连接是否成功: ``` redisContext *c = redisConnect("127.0.0.1", 6379); if (c == NULL || c->err) { if (c) { printf("Error: %s\n", c->errstr); // handle error } else { printf("Can't allocate redis context\n"); } } ``` 也可以使用 `redisConnectWithOptions`,它接受一个 `redisOptions` 参数, 该参数可以配置 endpoint 信息以及许多不同的 flags, 以改变 `redisContext` 的配置方式。 ``` redisOptions opt = {0}; /* One can set the endpoint with one of our helper macros */ if (tcp) { REDIS_OPTIONS_SET_TCP(&opt, "localhost", 6379); } else { REDIS_OPTIONS_SET_UNIX(&opt, "/tmp/redis.sock"); } /* And privdata can be specified with another helper */ REDIS_OPTIONS_SET_PRIVDATA(&opt, myPrivData, myPrivDataDtor); /* Finally various options may be set via the `options` member, as described below */ opt->options |= REDIS_OPT_PREFER_IPV4; ``` 如果连接丢失,可以使用 `int redisReconnect(redisContext *c)` 利用给定上下文相同的 endpoint 和选项来恢复连接。 ### 可配置的 redisOptions flags 你可以在 `redisOptions` 结构体中设置几个 flags 来改变默认行为。你可以通过 `redisOptions->options` 成员来指定这些 flags。 | Flag | 描述 | | --- | --- | | REDIS\_OPT\_NONBLOCK | 告诉 hiredis 建立非阻塞连接。 | | REDIS\_OPT\_REUSEADDR | 告诉 hiredis 设置 [SO_REUSEADDR](https://man7.org/linux/man-pages/man7/socket.7.html) socket 选项 | | REDIS\_OPT\_PREFER\_IPV4
REDIS\_OPT\_PREFER_IPV6
REDIS\_OPT\_PREFER\_IP\_UNSPEC | 通知 hiredis 在调用 [getaddrinfo](https://man7.org/linux/man-pages/man3/gai_strerror.3.html) 时优先使用 IPv4 或 IPv6。`REDIS_OPT_PREFER_IP_UNSPEC` 将使 hiredis 在 getaddrinfo 调用中指定 `AF_UNSPEC`,这意味着将同时搜索 IPv4 和 IPv6 地址。
Hiredis 默认优先使用 IPv4。 | | REDIS\_OPT\_NO\_PUSH\_AUTOFREE | 告诉 hiredis 不要安装默认的 RESP3 PUSH 处理程序(它仅负责拦截并释放回复)。这在你想在带内处理这些消息的情况下非常有用。 | | REDIS\_OPT\_NOAUTOFREEREPLIES | **异步**:告诉 hiredis 在执行回复 callback 后不要自动调用 `freeReplyObject`。 | | REDIS\_OPT\_NOAUTOFREE | **异步**:告诉 hiredis 在连接/通信失败时不要自动释放 `redisAsyncContext`,除非用户显式调用了 `redisAsyncDisconnect` 或 `redisAsyncFree` | *注意:`redisContext` 不是线程安全的。* ### 使用 socket 选项进行其他配置 以下 socket 选项将直接应用到底层 socket 上。 它们的值不会存储在 `redisContext` 中,因此在使用 `redisReconnect()` 重连时不会自动应用它们。 这些函数在成功时返回 `REDIS_OK`。 如果失败,将返回 `REDIS_ERR` 并且底层连接将被关闭。 要为异步上下文配置这些选项(参见下方的*异步 API*),可以使用 `ac->c` 从 asyncRedisContext 中获取 redisContext。 ``` int redisEnableKeepAlive(redisContext *c); int redisEnableKeepAliveWithInterval(redisContext *c, int interval); ``` 通过设置以下 socket 选项来启用 TCP keepalive(具体取决于操作系统会有一些变化): * `SO_KEEPALIVE`; * `TCP_KEEPALIVE` 或 `TCP_KEEPIDLE`,值可通过 `interval` 参数配置,默认为 15 秒; * `TCP_KEEPINTVL` 设置为 `interval` 的 1/3; * `TCP_KEEPCNT` 设置为 3。 ``` int redisSetTcpUserTimeout(redisContext *c, unsigned int timeout); ``` 设置 `TCP_USER_TIMEOUT` 特定于 Linux 的 socket 选项,如 `tcp` man page 中所述: ### 发送命令 有几种方法可以向 Redis 发送命令。首先要介绍的是 `redisCommand`。该函数采用类似于 `printf` 的格式。在最简单的形式中, 它是这样使用的: ``` reply = redisCommand(context, "SET foo bar"); ``` 说明符 `%s` 在命令中插入一个字符串,并使用 `strlen` 来 确定字符串的长度: ``` reply = redisCommand(context, "SET foo %s", value); ``` 当你需要在命令中传递二进制安全的字符串时,可以使用 `%b` 说明符。 与指向字符串的指针一起,它还需要一个表示字符串长度的 `size_t` 参数: ``` reply = redisCommand(context, "SET foo %b", value, (size_t) valuelen); ``` 在内部,Hiredis 会将命令拆分为不同的参数,并 将其转换为用于与 Redis 通信的协议。 一个或多个空格用于分隔参数,因此你可以将说明符 用在参数的任何位置: ``` reply = redisCommand(context, "SET key:%s %s", myid, value); ``` ### 使用回复 当命令成功执行时,`redisCommand` 的返回值会包含回复。 当发生错误时,返回值为 `NULL`,并且 上下文中的 `err` 字段将被设置(参见 **错误** 部分)。 一旦返回错误,该上下文就无法重用,你应该 建立一个新的连接。 `redisCommand` 返回的标准回复类型为 `redisReply`。 应该使用 `redisReply` 中的 `type` 字段来测试收到了 哪种类型的回复: ### RESP2 * **`REDIS_REPLY_STATUS`**: * 命令回复了一个状态回复。可以使用 `reply->str` 访问状态字符串。 可以使用 `reply->len` 访问此字符串的长度。 * **`REDIS_REPLY_ERROR`**: * 命令回复了一个错误。错误字符串的访问方式与 `REDIS_REPLY_STATUS` 完全相同。 * **`REDIS_REPLY_INTEGER`**: * 命令回复了一个整数。可以使用 类型为 `long long` 的 `reply->integer` 字段来访问整数值。 * **`REDIS_REPLY_NIL`**: * 命令回复了一个 **nil** 对象。没有可供访问的数据。 * **`REDIS_REPLY_STRING`**: * 一个批量(字符串)回复。可以使用 `reply->str` 访问回复的值。 可以使用 `reply->len` 访问此字符串的长度。 * **`REDIS_REPLY_ARRAY`**: * 一个多批量回复。多批量回复中的元素数量存储在 `reply->elements` 中。多批量回复中的每个元素也是一个 `redisReply` 对象, 可以通过 `reply->element[..index..]` 进行访问。 Redis 可能会回复嵌套数组,但这已得到完全支持。 ### RESP3 Hiredis 还支持所有新的 `RESP3` 数据类型,如下所示。有关该协议的更多信息,请参阅 `RESP3` [规范。](https://github.com/antirez/RESP3/blob/master/spec.md) * **`REDIS_REPLY_DOUBLE`**: * 命令回复了一个双精度浮点数。 该值作为字符串存储在 `str` 成员中,可以使用 `strtod` 或类似方法进行转换。 * **`REDIS_REPLY_BOOL`**: * 一个布尔值 true/false 回复。 该值存储在 `integer` 成员中,为 `0` 或 `1`。 * **`REDIS_REPLY_MAP`**: * 一个带有附加不变量的数组,即元素数量始终为偶数。 除了前面提到的不变量外,MAP 在功能上等同于 `REDIS_REPLY_ARRAY`。 * **`REDIS_REPLY_SET`**: * 一种每个条目都是唯一的数组回复。 与 MAP 类型一样,其数据与数组回复完全相同,只是没有重复的值。 * **`REDIS_REPLY_PUSH`**: * 一个可以由 Redis 自发生成的数组。 此数组回复将始终包含至少两个子元素。第一个包含 `PUSH` 消息的类型(例如 `message` 或 `invalidate`),第二个是包含 `PUSH` 负载本身的子数组。 * **`REDIS_REPLY_ATTR`**: * 在结构上与 `MAP` 完全相同,但旨在作为回复的元数据的数组。 _截至 Redis 6.0.6,此回复类型未在 Redis 中使用_ * **`REDIS_REPLY_BIGNUM`**: * 表示任意大的有符号或无符号整数值的字符串。 该数字将作为字符串编码在 `redisReply` 的 `str` 成员中。 * **`REDIS_REPLY_VERB`**: * 一个原样字符串,旨在不加修改地呈现给用户。 字符串 payload 存储在 `str` 成员中,而类型数据存储在 `vtype` 成员中(例如 `txt` 表示原始文本,或 `md` 表示 markdown)。 应该使用 `freeReplyObject()` 函数来释放回复。 请注意,此函数会负责释放包含在 数组及嵌套数组中的子回复对象,因此用户无需 释放子回复(实际上这样做是有害的,并且会破坏内存)。 **重要:**当前版本的 hiredis (1.0.0) 在使用 异步 时会自动释放回复。这意味着当 你使用此 API 时,你不应调用 `freeReplyObject`。回复会在 callback 返回 _之后_ 由 hiredis 清理。我们可能会在未来的库版本中引入一个 flag 使其可配置。 ### 清理 要断开连接并释放上下文,可以使用以下函数: ``` void redisFree(redisContext *c); ``` 此函数会立即关闭 socket,然后释放在 创建上下文时进行的分配。 ### 发送命令(续) 除了 `redisCommand`,还可以使用函数 `redisCommandArgv` 来发送命令。 其原型如下: ``` void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); ``` 它接受参数数量 `argc`、字符串数组 `argv` 以及参数 长度 `argvlen`。为了方便起见,可以将 `argvlen` 设置为 `NULL`,该函数将 对每个参数使用 `strlen(3)` 来确定其长度。显然,当任何参数 需要是二进制安全的时,应提供完整的长度数组 `argvlen`。 其返回值的语义与 `redisCommand` 相同。 ### Pipelining 为了解释 Hiredis 如何在阻塞连接中支持 pipelining,需要了解 内部的执行流程。 当调用 `redisCommand` 系列中的任何函数时,Hiredis 首先会根据 Redis 协议格式化命令。然后,格式化后的命令会被放入 上下文的输出缓冲区中。此输出缓冲区是动态的,因此它可以容纳任意数量的命令。 在将命令放入输出缓冲区后,会调用 `redisGetReply`。此函数有 以下两条执行路径: 1. 输入缓冲区非空: * 尝试从输入缓冲区解析单个回复并将其返回 * 如果无法解析出回复,则继续至 *2* 2. 输入缓冲区为空: * 将 **整个** 输出缓冲区写入 socket * 从 socket 读取,直到能够解析出单个回复 函数 `redisGetReply` 作为 Hiredis API 的一部分导出,可用于在 socket 上期待回复时使用。对于 pipeline 命令,唯一需要做的就是 填满输出缓冲区。为此,可以使用两个与 `redisCommand` 系列完全相同、但不返回回复的命令: ``` void redisAppendCommand(redisContext *c, const char *format, ...); void redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); ``` 在调用上述任一函数一次或多次后,可以使用 `redisGetReply` 来接收 后续的回复。此函数的返回值为 `REDIS_OK` 或 `REDIS_ERR`,其中 后者意味着在读取回复时发生了错误。与其他命令一样, 上下文中的 `err` 字段可用于找出此错误的原因。 以下示例展示了一个简单的 pipeline(仅导致一次对 `write(2)` 的调用和 一次对 `read(2)` 的调用): ``` redisReply *reply; redisAppendCommand(context,"SET foo bar"); redisAppendCommand(context,"GET foo"); redisGetReply(context,(void**)&reply); // reply for SET freeReplyObject(reply); redisGetReply(context,(void**)&reply); // reply for GET freeReplyObject(reply); ``` 此 API 还可用于实现阻塞的 subscriber: ``` reply = redisCommand(context,"SUBSCRIBE foo"); freeReplyObject(reply); while(redisGetReply(context,(void *)&reply) == REDIS_OK) { // consume message freeReplyObject(reply); } ``` ### 错误 当函数调用不成功时,根据函数的不同,可能会返回 `NULL` 或 `REDIS_ERR`。 上下文中的 `err` 字段将为非零值,并被设置为以下 常量之一: * **`REDIS_ERR_IO`**: 在创建连接、尝试写入 socket 或从 socket 读取时发生了 I/O 错误。如果你在应用程序中包含了 `errno.h`, 你可以使用全局的 `errno` 变量来找出问题 所在。 * **`REDIS_ERR_EOF`**: 服务器关闭了连接,导致读取到空内容。 * **`REDIS_ERR_PROTOCOL`**: 解析协议时出错。 * **`REDIS_ERR_OTHER`**: 任何其他错误。目前,它仅在无法解析要连接的指定主机名时使用。 在每种情况下,上下文中的 `errstr` 字段都会被设置为保存该错误 的字符串表示。 ## 异步 API Hiredis 提供了一个异步 API,可以轻松地与任何事件库配合使用。 包含了示例以展示 Hiredis 与 [libev](http://software.schmorp.de/pkg/libev.html) 和 [libevent](http://monkey.org/~provos/libevent/) 的配合使用。 ### 连接 可以使用 `redisAsyncConnect` 函数建立到 Redis 的非阻塞连接。它返回一个指向新创建的 `redisAsyncContext` 结构体的指针。 创建后应检查 `err` 字段,以查看创建连接时是否出现错误。 由于将要创建的连接是非阻塞的,内核无法 立即返回指定主机和端口是否能接受连接。 如果发生错误,调用者有责任使用 `redisAsyncFree()` 释放上下文 *注意:`redisAsyncContext` 不是线程安全的。* 一个创建连接的应用程序函数可能如下所示: ``` void appConnect(myAppData *appData) { redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); if (c->err) { printf("Error: %s\n", c->errstr); // handle error redisAsyncFree(c); c = NULL; } else { appData->context = c; appData->connecting = 1; c->data = appData; /* store application pointer for the callbacks */ redisAsyncSetConnectCallback(c, appOnConnect); redisAsyncSetDisconnectCallback(c, appOnDisconnect); } } ``` 异步上下文 _应该_ 包含一个 *connect* callback 函数,该函数会在连接 尝试完成(无论成功还是出现错误)时被调用。 它 _可以_ 还包含一个 *disconnect* callback 函数,该函数会在连接 断开时(无论是由于错误还是用户请求)被调用。这两个 callback 都应 具有以下原型: ``` void(const redisAsyncContext *c, int status); ``` 在 *connect* 时,如果连接尝试成功,`status` 参数将被设置为 `REDIS_OK`。在这种 情况下,上下文已准备好接受命令。如果调用时返回了 `REDIS_ERR`,则说明连接 尝试失败。可以访问上下文中的 `err` 字段以找出错误的原因。 在连接尝试失败后,库会在调用 connect callback 后自动释放上下文对象。这可能是 创建新上下文并重试连接的好时机。 在 disconnect 时,如果断开连接是由 用户发起的,`status` 参数会被设置为 `REDIS_OK`;如果断开连接是由错误引起的,则为 `REDIS_ERR`。当它为 `REDIS_ERR` 时,可以访问上下文中的 `err` 字段以找出错误的原因。 上下文对象总是在 disconnect callback 触发后被释放。当需要重连时, disconnect callback 是进行此操作的好时机。 每个上下文只能设置一次 connect 或 disconnect callback。对于后续的调用, API 将返回 `REDIS_ERR`。设置 callback 的函数具有以下原型: ``` /* Alternatively you can use redisAsyncSetConnectCallbackNC which will be passed a non-const redisAsyncContext* on invocation (e.g. allowing writes to the privdata member). */ int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn); int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn); ``` `ac->data` 可用于向两个 callback 传递用户数据。一个典型的实现 可能类似于这样: ``` void appOnConnect(redisAsyncContext *c, int status) { myAppData *appData = (myAppData*)c->data; /* get my application specific context*/ appData->connecting = 0; if (status == REDIS_OK) { appData->connected = 1; } else { appData->connected = 0; appData->err = c->err; appData->context = NULL; /* avoid stale pointer when callback returns */ } appAttemptReconnect(); } void appOnDisconnect(redisAsyncContext *c, int status) { myAppData *appData = (myAppData*)c->data; /* get my application specific context*/ appData->connected = 0; appData->err = c->err; appData->context = NULL; /* avoid stale pointer when callback returns */ if (status == REDIS_OK) { appNotifyDisconnectCompleted(mydata); } else { appNotifyUnexpectedDisconnect(mydata); appAttemptReconnect(); } } ``` ### 发送命令及其 callback 在异步上下文中,由于事件循环的特性,命令会自动进行 pipelining。 因此,与同步 API 不同,发送命令的方式只有一种。 由于命令是异步发送到 Redis 的,因此发送命令需要一个在收到回复时调用的 callback 函数。回复 callback 应具有以下原型: ``` void(redisAsyncContext *c, void *reply, void *privdata); ``` `privdata` 参数可用于在命令 最初排队等待执行时,将任意数据传递给 callback。 在异步上下文中可用于发送命令的函数有: ``` int redisAsyncCommand( redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...); int redisAsyncCommandArgv( redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen); ``` 这两个函数的工作方式与它们的阻塞版本类似。当命令 成功添加到输出缓冲区时,返回值为 `REDIS_OK`,否则为 `REDIS_ERR`。例如:当根据用户请求 正在断开连接时,不能再向输出缓冲区添加新命令,并且在调用 `redisAsyncCommand` 系列时会返回 `REDIS_ERR`。 如果读取到 callback 为 `NULL` 的命令回复,它会立即被释放。当命令的 callback 不为 `NULL` 时,内存会在 callback 执行后立即释放:回复仅在 callback 执行期间有效。 当上下文遇到错误时,所有待处理的 callback 都会以 `NULL` 回复被调用。 对于发出的每个命令,除 **SUBSCRIBE** 和 **PSUBSCRIBE** 外,callback 都会被 准确调用一次。即使上下文对象被断开或删除,每个待处理的 callback 也会以 `NULL` 回复被调用。 对于 **SUBSCRIBE** 和 **PSUBSCRIBE**,callback 可能会被重复调用,直到收到 `unsubscribe` 消息。这将是该 callback 的最后一次调用。如果发生错误,callback 可能会收到一个最终的 `NULL` 回复。 ### 断开连接 可以使用以下方法终止异步连接: ``` void redisAsyncDisconnect(redisAsyncContext *ac); ``` 调用此函数时,连接 **不会** 立即终止。相反,它将不再接受新 命令,只有在所有待处理的命令 都已写入 socket,且它们各自的回复都已被读取,各自的 callback 都已执行完毕后,连接才会终止。在此之后,断开连接的 callback 会以 `REDIS_OK` 状态执行,随后上下文对象将被释放。 可以使用以下方法强制断开连接 ``` void redisAsyncFree(redisAsyncContext *ac); ``` 在这种情况下,不会再向 socket 写入任何内容,所有待处理的 callback 都会以 `NULL` 回复被调用,然后断开连接的 callback 会以 `REDIS_OK` 被调用,之后上下文对象 将被释放。 ### 将其挂钩到事件库 *X* 在上下文对象创建后,需要在它上面设置几个 hooks。 有关 *libev* 和 *libevent* 的绑定,请参阅 `adapters/` 目录。 ## 回复解析 API Hiredis 附带了一个回复解析 API,使得编写更高级 语言绑定变得很容易。 回复解析 API 包含以下函数: ``` redisReader *redisReaderCreate(void); void redisReaderFree(redisReader *reader); int redisReaderFeed(redisReader *reader, const char *buf, size_t len); int redisReaderGetReply(redisReader *reader, void **reply); ``` 在创建正常的 Redis 上下文时,hiredis 内部也使用了同一套 函数,上述 API 只是将其暴露给用户以便直接 使用。 ### 用法 函数 `redisReaderCreate` 创建一个 `redisReader` 结构体,它包含一个 存放未解析数据的缓冲区以及协议解析器的状态。 传入的数据(很可能来自 socket)可以使用 `redisReaderFeed` 放入 `redisReader` 的内部缓冲区中。此函数会 将 `buf` 所指向的缓冲区复制 `len` 个字节。当调用 `redisReaderGetReply` 时,这些数据会被解析。此函数返回一个整数状态 并通过 `void **reply` 返回一个回复对象(如上所述)。返回的状态 可以是 `REDIS_OK` 或 `REDIS_ERR`,后者意味着出现了问题 (协议错误或内存不足错误)。 解析器将多批量 payload 的嵌套级别限制为 7。如果 多批量嵌套级别高于此值,解析器将返回错误。 ### 自定义回复 函数 `redisReaderGetReply` 会创建 `redisReply` 并使函数 参数 `reply` 指向所创建的 `redisReply` 变量。例如, 如果响应类型为 `REDIS_REPLY_STATUS`,则 `redisReply` 的 `str` 字段 将作为普通的 C 字符串保存该状态。但是,负责 创建 `redisReply` 实例的函数可以通过 设置 `redisReader` 结构体上的 `fn` 字段来进行自定义。这应该在 创建 `redisReader` 之后立即完成。 例如,[hiredis-rb](https://github.com/pietern/hiredis-rb/blob/master/ext/hiredis_ext/reader.c) 使用自定义的回复对象函数来创建 Ruby 对象。 ### Reader 最大缓冲区 无论是直接使用 Reader API,还是通过 普通的 Redis 上下文间接使用它,redisReader 结构体都会使用一个缓冲区来 累积来自服务器的数据。 通常,当缓冲区为空且大于 16 KiB 时,它会被销毁,以避免在未使用的缓冲区中浪费内存。 然而,在处理非常大的 payload 时,销毁缓冲区可能会大大 降低性能,因此可以将 reader 结构体的 `maxbuf` 字段的值 修改为所需的值,从而更改空闲缓冲区的最大大小。特殊值 0 意味着空闲缓冲区没有最大 值限制,因此缓冲区永远不会被释放。 例如,如果你有一个普通的 Redis 上下文,你可以只需通过以下方式将最大空闲 缓冲区设置为零(无限制): ``` context->reader->maxbuf = 0; ``` 只有在处理大 payload 时为了最大化性能才应这样做。应尽快将上下文重新设置为 `REDIS_READER_MAX_BUF`, 以防止分配无用的内存。 ### Reader 最大数组元素 默认情况下,hiredis 回复解析器将多批量元素的最大数量 设置为 2^32 - 1 即 4,294,967,295 个条目。如果你需要处理元素数量 超过此值的多批量回复,你可以将值设置得更高,或设置为零,这意味无限制: ``` context->reader->maxelements = 0; ``` ## SSL/TLS 支持 ### 构建 SSL/TLS 支持默认不会构建,需要显式的 flag: ``` make USE_SSL=1 ``` 这要求 OpenSSL 开发包可用(例如,包含头文件)。 启用后,SSL/TLS 支持将被构建到额外的 `libhiredis_ssl.a` 和 `libhiredis_ssl.so` 静态/动态库中。这使得原有的库 不受影响,因此不会引入任何额外的依赖项。 ### 使用它 首先,你需要确保包含了 SSL 头文件: ``` #include #include ``` 你还需要 **额外** 链接到 `libhiredis_ssl`, 以及 `libhiredis`,并添加 `-lssl -lcrypto` 以满足其依赖项。 Hiredis 在其正常的 `redisContext` 或 `redisAsyncContext` 之上实现了 SSL/TLS,因此你需要先建立连接,然后再 发起 SSL/TLS 握手。 #### Hiredis OpenSSL 封装器 在 Hiredis 协商 SSL/TLS 连接之前,必须 初始化 OpenSSL 并创建上下文。你可以通过两种方式做到这一点: 1. 直接使用 OpenSSL API 初始化库的全局上下文 并创建 `SSL_CTX *` 和 `SSL *` 上下文。有了 `SSL *` 对象,你就可以 调用 `redisInitiateSSL()`。 2. 使用一组由 Hiredis 提供的围绕 OpenSSL 的封装器,创建一个 `redisSSLContext` 对象来保存配置,并使用 `redisInitiateSSLWithContext()` 发 SSL/TLS 握手。 ``` /* An Hiredis SSL context. It holds SSL configuration and can be reused across * many contexts. */ redisSSLContext *ssl_context; /* An error variable to indicate what went wrong, if the context fails to * initialize. */ redisSSLContextError ssl_error = REDIS_SSL_CTX_NONE; /* Initialize global OpenSSL state. * * You should call this only once when your app initializes, and only if * you don't explicitly or implicitly initialize OpenSSL it elsewhere. */ redisInitOpenSSL(); /* Create SSL context */ ssl_context = redisCreateSSLContext( "cacertbundle.crt", /* File name of trusted CA/ca bundle file, optional */ "/path/to/certs", /* Path of trusted certificates, optional */ "client_cert.pem", /* File name of client certificate file, optional */ "client_key.pem", /* File name of client private key, optional */ "redis.mydomain.com", /* Server name to request (SNI), optional */ &ssl_error); if(ssl_context == NULL || ssl_error != REDIS_SSL_CTX_NONE) { /* Handle error and abort... */ /* e.g. printf("SSL error: %s\n", (ssl_error != REDIS_SSL_CTX_NONE) ? redisSSLContextGetError(ssl_error) : "Unknown error"); // Abort */ } /* Create Redis context and establish connection */ c = redisConnect("localhost", 6443); if (c == NULL || c->err) { /* Handle error and abort... */ } /* Negotiate SSL/TLS */ if (redisInitiateSSLWithContext(c, ssl_context) != REDIS_OK) { /* Handle error, in c->err / c->errstr */ } ``` ## RESP3 PUSH 回复 Redis 6.0 引入了回复类型为 `>` 的 PUSH 回复。这些消息是自发产生的,并且可能在任何时候到达,因此必须使用 callback 来处理。 ### 默认行为 Hiredis 默认在 `redisContext` 和 `redisAsyncContext` 上安装了处理程序,它们会拦截并释放检测到的任何 PUSH 回复。这意味着现有的代码在升级到 Redis 6 并切换到 `RESP3` 后可以直接照常工作。 ### 自定义 PUSH 处理程序原型 `redisContext` 和 `redisAsyncContext` 之间的 callback 原型有所不同。 #### redisContext ``` void my_push_handler(void *privdata, void *reply) { /* Handle the reply */ /* Note: We need to free the reply in our custom handler for blocking contexts. This lets us keep the reply if we want. */ freeReplyObject(reply); } ``` #### redisAsyncContext ``` void my_async_push_handler(redisAsyncContext *ac, void *reply) { /* Handle the reply */ /* Note: Because async hiredis always frees replies, you should not call freeReplyObject in an async push callback. */ } ``` ### 安装自定义处理程序 有两种方法可以设置你自己的 PUSH 处理程序。 1. 在 `redisOptions` 结构体中设置 `push_cb` 或 `async_push_cb`,并使用 `redisConnectWithOptions` 或 `redisAsyncConnectWithOptions` 进行连接。 redisOptions = {0}; REDIS_OPTIONS_SET_TCP(&options, "127.0.0.1", 6379); options->push_cb = my_push_handler; redisContext *context = redisConnectWithOptions(&options); 2. 在已连接的上下文上调用 `redisSetPushCallback` 或 `redisAsyncSetPushCallback`。 redisContext *context = redisConnect("127.0.0.1", 6379); redisSetPushCallback(context, my_push_handler); _注意 `redisSetPushCallback` 和 `redisAsyncSetPushCallback` 都会返回当前配置的任何处理程序,这使得覆盖并随后恢复到旧值变得很容易。_ ### 指定不使用处理程序 如果你有一个特殊的使用场景,不希望 hiredis 自动拦截并释放 PUSH 回复,你会希望根本不配置任何处理程序。这可以通过两种方式完成。 1. 在 `redisOptions` 中设置 `REDIS_OPT_NO_PUSH_AUTOFREE` flag,并将 callback 函数指针保留为 `NULL`。 redisOptions = {0}; REDIS_OPTIONS_SET_TCP(&options, "127.0.0.1", 6379); options->options |= REDIS_OPT_NO_PUSH_AUTOFREE; redisContext *context = redisConnectWithOptions(&options); 2. 连接后调用带有 `NULL` 的 `redisSetPushCallback`。 redisContext *context = redisConnect("127.0.0.1", 6379); redisSetPushCallback(context, NULL); _注意:在未配置处理程序的情况下,对 `redisCommand` 的调用可能会生成多个回复,因此此策略仅在存在某种阻塞的 `redisGetReply()` 循环时才适用(例如 `MONITOR` 或 `SUBSCRIBE` 工作负载)。_ ## Allocator 注入 Hiredis 使用了在 [alloc.h](https://github.com/redis/hiredis/blob/f5d25850/alloc.h#L41) 中定义的包含当前配置的分配和释放函数的函数指针直通结构体。默认情况下,它们仅仅是指向 libc(`malloc`、`calloc`、`realloc` 等)。 ### 覆盖 可以像这样覆盖 allocator: ``` hiredisAllocFuncs myfuncs = { .mallocFn = my_malloc, .callocFn = my_calloc, .reallocFn = my_realloc, .strdupFn = my_strdup, .freeFn = my_free, }; // Override allocators (function returns current allocators if needed) hiredisAllocFuncs orig = hiredisSetAllocators(&myfuncs); ``` 要将 allocator 重置为其默认的 libc 函数,只需调用: ``` hiredisResetAllocators(); ``` ## 作者 Salvatore Sanfilippo (antirez at gmail),\ Pieter Noordhuis (pcnoordhuis at gmail)\ Michael Grunder (michael dot grunder at gmail) _Hiredis 是在 BSD 许可证下发布的。_
标签:Redis, 客户端加密, 客户端库, 数据库连接器, 网络编程