Python高级用法之消息队列zmq

1,ZMQ介绍

ZMQ是一套嵌入式的网络链接库,是一个基于内存的消息队列,工作起来更像是一个并发式的框架。它提供的套接字可以在多种协议中传输消息,如线程间、进程间、TCP、广播等。你可以使用套接字构建多对多的连接模式,如扇出、发布-订阅、任务分发、请求-应答等。ZMQ的快速足以胜任集群应用产品。它的异步I/O机制让你能够构建多核应用程序,完成异步消息处理任务。

套接字事实上是用于网络编程的标准接口,ZMQ之所那么吸引人眼球,原因之一就是它是建立在标准套接字API之上。因此,ZMQ的套接字操作非常容易理解,其生命周期主要包含四个部分:

  • 创建和销毁套接字:zmq_socket(), zmq_close()
  • 配置和读取套接字选项:zmq_setsockopt(), zmq_getsockopt()
  • 为套接字建立连接:zmq_bind(), zmq_connect()
  • 发送和接收消息:zmq_send(), zmq_recv()

创建、销毁、以及配置套接字的工作和处理一个对象差不多,但请记住ZMQ是异步的,伸缩性很强,因此在将其应用到网络结构中时,可能会需要多一些时间来理解。

在连接两个节点时,其中一个需要使用zmq_bind(),另一个则使用zmq_connect()。通常来讲,使用zmq_bind()连接的节点称之为服务端,它有着一个较为固定的网络地址;使用zmq_connect()连接的节点称为客户端,其地址不固定。我们会有这样的说法:绑定套接字至端点;连接套接字至端点。端点指的是某个广为周知网络地址。

ZMQ连接和传统的TCP连接是有区别的,主要有:

  • 使用多种协议,inproc(进程内)、ipc(进程间)、tcp、pgm(广播)、epgm;
  • 当客户端使用zmq_connect()时连接就已经建立了,并不要求该端点已有某个服务使用zmq_bind()进行了绑定;
  • 连接是异步的,并由一组消息队列做缓冲;
  • 连接会表现出某种消息模式,这是由创建连接的套接字类型决定的;
  • 一个套接字可以有多个输入和输出连接;
  • ZMQ没有提供类似zmq_accept()的函数,因为当套接字绑定至端点时它就自动开始接受连接了;
  • 应用程序无法直接和这些连接打交道,因为它们是被封装在ZMQ底层的。

 

2,ZMQ消息模式

主要有三种常用模式: 
req/rep(请求答复模式):主要用于远程调用及任务分配等。 
pub/sub(订阅模式):主要用于数据分发。 
push/pull(管道模式):主要用于多任务并行。 
除此之外,还有一种模式,是因为大多数人还么有从"TCP"传统模式思想转变过来,习惯性尝试的独立成对模式(1to1).这个在后面会有介绍。 

ZeroMQ内置的有效绑定对: 

  • PUB and SUB
  • REQ and REP
  • REQ and XREP
  • XREQ and REP
  • XREQ and XREP
  • XREQ and XREQ
  • XREP and XREP
  • PUSH and PULL
  • PAIR and PAIR

非正常匹配会出现意料之外的问题(未必报错,但可能数据不通路什么的,官方说法是未来可能会有统一错误提示吧),未来还会有更高层次的模式(当然也可以自己开发)。 

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?

 

你可能感兴趣的:(Python)