If you have ever tried to make even a simple client-server application, you'll know that there are many issues for the programmer to solve. These problems are concurrency, flow control, reconnection, blocking, communication architecture, scalability, and much more. Even a simple demo application with a request-reply pattern can easily fail without proper precautions.
Fortunately, there's a ZeroMQ library that introduces a few network communication design patterns to make your application design much simpler.
In this recipe, you'll learn how to create simple request-reply applications, which you can later promote to simple HTTP web servers or file transferring applications. It all depends on your imagination.
One of the available communication patterns is called request-reply, which connects two nodes in a strict manner. You can send one message and you'll always get exactly one reply. After this, the whole process is repeated.
It might not seem obvious, but this design pattern is used fairly often even in applications you use today such as web browsers, FTP, network tool ping, and many others.
This pattern is easily adaptable in many applications because it emulates the most common form of communication between two people. First, you need to ask something. Eventually, you'll get your answer. The following diagram illustrates this design pattern:
This recipe will be divided into server and client parts to make understanding a bit easier. Each part should be in its separate file so that you can run them at the same time. You can even run them from different computers. However, be sure to include all the necessary files on both sides along with the Lua language interpreter.
This part of the communication application will wait for a request and replies with a message.
local zmq = require 'zmq'
zmq.ZMQ_REP
constant that corresponds to the reply part.local context = assert(zmq.context()) local socket = assert(context.socket(zmq.ZMQ_REP))
assert(socket.bind('tcp://*:12345'))
socket.recv
function to actually receive a message but that would introduce blocking, so your server application would wait idly until the request arrives. This is not always desirable. Therefore, there's a poll object to handle situations like this.local poll = zmq.poll()
poll.add(socket, zmq.ZMQ_POLLIN, function(socket) local result = assert(socket.recvAll()) print('Received a message: '..result) assert(socket.send('This is a reply to: '..result)) end)
local timeout = 500 -- 500ms while (true) do poll.start(timeout) end
socket.close()
This concludes the server part of this recipe.
The client code is very similar to the server part. The only difference is in the type of socket and instead of using the bind function to listen for connection, you'll be using the connect
function:
local zmq = require 'zmq' local context = assert(zmq.context()) local socket = assert(context.socket(zmq.ZMQ_REQ)) assert(socket.connect("tcp://localhost:12345")) local len = assert(socket.send("Test message")) if len and len > 0 then local poll = zmq.poll() poll.add(socket, zmq.ZMQ_POLLIN, function(s) print(assert(socket.recvAll())) end) poll.start() end socket.disconnect()
There are a few more notable differences. A function to start polling, poll.start()
, is used without parameters to define a timeout. This means that the client-side application will wait indefinitely until it gets a reply. After reply, it'll close the connection and quit.
This recipe uses the request-reply pattern, where the communication starts with a request and it's followed with a single reply. Other operations regarding message transport are blocked. However, this communication pattern is bound only to a single pair of requests and reply peers. Therefore, you can connect the request side to many peers to send a request to all of them and get a reply from each one.
By using the socket.bind
function, you'll create a listening part of the socket and it'll accept connections on your host. This function accepts an endpoint string that consists of three parts: a transport protocol, a host address, and a port number:
transport://address:port
A port number is omitted with interthread communication transports. Currently, ZeroMQ supports four types of transports, which are listed in the following table:
Similarly, there's a socket.connect
function to establish a network connection to the peer. Do note that the bind and connect functions are non-blocking. Therefore, code execution will continue until it reaches the send
or recv
functions. Connection and reconnection handling is processed in the background.
There are two functions used to send and receive messages. To send a message, just call a function socket.send,
which accepts at least one argument as a string value. This string will be sent to the other side of the communication. A message can be received with the socket.recv
or socket.recvAll
function. The key difference between these two functions is that socket.recv
will receive only one message. The other function, socket.recvAll
, can receive even multiple parts of a message and glues them together into one string. This is useful mainly when transferring large data over a network. Sending multiple parts of one message can be done with the socket.send
function, while adding a second argument with the zmq.ZMQ_SNDMORE
flag. This flag signals the ZeroMQ library that there is more data to be sent. The following sample code shows the usage pattern for multipart messages:
socket.send('Part one', zmq.ZMQ_SNDMORE) socket.send('Part two', zmq.ZMQ_SNDMORE) socket.send('The last part of a message')
There's a more elegant solution to send multiple parts of a message. You can use the socket.sendMultipart
function, where you can place all parts into a single table and send them in one shot.
socket.sendMultipart({ 'Part one', 'Part two', 'The last part of a message' })
This is especially useful for the router socket type where the peer identifier is sent as a part at the beginning of a message.
The polling object helps to determine when the connection line is ready to receive or send data. Polling in general will effectively block code execution until the line is ready for specified I/O operation. When transferring a lot of messages, it's better to use polling even for sending data. You can achieve this by adding another polling function as follows:
poll.add(socket, zmq.ZMQ_POLLOUT, function(s) -- user code to send a message end)
The polling function will always receive one parameter containing the socket object specific to the connection.
Polling must be started with the poll.start
function, otherwise it will have no effect. You can set up a timeout value by adding a numerical argument to the poll.start
function. The timeout value is expected to be in milliseconds.
You don't have to worry about the message queuing problem as the ZeroMQ library solves this in its separate thread.
18.191.168.203