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
asphalt command to run an application with a configuration file.
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
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:
The last command activates the virtualenv, meaning the shell will first look for commands in
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)
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)
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
on the first line will cause the task to be adjourned until a line of text has been read from the
client_connected() function receives two arguments: 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
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) run_application(component)
You may have noticed that
ClientComponent inherits from
CLIApplicationComponent instead of
Component and that instead of overriding the
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
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
To send the “Hello” message to the server, run this in the project directory:
python -m echo.client Hello
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
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.