Why Raw Sockets?
Most developers use libraries like Socket.IO or SignalR for real-time communication. That's perfectly reasonable for production. But if you want to truly understand how real-time communication works, there's no substitute for building it from scratch with raw TCP sockets.
ShadowLink is my exploration of that idea.
The Client-Server Architecture
Client A ──────┐
Client B ──────┼──── Server (Hub) ──── Routes messages
Client C ──────┘The server acts as a central hub. Every client connects to the server, and the server is responsible for routing messages between clients.
import socket
import threading
class ChatServer:
def __init__(self, host='0.0.0.0', port=9090):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((host, port))
self.clients = {} # {socket: username}
def handle_client(self, conn, addr):
username = conn.recv(1024).decode()
self.clients[conn] = username
while True:
try:
data = conn.recv(4096)
if not data:
break
self.broadcast(data, conn)
except:
break
del self.clients[conn]
conn.close()The Custom Protocol
One of the most interesting parts of this project was designing the message protocol. Every message needs a type header so the client knows how to handle it:
[TYPE:4][LENGTH:8][PAYLOAD:n]
Types:
- TEXT: Plain text message
- FILE: File transfer
- AUDIO: Audio stream chunk
- VIDEO: Video frame
- PRIVATE: Private message with targetVoice Calls with PyAudio
Audio streaming is surprisingly straightforward with PyAudio. You capture audio in chunks and send each chunk over the socket:
import pyaudio
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 44100
def stream_audio(sock):
audio = pyaudio.PyAudio()
stream = audio.open(format=FORMAT, channels=CHANNELS,
rate=RATE, input=True, frames_per_buffer=CHUNK)
while True:
data = stream.read(CHUNK)
sock.send(b'AUDIO' + data)Video Calls with OpenCV
Video is similar — capture frames with OpenCV, compress them as JPEG, and send over the socket:
import cv2
import numpy as np
def stream_video(sock):
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
_, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 50])
data = buffer.tobytes()
sock.send(len(data).to_bytes(4, 'big') + data)What I Learned
- TCP is reliable but not instant. You need to handle buffering carefully — received data might come in chunks that don't align with your message boundaries.
- Threading is essential. Every client needs its own thread on the server; blocking I/O will freeze everything.
- Latency is everything for AV. Video and audio need to be treated differently from text — dropped frames are acceptable, but dropped audio chunks cause awful artifacts.
Watch the full demo to see all features in action.