A web server from scratch
~6 mins read
Inspired by Ruslan’s blog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# coding: utf-8
"""
a simple concurrent web server
socket -> bind -> listen -> accept -> loop
"""
import socket
import os
import time
import signal
import errno
# Define socket host and port
SERVER_HOST = '0.0.0.0'
SERVER_PORT = 8000
"""
A socket is an abstraction of a communication endpoint
and it allows your program to communicate with another program using file descriptors.
The socket pair for a TCP connection is a 4-tuple
that identifies two endpoints of the TCP connection:
the local IP address, local port, foreign IP address, and foreign port.
eg. (0.0.0.1:80, 0.0.0.2:6379) is a socket pair and 0.0.0.1:80 is a socket
"""
def wait_for_children_process(signum, frame):
"""
If you don't close duplicate descriptors,
the clients won't terminate because the client connections won't get closed.
Moreover, your long-running server will eventually
run out of available file descriptors (max open files).
When you fork a child process and it exits
if the parent process doesn't wait for it and doesn't collect its termination status,
the child process becomes a zombie.
Zombies need to eat something and, in our case, it's memory.
Your server will eventually run out of available processes (max user processes)
if it doesn't take care of zombies.
You can't kill a zombie, you need to wait for it.
If you fork a child and don't wait for it, it becomes a zombie.
"""
while True:
try:
pid, status = os.waitpid(
-1, # Wait for any child process
os.WNOHANG # Do not block and return EWOULDBLOCK error
)
except OSError:
return
if pid == 0: # no more zombies
return
def serve():
# Create socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# add socket options
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# bind -> assign a local protocol address to the socket
server_socket.bind((SERVER_HOST, SERVER_PORT))
# listen -> make the socket a listening socket
server_socket.listen(1)
print('Listening on port %s ...' % SERVER_PORT)
"""
Use the SIGCHLD event handler to asynchronously
wait for a terminated child to get its termination status
When using an event handler you need to keep in mind that
system calls might get interrupted
and you need to be prepared for that scenario
"""
signal.signal(signal.SIGCHLD, wait_for_children_process)
# accept and loop
while True:
try:
# Wait for client connections
client_connection, client_address = server_socket.accept()
except IOError as e:
code, msg = e.args
# restart 'accept' if it was interrupted
if code == errno.EINTR:
continue
else:
raise
# Get the client request
request = client_connection.recv(1024).decode()
"""
The simplest way to write a concurrent server in Unix
is to use the fork() system call
When a process forks a new process,
it becomes a parent process to that newly forked child process.
Parent and child share the same file descriptors after the call to fork.
The kernel uses descriptor reference counts
to decide whether to close the file/socket or not
The role of a server parent process:
1. all it does now is accept a new connection from a client,
2. fork a child to handle the client request,
3. and loop over to accept a new client connection.
"""
pid = os.fork()
if pid == 0: # child
server_socket.close() # close child copy
print(f'Child PID: {pid=os.getpid()}. Parent PID {ppid=os.getppid()}')
response = "HTTP/1.1 200 OK\n\nHello, World!"
client_connection.sendall(response.encode())
client_connection.close()
os._exit(0) # child exits here
else: # parent
client_connection.close()
if __name__ == '__main__':
serve()