ZeroMQ for Clojurists

Dave Yarwood • @dave_yarwood

Clojure Remote 2017

Agenda

  • What is ZeroMQ?
  • ZeroMQ vs. core.async
  • Libraries
  • ezzmq
  • Building stuff

screenshot of zeromq.org

screenshot of zguide.zeromq.org

ZeroMQ vs. core.async

(def inbox (async/chan))
(def outbox (async/chan))

(defn server []
  (async/go-loop []
    (when-let [req (async/<!! inbox)]
      (async/>!! outbox (str "This is my response to " req))
      (recur))))

(defn client []
  (async/go
    (dotimes [n 100]
      (async/>!! inbox (str "msg " n))
      (let [res (async/<!! outbox)]
        (println "Response from server:" res)))))

clojure.core.async

  • Channels are local to a Clojure program
  • Useful for unidirectional task pipelines
  • A message can be any non-nil Clojure value

ZeroMQ vs. core.async

(defn server []
  (let [socket (zmq/socket :rep {:bind "tcp://*:12345"})]
    (while true
      (let [req (zmq/receive-msg socket :stringify true)
            res (str "This is my response to " req)]
        (zmq/send-msg socket res)))))

(defn client []
  (let [socket (zmq/socket :req {:connect "tcp://*:12345"})]
    (dotimes [n 100]
      (zmq/send-msg socket (str "msg " n))
      (let [res (zmq/receive-msg socket :stringify true)]
        (println "Response from server:" res)))))

ZeroMQ

  • Can send messages between programs
  • A socket can exist outside of your program
  • Specialized socket types for high-level patterns
  • Can send messages bidirectionally
  • Messages are byte arrays

ZMTP

ZeroMQ Message Transport Protocol

libzmq

low-level C/C++ API

czmq

high-level C library wrapping libzmq

jzmq

Java language binding for libzmq

JeroMQ

pure Java implementation of libzmq

(+ some nice higher level abstractions ported from czmq)

ezzmq

idiomatic Clojure wrapper around JeroMQ

import org.zeromq.ZContext;
import org.zeromq.ZMQ;

public static void main(String[] args) {
    ZContext context = new ZContext();

    ZMQ.Socket server = context.createSocket(ZMQ.REP);
    server.bind("tcp://*:12345");

    while (true) {
        byte[] request = server.recv(0);
        System.out.println("Received msg: " + new String(request));

        // simulate doing work
        Thread.sleep(1000);
        String response = "42";

        server.send(response.getBytes(), 0);
    }

    context.close();
}

jzmq / JeroMQ

(require '[ezzmq.core :as zmq])

(zmq/with-new-context
  (let [socket (zmq/socket :rep {:bind "tcp://*:12345"})]
    (while true
      (let [req (zmq/receive-msg socket :stringify true)
            res "42"]
        (println "Received msg:" req)

        ;; simulate doing work
        (Thread/sleep 1000)

        (zmq/send-msg socket res)))))

ezzmq

import org.zeromq.ZContext;
import org.zeromq.ZMQ;

public static void main (String[] args) {
    ZContext context = new ZContext();

    ZMQ.Socket pull = context.createSocket(ZMQ.PULL);
    pull.connect("tcp://*:12345");

    ZMQ.Socket sub = context.createSocket(ZMQ.SUB);
    sub.connect("tcp://*:12346");
    sub.subscribe("abc ".getBytes());

    ZMQ.Poller poller = new ZMQ.Poller(2);
    poller.register(pull, ZMQ.Poller.POLLIN);
    poller.register(sub, ZMQ.Poller.POLLIN);

    while (!Thread.currentThread().isInterrupted()) {
        byte[] msg;
        poller.poll();
        if (poller.pollin(0)) {
            msg = pull.recv(0);
            System.out.println("PULL: " + new String(msg));
        }
        if (poller.pollin(1)) {
            msg = sub.recv(0);
            System.out.println("SUB: " + new String(msg));
        }
    }

    context.close();
}

jzmq / JeroMQ

(require '[ezzmq.core :as zmq])

(zmq/with-new-context
  (let [pull (zmq/socket :pull {:connect "tcp://*:12345"})
        sub  (zmq/socket :sub  {:connect "tcp://*:12346"
                                :subscribe "abc "})]
    (zmq/polling {:stringify true}
      [pull :pollin [msg]
       (println (format "PULL: %s" msg))

       sub :pollin [msg]
       (println (format "SUB: %s" msg))]

      (zmq/while-polling
        (zmq/poll)))))

ezzmq

Socket Types: REQ/REP

(let [client (zmq/socket :req {:connect "tcp://*:12345"})
      _      (zmq/send-msg client "oh, hello")
      reply  (zmq/receive-msg client :stringify true)]
  (println "the server says:" reply))

Socket Types: PUB/SUB

Socket Types: PUB/SUB

(defn publisher
  (let [pub (zmq/socket :pub {:bind "tcp://*:12345"})]
    (while true
      (let [msg (format "%s %s"
                        (rand-nth ["abc" "def" "ghi"])
                        "this is a message")]
        (zmq/send-msg pub msg)))))
(defn subscriber-1
  (let [sub (zmq/socket :sub {:connect "tcp://*:12345" :subscribe "abc "})]
    (while true
      (let [msg (zmq/receive-msg sub)]
        (println "got msg:" msg)))))
(defn subscriber-2
  (let [sub (zmq/socket :sub {:connect "tcp://*:12345" :subscribe "def "})]
    (while true
      (let [msg (zmq/receive-msg sub)]
        (println "got msg:" msg)))))
(defn subscriber-3
  (let [sub (zmq/socket :sub {:connect "tcp://*:12345" :subscribe "ghi "})]
    (while true
      (let [msg (zmq/receive-msg sub)]
        (println "got msg:" msg)))))

Socket Types: PUSH/PULL

Socket Types: PUSH/PULL

(defn ventilator []
  (let [vent (zmq/socket :push {:bind "tcp://*:3333"})
        sink (zmq/socket :push {:connect "tcp://*:4444"})]
    ;; wait for workers to come online
    (Thread/sleep 30000)

    (zmq/send-msg sink "START BATCH")

    (dotimes [_ 100]
      (zmq/send-msg vent (str (rand-int 100))))))
(defn worker []
  (let [vent (zmq/socket :pull {:connect "tcp://*:3333"})
        sink (zmq/socket :push {:connect "tcp://*:4444"})]
    (while true
      (let [task-ms (-> (zmq/receive-msg vent :stringify true)
                        first
                        Integer/parseInt)]
        (Thread/sleep task-ms) ; simulate doing work
        (zmq/send-msg sink "success")))))
(defn sink []
  (let [sink (zmq/socket :pull {:bind "tcp://*:4444"})]
    ;; wait for start of batch
    (zmq/receive-msg sink)

    (dotimes [_ 100]
      (let [result (zmq/receive-msg sink :stringify true)]
        (println "Result:" result)))))

Socket Types: ROUTER/DEALER

Socket Types: PAIR

Socket Types: PAIR

(defn step-1 []
  (let [step2 (zmq/socket :pair {:connect "inproc://step2"})]
    (zmq/send-msg step2 "the eagle has landed")))
(defn step-2 []
  (let [step2 (zmq/socket :pair {:bind "inproc://step2"})
        msg   (zmq/receive-msg step2)
        step3 (zmq/socket :pair {:connect "inproc://step3"})]
    (zmq/send-msg step3 msg)))
(defn step-3 []
  (let [step3 (zmq/socket :pair {:bind "inproc://step3"})
        msg   (zmq/receive-msg step3)]
    (println msg)))

Building a Chat Server/Client

hi
hello
yo
sup

Building a Chat Server/Client

Building a Chat Server/Client

{"command": "join",
 "from":    "dave"}
{"command": "say",
 "from":    "dave",
 "body":    "oh, hello"}
{"command": "leave",
 "from":    "dave"}

Building a Chat Server/Client

the server

(let [rep (zmq/socket :rep {:bind "tcp://*:1111"})
      pub (zmq/socket :pub {:bind "tcp://*:2222"})]
  (zmq/polling {:stringify true}
    [rep :pollin [msg]
     (let [{:keys [command from body] :as req} (json/parse-string (first msg) true)]
       (case command
         "join"  ...
         "leave" ...
         "say"   ...))]
    (zmq/while-polling
      (zmq/poll 1000))))
    
"join"
(do
  (zmq/send-msg pub (json/generate-string
                      {:from "chat-server"
                       :body (format "%s joined." from)}))
  (zmq/send-msg rep (json/generate-string
                      {:success true
                       :body 2222}))) ; the port we're using to publish messages
chat-server> dave joined.

Building a Chat Server/Client

the client

(let [req (zmq/socket :req {:connect "tcp://*:1111"})
      sub (zmq/socket :sub {:connect (format "tcp://*:%s" (get-feed-port req)}))]
  
  ;; on another thread, print messages as they are received
  (zmq/worker-thread {}
    (zmq/polling {:stringify true}
      [sub :pollin [msg]
       (let [{:keys [from body]} (json/parse-string (first msg) true)]
         (println (format "%s> %s" from body)))]
      (zmq/while-polling
        (zmq/poll 1000))))
  ;; accept messages from user and send them to the server
  (let [cli (doto (jline.console.ConsoleReader.)
              (.setExpandEvents false)
              (.setPrompt "> "))]
    (while true
      (println)
      (let [msg (.readLine cli)]
        (when-not (empty? msg)
          (zmq/send-msg req (json/generate-string {:command "say"
                                                   :from    "dave"
                                                   :body    msg})))))))
dave> o hai

Building a Chat Server/Client

opportunities for improvement

  • client: don't interleave input/output
  • client: handle failure response from server
  • client: handle lack of response from server
  • server: heartbeating
  • feature: chat log w/ datetimes
  • feature: login / authentication

Questions?

Made with Slides.com