pyzmq说明
PyZMQ 表面上是 ØMQ 的 Python 绑定,但该项目遵循 Python 的“包含电池”理念,
不仅提供用于调用 ØMQ C++ 库的 Python 方法和对象。
绑定说明
PyZMQ 目前分为四个子模块,第一, Core. zmq.core 只包含绑定ZeroMQ 的基础功能。核心模块被拆分,这样每个基本的 ZeroMQ对象(或函数,如果没有关联对象)是一个单独的模块,例如 zmq.core.context 包含上下文对象,zmq.core.poll 包含一个 Poller 对象,以及 select() 函数等。zmq.core.constants 保存了 方便使用的ZMQ 常量。
将内核分解为子模块有两个原因:第一 重新编译和修改项目。为了避免对于一个单独的项目,做一个小的修改,还需要进行重新编译所有的文件内容,第二个原因与Cython有关。PyZMQ是用Cython编写的,Cython是一个可以有效地实现的工具为Python编写C扩展。通过将我们的对象分离到单独的pyx文件中,每个文件都有自己的声明在pxd标头中,其他项目可以在Cython中编写扩展,并在C级直接调用ZeroMQ,而无需付出调用的Python的对象的代价。
线程安全
在 ØMQ 中,上下文是线程安全的对象,但套接字不是。 使用单个上下文是安全的(例如,通过 zmq.Context.instance()) 在您的整个多线程应用程序中,但您应该在每个线程的基础上创建套接字。 如果你跨线程共享套接字,除非您使用明智地应用 threading.Lock,但不推荐这种方式。
套接字选项属性
在 0MQ 中,使用 set/getsockopt() 方法设置/检索套接字选项。 在pyzmq中使用基于类的方法,通过简单的属性访问执行这些操作, (pyzmq 2.1.9 添加该功能)
。 只需使用 sockopt 的(不区分大小写的)名称分配或请求 Socket 属性:
s = ctx.socket(zmq.DEALER)
s.identity = b"dealer"
s.hwm = 10
s.events
# 0
s.fd
# 16
上下文选项
就像在 Sockets 上将套接字选项设置为属性一样,也可以在 Contexts 上执行相同的操作。 这会影响后面创建的任何新套接字选项的默认值。
ctx = zmq.Context()
ctx.linger = 0
rep = ctx.socket(zmq.REP)
req = ctx.socket(zmq.REQ)
不适用于套接字的选项(例如,非 SUB 套接字上的 SUBSCRIBE)将被忽略。
libzmq 常量
libzmq 常量现在可用作 Python 枚举,从而被套接字选项容易使用。
上下文管理器
使用上下文
import zmq
with zmq.Context() as ctx:
with ctx.socket(zmq.PUSH) as s:
s.connect(url)
s.send_multipart([b"message"])
# exiting Socket context closes socket
# exiting Context context terminates context
此外,每个绑定/连接调用都可以用作上下文:
with socket.connect(url):
s.send_multipart([b"message"])
# exiting connect context calls socket.disconnect(url)
核心扩展
以两种出现在核心绑定中的方式扩展了核心功能,并且不是通用的 ØMQ 特性。
- 内置序列化
首先,我们在 Socket 类中添加了使用内置 json 和 pickle 作为流方法的通用序列化。 一个socket 有 send_json() 和 send_pyobj() 方法,它们对应于通过网络发送一个对象之后分别用 json 和 pickle 序列化,通过这些方法发送的任何对象都可以用
recv_json() 和 recv_pyobj() 方法。 Unicode 字符串是其他不可明确发送的对象
通过网络,因此我们包含了 send_string() 和 recv_string(),它们只是在对消息进行编码后发送字节(“utf-8”是默认值)。
- 消息跟踪器
基本 ØMQ 功能的第二个扩展是 MessageTracker。 MessageTracker 是一个用于
跟踪底层 ZeroMQ 何时使用消息缓冲区完成。 Python 中 ØMQ 的主要用例之一是执行非复制发送的能力。 由于 Python 的缓冲区接口,许多对象(包括 NumPy 数组)提供缓冲区接口,因此可以直接发送。 但是,与任何异步非复制消息传递一样像ØMQ或MPI这样的系统,知道消息何时实际发送可能很重要,因此再次安全编辑缓冲区而不必担心损坏消息。 这就是 MessageTracker 的用途。
MessageTracker 是一个简单的对象,但其使用会受到惩罚。因为就其本质而言,MessageTracker必须涉及线程安全通信(特别是内置队列对象),实例化 MessageTracker 需要适度的时间(10μs),因此在实例化许多小消息的情况下,这实际上可以占主要的性能消耗。因此,通过跟踪标志进行跟踪是可选的,该标志是可选传递的,始终默认为False,在 Frame 对象(用于包装消息片段的 pyzmq 对象)所在的三个位置中的每一个实例化:Frame 构造函数和非复制发送和接收。
MessageTracker 非常简单,只有一个方法和一个属性。 属性 MessageTracker.done
当被跟踪的 Frame(s) 不再被 ØMQ 使用时,将为 True,并且 MessageTracker.wait() 将阻塞,等待释放帧。
- 一个 Frame 实例化的时候没有设置跟踪, 就无法跟踪该Frame 。如果一个frame 需要被跟踪,它必须用track=True来实例化。
其他扩展
到目前为止,PyZMQ包括核心ØMQ的四个扩展,我们发现这些扩展足够基本,可以包含在PyZMQ本身中:
- zmq.log:用于将 Python 日志连接到网络的日志处理程序
- zmq.devices:用于在后台运行设备的自定义设备和对象
- zmq.eventloop :Tornado 事件循环,适用于ØMQ 套接字
- zmq.ssh :通过ssh 建立zeromq 连接的简单工具
PyZMQ的序列化消息
- 通过网络发送消息时,通常需要将数据封送为字节。
内置的序列化
PyZMQ 主要是 libzmq 的绑定,但为了方便起见,我们确实提供了三种内置序列化方法,以帮助Python开发人员学习libzmq。Python有两个主要包用于序列化对象:json和pickle,所以我们为发送和接收使用这些模块序列化的对象提供了简单的方便方法。套接字具有方法send_json() 和 send_pyobj(),它们对应于序列化后通过网络发送对象分别使用json和pickle,通过这些方法发送的任何对象都可以使用recv_json() 进行重建和recv_pyobj() 方法。这些方法是为了方便而设计的,而不是为了性能,所以开发人员想要强调性能应使用自己的序列化 send/recv 方法。
使用自定义的序列化
通常,您将希望提供自己的序列化,该序列化针对您的应用程序或库的可用性进行了优化。这可能包括使用您自己喜欢的序列化 (1,2),或在标准库中添加压缩 via3,或
超级 fast4 库。
有两个简单的模型可以实现你自己的序列化:编写一个函数,将套接字作为参数或子类 Socket 用于您自己的应用程序。
例如,通常可以通过压缩数据来大幅减小pickle的大小。以下将发送压缩pickle:
import pickle
import zlib
def send_zipped_pickle(socket, obj, flags=0, protocol=pickle.HIGHEST_PROTOCOL):
"""pickle an object, and zip the pickle before sending it"""
p = pickle.dumps(obj, protocol)
z = zlib.compress(p)
return socket.send(z, flags=flags)
def recv_zipped_pickle(socket, flags=0):
"""inverse of send_zipped_pickle"""
z = socket.recv(flags)
p = zlib.decompress(z)
return pickle.loads(p)
Python中常见的数据结构是numpy数组。PyZMQ 支持发送 numpy 数组,而无需复制任何数组数据,因为它们提供 Python 缓冲区接口。然而,仅仅缓冲区的信息是不够来重建接收端的数据。下面是一个 send/recv 示例,它允许 numpy 数组的非复制 send/recvs包括重建数组所需的 dtype/shape 数据
import numpy
def send_array(socket, A, flags=0, copy=True, track=False):
"""send a numpy array with metadata"""
md = dict(
dtype=str(A.dtype),
shape=A.shape,
)
socket.send_json(md, flags | zmq.SNDMORE)
return socket.send(A, flags, copy=copy, track=track)
def recv_array(socket, flags=0, copy=True, track=False):
"""recv a numpy array"""
md = socket.recv_json(flags=flags)
msg = socket.recv(flags=flags, copy=copy, track=track)
buf = memoryview(msg)
A = numpy.frombuffer(buf, dtype=md["dtype"])
return A.reshape(md["shape"])
pyzmq的设备 Devices
ØMQ有一个设备的概念,即管理连接两个或多个套接字的发送-接收模式的简单程序。作为完整程序,设备包含一个while(True)循环,因此一旦调用,就会永久阻止执行。我们在设备子模块中提供了一些在后台运行这些设备的工具,以及一个支持自定义三个套接字的 MonitoredQueue 设备。
BackgroundDevices
在python程序中,很少有人在主线程通过device() 创建zmq设备,因为这样的调用将永远阻塞执行,最适用 设备模型实在后台线程或者进程中。我们提供了用于在后台线程中启动设备ThreadDevice 的类和进行多进程处理的ProcessDevice类。对于线程安全和跨进程运行,这些方法不采取套接字对象作为参数,而是套接字类型,然后套接字的创建和配置通过BackgroundDevice 的 foo_in() 代理方法。对于每个配置方法(bind/connect/setsockopt),都有用于在后台线程或进程中创建的 Socket 对象上调用这些方法的代理方法,前缀为与“in_”或“out_”对应in_socket和out_socket:
from zmq.devices import ProcessDevice
pd = ProcessDevice(zmq.QUEUE, zmq.ROUTER, zmq.DEALER)
pd.bind_in(’tcp://*:12345’)
pd.connect_out(’tcp://127.0.0.1:12543’)
pd.setsockopt_in(zmq.IDENTITY, ’ROUTER’)
pd.setsockopt_out(zmq.IDENTITY, ’DEALER’)
pd.start()
# it will now be running in a background process
MonitoredQueue
MonitoredQueue是ØMQ的内置设备队列,是一个对称双套接字设备,支持通过任何模式在任一方向传递消息。QUEUE 的逻辑扩展 与 输入/输出套接字相同的功能,但可以通过第三个监视器套接字向任一方向发送每条消息。为了性能原因,这个monitored_queue()函数是用Cython写的,因此循环不涉及Python,并且应该与基本 QUEUE 设备有相同的性能。
队列设备的一个缺点是它不支持将ROUTER套接字作为输入和输出。这是因为ROUTER 套接字在接收到消息时,会预先在发送消息的套接字上追加 IDENTITY(用于路由回复)。结果是输出套接字将始终尝试将传入消息路由回原始发件人,这种模式通常不符合现实使用。为了让队列支持 ROUTER-ROUTER 连接时,它必须交换消息的前两部分,以便从另一端获得正确的消息。
调用受监视队列类似于调用常规 ØMQ 设备:
from zmq.devices import monitored_queue
ins = ctx.socket(zmq.ROUTER)
outs = ctx.socket(zmq.DEALER)
mons = ctx.socket(zmq.PUB)
configure_sockets(ins,outs,mons)
monitored_queue(ins, outs, mons, in_prefix=’in’, out_prefix=’out’)
in_prefix 和 out_prefix 默认分别为 ‘in’ 和 ‘out’,PUB 套接字适合监视器套接字,因为它永远不会接收消息,而 in/out 前缀非常适合 PUB/SUB 主题订阅 模型。 在 mons上发送的所有消息都是多部分的,消息的第一部分是接收消息的套接字的前缀。
在后台启动一个MQ,有ThreadMonitoredQueue和ProcessMonitoredQueue,它们功能与基本 BackgroundDevice 对象类似,但添加 foo_mon() 方法来配置监视器套接字。
事件循环和 PyZMQ
从 pyzmq 17 开始,将 pyzmq 与 eventloops 集成可以在没有任何预配置的情况下工作。由于使用了边缘触发的文件描述符,这已知存在问题,因此请报告事件循环集成问题。
AsyncIO
PyZMQ 15 通过 zmq.asyncio 添加了对 asyncio 的支持,其中包含一个在 asyncio 协程中使用, 由 asyncio.Future 返回的Socket 子类的对象。 要使用此 API,请导入 zmq.asyncio.Context。 由此创建的套接字Context 将从任何可能的阻塞方法返回 Futures。
import asyncio
import zmq
from zmq.asyncio import Context
ctx = Context.instance()
async def recv():
s = ctx.socket(zmq.SUB)
s.connect("tcp://127.0.0.1:5555")
s.subscribe(b"")
while True:
msg = await s.recv_multipart()
print("received", msg)
s.close()
- 注意:在 PyZMQ < 17 中,在启动任何异步代码之前需要一个额外的步骤来注册 zmq 轮询器:
import zmq.asyncio
zmq.asyncio.install()
ctx = zmq.asyncio.Context()
Tornado IOLoop
Tornado 包含一个事件循环,用于处理文件描述符和本机套接字上的轮询事件。 我们已经包括了一个小Tornado 的一部分(特别是它的 ioloop),并将其 IOStream 类改编为 ZMQStream 以处理轮询事件在 ØMQ 套接字上。 ZMQStream 对象的工作方式很像 Socket 对象,但不是直接调用 recv(),而是使用 on_recv() 注册回调。 还可以使用 on_send() 为发送事件注册回调。
- 注意:对于最新的 Python (3.6) 和 tornado (5),没有理由使用 zmq.eventloop.future 来代替更严格兼容的 zmq.asyncio。
PyZMQ 15 添加了 zmq.eventloop.future,包含一个 Socket 子类,该子类返回 Future 对象以供在Tornado协程。 要使用此 API,请导入 zmq.eventloop.future.Context。 此上下文创建的套接字将从任何可能的阻塞方法返回 Futures。
from tornado import gen, ioloop
import zmq
from zmq.eventloop.future import Context
ctx = Context.instance()
@gen.coroutine
def recv():
s = ctx.socket(zmq.SUB)
s.connect("tcp://127.0.0.1:5555")
s.subscribe(b"")
while True:
msg = yield s.recv_multipart()
print("received", msg)
s.close()
ZMQStream
ZMQStream 对象允许注册回调方法,用于在消息到达时对其进行处理,可以与Tornado事件循环一起使用。
-
send()
ZMQStream 对象确实有 send() 和 send_multipart() 方法,使用方法与 Socket.send()相同。但 IOLoop 不会立即发送,而是等待套接字能够发送(例如,如果满足 HWM,或 REQ/REP 模式禁止在某个点发送)。通过 send 发送的消息将通过on_send()注册的回调发送。 -
on_recv()
ZMQStream.on_recv() 是ZMQStream 经常使用的方法。 它注册一个回调来处理接收的消息,接受的消息是多部分的,即使它的长度为 1。可以轻松地使用它来构建类似echo socket的东西:
s = ctx.socket(zmq.REP)
s.bind("tcp://localhost:12345")
stream = ZMQStream(s)
def echo(msg):
stream.send_multipart(msg)
stream.on_recv(echo)
ioloop.IOLoop.instance().start()
on_recv 有一个 copy 标志,和 Socket.recv() 一样。 如果 copy=False,则使用 on_recv 注册回调将接收跟踪 Frame 对象而不是字节。
*注意:在底层套接字上接收任何数据之前,必须使用ZMQStream.on_recv() 或ZMQStream.on_recv_stream() 注册回调。 这允许通过将两个回调设置为 None 来临时暂停套接字上的处理。 稍后可以通过恢复任一回调来恢复处理
- on_recv_stream()
ZMQStream.on_recv_stream() 就像上面的 on_recv一样,但回调将同时传递消息和流,而不仅仅是消息。这是为了更容易在单个回调中使用多个流。
s1 = ctx.socket(zmq.REP)
s1.bind("tcp://localhost:12345")
stream1 = ZMQStream(s1)
s2 = ctx.socket(zmq.REP)
s2.bind("tcp://localhost:54321")
stream2 = ZMQStream(s2)
def echo(stream, msg):
stream.send_multipart(msg)
stream1.on_recv_stream(echo)
stream2.on_recv_stream(echo)
ioloop.IOLoop.instance().start()
-
flush()
使用事件循环,可以在循环的单个迭代中准备好多个事件。flush() 方法允许开发人员从队列中提取消息,以强制执行对事件循环排序的某种优先级。齐平拉力队列中的任何挂起事件。您可以指定仅刷新 recv 事件、仅发送事件或任何事件,并且您可以指定要刷新的事件数的限制,以防止饥饿。 -
install()
注意:如果你使用的是 pyzmq < 17,还有一个额外的步骤告诉 tornado 使用 zmq poller 而不是它的默认值。
pyzmq 17 不再需要 ioloop.install()。
使用 PyZMQ 的 ioloop,您可以在任何 tornado 应用程序中使用 zmq 套接字。 你可以告诉 tornado 使用 zmq 的 poller通过调用 ioloop.install() 函数:
from zmq.eventloop import ioloop
ioloop.install()
你也可以通过从 pyzmq 请求全局实例来做同样的事情:
from zmq.eventloop.ioloop import IOLoop
loop = IOLoop.current()
这将配置 tornado 的 tornado.ioloop.IOLoop 以使用 zmq 的轮询器,并注册当前实例。install() 或检索 zmq 实例必须在全局 * 实例注册之前完成,否则将成为冲突。
可以在不注册为全局实例的情况下将 PyZMQ 套接字与 tornado 一起使用,但不太方便。首先,您必须指示 tornado IOLoop 使用 zmq 轮询器:
from zmq.eventloop.ioloop import ZMQIOLoop
loop = ZMQIOLoop()
然后,当您实例化 tornado 和 ZMQStream 对象时,您必须传递 io_loop 参数以确保它们使用这个循环,而不是全局实例。这对于编写测试特别有用,例如:
from tornado.testing import AsyncTestCase
from zmq.eventloop.ioloop import ZMQIOLoop
from zmq.eventloop.zmqstream import ZMQStream
class TestZMQBridge(AsyncTestCase):
# Use a ZMQ-compatible I/O loop so that we can use `ZMQStream`.
def get_new_ioloop(self):
return ZMQIOLoop()
您还可以手动将此 IOLoop 安装为全局tornado实例,使用:
from zmq.eventloop.ioloop import ZMQIOLoop
loop = ZMQIOLoop()
loop.install()
PyZMQ and gevent
PyZMQ 2.2.0.1 附带一个与 gevent 兼容的 API,即 zmq.green。 要使用它,只需:
import zmq.green as zmq
然后像往常一样编写代码。
Socket.send/recv 和 zmq.Poller 是 gevent-aware.
在 PyZMQ 2.2.0.2 中,green.device 和 green.eventloop 也应该是 gevent-friendly 的一样
注意:green device 不会释放 GIL,这与 zmq.core 中的真实设备不同。
zmq.green.eventloop 包含最小修补的 IOLoop/ZMQStream 以便使用启用 gevent 的轮询器,因此应该能够在 gevent 应用程序中使用 ZMQStream 接口,但是不推荐同时使用两个事件循环 (tornado + gevent)
警告:gevent 1.0 或 libevent 中存在一个已知问题,这可能会导致 zeromq 套接字事件丢失。PyZMQ 通过添加超时来解决这个问题,因此它不会永远等待 gevent 通知事件。 唯一的对此的已知解决方案是使用当前为 1.0b3 的 gevent 1.0