庫與發(fā)布訂閱實例完整指南)
簡介這份資源面向在 Windows 平臺使用 VS2008 進行 ZeroMQ 開發(fā)的初學者圍繞訂閱與發(fā)布模式提供一套可快速上手的入門實例。包內(nèi)包含完整的服務端與客戶端測試工程以及一份 Windows 下 VS2008 使用 ZeroMQ 的說明文檔幫助讀者理解如何編譯 ZeroMQ 庫、配置頭文件與庫文件依賴并搭建自己的發(fā)布訂閱通信程序。資源共 31 個文件以 h 頭文件、cpp 源文件、dll 動態(tài)庫、lib 導入庫為主另有 vcproj 工程文件、sln 解決方案及若干說明文本壓縮包約 4.25MB目錄結(jié)構清晰便于按模塊查閱。目前已有 488 人學習下載。通過這份資料讀者可以省去從零摸索編譯配置的時間直接對照工程結(jié)構完成環(huán)境搭建掌握 ZeroMQ 在 VS2008 下的基本集成方式與訂閱發(fā)布通信流程適合需要快速入門消息隊列開發(fā)的讀者參考。1. 老 IDE 配新消息庫為什么這套組合還在被翻出來用VS2008 配 ZeroMQ聽起來像從技術博物館里翻出來的組合。但如果你手上維護的是工控上位機、醫(yī)療設備采集端或者某類產(chǎn)線測試軟件這個組合一點都不陌生——這些場景里 XP 嵌入式或 Win7 工控機還在跑VS2008 是唯一能出穩(wěn)定 Release 包的編譯環(huán)境而 ZeroMQ 的發(fā)布訂閱模式恰好能解決多臺設備狀態(tài)廣播的問題。問題在于ZeroMQ 官方早已不提供 VS2008 的工程文件網(wǎng)上能搜到的教程要么是 VS2010 以后的要么只給源碼不給編譯步驟。這份資源的價值就在于它把從源碼編譯到 pub/sub 實例跑通的完整鏈路補上了包括那個年代特有的 CRT 版本沖突和 socket 庫鏈接順序問題。適合兩類人一是被老項目綁住、必須用 VS2008 出包的一線維護者二是想理解 ZeroMQ 發(fā)布訂閱底層行為、不依賴高級封裝的開發(fā)者。2. 編譯 ZeroMQ 源碼從 libzmq 到 VS2008 可用的靜態(tài)庫2.1 為什么不用官方預編譯包ZeroMQ 官方在 4.x 之后就不再提供 VS2008 的二進制包了NuGet 上的 libzmq 包最低要求 VS2010 的 MSBuild 工具集。直接拿 VS2010 編譯的 lib 往 VS2008 工程里鏈大概率會在鏈接階段報LNK2001: 無法解析的外部符號 _zmq_ctx_new因為兩個版本的 CRT 對符號修飾規(guī)則不同。常見做法是下載 libzmq 的源碼包用 VS2008 自帶的 vcvarsall.bat 環(huán)境重新編譯。我一般會選 libzmq 4.1.x 這個分支它對 C98 的兼容性最好再往后的版本用了不少 C11 特性VS2008 的編譯器認不全。源碼目錄里有個builds/msvc文件夾里面是給 VS2010 以上用的 vcxproj 文件。VS2008 打不開這些工程需要手動建一個 Win32 靜態(tài)庫工程把src目錄下的 .cpp 和 .h 文件全部加進去。注意要排除src/curve_client.cpp和src/curve_server.cpp這兩個文件它們依賴 libsodium而 libsodium 在 VS2008 下編譯更麻煩普通 pub/sub 場景用不到加密。2.2 手動建工程的參數(shù)配置在 VS2008 里新建 Win32 項目類型選“靜態(tài)庫”不要勾“預編譯頭”。然后把 libzmq 源碼include目錄和src目錄下的文件按以下規(guī)則加入# 目錄結(jié)構參考假設源碼解壓在 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)修飾函數(shù)聲明否則鏈接時會去找 DLL 里的符號。第二C/C → 代碼生成 → 運行時庫選“多線程 (/MT)”不要選 DLL 版本的 /MD否則 Release 包拿到?jīng)]裝 VC 運行庫的工控機上會直接崩。第三鏈接器 → 輸入 → 附加依賴項加上ws2_32.lib和Iphlpapi.libZeroMQ 內(nèi)部用到了 Windows 的 socket 和 IP 輔助函數(shù)。編譯時如果報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 驗證靜態(tài)庫是否可用不要急著寫 pub/sub先寫一個最簡單的zmq_version調(diào)用驗證鏈接是否正常// test_zmq.cpp : 驗證 libzmq 靜態(tài)庫鏈接 #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說明靜態(tài)庫沒問題。如果報LNK2019: 無法解析的外部符號 _zmq_version檢查兩點一是ZMQ_STATIC宏有沒有加二是 lib 文件的路徑有沒有寫對。這一步過了后面的 pub/sub 才有意義。3. 發(fā)布訂閱實例從單進程到跨進程的完整代碼3.1 發(fā)布端代碼與參數(shù)說明發(fā)布端創(chuàng)建一個ZMQ_PUB類型的 socket綁定到某個端口然后循環(huán)發(fā)送消息。下面這段代碼可以直接在 VS2008 里編譯運行// publisher.cpp : ZeroMQ 發(fā)布端 #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 端口所有網(wǎng)卡都可訪問 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); // 發(fā)送消息第三個參數(shù)是長度第四個參數(shù) 0 表示阻塞發(fā)送 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); // 每秒發(fā)一條 } zmq_close(publisher); zmq_ctx_destroy(context); return 0; }zmq_socket的第二個參數(shù)ZMQ_PUB決定了這個 socket 只能發(fā)送不能接收。zmq_bind里的tcp://*:5555表示監(jiān)聽本機所有網(wǎng)卡的 5555 端口如果只想本機測試可以改成tcp://127.0.0.1:5555。zmq_send的第四個參數(shù)是標志位0 表示阻塞模式如果發(fā)送緩沖區(qū)滿了會等待如果不想等可以傳ZMQ_DONTWAIT但 pub 模式下一般不會滿因為 pub 端不維護訂閱者狀態(tài)發(fā)出去就不管了。這里有個容易忽略的點Sleep(1000)用的是 Windows API需要包含windows.h。如果不想依賴 Windows 頭文件可以用zmq_sleep(1)ZeroMQ 自己封裝了一個跨平臺的 sleep 函數(shù)參數(shù)單位是秒。3.2 訂閱端代碼與過濾機制訂閱端創(chuàng)建一個ZMQ_SUB類型的 socket連接到發(fā)布端地址然后通過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); // 連接到發(fā)布端 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的第三個參數(shù)ZMQ_SUBSCRIBE是訂閱選項第四個參數(shù)是過濾前綴。ZeroMQ 的過濾是在發(fā)布端做的訂閱端設置的前綴會通過底層協(xié)議傳到發(fā)布端發(fā)布端只發(fā)送匹配前綴的消息。這意味著如果訂閱端設置的前綴是status那么發(fā)布端發(fā)送sensor 1時訂閱端根本收不到消息在發(fā)布端就被丟棄了。這個機制的好處是節(jié)省網(wǎng)絡帶寬壞處是發(fā)布端需要維護每個訂閱端的過濾規(guī)則訂閱端數(shù)量多的時候發(fā)布端內(nèi)存會漲。zmq_recv的第四個參數(shù) 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 防火墻默認會攔截沒有數(shù)字簽名的程序監(jiān)聽端口第一次運行 publisher 時會彈窗詢問是否允許要點“允許訪問”。如果沒彈窗但 subscriber 收不到消息手動在防火墻入站規(guī)則里加一條 TCP 5555 端口的允許規(guī)則。如果要在兩臺機器上測試把 subscriber 里的tcp://127.0.0.1:5555改成tcp://發(fā)布端IP:5555同時確保兩臺機器在同一網(wǎng)段、發(fā)布端防火墻放行。跨機器測試時延遲通常在 1ms 以內(nèi)對于工控場景的狀態(tài)廣播完全夠用。4. 避坑排查VS2008 配 ZeroMQ 的五個血淚經(jīng)驗4.1 現(xiàn)象編譯時報LNK2005: _sprintf 已經(jīng)在 libcmt.lib 中定義原因libzmq 源碼里有些文件用了sprintf而 VS2008 的 CRT 庫對sprintf有安全警告如果項目里同時鏈接了libcmt.lib和msvcrt.lib就會報重復定義。解決在項目屬性 C/C → 代碼生成 → 運行時庫里統(tǒng)一選“多線程 (/MT)”不要混用。如果第三方庫必須用 /MD那就把 libzmq 也改成 /MD 重新編譯保持所有模塊的 CRT 一致。4.2 現(xiàn)象subscriber 收不到消息但 publisher 顯示發(fā)送成功原因ZeroMQ 的 pub/sub 模式有一個“慢連接”問題。publisher 綁定端口后subscriber 需要幾毫秒到幾百毫秒的時間完成 TCP 握手和訂閱信息同步。如果 publisher 在 subscriber 連接完成之前就發(fā)送了消息這些消息會被直接丟棄因為 pub 端不緩存消息。解決在 publisher 的zmq_bind之后加一個Sleep(500)等訂閱端連上來再開始發(fā)?;蛘咴?subscriber 端加一個重試邏輯收到第一條消息后再開始處理。4.3 現(xiàn)象程序退出時卡在zmq_ctx_destroy原因zmq_ctx_destroy會等待所有 socket 關閉后才銷毀上下文。如果還有 socket 沒關或者有線程阻塞在zmq_recv上這個函數(shù)就會一直等。解決確保每個 socket 都調(diào)用了zmq_close并且zmq_close之后不要再使用這個 socket 指針。如果有接收線程先用zmq_setsockopt設置ZMQ_LINGER為 0這樣zmq_close會立即返回不會等待未發(fā)送的消息。4.4 現(xiàn)象Release 版在開發(fā)機上正常拷到工控機上報“應用程序配置不正確”原因VS2008 編譯的 Release 版默認依賴 VC 2008 運行庫msvcr90.dll 等工控機上沒裝這個運行庫。解決在項目屬性里把運行時庫改成“多線程 (/MT)”這樣會把 CRT 靜態(tài)鏈接進 exe不依賴外部 DLL。注意 libzmq 也要用 /MT 重新編譯否則鏈接時會沖突。4.5 現(xiàn)象發(fā)送中文消息時 subscriber 收到亂碼原因zmq_send發(fā)送的是原始字節(jié)流不處理字符編碼。如果 publisher 源碼文件是 GBK 編碼而 subscriber 按 UTF-8 解析就會亂碼。解決統(tǒng)一用 UTF-8 編碼保存源碼文件或者在發(fā)送前把字符串轉(zhuǎn)成 UTF-8。VS2008 默認保存為 GBK需要在“文件 → 高級保存選項”里改成 UTF-8 無簽名。如果不想改源碼編碼可以在發(fā)送前用MultiByteToWideChar和WideCharToMultiByte做一次轉(zhuǎn)換。5. 進階技巧用多線程訂閱和消息分幀提升吞吐5.1 多線程訂閱模型單線程訂閱端在消息量大時會丟消息因為zmq_recv和業(yè)務處理在同一個線程里處理慢的時候接收緩沖區(qū)會滿。常見做法是用兩個線程一個線程專門zmq_recv收到消息后放進一個線程安全的隊列另一個線程從隊列取消息做業(yè)務處理。ZeroMQ 的 socket 不是線程安全的不能兩個線程同時操作同一個 socket所以接收線程和處理線程之間要用隊列傳遞數(shù)據(jù)。// 接收線程只負責收消息入隊 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之后立刻入隊然后繼續(xù)下一次接收。隊列用std::queue加CRITICAL_SECTION保護VS2008 不支持 C11 的std::mutex只能用 Windows 原生的臨界區(qū)。處理線程從隊列取數(shù)據(jù)時如果隊列為空就Sleep(1)讓出 CPU不要忙等。5.2 消息分幀與多部分消息ZeroMQ 支持多部分消息可以把一個邏輯消息拆成多個幀發(fā)送接收端按幀接收。這在發(fā)送結(jié)構化數(shù)據(jù)時很有用比如第一幀是主題第二幀是 JSON 負載// 發(fā)送端多部分消息 zmq_send(publisher, status, 6, ZMQ_SNDMORE); // 第一幀ZMQ_SNDMORE 表示還有后續(xù)幀 zmq_send(publisher, {\temp\:25}, 11, 0); // 第二幀最后一幀 // 接收端循環(huán)接收直到?jīng)]有更多幀 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選項判斷是否還有后續(xù)幀。多部分消息的原子性由 ZeroMQ 保證要么全部收到要么全部收不到不會出現(xiàn)只收到一半的情況。這個特性在發(fā)送配置更新或批量數(shù)據(jù)時很有用接收端可以等所有幀到齊后再統(tǒng)一處理。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第三個是監(jiān)聽的事件類型第四個是返回的事件類型。zmq_poll的第三個參數(shù)是超時毫秒數(shù)-1 表示無限等待0 表示立即返回。這個函數(shù)在需要同時監(jiān)聽多個 socket 時特別有用可以把訂閱 socket 和命令 socket 放在同一個 poll 數(shù)組里。從那以后我每次在 VS2008 里配 ZeroMQ都會先把ZMQ_STATIC和/MT這兩個編譯選項確認一遍再寫一個zmq_version的測試工程驗證鏈接最后才動 pub/sub 的代碼。這個順序能省掉八成以上的鏈接錯誤和運行時崩潰。希望幫到你。本文還有配套的精品資源點擊獲取