首先,一起看一段简单的代码,假设脚本名为tool.py。
import sys
if len(sys.argv) == 1:
print "Hello, World!"
else:
print "Hello, %s!" % " ".join(sys.argv[1:])
在终端中执行命令:
python tool.py Zhuibing Dan
终端会输出:
Hello, Zhuibing Dan!
这是一个典型的python工具,启动程序后,读取输入参数,输出结果,然后程序结束。 这个时候,很多人想,我这个工具,能不能做成Web服务:即客户端提交请求给服务端,告诉服务端输入参数是什么;服务端读取输入参数,输出结果,打包为请求,返回给客户端输出。服务的另一个特点是,程序一直执行,不结束。
那么,写好工具以后,怎么转换成一个Web服务呢?
一个Web服务之所以不会执行一段时间就结束,本质上是因为它在代码里启动了一个while死循环。另外一个重要特征是,它能够跟客户端打交道,需要进行网络通信,因此需要用到socket这个东西。
socket是一个4元组,标识TCP连接的两个终端:本地IP地址、本地端口、远程IP地址、远程端口。一个socket对唯一地标识着网络上的TCP连接。每个终端的IP地址和端口号,称为socket。服务器创建socket并开始接受客户端连接的流程如下: 1. 创建一个TCP/IP socket,socket.socket() 2. 设置一些socket选项,setsockopt()函数 3. 绑定指定地址,bind()函数 4. 将创建的socket变为监听socket,listen()函数
做完这些以后,服务器开始循环地一次接受一个客户端连接。当有连接到达时,accept调用返回已连接的客户端socket。然后服务器从这个socket读取请求数据,处理后发送一个响应给客户端。然后服务器关闭客户端连接,准备好接受新的客户端连接。
# -*- coding=utf-8 -*-
#
# @author: csp11@tsinghua.org.cn
# @date: 2015-12-31
#
import socket
HOST, PORT = 'localhost', 8888
# AF_INET是domain(协议族),表示使用ipv4地址
# SCOK_STREAM是type,提供面向连接的稳定数据传输,即TCP协议
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 默认情况下socket关闭后,端口没有释放,需要经过一个TIME_WAIT时间之后才能使用,以下方法实现端口的马上复用
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind((HOST, PORT))
# listen参数backlog指定tcpserver可以同时接受多少个客户端的连接,超过以后拒绝
# tcpserver尽管可以同时接受n个客户端连接,但只能和第一个连接的客户端通信,当第一个tcp连接的客户端关闭后才能和第二个连接的客户端通信
listen_socket.listen(1)
print 'Serving HTTP on port %s ...' % PORT
while True:
client_connection, client_address = listen_socket.accept()
request = client_connection.recv(1024)
print "request received:", request
request_body = request.rstrip().split()[1][1:]
if request_body == "":
request_body = "World"
http_response = """
HTTP/1.1 200 OK
Hello, %s!
""" % request_body
client_connection.sendall(http_response)
client_connection.close()
之前的服务器,只能够逐个处理客户端发来的请求。其他请求只能在队列中等待,直到上一个请求被处理完了。那么怎么样才能实现并发呢?这需要用到神奇的fork()函数。
###########################################################################
# Concurrent server - webserver3g.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
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()
一旦调用fork,父进程会启动子进程,两个进程在fork()的那一刻完全一样。那么怎么区分谁是父进程,谁是子进程呢?如果返回值pid是0,说明是子进程;否则是父进程,返回的数字是子进程的pid。
fork之后,有几个事情需要注意,否则会出现问题。
第一个问题是,父进程需要关闭client_connection,子进程需要关闭父进程的listen_socket。当服务器创建子进程时,内核会增加socket的引用计数。当socket的引用计数为0时才会关闭socket。上面的代码fork了以后,客户端连接和监听socket的引用计数都变为了2。父进程现在只需要监听是否有请求,如果有请求,就fork一个子进程;子进程只需要负责处理客户端连接,返回响应给客户端。所以父进程关闭了client_connection,子进程关闭了listen_socket。
第二个问题是,僵尸。僵尸就是一个子进程终止了,但是它的父进程没有等它,还没有收到它的终止状态。当一个子进程比父进程先终止,内核把子进程转成僵尸,存储进程的一些信息,等着它的父进程以后获取。僵尸杀必死,且占用资源,因此父进程必须处理子进程结束的信号。此外,因为信号是不排队的,并发服务器如果有许多子进程同时结束,信号如洪水般涌来,会导致部分信号没有处理,导致僵尸问题依然存在。解决方案就是设置一个SIGCHLD事件处理器,使用waitpid系统调用,带上WNOHANG参数,循环处理,确保所有的终止的子进程都被处理掉。