commit 361857d362fb4404e66673f76c7bdbf7181e3391 Author: Gregory Gauthier Date: Thu Mar 26 10:06:21 2026 +0000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/REAMDE.md b/REAMDE.md new file mode 100644 index 0000000..6db45ff --- /dev/null +++ b/REAMDE.md @@ -0,0 +1,3 @@ +# socket-samples + +A simple repo with examples of python sockets diff --git a/src/echo_client.py b/src/echo_client.py new file mode 100644 index 0000000..a3e64a3 --- /dev/null +++ b/src/echo_client.py @@ -0,0 +1,16 @@ +import socket + +HOST = '127.0.0.1' +PORT = 65432 + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((HOST, PORT)) + print("Connected to server.") + + while True: + message = input("Enter message to send (or 'quit' to exit): ") + if message.lower() == 'quit': + break + s.sendall(message.encode('utf-8')) # send as bytes + data = s.recv(1024) # receive echo + print(f"Server echoed: {data.decode('utf-8')}") diff --git a/src/echo_server.py b/src/echo_server.py new file mode 100644 index 0000000..3ee55d8 --- /dev/null +++ b/src/echo_server.py @@ -0,0 +1,20 @@ +import socket + +HOST = '127.0.0.1' # localhost (change to '0.0.0.0' to accept connections from other machines) +PORT = 65432 # any port > 1024 is usually fine + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((HOST, PORT)) + s.listen() + print(f"Server listening on {HOST}:{PORT}...") + + conn, addr = s.accept() + with conn: + print(f"Connected by {addr}") + while True: + data = conn.recv(1024) # receive up to 1KB + if not data: + break # client disconnected + print(f"Received: {data}") + conn.sendall(data) # echo it straight back + print(f"Echoed: {data}") diff --git a/src/mon_client.py b/src/mon_client.py new file mode 100644 index 0000000..091c80a --- /dev/null +++ b/src/mon_client.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +Socket demo: metric client +Connects to the metric server and interacts via the custom binary protocol. + +Commands: + ping Send a PING, expect a PONG + get Request a single metric value + sub Subscribe to a metric at an interval (e.g. sub cpu 1000) + quit Disconnect + +Usage: + python client.py [host] [port] + Defaults to localhost:9999 +""" + +import socket +import struct +import sys +import threading +import time + +# --- Protocol constants (must match server) --- +MSG_PING = 0x01 +MSG_PONG = 0x02 +MSG_METRIC_REQ = 0x03 +MSG_METRIC_RESP = 0x04 +MSG_SUBSCRIBE = 0x05 +MSG_PUSH = 0x06 +MSG_ERROR = 0xFF + +HEADER_FMT = "!BH" +HEADER_SIZE = struct.calcsize(HEADER_FMT) + + +def send_message(sock: socket.socket, msg_type: int, payload: bytes = b"") -> None: + header = struct.pack(HEADER_FMT, msg_type, len(payload)) + sock.sendall(header + payload) + + +def recv_exact(sock: socket.socket, n: int) -> bytes: + buf = bytearray() + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + return b"" + buf.extend(chunk) + return bytes(buf) + + +def recv_message(sock: socket.socket) -> tuple[int, bytes]: + header = recv_exact(sock, HEADER_SIZE) + if not header: + raise ConnectionError("Server disconnected") + msg_type, length = struct.unpack(HEADER_FMT, header) + payload = recv_exact(sock, length) if length > 0 else b"" + return msg_type, payload + + +def listener_thread(sock: socket.socket, running: threading.Event) -> None: + """Background thread that reads pushed messages from the server.""" + while running.is_set(): + try: + msg_type, payload = recv_message(sock) + + if msg_type == MSG_PONG: + print(f"\n [PONG] server is alive (rtt measured at application layer)") + + elif msg_type == MSG_METRIC_RESP: + value = struct.unpack("!d", payload)[0] + print(f"\n [METRIC] {value:.2f}") + + elif msg_type == MSG_PUSH: + name_len = payload[0] + name = payload[1:1 + name_len].decode("utf-8") + value = struct.unpack("!d", payload[1 + name_len:])[0] + print(f"\n [PUSH] {name}: {value:.2f}") + + elif msg_type == MSG_ERROR: + print(f"\n [ERROR] {payload.decode('utf-8')}") + + else: + print(f"\n [???] Unknown msg type 0x{msg_type:02X}") + + # Re-display prompt + print("> ", end="", flush=True) + + except ConnectionError: + if running.is_set(): + print("\nServer disconnected.") + break + except Exception as e: + if running.is_set(): + print(f"\nListener error: {e}") + break + + +def cmd_ping(sock: socket.socket) -> None: + send_message(sock, MSG_PING) + + +def cmd_get(sock: socket.socket, metric: str) -> None: + send_message(sock, MSG_METRIC_REQ, metric.encode("utf-8")) + + +def cmd_subscribe(sock: socket.socket, metric: str, interval_ms: int) -> None: + name_bytes = metric.encode("utf-8") + payload = struct.pack("B", len(name_bytes)) + name_bytes + struct.pack("!I", interval_ms) + send_message(sock, MSG_SUBSCRIBE, payload) + print(f" Subscribed to '{metric}' every {interval_ms}ms") + + +def show_wire_comparison() -> None: + """Print a quick comparison of our protocol overhead vs HTTP.""" + # Our METRIC_REQ for "cpu": 3-byte header + 3-byte payload = 6 bytes on the wire + our_size = HEADER_SIZE + len(b"cpu") + + # Equivalent HTTP: GET /metrics/cpu HTTP/1.1\r\nHost: localhost:9999\r\n\r\n + http_req = b"GET /metrics/cpu HTTP/1.1\r\nHost: localhost:9999\r\nAccept: application/json\r\n\r\n" + http_size = len(http_req) + + print(f"\n--- Wire overhead comparison ---") + print(f"Our protocol (METRIC_REQ 'cpu'): {our_size} bytes") + print(f"Equivalent HTTP GET: {http_size} bytes") + print(f"Ratio: HTTP is ~{http_size / our_size:.0f}x larger") + print(f"(And that's before HTTP response headers, JSON framing, etc.)\n") + + +def main(): + host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" + port = int(sys.argv[2]) if len(sys.argv) > 2 else 9999 + + print(f"Connecting to {host}:{port}...") + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((host, port)) + except ConnectionRefusedError: + print(f"Connection refused. Is the server running?") + sys.exit(1) + + print(f"Connected.\n") + show_wire_comparison() + + running = threading.Event() + running.set() + + # Start background listener for async responses / pushes + listener = threading.Thread(target=listener_thread, args=(sock, running), daemon=True) + listener.start() + + print("Commands: ping | get | sub | quit") + print("Metrics: cpu, memory, disk, loadavg, uptime\n") + + try: + while True: + try: + line = input("> ").strip() + except EOFError: + break + + if not line: + continue + + parts = line.split() + cmd = parts[0].lower() + + if cmd == "quit": + break + elif cmd == "ping": + cmd_ping(sock) + elif cmd == "get" and len(parts) == 2: + cmd_get(sock, parts[1]) + elif cmd == "sub" and len(parts) == 3: + try: + cmd_subscribe(sock, parts[1], int(parts[2])) + except ValueError: + print(" Usage: sub ") + else: + print(" Unknown command. Try: ping, get , sub , quit") + + except KeyboardInterrupt: + print() + finally: + running.clear() + sock.close() + print("Disconnected.") + + +if __name__ == "__main__": + main() diff --git a/src/mon_server.py b/src/mon_server.py new file mode 100644 index 0000000..3b3ff6f --- /dev/null +++ b/src/mon_server.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Socket demo: metric server +Custom binary protocol over TCP — no HTTP, no JSON, no overhead. + +Wire format (all values big-endian): + [1 byte msg_type] [2 bytes payload_length] [payload_length bytes payload] + +Message types: + 0x01 PING client -> server (no payload) + 0x02 PONG server -> client (no payload) + 0x03 METRIC_REQ client -> server (payload: metric name as UTF-8) + 0x04 METRIC_RESP server -> client (payload: 8-byte double) + 0x05 SUBSCRIBE client -> server (payload: metric name + 4-byte interval_ms) + 0x06 PUSH server -> client (payload: metric name len(1) + name + 8-byte double) + 0xFF ERROR server -> client (payload: error message as UTF-8) + +Usage: + python server.py [host] [port] + Defaults to localhost:9999 +""" + +import socket +import struct +import sys +import time +import os +import threading +import random + +# --- Protocol constants --- +MSG_PING = 0x01 +MSG_PONG = 0x02 +MSG_METRIC_REQ = 0x03 +MSG_METRIC_RESP = 0x04 +MSG_SUBSCRIBE = 0x05 +MSG_PUSH = 0x06 +MSG_ERROR = 0xFF + +HEADER_FMT = "!BH" # 1 byte type, 2 bytes length (big-endian) +HEADER_SIZE = struct.calcsize(HEADER_FMT) + + +def get_metric(name: str) -> float: + """Simulate reading a system metric. Replace with real psutil calls if available.""" + metrics = { + "cpu": lambda: random.uniform(5.0, 95.0), + "memory": lambda: random.uniform(30.0, 80.0), + "disk": lambda: random.uniform(40.0, 90.0), + "loadavg": lambda: os.getloadavg()[0] if hasattr(os, "getloadavg") else random.uniform(0.5, 4.0), + "uptime": lambda: float(int(time.time()) % 100000), + } + fn = metrics.get(name) + if fn is None: + raise KeyError(f"Unknown metric: {name}") + return fn() + + +def send_message(sock: socket.socket, msg_type: int, payload: bytes = b"") -> None: + """Frame and send a single protocol message.""" + header = struct.pack(HEADER_FMT, msg_type, len(payload)) + sock.sendall(header + payload) + + +def recv_message(sock: socket.socket) -> tuple[int, bytes]: + """Read exactly one protocol message. Returns (msg_type, payload).""" + header = recv_exact(sock, HEADER_SIZE) + if not header: + raise ConnectionError("Client disconnected") + msg_type, length = struct.unpack(HEADER_FMT, header) + payload = recv_exact(sock, length) if length > 0 else b"" + return msg_type, payload + + +def recv_exact(sock: socket.socket, n: int) -> bytes: + """Read exactly n bytes from socket (handles partial reads).""" + buf = bytearray() + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + return b"" + buf.extend(chunk) + return bytes(buf) + + +def handle_subscribe(conn: socket.socket, payload: bytes, stop_event: threading.Event) -> None: + """Push a metric at the requested interval until the connection drops.""" + name_len = payload[0] + name = payload[1:1 + name_len].decode("utf-8") + interval_ms = struct.unpack("!I", payload[1 + name_len:1 + name_len + 4])[0] + interval_s = interval_ms / 1000.0 + + print(f" -> subscription: {name} every {interval_ms}ms") + + def push_loop(): + while not stop_event.is_set(): + try: + value = get_metric(name) + name_bytes = name.encode("utf-8") + resp = struct.pack("B", len(name_bytes)) + name_bytes + struct.pack("!d", value) + send_message(conn, MSG_PUSH, resp) + except (ConnectionError, BrokenPipeError, OSError): + break + stop_event.wait(interval_s) + + t = threading.Thread(target=push_loop, daemon=True) + t.start() + + +def handle_client(conn: socket.socket, addr: tuple) -> None: + """Main loop for one connected client.""" + print(f"[+] Connection from {addr[0]}:{addr[1]}") + stop_event = threading.Event() + + try: + while True: + msg_type, payload = recv_message(conn) + + if msg_type == MSG_PING: + print(f" <- PING from {addr[0]}") + send_message(conn, MSG_PONG) + + elif msg_type == MSG_METRIC_REQ: + name = payload.decode("utf-8") + print(f" <- METRIC_REQ: {name}") + try: + value = get_metric(name) + send_message(conn, MSG_METRIC_RESP, struct.pack("!d", value)) + except KeyError as e: + send_message(conn, MSG_ERROR, str(e).encode("utf-8")) + + elif msg_type == MSG_SUBSCRIBE: + handle_subscribe(conn, payload, stop_event) + + else: + send_message(conn, MSG_ERROR, f"Unknown message type: 0x{msg_type:02X}".encode("utf-8")) + + except ConnectionError: + pass + finally: + stop_event.set() + conn.close() + print(f"[-] Disconnected: {addr[0]}:{addr[1]}") + + +def main(): + host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" + port = int(sys.argv[2]) if len(sys.argv) > 2 else 9999 + + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind((host, port)) + srv.listen(5) + + print(f"Metric server listening on {host}:{port}") + print(f"Available metrics: cpu, memory, disk, loadavg, uptime") + print(f"Wire format: {HEADER_SIZE}-byte header + payload (no HTTP, no JSON)\n") + + try: + while True: + conn, addr = srv.accept() + t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True) + t.start() + except KeyboardInterrupt: + print("\nShutting down.") + finally: + srv.close() + + +if __name__ == "__main__": + main()