简易web服务器
web服务器是指在物理服务器上搭建的网络连接服务器,时刻等待客户端的请求,并作出响应。
客户端与服务器的通信,是以HTTP协议进行的,客户端可以是任意支持HTTP协议的软件
- 客户端在向服务器发送HTTP请求前,先建立TCP连接,通过TCP连接发送HTTP请求,客户端和服务器建立TCP连接用到套接字(socket)
- 套接字:可以理解为通信端点的抽象形式,可以让一个程序通过文件描述符与另一个程序进行通信。
- TCP套接字对:4元组:本地ip,本地端口,外部ip,外部端口
- 服务器套接字创建流程:
- 客户端套接字创建流程
代码:
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
while True:
client_connection, client_address = listen_socket.accept()
handle_request(client_connection)
client_connection.close()
if __name__ == '__main__':
serve_forever()
实现并发
进程:在unix系统下,每个用户进程都会有一个父进程,父进程id为ppid,当启动服务器时,系统会创建一个服务器进程,指定一个pid,在bash中创建则其父进程为bash的id
文件描述符:当系统打开一个现有文件、创建一个新文件或者创建一个新的套接字之后,返回给进程的那个正整数。系统内核通过文件描述符来追踪一个进程所打开的文件。
套接字对象的listen方法中BACKLOG参数的意义:BACKLOG参数决定了内核中外部连接请求的队列大小。当服务器睡眠时,你运行的第二个命令之所以能够连接服务器,是因为连接请求队列仍有足够的位置。
fork函数:unix系统函数, fork()调用一次返回两次,在子进程中返回0,在父进程中返回子进程的pid。
当父进程fork一个新的子进程时,子进程会得到父进程文件描述符的副本
父进程:fork一个子进程处理客户端连接,然后回到循环起点准备接受其他的客户端连接
导致的问题:
- 如果不关闭重复(父进程)的文件描述符,客户端不会结束,且服务器会逐渐消耗尽文件描述符资源(最大打开文件数)
- 若子进程在父进程之前结束,会出现僵尸进程,消耗内存资源。因为父进程是不监听客户端结束的,故子进程结束后看到父进程未关闭,会保留信息供父进程之后调用。不处理好僵尸进程会逐渐消耗完可用的进程数(最大用户进程数)
系统内核通过描述符计数来决定是否关闭文件/套接字
linux下Python可以用os.fork()创建新进程,windows下是不行的
僵尸进程
如果你fork一个子进程,却不等待进程结束,该进程就会变成僵尸进程。
使用SIGCHLD时间处理函数来异步等待进程结束,获取其结束状态。
- 使用事件处理函数时,需要牢记系统函数调用可能会被中断,要做好这类情况发生的准备。
最终代码:
import errno
import os
import signal
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024
def grim_reaper(signum, frame):
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 handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
signal.signal(signal.SIGCHLD, grim_reaper)
while True:
try:
client_connection, client_address = listen_socket.accept()
except IOError as e:
code, msg = e.args
# restart 'accept' if it was interrupted
if code == errno.EINTR:
continue
else:
raise
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # parent
client_connection.close() # close parent copy and loop over
if __name__ == '__main__':
serve_forever()