Chapter 1: “Hello World” with ZeroMQ

This introduction uses the ubiquitous “Hello world” example to smoke test the python based installation of ZeroMQ. In this chapter we examine the code and illustrate some basic points useful for future chapters.

This is the interactions of hwserver and hwclient, each in a different terminal:

The message transfers start with the server listening. Nothing happens until a client says “Hello” by sending a message to the server. The server then replies by sending a message of “World”.

Please don’t be overly concerned if you find yourself somewhat confused or don’t quite understand everything. Just stick with this and the concepts will clarify.

NOTE: Please download from PyIoTMessaging. The code discussed in this blog is in the ch01 directory.

The Server Code

The hwserver.py code looks like:

The import time brings in the time package. Only the sleep() gets used. Timing routines will measure the overall responsiveness of this client/server package.

import zmq brings in the ZeroMQ package. Everything we need from ZeroMQ becomes available.

ZeroMQ requires a general context to operate. The context = xmq.Context() creates this context. ZeroMQ uses context to create sockets. A “context” creates a general ecological environment to perform capabilities specific to a given goal, in this case ZeroMQ.

One and only one ZeroMQ context should be created for each process! A ZeroMQ context must be created before using any other ZeroMQ functions and this context must be destroyed at the end of all other ZeroMQ function calls. In the act of destroying the ZeroMQ context, any lingering messages get flushed.

Lines 4 and 5 create the sockets bound to the context. Specifically, socket = context.socket(zmq.REP) uses the context created in line 4 build a ZeroMQ socket. This socket gets further specialized into a reply, “REP”, style socket. A reply socket in ZMQ can only for replying to messages.

Line 6, socket.bind("tcp://*:5555") creates a “home” for a socket. This socket uses the “tcp” protocol, a standard method of transmitting messages. Additionally, the port 5555 identifies that the socket uses the tcp for the specific transport layer. The socket thus becomes “bound” to port 5555. (If your eyes are rolling, just relax and it should clear up shortly.)

Lines 8 to the end of the file contains the actual handling of messages. The overall logic waits for a message, announces the message, performs some “work”, i.e., time.sleep(1), and sends a reply of “world”.

The Client Code

The client’s logic matches the simplistic nature of the server.

The zmq package gets imported, a context created, and a socket constructed just as the server.

One important note: the socket constructor on line 13 uses context.socket(zmq.REQ). The “REQ” constructs a socket that send requests. ZeroMQ specializes sockets, a point we will return to.

These messages:

  • Get created by the client with the word “World”.
  • Sent to the server.
  • The server receives the message.
  • The sever sends a response of “Hello”.
  • The client receives the response.

Timing Hello World

Now that the basic code works, let’s do something that should convince you of the speed of ZeroMQ.

Many people get overly concerned by the speed of anything software. Performing speed test builds confidence in the system under construction. ZeroMQ has performance tests that prove speed claims. ZeroMQ should easily meet almost any necessary speed used in common micro-board computers.

Performance test should also take into consideration the robustness of software, the ease of development and ease of maintenance. A good software package is much more than just speed.

hwclient_timing.py

hwclient_timing.py has the comments omitted because outputting takes a considerable amount of time. The number of messages has been soft-coded to total_messages = 100000. Depending upon the speed of your computer and your operating system, they may be adjusted up or down.

The client logic for messaging gets wrapped in a function named looper(). All the messages gets performed here. The client sends “Hello” and receives a reply. That is wrapped in a loop executed total_messages = 10000 times.

The timing client has a bit more logic added. This timing logic gets used over and over as a sanity check to ensure clean code and clean design. If we notice a significant deviation, we know something has run aground.

if __name__ == '__main__':
    import timeit
    my_setup = 'from __main__ import looper'

    the_timing = timeit.timeit(stmt='looper()', 
            setup=my_setup, number=1)

    print('%d messages in %f secs' % \
            (total_messages, the_timing))
    print('%6.0f messages / sec' % (total_messages / the_timing))

When hwclient_timing.py gets run by itself, it drops into this timing logic. A call to timeit.timeit() requires a special setup with the indicated parameters. Additionally, the strange lines at my_setup =... pulls in the looper function for timeit.timeit(). This becomes necessary because timeit forks a process and that process gets times.

The two print statements at the end report on collected statistics.

hwserver_timing.py

hwserver_timing.py remains mostly the same but removes any output when messaging.

Remove the output echoing message traffic to create hwserver_timing.py . The server receives a message replies “World” for any message received.

Timing Results

Run the timing test the same as before in two windows: one for python hwserver_timing.py and the other for python hwclient_timing.py.

For the 100,000 messages, this took 7.78 sec on my linux desktop for 13,568 msg/sec. Obviously you mileage may vary.

Since this blog also addresses Raspberry Pis, the same identical code yielded 31.64 sec for 3,161 msg/sec. This slower reading from my Rapsberry Pi 2 is expected. The inexpensive and older Raspberry Pi 2 does not have the speed of my desktop, and it obviously shows.

Let’s try something you might consider remarkable. On your desktop, start the server as python hwserver_timing.py and in another window start two timing clients:

    python hwclient_timing.py& python hwclient_timing.py &

The “&” and the end of each python command sends the process into the background. In the background they both run “simultaneously” sending and receiving messages with the server. The single server handles both client’s messages, simultaneously.

The result of this two clients banging on a server may surprise you: 7.05 sec with 14,172 msg/sec!

The result of having two clients sending messages round trip to a single server is faster than a lone single client! Why?

The answer lies in the way our code has been written. The send() and the recv() have been designed to be synchronous. Synchronous means they perform their function and wait. The type of sockets, REQ and REP, always send one request and then wait for a reply.

The client runs in lockstep and waits for a reply after sending a message.

Thus the overall flow is send a message, wait for a reply.

When multiple clients send messages to the server, the server generally is just waiting for something to do. The synchronous messaging in the server replies to each client. Due to the design of the superlative ZeroMQ sockets, each reply gets sent to the originator of the message.

Messaging Between Linux and Your Raspberry Pi

Let’s run a timing test between the desktop and a Raspberry Pi 2 system. The desktop will be the client and the Raspberry Pi the server.

This will require a bit of setup. We now tackle remote computers messaging each other.

We will connect a desktop system to a Raspberry Pi.

Because this operation becomes common as we continue to develop our messaging software, a detailed explanation will serve as a future reference.

For this test, the IP address of the Raspberry Pi is necessary to properly identify the Raspian system.

An “IP address” defines a unique string of numbers separated by periods that identifies each computer using a network. Each computer connected to the internet has an IP address.

On the Raspberry Pi system command line:

pi@raspberry1 2 pi$ ifconfig
eth0      Link encap:Ethernet  HWaddr b8:27:eb:28:3c:d2  
          inet addr:192.168.1.85  Bcast:192.168.1.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          ...

Your output may be different! What we need is the line with “inet addr:...“. This section may not be necessarily labeled “eth0” and could easily be something different.

The command ifconfig reports and/or configures ip addresses among other things. Our IP address is inet addr:192.168.1.85. Rather than a name, we need this IP address to tell our configuration where to send/receive messages.

Test this IP address by using a command line “ping”:

    ping 192.168.1.85   < -- Change to YOUR IP address!!
    PING 192.168.1.85 (192.168.1.85) 56(84) bytes of data.
    64 bytes from 192.168.1.85: icmp_seq=1 ttl=64 time=0.343 ms
    64 bytes from 192.168.1.85: icmp_seq=2 ttl=64 time=0.284 ms
    64 bytes from 192.168.1.85: icmp_seq=3 ttl=64 time=0.260 ms
    ...

This output indicates a successful connection.

However, if your output resembles:

    ping 192.168.1.85
    PING 192.168.1.85 (192.168.1.85) 56(84) bytes of data.
    From 192.168.1.80 icmp_seq=1 Destination Host Unreachable
    From 192.168.1.80 icmp_seq=2 Destination Host Unreachable
    ...

This indicates that the IP address is not correct or that your desktop cannot somehow reach your Raspberry Pi. Check that IP again on the Raspberry Pi using:

pi@raspberry1 2 pi$ ip addr
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: eth0:  mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether b8:27:eb:28:3c:d2 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.85/24 brd 192.168.1.255 scope global eth0
       valid_lft forever preferred_lft forever

An alternate method of IP address determination uses the command ip addr. Please verify that this IP address matches the expected IP address. Try this IP address in the ping test.

Your IP address that runs from a home network most likely uses something called DHCP, Dynamic Host Configuration Protocol. The “dynamic” means the IP address may change. This happens when a system boots or otherwise disconnects from the local home network. A DHCP server enables computers to request IP addresses and networking parameters dynamically and reduces the net for a network manager to configure settings manually.

We will return to this important topic of manually setting an IP address to simplify our configurations.

For now, take that IP address use it in the client code. In the hwclient_timing.pw code, change the line that performs a socket connect (remember to use your IP address):

    socket = context.socket(zmq.REQ)
    #socket.connect("tcp:///localhost:5555")   # Commented out. For local talking.
    socket.connect("tcp://192.168.1.85:5555")  # socket talks to this IP

This tells the socket to talk to the designated IP address, i.e., the Raspberry Pi server.

Summary to Find Your IP Address

On the computer with the unknown IP address:

  • ifconfig # Utility for network configuration
  • Find “inet …” # locate the line with the IP address
  • On another system in the network: # Test that IP address
  • ping IP-ADDRESS
  • Should have sequence of times.

Timing Between Linux and Your Raspberry Pi

Now the software in hwclient_timing.py on the desktop has the proper IP address of the Raspberry Pi.

On the Raspberry Pi, cd to the directory where hwserver_timing.py lives, and start the server:

    python hwserver_timing.py

Now start the client on the desktop:

    python hwclient_timing.py
    Connecting to hello world profiling server...
    100000 messages in 49.254013 secs
      2030 messages / sec

Notice the timing has slowed significantly. This expected slowness results from the less powerful computer system on the Raspberry Pi.

Time the impact of multiple clients sending messages round-trip to the server. On the desktop:

python hwclient_timing.py & python hwclient_timing.py &
[1] 2176
[2] 2177
Connecting to hello world profiling server...
Connecting to hello world profiling server...
100000 messages in 49.600041 secs
  2016 messages / sec
100000 messages in 49.607473 secs
  2016 messages / sec

The “[1]” and “[2]” indicate the two python jobs will run in the background.

Run this test multiple times and notice the timings are close.

Try this test with three or more clients.

In the table below, notice the python code is only slightly slower than C code! The ZeroMQ core code uses C for optimal speed and portability.

Server Sys Client Sys # Clients Time sec Messages/sec
Linux Linux 1 7.78 13,568
Linux Linux 2 7.05 14,172
Linux Linux 3 7.48 13,366
Linux C server Linux C 1 7.37 13,642
Raspberry Pi 2 Linux 1 31.64 3,151
Raspberry Pi 2 Raspberry Pi 2 1 49.20 2,032
Raspberry Pi 2 Linux 3 53.35 1,955

Links

All the code discussed: PyIoTMessaging in github in the PyIoTMessaging/ch01 directory.

This entry was posted in 0MQ, arduino, IoT, linux, messaging, networking, RaspberryPi, zeromq, zmq and tagged , , , , , , , . Bookmark the permalink.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.