initial commit
This commit is contained in:
commit
361857d362
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.idea/
|
||||||
3
REAMDE.md
Normal file
3
REAMDE.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# socket-samples
|
||||||
|
|
||||||
|
A simple repo with examples of python sockets
|
||||||
16
src/echo_client.py
Normal file
16
src/echo_client.py
Normal 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
20
src/echo_server.py
Normal 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
190
src/mon_client.py
Normal 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
171
src/mon_server.py
Normal 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()
|
||||||
Loading…
Reference in New Issue
Block a user