initial commit

This commit is contained in:
Gregory Gauthier 2026-03-26 10:06:21 +00:00
commit 361857d362
6 changed files with 401 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea/

3
REAMDE.md Normal file
View File

@ -0,0 +1,3 @@
# socket-samples
A simple repo with examples of python sockets

16
src/echo_client.py Normal file
View File

@ -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')}")

20
src/echo_server.py Normal file
View File

@ -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}")

190
src/mon_client.py Normal file
View File

@ -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 <metric> Request a single metric value
sub <metric> <ms> 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 <metric> | sub <metric> <ms> | 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 <metric> <interval_ms>")
else:
print(" Unknown command. Try: ping, get <metric>, sub <metric> <ms>, quit")
except KeyboardInterrupt:
print()
finally:
running.clear()
sock.close()
print("Disconnected.")
if __name__ == "__main__":
main()

171
src/mon_server.py Normal file
View File

@ -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()