自己写一个Web服务器(3)

转载自:http://www.codeceo.com/article/make-web-server-3.html

自己写一个Web服务器(1)

自己写一个Web服务器(2)

自己写一个Web服务器(3)

必须发明时我们学的最好——Piaget

第二篇你建了一个极简的WSGI服务器,可以出来基本的HTTP GET请求。结束时我问了个问题,你怎么保证你的服务器能同时处理多个请求?在这篇文章中你会找到答案。所以,系好安全带,换高档位,你将会超高速行驶。准备好你的Linux,Mac OS X(或其他*nix系统)和python。这篇文章的所有代码都在GitHub

首先让我们回忆一个基本的web服务器是什么样子,它需要对客户端的请求做什么。在第一篇第二篇中你建是一个的迭代服务器,一次处理一个客户端请求。它不能接受新的连接直到处理完当前客户端请求。一些客户端可能会不高兴,因为他们必须排队等待,而一些忙碌的服务器这队就太长了。

这是迭代服务器的代码webserver3a.py:

#####################################################################
# Iterative server - webserver3a.py                                 #
#                                                                   #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X  #
#####################################################################
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()

仔细看你的服务器一次只处理一个请求,稍微修改这个服务器在给客户端发送响应加上一个60秒的延时。这个改变只有一行来告诉服务器进程休眠60秒。

这是可休眠服务器的代码 webserver3b.py:

#########################################################################
# Iterative server - webserver3b.py                                     #
#                                                                       #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X      #
#                                                                       #
# - Server sleeps for 60 seconds after sending a response to a client   #
#########################################################################
import socket
import time

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)
    time.sleep(60)  # sleep and block the process for 60 seconds

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()

启动服务器:

$ python webserver3b.py

现在打开一个新的终端窗口然后运行curl命令。你应该会立即看到“Hello, World!”被打印在屏幕上:

$ curl http://localhost:8888/hello
Hello, World!

不要等待打卡第二个终端运行同样curl命令:

$ curl http://localhost:8888/hello

你要是在60秒内做完了,那第二个curl不会立即有任何显示而只停在那里。服务器也不会打印出一个新请求的标准输出。在我的Mac上是这个样子的(在右下方高亮的窗口显示了第二个curl命令挂起,等待连接被服务器接受):

等待时间足够长之后(多余60秒)你应该看到第一个curl终止,第二个curl的窗口打印出“Hello, World!”,然后挂起60秒,然后终止:

它的工作方式是这样的,服务器处理完第一个curl客户端请求后休眠60秒然后开始处理第二个请求。这些都是按顺序一步步来,或者在这个例子中一个时刻,一个客户端请求。

我们讨论一下客户端和服务器之间的通信。要让两个程序通过网络彼此通讯,他们需要用到socket。你在第一篇和第二篇都看到了socket,但socket是什么呢?

socket是一个通信终端的抽象,它允许你的程序通过描述文件与另一个程序通信。在这篇文章中我会谈到Linux/Mac OS X上典型的TCP/IP socket一个重要的概念是TCP socket对。

TCP连接的socket对是有4个值的tuple用来标识TCP连接的两个端点:本地IP地址,本地端口,外部IP地址,外部端口。socket对唯一标识网络上的每个TCP连接。这两个成对的值标识各自端点,一个IP地址和一个端口号,通常被称为一个socket。

tuple {10.10.10.2:49152, 12.12.12.3:8888} 是客户端上一个唯一标识两个TCP连接终端的socket, {12.12.12.3:8888, 10.10.10.2:49152} 是客户端上一个唯一标识相同的两个TCP连接终端的socket。IP地址12.12.12.3和端口8888在TCP连接中用来识别服务器端点(同样适用于客户端)。

标准的服务器创建一个socket然后接受客户端连接的流程如下图所示:

服务器创建一个TCP/IP socket。用下面的python语句:

listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

服务器可能会设置一些socket选项(这是可选的,单丝你看到上面的代码多次使用相同的地址,如果你想停止它那就马上重启服务器)。

listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

3. 然后,服务器绑定地址。bind函数给socket分配一个本地地址。在TCP中,调用bind允许你指定端口号,IP地址,要么两个要么就没有。

listen_socket.bind(SERVER_ADDRESS)

接着服务器让这个socket成为监听socket

listen_socket.listen(REQUEST_QUEUE_SIZE)

listen方法只供服务器调用。它告诉内核应该接受给这个socket传入的连接请求

这些完成后,服务器开始逐个接受客户端连接。当一个连接可用accept返回要连接的客户端socket。然后服务器读从客户端socket取请求数据,打印出响应标准输出然后给客户端socket传回消息。然后服务器关闭客户端连接,准备接受一个新的客户端连接。

下图就是在TCP/IP中客户端与服务器通信需要做的:

这里有同样的代码用来连接客户端和服务器,发出一个请求然后打印出响应:

 import socket

 # create a socket and connect to a server
 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 sock.connect(('localhost', 8888))

 # send and receive some data
 sock.sendall(b'test')
 data = sock.recv(1024)
 print(data.decode())

创建socket之后,客户端需要连接服务器。这是通过connect调用来完成的:

sock.connect(('localhost', 8888))

客户端只需提供服务器的远程地址或是主机名和远程端口号来连接。

你可能已经注意到客户端没有调用bind和accept。其原因是客户端不关心本地IP地址和端口号。客户端调用connect时内核中的TCP/IP socket会自动分配本地IP地址和端口号。本地端口被称为临时端口,一个短命的端口。

客户端连接用以获取已知服务的服务器端口成为已知端口(例如80是HTTP,22是SSH)。打开python shell在本地主机开启一个客户端连接,看看内核给你的socket分配了哪个临时端口(先启动webserver3a.py 或者 webserver3b.py):

>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.connect(('localhost', 8888))
>>> host, port = sock.getsockname()[:2]
>>> host, port
('127.0.0.1', 60589)

上面的例子内核分配给socket的临时端口是60589.

还有一些重要概念我需要在回答第二篇的问题前先做说明。你很快就会看到为什么这是非常重要的。这两个概念是一个是进程,一个是文件描述符。

什么事进程?进程是执行程序的实例。当服务器代码开始执行,比如,它要载入内存执行程序就会调用一个进程。内核记录一系列关于进程的信息——比如进程ID——用来追踪它。当你运行webserver3a.py 或 webserver3b.py你只运行了一个进程。

在一个终端中运行webserver3b.py:

$ python webserver3b.py

在另一个终端中用ps命令获取有关进程的信息:

$ ps | grep webserver3b | grep -v grep
7182 ttys003    0:00.04 python webserver3b.py

ps命令表明你却是只运行了一个python进程webserver3b。当一个进程产生内核就会给他分配进程ID,PID。在UNIX中每个用户进程还有一个父进程,当然也有自己的ID叫做父进程ID,或者简写成PPID。

我当你是在用默认BASH,那么启动服务器一个进程被创建同时一个PID被设定,同时一个PPID在BASH中被设定。

你自己试试看它是怎么做的。再次打开python shell,它就产生了一个新进程,然后用ow.getpid()和os.getppid()这恋歌系统调用查看PID和PPID。接着在另一个终端窗口运行ps命令同时grep搜索这个PPID(我这里是3148).在下面的截屏中你看到一个关于我Moc OS X系统上BASH进程和python shell进程的父子关系:

另一个必须知道的的概念是文件描述符。那什么是文件描述符呢?是当一个进程打开现有的文件,创建一个新文件,或者当它创建一个新的socket时,内核返回给它的一个非负整数。你应该知道在UNIX中所有东西都是文件。内核通过文件描述符指向一个打开的文件。当你需要读写文件是就用文件描述符来识别。python给你跟高级别的对象来处理文件,你不需要直接用文件描述符来识别文件,但在底层,UNIX中文件和socket的识别是用他们的整数文件描述符。

UNIX shell默认分配文件描述符0给标准输入进程,1是标准输出,2是标准错误。

前面说到的,python给你跟高层级文件或类文件对象,你还是可以用fileno()方法来得到相关联文件的文件描述符。回到python shell看看怎么做到:

>>> import sys
>>> sys.stdin
<open file '<stdin>', mode 'r' at 0x102beb0c0>
>>> sys.stdin.fileno()
0
>>> sys.stdout.fileno()
1
>>> sys.stderr.fileno()
2

在Python中处理文件和socket时,你通常会使用一个高层次的文件/ Socket对象,但也有可能,你需要直接使用文件描述符。这里给出一个例子,你用write系统调用给标准输出写入一个字符串,它将文件描述符作为一个参数:

>>> import sys  
>>> import os  
>>> res = os.write(sys.stdout.fileno(), 'hello\n')  
hello

这是一个有趣的部分——你不会在感到惊讶,因为你已经知道在UNIX中所有的都是文件——你的socket也有一个与其关联的文件描述符。继续,我前面说到那样创建一个socket你得到一个对象和一个非负整数,你总是可以直接通过fileno()方法获取文件描述符。

>>> import socket  
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
>>> sock.fileno()  
3

还有一件事情:你有没有注意到注意到在第二个迭代服务器webserver3b.py的例子中,服务器在60秒休眠中你还可以通过第二个curl命令连接到服务器。当然curl没有立即有任何输出,它挂起了,但是为什么服务器没有在那时就接受连接,也没有立即拒绝客户端,而是允许它连接到服务器呢?答案是socket对象的listen方法和它的BACKLOG 参数,在代码中是REQUEST_QUEUE_SIZE。BACKLOG 参数决定内核处理进来连接请求队列的长度。服务器 webserver3b.py休眠时,第二个curl能够连接到服务器是因为内核有足够的空间给进来的连接请求。

增加BACKLOG 参数不能让你的服务器理解神奇到可以同时处理多个客户端请求。要繁忙的服务器不必等待继而接受一个新的连接,而是立即从消息队列中抓取新的连接同时没有延迟的开始一个客户端响应进程,一个相当大的BACKLOG参数是非常重要的。

你已经了解够多了。来快速回顾一下你目前为止所学的(或者复习一下你的基础)。

  • 迭代服务器
  • 服务器socket创建过程(socket,bind,listen,accept)
  • 客户端socket创建过程(socket,connect)
  • socket对
  • socket
  • 临时端口和已知端口
  • 进程
  • 进程ID(PID),父进程ID(PPID),和父子关系
  • 文件描述符
  • 监听socket的BSCKLOG参数的意义

现在我准备回答第二篇的问题:你怎么保证你的服务器能同时处理多个请求?或者换个方式,如何编写并发服务器?

在UNIX下最简单的方法是用一个fork()系统调用。

这是你新的兵法服务器的代码[webserver3c.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3c.py),它可以同时处理多个客户端请求(跟在迭代服务器webserver3b.py一样,每个子进程休眠60秒):

###########################################################################  
# Concurrent server - webserver3c.py                                      #  
#                                                                         #  
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X        #  
#                                                                         #  
# - Child process sleeps for 60 seconds after handling a client's request #  
# - Parent and child processes close duplicate descriptors                #  
#                                                                         #  
###########################################################################  
import os  
import socket  
import time

SERVER_ADDRESS = (HOST, PORT) = '', 8888  
REQUEST_QUEUE_SIZE = 5

def handle_request(client_connection):  
request = client_connection.recv(1024)  
print(  
'Child PID: {pid}. Parent PID {ppid}'.format(  
pid=os.getpid(),  
ppid=os.getppid(),  
)  
)  
print(request.decode())  
http_response = b"""\  
HTTP/1.1 200 OK

Hello, World!  
"""  
client_connection.sendall(http_response)  
time.sleep(60)

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))  
print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid()))

while True:  
client_connection, client_address = listen_socket.accept()  
pid = os.fork()  
if pid == 0:  # child  
listen_socket.close()  # close child copy  
handle_request(client_connection)  
client_connection.close()  
os._exit(0)  # child exits here  
else:  # parent  
client_connection.close()  # close parent copy and loop over

if __name__ == '__main__':  
serve_forever()

在讨论fork怎么工作前,你自己看看这个服务器却是同时处理多个请求,而不像前连个。在命令行中用下面命令:

$ python webserver3c.py

接着运行连个curl命令,下载即便服务器子进程处理客户端请求后休眠60秒,却并不影响其他客户端,因为他们是完全不同的独立进程了。你可以运行你尽可能多的curl命令(你想多少就多少),每一个都会没有明显延迟立即打印出服务器响应“Hello, World” 。

理解fork()最重要的一点是你调用forl一次但是他返回两次:一次是父进程,一次是子进程。你fork你个新的进程返回子进程的ID是0,返回父进程的是子进程的PID。

我还记得第一次看到并尝试fork时是有多着迷。我正在看循序代码突然一声响:代码复制了自己成为两个同时运行的实例。我觉得这就是魔法,真的。

父进程fork出一个新子进程,这个子进程得到一个父进程文件描述符:

你可能注意到上面代码的父进程关闭了客户端连接:

else:  # parent
    client_connection.close()  # close parent copy and loop over

那么子进程怎么能继续读取客户端socket数据,如果父进程已经关闭了和它的连接?答案就在上面的图片中。内核根据文件描述符的值来决定是否关闭连接socket,只有其值为0才会关闭。服务器产生一个子进程,子进程拷贝父进程文件描述符,内核增加引用描述符的值。在一个父进程一个子进程的例子中,描述符引用值就是2,当父进程关闭连接socket,它只会把引用值减为1,不会小岛让内核关闭socket。子进程也关闭了父进程监听socket的重复拷贝,是因为它不关心接受新的客户端连接,而只在乎处理已连接客户端的响应:

listen_socket.close()  # close child copy

我会在这篇文章的后面谈到你不取消重复描述符会发生什么。

如你从当前服务器代码中看到的,父进程的唯一职责是接受客户端连接,fork一个子进程去处理客户端请求,然后继续接受另一个客户端请求,没别的。父进程不对客户端请求做处理——子进程来做。

我们说两个事件并发是什么意思呢?

说两事件并发通常是指他们在同一时间发生。作为一个简短的定义是好的,但是你应该记住严格的定义:

如果你不能通过看这个程序来告诉你,这两个事件是并发的。

又到了回顾概念和理念的时间了:

  • 在UNIX下写并发服务器最简单的方法是用fork()系统调用。
  • 一个进程fork出一个新进程,它就变成新进程的父进程
  • 调用fork后,父进程和子进程公用同样的文件描述符
  • 内核用文件描述符应用值来决定关闭或打开文件/socket
  • 服务器父进程的角色:从客户端接受新的连接,fork一个子进程去处理请求,继续接受新的连接。

我们来看看不取消父进程和子进程建重复描述符会发生什么。对个当前服务器代码稍作修改,webserver3d.py:

###########################################################################
# Concurrent server - webserver3d.py                                      #
#                                                                         #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X        #
###########################################################################
import os
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5

def handle_request(client_connection):
    request = client_connection.recv(1024)
    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))

    clients = []
    while True:
        client_connection, client_address = listen_socket.accept()
        # store the reference otherwise it's garbage collected
        # on the next loop run
        clients.append(client_connection)
        pid = os.fork()
        if pid == 0:  # child
            listen_socket.close()  # close child copy
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)  # child exits here
        else:  # parent
            # client_connection.close()
            print(len(clients))

if __name__ == '__main__':
    serve_forever()

启动服务器:

$ python webserver3d.py

用curl连接服务器:

$ curl http://localhost:8888/hello
Hello, World!

curl打印出了并发服务器的响应但没有终止然后保持挂起。发生了什么?服务器不再休眠60秒:它的子进程积积德处理了客户端请求,关闭客户端连接和退出,但curl仍然不终止。

为什么curl不终止?答案是重复的文件描述符。子进程关闭了客户端连接,内核将socket引用值减为1。子进程退出,客户端socket还不关闭是因为socket的引用值还不是0,结果就是终止包(在TCP/IP中叫FIN)没有被发送到客户端,客户端就持续连接。还有一个问题,你一直运行服务器而不关闭重复的文件描述符,最终会用完可用的文件描述符。

用Control-C停止你的服务器,在shell通过内置命令ulimit检查你服务器可用的默认资源:

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 3842
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 3842
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

从上面你可以看到,在我的Ubuntu上open files文件描述符(打开多少文件)的最大可用数值是1024.

来看看不关闭重复描述符服务器怎样用完可用文件描述符。在终端窗口设置open files描述符为256:

$ ulimit -n 256

在同一终端启动服务器 webserver3d.py:

$ python webserver3d.py

用下面的客户端client3.py来测试服务器。

#####################################################################
# Test client - client3.py                                          #
#                                                                   #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X  #
#####################################################################
import argparse
import errno
import os
import socket

SERVER_ADDRESS = 'localhost', 8888
REQUEST = b"""\
GET /hello HTTP/1.1
Host: localhost:8888

"""

def main(max_clients, max_conns):
    socks = []
    for client_num in range(max_clients):
        pid = os.fork()
        if pid == 0:
            for connection_num in range(max_conns):
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.connect(SERVER_ADDRESS)
                sock.sendall(REQUEST)
                socks.append(sock)
                print(connection_num)
                os._exit(0)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Test client for LSBAWS.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        '--max-conns',
        type=int,
        default=1024,
        help='Maximum number of connections per client.'
    )
    parser.add_argument(
        '--max-clients',
        type=int,
        default=1,
        help='Maximum number of clients.'
    )
    args = parser.parse_args()
    main(args.max_clients, args.max_conns)

在一个新的终端窗口开启client3.py然后告诉他创建300个与服务器的并发连接:

$ python client3.py –max-clients=300

很快你的服务器就会爆掉。这是我的异常报告截屏:

教训是明显的——服务器应该关闭重复描述符。但即使关闭重复描述符你也没有走出困境,你这服务器还有一个问题,这个问题是僵尸!

真的,你的代码创造了僵尸进程。看下怎么回事,再次启动服务器:

$ python webserver3d.py

在另一个终端运行下面的curl命令:

$ curl http://localhost:8888/hello

接着运行ps命令看看运行的python进程。下面是我在Ubuntu上的样子:

$ ps auxw | grep -i python | grep -v grep
vagrant   9099  0.0  1.2  31804  6256 pts/0    S+   16:33   0:00 python webserver3d.py
vagrant   9102  0.0  0.0      0     0 pts/0    Z+   16:33   0:00 [python] <defunct>

你看到上面第二行PID为9102的进程是Z+,而且进程名是了吗?这就是我们的僵尸进程。僵尸进程的问题是你不能杀死他们。

即使你用’$ kill -9’来杀僵尸进程,他们还会复活,你自己试试看。

什么是僵尸进程而我们的服务器为什么会产生他们?僵尸进程是一个已经终止进程,但是他的父进程没有等待并收到它的终止状态。

一个子进程先于它的父进程退出,内核将其转为僵尸进程并存储器父进程的一些信息用来以后恢复。通常存储进程ID,终止状态,进程使用的资源。所以僵尸进程是有用的,但你的服务器不处理好这些僵尸进程就会造成系统阻塞。看看吧,先停止运行的服务器,然后再新的终端窗口用ulimit命令设定你的最大用户进程为400(确定open files是更大的数,就500吧):

$ ulimit -u 400
$ ulimit -n 500

在刚运行’$ ulimit -u 400’ 命令的终端启动服务器webserver3d.py:

$ python webserver3d.py

在新的终端窗口,启动client3.py产生500个同时到服务器的连接:

$ python client3.py --max-clients=500

很快你疯服务器就会出现在创建新的子进程时OSError: Resource temporarily unavailable异常,因为它已经达到了允许子进程数的上限。下面是我的异常截图:

我会简要说明服务器该怎样对待僵尸进程问题。

再回顾一下主要内容:

  • 你不关闭重复描述符,客户端就不终止因为客户端连接没有关闭
  • 你不关闭重复描述符,长时间运行的服务器最终会用完可用文件描述符
  • 你fork的子进程退出了但是其父进程没等待和回收它的终止状态,那它就成了僵尸进程
  • 你不能杀死僵尸进程,你需要等待它

那么你要做什么来对付僵尸进程?你要修改服务器代码来等待僵尸进程回收他们的终止状态。你可以用系统调用wait来修改服务器。不幸的是这太不理想,因为调用wait而没有终止的子进程的话,wait调用会锁住服务器,从而阻止服务器从处理新的客户端连接请求。有别的办法吗?有,其中一个是将一个信号处理器和wait调用结合。

它的工作原理是,一个子进程退出,内核发出一个SIGCHLD信号。父进程可以建一个信号处理器异步接收SIGCHLD信号,然后它就等待并回收子进程终止状态,从而防止留下僵尸进程。

顺便说下,异步事件意味着父进程不会提前知道该事件将要发生。

修改服务器代码,设置SIGCHLD事件处理器等待终止的子进程。代码是webserver3e.py

###########################################################################
# Concurrent server - webserver3e.py                                      #
#                                                                         #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X        #
###########################################################################
import os
import signal
import socket
import time

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5

def grim_reaper(signum, frame):
    pid, status = os.wait()
    print(
        'Child {pid} terminated with status {status}'
        '\n'.format(pid=pid, status=status)
    )

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)
    # sleep to allow the parent to loop over to 'accept' and block there
    time.sleep(3)

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:
        client_connection, client_address = listen_socket.accept()
        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()

if __name__ == '__main__':
    serve_forever()

启动服务器:

$ python webserver3e.py

用curl给修改过的服务器发送请求:

$ curl http://localhost:8888/hello

看看服务器怎么样:

发生了什么?因为错误EINTR调用accept失败。

子进程退出出发SIGCHLD事件然后父进程在调用accept时被锁住,父进程激活信号处理器完成工作后导致了系统调用accept中断:

别担心,这是个很好解决的问题。你需要的只是重启系统调用accept。这是修改的服务器用 webserver3f.py 来解决那个问题:

###########################################################################
# Concurrent server - webserver3f.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):
    pid, status = os.wait()

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()

启动webserver3f.py:

$ python webserver3f.py

用curl给服务器发送请求:

$ curl http://localhost:8888/hello

看到了吧?没有EINTR异常了。现在,确定没有僵尸进程同时SIGCHLD事件处理器等待并处理子进程终止。运行ps命令不会在意python进程是Z+状态(没有进程)。太好了,没有僵尸进程就安全了。

  • 如果你fork一个子进程却没有等待它,它会变僵尸进程
  • 用SIGCHLD事件处理器异步等待终止的子进程回收它的终止状态
  • 用事件处理器时你要记住系统调用可能终止,您需要为此做好准备方案

目前为止没什么问题,对吗?嗯,基本上是。在试试看webserver3f.py 但是不要用curl只发一个请求,用client3.py 发出128个同时连接:

$ python client3.py --max-clients 128

再次运行ps命令:

$ ps auxw | grep -i python | grep -v grep

看到了吧,天呐,僵尸进程又回来了!

这次是哪出错了?当运行128个链接且连接成功,服务器子进程处理请求并推出基本在同一时间,造成了SIGCHLD信号的洪流传向父进程。问题在于这些信号不排队,你的服务器就漏掉了一些信号,留下几个僵尸进程乱跑没人管:

解决方法是设一个SIGCHLD事件处理器但用WNOHANG来代替系统调用waitpid来排一个队,以确保所有终止进程都被处理。修改后代码 webserver3g.py:

###########################################################################
# 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()

启动服务器:

$ python webserver3g.py

用测试客户端client3.py:

$ python client3.py --max-clients 128

现在确认有没有僵尸进程。 好极了!生活是美好的:)

恭喜!这是一个相当漫长的旅程,但我希望你喜欢它。现在你有了简单的并发服务器,这代码可以作为在高曾次web服务器进一步的工作取的基础。

我把第二篇中WSGI服务器升级成并发服务器留给你当练习。你在这里可以找到修改的版本.但是只能在自己实现了之后看。你用完成它的所有信息,那就做吧:  )

下来时什么呢?就像Josh Billings说的

要像一张邮票,坚持一件事情直到你到达目的地。

从掌握的基楚开始,质疑你已经知道的,始终深入。

如果你仅仅学习方法,你将被被你的方法束缚。但是如果你学习原则,你可以设计自己的方法。—— Ralph Waldo Emerson

下面是我在这篇文章引用素材的书单。他们会帮你扩大并深入我在文章中提到的知识。

译文链接: http://www.codeceo.com/article/make-web-server-3.html
英文原文: Let’s Build A Web Server. Part 3
翻译作者: 码农网  – 王坚
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值