ZeroMQ 教程 001 : 基本概覽

本文主要譯自 zguide - chapter one. 但並非照本翻譯.linux

介紹性的話我這裏就不翻譯了, 總結起來就是zmq很cool, 你應該嘗試一下.git

如何安裝與使用zmq

在Linux和Mac OS上, 請經過隨機附帶的包管理軟件, 或者home brew安裝zmq. 包名通常就叫zmq, 安裝上就好.程序員

安裝後, 以Mac OS爲例, 會出現一個新的頭文件 /usr/local/include/zmq.h , 和一個連接庫 /usr/local/lib/libzmq.a.github

因此, 若是你使用C語言, 那麼很簡單, 寫代碼的時候加上頭文件 #include <zmq.h> 就行了, 連接的時候加上庫 -lzmq 就行了.面試

若是你使用的不是C語言, 那麼也很簡單, 去複習一下C語言, 而後再回來看這個教程. 須要注意的是, 這個教程裏的全部示例代碼在編譯的時候須要指定 -std=c99.apache

一問一答例子入門

先放一個一問一答的例子來讓你感覺一下編程

這是服務端代碼設計模式

#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;
}

這是客戶端代碼api

#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

這個例子就很簡單, 要點有如下

服務端上:

  1. 服務端建立一個context, 再用context去建立一個socket, 再把socket綁定到tcp的5555端口上
  2. 先從socket中讀取數據, 再向socket中寫入數據

客戶端上

  1. 客戶端也是建立一個context, 再用context去建立一個socket, 與服務端不一樣的, 客戶端是用tcp協議鏈接到本機的5555端口上, 這也是服務端監聽的網絡地址
  2. 客戶端先向socket裏寫入數據, 再從socket中讀取數據
  3. 客戶端執行 寫入-讀出 這樣的操做十遍後, 關閉socket, 銷燬context, 退出程序

看起來套路和你從<Unix 網絡編程>裏學到的差很少嘛. 不過, 你能夠試試, 先啓動客戶端, 而後再啓動服務端, 你會發現, 程序沒有崩潰. 這就是zmq高明的地方, 把操做系統原生脆弱的網絡編程接口進行了封裝. 而且實際上不止於此, 後面咱們會學到更多. 這只是開胃小菜.

注意: 網絡通訊中沒有字符串!

你可能注意到了咱們上面的例子裏, 其實客戶端與服務端互相傳輸的數據裏, 並無包含C風格字符串最後一位的'\0'. 請時刻謹記這一點, 網絡通訊中, 流動在網絡編程API上的數據, 對於API自己來講, 都是字節序列而已, 如何解釋這些字節序列, 是網絡編程API的使用者的責任. 好比上面, 咱們須要在每次接收數據的時候記錄接收的數據的大小, 而且在buffer中爲接收到的數據以後的一個字節賦值爲0, 即人爲的把接收到的數據解釋爲字符串. 而對於zmq_sendzmq_recv來講, 它並不關心客戶端與服務端傳輸的數據具體是什麼.

這在全部網絡編程API中都是這個套路, 不光是zmq, linux socket, winsock, 都是這樣. 字符串? 不存在的. 我能看見的, 只是字節序列而已.

獲取zmq的版本信息

當你要把zmq應用到實際項目中的時候, 版本號注是一個你必須關注的事情了. 固然, 項目初期你能夠不關心它, 或者項目規模較小的時候你能夠不關心它. 但隨着項目的進展, 項目中使用到的庫的版本號就成了全部人必須關心的事情. 實際上全部第三方庫的版本都是一個須要項目owner關心的事情, 由於總有一些sb會作出如下的事情:

  1. 當一個sb須要一個額外功能的時候, 他會以光速引入一個庫, 而且從不檢查這個庫是否已經被引入到項目中.
  2. 當這個sb引入這個第三方庫的時候, 這個sb只關心本身寫的代碼能不能順利編譯運行
  3. 很大機率這個sb不會仔細閱讀項目的構造工具腳本, 這個sb只關心如何把這坨他看不懂的東西, 搞的不報錯, 能運行起來.
  4. 很在可能這個sb引入的這個第三方庫, 項目已經在先前引入了, 通過這個sb此次操做, 項目中會存在不一樣版本的兩個同名庫的引用.
  5. 通常狀況下這個sb因爲追求cool, 會引入最新的版本, 甚至是beta版
  6. 多數狀況下, 此次操做引入的負面影響會在幾個月後爆發.

因此, 在這裏衷心的建議你, 時刻關注你項目中使用的全部第三方庫, 搞清楚你的項目構造工具鏈的運行過程. 而對於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的返回值

有三件事我建議你養成習慣

  1. 封裝一些工具函數, 而且在你的編程生涯中不斷的改進它們
  2. 多查閱編程手冊, 在*nix平臺上, 多查閱manpage
  3. 對於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

這個例子中須要特別注意的點有:

  1. 村民必須經過zmq_setsockopt函數設置一個過濾器, 以說明關心哪些消息. 若是不設置過濾器, 那麼什麼消息都不會收到
  2. 即使你先啓動村民, 再給大喇叭上電, 村民仍是會遺漏掉大喇叭最初始發送的一些消息..呃, 這麼講吧, 大概能丟失幾萬條這樣.這是由於tcp創建鏈接須要時間. 在創建鏈接這段時間內, 大喇叭已經向外瘋狂發送了不少消息. 在後續章節, 大概在第三章, 咱們將會學到如何嚴格同步村民與喇叭. 讓喇叭在全部村民就緒以後再開始發送消息.

另外, 關於這個例子中的兩種socket類型, 有如下特色

  1. ZMQ_PUB類型的socket, 若是沒有任何村民與其相連, 其全部消息都將被簡單就地拋棄
  2. ZMQ_SUB類型的socket, 便是村民, 能夠與多個ZMQ_PUB類型的socket相連, 即村民能夠同時收聽多個喇叭, 但必須爲每一個喇叭都設置過濾器. 不然默認狀況下, zmq認爲村民不關心喇叭裏的全部內容.
  3. 當一個村民收聽多個喇叭時, 接收消息採用公平隊列策略
  4. 若是存在至少一個村民在收聽這個喇叭, 那麼這個喇叭的消息就不會被隨意拋棄: 這句話的意思是, 當消息過多, 而村民的消化能力比較低的話, 未發送的消息會緩存在喇叭裏.
  5. 在ZMQ大版本號在3以上的版本里, 當喇叭與村民的速度不匹配時. 若使用的傳輸層協議是tcpipc這種面向鏈接的協議, 則堆積的消息緩存在喇叭裏, 當使用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數嗎?

分治套路

分治套路里有三個角色:

  1. Ventilator. 包工頭, 向手下各個工程隊分派任務. 一個.
  2. Worker. 工程隊, 從包工頭裏接收任務, 幹活. 多個.
  3. 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;
}

這個示例程序的邏輯流程是這樣的:

  1. 包工頭向兩個角色發送消息: 向工程隊發送共計100個任務, 向監理髮送消息, 通知監理開始幹活
  2. 工程隊接收來自包工頭的消息, 並按消息裏的數值, 睡眠指定毫秒. 每一個任務結束後都通知監理.
  3. 監理先是接收來自包工頭的消息, 開始計時. 而後統計來自工程隊的消息, 當收集到100個任務完成的消息後, 計算實際耗時.

包工頭裏輸出的預計耗時是100個任務的共計耗時, 在監理那裏統計的實際耗時則是由多個工程隊並行處理100個任務實際的耗時.

這裏個例子中須要注意的點有:

  1. 這個例子中使用了ZMQ_PULLZMQ_PUSH兩種socket. 分別供消息分發方與消息接收方使用. 看起來略微有點相似於發佈-訂閱套路, 具體之間的區別後續章節會講到.
  2. 工程隊上接包工頭, 下接監理. 在任務執行過程當中, 你能夠隨意的增長工程隊的數量.
  3. 咱們經過讓包工頭通知監理, 以及手動輸入enter來啓動任務分發的方式, 手動同步了工程隊/包工頭/監理. PUSH/PULL模式雖然和PUB/SUB不同, 不會丟失消息. 但若是不手動同步的話, 最早創建鏈接的工程隊將幾乎把全部任務都接收到手, 致使後續完成鏈接的工程隊拿不到任務, 任務分配不平衡.
  4. 包工頭分派任務使用的是輪流/平均分配的方式.這是一種簡單的負載均衡
  5. 監理接收多個工程隊的消息, 使用的是公平隊列策略.

因此, 你大體能看出來, 分治套路里有一個核心問題, 就是任務分發者與任務執行者之間的同步. 若是在全部執行者均與分發者創建鏈接後, 進行分發, 那麼任務分發是比較公平的. 這就須要應用程序開發者本身負責同步事宜. 關於這個話題進一步的技巧將在第三章進一步討論.

使用ZMQ的一點建議

如今咱們寫了三個例子, 分別是請求-迴應套路, 發佈-訂閱套路, 流水線套路. 在繼續進一步學習以前, 有必要對一些點進行強調

  1. 學習ZMQ請慢慢學. 不要着急. 其實學習全部庫工具都是如此, learn it by hard way. 不少程序員老是看不到 "看懂" 和 "學會" 這兩個層次之間的十萬千米距離, 以爲"看懂"了, 再抄點代碼, 複製粘貼一下, 就算是"精通"ZMQ了, 不, 不, 不, 差得遠, 當年你就這這樣學C語言的, 因此除了數據結構實驗, 你寫不出任何有用的代碼. 我建議你一步一步的學習, 不要急功近利, 仔細的寫代碼, 琢磨, 體會, 理解.
  2. 養成良好的編程風格, 不要寫屎同樣的代碼.
  3. 重試自測, 不管是在工做仍是在學習上, 用各類測試手段來保證代碼質量, 不要從心理上過分依賴debug
  4. 學會抽象, 不管是工做仍是學習中, 積累代碼, 本身動手寫一些函數, 封裝, 並隨着時間去精煉它們, 慢慢的, 雖然最終你會發現你寫的代碼99%都是屎, 但這個沉澱的過程對你必定有很大的幫助.
  5. 上面四條是zmq guide原做者的建議, 我表示比較贊同.

正確的處理context

你大體注意到了, 在上面的全部示例代碼中, 每次都以zmq_ctx_new()函數建立出一個名爲context的變量, 目前你不須要了解它的細節, 這只是ZMQ庫的標準套路. 甚至於你未來都不須要了解這個context裏面究竟是什麼. 但你必需要遵循zmq中關於這個context的一些編程規定:

  1. 在一個進程起始時調用zmq_ctx_new()建立context
  2. 在進程結束以前調用zmq_ctx_destroy()銷燬掉它

每一個進程, 應該持有, 且應該只持有, 一個context. 固然, 目前來講, 你這樣理解就好了, 後續章節或許咱們會深刻探索一下context, 但目前, 請謹記, one context per process.

若是你在代碼中調用了fork系統調用, 那麼請在子進程代碼區的開始處調用zmq_ctx_new(), 爲子進程建立本身的context

把屁股擦乾淨

網絡編程和內存泄漏簡直就是一對狗男女, 要避免這些狗血的場景, 寫代碼的時候, 時刻要謹記: 把屁股擦乾淨.在使用ZMQ編程的過程當中, 我建議你:

  1. 在調用zmq_ctx_destroy()以前, 先調用zmq_close()關閉掉全部的zmq socket. 不然zmq_ctx_destroy可能會被一直阻塞着
  2. 儘可能使用zmq_send()zmq_recv()來收發消息, 儘可能避免使用與zmq_msg_t相關的API接口. 是的, 那些接口有額外的特性, 有額外的性能提高, 但在性能瓶頸不在這些細枝末節的時候, 不要過分造做.
  3. 假如你非得用zmq_msg_t相關的接口收發消息, 那麼請在調用zmq_msg_recv()以後, 儘快的調用zmq_msg_close()釋放掉消息對象
  4. 若是你在一個進程中開了一堆堆的socket, 那麼你就須要在架構上思考一下, 你的程序是否是有設計不合理的地方.
  5. 在進程退出的時候, 時刻謹記關閉socket, 銷燬context
  6. 不要在多個線程間共享socket.
  7. 用完socket以後記得關閉.
  8. 上面是zmq guide做者給出的建議, 下面, 我再給你一條: 熟讀相關接口的manpage, 注意接口的返回值, 作好調用失敗後的災後重建工做

固然, 上面主要是對C語言做者的一些建議, 對於其它語言, 特別是有GC的語言, 使用ZMQ相關接口以前建議確認相關的binding接口是否正確處理了資源句柄.

你爲何須要ZMQ

網絡編程, 特別是*nix平臺的網絡編程, 99%程序員的啓蒙始於<Unix網絡編程>這本書, 90%裏的項目充斥着linux socket, epoll與fd. 是的, 2018年了, 他們仍是這麼幹的. 咱們就從這個視角來列舉一下, 使用*nix平臺原生的網絡API與多路IO接口, 你在寫服務端程序時須要頭疼的事情:

  1. 如何處理IO. 阻塞式IO過低效, 異步IO代碼很差寫.
  2. 如何平滑的向你的服務增刪機器, 平行擴容
  3. 如何傳遞消息? 如何設計消息結構? 通信協議?
  4. 消息傳遞過程當中如何緩衝? 生產消費速度不一致時採用何種策略?
  5. 如何處理消息丟失? 如何保證通信的可靠性?
  6. 如何處理多種三層四層協議之間的協同?
  7. 消息如何路由? 如何負載均衡? 如何實現有狀態的會話?
  8. 如何處理多編程語言的協同?
  9. 如何使消息在多種架構機器上能通用讀寫? 如何實現了, 如何保證效率和成本?
  10. 如何處理網絡錯誤?

我問你, 你頭大不大? 想不想死?

讀過開源項目嗎? 好比Hadoop Zookeeper, 你去觀摩一下zookeeper.c, 真是看的人頭大想死. 你再翻翻其它開源項目, 特別是用C/C++寫的Linux端程序, 每一個都要把網絡庫事件庫從新寫一遍.

因此矛盾很突出, 爲何不能造一個你們都用的輪子呢? 緣由很簡單, 有兩個方面:

  1. 對於大佬來講, 操做系統提供的網絡API和事件API已經算是輪子了
  2. 真的要作一個通用的網絡庫, 或者消息庫, 其實難度很是大. AMQP就是一個例子, 你能夠去感覺一下.

那麼ZMQ解決了什麼問題呢? ZMQ給上面提出的問題都給了完美答案嗎? 理性的說, 確定沒有, 可是ZMQ是這樣回答這些問題的:

  1. ZMQ用後臺線程實現了IO的異步處理. 應用間的通訊使用了無鎖的數據結構.
  2. 集羣中的結點動態增刪的時候, ZMQ能默默的正確處理重連/斷連等髒活.
  3. ZMQ努力的對消息作了隊列緩存, 多數狀況下, 默認的ZMQ行爲爲你提供了便利, 也足夠應付你的應用場景.
  4. 當緩衝隊列爆掉時, ZMQ提供了"高水位閾值"這個機制. 這個機制在隊列爆掉時將自動阻塞發送者, 或者靜靜的扔掉數據. 具體哪一種行爲, 取決於你使用的socket的類型
  5. ZMQ能夠歡快的跑在多種傳輸層協議上, 更改協議甚至不須要怎麼改代碼(好吧, 至少要改那麼一兩行)
  6. ZMQ在多種套路下, 都會像爸爸看兒子那樣當心翼翼的照顧那些低能兒(處理消息的速度比較慢的那些結點)
  7. 有多種現成的套路讓你實現花式負載均衡. 好比請求迴應套路, 發佈訂閱套路.
  8. ZMQ能夠很簡單的建立代理, 代理是一種有效下降網絡局部複雜度的技術.
  9. ZMQ保證消息傳遞的原子性. 要麼全部消息都收到, 要麼你一根毛都收不到.
  10. ZMQ自己並不引入二進制消息的規範. 你如何解釋消息, 那徹底是你的自由.
  11. ZMQ多數狀況下能夠妥善的處理網絡異常, 好比在合適的場合進行合適的重試重傳, 這些髒活對於你來講, 都是透明的, 不可見的.
  12. ZMQ能有效下降你IDC裏的碳排放. 保護環境人人有責.

總之, 就是很好, 固然了沒有一個框架庫的做者會說本身的產品很差, 而具體好很差, 學了用了以後纔會知道, 上面的點看一看得了, 別當真.

socket的可擴展性

在發佈-訂閱套路由, 當你開啓多個村民的時候, 你會發現, 全部村民都能收到消息, 而村口的喇叭也工做正常. 這就是zmq socket的可擴展性. 對於發佈端來說, 開發人員始終面對的是一個socket, 而不用去管鏈接我到底下面會有多少訂閱用戶. 這樣極大簡化了開發人員的工做, 實際發佈端程序跑起來的時候, 會自主進行適應, 並執行最合理的行爲. 更深層次一點, 你可能會說, 這樣的功能, 我用epoll在linux socket上也能實現, 可是, 當多個訂閱者開始接收數據的時候, 你仔細觀察你cpu的負載, 你會發現發佈端進程不光正確接納了全部訂閱者, 更重要的是把工做負載經過多線程均衡到了你電腦的多個核心上. 日最大程度的榨乾了你的cpu性能. 若是你單純的用epoll和linux socket來實現這個功能, 發佈端只會佔用一個核心, 除非你再寫一坨代碼以實現多線程或多進程版的村口大喇叭.

相關文章
相關標籤/搜索