为什么ZeroMQ PUB会在没有连接订阅者的情况下对消息进行排队? (好吧,“断开连接”SUB-s)

时间:2017-01-17 14:57:04

标签: zeromq

我使用 ZMQ_PUB 看到一种奇怪的行为。

我有一个生产者.connect() - s到不同的流程,.bind()ZMQ_SUB套接字上。

订阅者.bind(),发布商.connect() - s。

当一个生产者启动时,它会创建一个ZMQ_PUB套接字并将.connect()创建到不同的进程。然后它会立即开始定期发送消息。

正如预期的那样,如果没有连接的用户,它将丢弃所有消息,直到用户启动。

当用户启动时,流程正常,然后从该时刻开始接收消息。

现在,问题是:

  1. 我断开订阅者的连接(停止进程)。
  2. 此时没有活跃的订阅者,因为我停止了唯一的订阅者。制作人继续发送消息,应该删除,因为没有连接的用户了......
  3. 我重新启动原始订阅者,它绑定,发布者重新连接...并且订阅者接收在此期间生成的所有消息!!
  4. 所以我看到的是,当订阅者关闭时,生产者将所有消息排入队列。一旦套接字重新连接,由于订户进程重新启动,它就会发送所有排队的消息。

    据我所知here,发布商应在没有关联订阅者时删除所有已发送的消息:

      

    ZeroMQ examples

      “发布商没有关联的订阅者,那么它只会丢弃所有消息。”

    为什么会这样?

    顺便说一句,我在Linux上使用C ++来进行这些测试。

    我尝试在绑定时在订阅者上设置不同的身份,但它不起作用。发布者仍会将消息排入队列,并在用户重新启动时将其全部传递。

    提前致谢,

    路易斯

    更新

      

    重要更新!!!!!
    在发布此问题之前,我尝试了不同的解决方案。一个是将 ZMQ_LINGER 设置为0,这不起作用。
    我添加了 ZMQ:IMMEDIATE ,但它确实有效,但我刚发现仅ZMQ:IMMEDIATE不起作用。它还需要 ZMQ_LINGER
    Luis Rojas 3 hours ago

    更新 根据要求,我将添加一些简单的测试用例来表明我的观点。 一个是简单订阅者,它在命令行上运行并接收uri绑定的位置,例如:

    $ ./sub tcp://127.0.0.1:50001

    另一个是发布者,它接收要连接的uris列表,例如:

    ./ pub tcp://127.0.0.1:50001 tcp://127.0.0.1:50002

    订阅者最多接收5条消息,然后关闭套接字并退出。我们可以在wireshark上看到FIN / ACK的交换,两种方式,以及套接字如何移动到TIME_WAIT状态。然后,发布者开始发送SYN,尝试重新连接(探测ZMQ_PUB知道该连接已关闭)

    我明确地没有取消订阅套接字,只是关闭它。在我看来,如果套接字关闭,发布者应该自动结束该连接的任何订阅。

    所以我看到的是:我启动订阅者(一个或多个),我启动发布者,开始发送消息。订阅者收到5条消息并结束。在此期间,发布者继续发送消息,没有连接的订阅者。我重新启动订阅者,并立即收到几条消息,因为它们在发布者端排队。我认为这些排队的消息会破坏发布/订阅模式,其中消息应仅传递给已连接的订阅者。如果susbcriber关闭了连接,则应删除该订户的消息。更重要的是,当用户重新启动时,它可能决定订阅其他消息,但它仍将接收那些在同一端口绑定的“之前的版本”订阅的消息。

    我的建议是ZMQ_PUB(在连接模式下),当检测到套接字断开时,应该清除该套接字上的所有订阅,直到它重新连接并且新订户决定重新订阅。

    我为语言错误道歉,但英语不是我的母语。

    Pub的代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <libgen.h>
    #include <unistd.h>
    
    #include <string>
    #include <zeromq/zmq.hpp>
    
    int main( int argc, char *argv[] )
    {
        if ( argc < 2 )
        {
            fprintf( stderr, "Usage : %s <remoteUri1> [remoteUri2...]\n",   
            basename( argv[0] ) );
            exit ( EXIT_FAILURE );
        }
    
        std::string pLocalUri( argv[1] );
        zmq::context_t localContext( 1 );
        zmq::socket_t *pSocket = new zmq::socket_t( localContext, ZMQ_PUB );
        if ( NULL == pSocket )
        {
            fprintf( stderr, "Couldn't create socket. Aborting...\n" );
            exit ( EXIT_FAILURE );
        }
    
        int i;
        try
        {
            for ( i = 1; i < argc; i++ )
            {
                printf( "Connecting to [%s]\n", argv[i] );
                {
                    pSocket->connect( argv[i] );
                }
            }
        }
        catch( ... )
        {
            fprintf( stderr, "Couldn't connect socket to %s. Aborting...\n", argv[i] );
            exit ( EXIT_FAILURE );
        }
    
        printf( "Publisher Up and running... sending messages\n" );
        fflush(NULL);
    
        int msgCounter = 0;
        do
        {
            try
            {
                char msgBuffer[1024];
                sprintf( msgBuffer, "Message #%d", msgCounter++ );
                zmq::message_t outTask( msgBuffer, strlen( msgBuffer ) + 1 );
                printf("Sending message [%s]\n", msgBuffer );
                pSocket->send ( outTask );
                sleep( 1 );
            }
            catch( ... )
            {
                fprintf( stderr, "Some unknown error ocurred. Aborting...\n" );
                exit ( EXIT_FAILURE );
            }
        }
        while ( true );
    
        exit ( EXIT_SUCCESS );
    }
    

    子代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <libgen.h>
    #include <unistd.h>
    
    #include <string>
    #include <zeromq/zmq.hpp>
    
    int main( int argc, char *argv[] )
    {
        if ( argc != 2 )
        {
            fprintf( stderr, "Usage : %s <localUri>\n", basename( argv[0] ) );
            exit ( EXIT_FAILURE );
        }
    
        std::string pLocalUri( argv[1] );
        zmq::context_t localContext( 1 );
        zmq::socket_t *pSocket = new zmq::socket_t( localContext, ZMQ_SUB );
        if ( NULL == pSocket )
        {
            fprintf( stderr, "Couldn't create socket. Aborting...\n" );
            exit ( EXIT_FAILURE );
        }
        try
        {
            pSocket->setsockopt( ZMQ_SUBSCRIBE, "", 0 );
            pSocket->bind( pLocalUri.c_str() );
        }
        catch( ... )
        {
            fprintf( stderr, "Couldn't bind socket. Aborting...\n" );
            exit ( EXIT_FAILURE );
        }
    
        int msgCounter = 0;
        printf( "Subscriber Up and running... waiting for messages\n" );
        fflush( NULL );
    
        do
        {
            try
            {
                zmq::message_t inTask;
                pSocket->recv ( &inTask );
                printf( "Message received : [%s]\n", inTask.data() );
                fflush( NULL );
                msgCounter++;
            }
            catch( ... )
            {
                fprintf( stderr, "Some unknown error ocurred. Aborting...\n" );
                exit ( EXIT_FAILURE );
            }
        }
        while ( msgCounter < 5 );
    
        // pSocket->setsockopt( ZMQ_UNSUBSCRIBE, "", 0 ); NOT UNSUBSCRIBING
        pSocket->close();
        exit ( EXIT_SUCCESS );
    }
    

2 个答案:

答案 0 :(得分:2)

问:为什么会这样?

因为SUB实际上仍然是连接的(不是&#34;断开&#34;足够)。

是的,可能会令人惊讶,但杀死 SUB -process,无论是.bind()还是.connect() - 在插座的传输介质的附加侧,并不意味着,I / O泵的有限状态机已经移动了#34;进入断开状态。

鉴于此, PUB -side没有其他选择,只能考虑 SUB - 仍然存在并且已连接(即使这个过程在PUB - 侧的视线之外被默默地杀死了,并且对于这样的&#34;分布式&#34; -state有一个ZeroMQ协议定义的行为(PUB ({1}} - SUB方面仍然认为可以公平生活(但可能只是暂时存在一些间接问题)的所有临时信息某个地方较低,在传输I / O级别或某些远程CPU资源starvations或并发引入的瞬时间歇{local | remote}阻塞状态等。

所以它缓冲......

如果你暗杀 PUB - 代理似乎更优雅(使用归零SUB +足够ZMQ_LINGER在socket-resource实例上) .close() -side将识别&#34;分布式&#34;系统范围的有限状态自动机转换为确实&#34 ; DISCONNECT&#34; -ed状态和应有的行为改变将发生在&#34; distributed-FSA&#34;的PUB侧,而不是为此存储任何消息&#34;明显&#34;确实&#34; DISCONNECT&#34; -ed PUB - 正是文档所说的。

&#34;分布式FSA&#34;有一种相当薄弱的手段来识别状态变化事件&超越它的 localhost contols的视野。 SUB - 一个远程流程,它实现了&#34; distributed-FSA&#34;是一个毁灭性的事件,而不是一个如何保持系统工作的方法。这种外部风险的一个很好的选择可能是

听起来很复杂?

哦,是的,确实很复杂。这正是ZeroMQ为我们解决这个问题的原因所在,我们可以自由地享受基于这些(已经解决的)低级复杂性的设计应用程序架构。

分布式系统FSA(sub-FSA-s的分层组合的系统级FSA)

想象一下隐藏在引擎盖下的东西,想象一下只有一对简单的串联FSA-FSA - 正是 KILL 实例试图处理的对我们处于最简单的1:1 .Context() 场景中,用例PUB/SUB - KILL侧的所有子FSA-s,没有尝试向SUB方面表达意图。甚至TCP协议(同时居住在PUB - 侧和PUB - 侧)也有一些状态转换,从[ SUB ]到[ ESTABLISHED ]州。

分布式系统上的快速X射线视图&#39; FSA-的-FSA-S

(为了清楚起见,仅描述了TCP协议FSA)

<强> CLOSED 侧的:

enter image description here

PUB实例的行为FSA:

enter image description here

<强> .socket( .. ) 侧的:

enter image description here

(由nanomsg提供)。

答案 1 :(得分:1)

Bind and Connect虽然无动于衷,但在这里有特定的含义。

选项1:

将代码更改为这种方式,没有问题:

  1. 发布商应bind到地址
  2. 订阅者应connect到该地址
  3. '如果您绑定订阅者然后中断它,则发布者无法知道订阅者是未绑定的,因此它将消息排入绑定端口,并且当您在同一端口上再次重新启动时,排队的消息将被抽干了。

    选项2:

    但是如果你想按自己的方式去做,你需要做以下事情:

    1. 在订户代码
    2. 中注册中断处理程序(SIGINT
    3. 关于订户的中断,请执行以下操作:
      • unsubscribe主题
      • close子插座
      • 使用优选0返回代码
      • 彻底退出订户进程
    4. <强>更新

      关于身份点,不要假设设置标识将唯一地标识连接。如果留给zeromq,它将使用唯一的任意数字分配传入连接的标识。

      一般而言,身份不会用于回复客户。它们用于在使用ROUTER套接字的情况下响应客户端。 'Coz ROUTERsockets是异步的,因为REQ / REP是同步的。在Async中,我们需要知道我们回复的对象。它可以是n / w地址或随机数或uuid等。

      <强>更新

      我不认为这与zeromq有关,因为在整个指南中PUB / SUB的解释方式是Publisher通常是静态的(服务器并绑定到端口),订阅者一路走来(客户端)连接到端口。)

      还有另一种选择完全符合您的要求 ZMQ_IMMEDIATEZMQ_DELAY_ATTACH_ON_CONNECT

      在发布者上设置上述套接字选项时,如果消息没有活动连接,则不会让消息排队。