Simple TCP Listener in 50 Lines or Less

Simple TCP Listener — Minimal Example in PythonA TCP listener is the foundation of many networked applications: it waits for incoming TCP connections, accepts them, and communicates with connected clients. This article shows a minimal, clear Python example of a TCP listener, explains the code line-by-line, covers common extensions (concurrency, error handling, basic protocols), and gives tips for testing and debugging. The goal is practical: you’ll be able to run the example, understand how it works, and adapt it for small tools, prototypes, or learning purposes.


Why a minimal TCP listener?

  • Simple: Focuses on the essential concepts without framework overhead.
  • Educational: Demonstrates socket basics — binding, listening, accepting, sending/receiving.
  • Practical: Easy to extend for real use (concurrency, TLS, authentication, simple protocols).

Minimal synchronous TCP listener (Python 3 — complete example)

#!/usr/bin/env python3 import socket HOST = '127.0.0.1'   # localhost; use '0.0.0.0' to listen on all interfaces PORT = 65432         # non-privileged port > 1023 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:     s.bind((HOST, PORT))     s.listen()     print(f"Listening on {HOST}:{PORT} ...")     conn, addr = s.accept()     with conn:         print('Connected by', addr)         while True:             data = conn.recv(1024)             if not data:                 break             print('Received:', data)             conn.sendall(data)  # echo back 

This script implements a minimal TCP echo server: it accepts one connection, echoes back any received data, and exits when the client closes the connection.


Line-by-line explanation

  • import socket
    Loads the standard library socket module, which exposes low-level networking primitives.

  • HOST = ‘127.0.0.1’ / PORT = 65432
    Choose an address and port. Use loopback for local testing. Ports under 1024 usually require elevated privileges.

  • socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    Creates a TCP/IPv4 socket. AF_INET means IPv4; SOCK_STREAM selects the TCP protocol.

  • s.bind((HOST, PORT))
    Associates the socket with the chosen address and port. If the port is in use, bind raises an error.

  • s.listen()
    Puts the socket into listening mode, ready to accept connections. You can pass a backlog value, e.g., s.listen(5), to limit queued connections.

  • conn, addr = s.accept()
    Blocks until an incoming connection arrives. Returns a new socket object (conn) for that client and the client’s address tuple (addr).

  • with conn:
    Ensures the client socket is closed automatically when the block exits.

  • data = conn.recv(1024)
    Reads up to 1024 bytes from the client. recv returns b” when the client cleanly closes the connection.

  • conn.sendall(data)
    Sends data back to the client, ensuring all bytes are transmitted.


Running and testing the listener

  1. Save the script as simple_listener.py and run: python3 simple_listener.py

  2. From another terminal, connect with netcat (nc) or telnet: nc 127.0.0.1 65432 Type text and hit Enter — the server will echo it back.

  3. To stop the server: close the client connection (Ctrl+C in the client or close netcat). The server in this minimal example will exit after the connection ends. For persistent servers, wrap accept() in a loop.


Making it persistent (handle multiple sequential connections)

Wrap accept in a loop so the server continues accepting new clients:

while True:     conn, addr = s.accept()     with conn:         print('Connected by', addr)         while True:             data = conn.recv(1024)             if not data:                 break             conn.sendall(data) 

This accepts clients one at a time, serving each to completion before accepting the next.


Adding concurrency (serve multiple clients simultaneously)

For simple concurrency, use threads:

import threading def handle_client(conn, addr):     with conn:         print('Connected by', addr)         while True:             data = conn.recv(1024)             if not data:                 break             conn.sendall(data) with socket.socket(...) as s:     s.bind((HOST, PORT))     s.listen()     while True:         conn, addr = s.accept()         thread = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)         thread.start() 

Threads let each client be handled independently. For high concurrency or async-friendly designs, consider asyncio or a process pool.


Asyncio example (modern, scalable)

import asyncio async def handle(reader, writer):     addr = writer.get_extra_info('peername')     print('Connected by', addr)     while True:         data = await reader.read(1024)         if not data:             break         writer.write(data)         await writer.drain()     writer.close()     await writer.wait_closed() async def main():     server = await asyncio.start_server(handle, '127.0.0.1', 65432)     async with server:         await server.serve_forever() asyncio.run(main()) 

asyncio scales better for many concurrent connections with low per-connection overhead.


Basic robustness and best practices

  • Use try/except to handle socket.error and clean up resources.
  • Set socket options like SO_REUSEADDR to avoid “address already in use” during quick restarts: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  • Validate and limit client input to avoid memory exhaustion.
  • Use timeouts (conn.settimeout(seconds)) to prevent stuck connections.
  • For production, prefer TLS (wrap socket with ssl) to encrypt traffic.
  • Run network services with least privilege and consider chroot/jails or containers for isolation.

Simple protocol ideas

  • Plain text command/response (commands terminated by newline).
  • Line-based HTTP-like responses for quick testing.
  • Length-prefixed binary frames for reliable message boundaries.

Example: line-based echo — read until newline, respond, continue.


Debugging tips

  • Use tcpdump/wireshark or ss/netstat to inspect connections and ports.
  • Test with telnet/nc for text protocols and curl for HTTP.
  • Log client addresses and errors; include timestamps for incident analysis.

When to use this minimal approach

  • Learning socket programming fundamentals.
  • Prototyping simple tools or local services.
  • Embedded scripts or utilities where full frameworks are unnecessary.

When to use more advanced solutions

  • High throughput, many concurrent clients: use asyncio or an event-driven framework.
  • Production-grade services: add TLS, structured logging, metrics, robust error handling, and a tested protocol design.

Full minimal example recap

#!/usr/bin/env python3 import socket HOST = '127.0.0.1' PORT = 65432 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:     s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)     s.bind((HOST, PORT))     s.listen()     print(f"Listening on {HOST}:{PORT} ...")     while True:         conn, addr = s.accept()         with conn:             print('Connected by', addr)             while True:                 data = conn.recv(1024)                 if not data:                     break                 print('Received:', data)                 conn.sendall(data) 

This version adds SO_REUSEADDR and runs continuously, echoing every client until they disconnect.


If you want, I can:

  • Provide a TLS-enabled minimal listener.
  • Convert the example to Windows-compatible idioms or containerize it with Docker.
  • Add a simple line-based protocol parser.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *