
简介这份资源面向在 Windows 平台使用 VS2008 进行 ZeroMQ 开发的初学者围绕订阅与发布模式提供一套可快速上手的入门实例。包内包含完整的服务端与客户端测试工程以及一份 Windows 下 VS2008 使用 ZeroMQ 的说明文档帮助读者理解如何编译 ZeroMQ 库、配置头文件与库文件依赖并搭建自己的发布订阅通信程序。资源共 31 个文件以 h 头文件、cpp 源文件、dll 动态库、lib 导入库为主另有 vcproj 工程文件、sln 解决方案及若干说明文本压缩包约 4.25MB目录结构清晰便于按模块查阅。目前已有 488 人学习下载。通过这份资料读者可以省去从零摸索编译配置的时间直接对照工程结构完成环境搭建掌握 ZeroMQ 在 VS2008 下的基本集成方式与订阅发布通信流程适合需要快速入门消息队列开发的读者参考。1. 老 IDE 配新消息库为什么这套组合还在被翻出来用VS2008 配 ZeroMQ听起来像从技术博物馆里翻出来的组合。但如果你手上维护的是工控上位机、医疗设备采集端或者某类产线测试软件这个组合一点都不陌生——这些场景里 XP 嵌入式或 Win7 工控机还在跑VS2008 是唯一能出稳定 Release 包的编译环境而 ZeroMQ 的发布订阅模式恰好能解决多台设备状态广播的问题。问题在于ZeroMQ 官方早已不提供 VS2008 的工程文件网上能搜到的教程要么是 VS2010 以后的要么只给源码不给编译步骤。这份资源的价值就在于它把从源码编译到 pub/sub 实例跑通的完整链路补上了包括那个年代特有的 CRT 版本冲突和 socket 库链接顺序问题。适合两类人一是被老项目绑住、必须用 VS2008 出包的一线维护者二是想理解 ZeroMQ 发布订阅底层行为、不依赖高级封装的开发者。2. 编译 ZeroMQ 源码从 libzmq 到 VS2008 可用的静态库2.1 为什么不用官方预编译包ZeroMQ 官方在 4.x 之后就不再提供 VS2008 的二进制包了NuGet 上的 libzmq 包最低要求 VS2010 的 MSBuild 工具集。直接拿 VS2010 编译的 lib 往 VS2008 工程里链大概率会在链接阶段报LNK2001: 无法解析的外部符号 _zmq_ctx_new因为两个版本的 CRT 对符号修饰规则不同。常见做法是下载 libzmq 的源码包用 VS2008 自带的 vcvarsall.bat 环境重新编译。我一般会选 libzmq 4.1.x 这个分支它对 C98 的兼容性最好再往后的版本用了不少 C11 特性VS2008 的编译器认不全。源码目录里有个builds/msvc文件夹里面是给 VS2010 以上用的 vcxproj 文件。VS2008 打不开这些工程需要手动建一个 Win32 静态库工程把src目录下的 .cpp 和 .h 文件全部加进去。注意要排除src/curve_client.cpp和src/curve_server.cpp这两个文件它们依赖 libsodium而 libsodium 在 VS2008 下编译更麻烦普通 pub/sub 场景用不到加密。2.2 手动建工程的参数配置在 VS2008 里新建 Win32 项目类型选“静态库”不要勾“预编译头”。然后把 libzmq 源码include目录和src目录下的文件按以下规则加入# 目录结构参考假设源码解压在 D:\libzmq-4.1.7 D:\libzmq-4.1.7\include\zmq.h D:\libzmq-4.1.7\include\zmq_utils.h D:\libzmq-4.1.7\src\*.cpp # 排除 curve_client.cpp 和 curve_server.cpp D:\libzmq-4.1.7\src\*.hpp项目属性里需要改三个地方。第一C/C → 预处理器 → 预处理器定义加上ZMQ_STATIC和_CRT_SECURE_NO_WARNINGS。ZMQ_STATIC告诉头文件不要用__declspec(dllimport)修饰函数声明否则链接时会去找 DLL 里的符号。第二C/C → 代码生成 → 运行时库选“多线程 (/MT)”不要选 DLL 版本的 /MD否则 Release 包拿到没装 VC 运行库的工控机上会直接崩。第三链接器 → 输入 → 附加依赖项加上ws2_32.lib和Iphlpapi.libZeroMQ 内部用到了 Windows 的 socket 和 IP 辅助函数。编译时如果报error C2065: “sockaddr_in”: 未声明的标识符说明winsock2.h的包含顺序有问题。libzmq 的源码里有些文件先包含了windows.h而windows.h默认会拉进winsock.h和winsock2.h冲突。解决办法是在项目属性 C/C → 预处理器里加WIN32_LEAN_AND_MEAN这个宏会让windows.h跳过 winsock 部分。编译成功后会在Release目录下生成libzmq.lib大约 2MB 左右。这个 lib 就是后面所有实例的基础。2.3 验证静态库是否可用不要急着写 pub/sub先写一个最简单的zmq_version调用验证链接是否正常// test_zmq.cpp : 验证 libzmq 静态库链接 #include zmq.h #include stdio.h int main() { int major, minor, patch; zmq_version(major, minor, patch); printf(ZeroMQ version: %d.%d.%d\n, major, minor, patch); return 0; }在 VS2008 里新建 Win32 控制台工程把这个文件加进去链接器附加依赖项填上刚才编译出的libzmq.lib的完整路径。运行后如果输出ZeroMQ version: 4.1.7说明静态库没问题。如果报LNK2019: 无法解析的外部符号 _zmq_version检查两点一是ZMQ_STATIC宏有没有加二是 lib 文件的路径有没有写对。这一步过了后面的 pub/sub 才有意义。3. 发布订阅实例从单进程到跨进程的完整代码3.1 发布端代码与参数说明发布端创建一个ZMQ_PUB类型的 socket绑定到某个端口然后循环发送消息。下面这段代码可以直接在 VS2008 里编译运行// publisher.cpp : ZeroMQ 发布端 #include zmq.h #include stdio.h #include string.h #include windows.h int main() { void* context zmq_ctx_new(); void* publisher zmq_socket(context, ZMQ_PUB); // 绑定到 5555 端口所有网卡都可访问 int rc zmq_bind(publisher, tcp://*:5555); if (rc ! 0) { printf(bind failed: %s\n, zmq_strerror(zmq_errno())); return 1; } int count 0; while (1) { char buffer[64]; // 消息格式主题 空格 序号 sprintf(buffer, status %d, count); // 发送消息第三个参数是长度第四个参数 0 表示阻塞发送 rc zmq_send(publisher, buffer, strlen(buffer), 0); if (rc 0) { printf(send failed: %s\n, zmq_strerror(zmq_errno())); break; } printf(sent: %s\n, buffer); Sleep(1000); // 每秒发一条 } zmq_close(publisher); zmq_ctx_destroy(context); return 0; }zmq_socket的第二个参数ZMQ_PUB决定了这个 socket 只能发送不能接收。zmq_bind里的tcp://*:5555表示监听本机所有网卡的 5555 端口如果只想本机测试可以改成tcp://127.0.0.1:5555。zmq_send的第四个参数是标志位0 表示阻塞模式如果发送缓冲区满了会等待如果不想等可以传ZMQ_DONTWAIT但 pub 模式下一般不会满因为 pub 端不维护订阅者状态发出去就不管了。这里有个容易忽略的点Sleep(1000)用的是 Windows API需要包含windows.h。如果不想依赖 Windows 头文件可以用zmq_sleep(1)ZeroMQ 自己封装了一个跨平台的 sleep 函数参数单位是秒。3.2 订阅端代码与过滤机制订阅端创建一个ZMQ_SUB类型的 socket连接到发布端地址然后通过zmq_setsockopt设置订阅过滤前缀// subscriber.cpp : ZeroMQ 订阅端 #include zmq.h #include stdio.h #include string.h int main() { void* context zmq_ctx_new(); void* subscriber zmq_socket(context, ZMQ_SUB); // 连接到发布端 int rc zmq_connect(subscriber, tcp://127.0.0.1:5555); if (rc ! 0) { printf(connect failed: %s\n, zmq_strerror(zmq_errno())); return 1; } // 设置订阅过滤前缀空字符串表示接收所有消息 const char* filter status; rc zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, filter, strlen(filter)); if (rc ! 0) { printf(setsockopt failed: %s\n, zmq_strerror(zmq_errno())); return 1; } while (1) { char buffer[64]; // 接收消息阻塞模式 int size zmq_recv(subscriber, buffer, sizeof(buffer) - 1, 0); if (size 0) { printf(recv failed: %s\n, zmq_strerror(zmq_errno())); break; } buffer[size] \0; printf(received: %s\n, buffer); } zmq_close(subscriber); zmq_ctx_destroy(context); return 0; }zmq_setsockopt的第三个参数ZMQ_SUBSCRIBE是订阅选项第四个参数是过滤前缀。ZeroMQ 的过滤是在发布端做的订阅端设置的前缀会通过底层协议传到发布端发布端只发送匹配前缀的消息。这意味着如果订阅端设置的前缀是status那么发布端发送sensor 1时订阅端根本收不到消息在发布端就被丢弃了。这个机制的好处是节省网络带宽坏处是发布端需要维护每个订阅端的过滤规则订阅端数量多的时候发布端内存会涨。zmq_recv的第四个参数 0 表示阻塞接收如果没有消息会一直等。如果不想阻塞可以传ZMQ_DONTWAIT但返回 -1 时需要判断zmq_errno()是不是EAGAIN是的话表示暂时没消息不是错误。3.3 编译运行与跨进程验证把 publisher.cpp 和 subscriber.cpp 分别建成两个 Win32 控制台工程链接同一个libzmq.lib。先运行 publisher再运行 subscribersubscriber 的控制台会每秒打印一条received: status N。如果 subscriber 先启动、publisher 后启动subscriber 也能收到消息因为 ZeroMQ 的 connect 是异步的会自动重连。跨进程验证时注意防火墙。Windows 防火墙默认会拦截没有数字签名的程序监听端口第一次运行 publisher 时会弹窗询问是否允许要点“允许访问”。如果没弹窗但 subscriber 收不到消息手动在防火墙入站规则里加一条 TCP 5555 端口的允许规则。如果要在两台机器上测试把 subscriber 里的tcp://127.0.0.1:5555改成tcp://发布端IP:5555同时确保两台机器在同一网段、发布端防火墙放行。跨机器测试时延迟通常在 1ms 以内对于工控场景的状态广播完全够用。4. 避坑排查VS2008 配 ZeroMQ 的五个血泪经验4.1 现象编译时报LNK2005: _sprintf 已经在 libcmt.lib 中定义原因libzmq 源码里有些文件用了sprintf而 VS2008 的 CRT 库对sprintf有安全警告如果项目里同时链接了libcmt.lib和msvcrt.lib就会报重复定义。解决在项目属性 C/C → 代码生成 → 运行时库里统一选“多线程 (/MT)”不要混用。如果第三方库必须用 /MD那就把 libzmq 也改成 /MD 重新编译保持所有模块的 CRT 一致。4.2 现象subscriber 收不到消息但 publisher 显示发送成功原因ZeroMQ 的 pub/sub 模式有一个“慢连接”问题。publisher 绑定端口后subscriber 需要几毫秒到几百毫秒的时间完成 TCP 握手和订阅信息同步。如果 publisher 在 subscriber 连接完成之前就发送了消息这些消息会被直接丢弃因为 pub 端不缓存消息。解决在 publisher 的zmq_bind之后加一个Sleep(500)等订阅端连上来再开始发。或者在 subscriber 端加一个重试逻辑收到第一条消息后再开始处理。4.3 现象程序退出时卡在zmq_ctx_destroy原因zmq_ctx_destroy会等待所有 socket 关闭后才销毁上下文。如果还有 socket 没关或者有线程阻塞在zmq_recv上这个函数就会一直等。解决确保每个 socket 都调用了zmq_close并且zmq_close之后不要再使用这个 socket 指针。如果有接收线程先用zmq_setsockopt设置ZMQ_LINGER为 0这样zmq_close会立即返回不会等待未发送的消息。4.4 现象Release 版在开发机上正常拷到工控机上报“应用程序配置不正确”原因VS2008 编译的 Release 版默认依赖 VC 2008 运行库msvcr90.dll 等工控机上没装这个运行库。解决在项目属性里把运行时库改成“多线程 (/MT)”这样会把 CRT 静态链接进 exe不依赖外部 DLL。注意 libzmq 也要用 /MT 重新编译否则链接时会冲突。4.5 现象发送中文消息时 subscriber 收到乱码原因zmq_send发送的是原始字节流不处理字符编码。如果 publisher 源码文件是 GBK 编码而 subscriber 按 UTF-8 解析就会乱码。解决统一用 UTF-8 编码保存源码文件或者在发送前把字符串转成 UTF-8。VS2008 默认保存为 GBK需要在“文件 → 高级保存选项”里改成 UTF-8 无签名。如果不想改源码编码可以在发送前用MultiByteToWideChar和WideCharToMultiByte做一次转换。5. 进阶技巧用多线程订阅和消息分帧提升吞吐5.1 多线程订阅模型单线程订阅端在消息量大时会丢消息因为zmq_recv和业务处理在同一个线程里处理慢的时候接收缓冲区会满。常见做法是用两个线程一个线程专门zmq_recv收到消息后放进一个线程安全的队列另一个线程从队列取消息做业务处理。ZeroMQ 的 socket 不是线程安全的不能两个线程同时操作同一个 socket所以接收线程和处理线程之间要用队列传递数据。// 接收线程只负责收消息入队 DWORD WINAPI recv_thread(LPVOID param) { void* subscriber (void*)param; while (1) { char buffer[256]; int size zmq_recv(subscriber, buffer, sizeof(buffer) - 1, 0); if (size 0) break; buffer[size] \0; // 入队操作伪代码实际用 CriticalSection 或互斥量保护 EnterCriticalSection(cs); queue.push(std::string(buffer)); LeaveCriticalSection(cs); } return 0; }接收线程里不要做任何耗时操作zmq_recv之后立刻入队然后继续下一次接收。队列用std::queue加CRITICAL_SECTION保护VS2008 不支持 C11 的std::mutex只能用 Windows 原生的临界区。处理线程从队列取数据时如果队列为空就Sleep(1)让出 CPU不要忙等。5.2 消息分帧与多部分消息ZeroMQ 支持多部分消息可以把一个逻辑消息拆成多个帧发送接收端按帧接收。这在发送结构化数据时很有用比如第一帧是主题第二帧是 JSON 负载// 发送端多部分消息 zmq_send(publisher, status, 6, ZMQ_SNDMORE); // 第一帧ZMQ_SNDMORE 表示还有后续帧 zmq_send(publisher, {\temp\:25}, 11, 0); // 第二帧最后一帧 // 接收端循环接收直到没有更多帧 int more 1; size_t more_size sizeof(more); while (more) { char buffer[256]; int size zmq_recv(subscriber, buffer, sizeof(buffer) - 1, 0); buffer[size] \0; printf(frame: %s\n, buffer); zmq_getsockopt(subscriber, ZMQ_RCVMORE, more, more_size); }ZMQ_SNDMORE标志告诉 ZeroMQ 这一帧后面还有帧接收端通过ZMQ_RCVMORE选项判断是否还有后续帧。多部分消息的原子性由 ZeroMQ 保证要么全部收到要么全部收不到不会出现只收到一半的情况。这个特性在发送配置更新或批量数据时很有用接收端可以等所有帧到齐后再统一处理。5.3 用zmq_poll做超时接收阻塞式zmq_recv在没有消息时会一直等程序无法响应退出信号。用zmq_poll可以设置超时时间超时后返回 0程序可以检查退出标志zmq_pollitem_t items[] { { subscriber, 0, ZMQ_POLLIN, 0 } }; int rc zmq_poll(items, 1, 100); // 超时 100ms if (rc 0 (items[0].revents ZMQ_POLLIN)) { // 有消息可收 char buffer[256]; int size zmq_recv(subscriber, buffer, sizeof(buffer) - 1, 0); // 处理消息 } else if (rc 0) { // 超时检查退出标志 if (g_exit) break; }zmq_pollitem_t的第一个字段是 socket 指针第二个是文件描述符Windows 下填 0第三个是监听的事件类型第四个是返回的事件类型。zmq_poll的第三个参数是超时毫秒数-1 表示无限等待0 表示立即返回。这个函数在需要同时监听多个 socket 时特别有用可以把订阅 socket 和命令 socket 放在同一个 poll 数组里。从那以后我每次在 VS2008 里配 ZeroMQ都会先把ZMQ_STATIC和/MT这两个编译选项确认一遍再写一个zmq_version的测试工程验证链接最后才动 pub/sub 的代码。这个顺序能省掉八成以上的链接错误和运行时崩溃。希望帮到你。本文还有配套的精品资源点击获取