Day 20 - zeromq

Relevancy: 1.9 stable

ZeroMQ is a language-independent messaging solution. It's not a full-fledged system such as for example RabbitMQ, basically it's just a transport layer. From the programmer's perspective working with it doesn't differ much from ordinary sockets, but there's a lot of power hidden underneath. The rust-zmq crate is a Rust binding to the C library. There used to be a working native binding (zeromq), but it's now undergoing a redesign and rewrite.

Operational patterns

The ZeroMQ guide lists several messaging patterns such as request-response, pub-sub, pipeline etc. Different patterns suit different needs for distributed systems; for example the request-response pattern is commonly used for remote procedure calls. This is the only mode of operation implemented in the zeromq crate at the moment, but hopefully more will be added soon.

Before we start implementing the client and server, let's prepare some boilerplate code. We will decide whether to run our demo program as client or server based on the commandline argument.

day20.rs

extern crate zmq;

day20.rs

use zmq::{Context, Message, Error};

day20.rs

if args.len() < 2 {
        println!("Usage: {} (client|server)", args[0]);
        return;
    }
    let mut ctx = Context::new();
    let addr = "tcp://127.0.0.1:25933";
    if args[1] == "client" {
        println!("ZeroMQ client connecting to {}", addr);
        run_client(&mut ctx, addr).unwrap_or_else(|err| println!("{:?}", err));
    } else {
        println!("ZeroMQ server listening on {}", addr);
        run_server(&mut ctx, addr).unwrap_or_else(|err| println!("{:?}", err));
    }
}

Client

day20.rs

fn run_client(ctx: &mut Context, addr: &str) -> Result<(), Error> {
    let sock = ctx.socket(zmq::REQ)?;
    sock.connect(addr)?;
    let payload = "Hello world!";
    println!("-> {:?}", payload);
    let mut msg = Message::new()?;
    sock.send(payload.as_bytes(), 0)?;
    sock.recv(&mut msg, 0)?;
    let contents = msg.as_str().unwrap();
    println!("<- {:?}", contents);
    Ok(())
}

A ZeroMQ request starts with opening a REQ socket. The sockets send and receive Message objects. You can use any encoding you like, ZeroMQ doesn't enforce anything. It can be JSON, msgpack, protobuf, whatever - as long as you push some bytes over the wire, ZeroMQ is happy.

Note that we're using the try! macro for error handling.

Server

We're going to build a simple echo server that repeats the incoming message in the response.

day20.rs

fn run_server(ctx: &mut Context, addr: &str) -> Result<(), Error> {
    let sock = ctx.socket(zmq::REP)?;
    sock.bind(addr)?;
    let mut msg = Message::new()?;
    loop {
        if sock.recv(&mut msg, 0).is_ok() {
            sock.send(msg.as_str().unwrap().as_bytes(), 0)?;
        }
    }
}

The server opens a REP socket and then loops infinitely and echoes back incoming message data. In my first implementation I forgot to send the response and got weird socket errors - turns out a response is necessary in a request/response mode, who would have thought...

Let's start the server now:

$ cargo run -- server
ZeroMQ server listening on tcp://127.0.0.1:25933

And if we fire up the client in a new tab we should see a roundtrip message:

$ cargo run -- client
ZeroMQ client connecting to tcp://127.0.0.1:25933
-> "Hello world!"
<- "Hello world!"

See also