ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

使用 Hiredis 0.14.1 开发 Redis C 客户端:同步、异步与回复解析 API 实战指南

使用 Hiredis 0.14.1 开发 Redis C 客户端:同步、异步与回复解析 API 实战指南 使用 Hiredis 0.14.1 开发 Redis C 客户端同步、异步与回复解析 API 实战指南【免费下载链接】ZeroTierOneA Smart Ethernet Switch for Earth项目地址: https://gitcode.com/GitHub_Trending/ze/ZeroTierOneHiredis 是 Redis 官方生态中最轻量的 C 客户端库它只做两件事把 C 语言调用按 Redis 协议RESP编码成命令发送出去再把服务器返回的字节流解析成结构化的redisReply。本文以本仓库中随附的 ext/hiredis-0.14.1/README.md 为骨架完整讲解它的同步 API、异步 API、回复解析 API 三大接口体系并结合仓库源码说明它在 ZeroTier 控制器CentralDB中的真实落地方式——即如何作为 redis-plus-plus 的底层协议引擎支撑控制器的成员状态同步与网络变更监听。读完本文你将能独立使用 Hiredis 编写阻塞式、流水线式与事件驱动式三种风格的 Redis 客户端代码并理解它被上层语言绑定复用的设计原理。一、Hiredis 是什么Hiredis 是一个极简主义的 Redis C 客户端库minimalistic C client library。说它极简是因为它在协议层面只做最小化的支持但与此同时它通过一套 printf 风格的命令格式化 API让调用者的使用体验远高于其精简代码量所暗示的水平——开发者无需为每一条 Redis 命令手写专属绑定函数。除了发送命令与接收回复之外Hiredis 还附带一个与 I/O 层完全解耦的回复解析器stream parser。这个解析器被设计成可独立复用的流式解析器例如高级语言绑定如 Ruby 的 hiredis-rb可以直接借用它来高效解析回复而不必重新实现 RESP 协议。Hiredis 只支持二进制安全的 Redis 协议因此可用于任何版本 1.2.0的 Redis 服务器。库本身对外提供三种 API同步 APISynchronous API异步 APIAsynchronous API回复解析 APIReply parsing API本仓库将该库以源码形式完整随附在 ext/hiredis-0.14.1/ 下同时还有更新的 1.0.2 版本并在 cmake/redis-plus-plus.cmake 中将其作为 redis-plus-plus 静态库的底层依赖链接进构建产物具体使用方式见后文仓库中的实际应用一节。二、升级注意0.13.x → 0.14.x 的破坏性变更如果是从 0.13.x 升级而来0.14.x 引入了两处需要开发者注意的破坏性变更详见 ext/hiredis-0.14.1/CHANGELOG.md长度边界收紧为协议错误bulk 与 multi-bulk 的长度如果小于-1或大于LLONG_MAX现在会被判定为协议错误这与 RESP 规范保持一致在 32 位平台上上限进一步降低为SIZE_MAX。redisReply.len类型改为size_t由于它表示字符串的长度用户代码中的比较逻辑应改为与size_t类型比较如果之前做过强制类型转换现在可以移除反之如果需要与其他类型比较则可能仍需显式转换。三、同步 API同步 API 只需掌握三个函数即可上手redisContext *redisConnect(const char *ip, int port); void *redisCommand(redisContext *c, const char *format, ...); void freeReplyObject(void *reply);3.1 建立连接redisConnect用于创建所谓的redisContext上下文是 Hiredis 保存连接状态的地方。redisContext结构体包含一个整型字段err当连接处于错误状态时其值非零字段errstr则保存人类可读的错误描述字符串详细错误分类见错误处理一节。连接建立后必须检查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(Cant allocate redis context\n); } }注意redisContext不是线程安全的多个线程不能共享同一个上下文。3.2 发送命令发送命令的首选方式是redisCommand它接受类似 printf 的格式化字符串。最简形式reply redisCommand(context, SET foo bar);%s说明符会把一个 C 字符串内插进命令并用strlen计算其长度reply redisCommand(context, SET foo %s, value);当需要传递二进制安全的字符串时使用%b说明符它除了字符串指针外还需要一个size_t类型的长度参数reply redisCommand(context, SET foo %b, value, (size_t) valuelen);内部实现上Hiredis 会把命令拆分成多个参数再按 Redis 协议RESP编码后发送。参数之间以空格分隔因此你可以在一个参数内的任意位置使用格式化说明符reply redisCommand(context, SET key:%s %s, myid, value);3.3 处理回复redisCommand的返回值在命令成功执行时持有回复对象出错时返回NULL同时上下文的err字段被置位见错误处理一节。一旦出错该上下文就不能再复用必须建立新连接。标准回复对象的类型为redisReply通过其type字段判断回复种类REDIS_REPLY_STATUS状态回复。状态字符串通过reply-str访问长度通过reply-len访问。REDIS_REPLY_ERROR错误回复。错误字符串的访问方式与REDIS_REPLY_STATUS相同。REDIS_REPLY_INTEGER整数回复。通过reply-integer字段访问类型为long long。REDIS_REPLY_NILnil 对象没有可访问的数据。REDIS_REPLY_STRINGbulk字符串回复。值通过reply-str访问长度通过reply-len访问。REDIS_REPLY_ARRAYmulti bulk 回复。元素个数存放在reply-elements中每个元素本身也是一个redisReply对象通过reply-element[..index..]访问。Redis 可能返回嵌套数组Hiredis 完全支持。回复对象使用freeReplyObject()释放。该函数会递归释放数组及其嵌套数组中的子回复对象用户不需要也不应该手动释放子回复——手动释放反而会破坏内存。重要提醒使用异步 API 时当前版本的 Hiredis 会在回调返回后自动清理回复对象因此不要在异步回调里调用freeReplyObject。该行为在未来版本中可能变化升级时请留意 CHANGELOG.md。3.4 清理连接断开连接并释放上下文使用void redisFree(redisContext *c);该函数会立即关闭 socket然后释放创建上下文时分配的所有内存。3.5 用 redisCommandArgv 发送命令与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 如何在阻塞连接上实现流水线需要先弄懂其内部执行流程当调用redisCommand家族的任一函数时Hiredis 首先把命令按 Redis 协议格式化然后将格式化后的命令放入上下文的输出缓冲区。该缓冲区是动态的可以容纳任意数量的命令。命令入队后接着调用redisGetReply它有两套执行路径输入缓冲区非空尝试从输入缓冲区解析出一条回复并返回若无法解析出回复则继续走路径 2。输入缓冲区为空把整个输出缓冲区一次性写入 socket从 socket 读取数据直到能解析出第一条回复。redisGetReply是 Hiredis API 的导出函数当期望 socket 上有回复时可以直接调用。要实现流水线唯一要做的就是填满输出缓冲区。为此提供了两个与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字段查明原因。下面的例子展示了一个简单流水线最终只产生一次write(2)和一次read(2)系统调用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);该 API 还可用于实现阻塞式订阅者reply redisCommand(context,SUBSCRIBE foo); freeReplyObject(reply); while(redisGetReply(context,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字段都会被设置为该错误的字符串描述。六、异步 APIHiredis 的异步 API 可以与任意事件库配合使用仓库随附了与 libev、libevent、libuv、glib、aeRedis 自身的事件库、ivykis 和 macOS 的适配器全部位于 adapters/ 目录并有对应的 examples 示例代码。6.1 建立异步连接redisAsyncConnect用于建立到 Redis 的非阻塞连接返回新建的redisAsyncContext结构体指针。创建后应检查err字段确认是否有错误。由于连接是非阻塞的内核无法立即返回主机与端口是否可接受连接。注意redisAsyncContext同样不是线程安全的。redisAsyncContext *c redisAsyncConnect(127.0.0.1, 6379); if (c-err) { printf(Error: %s\n, c-errstr); // handle error }异步上下文可以持有断开回调disconnect callback在连接断开时无论是出错还是用户主动断开被调用。回调原型void(const redisAsyncContext *c, int status);断开时若断开由用户发起status为REDIS_OK若由错误引起则为REDIS_ERR。为REDIS_ERR时可通过上下文的err字段查明原因。断开回调触发后上下文对象总是会被释放如果需要重连断开回调是执行重连的理想位置。断开回调每个上下文只能设置一次再次设置会返回REDIS_ERR。设置函数的原型int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn);6.2 发送命令与回复回调在异步上下文中由于事件循环的本质命令会被自动流水线化因此与同步 API 不同异步模式只有一种发送命令的方式。因为命令是异步发送的发出命令时必须同时提供回复到达时被调用的回调。回复回调原型void(redisAsyncContext *c, void *reply, void *privdata);privdata参数可用来把任意数据从命令入队处携带curry到回调中。异步上下文可用的命令发送函数为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。如果某命令的回调为NULL其回复被读取后会立即释放回调非NULL时内存在回调执行完毕后立即释放——回复只在回调执行期间有效。当上下文遇到错误时所有挂起的回调都会以NULL回复被调用。6.3 断开异步连接void redisAsyncDisconnect(redisAsyncContext *ac);调用该函数不会立即终止连接而是不再接受新命令只有当所有挂起命令都已写入 socket、对应回复已被读取且回调都已执行完毕后连接才被终止。此后断开回调以REDIS_OK状态被执行上下文对象被释放。6.4 接入事件库 X要接入某个事件库需要在上下文创建后设置几个钩子。仓库 adapters/ 目录中提供了 libev 与 libevent 等事件库的绑定实现例如 example-libev.c 与 example-libevent.c 展示了完整的接入流程。七、回复解析 APIHiredis 的回复解析 API 让编写高级语言绑定变得容易。它由以下函数组成redisReader *redisReaderCreate(void); void redisReaderFree(redisReader *reader); int redisReaderFeed(redisReader *reader, const char *buf, size_t len); int redisReaderGetReply(redisReader *reader, void **reply);这组函数正是 Hiredis 创建普通 Redis 上下文时的内部实现上述 API 只是把它暴露给用户直接使用。7.1 使用方式redisReaderCreate创建一个redisReader结构体其中保存了未解析数据的缓冲区与协议解析器的状态。来自 socket 的输入数据用redisReaderFeed放入redisReader的内部缓冲区——该函数会把buf指向的len字节拷贝一份。数据在调用redisReaderGetReply时被解析它返回一个整型状态码并通过void **reply输出回复对象类型见处理回复一节。返回状态为REDIS_OK或REDIS_ERR后者表示出错了协议错误或内存不足。解析器把 multi bulk 负载的嵌套层级限制为 7 层超过该深度会返回错误。7.2 自定义回复对象redisReaderGetReply创建redisReply并让reply参数指向它。例如对REDIS_REPLY_STATUS类型的回复redisReply的str字段保存的是普通 C 字符串。而负责创建redisReply实例的函数可以被定制——通过设置redisReader结构体上的fn字段实现且应在创建redisReader之后立即设置。例如 hiredis-rb 就通过定制回复对象函数来创建 Ruby 对象这也是本仓库 ext/hiredis-0.14.1/README.md 中官方给出的跨语言复用案例。7.3 调整 Reader 最大缓冲区maxbuf无论直接使用 Reader API还是通过普通 Redis 上下文间接使用redisReader结构体都会用一个缓冲区来累积来自服务器的数据。通常当缓冲区为空且大于 16 KiB 时会被销毁以避免浪费内存。但当处理非常大的负载时频繁销毁缓冲区会显著拖慢性能因此可以修改maxbuf字段来调整空闲缓冲区的最大尺寸。特殊值0表示空闲缓冲区没有上限缓冲区永远不会被释放。例如对于普通 Redis 上下文把最大空闲缓冲区设为无限context-reader-maxbuf 0;该设置只应在处理大负载时为了最大化性能而使用并且应尽快恢复为REDIS_READER_MAX_BUF防止后续分配无用内存。八、在 ZeroTier 仓库中的实际应用本仓库把 Hiredis 0.14.1 与 1.0.2 两个版本以源码形式随附在 ext/hiredis-0.14.1/ 与 ext/hiredis-1.0.2/ 下这与 ext/README.md 的说明一致该目录存放在目标平台系统库缺失时编译进二进制的第三方库。ZeroTier 的控制器并未直接调用 Hiredis 的 C API而是通过其上层的redis-plus-plusC 客户端间接使用Hiredis 在其中扮演 RESP 协议编解码的底层引擎角色。8.1 构建链路Hiredis 作为 redis-plus-plus 的协议底层cmake/redis-plus-plus.cmake 中有一段非常关键的注释直接印证了 Hiredis 在整个依赖链中的位置rediss static library calls into hiredis but does not propagate that dependency to consumers, so the final link fails with undefined hiredis symbols (redisAppendCommand, redisFree, freeReplyObject, ...). Attach hiredis to the static targets interface.即 redis-plus-plus 的静态库会调用 Hiredis 的符号如redisAppendCommand、redisFree、freeReplyObject正是本文前面介绍的 API但未向消费者传递该依赖因此构建脚本通过find_library(HIREDIS_LIB hiredis REQUIRED)查找系统 Hiredis 库并链接到redis_static的 INTERFACE 上之所以用find_library而非find_package是因为 Debian 的libhiredis-dev不提供 CMake 配置。这条链接关系说明Hiredis 的同步 API、命令入队append与回复释放等能力正是上层 C 客户端乃至控制器数据库层赖以工作的基础。8.2 CentralDBRedis 在 ZeroTier 控制器中的三种用法在控制器目录 nonfree/controller/ 中Redis 通过环境变量与配置结构接入Redis.hpp 定义了ZeroTier::RedisConfig结构体包含hostname、port、password与clusterMode四个字段用于描述单机/集群两种部署形态。CentralDB.cpp 根据ZT_REDIS_MEMBER_STATUS等环境变量决定是否启用 Redis 成员状态随后依据LISTENER_MODE_REDIS与STATUS_WRITER_MODE_REDIS两种模式把RedisConfig转成 redis-plus-plus 的sw::redis::ConnectionOptions与ConnectionPoolOptions再按clusterMode分别创建sw::redis::RedisCluster或sw::redis::Redis实例。RedisListener.hpp 中RedisListener派生类RedisNetworkListener、RedisMemberListener各自启动一个监听线程订阅网络/成员变更通知——这正是前面异步/订阅思路在事件循环之外的线程模型变体。RedisStatusWriter.hpp 的RedisStatusWriter实现了StatusWriter接口将节点状态network_id、node_id、os、arch、version、地址、last_seen批量写入 Redis并通过_doWritePending使用事务sw::redis::Transaction保证一致性。如果你对这套通知监听 状态上报模式感兴趣可以继续阅读 nonfree/controller/README_CENTRAL_CONTROLLER.md 与 ext/central-controller-docker/README.md 了解部署方式。九、构建与自测Hiredis 0.14.1 随附完整的 Makefileext/hiredis-0.14.1/Makefile与自测程序ext/hiredis-0.14.1/test.c支持通过make编译出静态库libhiredis.a并运行make test执行单元测试。核心头文件 hiredis.h及 net.h、async.h声明了本文涉及的全部 API是查阅符号与结构体字段的第一手资料。仓库同时提供了 example.c同步示例与 libev/libevent/libuv 等异步示例可作为快速上手的模板。十、总结Hiredis 以最小协议支持 高层格式化 API的设计哲学覆盖了 Redis C 客户端开发的三种典型形态同步阻塞调用、流水线批处理、事件驱动异步回调并通过独立的流式回复解析器把 RESP 协议解析能力开放给任何上层语言绑定复用。在本仓库中它作为 redis-plus-plus 的底层协议引擎被链接进 ZeroTier 控制器的构建产物支撑 CentralDB 的 Redis 成员状态同步与网络变更监听。掌握本文介绍的redisConnect/redisCommand/redisGetReply同步链路、redisAsyncConnect回调机制与redisReader解析管线即可在各类 C/C 项目中稳定、高效地接入 Redis。【免费下载链接】ZeroTierOneA Smart Ethernet Switch for Earth项目地址: https://gitcode.com/GitHub_Trending/ze/ZeroTierOne创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表