Example #1
0
int main (int argc, char *argv [])
{
    //  Let's start a new Malamute broker
    zactor_t *broker = zactor_new (mlm_server, NULL);

    //  Switch on verbose tracing... this gets a little overwhelming so you
    //  can comment or delete this when you're bored with it:
    zsock_send (broker, "s", "VERBOSE");

    //  We control the broker by sending it commands. It's a CZMQ actor, and
    //  we can talk to it using the zsock API (or zstr, or zframe, or zmsg).
    //  To get things started, let's tell the broker to bind to an endpoint:
//     zsock_send (broker, "ss", "BIND", "tcp://*:12345");

    //  This is how we configure a server from an external config file, which
    //  is in http://rfc.zeromq.org/spec:4/ZPL format:
    zstr_sendx (broker, "LOAD", "src/malamute.cfg", NULL);

    //  We can also, or alternatively, set server properties by sending it
    //  SET commands like this (see malamute.cfg for details):
    zsock_send (broker, "sss", "SET", "server/timeout", "5000");

    //  For PLAIN authentication, we start a zauth instance. This handles
    //  all client connection requests by checking against a password file
    zactor_t *auth = zactor_new (zauth, NULL);
    assert (auth);

    //  We can switch on verbose tracing to debug authentication errors
    zstr_sendx (auth, "VERBOSE", NULL);
    zsock_wait (auth);

    //  Now specify the password file; each line says 'username=password'
    zstr_sendx (auth, "PLAIN", "src/passwords.cfg", NULL);
    zsock_wait (auth);

    //  The broker is now running. Let's start two clients, one to publish
    //  messages and one to receive them. We're going to test the stream
    //  pattern with some natty wildcard patterns.
    mlm_client_t *reader = mlm_client_new ();
    assert (reader);
    int rc = mlm_client_set_plain_auth (reader, "reader", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (reader, "tcp://127.0.0.1:9999", 1000, "reader");
    assert (rc == 0);

    mlm_client_t *writer = mlm_client_new ();
    assert (writer);
    rc = mlm_client_set_plain_auth (writer, "writer", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (writer, "tcp://127.0.0.1:9999", 1000, "writer");
    assert (rc == 0);

    //  The writer publishes to the "weather" stream
    mlm_client_set_producer (writer, "weather");

    //  The reader consumes temperature messages off the "weather" stream
    mlm_client_set_consumer (reader, "weather", "temp.*");

    //  The writer sends a series of messages with various subjects. The
    //  sendx method sends string data to the stream (we send the subject,
    //  then one or more strings):
    mlm_client_sendx (writer, "temp.moscow", "1", NULL);
    mlm_client_sendx (writer, "rain.moscow", "2", NULL);
    mlm_client_sendx (writer, "temp.madrid", "3", NULL);
    mlm_client_sendx (writer, "rain.madrid", "4", NULL);
    mlm_client_sendx (writer, "temp.london", "5", NULL);
    mlm_client_sendx (writer, "rain.london", "6", NULL);

    //  The simplest way to receive a message is via the recvx method,
    //  which stores multipart string data:
    char *subject, *content;
    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "temp.moscow"));
    assert (streq (content, "1"));
    zstr_free (&subject);
    zstr_free (&content);

    //  The last-received message has other properties:
    assert (streq (mlm_client_subject (reader), "temp.moscow"));
    assert (streq (mlm_client_command (reader), "STREAM DELIVER"));
    assert (streq (mlm_client_sender (reader), "writer"));
    assert (streq (mlm_client_address (reader), "weather"));

    //  Let's get the other two messages:
    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "temp.madrid"));
    assert (streq (content, "3"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "temp.london"));
    assert (streq (content, "5"));
    zstr_free (&subject);
    zstr_free (&content);

    //  Great, it all works. Now to shutdown, we use the destroy method,
    //  which does a proper deconnect handshake internally:
    mlm_client_destroy (&reader);
    mlm_client_destroy (&writer);

    //  Finally, shut down the broker by destroying the actor; this does
    //  a proper shutdown so that all memory is freed as you'd expect.
    zactor_destroy (&broker);
    zactor_destroy (&auth);
    return 0;
}
Example #2
0
void
mlm_server_test (bool verbose)
{
    printf (" * mlm_server: ");
    if (verbose)
        printf ("\n");

    //  @selftest
    zactor_t *server = zactor_new (mlm_server, "mlm_server_test");
    if (verbose)
        zstr_send (server, "VERBOSE");
    zstr_sendx (server, "BIND", "tcp://127.0.0.1:*", NULL);
    zstr_sendx (server, "PORT", NULL);
    char *command, *port;
    int rc = zstr_recvx (server, &command, &port, NULL);
    assert (rc == 2);
    assert (streq (command, "PORT"));
    assert (strlen (port) > 0 && strlen (port) < 6);
    assert (!streq (port, "-1"));

    zsock_t *reader = zsock_new (ZMQ_DEALER);
    assert (reader);
    zsock_connect (reader, "tcp://127.0.0.1:%s", port);
    zsock_set_rcvtimeo (reader, 500);

    mlm_proto_t *proto = mlm_proto_new ();

    //  Server insists that connection starts properly
    mlm_proto_set_id (proto, MLM_PROTO_STREAM_WRITE);
    mlm_proto_send (proto, reader);
    zclock_sleep (500); //  to calm things down && make memcheck pass. Thanks @malanka
    mlm_proto_recv (proto, reader);
    zclock_sleep (500); //  detto as above
    assert (mlm_proto_id (proto) == MLM_PROTO_ERROR);
    assert (mlm_proto_status_code (proto) == MLM_PROTO_COMMAND_INVALID);

    //  Now do a stream publish-subscribe test
    zsock_t *writer = zsock_new (ZMQ_DEALER);
    assert (writer);
    zsock_connect (writer, "tcp://127.0.0.1:%s", port);
    zsock_set_rcvtimeo (reader, 500);

    //  Open connections from both reader and writer
    mlm_proto_set_id (proto, MLM_PROTO_CONNECTION_OPEN);
    mlm_proto_send (proto, reader);
    mlm_proto_recv (proto, reader);
    assert (mlm_proto_id (proto) == MLM_PROTO_OK);

    mlm_proto_set_id (proto, MLM_PROTO_CONNECTION_OPEN);
    mlm_proto_send (proto, writer);
    mlm_proto_recv (proto, writer);
    assert (mlm_proto_id (proto) == MLM_PROTO_OK);

    //  Prepare to write and read a "weather" stream
    mlm_proto_set_id (proto, MLM_PROTO_STREAM_WRITE);
    mlm_proto_set_stream (proto, "weather");
    mlm_proto_send (proto, writer);
    mlm_proto_recv (proto, writer);
    assert (mlm_proto_id (proto) == MLM_PROTO_OK);

    mlm_proto_set_id (proto, MLM_PROTO_STREAM_READ);
    mlm_proto_set_pattern (proto, "temp.*");
    mlm_proto_send (proto, reader);
    mlm_proto_recv (proto, reader);
    assert (mlm_proto_id (proto) == MLM_PROTO_OK);

    //  Now send some weather data, with null contents
    mlm_proto_set_id (proto, MLM_PROTO_STREAM_SEND);
    mlm_proto_set_subject (proto, "temp.moscow");
    mlm_proto_send (proto, writer);
    mlm_proto_set_subject (proto, "rain.moscow");
    mlm_proto_send (proto, writer);
    mlm_proto_set_subject (proto, "temp.chicago");
    mlm_proto_send (proto, writer);
    mlm_proto_set_subject (proto, "rain.chicago");
    mlm_proto_send (proto, writer);
    mlm_proto_set_subject (proto, "temp.london");
    mlm_proto_send (proto, writer);
    mlm_proto_set_subject (proto, "rain.london");
    mlm_proto_send (proto, writer);

    //  We should receive exactly three deliveries, in order
    mlm_proto_recv (proto, reader);
    assert (mlm_proto_id (proto) == MLM_PROTO_STREAM_DELIVER);
    assert (streq (mlm_proto_subject (proto), "temp.moscow"));

    mlm_proto_recv (proto, reader);
    assert (mlm_proto_id (proto) == MLM_PROTO_STREAM_DELIVER);
    assert (streq (mlm_proto_subject (proto), "temp.chicago"));

    mlm_proto_recv (proto, reader);
    assert (mlm_proto_id (proto) == MLM_PROTO_STREAM_DELIVER);
    assert (streq (mlm_proto_subject (proto), "temp.london"));

    mlm_proto_destroy (&proto);

    //  Finished, we can clean up
    zsock_destroy (&writer);
    zsock_destroy (&reader);
    zactor_destroy (&server);
    zstr_free (&port);
    zstr_free (&command);

    // Test Case:
    //      CLIENTLIST command
    {
        const char *endpoint = "inproc://mlm_server_clientlist_test";
        zactor_t *server = zactor_new (mlm_server, "mlm_server_clientlist_test");
        if (verbose)
            zstr_send (server, "VERBOSE");
        zstr_sendx (server, "BIND", endpoint, NULL);

        mlm_client_t *client_1 = mlm_client_new ();
        int rv = mlm_client_connect (client_1, endpoint, 1000, "Karol");
        assert (rv >= 0);

        mlm_client_t *client_2 = mlm_client_new ();
        rv = mlm_client_connect (client_2, endpoint, 1000, "Tomas");
        assert (rv >= 0);

        mlm_client_t *client_3 = mlm_client_new ();
        rv = mlm_client_connect (client_3, endpoint, 1000, "Alenka");
        assert (rv >= 0);

        zclock_sleep (500);

        zstr_sendx (server, "CLIENTLIST", NULL);

        zmsg_t *message = zmsg_recv (server);
        assert (message);
        assert (zmsg_size (message) == 4);

        char *pop = zmsg_popstr (message);
        assert (streq (pop, "CLIENTLIST"));
        zstr_free (&pop);

        zlistx_t *expected_names = zlistx_new ();
        assert (expected_names);
        zlistx_set_destructor (expected_names, (czmq_destructor *) zstr_free);
        zlistx_set_duplicator (expected_names, (czmq_duplicator *) strdup);
        zlistx_set_comparator (expected_names, (czmq_comparator *) strcmp);

        zlistx_add_end (expected_names, (void *) "Karol");
        zlistx_add_end (expected_names, (void *) "Tomas");
        zlistx_add_end (expected_names, (void *) "Alenka");

        pop = zmsg_popstr (message);
        assert (pop);
        void *handle = zlistx_find (expected_names, pop);
        assert (handle);
        rv = zlistx_delete (expected_names, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        handle = zlistx_find (expected_names, pop);
        assert (handle);
        rv = zlistx_delete (expected_names, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        handle = zlistx_find (expected_names, pop);
        assert (handle);
        rv = zlistx_delete (expected_names, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop == NULL);
        assert (zlistx_size (expected_names) == 0);


        zmsg_destroy (&message);

        // remove a client Karol
        mlm_client_destroy (&client_1);

        zlistx_add_end (expected_names, (void *) "Tomas");
        zlistx_add_end (expected_names, (void *) "Alenka");

        zstr_sendx (server, "CLIENTLIST", NULL);
        zclock_sleep (100);

        message = zmsg_recv (server);
        assert (message);
        assert (zmsg_size (message) == 3);

        pop = zmsg_popstr (message);
        assert (streq (pop, "CLIENTLIST"));
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        handle = zlistx_find (expected_names, pop);
        assert (handle);
        rv = zlistx_delete (expected_names, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        handle = zlistx_find (expected_names, pop);
        assert (handle);
        rv = zlistx_delete (expected_names, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop == NULL);
        assert (zlistx_size (expected_names) == 0);

        zlistx_destroy (&expected_names);
        zmsg_destroy (&message);

        mlm_client_destroy (&client_2);
        mlm_client_destroy (&client_3);
        zactor_destroy (&server);
    }

    // Test Case:
    //      STREAMLIST command
    {
        const char *endpoint = "inproc://mlm_server_streamlist_test";
        zactor_t *server = zactor_new (mlm_server, "mlm_server_streamlist_test");
        if (verbose)
            zstr_send (server, "VERBOSE");
        zstr_sendx (server, "BIND", endpoint, NULL);

        mlm_client_t *client_1 = mlm_client_new ();
        int rv = mlm_client_connect (client_1, endpoint, 1000, "Karol");
        assert (rv != -1);
        rv = mlm_client_set_producer (client_1, "STREAM_1");
        assert (rv != -1);

        mlm_client_t *client_2 = mlm_client_new ();
        rv = mlm_client_connect (client_2, endpoint, 1000, "Tomas");
        assert (rv != -1);
        rv = mlm_client_set_producer (client_2, "STREAM_2");
        assert (rv != -1);

        mlm_client_t *client_3 = mlm_client_new ();
        rv = mlm_client_connect (client_3, endpoint, 1000, "Alenka");
        assert (rv != -1);
        rv = mlm_client_set_consumer (client_3, "STREAM_2", ".*");
        assert (rv != -1);

        zclock_sleep (100);

        zlistx_t *expected_streams = zlistx_new ();
        assert (expected_streams);
        zlistx_set_destructor (expected_streams, (czmq_destructor *) zstr_free);
        zlistx_set_duplicator (expected_streams, (czmq_duplicator *) strdup);
        zlistx_set_comparator (expected_streams, (czmq_comparator *) strcmp);

        zlistx_add_end (expected_streams, (void *) "STREAM_1");
        zlistx_add_end (expected_streams, (void *) "STREAM_2");

        zstr_sendx (server, "STREAMLIST", NULL);

        zmsg_t *message = zmsg_recv (server);
        assert (message);
        assert (zmsg_size (message) == 3);

        char *pop = zmsg_popstr (message);
        assert (streq (pop, "STREAMLIST"));
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        void *handle = zlistx_find (expected_streams, pop);
        assert (handle);
        rv = zlistx_delete (expected_streams, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        handle = zlistx_find (expected_streams, pop);
        assert (handle);
        rv = zlistx_delete (expected_streams, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop == NULL);
        assert (zlistx_size (expected_streams) == 0);

        zmsg_destroy (&message);

        //  NOTE: Currently when producer disconnects, malamute does not destroy the stream
        //  Therefore it doesn't make sense to test removal of streams, but addition
        mlm_client_t *client_4 = mlm_client_new ();
        rv = mlm_client_connect (client_4, endpoint, 1000, "Michal");
        assert (rv >= 0);
        rv = mlm_client_set_producer (client_4, "New stream");
        assert (rv >= 0);

        zlistx_add_end (expected_streams, (void *) "STREAM_1");
        zlistx_add_end (expected_streams, (void *) "STREAM_2");
        zlistx_add_end (expected_streams, (void *) "New stream");
        zclock_sleep (100);

        zstr_sendx (server, "STREAMLIST", NULL);
        zclock_sleep (100);

        message = zmsg_recv (server);
        assert (message);
        assert (zmsg_size (message) == 4);

        pop = zmsg_popstr (message);
        assert (streq (pop, "STREAMLIST"));
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        handle = zlistx_find (expected_streams, pop);
        assert (handle);
        rv = zlistx_delete (expected_streams, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        handle = zlistx_find (expected_streams, pop);
        assert (handle);
        rv = zlistx_delete (expected_streams, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop);
        handle = zlistx_find (expected_streams, pop);
        assert (handle);
        rv = zlistx_delete (expected_streams, handle);
        assert (rv == 0);
        zstr_free (&pop);

        pop = zmsg_popstr (message);
        assert (pop == NULL);
        assert (zlistx_size (expected_streams) == 0);

        zlistx_destroy (&expected_streams);
        zmsg_destroy (&message);

        mlm_client_destroy (&client_1);
        mlm_client_destroy (&client_2);
        mlm_client_destroy (&client_3);
        mlm_client_destroy (&client_4);
        zactor_destroy (&server);
    }

    // Regression Test Case:
    //      Segfault from deregistering zombie connection
    {
        const char *endpoint = "inproc://mlm_server_deregister_zombie_connection_test";
        zactor_t *server = zactor_new (mlm_server, "mlm_server_deregister_zombie_connection_test");
        if (verbose)
            zstr_send (server, "VERBOSE");
        zstr_sendx (server, "BIND", endpoint, NULL);
        zstr_sendx (server, "SET", "server/timeout", "3000", NULL); // 3 second client timeout

        zsock_t *reader = zsock_new (ZMQ_DEALER);
        assert (reader);
        zsock_connect (reader, "inproc://mlm_server_deregister_zombie_connection_test");
        zsock_set_rcvtimeo (reader, 500);

        mlm_proto_t *proto = mlm_proto_new ();

        // If the malamute server is restarted and clients have queued
        // up ping messages, the'll be sent before any
        // CONNECTION_OPEN.  The server eventually tries to deregister
        // this and (previously) would derefence a null pointer for
        // the client address.
        mlm_proto_set_id (proto, MLM_PROTO_CONNECTION_PING);
        mlm_proto_send (proto, reader);

        printf("Regression test for segfault due to leftover client messages after restart...\n");
        // Give the server more than 3 seconds to time out the client...
        zclock_sleep (3100);
        printf("passed\n");

        mlm_proto_destroy (&proto);
        zsock_destroy (&reader);
        zactor_destroy (&server);
    }

    {
        const char *endpoint = "inproc://mlm_server_disconnect_pending_stream_traffic";
        zactor_t *server = zactor_new (mlm_server, "mlm_server_disconnect_pending_stream_traffic");
        if (verbose) {
            zstr_send (server, "VERBOSE");
            printf("Regression test for use-after-free with pending stream traffic after disconnect\n");
        }
        zstr_sendx (server, "BIND", endpoint, NULL);

        mlm_client_t *producer = mlm_client_new ();
        assert (mlm_client_connect (producer, endpoint, 1000, "producer") >= 0);
        assert (mlm_client_set_producer (producer, "STREAM_TEST") >= 0);

        zstr_sendx (server, "SLOW_TEST_MODE", NULL);

        mlm_client_t *consumer = mlm_client_new ();
        assert (mlm_client_connect (consumer, endpoint, 1000, "consumer") >= 0);
        assert (mlm_client_set_consumer (consumer, "STREAM_TEST", ".*") >= 0);

        zmsg_t *msg = zmsg_new ();
        zmsg_addstr (msg, "world");
        assert (mlm_client_send (producer, "hello", &msg) >= 0);

        mlm_client_destroy (&consumer);

        zclock_sleep (2000);

        mlm_client_destroy (&producer);
        zactor_destroy (&server);
    }

    //  @end
    printf ("OK\n");
}
Example #3
0
//  ---------------------------------------------------------------------------
//  Selftest
void
mlm_client_test (bool verbose)
{
    printf (" * mlm_client: \n");

    //  @selftest
    mlm_client_verbose = verbose;

    //  Start a server to test against, and bind to endpoint
    zactor_t *server = zactor_new (mlm_server, "mlm_client_test");
    if (verbose)
        zstr_send (server, "VERBOSE");
    zstr_sendx (server, "LOAD", "src/mlm_client.cfg", NULL);

    //  Install authenticator to test PLAIN access
    zactor_t *auth = zactor_new (zauth, NULL);
    assert (auth);
    if (verbose) {
        zstr_sendx (auth, "VERBOSE", NULL);
        zsock_wait (auth);
    }
    zstr_sendx (auth, "PLAIN", "src/passwords.cfg", NULL);
    zsock_wait (auth);

    /////
    // Test mutual address exchange
    /**
       -----------                                                                                             ----------

       | Frontend | -> 1) provide addr, req opp addr   ->  |\         / |  <-  1) Provide addr, Req opp addr  | Backend |
                                                              >Broker<                                              
       -----------     2) Receive opp addr             <-                  -> 2) Receive opposite address      ----------
       
       3) Direct connection establishment
       -----------              ----------- 
                                            
       | Frontend | <- Send -> | Backend |
                                            
       -----------              ----------- 

     */
    mlm_client_t *frontend = mlm_client_new ();
    assert(frontend);
    mlm_client_t *backend = mlm_client_new();
    assert(backend);

    // connect front side to broker
    printf("connecting frontend to broker\n");
    int rc = mlm_client_set_plain_auth (frontend, "writer", "secret");
    assert (rc == 0);
    rc=mlm_client_connect (frontend, "tcp://127.0.0.1:9999", 1000, "");
    assert (rc == 0);

    // connect back side to broker    
    printf("connecting backend to broker\n");
    rc = mlm_client_set_plain_auth (backend, "reader", "secret");
    assert (rc == 0);
    rc=mlm_client_connect (backend, "tcp://127.0.0.1:9999", 1000, "");
    assert (rc == 0);
    
    //before you ever set a service, you should've already called bind in order to facilitate a 
    // connection from the other side. In this way, it's causally correct.
    // the frontend tells the broker that it is interested in addresses that are on the other side
    printf("setting frontend service\n");
    mlm_client_set_worker(frontend, "backendEndpoints", "SET*");
    // the backend tells the broker that it is interested in addresses that are on the other side
    printf("setting backend service\n");
    mlm_client_set_worker(backend, "frontendEndpoints", "SET*");
    
    // the frontend tells the broker what it's address is, fulfilling a need that the backend set;
    //  consequently, the broker sends this to the backend, which has set a service that it is
    //  subscribed to. SET* matches SET
    printf("sending frontend address SET message\n");
    mlm_client_sendforx(frontend, "frontendEndpoints", "SET", "inproc://frontend", NULL);
    // the backend tells the broker what it's address is, fullfilling a need that the frontend set;
    //  consequently, the broker sends this to the frontend, which has reported a service to the broker
    //  the broker matches SET to SET*.
    printf("sending backend address SET message\n");
    mlm_client_sendforx(backend, "backendEndpoints", "SET", "inproc://backend", NULL);
    
    char *set=NULL, *opp_addr=NULL;
    printf("receiving on backend");
    mlm_client_recvx(backend, &set, &opp_addr, NULL);
    assert(set); 
    assert(opp_addr); 
    // connect backend to frontend here, then clean up
    zstr_free(&opp_addr);
    zstr_free(&set);

    printf("receiving on backend");
    mlm_client_recvx(frontend, &set, &opp_addr, NULL);
    assert(set);
    assert(opp_addr);
    // connect frontend to backend here, then clean up
    zstr_free(&opp_addr);
    zstr_free(&set);

    // cleanup
    mlm_client_destroy(&backend);
    mlm_client_destroy(&frontend);
    
    // End of mutual address exchange tests
    /////
    
    //  Test stream pattern
    mlm_client_t *writer = mlm_client_new ();
    assert (writer);
    rc = mlm_client_set_plain_auth (writer, "writer", "secret");
    assert (rc == 0);
    assert (mlm_client_connected (writer) == false);
    rc = mlm_client_connect (writer, "tcp://127.0.0.1:9999", 1000, "writer");
    assert (rc == 0);
    assert (mlm_client_connected (writer) == true);

    mlm_client_t *reader = mlm_client_new ();
    assert (reader);
    rc = mlm_client_set_plain_auth (reader, "reader", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (reader, "tcp://127.0.0.1:9999", 1000, "");
    assert (rc == 0);

    mlm_client_set_producer (writer, "weather");
    mlm_client_set_consumer (reader, "weather", "temp.*");

    mlm_client_sendx (writer, "temp.moscow", "1", NULL);
    mlm_client_sendx (writer, "rain.moscow", "2", NULL);
    mlm_client_sendx (writer, "temp.madrid", "3", NULL);
    mlm_client_sendx (writer, "rain.madrid", "4", NULL);
    mlm_client_sendx (writer, "temp.london", "5", NULL);
    mlm_client_sendx (writer, "rain.london", "6", NULL);

    char *subject, *content;
    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "temp.moscow"));
    assert (streq (content, "1"));
    assert (streq (mlm_client_command (reader), "STREAM DELIVER"));
    assert (streq (mlm_client_sender (reader), "writer"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "temp.madrid"));
    assert (streq (content, "3"));
    assert (streq (mlm_client_command (reader), "STREAM DELIVER"));
    assert (streq (mlm_client_subject (reader), "temp.madrid"));
    assert (streq (mlm_client_sender (reader), "writer"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "temp.london"));
    assert (streq (content, "5"));
    assert (streq (mlm_client_command (reader), "STREAM DELIVER"));
    assert (streq (mlm_client_sender (reader), "writer"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_destroy (&reader);

    //  Test mailbox pattern
    reader = mlm_client_new ();
    assert (reader);
    rc = mlm_client_set_plain_auth (reader, "reader", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (reader, "tcp://127.0.0.1:9999", 1000, "mailbox");
    assert (rc == 0);

    mlm_client_sendtox (writer, "mailbox", "subject 1", "Message 1", "attachment", NULL);

    char *attach;
    mlm_client_recvx (reader, &subject, &content, &attach, NULL);
    assert (streq (subject, "subject 1"));
    assert (streq (content, "Message 1"));
    assert (streq (attach, "attachment"));
    assert (streq (mlm_client_command (reader), "MAILBOX DELIVER"));
    assert (streq (mlm_client_subject (reader), "subject 1"));
    assert (streq (mlm_client_sender (reader), "writer"));
    zstr_free (&subject);
    zstr_free (&content);
    zstr_free (&attach);

    //  Now test that mailbox survives reader disconnect
    mlm_client_destroy (&reader);
    mlm_client_sendtox (writer, "mailbox", "subject 2", "Message 2", NULL);
    mlm_client_sendtox (writer, "mailbox", "subject 3", "Message 3", NULL);

    reader = mlm_client_new ();
    assert (reader);
    rc = mlm_client_set_plain_auth (reader, "reader", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (reader, "tcp://127.0.0.1:9999", 500, "mailbox");
    assert (rc == 0);

    mlm_client_recvx (reader, &subject, &content, &attach, NULL);
    assert (streq (subject, "subject 2"));
    assert (streq (content, "Message 2"));
    assert (streq (mlm_client_command (reader), "MAILBOX DELIVER"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_recvx (reader, &subject, &content, &attach, NULL);
    assert (streq (subject, "subject 3"));
    assert (streq (content, "Message 3"));
    assert (streq (mlm_client_command (reader), "MAILBOX DELIVER"));
    zstr_free (&subject);
    zstr_free (&content);

    //  Test service pattern
    mlm_client_set_worker (reader, "printer", "bw.*");
    mlm_client_set_worker (reader, "printer", "color.*");

    mlm_client_sendforx (writer, "printer", "bw.A4", "Important contract", NULL);
    mlm_client_sendforx (writer, "printer", "bw.A5", "Special conditions", NULL);

    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "bw.A4"));
    assert (streq (content, "Important contract"));
    assert (streq (mlm_client_command (reader), "SERVICE DELIVER"));
    assert (streq (mlm_client_sender (reader), "writer"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "bw.A5"));
    assert (streq (content, "Special conditions"));
    assert (streq (mlm_client_command (reader), "SERVICE DELIVER"));
    assert (streq (mlm_client_sender (reader), "writer"));
    zstr_free (&subject);
    zstr_free (&content);

    //  Test that writer shutdown does not cause message loss
    mlm_client_set_consumer (reader, "weather", "temp.*");
    mlm_client_sendx (writer, "temp.brussels", "7", NULL);
    mlm_client_destroy (&writer);

    mlm_client_recvx (reader, &subject, &content, NULL);
    assert (streq (subject, "temp.brussels"));
    assert (streq (content, "7"));
    zstr_free (&subject);
    zstr_free (&content);
    mlm_client_destroy (&reader);

    //  Test multiple readers and multiple writers
    mlm_client_t *writer1 = mlm_client_new ();
    assert (writer1);
    rc = mlm_client_set_plain_auth (writer1, "writer", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (writer1, "tcp://127.0.0.1:9999", 1000, "");
    assert (rc == 0);

    mlm_client_t *writer2 = mlm_client_new ();
    assert (writer2);
    rc = mlm_client_set_plain_auth (writer2, "writer", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (writer2, "tcp://127.0.0.1:9999", 1000, "");
    assert (rc == 0);

    mlm_client_t *reader1 = mlm_client_new ();
    assert (reader1);
    rc = mlm_client_set_plain_auth (reader1, "reader", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (reader1, "tcp://127.0.0.1:9999", 1000, "");
    assert (rc == 0);

    mlm_client_t *reader2 = mlm_client_new ();
    assert (reader2);
    rc = mlm_client_set_plain_auth (reader2, "reader", "secret");
    assert (rc == 0);
    rc = mlm_client_connect (reader2, "tcp://127.0.0.1:9999", 1000, "");
    assert (rc == 0);

    mlm_client_set_producer (writer1, "weather");
    mlm_client_set_producer (writer2, "traffic");
    mlm_client_set_consumer (reader1, "weather", "newyork");
    mlm_client_set_consumer (reader1, "traffic", "newyork");
    mlm_client_set_consumer (reader2, "weather", "newyork");
    mlm_client_set_consumer (reader2, "traffic", "newyork");

    mlm_client_sendx (writer1, "newyork", "8", NULL);

    mlm_client_recvx (reader1, &subject, &content, NULL);
    assert (streq (mlm_client_address (reader1), "weather"));
    assert (streq (subject, "newyork"));
    assert (streq (content, "8"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_recvx (reader2, &subject, &content, NULL);
    assert (streq (mlm_client_address (reader2), "weather"));
    assert (streq (subject, "newyork"));
    assert (streq (content, "8"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_sendx (writer2, "newyork", "85", NULL);

    mlm_client_recvx (reader1, &subject, &content, NULL);
    assert (streq (mlm_client_address (reader1), "traffic"));
    assert (streq (subject, "newyork"));
    assert (streq (content, "85"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_recvx (reader2, &subject, &content, NULL);
    assert (streq (mlm_client_address (reader2), "traffic"));
    assert (streq (subject, "newyork"));
    assert (streq (content, "85"));
    zstr_free (&subject);
    zstr_free (&content);

    mlm_client_destroy (&writer1);
    mlm_client_destroy (&writer2);
    mlm_client_destroy (&reader1);
    mlm_client_destroy (&reader2);

    //  Done, shut down
    zactor_destroy (&auth);
    zactor_destroy (&server);
    //  @end
    printf ("OK\n");
}