系列简介
该系列是根据电子工业出版社出版的《ZeroMQ云时代极速消息通信库》的章节进行系列章节分割(即一为书中第一章)
由于作者在书里的项目都是开源的,应该不会侵权,如有侵权可告知删除
什么是ZMQ
书中其实是有zmq的简介的,大概概括以下,就是一个可嵌入的网络库,作用就像是一个并发框架。
为什么叫ZMQ
如果你和我一样只是在网上找资料可能会困惑,为什么有的叫ZeroMq;有的叫0MQ;有的叫zmq,还有人叫ØMQ。
是因为这个框架想体现自己的“零代理”“零延时(尽可能零延时)”
ZMQ的模式
- 扇出
- 发布 - 订阅
- 任务分配
- 请求 - 应答
代码样例
3.7.8的python上成功运行的,此处不附上结果了,因为在网上看很多博主写的都有点笼统,所以建议如果有时间可以找一下那本书,去成体系的看一下。
请求-应答
应答服务端:有的博主把这一端比喻成老师,负责回应
"""
简易版的服务器
请求-应答模式,它对应适用于RPC(远程过程调用)和传统的客户端/服务器模型
在这个情况下,如果终止服务器并且重新启动它,客户端是无法正常恢复的
"""
import zmq
import time
context=zmq.Context()
# REP是应答方
socket=context.socket(zmq.REP)
socket.bind('tcp://127.0.0.1:5559')
while True:
message=socket.recv()
print("Received request: ", message)
time.sleep(1)
socket.send("World".encode())
请求-客户端:对应教师,这一端被比喻为学生,负责提问
import zmq
import time
context=zmq.Context()
print("Connecting to hello world server..." )
# REQ 是提问方
socket=context.socket(zmq.REQ)
socket.connect("tcp://127.0.0.1:5559")
for request in range(10):
print("Sending request ", request, "...")
socket.send('Hello'.encode())
message=socket.recv()
print("Received reply ", request, "[", message, "]")
我的理解是,这是一问一答的工作,所以即使你先打开客户端,再打开服务器,一问一答后仍然能够正常的工作。
需要注意的一点,如果终止服务器并重新启动它,客户端将无法恢复,因为从崩溃状态恢复是很复杂的(我还没看到那一章,后续应该会有)
发布 - 订阅
书上的例子是一个查询天气的服务端
发布者
from random import randrange
import zmq
context = zmq.Context()
# 发布人
publisher = context.socket(zmq.PUB)
publisher.bind("tcp://*:5556")
while True:
# 订阅号
zipcode = randrange(1, 100000)
# 温度
temperature = randrange(-80, 135)
# 相对湿度
relhumidity = randrange(10, 60)
publisher.send_string(f"{zipcode} {temperature} {relhumidity}")
订阅者
"""
单向数据发布模式-客户端
发布-订阅模式,没有终点也没有起点,就像是一个永无休止的广播
"""
import random
import sys
import time
import zmq
context = zmq.Context()
# 订阅人
subscriber = context.socket(zmq.SUB)
print("尝试连接天气服务器:Collecting updates from weather server...")
subscriber.connect("tcp://localhost:5556")
# 订阅邮政编码
"""sys.argv表示sys模块中的argv变量,
sys.argv是一个字符串的列表,其包含了命令行参数的列表,即使用命令行传递给你的程序的参数。
特别注意:脚本的名称总是sys.argv列表的第一个参数"""
zip_filter = sys.argv[1] if len(sys.argv) > 1 else "10001"
# 使用sub套接字时必须使用zmq_setsockopt设置一个订阅,如果没有设置订阅就不会受到消息
# zip_filter设置一个订阅码,有订阅号是特定值时客户端才可以接收,如果不设置订阅号则设置“”即可
subscriber.setsockopt_string(zmq.SUBSCRIBE, zip_filter)
total_temp = 0
print('连接成功开始接收信息')
for update_nbr in range(5):
now_string = subscriber.recv_string()
zipcode, temperature, relhumidity = now_string.split()
total_temp += int(temperature)
print(f'接收到{zipcode}')
print((f"Average temperature for zipcode "
f"'{zip_filter}' was {total_temp / (update_nbr + 1)} F"))
需要注意的是:
- 发布者不知道订阅者开始得到信息的精确时间,即使你先启动了一个订阅者,稍等片刻后再启动发布者,订阅者也会错过发布者发送的第一个消息(书上是说一个,我测试了一下应该不止一条消息)。这是因为订阅者连接到发布者的时候,需要的时间很短,在这个时候发布者可能已经把消息发出去了。
我在测试的时候直接在发布端加了个延时进行测试,延时后客户端可以正常接收,但是作者在书中说:“这是一个很愚蠢的方式,因为这脆弱且不雅又缓慢”之后会有介绍如何优雅实现。 - 一个订阅者可以连接到多个发布者,每次使用一个connect调用,那么数据将交错到达(‘公平排队’)因此,没有任何一个发布者能够淹没其他发布者(前面是原文,我的理解是:你可以看B站,可以看微博,可以订阅多个平台)
- 如果一个发布者没有连接的订阅者,那么他会简单的丢弃所有消息。
- 如果你是用的是TCP并且订阅者是慢速的,那么消息将在发布方排队。我们将在下一章了解如何通过“高水位线”来针对这种情况保护发布者。
- 从 ØMQ v3.x 开始,在使用连接的协议(tcp 或 ipc)时,过滤发生在发布方。使用epgm 协议,过滤发生在订阅方。但在 ØMQ v2.x 版本中,所有过滤都发生在订阅方。
Push-Pull
这个例子是,在发生器中生成多个并行任务,分发给多个工人,然后让工人汇总结果发送给接收器
# 生成并行任务端
import random
import time
import zmq
context = zmq.Context()
# 用于发送信息的套接字
sender = context.socket(zmq.PUSH)
sender.bind("tcp://*:5557")
# 用于发送批次开始消息的套接字
sink = context.socket(zmq.PUSH)
sink.connect("tcp://localhost:5558")
print("当工人准备好后点击回程Press Enter when the workers are ready: ")
_ = input()
print("开始发送任务Sending tasks to workers...")
# 第一个消息是“0”,他表示批次的开始
sink.send(b'0')
# 初始化随机数发生器
random.seed()
total_msec = 0
print('准备进入循环')
# 发送一百个任务
for task_nbr in range(100):
print('-----------------------------')
print(f'开始发送{task_nbr}')
# 从1到100毫秒的随机工作负载
workload = random.randint(1, 100)
total_msec += workload
sender.send_string(f"{workload}")
print(f"总体耗费时间为Total expected cost: {total_msec} msec")
# 给ZMQ一点时间来传递
time.sleep(1)
""""
并行任务工人
将PULL套接字连接到tcp://localhost:5557
通过上面的套接字来手机自来发生器的工作负载
将PUSH套接字连接到tcp://localhost:5558
通过5558的套接字发送结果给接收器
"""
import sys
import time
import zmq
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:
print('-----------------------------')
print('接收信息')
s = receiver.recv()
# 用于查看器的简易过程指示器
sys.stdout.write('.')
sys.stdout.flush()
# 不做工作
time.sleep(int(s) * 0.001)
# 将结果发送给接收器
sender.send_string(f'{s}')
print('发送消息')
"""
并行任务接收器
将PULL套接字绑定到 tcp://localhost:5558
通过上述套接字收集来自各个工人的结果
"""
import sys
import time
import zmq
# 准备上下文和套接字
context = zmq.Context()
receiver = context.socket(zmq.PULL)
# 接收各个工人结果的服务器
receiver.bind('tcp://*:5558')
# 等待批次的开始
s = receiver.recv()
# 启动时钟
tstart_time = time.time()
# 处理100个确认
for task_nbr in range(100):
print('-----------------------------')
print('开始接收任务')
s = receiver.recv()
print(s)
if task_nbr % 10 == 0:
sys.stdout.write(':')
else:
sys.stdout.write('.')
sys.stdout.flush()
print('准备接收下一个任务')
# 计算并报告批次的用时
tend=time.time()
print(f"Total elapsed time: {(tend-tstart_time)*1000} msec")
在我的理解里:Push和Pull都能成为服务器,绑定一个端口
书中:这段代码有一个问题,必须同步开始同批次所有工人的启动和运行,这是一个疑难杂症,并没有简单的解决方案。connect需要一定的时间,所以当一组工人连接到发生器的时候,第一个连接成功的工人会在很短的时间得到消息的整个负载,而其他的工人还在进行连接。如果不知道为什么批次开始的不同步,那么系统就将无法并行运行,可以尝试在发生器中移走等待,看看会发生什么?
负载均衡:发生器的PUSH套接字将任务均匀的分配给工人(假设批处理开始发出之前,都已连接)。这就是所谓的负载均衡
公平排队:接收器的PULL套接字均匀的接受来自工人的结果。这就是所谓的公平排队