概述
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 連接。
服務端代碼如下:
- 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 寫基本的程序的方法,需要注意的是:
- 服務端和客戶端無論誰先啟動,效果是相同的,這點不同于 Socket。
- 在服務端收到信息以前,程序是阻塞的,會一直等待客戶端連接上來。
- 服務端收到信息后,會發送一個 “World” 給客戶端。值得注意的是一定是客戶端連接上來以后,發消息給服務端,服務端接受消息然后響應客戶端,這樣一問一答。
- ZMQ 的通信單元是消息,它只知道消息的長度,并不關心消息格式。因此,你可以使用任何你覺得好用的數據格式,如 Xml、Protocol Buffers、Thrift、json 等等。
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 分別代表城市代碼、溫度值和濕度值,然后不斷地廣播信息。而客戶端通過設置過濾參數,接受特定城市代碼的信息,最終將收集到的溫度求平均值。
需要注意的是:
- 與 “Hello World” 例子不同的是,Socket 的類型變成 ZMQ.PUB 和 ZMQ.SUB 。
- 客戶端需要設置一個過濾條件,接收自己感興趣的消息。
- 發布者一直不斷地發布新消息,如果中途有訂閱者退出,其他均不受影響。當訂閱者再連接上來的時候,收到的就是后來發送的消息了。這樣比較晚加入的或者是中途離開的訂閱者必然會丟失掉一部分信息。這是該模式的一個問題,即所謂的 "Slow joiner" 。
Parallel Pipeline
Parallel Pipeline 處理模式如下:
- ventilator 分發任務到各個 worker
- 每個 worker 執行分配到的任務
- 最后由 sink 收集從 worker 發來的結果
- 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))
以下兩點需要注意:
- ventilator 使用 ZMQ.PUSH 來分發任務;worker 用 ZMQ.PULL 來接收任務,用 ZMQ.PUSH 來發送結果;sink 用 ZMQ.PULL 來接收 worker 發來的結果。
- ventilator 既是服務端,也是客戶端(此時服務端是 sink);worker 在兩個過程中都是客戶端;sink 也一直都是服務端。
參考資料
歡迎進入博客 :linbingdong.com 獲取最新文章哦~
歡迎關注公眾號: FullStackPlan 獲取更多干貨哦~