Skip to content

Commit 95499d9

Browse files
author
Don Johnson
committed
v2
v2
1 parent d1f3393 commit 95499d9

File tree

1 file changed

+156
-0
lines changed

1 file changed

+156
-0
lines changed

yolo-echo/2v.py

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python3
2+
"""
3+
A simple, expert-level Python TCP server that always returns 'yolo' to any client request.
4+
Demonstrates best practices, including:
5+
- Only using built-in standard libraries
6+
- Graceful shutdown handling
7+
- Logging
8+
- Clear docstrings
9+
"""
10+
11+
import socket
12+
import threading
13+
import logging
14+
import signal
15+
import sys
16+
from typing import Tuple
17+
18+
class YoloEchoServer:
19+
"""
20+
A TCP server that returns 'yolo' to any connected client.
21+
22+
Attributes:
23+
host (str): The hostname or IP address on which the server listens.
24+
port (int): The TCP port number on which the server listens.
25+
backlog (int): The maximum length to which the queue of pending connections may grow.
26+
_server_socket (socket.socket): The main server socket that listens for new connections.
27+
_running (bool): A flag indicating whether the server is running.
28+
"""
29+
30+
def __init__(self, host: str = "127.0.0.1", port: int = 5000, backlog: int = 5) -> None:
31+
"""
32+
Initialize the YoloEchoServer with specified host, port, and backlog.
33+
34+
Args:
35+
host (str, optional): Hostname or IP address for the server. Defaults to "127.0.0.1".
36+
port (int, optional): Port number to listen on. Defaults to 5000.
37+
backlog (int, optional): Maximum pending connections. Defaults to 5.
38+
"""
39+
self.host = host
40+
self.port = port
41+
self.backlog = backlog
42+
self._running = False
43+
self._server_socket: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
44+
self._setup_logging()
45+
46+
def _setup_logging(self) -> None:
47+
"""
48+
Set up a basic configuration for logging.
49+
"""
50+
logging.basicConfig(
51+
level=logging.INFO,
52+
format="%(asctime)s [%(levelname)s] %(message)s",
53+
datefmt="%Y-%m-%d %H:%M:%S"
54+
)
55+
56+
def _bind_socket(self) -> None:
57+
"""
58+
Bind the server socket to the specified host and port.
59+
"""
60+
try:
61+
self._server_socket.bind((self.host, self.port))
62+
self._server_socket.listen(self.backlog)
63+
self._server_socket.settimeout(1.0) # Non-blocking accept with a short timeout
64+
logging.info("Server bound to %s:%d", self.host, self.port)
65+
except socket.error as err:
66+
logging.error("Failed to bind socket: %s", err)
67+
sys.exit(1)
68+
69+
def run(self) -> None:
70+
"""
71+
Run the main server loop, accepting and handling new client connections.
72+
"""
73+
self._bind_socket()
74+
self._running = True
75+
logging.info("Server is now running. Press Ctrl+C to stop.")
76+
77+
while self._running:
78+
try:
79+
client_sock, client_addr = self._server_socket.accept()
80+
except socket.timeout:
81+
# Loop around to check if we're still running
82+
continue
83+
except OSError as err:
84+
# Socket likely closed during shutdown
85+
logging.debug("Server socket accept interrupted: %s", err)
86+
break
87+
88+
# Handle client in a separate thread
89+
client_thread = threading.Thread(
90+
target=self._handle_client,
91+
args=(client_sock, client_addr),
92+
daemon=True
93+
)
94+
client_thread.start()
95+
96+
self._server_socket.close()
97+
logging.info("Server has been shut down.")
98+
99+
def shutdown(self) -> None:
100+
"""
101+
Signal the server to shut down gracefully.
102+
"""
103+
self._running = False
104+
105+
def _handle_client(self, client_sock: socket.socket, client_addr: Tuple[str, int]) -> None:
106+
"""
107+
Handle a single client connection by sending 'yolo' and closing the connection.
108+
109+
Args:
110+
client_sock (socket.socket): The client socket to communicate with.
111+
client_addr (Tuple[str, int]): The client's address (IP, port).
112+
"""
113+
logging.info("Client connected: %s:%d", client_addr[0], client_addr[1])
114+
try:
115+
# Send 'yolo' to the client
116+
response = b"yolo"
117+
client_sock.sendall(response)
118+
logging.debug("Sent response %r to client %s:%d", response, client_addr[0], client_addr[1])
119+
except (socket.error, ConnectionError) as err:
120+
logging.error("Error sending data to client %s:%d - %s", client_addr[0], client_addr[1], err)
121+
finally:
122+
client_sock.close()
123+
logging.info("Connection closed: %s:%d", client_addr[0], client_addr[1])
124+
125+
126+
def _handle_signals(server_instance: YoloEchoServer) -> None:
127+
"""
128+
Set up signal handlers to gracefully stop the server on SIGINT or SIGTERM.
129+
130+
Args:
131+
server_instance (YoloEchoServer): The server instance to shutdown.
132+
"""
133+
def shutdown_signal_handler(signum, frame):
134+
logging.info("Received shutdown signal (%d). Shutting down...", signum)
135+
server_instance.shutdown()
136+
137+
signal.signal(signal.SIGINT, shutdown_signal_handler)
138+
signal.signal(signal.SIGTERM, shutdown_signal_handler)
139+
140+
141+
def main() -> None:
142+
"""
143+
Entry point for running the YoloEchoServer.
144+
"""
145+
server = YoloEchoServer(host="127.0.0.1", port=5000, backlog=5)
146+
_handle_signals(server)
147+
148+
try:
149+
server.run()
150+
finally:
151+
# Ensure shutdown is called in any exit scenario
152+
server.shutdown()
153+
154+
155+
if __name__ == "__main__":
156+
main()

0 commit comments

Comments
 (0)