ZeroMQ入門

概述

ZeroMQ(也稱為 ?MQ,0MQ 或 zmq)是一個可嵌入的網絡通訊庫(對 Socket 進行了封裝)。 它提供了攜帶跨越多種傳輸協議(如:進程內,進程間,TCP 和多播)的原子消息的 sockets 。 有了ZeroMQ,我們可以通過發布-訂閱、任務分發、和請求-回復等模式來建立 N-N 的 socket 連接。 ZeroMQ 的異步 I / O 模型為我們提供可擴展的基于異步消息處理任務的多核應用程序。 它有一系列語言API(幾乎囊括所有編程語言),并能夠在大多數操作系統上運行。

傳統的 TCP Socket 的連接是1對1的,可以認為“1個 socket = 1個連接”,每一個線程獨立維護一個 socket 。但是 ZMQ 摒棄了這種1對1的模式,ZMQ的 Socket 可以很輕松地實現1對N和N對N的連接模式,一個 ZMQ 的 socket 可以自動地維護一組連接,用戶無法操作這些連接,用戶只能操作套接字而不是連接本身。所以說在 ZMQ 的世界里,連接是私有的。

三種基本模型

ZMQ 提供了三種基本的通信模型,分別是 Request-Reply 、Publish-Subscribe 和 Parallel Pipeline ,接下來舉例說明三種模型并給出相應的代碼實現。

Request-Reply(請求-回復)

以 “Hello World” 為例??蛻舳税l起請求,并等待服務端回應請求。客戶端發送一個簡單的 “Hello”,服務端則回應一個 “World”??梢杂?N 個客戶端,一個服務端,因此是 1-N 連接。

Request-Reply

服務端代碼如下:

  • hwserver.java
import org.zeromq.ZMQ;
public class hwserver {
    public static void main(String[] args) throws InterruptedException {
        ZMQ.Context context = ZMQ.context(1);
        ZMQ.Socket responder = context.socket(ZMQ.REP);
        responder.bind("tcp://*:5555");
        while (!Thread.currentThread().isInterrupted()) {
            byte[] request = responder.recv(0);
            System.out.println("Received" + new String(request));
            Thread.sleep(1000);
            String reply = "World";
            responder.send(reply.getBytes(),0);
        }
        responder.close();
        context.term();
    }
}
  • hwserver.py
import time
import zmq
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("tcp://*:5555")
while True:
    message = socket.recv()
    print("Received request: %s" % message)
    # Do some 'work'
    time.sleep(1)
    socket.send(b"World")

客戶端代碼如下:

  • hwclient.java
import org.zeromq.ZMQ;
public class hwclient {
    public static void main(String[] args) {
        ZMQ.Context context = ZMQ.context(1);
        ZMQ.Socket requester = context.socket(ZMQ.REQ);
        requester.connect("tcp://localhost:5555");
        for (int requestNbr = 0; requestNbr != 10; requestNbr++) {
            String request = "Hello";
            System.out.println("Sending Hello" + requestNbr);
            requester.send(request.getBytes(),0);
            byte[] reply = requester.recv(0);
            System.out.println("Reveived " + new String(reply) + " " + requestNbr);
        }
        requester.close();
        context.term();
    }
}
  • hwclient.py
import zmq
context = zmq.Context()
print("Connecting to hello world server...")
socket = context.socket(zmq.REQ)
socket.connect("tcp://localhost:5555")
for request in range(10):
    print("Sending request %s ..." % request)
    socket.send(b"Hello")
    message = socket.recv()
    print("Received reply %s [ %s ]" % (request,message))

從以上的過程,我們可以了解到使用 ZMQ 寫基本的程序的方法,需要注意的是:

  1. 服務端和客戶端無論誰先啟動,效果是相同的,這點不同于 Socket。
  2. 在服務端收到信息以前,程序是阻塞的,會一直等待客戶端連接上來。
  3. 服務端收到信息后,會發送一個 “World” 給客戶端。值得注意的是一定是客戶端連接上來以后,發消息給服務端,服務端接受消息然后響應客戶端,這樣一問一答。
  4. ZMQ 的通信單元是消息,它只知道消息的長度,并不關心消息格式。因此,你可以使用任何你覺得好用的數據格式,如 Xml、Protocol Buffers、Thrift、json 等等。

Publish-Subscribe(發布-訂閱)

下面以一個天氣預報的例子來介紹該模式。

服務端不斷地更新各個城市的天氣,客戶端可以訂閱自己感興趣(通過一個過濾器)的城市的天氣信息。

Publish-Subscribe

服務端代碼如下:

  • wuserver.java
import org.zeromq.ZMQ;
import java.util.Random;
public class wuserver {
    public static void main(String[] args) {
        ZMQ.Context context = ZMQ.context(1);
        ZMQ.Socket publisher = context.socket(ZMQ.PUB);
        publisher.bind("tcp://*:5556");
        publisher.bind("icp://weather");
        Random srandom = new Random(System.currentTimeMillis());
        while (!Thread.currentThread().isInterrupted()) {
            int zipcode, temperature, relhumidity;
            zipcode = 10000 + srandom.nextInt(10000);
            temperature = srandom.nextInt(215) - 80 + 1;
            relhumidity = srandom.nextInt(50) + 10 + 1;
            String update = String.format("%05d %d %d", zipcode, temperature, relhumidity);
        }
        publisher.close();
        context.term();
    }
}
  • wuserver.py
from random import randrange
import zmq
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind("tcp://*:5556")
while True:
    zipcode = randrange(1, 100000)
    temperature = randrange(-80, 135)
    relhumidity = randrange(10, 60)
    socket.send_string("%i %i %i" % (zipcode, temperature, relhumidity))

客戶端代碼如下:

  • wuclient.java
import org.zeromq.ZMQ;
import java.util.StringTokenizer;
public class wuclient {
    public static void main(String[] args) {
        ZMQ.Context context = ZMQ.context(1);
        ZMQ.Socket suscriber = context.socket(ZMQ.SUB);
        suscriber.connect("tcp://localhost:5556");
        String filter = (args.length > 0) ? args[0] : "10001";
        suscriber.suscribe(filter.getBytes());  //過濾條件
        int update_nbr;
        long total_temp = 0;
        for (update_nbr = 0; update_nbr < 100; update_nbr++) {
            String string = suscriber.recvStr(0).trim();
            StringTokenizer sscanf = new StringTokenizer(string, " ");
            int zipcode = Integer.valueOf(sscanf.nextToken());
            int temperature = Integer.valueOf(sscanf.nextToken());
            int relhumidity = Integer.valueOf(sscanf.nextToken());
            total_temp += temperature;
        }
        System.out.println("Average temperature for zipcode '" + filter + "' was " + (int) (total_temp / update_nbr));
        suscriber.close();
        context.term();
    }
}
  • wuclient.py
import sys
import zmq
context = zmq.Context()
socket = context.socket(zmq.SUB)
print("Collecting updates from weather server...")
socket.connect("tcp://localhost:5556")
zip_filter = sys.argv[1] if len(sys.argv) > 1 else "10001"
if isinstance(zip_filter, bytes):
    zip_filter = zip_filter.decode('ascii')
socket.setsockopt_string(zmq.SUBSCRIBE, zip_filter)
total_temp = 0
for update_nbr in range(5):
    string = socket.recv_string()
    zipcode, temperature, relhumidity = string.split()
    total_temp += int(temperature)
print("Average temperature for zipcode '%s' was %dF" % (zip_filter, total_temp / (update_nbr + 1)))

服務器端生成隨機數 zipcode、temperature、relhumidity 分別代表城市代碼、溫度值和濕度值,然后不斷地廣播信息。而客戶端通過設置過濾參數,接受特定城市代碼的信息,最終將收集到的溫度求平均值。

需要注意的是:

  1. 與 “Hello World” 例子不同的是,Socket 的類型變成 ZMQ.PUB 和 ZMQ.SUB 。
  2. 客戶端需要設置一個過濾條件,接收自己感興趣的消息。
  3. 發布者一直不斷地發布新消息,如果中途有訂閱者退出,其他均不受影響。當訂閱者再連接上來的時候,收到的就是后來發送的消息了。這樣比較晚加入的或者是中途離開的訂閱者必然會丟失掉一部分信息。這是該模式的一個問題,即所謂的 "Slow joiner" 。

Parallel Pipeline

Parallel Pipeline 處理模式如下:

  • ventilator 分發任務到各個 worker
  • 每個 worker 執行分配到的任務
  • 最后由 sink 收集從 worker 發來的結果
Parallel Pipeline
  • taskvent.java
import org.zeromq.ZMQ;
import java.io.IOException;
import java.util.Random;
import java.util.StringTokenizer;
public class taskvent {
    public static void main(String[] args) throws IOException {
        ZMQ.Context context = new ZMQ.context(1);
        ZMQ.Socket sender = context.socket(ZMQ.PUSH);
        sender.bind("tcp://*:5557");
        ZMQ.Socket sink = context.socket(ZMQ.PUSH);
        sink.connect("tcp://localhost:5558");
        System.out.println("Please enter when the workers are ready: ");
        System.in.read();
        System.out.println("Sending task to workes\n");
        sink.send("0",0);
        Random srandom = new Random(System.currentTimeMillis());
        int task_nbr;
        int total_msec = 0;
        for (task_nbr = 0; task_nbr < 100; task_nbr++) {
            int workload;
            workload = srandom.nextInt(100) + 1;
            total_msec += workload;
            System.out.print(workload + ".");
            String string = String.format("%d", workload);
            sender.send(string, 0);
        }
        System.out.println("Total expected cost: " + total_msec + " msec");
        sink.close();
        sender.close();
        context.term();
    }
}
  • taskvent.py
import zmq
import time
import random
try:
    raw_input
except NameError:
    raw_input = input
context = zmq.Context()
sender = context.socket(zmq.PUSH)
sender.bind("tcp://*:5557")
sink = context.socket(zmq.PUSH)
sink.connect("tcp://localhost:5558")
print("Please enter when workers are ready: ")
_ = raw_input()
print("Sending tasks to workers...")
sink.send(b'0')
random.seed()
total_msec = 0
for task_nbr in range(100):
    workload = random.randint(1, 100)
    total_msec += workload
    sender.send_string(u'%i' % workload)
print("Total expected cost: %s msec" % total_msec)
time.sleep(1)
  • taskwork.java
import org.zeromq.ZMQ;
public class taskwork {
    public static void main(String[] args) {
        ZMQ.Context context = ZMQ.context(1);
        ZMQ.Socket receiver = context.socket(ZMQ.PULL);
        receiver.connect("tcp://localhost:5557");
        ZMQ.Socket sender = context.socket(ZMQ.PUSH);
        sender.connect("tcp://localhost:5558");
        while (!Thread.currentThread().isInterrupted()) {
            String string = receiver.recv(0).trim();
            Long mesc = Long.parseLong(string);
            System.out.flush();
            System.out.print(string + ".");
            Sleep(mesc);
            sender.send("".getBytes(), 0);
        }
        sender.close();
        receiver.close();
        context.term();
    }
}
  • taskwork.py
import zmq
import time
import sys
context = zmq.Context()
receiver = context.socket(zmq.PULL)
receiver.connect("tcp://localhost:5557")
sender = context.socket(zmq.PUSH)
sender.connect("tcp://localhost:5558")
while True:
    s = receiver.recv()
    sys.stdout.write('.')
    sys.stdout.flush()
    time.sleep(int(s) * 0.001)
    sender.send(b'')
  • tasksink.java
import org.zeromq.ZMQ;
public class tasksink {
    public static void main(String[] args) {
        ZMQ.Context context = ZMQ.context(1);
        ZMQ.Socket receiver = context.socket(ZMQ.PULL);
        receiver.bind("tcp://*:5558");
        String string = new String(receiver.recv(0));
        long tstart = System.currentTimeMillis();
        int task_nbr;
        int total_mesc = 0;
        for (task_nbr = 0; task_nbr < 100; task_nbr++) {
            string = new String(receiver.recv(0).trim());
            if ((task_nbr / 10) * 10 == task_nbr) {
                System.out.print(":");
            } else {
                System.out.print(".");
            }
        }
        long tend = System.currentTimeMillis();
        System.out.println("\nTotal elapsed time: " + (tend - tstart) + "msec");
        receiver.close();
        context.term();
    }
}
  • tasksink.py
import time
import zmq
import sys
context = zmq.Context()
receiver = context.socket(zmq.PULL)
receiver.bind("tcp://*:5558")
s = receiver.recv()
tstart = time.time()
for task_nbr in range(1, 100):
    s = receiver.recv()
    if task_nbr % 10 == 0:
        sys.stdout.write(':')
    else:
        sys.stdout.write('.')
    sys.stdout.flush()
tend = time.time()
print("Total elapsed time: %d msec" % ((tend - tstart) * 1000))

以下兩點需要注意:

  1. ventilator 使用 ZMQ.PUSH 來分發任務;worker 用 ZMQ.PULL 來接收任務,用 ZMQ.PUSH 來發送結果;sink 用 ZMQ.PULL 來接收 worker 發來的結果。
  2. ventilator 既是服務端,也是客戶端(此時服務端是 sink);worker 在兩個過程中都是客戶端;sink 也一直都是服務端。

參考資料

linbingdong.com

歡迎進入博客 :linbingdong.com 獲取最新文章哦~

FullStackPlan

歡迎關注公眾號: FullStackPlan 獲取更多干貨哦~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • 拯救世界 略 開始的假設 我們假設你使用ZeroMQ 3.2以上的版本。我們假設你使用Linux或者類似的操作系統...
    lakerszhy閱讀 11,312評論 1 14
  • ZeroMQ API Reference 創建一個ZMQ的上下文環境,是ZMQ一切的開始。 線程安全,不需要自己加...
    分享放大價值閱讀 3,224評論 0 3
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • 1 Install ubuntu 16.10上安裝zeroMQ:(1) 下載zeromqwget https://...
    ColdRomantic閱讀 928評論 0 1
  • 小琪去男朋友阿澤家,阿澤媽媽對小琪說:“阿澤以前脾氣很壞,常發火。跟他爸相處超過半個月,指不定就要打起來。跟你一起...
    萱小蕾閱讀 266評論 0 0