Certain types of applications require low-latency, bidirectional communications. For example, this includes messaging apps and social media networks, which require constant feed updates. If you build these kinds of applications with HTTP, you’re likely to have latency issues. This is because HTTP was never built with two-way messaging in mind. It’s possible to have something resembling a two-way connection via HTTP polling, but this is more of a crutch than a complete solution, as it’s resource-intensive and complicated to set up.

To resolve these kinds of issues, you can use a WebSocket, which is one of the best tools for these communication-heavy applications. WebSockets are much less resource-intensive than polling, and they’re built for two-way communication.

A WebSocket is a wholly different protocol from HTTP. You can see the difference in their URLs. HTTP starts with http://, while WebSocket URIs start with ws://. Instead of working in a request-response model, a WebSocket establishes a communication channel that both parties can use.

Since WebSockets have been around for quite some time, there are comprehensive libraries to help you create WebSocket servers in Python or JavaScript without having to worry about the low-level details.

In this article, we’ll demonstrate how to use Python to create a WebSocket server and a command-line client that will serve as an interface for trying out this server. The server will let multiple users connect to a single room and exchange messages with each other.

Prerequisites

To follow this tutorial, you’ll need a working installation of a recent version of Python 3. We recommend you install the latest version on your machine.

Additionally, the WebSockets protocol supports asynchronous communication, so this tutorial will use asyncio for the server. Because of that, some familiarity with asynchronous programming is an asset.

How to Create a WebSocket Server with Python

Project Setup

To get started, create a new directory called chat_app. Then, open that directory in the code editor of your choice.

For this tutorial, you’ll use three libraries: asyncio (for async programming in Python), websockets (for managing WebSocket connections), and aioconsole (to write and read from console asynchronously).

Although asyncio is available by default in the standard library, you need to install the other two libraries by running the pip install websockets and pip install aioconsole commands on your terminal or command prompt.

After that, create two files: chat_server.py and chat_client.py.

Create the Server

Open the file called chat_server.py. In this file, you’ll place all the code that concerns the WebSocket server — the application responsible for hosting the chat room.

Here’s how the server will work. Clients will join the server and get assigned a random username. Then, they’ll be able to send the server messages, and the server will log these messages by printing them locally. Then, the server will send those messages to each client connected to the server.

To create the server, start by adding import statements for the following libraries in the file:

import asyncio

import websockets

Then, create a global variable for all of your connections:

connected = []

To generate usernames, this tutorial will use a dictionary instead of a real random generator. Add this to the file as a global variable as well:

username_generator = {
    0: "VerdantPostbox",
    1: "PrudentNecktie",
    2: "BreezyCocktail",
    3: "HelpfulPoodle",
    4: "ReassuringSpeedboat"
}

After that, you need to create a handle function that will handle the incoming requests. On a high level, this function will take each incoming WebSocket connection, add it to the list of active connections, and make sure to propagate the messages of each client to all the other clients.

Since this function does a lot, we’ll look at it part by part. But, so that you know what to expect, here’s the full code for the function:

async def handle(websocket):

    user_count = len(connected)
    username = username_generator.get(user_count)
    print(f"{username} has joined")
    connected.append((websocket, username))

    try:
        async for message in websocket:

            formatted_message = f'{username}: {message}'
            print(formatted_message)

            for (user, id) in connected:
                if user != websocket:
                    await user.send(formatted_message)
                else:
                    pass

    except websockets.exceptions.ConnectionClosed:
        disconnected = list(filter(lambda x: x[0] == websocket, connected))

        for user in disconnected:
            connected.remove(user)
            print(f'{user[1]} left')

So what’s happening here?

First, whenever a new connection happens, the handle function needs to assign a random username to the user and print a notification that the user has connected to the server.

Below is the piece of code that is responsible for doing this. It gets the current user count, uses it as the input for the mock generator, prints out the “user has joined” notification, and then appends the WebSocket connection to the list of active connections.

async def handle(websocket):

    user_count = len(connected)
    username = username_generator.get(user_count)
    print(f"{username} has joined")
    connected.append((websocket, username))

connected.append((websocket, username))

After registering a user, the function needs to format and print any messages that it receives through that user’s WebSocket connection.

Below is the code responsible for handling those messages. It loops through the messages in the WebSocket connection.

    async for message in websocket:

        formatted_message = f'{username}: {message}'
        print(formatted_message)

Then, the server needs to send each message to all the other clients that are connected. This can be done by looping through the list of connected users, like this:

for (user, id) in connected:

  if user != websocket:

    await user.send(formatted_message)

  else:

    pass

But what if a user gets disconnected? To handle that, you need to wrap the messaging logic in a try/except block that will catch the exception of the user disconnecting and remove them from the connection list:

    try:
        async for message in websocket:

            formatted_message = f'{username}: {message}'
            print(formatted_message)

            for (user, id) in connected:
                if user != websocket:
                    await user.send(formatted_message)
                else:
                    pass

    except websockets.exceptions.ConnectionClosed:
        disconnected = list(filter(lambda x: x[0] == websocket, connected))

        for user in disconnected:
            connected.remove(user)
            print(f'{user[1]} left')

Once you’ve added all of these code snippets to your handle function, it’s ready.

Now, you need to make sure that this function is served on a location and port. To do that, you’ll use websockets.serve and asyncio.

First, create a variable for the server function. The following variable will contain a function to serve the server locally on port 8765:


start_server = websockets.serve(handle, "localhost", 8765)

Then, use asyncio to run it forever using the following commands:


asyncio.get_event_loop().run_until_complete(start_server)

asyncio.get_event_loop().run_forever()

The simple chat server is now done! Here’s the full code:

# chat_server.py

import asyncio
import websockets

connected = []

username_generator = {
0: "VerdantPostbox",
1: "PrudentNecktie",
2: "BreezyCocktail",
3: "HelpfulPoodle",
4: "ReassuringSpeedboat"
}

async def handle(websocket):

user_count = len(connected)
username = username_generator.get(user_count)
print(f"{username} has joined")
connected.append((websocket, username))

try:
async for message in websocket:

formatted_message = f'{username}: {message}'
print(formatted_message)

for (user, id) in connected:
if user != websocket:
await user.send(formatted_message)
else:
pass

except websockets.exceptions.ConnectionClosed:
disconnected = list(filter(lambda x: x[0] == websocket, connected))

for user in disconnected:
connected.remove(user)
print(f'{user[1]} left')

start_server = websockets.serve(handle, "localhost", 8765)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

Test the Server

To ensure everything is working correctly, test the server out with the websockets interactive client.

To do so, start by running the server using the python3 chat_server.py command in your terminal. This command will serve it locally via port 8765.

Then, open two other terminals. In those, you need to run python3 -m websockets ws://localhost:8765/.

After that, you’ll be able to type messages in both of those terminals and have them appear in the other terminal. All of the messages will also be printed out in the terminal in which you have the server running.

Create the Client

To make better use of the server, you need a client. For this tutorial, you’ll create one with Python.

The client will be quite similar to the server, but in contrast to the server, it needs to handle both user input and read/write functionality on the WebSocket.

Create the client in the chat_client.py file. Start by importing the required libraries:

import asyncio
import websockets
import aioconsole

Then, you need to write the main function.

First, define the URI of the server you’re connecting to. The client will connect to that URI and print an announcement in the console.


async def main():
uri = "ws://localhost:8765"
print("You can now chat with other people in the room!")

After that, connect to the server. This can be done with the websockets.connect method. After the client connects, it will simultaneously handle both the loop of receiving messages and the loop of sending messages with asyncio.gather:

    async with websockets.connect(uri) as websocket:
        await asyncio.gather(
            received_message_handler(websocket),
            sent_message_handler(websocket)
        )

But, right now, there are no functions with such names. You’ll need to create them. Remember to put them on top of the main() function in the file.

First, create the received_message_handler. It’s a simple loop that will wait for a message and then print it out asynchronously on the console. The snippet aprint is the same as print, but asynchronous.

async def received_message_handler(websocket):
    while True:
        message = await websocket.recv()
        await aioconsole.aprint(message)

Then, create the sent_message_handler. It’s also a simple loop that will wait for a message to be typed in the console and then send it to the server. Again, ainput is the same as input, but asynchronous.

async def sent_message_handler(websocket):
    while True:
        message = await aioconsole.ainput()
        await websocket.send(message)

 

Then, because you need the client to run until you stop it, add this at the bottom of the code:

asyncio.get_event_loop().run_until_complete(main())

asyncio.get_event_loop.run_forever()

The client is now ready! The full client code is below:

# chat_client.py

import asyncio
import websockets
import aioconsole


async def received_message_handler(websocket):
    while True:
        message = await websocket.recv()
        await aioconsole.aprint(message)


async def sent_message_handler(websocket):
    while True:
        message = await aioconsole.ainput()
        await websocket.send(message)


async def main():
    uri = "ws://localhost:8765"
    print("You can now chat with other people in the room!")

    async with websockets.connect(uri) as websocket:
        await asyncio.gather(
            received_message_handler(websocket),
            sent_message_handler(websocket)
        )

asyncio.get_event_loop().run_until_complete(main())
asyncio.get_event_loop.run_forever()

 

asyncio.get_event_loop().run_until_complete(main())

asyncio.get_event_loop.run_forever()

You can try the server and the client together by running them on different terminals. First, open a terminal and run python3 chat_server.py. Then, open two more terminals and run python3 chat_client.py. You’ll be able to send messages between them and have them displayed with their usernames.

Conclusion

Now, you know how to create a simple WebSocket chat server as well as a WebSocket client that can use that chat server.

A WebSocket server is very similar to a regular HTTP server, with the major difference being the ability of the server to push messages to connected users. In the case of this tutorial, it was triggered by users typing in the chat, but it could also have used an event like a ping message every minute.

A WebSocket is one of the best tools for creating applications that require bidirectional, real-time communication between the client and the server. This includes things like chat applications and real-time social media.

The Python websockets library makes it extremely easy to create WebSockets servers. It also integrates with Django well, so you can use it as an add-on to your web projects without breaking a sweat.

Avatar photo
Janhavi Sathe
Associate Solutions Engineer