Inspirel banner

Programming Distributed Systems with YAMI4

9.1.5 Python

The Python client and server are simple scripts that only require the visibility of the YAMI4 implementation files from the place where the scripts are started - please see the chapter about library structure and compilation for instructions on how to ensure this is the case.

Assuming that the environment is properly set up, the following invocation is enough to start the server program with the shortest possible target:

$ python3 server.py 'tcp://*:*'

The client program is entirely implemented in the client.py file, which is dissected in detail below.

First the necessary import statements:

import sys
import yami

The yami module is the single module encapsulating all YAMI4 definitions.

The sys module is needed to read text from the standard input as well as to access the command-line program arguments.

Initial actions in the client program check whether there is one required argument describing server destination address:

if len(sys.argv) != 2:
    print("expecting one parameter: server destination")
    exit()

server_address = sys.argv[1]

After the program arguments are processed, client can create its own YAMI agent.

try:
    with yami.Agent() as client_agent:

Above, the context manager is used to enclose the scope of the client_agent object and to ensure that it will be properly closed when no longer used. This is the recommended usage pattern for all YAMI4 objects that manage their own internal resources.

The whole logic of the client program is devoted to reading lines of text from standard input:

        # read lines of text from standard input
        # and post each one for transmission

        for line in sys.stdin:

For each line of text, a new message is sent to the server with parameters object that contains the given text. In this example a built-in dictionary object is used instead of dedicated Parameters class, as it is perfectly sufficient here:

            # the "content" field name is arbitrary,
            # but needs to be recognized at the server side

            parameters = {"content":line.rstrip()}

The data entry is named "content", but the name does not matter as long as it is properly recognized by the server. In other words, the choice of entry names forms a kind of contract between client and server and can be compared to the naming of method parameters in object-oriented programming.

Everything is now ready for sending the message:

            client_agent.send_one_way(
                server_address, "printer", "print", parameters)

The message is sent as one-way, which means that there is no feedback on its progress.

The arguments to the send_one_way() function are:

It should be noted that the message is being sent in background, so the user task can immediately continue and read another line of text.

The agent object manages system resources and needs to be explicitly closed when no longer needed - here, this process is automated by the context manager.

The program code concludes with rudimentary exception handling:

except Exception as e:
    print("error:", e)

Another version of the client program (client_synchronous.py) synchronizes each message separately and allows to guarantee that all lines of text are really delivered to the server, so that when the client program finishes there are no messages pending in its outgoing queue.

The code of that alternative client is very similar to the previous version.

The majority of the program is implemented as before:

import sys
import yami

if len(sys.argv) != 2:
    print("expecting one parameter: server destination")
    exit()

server_address = sys.argv[1]

try:
    with yami.Agent() as client_agent:

        # read lines of text from standard input
        # and post each one for transmission

        for line in sys.stdin:

            # the "content" field name is arbitrary,
            # but needs to be recognized at the server side

            parameters = {"content":line.rstrip()}

The only implementation difference is the part that sends the message - instead of posting a one-way message to the queue, a regular message object is created and later used for synchronization with its delivery.

            with client_agent.send(
                server_address, "printer", "print", parameters) as msg:

Similarly to the use of agent object, the outgoing message object is also created within the context manager to ensure its proper cleanup.

The user code can then synchronize itself with the message delivery by waiting on its transmission condition:

                msg.wait_for_transmission()

The above call ensures that the user code will not proceed until the line of text is sent to the server - if this was the last line, the program can finish immediately by exiting the main loop without any risk of losing pending messages.

Program code concludes with simple exception handling:

except Exception as e:
    print("error:", e)

The server program (server.py) is also implemented in a single file and begins with appropriate import statements:

import sys
import yami

At the beginning the server program verifies whether it was given a single required argument that is interpreted as a server target:

if len(sys.argv) != 2:
    print("expecting one parameter: server destination")
    exit()

server_address = sys.argv[1]

Then the message handler is implemented as a simple call() function that accepts the incoming message object:

def call(msg):
    # extract the content field
    # and print it on standard output

    params = msg.get_parameters()
    print(params["content"])


In principle any callable entity can be used as a message handler.

In this example the call() function has to print the line of text that was sent by remote client on the screen - here this process is straightforward since the parameters object can be accessed as a ``subcomponent'' of the incoming message object. No other processing is performed and in particular no feedback - neither reply nor rejection - is sent back to the client.

Then the agent object is created and a listener is added to it with the given target name:

try:
    with yami.Agent() as server_agent:

        resolved_address = server_agent.add_listener(server_address)

The resolved server address is printed on the screen to show how it was expanded and to allow the user to pass it properly to the client program:

        print("The server is listening on", resolved_address)

As the last preparatory step, the ``printer'' object is registered with the already defined message handler function:

        server_agent.register_object("printer", call)

After creating the listener and registering the object the server is ready for operation - every message that will arrive from remote clients will be routed to the registered message handler and the line of text that was sent by client will be printed on the screen.

In order to allow the agent to operate in background the main thread effectively stops by executing dummy input operation. This, or more often some form of infinite loop, is typical for servers that are entirely driven by remote messages:

        # block
        dummy = sys.stdin.read()

The server code finishes with simple exception handling:

except Exception as e:
    print("error:", e)