ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Windows下C++接入Redis:预编译hiredis库的VS配置与避坑指南

Windows下C++接入Redis:预编译hiredis库的VS配置与避坑指南 简介面向 Windows 开发者的 Redis 编译库资源包旨在解决在原生 Linux 环境外集成 Redis 的难题。压缩包内含 hiredis.lib 与 Win32_Interop.lib前者负责 C 语言客户端与 Redis 服务器的高效通信后者提供 Windows 兼容层支持资源同时附带配套客户端头文件与实现源码便于开发者直接链接调用。包内按 x86 与 x64 分为两套运行库并涵盖调试版与发布版适用于 32 位和 64 位 Windows 环境下的 C/C 项目是希望快速接入 Redis 的中初级开发者的实用工具。资源包共 20 个文件以库文件、可执行程序、头文件、源码和说明文档为主整体大小 6.9MB小巧但体系完整。已有 1723 人学习下载。通过解压可获得完整的 Windows 下编译链接方案包括多线程调试版与多线程发布版运行库配置能省去自行移植与编译的繁琐过程在此基础上开发者还能结合 Redis 的字符串、哈希、列表等常用数据结构特性进一步掌握客户端调用方法和跨平台编译的注意事项。1. 拿到这套Windows版Redis库先搞清楚它替你省了什么在Windows上给C/C项目接Redis最常见的卡点不是业务逻辑而是连一个能编译、能链接、能跑起来的客户端库都要费一番周折。标题里这套Windows下编译好的Redis库正是解决这个问题hiredis.lib是Redis官方C客户端的编译产物负责命令发送和RESP响应解析Win32_Interop.lib处理Windows的Winsock初始化与平台适配配套的头文件让Visual Studio工程可以直接引用不用你自己去跑CMake、调Makefile。适合三类人老桌面程序要接入Redis缓存、工具链不方便现场编译第三方库的团队、以及不想在平台适配细节上反复交学费的C/C开发者。拿到它重点就转移到业务代码上。2. 拆开库的底细hiredis和Win32_Interop.lib各管哪一段先明确一件事这套库是Redis的客户端库不是Redis服务端。服务端是那个常驻内存的redis-server进程而这里说的是让你自己的程序能“跟Redis说话”的那一层。很多刚接手的同事会把两者搞混结果在配置环境时走了不少弯路——服务端要装的是Redis安装包或Docker镜像而客户端要引的是hiredis.lib和Win32_Interop.lib。分清这两层后面所有配置才不会张冠李戴。2.1 hiredis是Redis官方C客户端为什么Windows下编译这么费劲hiredis是Redis官方维护的C语言客户端核心能力可以拆成四块。第一是连接管理负责和redis-server建立TCP连接并暴露连接超时和读写超时参数第二是协议层把RESP协议Redis Serialization Protocol的编解码封装好了你不需要手写字节流去构造请求也不用逐字节解析服务端返回第三是命令封装redisCommand、redisCommandArgv这两个API支持类似printf的格式化写法命令拼装错误率能降下来不少第四是双API结构同步API适合普通业务线程异步APIasync.h里那套适合需要高吞吐、不想阻塞线程的事件驱动场景。在Linux下hiredis的编译基本是一条make命令的事因为它的源码默认面向POSIX环境socket、connect、read、write这些函数直接落在系统接口上。可这套代码挪到Windows下就麻烦得多Windows没有unistd.h和sys/socket.hsocket能力全部收敛到Winsock2里函数名和错误码体系都变了更隐蔽的是Windows要求程序在第一次使用socket前必须调用WSAStartup完成Winsock初始化否则后续系统调用全部静默失败。这一条在Linux上完全不存在所以很多人在Windows下编译hiredis时明明configure和build都过了跑起来却连不上。第二个编译难点是头文件的包含顺序。Windows平台的经典冲突是windows.h和winsock2.h都定义了某些网络相关的宏和类型一旦windows.h被先包含winsock2.h就会报一堆重定义错误。hiredis源码里为了避免这个问题通常做法是要求调用方先包含winsock2.h或者工程里预先定义WIN32_LEAN_AND_MEAN来裁剪windows.h。但这些细节在Visual Studio工程模板里默认不是开着的于是编译报错就成了常态。第三是工程文件的缺失hiredis官方仓库主要维护Makefile和CMakeLists对Visual Studio的.sln/.vcxproj支持并不完整你要么用CMake生成工程要么手动组织源文件列表哪个都不省心。所以预编译库的价值不在“省掉一条编译命令”而在于把上面三件事全部替你处理完源码的POSIX到Winsock映射、WSAStartup初始化时机、以及头文件包含顺序都已经调好。你拿到手的是一个相对黑匣子的模块只要头文件路径指对、lib链接上就能专注于Redis命令本身。我通常拿到这类预编译库后第一件事是打开依赖工具看一眼它引用了哪些系统库如果依赖列表里出现了ws2_32说明Winsock适配确实打进去了可以放心用。2.2 Win32_Interop.lib替你把Winsock初始化和平台差异封装好Win32_Interop.lib这个名字里的“Interop”已经说明了它的角色它是hiredis和Windows运行环境之间的互操作层。简单说hiredis本身是跨平台的但跨平台的部分集中在网络系统调用附近而Windows恰恰在这块和POSIX差别最大。Win32_Interop.lib做的就是把这层差异封装成Windows下可链接的代码典型任务包括封装WSAStartup/WSACleanup的调用时机、把hiredis内部的socket创建方式替换成Winsock的socket创建方式、以及把WSA错误码映射到hiredis的err/errstr字段上让你的出错处理代码不必区分平台。业务代码是否需要直接调用Win32_Interop里的函数取决于这套库封装到什么程度。一种常见的设计是在库的初始化函数里自动完成WSAStartup你引用Win32_Interop.lib后无需额外调用另一种设计是暴露一个显式的初始化函数要求在main函数的第一行调用。我的建议是无论头文件里是否声明了初始化函数都在程序入口显式调用一次WSAStartup做双保险代价几乎为零但能规避掉很大一类“初始化时机不确定”的问题。尤其是程序里其他第三方库也可能调用WSAStartupWinsock做了引用计数多次调用不会出错这点和Linux的socket初始化方式完全不同。注意一点Windows下的Winsock还分版本。老代码可能只初始化了Winsock 1.1但hiredis需要的是Winsock 2.2宏定义是MAKEWORD(2,2)。如果你发现自己的程序里本来就有WSAStartup调用要确认请求的版本号是高版本否则后续bind、connect都可能返回WSAEINVAL这类怪错误。Win32_Interop.lib如果内部封装了初始化通常会直接请求Winsock 2.2这也是它能“开箱即用”的原因之一。2.3 拿到文件先核对这四样否则后面全是白干拿到一个hello_world能编译通过之后紧接着就要面对链接和运行时的问题。文件是否齐全是最容易忽略的一点。典型的一套文件至少包含这些我先列表说明。文件作用使用方式hiredis.libhiredis的静态库或导入库链接器附加依赖项加入Win32_Interop.libWinsock互操作辅助库链接器附加依赖项加入hiredis.h同步API的头文件源码包含async.h异步API的头文件异步场景下包含read.h、sds.h、alloc.hhiredis内部数据结构一般不需要直接包含win32.h 或类似头文件Windows宏定义与兼容声明由hiredis.h间接包含拿到文件之后我的习惯是核对四件事任何一件不对后边调试都相当被动。第一是位数x86的lib不能放到x64工程里反过来也一样Visual Studio里平台下拉框和lib文件名要能对应上第二是CRT运行库lib是用/MD动态链接CRT还是/MT静态链接CRT编译的工程里“C/C→代码生成→运行库”要与之保持一致否则链接期可能只给你一个LNK4098警告运行时却莫名崩溃第三是字符集虽然hiredis本身以字节流为主字符集影响不大但工程里长时间用Unicode的会默认定义UNICODE这不影响lib链接只是提醒你注意字符串API的版本第四是VS工具集版本高版本VS通常能兼容低版本工具集编译的lib但反过来不成立如果lib是用特别老的VC6时代的工具链编的在VS2019里链接会有符号修饰问题。这四件事里CRT运行时不一致是最隐蔽的坑因为它不一定在链接时报错而可能表现为“Debug下正常、Release下崩溃”或“运行一小段后堆内存损坏”。我一般是打开Visual Studio的开发者命令行用dumpbin /headers查看lib的机器码位数和DLL特征也能用dumpbin /directives查看它声明的默认库名比如如果显示/defaultlib:msvcrt说明它是动态CRT的Release版对应工程的/MD。这些信息比猜文件后缀名要可靠得多。3. 把库接进Visual Studio工程三步配置和一段能跑的Demo文件核对完下一步就是把它真正接进工程。Visual Studio的工程配置项看着多但和这个库相关的只有三处配置完就能跑通最小示例。3.1 Visual Studio工程里的三个关键配置项新建一个空项目或者使用已有C/C工程接入这套库只需要动工程属性里的几处地方。第一处是“VC目录→包含目录”把头文件所在目录加进去让#include hiredis/hiredis.h能直接被找到第二处是“VC目录→库目录”把.lib文件所在目录加进去第三处是“链接器→输入→附加依赖项”填上hiredis.lib、Win32_Interop.lib以及Windows网络库ws2_32.lib。最后这一项很多人会漏掉因为hiredis在Linux下不需要额外指定网络库但在Windows下所有socket相关符号都来自ws2_32缺了它就会在链接阶段爆出一堆未解析符号。配置里还有一个值得提前确认的地方“C/C→代码生成→运行库”要跟lib的CRT匹配。我在上一章强调过这里直接给一个实用原则工程默认值是“多线程调试DLL/多线程DLL”/MDd或/MD如果这套预编译库也是/MD编的直接默认即可如果不确定就先按/MD跑链接出现LNK2038或LNK4098再往/MT切。注意这个参数在Debug和Release两种配置下分别设置不要只改一个配置就以为全部生效。关于包含目录不要为了省事直接把整个库源码目录塞进包含路径。我见过有人把第三方源码目录和工程源码目录混在一个include搜索路径里结果不同目录下的同名头文件互相遮蔽hiredis.h和async.h倒是能包含但内部的相对引用顺序乱了编译报错就开始变玄学。干净的做法是单独建一个ThirdParty/RedisClient目录把.h和.lib按include、lib两个子目录分好工程里只引用这个隔离目录。3.2 一段能跑的最小Demo连接、SET、GET配置完成后写一段最小可运行的程序验证链路。下面这段代码在普通控制台工程里可以直接编译运行前提是你本机已经有一个Redis服务端在6379端口监听安装方式可以是本机跑redis-server也可以是Docker映射了6379的容器。我用redisConnectWithTimeout而不是redisConnect目的是让连接阶段有个明确的超时避免Redis服务端没启动时程序卡在系统默认的超时上。#include stdio.h #include stdlib.h #include string.h #include hiredis/hiredis.h int main(void) { // 连接参数IP、端口、3秒连接超时 struct timeval timeout {3, 0}; redisContext *ctx redisConnectWithTimeout(127.0.0.1, 6379, timeout); if (ctx NULL) { printf(内存不足无法创建 redisContext\n); return -1; } if (ctx-err ! 0) { printf(连接 Redis 失败: %s\n, ctx-errstr); redisFree(ctx); return -1; } printf(连接成功开始执行命令\n); // 执行 SET参数用 %s 占位 redisReply *reply redisCommand(ctx, SET %s %s, name, alice); if (reply NULL) { printf(SET 命令失败: %s\n, ctx-errstr); redisFree(ctx); return -1; } freeReplyObject(reply); // 执行 GET并检查返回类型 reply redisCommand(ctx, GET %s, name); if (reply ! NULL reply-type REDIS_REPLY_STRING) { printf(GET 结果: %s\n, reply-str); } else { printf(GET 命令异常或键不存在\n); } if (reply ! NULL) { freeReplyObject(reply); } redisFree(ctx); return 0; }代码逻辑分四段连接、SET、GET、清理。连接段检查了两个条件第一个是ctx是否为NULL这表示内存分配失败第二个是ctx-err是否非零这表示TCP连接层面失败比如端口不通、超时、目标机拒绝具体的失败原因会写在ctx-errstr里。这两个条件在hiredis的API设计里是必须分开检查的因为错误信息的位置和含义不同。SET段和GET段都用到redisCommand它内部完成格式化、命令发送和响应解析返回的redisReply*由调用方负责释放释放统一走freeReplyObject。注意GET返回值的类型检查如果键不存在返回的是REDIS_REPLY_NIL而不是字符串类型这时候访问reply-str会读到无效指针所以先判断reply-type再取值是写hiredis代码的一个基本习惯。参数说明连接超时用struct timeval表达秒和微秒分开传这里填的{3, 0}表示3秒。redisCommand的格式串里%s对应字符串指针这与printf一致但hiredis内部会把命令按RESP协议编码所以空格的追加、引号的转义都不用你操心只需要确保参数个数和格式串匹配。如果参数里包含用户输入别直接拼进格式串用redisCommandArgv或者参数占位符可以避免命令注入类问题。3.3 链接报错时按这个顺序排查如果编译过了但链接报错别急着怀疑lib文件坏了按下面顺序排查基本五分钟内定位问题。第一步确认ws2_32.lib是否加入。如果报错信息里大量出现__imp_开头的符号比如__imp_WSASocketA、__imp_getaddrinfo这类几乎可以肯定是网络库缺失解决的路径是在“链接器→输入”里补上ws2_32.lib。第二步确认平台位数一致。x64工程尝试链接32位lib时常见报错是LNK2001外部符号无法解析或者“无法打开文件hiredis.lib”这类路径级错误因为VS会在x64的库搜索路径里找文件但路径不存在。第三步看CRT运行库匹配。链接期出现LNK2038或LNK4098通常就是/MD和/MT混用或者Debug库和Release库混用。第四步看头文件路径。include路径指错时编译期就会报“无法打开包含文件hiredis.h”这个错在编译阶段而不是链接阶段所以如果编译已经通过基本可以排除头文件路径问题。有一个容易被忽略的细节lib文件的搜索顺序。VS在“VC目录→库目录”里列多个路径时如果系统目录或Windows SDK目录恰好也有同名lib可能会优先解析到错误的那份。我一般会在“链接器→输入”里写入lib文件的绝对路径或相对工程路径而不只依赖库搜索目录这样能彻底避开同名文件遮蔽问题。链接阶段的报错信息往往比较长建议先看第一个error而不是最后一个第一个错误通常是最根本的后续一大串都是它引发的连锁反应。4. 避坑Windows下用这套客户端库最容易翻车的五个现场Windows下使用这套库编译链接阶段只是第一关。真正让人头疼的是它编译过了、程序也起来了却在特定条件下表现诡异。我把这些年见到的现场按频率排序列了五条每一条都按现象、原因、解决来讲。4.1 现象链接报错一堆__imp_开头的未解析符号链接器的输出窗口里成片出现“LNK2019无法解析的外部符号 __imp_connect该符号在函数…中被引用”后面跟着一串__imp_WS2_32相关的符号看起来像是库本身缺了实现。原因很简单Windows的socket实现全部在ws2_32.dll里它的导入库是ws2_32.lib如果只加了hiredis.lib和Win32_Interop.lib却没有把ws2_32.lib加进附加依赖项所有socket、connect、shutdown、getaddrinfo相关符号都会悬空。这也是这套库标题里特意带上“Windows下编译”的原因在Linux里不需要这一步但Windows里少写一行配置就翻车。解决方式就是在“链接器→输入→附加依赖项”里追加ws2_32.lib重新编译后符号就能全部解析。4.2 现象程序编译链接都过了运行时连接阶段返回失败程序能起来但在redisConnectWithTimeout之后就退出打印出来的错误是“连接失败”或“Connection refused”可服务端明明在监听端口也通。检查一圈后发现在没装过其他网络组件的机器上必现装了某些网络库的机器上却正常。原因大概率是Winsock未初始化。Windows的Winsock必须先用WSAStartup(MAKEWORD(2,2), ...)完成初始化没有这一步任何socket调用都会直接返回错误。如果Win32_Interop.lib里没有在加载时自动初始化或者初始化时机晚于你的第一次连接就会出现这个现象。解决方式是在程序入口显式调用WSAStartup请求版本2.2退出时对应WSACleanup。多次调用WSAStartup不会出错因为Winsock有引用计数所以即使库里已经初始化过你再调用一次也安全。4.3 现象DEBUG配置下跑得好好的一换RELEASE就随机崩溃这是最折磨人的一类问题因为它不在固定位置崩溃而是运行一段时间后突然报堆损坏或访问越界。原因基本锁定在CRT运行时不一致工程Debug默认走/MTd或多线程调试DLL如果预编译的lib是按Release模式的/MD编译两个模块的堆分配器不同hiredis内部malloc出来的内存如果交到你的代码里用另一个堆管理器释放就会产生堆损坏。这类问题未必在链接时报错所以很多人会怀疑自己的业务代码有内存越界排查方向完全走偏。解决方式是先确认lib的CRT模式再用dumpbin /directives看它依赖的默认库是msvcrt还是ucrtbase然后把工程所有配置的“运行库”选项统一成一致。改完配置后Debug和Release都要重新编译一遍。4.4 现象链接阶段报“无法打开文件hiredis.lib”这个现象看起来像文件缺失但你明明刚打开目录看过文件就在那里。多数时候不是文件真的丢了而是工程属性里的“库目录”没有指向lib所在位置。Visual Studio的“库目录”搜索路径基于平台在x86和x64下实际展开的宏不同如果你把路径写在源文件目录下而没有用环境变量区分平台x64编译时就会去x64的默认库目录找自然找不到。解决方式是在“VC目录→库目录”里填入绝对路径并在“链接器→输入”里写全文件名。注意工程配置有Debug/Release、x86/x64四个组合四个都要设置或者用属性表统一管理否则切一次配置就报一次错。4.5 现象异步API连接成功但回调一直不触发用了async.h里的redisAsyncConnect连接阶段没有报错连接成功回调也打印了但之后redisAsyncCommand下发的命令响应回调始终不执行。原因不是库坏了而是hiredis异步API需要事件循环驱动。Linux下往往配合libevent、libuv这类事件库Windows控制台程序如果直接在主循环里while(1)空转socket的可读事件永远不会被处理回调自然不触发。解决方式是每个循环周期调用redisAsyncHandleRead和redisAsyncHandleWrite处理fd上的读写事件或者把异步socket接入Winsock的WSAEventSelect模式把socket事件转换成Windows消息驱动消息循环。如果项目允许优先引入一个跨平台事件库把hiredis的异步fd托管给事件循环这是更省心的长期方案。5. 进阶多线程、异步回调和Redis分布式锁的落地姿势在多数业务系统里Redis不是简单的键值缓存而是承担会话、热点数据、分布式锁等职责的中间件所以客户端库的稳定性、线程模型和异步能力直接决定上层架构怎么搭。这套预编译库把底层编译问题解决之后剩下的问题就是怎么把它用好。5.1 同步API的线程边界一个连接别跨线程用凡是商用项目Redis基本都跑在多线程环境里。hiredis的同步API没有内建锁一个redisContext在同一时刻只允许一个线程执行命令多个线程共用一条连接轻则返回结果错乱重则直接崩溃。原因是hiredis内部复用了读写缓冲区和socket两个线程交替读写时状态会互相践踏这种问题几乎没有规律可循排查成本极高。所以常见做法是连接池每个线程持有自己的连接或者用池子管理一组连接每次取用都独占。连接池的实现不复杂核心就是队列加互斥锁从队列头部取一条空闲连接用完后归还。如果业务量小最简单的懒加载是每个线程首次使用时创建连接线程退出时统一释放避免连接频繁建立销毁。连接建立成本不算高但高并发时握手频繁也会拖慢响应所以连接池更稳妥也能顺手把缓存治理里的连接数控制住。5.2 异步API在Windows上把回调跑起来hiredis的异步API核心是redisAsyncContext和redisAsyncCommand。调用流程是redisAsyncConnect拿到异步上下文、设置连接成功回调和连接断开回调、然后用redisAsyncCommand下发命令命令的响应通过一个回调函数返回来整个过程调用线程不会被阻塞。这套模型在Linux上用事件循环驱动但在Windows下面临一个选择是自己造轮子做socket事件轮询还是引入现成的事件库。我一般倾向于引入事件库比如libuv或libevent的Windows移植版把redisAsyncContext里的fd注册到事件循环的可读事件上事件到来时调用redisAsyncHandleRead。这样一个异步Redis客户端就干净地嵌进了整个应用的事件驱动框架里不会阻塞主线程也避免手动轮询的忙等。如果项目坚持零第三方依赖那就用WSAEventSelect把socket事件映射到Windows事件对象然后在消息循环里等待这也是可行的只不过代码要多写不少。这里要非常注意回调函数的生命周期异步API的回调触发时你的业务代码可能在任意线程上下文里。如果回调里需要操作UI或访问共享数据必须把数据切到目标线程或者用队列把结果抛回主线程处理。把这个设计和链接库时的编译器选项配置一起写进工程规范是我在经历过一次回调里直接改控件差点崩溃之后养成的习惯。5.3 用这套库支撑Redis分布式锁的最小写法业务上要控制多个进程对同一资源的访问时用SET NX PX是经典做法。命令格式是SET lock_key random_value NX PX 30000其中NX表示键不存在时才设置PX指定过期时间毫秒。获取锁时如果返回OK说明拿锁成功拿到锁后执行业务逻辑释放锁时不要直接DEL而是先比较value是否还是自己的用一段Lua脚本保证比较和删除的原子性。if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end在C代码里下发这段Lua时我的习惯是把脚本写成一个字符串常量再用redisCommand(ctx, EVAL %s 1 %s %s, script, key, value)调用。注意KEYS[1]和ARGV[1]在Lua里下标从1开始这里传入的顺序不能调换。EVAL之后如果返回结果是整数1说明删除成功返回0说明锁已经过期或属于别人。这套写法看起来简单但解决了检查与删除的原子性是分布式锁落地的常规形态。5.4 验证库的运行表现一个简单的性能对照跑通了功能后最好做一次性能验证用数据确认这套Windows编译的库在目标机器上没被拖慢。常见做法是写一个压测小程序在线程池里循环执行GET命令记录每秒完成次数再和服务端官方redis-benchmark的结果对照。不必追求完全一样的QPS因为客户端语言和网络栈不同但如果本机压测时QPS比redis-benchmark低一个数量级就要怀疑库里有额外拷贝或Windows防火墙干扰。对照的时候有一个经验参数Windows下用loopback地址跑hiredis单线程QPS一般在几万到十几万这个量级视CPU和Redis版本而定。如果你的程序远低于这个范围先查是不是在每条命令前后做了多余的sleep或加锁再看是否每次命令都新建连接。后一种情况尤其常见连接复用和不复用之间能差出10倍以上的吞吐。6. 接入前先留15分钟三个验证动作和一条血泪教训把库正式装进工程前我会先花15分钟跑三个验证确认这套拿到手的库值得投入。第一个验证是最小连接测试直接复用第3章那段Demo在目标机器上跑通“连接SETGET”确认Redis服务端版本、目标机器位数、Winsock环境没有问题。这步的作用是尽早证明“库本身是活的”如果连最小Demo都跑不过说明要么传入的版本与本地环境不兼容要么文件缓存损坏这时果断换一份文件不要浪费时间在已经损坏的依赖上。第二个验证是配置组合矩阵测试用Debug/Release加x86/x64四个组合各编译一次工程确认原先“默认配置能编过”不是偶然。留意是否有哪个组合出现链接错误或运行时崩溃如果有用第4章的排查顺序定位一般是CRT或位数问题。这一步能防止三个月后同事在新机器上遇到你从没见过的报错也能让你在做缓存治理方案评审时拿出可靠的兼容性结论。第三个验证是压测用5.4的方式记录QPS和错误率同时观察Redis服务端日志里有没有异常断连记录。这个验证帮你建立一个基准线上线后如果性能明显衰减至少知道是业务代码引起的问题而非底层库退化。最后一条血泪教训我曾经把一套看起来完全正常的预编译库接进老项目图省事没做验证结果项目在Debug下稳定运行一到Release就随机崩溃。排查了两天最后用dumpbin看lib头信息才发现那套库是Release加动态CRT编译的而工程是Debug加静态CRT两个模块用不同堆管理器崩溃点是hiredis内部返回的字符串在业务侧被错误释放。从那次以后无论第三方库包装多友好我都会先把这几项验证做完再让业务代码接手。这个习惯让后面几个项目的接入顺利很多也希望帮到你。本文还有配套的精品资源点击获取
返回列表