通常需要用到ZeroMQ的人应该已经具备比较深的编程知识,所以有个问题就是目前能找到的大多数入门教程对小白不够友好。
所以我最近在学的时候,顺便做些记录。
就不摆概念定义了,百度一下就有。而且刚刚学可能也看不懂,容易打击信心,所以直接从例子开始吧。
接下来会用三个例子,介绍最基本的模式和用法。
第一个例子
我们想实现Hello World这个经典功能。但是ZeroMQ不是一个编程语言,是一个传输工具。所以我们想实现的功能是:A给B发送一个消息,B收到消息返回给A一些回答。也就是客户端和服务器干的事情。
客户端发送“Hello”到服务器,然后服务器用“World”来应答。
下面是Hello world服务器(hwserver.py):
# -*-coding:utf-8-*-
import time
import zmq
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("tcp://*:5555")
while True:
# Wait for next request from client
message = socket.recv()
print("Received request: %s" % message)
# Do some 'work'
time.sleep(0.5)
# Send reply back to client
socket.send(b"World")
把REP套接字绑定到tcp://*:5555上,就像是安装一个电话机在这里,这样别人就可以给它打电话传输信息了。
while True让服务器会一直运行,等待客户端的消息,接收到之后,发送消息“World”。
字符串前面b代表这是一个bytes对象,下同。
下面是客户端(hwclient.py):
# -*-coding:utf-8-*-
import zmq
context = zmq.Context()
# Socket to talk to server
print("Connecting to hello world server...")
socket = context.socket(zmq.REQ)
socket.connect("tcp://localhost:5555")
# Do 10 requests, waiting each time for a response
for request in range(10):
print("Sending request %s ..." % request)
socket.send(b"Hello")
# Get the reply.
message = socket.recv()
print("Received reply %s [ %s ]" % (request, message))
客户端取连接服务器的位置,相当于给之前安装的电话机打电话。
然后测试,发送了10次消息,每次都是发送的“Hello”。
然后接受服务器返回的信息,打印出来。
首先打开服务器,即运行hwserver.py :
服务器正在等待。
然后运行客户端,即hwclient.py :
可以看到客户端发送了10次消息,然后每次都收到回复“World”。
而服务器:
可以看到服务器接收到了10次消息“Hello”。并且还在运行,等待。此时如果再运行客户端,服务器就会再多出10条消息。
第二个例子:
这是一个经典模式,即单向的数据发布。一台服务器把更新推到一组客户端。
下面我们要做的事情就是:模拟天气预报,服务器更新天气信息,并且把天气信息告诉每一个订阅电台收听天气预报的人。
下面是天气更新服务器(wuserver.py):
# -*-coding:utf-8-*-
# Weather update server
# Binds PUB socket to tcp://*:5556
# Publishes random weather updates
#
import zmq
from random import randrange
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))
用随机数来得到模拟的天气信息。
PUB代表publish发布,将PUB套接字绑到tcp://*:5556
得到天气信息之后,发送。
然后是天气客户端(wuclient.py):
# -*-coding:utf-8-*-
# Weather update client
# Connects SUB socket to tcp://localhost:5556
# Collects weather updates and finds avg temp in zipcode
#
import sys
import zmq
# Socket to talk to server
context = zmq.Context()
socket = context.socket(zmq.SUB)
print("Collecting updates from weather server...")
socket.connect("tcp://localhost:5556")
# Subscribe to zipcode, default is NYC, 10001
zip_filter = sys.argv[1] if len(sys.argv) > 1 else "10001"
# Python 2 - ascii bytes to unicode str
if isinstance(zip_filter, bytes):
zip_filter = zip_filter.decode('ascii')
socket.setsockopt_string(zmq.SUBSCRIBE, zip_filter)
# Process 5 updates
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))
)
SUB 就是subscribe,订阅。要与天气预报电台连接上。
zip_filter就是你要找出特定地区的相关信息所用到的筛选器。默认是10001。
如果是bytes类型的,就把它用decode函数编程str类型数据。
然后非常重要的一点是,要用setsockopt_string和SUBSCRIBE来设置一个订阅。就像你看油管,要点击“订阅”键才能订阅成功。
如果漏掉这一步,不会得到任何信息。
然后更新5个信息,取平均值,打印出来。
首先我们运行天气服务器(wuserver.py):
可以看到天气服务器开始运行。
然后我们运行客户端(wuclient.py):
第三个例子:
假设我们有一个很艰巨的任务,如果我们能把这个任务分成很多小任务,并且把它们分配给各个工人同时来完成的话,就会提高效率。
为了完成这样一个模型,我们需要三个部分:
流水线如图所示:
接下来我们挨个实现每个部分:
下面是并行任务发生器(taskvent.py):
# -*-coding:utf-8-*-
# Task ventilator
# Binds PUSH socket to tcp://localhost:5557
# Sends batch of tasks to workers via that socket
#
# Author: Lev Givon
import zmq
import random
import time
try:
raw_input
except NameError:
# Python 3
raw_input = input
context = zmq.Context()
# Socket to send messages on
sender = context.socket(zmq.PUSH)
sender.bind("tcp://*:5557")
# Socket with direct access to the sink: used to syncronize start of batch
sink = context.socket(zmq.PUSH)
sink.connect("tcp://localhost:5558")
print("Press Enter when the workers are ready: ")
_ = raw_input()
print("Sending tasks to workers...")
# The first message is "0" and signals start of batch
sink.send(b'0')
# Initialize random number generator
random.seed()
# Send 100 tasks
total_msec = 0
for task_nbr in range(100):
# Random workload from 1 to 100 msecs
workload = random.randint(1, 100)
total_msec += workload
sender.send_string(u'%i' % workload)
print("Total expected cost: %s msec" % total_msec)
# Give 0MQ time to deliver
time.sleep(1)
PUSH是用于发送的,PULL是用于接收的。
我们把sender绑到tcp://*:5557上,sender用于发送每个任务。
把sink绑到tcp://localhost:5558上,用于同时开始处理一批任务。
sink发送一个0过去,作为开始处理的信号。
然后随机产生一百个数字,每个数字相当于每个任务所需要的时间,然后把任务信息用sender分发下去。
并打印出依次完成任务需要的总时间。
下面是并行任务工人(taskwork.py):
# -*-coding:utf-8-*-
# Task worker
# Connects PULL socket to tcp://localhost:5557
# Collects workloads from ventilator via that socket
# Connects PUSH socket to tcp://localhost:5558
# Sends results to sink via that socket
#
# Author: Lev Givon
import sys
import time
import zmq
context = zmq.Context()
# Socket to receive messages on
receiver = context.socket(zmq.PULL)
receiver.connect("tcp://localhost:5557")
# Socket to send messages to
sender = context.socket(zmq.PUSH)
sender.connect("tcp://localhost:5558")
# Process tasks forever
while True:
s = receiver.recv()
# Simple progress indicator for the viewer
sys.stdout.write('.')
sys.stdout.flush()
# Do the work
time.sleep(int(s)*0.001)
# Send results to sink
sender.send(b'')
receiver用于接收5557上的消息,也就是之钱并行任务发生器中sender发出来的任务。
这里的sender用于向5558传送消息。
while True一直等待消息。
s从为从5557收到的消息,也就是每一个任务。
用sys.stdout来显示完成进度。
因为s代表的是每个任务耗时,所以Do the work部分就是sleep对应的毫秒数,方便最后用time函数来计算实际总运行时间。
再用sender把结果传送给5558上的sink。但由于这里没有结果,所以是空字符串。
下面是并行任务接收器(tasksink.py):
# -*-coding:utf-8-*-
# Task sink
# Binds PULL socket to tcp://localhost:5558
# Collects results from workers via that socket
#
# Author: Lev Givon
import sys
import time
import zmq
context = zmq.Context()
# Socket to receive messages on
receiver = context.socket(zmq.PULL)
receiver.bind("tcp://*:5558")
# Wait for start of batch
s = receiver.recv()
# Start our clock now
tstart = time.time()
# Process 100 confirmations
for task_nbr in range(100):
s = receiver.recv()
if task_nbr % 10 == 0:
sys.stdout.write(':')
else:
sys.stdout.write('.')
sys.stdout.flush()
# Calculate and report duration of batch
tend = time.time()
print("Total elapsed time: %d msec" % ((tend-tstart)*1000))
receiver用于接收5558上的消息也就是任务接收器中的sender传输过来的信息,当然,我们已经知道,是空字符串而已。
但是别忘了,不全是空字符串,因为第一步中传输了一个0给5558的sink,用于开始批处理任务。这就是for循环之外的s所接收到的信息,接收到开始信号之后,用tstart代表开始工作的时间,然后接收所有任务的结果,也就是100条信息。
最后用tend代表结束时的时间,就可以得到并行任务的总耗时。
下面执行上述代码,看看结果:
首先运行并行任务发生器(taskevent.py):
等待工人就位。
按下回车键之后:
然后运行并行任务工人(taskwork.py):
任务理论总耗时为5秒左右。
最后运行并行任务接收器(tasksink.py):
实际总耗时为36毫秒,可以看到确实是所有任务确实是并行的。
速度快了140倍。
未完待续。。。