You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用ZeroMQ时,如何等待SUB套接字订阅在PUB端完成注册?

问题描述

我实现了一个消息总线,功能是把PULL套接字收到的消息转发到PUB套接字,作为系统内其他组件的通信媒介——这些组件用PUSH发消息、SUB收消息。总线的主要通信在守护进程之间,但也存在临时程序连接总线后立即发请求、等回复就断开的场景。

问题在于SUB套接字的订阅是非阻塞操作,可能订阅还没在消息总线的PUB端完成注册,回复就已经发出去了。我写了个示例:启动20个ephemeral_program临时程序,它们连接总线、发消息并期望收到自己的消息,但部分能收到,部分收不到。

目前有两种不太理想的解决办法:

  • 发消息前短暂休眠,这种方法不可靠
  • 循环发消息直到收到回复来检查订阅状态,本质是轮询,不是发布者主动的就绪确认

想知道有没有其他套接字选项或一对多的套接字组合,能确保订阅者侧确认发布者已完成订阅注册?

import time
from multiprocessing import Process

from zmq import Context, Poller, Socket
from zmq.constants import POLLIN, PUB, PULL, PUSH, SUB


def message_bus():
    context = Context.instance()
    in_socket: Socket = context.socket(PULL)
    in_socket.bind("ipc:///tmp/in-socket.ipc")
    out_socket: Socket = context.socket(PUB)
    out_socket.bind("ipc:///tmp/out-socket.ipc")

    while True:
        msg = in_socket.recv_string()
        out_socket.send_string(msg)


# 生成测试流量的守护进程
def daemon():
    context = Context.instance()
    receive_socket: Socket = context.socket(SUB)
    receive_socket.connect("ipc:///tmp/out-socket.ipc")
    receive_socket.subscribe("")
    send_socket: Socket = context.socket(PUSH)
    send_socket.connect("ipc:///tmp/in-socket.ipc")

    poller = Poller()
    poller.register(receive_socket, POLLIN)

    while True:
        polling_result = poller.poll(timeout=1)

        if polling_result:
            receive_socket.recv_string()
        else:
            send_socket.send_string("Hi!")


def ephemeral_program(i):
    context = Context.instance()

    send_socket: Socket = context.socket(PUSH)
    send_socket.connect("ipc:///tmp/in-socket.ipc")

    receive_socket: Socket = context.socket(SUB)
    receive_socket.connect("ipc:///tmp/out-socket.ipc")
    receive_socket.subscribe(f"ephemeral_program_{i}")

    poller = Poller()
    poller.register(receive_socket, POLLIN)

    # time.sleep(1)
    # 这种方法大多时候能给足够时间让订阅在PUB端完成注册,但不可靠

    # 相对好一点的办法是循环发送消息确认订阅
    # while True:
    #    send_socket.send_string(f"ephemeral_program_{i}:am I subscribed?")
    #    polling_result = poller.poll(timeout=1)
    #
    #    if polling_result:
    #        msg = receive_socket.recv_string()
    #        if msg == f"ephemeral_program_{i}:am I subscribed?":
    #            print("Subscription confirmed")
    #            break
    #    else:
    #        print("Not yet subscribed")

    # 发送实际请求
    send_socket.send_string(f"ephemeral_program_{i}:Hello")
    polling_result = poller.poll(timeout=100)

    if polling_result:
        print(receive_socket.recv_string())
    else:
        print("Did not receive message")


message_bus_p = Process(target=message_bus, args=())
message_bus_p.start()

n = 20
daemons_p = [Process(target=daemon, args=()) for _ in range(n)]
for daemon_p in daemons_p:
    daemon_p.start()

# 给消息总线和守护进程启动时间,模拟已运行一段时间的状态
time.sleep(0.2)

ephemeral_programs_p = [
    Process(target=ephemeral_program, args=(i,)) for i in range(n)
]
for ephemeral_program_p in ephemeral_programs_p:
    ephemeral_program_p.start()
for ephemeral_program_p in ephemeral_programs_p:
    ephemeral_program_p.join()

for daemon_p in daemons_p:
    daemon_p.kill()

message_bus_p.kill()
可行解决方案

1. 使用XPUB/XSUB替代PUB/SUB

ZeroMQ的XPUB套接字会主动发送订阅/取消订阅事件给连接的客户端。你可以把消息总线的PUB换成XPUB,然后在总线里监听这些事件:

  • 当XPUB收到某个临时程序的订阅事件时,总线可以通过一个单独的REQ/REP通道通知该程序订阅已完成注册
  • 临时程序连接总线时,先通过REQ套接字等待订阅确认,确认后再发送实际消息

修改后的消息总线核心逻辑:

def message_bus():
    context = Context.instance()
    # 原有PULL接收消息
    in_socket = context.socket(PULL)
    in_socket.bind("ipc:///tmp/in-socket.ipc")
    # 替换为XPUB
    out_socket = context.socket(XPUB)
    out_socket.bind("ipc:///tmp/out-socket.ipc")
    # 新增REQ/REP用于订阅确认
    confirm_socket = context.socket(REP)
    confirm_socket.bind("ipc:///tmp/confirm-socket.ipc")

    poller = Poller()
    poller.register(in_socket, POLLIN)
    poller.register(out_socket, POLLIN)
    poller.register(confirm_socket, POLLIN)

    subscribed_clients = set()

    while True:
        socks = dict(poller.poll())
        if in_socket in socks:
            msg = in_socket.recv_string()
            out_socket.send_string(msg)
        if out_socket in socks:
            # XPUB收到的订阅事件:第一个字节是0(取消)或1(订阅),后面是订阅主题
            event, topic = out_socket.recv_multipart()
            if event == b'\x01':
                subscribed_clients.add(topic.decode())
        if confirm_socket in socks:
            client_topic = confirm_socket.recv_string()
            if client_topic in subscribed_clients:
                confirm_socket.send_string("READY")
            else:
                confirm_socket.send_string("WAIT")

临时程序修改:

def ephemeral_program(i):
    context = Context.instance()
    topic = f"ephemeral_program_{i}"

    # 订阅确认用REQ
    confirm_socket = context.socket(REQ)
    confirm_socket.connect("ipc:///tmp/confirm-socket.ipc")

    # 原有PUSH发消息、SUB收消息
    send_socket = context.socket(PUSH)
    send_socket.connect("ipc:///tmp/in-socket.ipc")
    receive_socket = context.socket(SUB)
    receive_socket.connect("ipc:///tmp/out-socket.ipc")
    receive_socket.subscribe(topic)

    # 等待订阅确认
    while True:
        confirm_socket.send_string(topic)
        reply = confirm_socket.recv_string()
        if reply == "READY":
            break
        time.sleep(0.01)

    # 发送实际消息
    send_socket.send_string(f"{topic}:Hello")
    poller = Poller()
    poller.register(receive_socket, POLLIN)
    polling_result = poller.poll(timeout=100)
    if polling_result:
        print(receive_socket.recv_string())
    else:
        print("Did not receive message")

2. 给临时程序分配专属REQ/REP通道

如果临时程序的请求-响应模式是一对一的,可以直接在消息总线中新增REQ/REP套接字,临时程序用REQ发送请求,总线用REP接收后处理并回复,这样天然保证了消息的可靠传递,不需要依赖SUB/PUB的订阅确认。但这种方式不适用于一对多的广播场景。

3. 使用ZMQ_SUBSCRIBE_NOTIFY选项(部分版本支持)

部分ZeroMQ版本支持ZMQ_SUBSCRIBE_NOTIFY套接字选项,当订阅在PUB端完成注册时,会给SUB套接字发送一个通知事件。你可以在SUB套接字上设置这个选项,然后通过Poller监听该事件,收到通知后再发送消息。

示例代码片段:

receive_socket = context.socket(SUB)
receive_socket.connect("ipc:///tmp/out-socket.ipc")
receive_socket.subscribe(topic)
# 设置订阅通知选项
receive_socket.setsockopt(zmq.SUBSCRIBE_NOTIFY, 1)

poller = Poller()
poller.register(receive_socket, POLLIN | zmq.POLLSUB)

# 等待订阅通知
while True:
    socks = dict(poller.poll())
    if receive_socket in socks and socks[receive_socket] & zmq.POLLSUB:
        # 订阅已完成注册
        break

注意:这个选项不是所有版本都支持,需要确认你的ZeroMQ版本是否包含该特性。

内容的提问来源于stack exchange,提问作者Afkaaja

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 09:55:40