1,ZMQ介绍
ZMQ是一套嵌入式的网络链接库,是一个基于内存的消息队列,工作起来更像是一个并发式的框架。它提供的套接字可以在多种协议中传输消息,如线程间、进程间、TCP、广播等。你可以使用套接字构建多对多的连接模式,如扇出、发布-订阅、任务分发、请求-应答等。ZMQ的快速足以胜任集群应用产品。它的异步I/O机制让你能够构建多核应用程序,完成异步消息处理任务。
套接字事实上是用于网络编程的标准接口,ZMQ之所那么吸引人眼球,原因之一就是它是建立在标准套接字API之上。因此,ZMQ的套接字操作非常容易理解,其生命周期主要包含四个部分:
创建、销毁、以及配置套接字的工作和处理一个对象差不多,但请记住ZMQ是异步的,伸缩性很强,因此在将其应用到网络结构中时,可能会需要多一些时间来理解。
在连接两个节点时,其中一个需要使用zmq_bind(),另一个则使用zmq_connect()。通常来讲,使用zmq_bind()连接的节点称之为服务端,它有着一个较为固定的网络地址;使用zmq_connect()连接的节点称为客户端,其地址不固定。我们会有这样的说法:绑定套接字至端点;连接套接字至端点。端点指的是某个广为周知网络地址。
ZMQ连接和传统的TCP连接是有区别的,主要有:
2,ZMQ消息模式
主要有三种常用模式:
req/rep(请求答复模式):主要用于远程调用及任务分配等。
pub/sub(订阅模式):主要用于数据分发。
push/pull(管道模式):主要用于多任务并行。
除此之外,还有一种模式,是因为大多数人还么有从"TCP"传统模式思想转变过来,习惯性尝试的独立成对模式(1to1).这个在后面会有介绍。
ZeroMQ内置的有效绑定对:
非正常匹配会出现意料之外的问题(未必报错,但可能数据不通路什么的,官方说法是未来可能会有统一错误提示吧),未来还会有更高层次的模式(当然也可以自己开发)。
2.1 Request-Reply请求回应模式:
客户端在请求后,服务端必须回响应。
请求回应模型。由请求端发起请求,并等待回应端回应请求。从请求端来看,一定是一对对收发配对的;请求端和回应端都可以是1:N的模型。通常把1认为是server,N认为是Client。ZMQ 可以很好的支持路由功能(实现路由功能的组件叫作 Device),把 1:N 扩展为N:M (只需要加入若干路由节点)
Server端示例代码:
import zmq
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("tcp://*:5555")
while True:
message = socket.recv_string()
print(message)
socket.send_string("OK, 200")
Client端示例代码:
import zmq, sys
context = zmq.Context()
socket = context.socket(zmq.REQ)
socket.connect("tcp://localhost:5555")
while True:
data = input("input your data:")
if data == 'q':
sys.exit()
socket.send_string(data)
response = socket.recv_string()
print(response)
从以上的过程,我们可以了解到使用 ZMQ 写基本的程序的方法,需要注意的是:
服务端和客户端无论谁先启动,效果是相同的,这点不同于 Socket。
在服务端收到信息以前,程序是阻塞的,会一直等待客户端连接上来。
服务端收到信息以后,会 send 一个“World”给客户端。值得注意的是一定是 client 连接上来以后,send 消息给 Server,然后 Server 再 rev 然后响应 client,这种一问一答式的。如果 Server 先 send,client 先 rev 是会报错的。
ZMQ 通信通信单元是消息,他除了知道 Bytes 的大小,他并不关心的消息格式。因此,你可以使用任何你觉得好用的数据格式。Xml、Protocol Buffers、Thrift、json 等等。
虽然可以使用 ZMQ 实现 HTTP 协议,但是,这绝不是他所擅长的。
2.2 Publish-Subscribe发布订阅模式:
广播所有client,没有队列缓存,断开连接数据将永远丢失。client可以进行数据过滤。
这个模型里,发布端是单向只发送数据的,且不关心是否把全部的信息都发送给订阅者。
如果发布端开始发布信息的时候,订阅端尚未连接上,这些信息直接丢弃。
不过一旦订阅端连接上来,中间会保证没有信息丢失。
同样,订阅端则只负责接收,而不能反馈。
如果发布端和订阅端需要交互(比如要确认订阅者是否已经连接上),则使用额外的socket采用请求回应模型满足这个需求。
Server端示例代码:
import zmq
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind("tcp://*:5555")
while True:
data = input("input your data:")
print(data)
socket.send_string(data)
Client端示例代码:
import zmq
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://localhost:5555")
socket.setsockopt_string(zmq.SUBSCRIBE,'')
while True:
response = socket.recv_string()
print(response)
注意:
这里的发布与订阅角色是绝对的,即发布者无法使用recv,订阅者不能使用send,并且订阅者需要设置订阅条件"setsockopt"。
按照官网的说法,在这种模式下很可能发布者刚启动时发布的数据出现丢失,原因是用zmq发送速度太快,在订阅者尚未与发布者建立联系时,已经开始了数据发布(内部局域网没这么夸张的)。官网给了两个解决方案;1,发布者sleep一会再发送数据(这个被标注成愚蠢的);2,(还没有看到那,在后续中发现的话会更新这里)。
官网还提供了一种可能出现的问题:当订阅者消费慢于发布,此时就会出现数据的堆积,而且还是在发布端的堆积,显然,这是不可以被接受的。至于解决方案,或许后面的"分而治之"就是吧。
2.3 Parallel Pipeline管道模式:
这个模型里,管道是单向的,从PUSH端单向的向PULL端单向的推送数据流。
由三部分组成,push进行数据推送,work进行数据缓存,pull进行数据竞争获取处理。区别于Publish-Subscribe存在一个数据缓存和处理负载。
当连接被断开,数据不会丢失,重连后数据继续发送到对端。
消息结构:
在每个消息buff前均会自带一个buff长度
server端代码:
import zmq
context = zmq.Context()
socket = context.socket(zmq.PULL)
socket.bind("tcp://*:5558")
while True:
data = socket.recv_string()
print(data)
worker端代码:
import zmq
context = zmq.Context()
recive = context.socket(zmq.PULL)
recive.connect('tcp://127.0.0.1:5557')
sender = context.socket(zmq.PUSH)
sender.connect('tcp://127.0.0.1:5558')
while True:
data = recive.recv_string()
print(data)
sender.send_string(data)
client端代码:
import zmq
context = zmq.Context()
socket = context.socket(zmq.PUSH)
socket.bind('tcp://*:5557')
while True:
data = input("input your data:")
socket.send_string(data)
3,项目实践
3.1 locust源码rpc模块探索
公用模块locust_rpc.py代码:
import msgpack
import zmq.green as zmq
class Message(object):
def __init__(self, message_type, data, node_id):
self.type = message_type
self.data = data
self.node_id = node_id
def serialize(self):
return msgpack.dumps((self.type, self.data, self.node_id))
@classmethod
def unserialize(cls, data):
msg = cls(*msgpack.loads(data, encoding='utf-8'))
return msg
class BaseSocket(object):
def send(self, msg):
self.sender.send(msg.serialize())
def recv(self):
data = self.receiver.recv()
return Message.unserialize(data)
class Server(BaseSocket):
def __init__(self, host, port):
context = zmq.Context()
self.receiver = context.socket(zmq.PULL)
self.receiver.bind("tcp://%s:%i" % (host, port))
self.sender = context.socket(zmq.PUSH)
self.sender.bind("tcp://%s:%i" % (host, port + 1))
class Client(BaseSocket):
def __init__(self, host, port):
context = zmq.Context()
self.receiver = context.socket(zmq.PULL)
self.receiver.connect("tcp://%s:%i" % (host, port + 1))
self.sender = context.socket(zmq.PUSH)
self.sender.connect("tcp://%s:%i" % (host, port))
rpc_server代码:
import time
from locust_rpc import Server, Message
host = '127.0.0.1'
port = 5555
data = {'num_clients': 5, 'hatch_rate': 0.2}
server = Server(host=host, port=port)
server.send(Message('hatch', data, None))
data_list = [
{"hatch": {'num_clients': 1, 'hatch_rate': 0.2}},
{"start": {'num_clients': 1, 'hatch_rate': 0.2}},
{"stop": {'num_clients': 1, 'hatch_rate': 0.2}}
]
for data in data_list:
for k, v in data.items():
server.send(Message(k, v, None))
time.sleep(1)
rpc_client代码:
from locust_rpc import Client
host = '127.0.0.1'
port = 5555
client = Client(host=host, port=port)
while True:
data = client.recv()
msg_type = data.type
msg_data = data.data
if msg_type == "hatch":
print("hatching: {}".format(msg_data))
else:
print("hatch type {} : {}".format(msg_type, msg_data))
参考文献:
https://blog.csdn.net/woaizard100/article/details/80910376
https://www.cnblogs.com/binchen-china/p/5643531.html
https://blog.csdn.net/suhheng/article/category/902641/2?