Creating a basic client-server architecture

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.

Getting ready

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:

Getting ready

How to do it…

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.

The server part

This part of the communication application will wait for a request and replies with a message.

  1. First, you'll need to include the LuaZMQ library namespace with the following line of code:
    local zmq = require 'zmq'
  2. Now, you can initialize the context object for the ZeroMQ library and use it to define the kind of network socket you'll be using. In this case, the server path will use the zmq.ZMQ_REP constant that corresponds to the reply part.
    local context = assert(zmq.context())
    local socket = assert(context.socket(zmq.ZMQ_REP))
  3. This will prepare the socket object that can be used to prepare one side of the network communication:
    assert(socket.bind('tcp://*:12345'))
  4. After this step, your application will expect network communication over the TCP/IP protocol at the network service port 12345. Of course, you can change the port number to suit your needs, but be sure to change the port number on the client side as well.
  5. From this point, you can use the 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.
  6. First you need to create a poll object:
    local poll = zmq.poll()
  7. Now you can assign a function to the poll, which will be used after you receive a message:
    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)
  8. This will print a received message and sends a reply to the Request side. For this to work, you'll have to start polling with the following lines:
    local timeout = 500 -- 500ms
    while (true) do
      poll.start(timeout)
    end
  9. Notice that there's a timeout variable that contains a time in milliseconds to wait before doing other things. The whole process of waiting and replying is repeated indefinitely until you decide to break the cycle.
  10. Lastly, you need to close a socket object after use with a single line of code:
    socket.close()

This concludes the server part of this recipe.

The client part

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.

How it works…

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:

Transport names

Description

tcp

This is a unicast transport over TCP

ipc

This is an inter-process communication transport with socket files (Unix-like systems only)

inproc

This is an in-process or inter-thread communication transport over shared memory area

pgm or egm

This is a reliable multicast transport that uses PGM

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.168.203