Asynchronous Server/Client with Python [0x01]: Starting our Server

Advertisements

In this project series, we are going to create a group chat using Python.   One very important tool we’re going to utilize here is called asyncio, which will allow us to take advantage of asynchronous programming. Asynchronous programming is particularly well suited to projects that need to be able to handle many different tasks simultaneously.   In a group chat, we can expect that the server will need to be able to juggle multiple people typing at once, sending read receipts, and pushing out notifications to individual users all at the same time.  Before we jump right into the code, let’s take a step back and get an overview of what we’re building.

Click here for part 0x02: Logging
Github link and other useful documentation/references can be found below in the “Sauce, References, & Documentation” section!

What is Asynchronous Programming?

First, let’s revisit a concept you may not know that you’re already familiar with; synchronous programming. Synchronous programming is the execution of code in which each task is completed one after the other.  If you were making a PB&J, you would get out the bread. Then you would put the peanut butter on one slice. Then you’d put up the peanut butter.  Then you would get the jelly out of the fridge.  Then you would put jelly on the other slice.  Then you would put them together.  Most people will  learn and become comfortable with synchronous programming first.

In asynchronous programming, one task can be executed, but the computer does not need to wait for that task to finish before moving onto the next one.  Perhaps a diagram can help you visualize synchronous vs asynchronous programming:

“WTF is Synchronous and Asynchronous!” (https://medium.com/from-the-scratch/wtf-is-synchronous-and-asynchronous-1a75afd039df)

Asynchronous programming is incredibly useful for server/client code, which is why we are going to use it for this project.  There will be a lot of tasks that need to be carried out essentially at the same time, and the only real way to accomplish this is by shifting our attention to new tasks while others are running “behind-the-scenes”.  When I think of asynchronous programming, I like to think of it as a lot like cooking a meal.   For example, I might turn on the front burner to heat up my pot of tomato sauce, and then immediately redirect my attention to filling a different pot with water to boil the noodles. After the water starts heating up, I can start buttering some bread. Each of these tasks are being initialized, but I don’t have to wait until one task is completed before I start the next since some of them are then simply a matter of waiting.  As all of these tasks are happening, I might occasionally check the status of each of them to decide whether or not I need to turn down the heat or end the task and proceed to the next step for that item.

In this case, our server will be juggling a bunch of different tasks in a similar manner.  The server we write will be able to continuously listen for any incoming connections, receive messages from clients, send messages to clients, and maybe even handle any server-side commands that an admin might want to execute to manage their server.

Advertisements

Getting Started

For this project, we will only be working on our Server.py file. Let’s go ahead and add the following to the top of our file:

import asyncio
import sys

I’ll also be developing this project using the following (note what is required has been marked):

  • Python 3.9.1 [Required] – Needed for asyncio
  • Visual Studio Code
  • Tmux

Creating the Server

To get started, we’re going to first create our Server’s constructor, which will take in a host ip, a port, and asyncio’s event loop for handling our tasks:

class Server:
    def __init__(self, ip: str, port: int, loop: asyncio.AbstractEventLoop):
        '''
        Parameters
        ----------
        ip : str
            IP that the server will be using
        port : int
            Port that the server will be using
        ----------
        '''
        self.__ip: str = ip
        self.__port: int = port
        self.__loop: asyncio.AbstractEventLoop = loop

        print(f"Server Initialized with {self.ip}:{self.port}")

    @property
    def ip(self):
        '''
        The IP of the server in string format.
        '''
        return self.__ip

    @property
    def port(self):
        '''
        The port of the server in int format.
        '''
        return self.__port

    @property
    def loop(self):
        return self.__loop

Now that we have our Server class setup, let’s go ahead and set up the beginning of our program so that we can start testing our code right away!

We are going to have two command-line arguments that we will input: the host ip and the port. We’ll first do a quick and dirty check to ensure that the user input has the correct number of arguments.  Once that is passed, we will  create our event loop using asyncio, and then we will pass the provided host ip, port, and the created event loop into our Server:

if __name__ == '__main__':
    if len(sys.argv) < 3:
        sys.exit(f"Usage: {sys.argv[0]} HOST_IP PORT")

    loop = asyncio.get_event_loop()
    server = Server(sys.argv[1], sys.argv[2], loop)

Now, we can quickly test a few things to make sure our program is working as expected:

So far, so good! The screenshot above shows that our program is only proceeding forward if the user provides the correct number of arguments. Additional checks could likely be added to make sure the host ip and port are received in the proper formats, but for the sake of time, we won’t be worrying about those here. The screenshot also shows that with the correct arguments, the server is being constructed, as indicated by our print message!

Now that we have our Server class started, the real fun can begin. Let’s go ahead and define the function that will be used to start our server using our event loop.

    def start_server(self):
        '''
        Starts the server on the IP and PORT.
        '''
        try:
            self.server = asyncio.start_server(
                self.accept_client, self.ip, self.port
            )
            self.loop.run_until_complete(self.server)
            self.loop.run_forever()
        except Exception as e:
            print(e)
        except KeyboardInterrupt:
            print("Keyboard Interrupt Detected. Shutting down!")

What we’re doing here is creating a server object on line 7 using asyncio.start_server().

 Asyncio will take in three arguments:

  • Arg 1: Client Accepted Callback – A function that we will define that determine how our event loop handles new client connections to the server
  • Arg 2: IP – The IP we provided in __init__ above
  • Arg 3: Port – The Port we provided in __init__ above

Asyncio will use these arguments to create our socket server.   We will save that server object in self.server. 

Moving forward, we then take our server object and start our event loop with lines 10 – 11.

I went ahead and tossed this section of code within a try/except. In the event of an error, I’ll just print that to the screen for now. I added a catch for KeyboardInterrupt too because I thought it might be useful to know if/when the server crashes because of CTRL+C (which I will probably be using a lot). In the event that any exception occurs, the server will shutdown. For now, this will be the end of our program because the Event Loop will no longer be running.

Going back to our Client Accepted Callback referenced in Arg 1 of asyncio.start_server(), you may have noticed that we do not have self.accept_client() defined. We will now take a moment to code up our client accepting and handling functions.

 I’ll go ahead and provide the code for those two together:

Advertisements
    def accept_client(self, client_reader: asyncio.StreamReader, client_writer: asyncio.StreamWriter):
        '''
        Callback that is used when server accepts clients

        Parameters
        ----------
        client_reader : asyncio.StreamReader
            StreamReader generated by asyncio.start_server.
        client_writer : asyncio.StreamWriter
            StreamWriter generated by asyncio.start_server.
        ----------
        '''
        task = asyncio.Task(self.handle_client(client_reader, client_writer))
        client_ip = client_writer.get_extra_info('peername')[0]
        client_port = client_writer.get_extra_info('peername')[1]
        print(f"New Connection: {client_ip}:{client_port}")

    async def handle_client(self, client_reader: asyncio.StreamReader, client_writer: asyncio.StreamWriter):
        '''
        Handles incoming messages from client

        Parameters
        ----------
        client_reader : asyncio.StreamReader
            StreamReader generated by asyncio.start_server.
        client_writer : asyncio.StreamWriter
            StreamWriter generated by asyncio.start_server.
        ----------
        '''
        while True:
            client_message = str((await client_reader.read(255)).decode('utf8'))

            if client_message.startswith("quit"):
                break

            print(f"{client_message}")

            await client_writer.drain()

        print("Client Disconnected!")

With this code, our accept_client() callback will be called every time our server receives a new connection. Our callback will then put together a new task for our event loop, which will tell our program how we want each of our clients handled.  In this case, our clients will be handled with handle_client(). Handle_client() will take in the generated StreamReader and StreamWriter and listen to any messages received from a client. If a client sends a message to the server, that message will be outputted to the server. If a client sends a “quit” message, the program will then complete the loop and disconnect from our server.

You may also notice that handle_client() is not a regular function because it has the “async” keyword. As you might be able to infer, this means that it is an asynchronous function. This function will be capable of executing in the background in the event that we need to “await” any results. In this function, we do exactly just that on line 31, when we wait for client input. Keep in mind that a client might not always have a message for our server to read, so when our code hits this line it will “wait” in the background until results are ready for us to process.

At this point, our server is now capable of accepting and handling multiple clients. To test this locally, I will be connecting to localhost (127.0.0.1) on port 10000. I will use netcat in separate terminals to connect to my server. We can see this in action here:

In our test, we can see that everything  is working exactly as we programmed it so far.  However, you may notice that the clients are not able to see each other’s messages. In fact, as of right now, only the server is getting any meaningful information. That is because we’ll need to keep adding on to our server for additional features, like broadcasting messages to all of the clients. It might also be useful to modify our code to help us identify who our users are as they chat. These are all features I plan to cover in future posts, but this is a solid start.  Stay tuned for the next installment!

Conclusion 0x01

As the title of this post might suggest, this is the first part of a coding series that is still in the works. I sincerely hope that if you’ve made it this far into my post that you’ve enjoyed it and have been able to find the content informative in some way. I always try to push myself to learn something new everyday, so if you notice any errors or code smells above, I’d encourage you to please point them out to me so that I can correct them accordingly.

In part 0x02, I will probably take a brief step back from Server/Client and add a logging system to our server. This code is going to get very large very quickly, so having a good logging system established early on will help us debug any problems that may crop up along way.

Click here for part 0x02: Logging

As always, if you liked what you read and you’d like to support me, please consider buying me some coffee! Every cup of coffee sent my way helps me stay awake after my full-time job so that I can produce more high quality blog posts! Any and all support would be greatly appreciated!

Ballad Serial — Trans Arsonist Art Network
Advertisements

Sauce, References, & Documentation

Github: https://github.com/D3ISM3/Python-Asynchronous-Server-Client
Python Asyncio Documentation: https://docs.python.org/3/library/asyncio.html

10 thoughts on “Asynchronous Server/Client with Python [0x01]: Starting our Server

  1. Howdy! I could have sworn I’ve been to your blog before but after looking at many
    of the articles I realized it’s new to me. Nonetheless, I’m
    certainly delighted I found it and I’ll be book-marking it and checking back often!

    Like

Leave a comment