Case Study

Asynchronous TCP Socket Server

Zero-dependency Python asyncio chat gateway — raw TCP sockets, persistent client state, and real-time broadcast without threading overhead.

  • Python
  • asyncio
  • StreamReader
  • StreamWriter
  • python
  • asyncio
  • tcp
  • networking
  • sockets
  • zero-dependency

Overview

Most modern web developers build on top of massive abstraction layers, relying entirely on REST APIs or HTTP frameworks like Express and FastAPI. Building a raw TCP server strips away those abstractions, proving you understand network protocols at the foundational socket level.

This implementation uses Python’s asyncio library to build a non-blocking, multi-user chat server. It mimics the underlying architecture of a classic text-based Multi-User Dungeon (MUD) by keeping a persistent state of connected users and broadcasting messages in real time over an event loop — all without threading overhead.

Part of the DIY build series: Projects 1 · 2 · 3 · 4 · 5 · 6 · 7.

What it implements

  • Raw TCP serverasyncio.start_server on 0.0.0.0:8888, no HTTP layer
  • Persistent client state — in-memory clients dict mapping writers to usernames
  • Async broadcastawait writer.drain() yields to the event loop during I/O waits
  • Connection lifecycle — handshake, read loop, graceful cleanup on disconnect
  • Multi-client concurrency — one coroutine per connection, no threading module

Project setup

mkdir async-tcp-gateway && cd async-tcp-gateway
touch tcp_server.py

The code (tcp_server.py)

State lives purely in memory. asyncio pauses and resumes execution via await whenever network I/O is waiting:

#!/usr/bin/env python3
import asyncio

# --- ANSI Terminal Colors ---
CLR_RESET = "\033[0m"
CLR_CYAN = "\033[96m"
CLR_GREEN = "\033[92m"
CLR_YELLOW = "\033[93m"
CLR_GRAY = "\033[90m"
CLR_MAGENTA = "\033[95m"

class AsyncChatGateway:
    def __init__(self):
        self.clients = {}

    async def broadcast(self, message: str, sender_writer=None):
        """Pushes a message to all connected TCP clients asynchronously."""
        for writer in list(self.clients.keys()):
            if writer != sender_writer:
                try:
                    writer.write(message.encode('utf-8'))
                    await writer.drain()
                except ConnectionError:
                    self._remove_client(writer)

    def _remove_client(self, writer):
        """Cleans up memory when a connection dies."""
        if writer in self.clients:
            del self.clients[writer]

    async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
        """The coroutine spawned for every new network connection."""
        addr = writer.get_extra_info('peername')

        welcome_msg = f"{CLR_CYAN}Welcome to the davidcole.cloud Gateway.{CLR_RESET}\n"
        welcome_msg += "Enter your handle, traveler: "
        writer.write(welcome_msg.encode('utf-8'))
        await writer.drain()

        name_line = await reader.readline()
        if not name_line:
            writer.close()
            return

        username = name_line.decode('utf-8').strip() or "Guest"
        self.clients[writer] = username
        print(f"[{addr[0]}:{addr[1]}] User '{username}' connected.")

        await self.broadcast(f"{CLR_YELLOW}*** {username} has entered the realm ***{CLR_RESET}\n", writer)

        try:
            while True:
                data = await reader.readline()
                if not data:
                    break

                message = data.decode('utf-8').strip()
                if message:
                    formatted_msg = f"{CLR_GREEN}{username}{CLR_RESET}: {message}\n"
                    await self.broadcast(formatted_msg, writer)

        except asyncio.CancelledError:
            pass
        except ConnectionResetError:
            pass
        finally:
            self._remove_client(writer)
            print(f"[{addr[0]}:{addr[1]}] User '{username}' disconnected.")
            await self.broadcast(f"{CLR_GRAY}*** {username} has vanished into the aether ***{CLR_RESET}\n")

            writer.close()
            await writer.wait_closed()

async def main():
    HOST, PORT = '0.0.0.0', 8888
    gateway_state = AsyncChatGateway()

    server = await asyncio.start_server(gateway_state.handle_client, HOST, PORT)

    print(f"{CLR_MAGENTA}🚀 Async TCP Gateway listening on {HOST}:{PORT}{CLR_RESET}")
    print("Awaiting concurrent telnet connections...")

    async with server:
        await server.serve_forever()

if __name__ == '__main__':
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nGateway shutting down. Connections severed.")

Execution & testing

You need the server plus multiple clients simultaneously.

1. Launch the server

chmod +x tcp_server.py
./tcp_server.py

2. Connect client one

In a second terminal:

telnet 127.0.0.1 8888

Or use nc if telnet is not installed:

nc 127.0.0.1 8888

3. Connect client two

Run the same command in a third terminal.

Type messages in terminal #2 and watch them appear instantly in terminal #3. The server terminal logs IP addresses and routing activity.


Why this shines on a portfolio

  1. Mastery of the event loopawait reader.readline() means the server does not freeze waiting for one user; it switches to serve other connected clients immediately.
  2. State management — unlike stateless HTTP, TCP sockets require persistent memory (self.clients). Clean connection teardown without leaks is a core backend skill.