This assignment will be closed on May 20, 2026 (23:59:59).
You must be authenticated to submit your files

CSE 102 – Tutorial 10 – A Chat Server

Based on an original tutorial by P.-Y. Strub

Sections tagged with a green marginal line are mandatory.

Sections tagged with an orange marginal line are either more advanced or supplementary, and are therefore considered optional. They will help you deepen your understanding of the material in the course. You may skip these sections on a first pass through the tutorial, and come back to them after finishing the mandatory exercises.

Discussion of the tutorials with friends and classmates is allowed, with acknowledgment, but please read the Moodle page on “Collaboration, plagiarism, and AI”.

There are no automated tests for this tutorial, although you should nevertheless upload your files to the submission server (as usual, you can upload as many times as you like).

Introducing async networking

In this tutorial, we are going to work our way towards implementing a simple chat server based on Transports and Protocols from Python’s asyncio library.

An echo server (and client)

We expect you to write your solutions to all of the problems in this section in a file named echo.py and to upload it using the form below.

Upload form is only available when connected

Let’s start with a very simple echo server, which is a simple program that accepts connections from clients, waits for data from them, and writes it back to the clients. The initial code for our echo server is given below. (We have included type hints to make the code easier to understand and modify.)

# --------------------------------------------------------------------
import asyncio
import sys
from typing import cast

# --------------------------------------------------------------------
class EchoServerClientProtocol(asyncio.Protocol):

    def connection_made(self, transport : asyncio.BaseTransport) -> None:
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = cast(asyncio.Transport, transport)

    def data_received(self, data : bytes) -> None:
        # convert the binary data received into a string
        message : str = data.decode()
        print('Data received: {!r}'.format(message))

        # send the same data back
        print('Send: {!r}'.format(message))
        self.transport.write(data)

        print('Close the client socket')
        self.transport.close()

# --------------------------------------------------------------------
try:
    loop = asyncio.get_running_loop()
except RuntimeError:
    loop = asyncio.new_event_loop()

asyncio.set_event_loop(loop)

coro   = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888)
server : asyncio.base_events.Server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))

try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

You should copy this code and save it to a file named echo.py.

Now for some explanations.

The asyncio.Protocol relies on the asyncio event loop to dispatch events from the network. To create a TCP server, we recover the asyncio event loop via the asyncio.get_event_loop() function, and then create the server by calling the .create_server() method on the asyncio event loop. This method takes three arguments:

Once the server has been created, we just tell asyncio to run the server, and wait for the completion of its creation. From there, the server is going to accept new connections from clients and serve them.

Let’s go back to the EchoServerClientProtocol class. As we said, this class is reponsible for the communication with clients, and one instance of that class is created per client. The asyncio.Protocol uses a callback-oriented API. This means that asyncio tells your class about (network) events by calling specific methods of this class.

Here, we use two methods: .connection_made() and .data_received().

If you haven’t already done so, at this point you should launch the server! This is simply a matter of opening a terminal and running the command python echo.py, or alternatively following the instructions below in Spyder.

If you are using Spyder, you cannot launch the server using the default “Run file” command. This will result in an exception like so:

RuntimeError: This event loop is already running

Instead you should run the file by navigating to “Run $ Run in external terminal” from the menu.

To test the echo server you need to run a separate program, called a “client”. If you are familiar with the telnet command and have it installed on your system, then you can use it as a client by running telnet localhost 8888. Otherwise, we have provided the code for a very simple telnet-like client (client.py), which you can download and execute directly in the same way as above. You should get this message:

Connected to 127.0.0.1 at port 8888.
To quit, type "/quit".

You can enter a single line of text, and should see it repeated (“echoed”) back to you. At that point the client will quit after realizing that the server dropped the connection (see point 3 below); to connect to the server again, simply restart another client.

Now we are going to start with a simple modification of the server to modify this behavior:

  1. Modify the echo server so that it transforms the received string to uppercase letters before sending it back to the client. (For instance, if the client sends the line “hello” then the server should respond with the line “HELLO”.) Keep in mind that the data is sent back to the user in binary format. So, after you transform the underlying string message, you will have to encode it again by calling the encode() method.

As previously mentioned, the code for the echo server above makes the assumption that all the data from the client is received on the first call to .data_received(). However, in practice the data may actually only arrive in chunks — for example because the client is sending a lot of data, or because of some networking latency issues. In that case, the .data_received() method is going to be called several times. As such, when developing a client/server protocol, one must design the communication formats so that it is possible for the peers to know when they received all the data. In this tutorial, we are only going to work with text-oriented protocols: when a peer wants to send some data, it sends it using some ASCII format that must fit in one line of text. Hence, a peer knows that it fully received the data when it sees a newline.

  1. Further modify the echo server so that it buffers the data sent by the client until it receives a full line marked by a trailing '\r\n' sequence, before sending the line (converted to uppercase) back to the client. Hint: You can add new data attributes to the EchoServerClientProtocol class for storing the partially received data.

    To test the server, try connecting as before, but now type the escape character Ctrl-D after entering some text (you may have to hit it twice), then entering some more text and typing Ctrl-D again, and so on, eventually ending the line by pressing Enter. For example, a client might have an interaction like the transcript below (where “^D” stands for Ctrl-D, and “^J” stands for Enter):

    Connected to 127.0.0.1 at port 8888.
    To quit, type "/quit".
    Hi,^DI um^D have to tell you something!^J
    HI,I UM HAVE TO TELL YOU SOMETHING!
    Connection closed by server.

    On the side of the server, the log of its interaction with the client may look something like this:

    Connection from ('127.0.0.1', 43632)
    Data received: 'Hi,'
    Current buffer: HI,
    Data received: 'I um'
    Current buffer: HI,I UM
    Data received: ' have to tell you something!\r\n'
    Current buffer: HI,I UM HAVE TO TELL YOU SOMETHING!
    Send: 'HI,I UM HAVE TO TELL YOU SOMETHING!\r\n'
    Close the client socket

Another flaw of our server is that it closes the connection as soon as it receives a line of data. Instead, we’d like the server to continue waiting for new data, echoing lines one-by-one as soon as they are received, until the client closes the connection.

  1. Further modify the echo server to keep echoing the clients lines one-by-one until the client closes the connection. Although you do not need to do anything on the side of the echo server when the client leaves, you could for example print a message that the connection has been closed. To that end, you can add an override for the connection_lost method (see also below).

    The client should now be able to have a longer conversation with the server:

    Connected to 127.0.0.1 at port 8888.
    To quit, type "/quit".
    hey
    HEY
    how are you doing?
    HOW ARE YOU DOING?
    I asked first
    I ASKED FIRST
    stop copying me
    STOP COPYING ME
    i said stop!
    I SAID STOP!
    /quit

A simple chat server

We expect you to write your solutions to all of the problems in this section in a file named chat.py and to upload it using the form below.

Upload form is only available when connected

We are now moving to our chat server. In its simplest form, a chat server is just a super echo server: it receives data from clients and echos it back to them. The only difference is that, instead of sending the data back to the sending client, it echos the data to all the other clients.

For that purpose, we are going to modify the echo server so that all the EchoServerClientProtocol instances share a global state that can be used to store some common data shared by all the clients. Since the intention is to implement a chat server, we’ll also take the opportunity to rename it ChatServerClientProtocol:

# --------------------------------------------------------------------
import asyncio
import sys

# --------------------------------------------------------------------
class ChatServerState:
    pass

# --------------------------------------------------------------------
class ChatServerClientProtocol(asyncio.Protocol):
    def __init__(self, state : ChatServerState):
        self.state = state

    def connection_made(self, transport : asyncio.BaseTransport) -> None:
        # register the client in self.state
        # note that `transport` can be used as a key in a dictionary
        pass

    def connection_lost(self, exc : Exception | None) -> None:
        # unregister the client from self.state
        pass

    def data_received(self, data : bytes) -> None:
        # buffer the received data until a full line is received
        # then, forward that full line to all clients
        pass

# --------------------------------------------------------------------
if sys.version_info < (3, 10):
    loop = asyncio.get_event_loop()
else:
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = asyncio.new_event_loop()

    asyncio.set_event_loop(loop)

state = ChatServerState()

def create_protocol() -> ChatServerClientProtocol:
    global state
    return ChatServerClientProtocol(state)

coro   = loop.create_server(create_protocol, '127.0.0.1', 8888)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))

try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

In the new program, a ChatServerClientProtocol instance has a new data attribute state (of type ChatServerState) that is shared among all the clients. When a client connects, we simply register the new transport into that global state, so that it is then possible to reach all clients’ transport from the methods. Relative to the original version of the echo server, we also added a method .connection_lost() that is called when a client disconnects. We are going to use that callback to unregister clients when they leave the chat server.

  1. Implement the three callbacks of ChatServerClientProtocol to implement a super-echo server that forwards the data it receives to all clients. Some hints are given in comments. For testing your server, you can run several instances of the client program.

From this point, you can start chatting with people that connect to your server. However, there is nothing in the protocol that lets you distinguish messages coming from different clients. We would like to associate each client with a nickname, and to that end we need to make the protocol a bit more sophisticated. When a client connects, it should send first a line containing its nickname, which must be an alphanumerical string. During this period, the client does not receive any message, and if the client tries to use a nickname that is already in use, the server simply closes the connection. Once a valid nickname has been registered, the client starts receiving chat messages, which moreover are always prefixed by the sender’s nickname.

  1. Modify the chat server to implement the nickname protocol.

Since now all clients have a unique identity, it is possible to tag messages for delivery to a specific target user. For that, we are going to extend our protocol again: when the server receives a line of the form:

#nick message...

then it searches for a client whose identity is nick and sends the message privately to that client. If the nickname does not exist, the server simply sends some error message to the sender.

Additionally, we would like for clients to be able to see who is online with the command

!users

which tells the server to send a space-separated list of all the client nicknames.

  1. Implement the private message & clients listing protocol.

We may like to add chat rooms to our server. A chat room is uniquely defined by a name, and users can freely enter/leave chatrooms. When in a chatroom, a client can send a message to it and all the clients in that chatroom will receive that message (prefixed with the chatroom name). It is also possible to get a listing of all the chatrooms and of all the users present in a given chatroom.

We add the following commands to the protocol and ask you to (optionally) implement them:

  • @roomname message: send the message message to the chatroom roomname. If the client is not in the chatroom or if the chatroom does not exist, an error message is returned.

  • !enter roomname: enter the chatroom roomname - the chatroom is created if it does not exist. Does nothing if the client is already part of that chatroom.

  • !leave roomname: leave the chatroom roomname - the chatroom is destroyed if no clients remain in that chatroom. Does nothing if the client is not part of that chatroom.

  • !rooms: send a space-separated list of all the chatrooms.

  • !members roomname: send a space-separated list of all the users of a given chatroom. Send an error message if the chatroom does not exists.

You are also free to extend the protocol with other commands, of course! Please just document them.