本文主要譯自 zguide - chapter one. 但並不是照本翻譯.
介紹性的話我這裡就不翻譯了, 總結起來就是zmq很cool, 你應該嘗試一下.
如何安裝與使用zmq
在Linux和Mac OS上, 請通過隨機附帶的包管理軟體, 或者home brew安裝zmq. 包名一般就叫zmq, 安裝上就好.
安裝後, 以Mac OS為例, 會出現一個新的標頭檔案 /usr/local/include/zmq.h
, 和一個連結庫 /usr/local/lib/libzmq.a
.
所以, 如果你使用C語言, 那麼很簡單, 寫程式碼的時候加上標頭檔案 #include <zmq.h>
就好了, 連結的時候加上庫 -lzmq
就好了.
如果你使用的不是C語言, 那麼也很簡單, 去複習一下C語言, 然後再回來看這個教程. 需要注意的是, 這個教程裡的所有示例程式碼在編譯的時候需要指定 -std=c99.
一問一答例子入門
先放一個一問一答的例子來讓你感受一下
這是服務端程式碼
#include <zmq.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
int main(void)
{
void * context = zmq_ctx_new();
void * socket = zmq_socket(context, ZMQ_REP);
zmq_bind(socket, "tcp://*:5555");
while(1)
{
char buffer[10];
int bytes = zmq_recv(socket, buffer, 10, 0);
buffer[bytes] = '\0';
printf("[Server] Recevied Request Message: %d bytes, content == \"%s\"\n", bytes, buffer);
sleep(1);
const char * replyMsg = "World";
bytes = zmq_send(socket, replyMsg, strlen(replyMsg), 0);
printf("[Server] Sended Reply Message: %d bytes, content == \"%s\"\n", bytes, replyMsg);
}
zmq_close(socket);
zmq_ctx_destroy(context);
return 0;
}
這是客戶端程式碼
#include <zmq.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("Connecting to server...\n");
void * context = zmq_ctx_new();
void * socket = zmq_socket(context, ZMQ_REQ);
zmq_connect(socket, "tcp://localhost:5555");
for(int i = 0; i < 10; ++i)
{
char buffer[10];
const char * requestMsg = "Hello";
int bytes = zmq_send(socket, requestMsg, strlen(requestMsg), 0);
printf("[Client][%d] Sended Request Message: %d bytes, content == \"%s\"\n", i, bytes, requestMsg);
bytes = zmq_recv(socket, buffer, 10, 0);
buffer[bytes] = '\0';
printf("[Client][%d] Received Reply Message: %d bytes, content == \"%s\"\n", i, bytes, buffer);
}
zmq_close(socket);
zmq_ctx_destroy(context);
return 0;
}
這是makefile
all: client server
%: %.c
gcc -std=c99 $^ -o $@ -lzmq
這個例子就很簡單, 要點有以下
服務端上:
- 服務端建立一個context, 再用context去建立一個socket, 再把socket繫結到tcp的5555埠上
- 先從socket中讀取資料, 再向socket中寫入資料
客戶端上
- 客戶端也是建立一個context, 再用context去建立一個socket, 與服務端不同的, 客戶端是用tcp協議連線到本機的5555埠上, 這也是服務端監聽的網路地址
- 客戶端先向socket裡寫入資料, 再從socket中讀取資料
- 客戶端執行 寫入-讀出 這樣的操作十遍後, 關閉socket, 銷燬context, 退出程式
看起來套路和你從<Unix 網路程式設計>裡學到的差不多嘛. 不過, 你可以試試, 先啟動客戶端, 然後再啟動服務端, 你會發現, 程式沒有崩潰. 這就是zmq高明的地方, 把作業系統原生脆弱的網路程式設計介面進行了封裝. 並且實際上不止於此, 後面我們會學到更多. 這只是開胃小菜.
注意: 網路通訊中沒有字串!
你可能注意到了我們上面的例子裡, 其實客戶端與服務端互相傳輸的資料裡, 並沒有包含C風格字串最後一位的'\0'. 請時刻謹記這一點, 網路通訊中, 流動在網路程式設計API上的資料, 對於API本身來說, 都是位元組序列而已, 如何解釋這些位元組序列, 是網路程式設計API的使用者的責任. 比如上面, 我們需要在每次接收資料的時候記錄接收的資料的大小, 並且在buffer中為接收到的資料之後的一個位元組賦值為0, 即人為的把接收到的資料解釋為字串. 而對於zmq_send
與zmq_recv
來說, 它並不關心客戶端與服務端傳輸的資料具體是什麼.
這在所有網路程式設計API中都是這個套路, 不光是zmq, linux socket, winsock, 都是這樣. 字串? 不存在的. 我能看見的, 只是位元組序列而已.
獲取zmq的版本資訊
當你要把zmq應用到實際專案中的時候, 版本號注是一個你必須關注的事情了. 當然, 專案初期你可以不關心它, 或者專案規模較小的時候你可以不關心它. 但隨著專案的進展, 專案中使用到的庫的版本號就成了所有人必須關心的事情. 實際上所有第三方庫的版本都是一個需要專案owner關心的事情, 因為總有一些sb會做出以下的事情:
- 當一個sb需要一個額外功能的時候, 他會以光速引入一個庫, 並且從不檢查這個庫是否已經被引入到專案中.
- 當這個sb引入這個第三方庫的時候, 這個sb只關心自己寫的程式碼能不能順利編譯執行
- 很大概率這個sb不會仔細閱讀專案的構造工具指令碼, 這個sb只關心如何把這坨他看不懂的東西, 搞的不報錯, 能執行起來.
- 很在可能這個sb引入的這個第三方庫, 專案已經在先前引入了, 經過這個sb這次操作, 專案中會存在不同版本的兩個同名庫的引用.
- 一般情況下這個sb由於追求cool, 會引入最新的版本, 甚至是beta版
- 多數情況下, 這次操作引入的負面影響會在幾個月後爆發.
所以, 在這裡衷心的建議你, 時刻關注你專案中使用的所有第三方庫, 搞清楚你的專案構造工具鏈的執行過程. 而對於zmq來說, 要獲得zmq的版本, 需要如下呼叫一些函式
#include <zmq.h>
#include <stdio.h>
int main(void)
{
int major = 0;
int minor = 0;
int patch = 0;
zmq_version(&major, &minor, &patch);
printf("ZMQ_VERSION == %d.%d.%d\n", major, minor, patch);
return 0;
}
在我寫(抄)這個教程的時候, 我使用的版本號是4.2.5
封裝一些工具函式, 閱讀manpage, 並關心zmq API的返回值
有三件事我建議你養成習慣
- 封裝一些工具函式, 並且在你的程式設計生涯中不斷的改進它們
- 多查閱程式設計手冊, 在*nix平臺上, 多查閱manpage
- 對於C網路的API, 多關心函式的返回值的意義. 這裡的返回值包括但不限於: 函式的返回值, errno, errmsg等
現在我要寫三個工具函式, 這三個函式都不完美, 但它們都會出現大後續的示例程式裡, 用於縮減示例程式的篇幅:
第一個工具函式: 向zmq socket傳送字串資料, 但不帶結尾的'\0'
/*
* 把字串作為位元組資料, 傳送至zmq socket, 但不傳送字串末尾的'\0'位元組
* 傳送成功時, 返回傳送的位元組數
*/
static inline int s_send(void * socket, const char * string)
{
return zmq_send(socket, string, strlen(string), 0);
}
第二個工具函式: 從zmq socket中接收資料, 並把其解釋為一個字串
/*
* 從zmq socket中接收資料, 並將其解釋為C風格字串
* 注意: 該函式返回的字串是為在堆區建立的字串
* 請在使用結束後手動呼叫free將其釋放
*/
static inline char * s_recv(void * socket)
{
char buffer[256];
int length = zmq_recv(socket, buffer, 255, 0);
if(length == -1)
{
return NULL;
}
buffer[length] = '\0';
return strndup(buffer, sizeof(buffer) - 1);
}
第三個函式: 在取值範圍 [0, x) 中隨機生成一個整數
/*
* 生成一個位於 [0, num)區間的隨機數
*/
#define randof(num) (int)((float)(num) * random() / (RAND_MAX + 1.0))
這些工具函式都會以靜態行內函數的形式寫在一個名為 "zmq_helper.h" 的標頭檔案中, 在後續用得著這些工具函式的時候, 示例程式將直接使用, 而不做額外的說明. 對應的, 當新增一個工具函式的時候, 工具函式本身的原始碼會在合適的時候貼出
什麼是模式? pattern?
相信以Java為主要工作語言的同學, 在畢業面試的時候基本上都被面試官問過各種設計模式, design patterns. 不知道你們有沒有思考過一個哲學問題: 什麼是模式? 什麼是pattern? 為什麼我們需要設計模式?
我在這裡給出我的理解: 模式並不高大上, 模式其實就是"套路". 所謂的設計模式就是在物件導向程式設計架構中, 前人總結出來的一些慣用套路.
網路程式設計中也有這樣的套路, 也被稱之為模式, pattern. ZMQ作為一個像訊息庫的網路庫, 致力於向你提供套路, 或者說, 向你提供一些便於實現套路的工具集. 下面, 我們來看我們接觸的第二個套路: 釋出-訂閱套路. (第一個套路是 請求-應答 套路)
釋出-訂閱 套路
釋出-訂閱套路中有兩個角色: 釋出者, 訂閱者. 或者通俗一點: 村口的大喇叭, 與村民.
釋出者, 與村口的大喇叭的共性是: 只生產訊息, 不接收訊息. 而訂閱者與村民的共性是: 只接收訊息, 而不生產訊息(好嗎, 村民會生產八卦訊息, 抬槓就沒意思了). ZMQ提供了兩種特殊的socket用於實現這個模式, 這個套路, 下面是一個例子:
村口的大喇叭迴圈播放天氣預報, 播放的內容很簡單: 郵編+溫度+相對溫度. 各個村民只關心自己村的天氣情況, 他們村的郵編是10001, 對於其它地區的天氣, 村民不關心.
釋出者/村口的大喇叭:
#include <zmq.h>
#include <stdio.h>
#include <stdlib.h>
#include "zmq_helper.h"
int main(void)
{
void * context = zmq_ctx_new();
void * socket = zmq_socket(context, ZMQ_PUB);
zmq_bind(socket, "tcp://*:5556");
srandom((unsigned)time(NULL));
while(1)
{
int zipcode = randof(100000); // 郵編: 0 ~ 99999
int temp = randof(84) - 42; // 溫度: -42 ~ 41
int relhumidity = randof(50) + 10; // 相對溼度: 10 ~ 59
char msg[20];
snprintf(msg, sizeof(msg), "%5d %d %d", zipcode, temp, relhumidity);
s_send(socket, msg);
}
zmq_close(socket);
zmq_ctx_destroy(context);
return 0;
}
訂閱者/村民:
#include <zmq.h>
#include <stdio.h>
#include "zmq_helper.h"
int main(void)
{
void * context = zmq_ctx_new();
void * socket = zmq_socket(context, ZMQ_SUB);
zmq_connect(socket, "tcp://localhost:5556");
char * zipcode = "10001";
zmq_setsockopt(socket, ZMQ_SUBSCRIBE, zipcode, strlen(zipcode));
for(int i = 0; i < 50; ++i)
{
char * string = s_recv(socket);
printf("[Subscriber] Received weather report msg: %s\n", string);
free(string);
}
zmq_close(socket);
zmq_ctx_destroy(context);
return 0;
}
makefile
all: publisher subscriber
%: %.c
gcc -std=c99 $^ -o $@ -lzmq
這個例子中需要特別注意的點有:
- 村民必須通過
zmq_setsockopt
函式設定一個過濾器, 以說明關心哪些訊息. 如果不設定過濾器, 那麼什麼訊息都不會收到 - 即便你先啟動村民, 再給大喇叭上電, 村民還是會遺漏掉大喇叭最初始傳送的一些訊息..呃, 這麼講吧, 大概能丟失幾萬條這樣.這是因為tcp建立連線需要時間. 在建立連線這段時間內, 大喇叭已經向外瘋狂傳送了很多訊息. 在後續章節, 大概在第三章, 我們將會學到如何嚴格同步村民與喇叭. 讓喇叭在所有村民就緒之後再開始傳送訊息.
另外, 關於這個例子中的兩種socket型別, 有以下特點
ZMQ_PUB
型別的socket, 如果沒有任何村民與其相連, 其所有訊息都將被簡單就地拋棄ZMQ_SUB
型別的socket, 即是村民, 可以與多個ZMQ_PUB
型別的socket相連, 即村民可以同時收聽多個喇叭, 但必須為每個喇叭都設定過濾器. 否則預設情況下, zmq認為村民不關心喇叭裡的所有內容.- 當一個村民收聽多個喇叭時, 接收訊息採用公平佇列策略
- 如果存在至少一個村民在收聽這個喇叭, 那麼這個喇叭的訊息就不會被隨意拋棄: 這句話的意思是, 當訊息過多, 而村民的消化能力比較低的話, 未傳送的訊息會快取在喇叭裡.
- 在ZMQ大版本號在3以上的版本里, 當喇叭與村民的速度不匹配時. 若使用的傳輸層協議是
tcp
或ipc
這種面向連線的協議, 則堆積的訊息快取在喇叭裡, 當使用epgm
這種協議時, 堆積的訊息快取了村民裡. 在ZMQ 大版本號為2的版本中, 所有情況下, 訊息都將堆積在村民裡. 後續章節我們會學習到, 如何以"高水位閾值"來保護喇叭.
ZMQ裡的ZMQ_PUB
型的釋出者, 也就是喇叭, 其傳送訊息的能力是很炸的, zmq的作者在官方的guide裡講到, 釋出者與訂閱者位於同臺機器上, 通過tcp://locahost連線, 釋出者釋出一千萬條訊息, 大概用時4秒多. 這還是一臺2011年的i5處理器的膝上型電腦. 還不是IDC機房裡的伺服器...你大致感受一下..這個時候有人就跳出來說了, 這同臺機器走了loopback, 肯定效率高啊.
如果你也冒出這樣的想法, pong友, 看來你沒理解zmq的作者想表達的意思. 顯然, 如果採用乙太網作鏈路層, 這個資料不可能這麼炸裂, 但作者只是想向你表達: ZMQ本身絕對不會成為效能的瓶頸, 瓶頸肯定在網路IO上, 而不是ZMQ庫, 甚至於說作業系統協議棧上. 應用程式的效能瓶頸, 99.9999%都不在協議棧與網路庫上, 而是受限於物理規格的網路IO.
效能低? 你不買個幾百張82599武裝你的機房, 效能低你怪誰? 心裡沒一點i3數嗎?
分治套路
分治套路里有三個角色:
- Ventilator. 包工頭, 向手下各個工程隊分派任務. 一個.
- Worker. 工程隊, 從包工頭裡接收任務, 幹活. 多個.
- Sink. 甲方監理, 工程隊幹完活後, 向甲方監理報告. 所以工程隊的活幹完之後, 監理統一收集所有工程隊的成果. 一個.
在介紹這一節的示例程式碼之前, 我們先引入了兩個工具函式:
/*
* 獲取當時時間戳, 單位ms
*/
static inline int64_t s_clock(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return (int64_t)(tv.tv_sec * 1000 + tv.tv_usec / 1000);
}
/*
* 使當前程式睡眠指定毫秒
*/
static inline void s_sleep(int ms)
{
struct timespec t;
t.tv_sec = ms/1000;
t.tv_nsec = (ms % 1000) * 1000000;
nanosleep(&t, NULL);
}
分治套路也被稱為流水線套路. 下面是示例程式碼:
包工頭程式碼:
#include <zmq.h>
#include <stdio.h>
#include <time.h>
#include "zmq_helper.h"
int main(void)
{
void * context = zmq_ctx_new();
void * socket_to_sink = zmq_socket(context, ZMQ_PUSH);
void * socket_to_worker = zmq_socket(context, ZMQ_PUSH);
zmq_connect(socket_to_sink, "tcp://localhost:5558");
zmq_bind(socket_to_worker, "tcp://*:5557");
printf("Press Enter when all workers get ready:");
getchar();
printf("Sending tasks to workers...\n");
s_send(socket_to_sink, "Get ur ass up"); // 通知監理, 幹活了
srandom((unsigned)time(NULL));
int total_ms = 0;
for(int i = 0; i < 100; ++i)
{
int workload = randof(100) + 1; // 工作需要的耗時, 單位ms
total_ms += workload;
char string[10];
snprintf(string, sizeof(string), "%d", workload);
s_send(socket_to_worker, string); // 將工作分派給工程隊
}
printf("Total expected cost: %d ms\n", total_ms);
zmq_close(socket_to_sink);
zmq_close(socket_to_worker);
zmq_ctx_destroy(context);
return 0;
}
工程隊程式碼:
#include <zmq.h>
#include <stdio.h>
#include "zmq_helper.h"
int main(void)
{
void * context = zmq_ctx_new();
void * socket_to_ventilator = zmq_socket(context, ZMQ_PULL);
void * socket_to_sink = zmq_socket(context, ZMQ_PUSH);
zmq_connect(socket_to_ventilator, "tcp://localhost:5557");
zmq_connect(socket_to_sink, "tcp://localhost:5558");
while(1)
{
char * msg = s_recv(socket_to_ventilator);
printf("Received msg: %s\n", msg);
fflush(stdout);
s_sleep(atoi(msg)); // 幹活, 即睡眠指定毫秒
free(msg);
s_send(socket_to_sink, "DONE"); // 活幹完了通知監理
}
zmq_close(socket_to_ventilator);
zmq_close(socket_to_sink);
zmq_ctx_destroy(context);
return 0;
}
監理程式碼:
#include <zmq.h>
#include <stdio.h>
#include "zmq_helper.h"
int main(void)
{
void * context = zmq_ctx_new();
void * socket_to_worker_and_ventilator = zmq_socket(context, ZMQ_PULL);
zmq_bind(socket_to_worker_and_ventilator, "tcp://*:5558");
char * msg = s_recv(socket_to_worker_and_ventilator);
printf("Received msg: %s", msg); // 接收來自包工頭的開始幹活的訊息
free(msg);
int64_t start_time = s_clock();
for(int i = 0; i < 100; ++i)
{
// 接收100個worker幹完活的訊息
char * msg = s_recv(socket_to_worker_and_ventilator);
free(msg);
if(i / 10 * 10 == i)
printf(":");
else
printf(".");
fflush(stdout);
}
printf("Total elapsed time: %d ms]\n", (int)(s_clock() - start_time));
zmq_close(socket_to_worker_and_ventilator);
zmq_ctx_destroy(context);
return 0;
}
這個示例程式的邏輯流程是這樣的:
- 包工頭向兩個角色傳送訊息: 向工程隊傳送共計100個任務, 向監理髮送訊息, 通知監理開始幹活
- 工程隊接收來自包工頭的訊息, 並按訊息裡的數值, 睡眠指定毫秒. 每個任務結束後都通知監理.
- 監理先是接收來自包工頭的訊息, 開始計時. 然後統計來自工程隊的訊息, 當收集到100個任務完成的訊息後, 計算實際耗時.
包工頭裡輸出的預計耗時是100個任務的共計耗時, 在監理那裡統計的實際耗時則是由多個工程隊並行處理100個任務實際的耗時.
這裡個例子中需要注意的點有:
- 這個例子中使用了
ZMQ_PULL
與ZMQ_PUSH
兩種socket. 分別供訊息分發方與訊息接收方使用. 看起來略微有點類似於釋出-訂閱套路, 具體之間的區別後續章節會講到. - 工程隊上接包工頭, 下接監理. 在任務執行過程中, 你可以隨意的增加工程隊的數量.
- 我們通過讓包工頭通知監理, 以及手動輸入enter來啟動任務分發的方式, 手動同步了工程隊/包工頭/監理.
PUSH/PULL
模式雖然和PUB/SUB
不一樣, 不會丟失訊息. 但如果不手動同步的話, 最先建立連線的工程隊將幾乎把所有任務都接收到手, 導致後續完成連線的工程隊拿不到任務, 任務分配不平衡. - 包工頭分派任務使用的是輪流/平均分配的方式.這是一種簡單的負載均衡
- 監理接收多個工程隊的訊息, 使用的是公平佇列策略.
所以, 你大致能看出來, 分治套路里有一個核心問題, 就是任務分發者與任務執行者之間的同步. 如果在所有執行者均與分發者建立連線後, 進行分發, 那麼任務分發是比較公平的. 這就需要應用程式開發者自己負責同步事宜. 關於這個話題進一步的技巧將在第三章進一步討論.
使用ZMQ的一點建議
現在我們寫了三個例子, 分別是請求-迴應套路, 釋出-訂閱套路, 流水線套路. 在繼續進一步學習之前, 有必要對一些點進行強調
- 學習ZMQ請慢慢學. 不要著急. 其實學習所有庫工具都是如此, learn it by hard way. 很多程式設計師總是看不到 "看懂" 和 "學會" 這兩個層次之間的十萬公里距離, 覺得"看懂"了, 再抄點程式碼, 複製貼上一下, 就算是"精通"ZMQ了, 不, 不, 不, 差得遠, 當年你就這這樣學C語言的, 所以除了資料結構實驗, 你寫不出任何有用的程式碼. 我建議你一步一步的學習, 不要急功近利, 仔細的寫程式碼, 琢磨, 體會, 理解.
- 養成良好的程式設計風格, 不要寫屎一樣的程式碼.
- 重試自測, 無論是在工作還是在學習上, 用各種測試手段來保證程式碼質量, 不要從心理上過度依賴debug
- 學會抽象, 無論是工作還是學習中, 積累程式碼, 自己動手寫一些函式, 封裝, 並隨著時間去精煉它們, 慢慢的, 雖然最終你會發現你寫的程式碼99%都是屎, 但這個沉澱的過程對你一定有很大的幫助.
- 上面四條是zmq guide原作者的建議, 我表示比較贊同.
正確的處理context
你大致注意到了, 在上面的所有示例程式碼中, 每次都以zmq_ctx_new()
函式建立出一個名為context
的變數, 目前你不需要了解它的細節, 這只是ZMQ庫的標準套路. 甚至於你將來都不需要了解這個context裡面到底是什麼. 但你必須要遵循zmq中關於這個context的一些程式設計規定:
- 在一個程式起始時呼叫
zmq_ctx_new()
建立context - 在程式結束之前呼叫
zmq_ctx_destroy()
銷燬掉它
每個程式, 應該持有, 且應該只持有, 一個context. 當然, 目前來說, 你這樣理解就行了, 後續章節或許我們會深入探索一下context, 但目前, 請謹記, one context per process.
如果你在程式碼中呼叫了fork
系統呼叫, 那麼請在子程式程式碼區的開始處呼叫zmq_ctx_new()
, 為子程式建立自己的context
把屁股擦乾淨
網路程式設計和記憶體洩漏簡直就是一對狗男女, 要避免這些狗血的場景, 寫程式碼的時候, 時刻要謹記: 把屁股擦乾淨.在使用ZMQ程式設計的過程中, 我建議你:
- 在呼叫
zmq_ctx_destroy()
之前, 先呼叫zmq_close()
關閉掉所有的zmq socket. 否則zmq_ctx_destroy
可能會被一直阻塞著 - 儘量使用
zmq_send()
與zmq_recv()
來收發訊息, 儘量避免使用與zmq_msg_t
相關的API介面. 是的, 那些介面有額外的特性, 有額外的效能提升, 但在效能瓶頸不在這些細枝末節的時候, 不要過度造作. - 假如你非得用
zmq_msg_t
相關的介面收發訊息, 那麼請在呼叫zmq_msg_recv()
之後, 儘快的呼叫zmq_msg_close()
釋放掉訊息物件 - 如果你在一個程式中開了一堆堆的socket, 那麼你就需要在架構上思考一下, 你的程式是不是有設計不合理的地方.
- 在程式退出的時候, 時刻謹記關閉socket, 銷燬context
- 不要在多個執行緒間共享socket.
- 用完socket之後記得關閉.
- 上面是zmq guide作者給出的建議, 下面, 我再給你一條: 熟讀相關介面的manpage, 注意介面的返回值, 做好呼叫失敗後的災後重建工作
當然, 上面主要是對C語言作者的一些建議, 對於其它語言, 特別是有GC的語言, 使用ZMQ相關介面之前建議確認相關的binding介面是否正確處理了資源控制程式碼.
你為什麼需要ZMQ
網路程式設計, 特別是*nix平臺的網路程式設計, 99%程式設計師的啟蒙始於<Unix網路程式設計>這本書, 90%裡的專案充斥著linux socket, epoll與fd. 是的, 2018年了, 他們還是這麼幹的. 我們就從這個視角來列舉一下, 使用*nix平臺原生的網路API與多路IO介面, 你在寫服務端程式時需要頭疼的事情:
- 如何處理IO. 阻塞式IO太低效, 非同步IO程式碼不好寫.
- 如何平滑的向你的服務增刪機器, 平行擴容
- 如何傳遞訊息? 如何設計訊息結構? 通訊協議?
- 訊息傳遞過程中如何緩衝? 生產消費速度不一致時採用何種策略?
- 如何處理訊息丟失? 如何保證通訊的可靠性?
- 如何處理多種三層四層協議之間的協同?
- 訊息如何路由? 如何負載均衡? 如何實現有狀態的會話?
- 如何處理多程式語言的協同?
- 如何使訊息在多種架構機器上能通用讀寫? 如何實現了, 如何保證效率和成本?
- 如何處理網路錯誤?
我問你, 你頭大不大? 想不想死?
讀過開源專案嗎? 比如Hadoop Zookeeper, 你去觀摩一下zookeeper.c, 真是看的人頭大想死. 你再翻翻其它開源專案, 特別是用C/C++寫的Linux端程式, 每個都要把網路庫事件庫重新寫一遍.
所以矛盾很突出, 為什麼不能造一個大家都用的輪子呢? 原因很簡單, 有兩個方面:
- 對於大佬來說, 作業系統提供的網路API和事件API已經算是輪子了
- 真的要做一個通用的網路庫, 或者訊息庫, 其實難度非常大. AMQP就是一個例子, 你可以去感受一下.
那麼ZMQ解決了什麼問題呢? ZMQ給上面提出的問題都給了完美答案嗎? 理性的說, 肯定沒有, 但是ZMQ是這樣回答這些問題的:
- ZMQ用後臺執行緒實現了IO的非同步處理. 應用間的通訊使用了無鎖的資料結構.
- 叢集中的結點動態增刪的時候, ZMQ能默默的正確處理重連/斷連等髒活.
- ZMQ努力的對訊息做了佇列快取, 多數情況下, 預設的ZMQ行為為你提供了便利, 也足夠應付你的應用場景.
- 當緩衝佇列爆掉時, ZMQ提供了"高水位閾值"這個機制. 這個機制在佇列爆掉時將自動阻塞傳送者, 或者靜靜的扔掉資料. 具體哪種行為, 取決於你使用的socket的型別
- ZMQ可以歡快的跑在多種傳輸層協議上, 更改協議甚至不需要怎麼改程式碼(好吧, 至少要改那麼一兩行)
- ZMQ在多種套路下, 都會像爸爸看兒子那樣小心翼翼的照顧那些低能兒(處理訊息的速度比較慢的那些結點)
- 有多種現成的套路讓你實現花式負載均衡. 比如請求迴應套路, 釋出訂閱套路.
- ZMQ可以很簡單的建立代理, 代理是一種有效降低網路區域性複雜度的技術.
- ZMQ保證訊息傳遞的原子性. 要麼所有訊息都收到, 要麼你一根毛都收不到.
- ZMQ本身並不引入二進位制訊息的規範. 你如何解釋訊息, 那完全是你的自由.
- ZMQ多數情況下可以妥善的處理網路異常, 比如在合適的場合進行合適的重試重傳, 這些髒活對於你來說, 都是透明的, 不可見的.
- ZMQ能有效降低你IDC裡的碳排放. 保護環境人人有責.
總之, 就是很好, 當然了沒有一個框架庫的作者會說自己的產品不好, 而具體好不好, 學了用了之後才會知道, 上面的點看一看得了, 別當真.
socket的可擴充套件性
在釋出-訂閱套路由, 當你開啟多個村民的時候, 你會發現, 所有村民都能收到訊息, 而村口的喇叭也工作正常. 這就是zmq socket的可擴充套件性. 對於釋出端來講, 開發人員始終面對的是一個socket, 而不用去管連線我到底下面會有多少訂閱使用者. 這樣極大簡化了開發人員的工作, 實際釋出端程式跑起來的時候, 會自主進行適應, 並執行最合理的行為. 更深層次一點, 你可能會說, 這樣的功能, 我用epoll在linux socket上也能實現, 但是, 當多個訂閱者開始接收資料的時候, 你仔細觀察你cpu的負載, 你會發現釋出端程式不光正確接納了所有訂閱者, 更重要的是把工作負載通過多執行緒均衡到了你電腦的多個核心上. 日最大程度的榨乾了你的cpu效能. 如果你單純的用epoll和linux socket來實現這個功能, 釋出端只會佔用一個核心, 除非你再寫一坨程式碼以實現多執行緒或多程式版的村口大喇叭.