Tutorial 1: Getting your feet wet – a simple echo server and client

This tutorial will get you started with Asphalt development from the ground up. You will learn how to build a simple network server that echoes back messages sent to it, along with a matching client application. It will however not yet touch more advanced concepts like using the asphalt command to run an application with a configuration file.

Prerequisites

Asphalt requires Python 3.7 or later. You will also need to have the venv module installed for your Python version of choice. It should come with most Python installations, but if it does not, you can usually install it with your operating system’s package manager (python3-venv is a good guess).

Setting up the virtual environment

Now that you have your base tools installed, it’s time to create a virtual environment (referred to as simply virtualenv later). Installing Python libraries in a virtual environment isolates them from other projects, which may require different versions of the same libraries.

Now, create a project directory and a virtualenv:

mkdir tutorial1
cd tutorial1
python -m venv tutorialenv
source tutorialenv/bin/activate

On Windows, the last line should be:

tutorialenv\Scripts\activate

The last command activates the virtualenv, meaning the shell will first look for commands in its bin directory (Scripts on Windows) before searching elsewhere. Also, Python will now only import third party libraries from the virtualenv and not anywhere else. To exit the virtualenv, you can use the deactivate command (but don’t do that now!).

You can now proceed with installing Asphalt itself:

pip install asphalt

Creating the project structure

Every project should have a top level package, so create one now:

mkdir echo
touch echo/__init__.py

On Windows, the last line should be:

copy NUL echo\__init__.py

Creating the first component

Now, let’s write some code! Create a file named server.py in the echo package directory:

from asphalt.core import Component, Context, run_application


class ServerComponent(Component):
    async def start(self, ctx: Context) -> None:
        print('Hello, world!')

if __name__ == '__main__':
    component = ServerComponent()
    run_application(component)

The ServerComponent class is the root component (and in this case, the only component) of this application. Its start() method is called by run_application when it has set up the event loop. Finally, the if __name__ == '__main__': block is not strictly necessary but is good, common practice that prevents run_application() from being called again if this module is ever imported from another module.

You can now try running the above application. With the project directory (tutorial) as your current directory, do:

python -m echo.server

This should print “Hello, world!” on the console. The event loop continues to run until you press Ctrl+C (Ctrl+Break on Windows).

Making the server listen for connections

The next step is to make the server actually accept incoming connections. For this purpose, the asyncio.start_server() function is a logical choice:

from asyncio import start_server

from asphalt.core import Component, run_application


async def client_connected(reader: StreamReader, writer: StreamWriter) -> None:
    message = await reader.readline()
    writer.write(message)
    writer.close()
    print('Message from client:', message.decode().rstrip())


class ServerComponent(Component):
    async def start(self, ctx: Context) -> None:
        await start_server(client_connected, 'localhost', 64100)

if __name__ == '__main__':
    component = ServerComponent()
    run_application(component)

Here, asyncio.start_server() is used to listen to incoming TCP connections on the localhost interface on port 64100. The port number is totally arbitrary and can be changed to any other legal value you want to use.

Whenever a new connection is established, the event loop launches client_connected() as a new Task. Tasks work much like green threads in that they’re adjourned when waiting for something to happen and then resumed when the result is available. The main difference is that a coroutine running in a task needs to use the await statement (or async for or async with) to yield control back to the event loop. In client_connected(), the await on the first line will cause the task to be adjourned until a line of text has been read from the network socket.

The client_connected() function receives two arguments: a StreamReader and a StreamWriter. In the callback we read a line from the client, write it back to the client and then close the connection. To get at least some output from the application, the function was made to print the received message on the console (decoding it from bytes to str and stripping the trailing newline character first). In production applications, you will want to use the logging module for this instead.

If you have the netcat utility or similar, you can already test the server like this:

echo Hello | nc localhost 64100

This command, if available, should print “Hello” on the console, as echoed by the server.

Creating the client

No server is very useful without a client to access it, so we’ll need to add a client module in this project. And to make things a bit more interesting, we’ll make the client accept a message to be sent as a command line argument.

Create the file client.py file in the echo package directory as follows:

import sys
from asyncio import open_connection

from asphalt.core import CLIApplicationComponent, Context, run_application


class ClientComponent(CLIApplicationComponent):
    def __init__(self, message: str):
        super().__init__()
        self.message = message

    async def run(self, ctx: Context) -> None:
        reader, writer = await open_connection('localhost', 64100)
        writer.write(self.message.encode() + b'\n')
        response = await reader.readline()
        writer.close()
        print('Server responded:', response.decode().rstrip())

if __name__ == '__main__':
    component = ClientComponent(sys.argv[1])
    run_application(component)

You may have noticed that ClientComponent inherits from CLIApplicationComponent instead of Component and that instead of overriding the start() method, run() is overridden instead. This is standard practice for Asphalt applications that just do one specific thing and then exit.

The script instantiates ClientComponent using the first command line argument as the message argument to the component’s constructor. Doing this instead of directly accessing sys.argv from the run() method makes this component easier to test and allows you to specify the message in a configuration file (covered in the next tutorial).

When the client component runs, it grabs the message to be sent from the list of command line arguments (sys.argv), converts it from a unicode string to a bytestring and adds a newline character (so the server can use readline()). Then, it connects to localhost on port 64100 and sends the bytestring to the other end. Next, it reads a response line from the server, closes the connection and prints the (decoded) response. When the run() method returns, the application exits.

To send the “Hello” message to the server, run this in the project directory:

python -m echo.client Hello

Conclusion

This covers the basics of setting up a minimal Asphalt application. You’ve now learned to:

  • Create a virtual environment to isolate your application’s dependencies from other applications

  • Create a package structure for your application

  • Start your application using run_application()

  • Use asyncio streams to create a basic client-server protocol

This tutorial only scratches the surface of what’s possible with Asphalt, however. The second tutorial will build on the knowledge you gained here and teach you how to work with components, resources and configuration files to build more useful applications.