PUB:20160809 Part 3 - Let’s Build A Web Server

@StdioA 哈哈哈,终于校对发布了!~
This commit is contained in:
wxy 2016-10-09 14:47:51 +08:00
parent 18b012cd51
commit 4b27864360

View File

@ -1,13 +1,13 @@
translating by StdioA
搭个 Web 服务器(三)
=====================================
>“当我们必须创造时,才能够学到更多。” ——伯爵
>“当我们必须创造时,才能够学到更多。” ——皮亚杰
在本系列的第二部分中,你创造了一个可以处理基本 HTTP GET 请求的、朴素的 WSGI 服务器。当时我问了一个问题:“你该如何让你的服务器在同一时间处理多个请求呢?”在这篇文章中,你会找到答案。系好安全带,我们要认真起来,全速前进了!你将会体验到一段非常快速的旅程。准备好你的 LinuxMac OS X或者其他 *nix 系统),还有你的 Python. 本文中所有源代码均可在 [GitHub][1] 上找到。
在本系列的[第二部分](https://linux.cn/article-7685-1.html)中,你创造了一个可以处理基本 HTTP GET 请求的、朴素的 WSGI 服务器。当时我问了一个问题:“你该如何让你的服务器在同一时间处理多个请求呢?”在这篇文章中,你会找到答案。系好安全带,我们要认真起来,全速前进了!你将会体验到一段非常快速的旅程。准备好你的 Linux、Mac OS X或者其他 *nix 系统),还有你的 Python。本文中所有源代码均可在 [GitHub][1] 上找到。
首先,我们来回顾一下 Web 服务器的基本结构,以及服务器处理来自客户端的请求时,所需的必要步骤。你在第一及第二部分中创建的轮询服务器只能够在同一时间内处理一个请求。在处理完当前请求之前,它不能够打开一个新的客户端连接。所有请求为了等待服务都需要排队,在服务繁忙时,这个队伍可能会排的很长,一些客户端可能会感到不开心。
### 服务器的基本结构及如何处理请求
首先,我们来回顾一下 Web 服务器的基本结构,以及服务器处理来自客户端的请求时,所需的必要步骤。你在[第一部分](https://linux.cn/article-7662-1.html)及[第二部分](https://linux.cn/article-7685-1.html)中创建的轮询服务器只能够一次处理一个请求。在处理完当前请求之前,它不能够接受新的客户端连接。所有请求为了等待服务都需要排队,在服务繁忙时,这个队伍可能会排的很长,一些客户端可能会感到不开心。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it1.png)
@ -53,7 +53,7 @@ if __name__ == '__main__':
serve_forever()
```
为了观察到你的服务器在同一时间只能处理一个请求,我们对服务器的代码做一点点修改:在将响应发送至客户端之后,将程序阻塞 60 秒。这个修改只需要一行代码,来告诉服务器进程暂停 60 秒钟。
为了观察到你的服务器在同一时间只能处理一个请求的行为,我们对服务器的代码做一点点修改:在将响应发送至客户端之后,将程序阻塞 60 秒。这个修改只需要一行代码,来告诉服务器进程暂停 60 秒钟。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it2.png)
@ -84,7 +84,7 @@ HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
time.sleep(60) # 睡眠语句,阻塞该进程 60 秒
time.sleep(60) ### 睡眠语句,阻塞该进程 60 秒
def serve_forever():
@ -126,88 +126,85 @@ $ curl http://localhost:8888/hello
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it3.png)
当你等待足够长的时间60 秒以上)后,你会看到第一个 `curl` 程序完成,而第二个 `curl` 在屏幕上输出了“Hello, World!”,然后休眠 60 秒,进而停止运行
当你等待足够长的时间60 秒以上)后,你会看到第一个 `curl` 程序完成,而第二个 `curl` 在屏幕上输出了“Hello, World!”,然后休眠 60 秒,进而终止
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it4.png)
两个程序这样运行,是因为在服务器在处理完第一个来自 `curl` 的请求之后,只有等待 60 秒才能开始处理第二个请求。这个处理请求的过程按顺序进行(也可以说,迭代进行),一步一步进行,在我们刚刚给出的例子中,在同一时间内只能处理一个请求。
样运行的原因是因为在服务器在处理完第一个来自 `curl` 的请求之后,只有等待 60 秒才能开始处理第二个请求。这个处理请求的过程按顺序进行(也可以说,迭代进行),一步一步进行,在我们刚刚给出的例子中,在同一时间内只能处理一个请求。
现在,我们来简单讨论一下客户端与服务器的交流过程。为了让两个程序在网络中互相交流,它们必须使用套接字。你应当在本系列的前两部分中见过它几次了。但是,套接字是什么?
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_socket.png)
套接字是一个交互通道的端点的抽象形式,它可以让你的程序通过文件描述符来与其它程序进行交流。在这篇文章中,我只会单独讨论 Linux 或 Mac OS X 中的 TCP/IP 套接字。这里有一个重点概念需要你去理解TCP 套接字对。
套接字socket是一个通讯通道端点endpoint的抽象描述,它可以让你的程序通过文件描述符来与其它程序进行交流。在这篇文章中,我只会单独讨论 Linux 或 Mac OS X 中的 TCP/IP 套接字。这里有一个重点概念需要你去理解TCP 套接字对socket pair
> TCP 连接使用的套接字对是一个由 4 个元素组成的元组,它确定了 TCP 连接的两端:本地 IP 地址、本地端口、远端 IP 地址及远端端口。一个套接字对独一无二地确定了网络中的每一个 TCP 连接。在连接一端的两个值:一个 IP 地址和一个端口,通常被称作一个套接字。[1][4]
> TCP 连接使用的套接字对是一个由 4 个元素组成的元组,它确定了 TCP 连接的两端:本地 IP 地址、本地端口、远端 IP 地址及远端端口。一个套接字对唯一地确定了网络中的每一个 TCP 连接。在连接一端的两个值:一个 IP 地址和一个端口,通常被称作一个套接字。(引自[《UNIX 网络编程 卷1:套接字联网 API 第3版][4]
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_socketpair.png)
所以,元组 {10.10.10.2:49152, 12.12.12.3:8888} 就是一个能够在客户端确定 TCP 连接两端的套接字对,而元组 {12.12.12.3:8888, 10.10.10.2:49152} 则是在服务端确定 TCP 连接两端的套接字对。在这个例子中,确定 TCP 服务端的两个值IP 地址 `12.12.12.3` 及端口 `8888`),代表一个套接字;另外两个值则代表客户端的套接字。
所以,元组 `{10.10.10.2:49152, 12.12.12.3:8888}` 就是一个能够在客户端确定 TCP 连接两端的套接字对,而元组 `{12.12.12.3:8888, 10.10.10.2:49152}` 则是在服务端确定 TCP 连接两端的套接字对。在这个例子中,确定 TCP 服务端的两个值IP 地址 `12.12.12.3` 及端口 `8888`),代表一个套接字;另外两个值则代表客户端的套接字。
一个服务器创建一个套接字并开始建立连接的基本工作流程如下:
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_server_socket_sequence.png)
1. 服务器创建一个 TCP/IP 套接字。我们可以用下面那条 Python 语句来创建:
1. 服务器创建一个 TCP/IP 套接字。我们可以用条 Python 语句来创建:
```
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
```
```
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
```
2. 服务器可能会设定一些套接字选项(这个步骤是可选的,但是你可以看到上面的服务器代码做了设定,这样才能够在重启服务器时多次复用同一地址):
2. 服务器可能会设定一些套接字选项(这个步骤是可选的,但是你可以看到上面的服务器代码做了设定,这样才能够在重启服务器时多次复用同一地址)。
```
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
```
3. 然后,服务器绑定一个地址。绑定函数可以将一个本地协议地址赋给套接字。若使用 TCP 协议,调用绑定函数时,需要指定一个端口号,一个 IP 地址,或两者兼有,或两者兼无。[1][4]
```
listen_socket.bind(SERVER_ADDRESS)
```
```
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
```
3. 然后,服务器绑定一个地址。绑定函数 `bind` 可以将一个本地协议地址赋给套接字。若使用 TCP 协议,调用绑定函数 `bind` 时,需要指定一个端口号,一个 IP 地址,或两者兼有,或两者全无。(引自[《UNIX网络编程 卷1套接字联网 API 第3版》][4]
```
listen_socket.bind(SERVER_ADDRESS)
```
4. 然后,服务器开启套接字的监听模式。
```
listen_socket.listen(REQUEST_QUEUE_SIZE)
```
```
listen_socket.listen(REQUEST_QUEUE_SIZE)
```
监听函数只应在服务端调用。它会通知操作系统内核,标明它会接受所有向该套接字发送请求的链接
监听函数 `listen` 只应在服务端调用。它会通知操作系统内核,表明它会接受所有向该套接字发送的入站连接请求
以上四步完成后,服务器将循环接收来自客户端的连接,一次循环处理一条。当有连接可用时,`accept` 函数将会返回一个已连接的客户端套接字。然后,服务器从客户端套接字中读取请求数据,将它在标准输出流中打印出来,并向客户端回送一条消息。然后,服务器会关闭这个客户端连接,并准备接收一个新的客户端连接。
以上四步完成后,服务器将循环接收来自客户端的连接,一次循环处理一条。当有连接可用时,接受请求函数 `accept` 将会返回一个已连接的客户端套接字。然后,服务器从这个已连接的客户端套接字中读取请求数据,将数据在其标准输出流中输出出来,并向客户端回送一条消息。然后,服务器会关闭这个客户端连接,并准备接收一个新的客户端连接。
这是客户端使用 TCP/IP 协议与服务器通信的必要步骤:
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_client_socket_sequence.png)
下面是一段示例代码,使用这段代码,客户端可以连接你的服务器,发送一个请求,并打印响应内容:
下面是一段示例代码,使用这段代码,客户端可以连接你的服务器,发送一个请求,并输出响应内容:
```
import socket
# 创建一个套接字,并连接值服务器
### 创建一个套接字,并连接值服务器
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 8888))
# 发送一段数据,并接收响应数据
### 发送一段数据,并接收响应数据
sock.sendall(b'test')
data = sock.recv(1024)
print(data.decode())
```
在创建套接字后,客户端需要连接至服务器。我们可以调用 `connect` 函数来完成这个操作:
在创建套接字后,客户端需要连接至服务器。我们可以调用连接函数 `connect` 来完成这个操作:
```
sock.connect(('localhost', 8888))
```
客户端只需提供待连接服务器的 IP 地址(或主机名),及端口号,即可连接至远端服务器。
客户端只需提供待连接的远程服务器的 IP 地址(或主机名),及端口号,即可连接至远端服务器。
你可能已经注意到了,客户端不需要调用 `bind``accept` 函数,就可以与服务器建立连接。客户端不需要调用 `bind` 函数是因为客户端不需要关注本地 IP 地址及端口号。操作系统内核中的 TCP/IP 协议栈会在客户端调用 `connect` 函数时,自动为套接字分配本地 IP 地址及本地端口号。这个本地端口被称为临时端口,也就是一个短暂开放的端口。
你可能已经注意到了,客户端不需要调用 `bind``accept` 函数,就可以与服务器建立连接。客户端不需要调用 `bind` 函数是因为客户端不需要关注本地 IP 地址及端口号。操作系统内核中的 TCP/IP 协议栈会在客户端调用 `connect` 函数时,自动为套接字分配本地 IP 地址及本地端口号。这个本地端口被称为临时端口ephemeral port一个短暂开放的端口。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_ephemeral_port.png)
服务器中有一些端口被用于承载一些众所周知的服务,它们被称作通用端口:如 80 端口用于 HTTP 服务22 端口用于 SSH 服务。打开你的 Python shell与你在本地运行的服务器建立一个连接来看看内核给你的客户端套接字分配了哪个临时端口在尝试这个例子之前你需要运行服务器程序 `webserver3a.py``webserver3b.py`
服务器中有一些端口被用于承载一些众所周知的服务,它们被称作通用well-known端口:如 80 端口用于 HTTP 服务22 端口用于 SSH 服务。打开你的 Python shell与你在本地运行的服务器建立一个连接来看看内核给你的客户端套接字分配了哪个临时端口在尝试这个例子之前你需要运行服务器程序 `webserver3a.py``webserver3b.py`
```
>>> import socket
@ -222,12 +219,11 @@ sock.connect(('localhost', 8888))
在我开始回答我在第二部分中提出的问题之前,我还需要快速讲解一些概念。你很快就会明白这些概念为什么非常重要。这两个概念,一个是进程,另外一个是文件描述符。
什么是进程?进程就是一个程序执行的实体。举个例子:当你的服务器代码被执行时,它会被载入内存,而内存中表现此次程序运行的实体就叫做进程。内核记录了进程的一系列有关信息——比如进程 ID——来追踪它的运行情况。当你在执行轮询服务器 `webserver3a.py``webserver3b.py` 时,你只启动了一个进程。
什么是进程?进程就是一个程序执行的实体。举个例子:当你的服务器代码被执行时,它会被载入内存,而内存中表现此次程序运行的实体就叫做进程。内核记录了进程的一系列有关信息——比如进程 ID——来追踪它的运行情况。当你在执行轮询服务器 `webserver3a.py``webserver3b.py` 时,你其实启动了一个进程。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_server_process.png)
我们在终端窗口中运行 `webserver3b.py`
Start the server webserver3b.py in a terminal window:
```
$ python webserver3b.py
@ -240,7 +236,7 @@ $ ps | grep webserver3b | grep -v grep
7182 ttys003 0:00.04 python webserver3b.py
```
`ps` 命令显示,我们刚刚只运行了一个 Python 进程 `webserver3b`。当一个进程被创建时,内核会为其分配一个进程 ID也就是 PID。在 UNIX 中,所有用户进程都有一个父进程;当然,这个父进程也有进程 ID叫做父进程 ID缩写为 PPID。假设你默认使用 BASH shell那当你启动服务器时一个新的进程会被启动,同时被赋予一个 PID而它的父进程 PID 会被设为 BASH shell 的 PID。
`ps` 命令显示,我们刚刚只运行了一个 Python 进程 `webserver3b.py`。当一个进程被创建时,内核会为其分配一个进程 ID也就是 PID。在 UNIX 中,所有用户进程都有一个父进程;当然,这个父进程也有进程 ID叫做父进程 ID缩写为 PPID。假设你默认使用 BASH shell那当你启动服务器时就会启动一个新的进程,同时被赋予一个 PID而它的父进程 PID 会被设为 BASH shell 的 PID。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_ppid_pid.png)
@ -248,11 +244,11 @@ $ ps | grep webserver3b | grep -v grep
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_pid_ppid_screenshot.png)
另外一个需要了解的概念,就是文件描述符。什么是文件描述符?文件描述符是一个非负整数,当进程打开一个现有文件、创建新文件或创建一个新的套接字时,内核会将这个数返回给进程。你以前可能听说过,在 UNIX 中,一切皆是文件。内核会根据一个文件描述符来为一个进程打开一个文件。当你需要读取文件或向文件写入时我们同样通过文件描述符来定位这个文件。Python 提供了高层次的文件(或套接字)对象,所以你不需要直接通过文件描述符来定位文件。但是,在高层对象之下,我们就是用它来在 UNIX 中定位文件及套接字:整形的文件描述符。
另外一个需要了解的概念,就是文件描述符。什么是文件描述符?文件描述符是一个非负整数,当进程打开一个现有文件、创建新文件或创建一个新的套接字时,内核会将这个数返回给进程。你以前可能听说过,在 UNIX 中,一切皆是文件。内核会按文件描述符来找到一个进程所打开的文件。当你需要读取文件或向文件写入时我们同样通过文件描述符来定位这个文件。Python 提供了高层次的操作文件(或套接字)对象,所以你不需要直接通过文件描述符来定位文件。但是,在高层对象之下,我们就是用它来在 UNIX 中定位文件及套接字,通过这个整数的文件描述符。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_process_descriptors.png)
一般情况下UNIX shell 会将一个进程的标准输入流的文件描述符设为 0标准输出流设为 1而标准错误打印的文件描述符会被设为 2。
一般情况下UNIX shell 会将一个进程的标准输入流STDIN的文件描述符设为 0标准输出流STDOUT设为 1而标准错误打印STDERR的文件描述符会被设为 2。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_it_default_descriptors.png)
@ -289,7 +285,7 @@ hello
3
```
我还想再提一件事:不知道你有没有注意到,在我们的第二个轮询服务器 `webserver3b.py` 中,当你的服务器休眠 60 秒的过程中,你仍然可以通过第二个 `curl` 命令连接至服务器。当然 `curl` 命令并没有立刻输出任何内容而是挂在哪里,但是既然服务器没有接受连接,那它为什么不立即拒绝掉连接,而让它还能够继续与服务器建立连接呢?这个问题的答案是:当我在调用套接字对象的 `listen` 方法时,我为该方法提供了一个 `BACKLOG` 参数,在代码中用 `REQUEST_QUEUE_SIZE` 量来表示。`BACKLOG` 参数决定了在内核中为存放即将到来的连接请求所创建的队列的大小。当服务器 `webserver3b.py` 被挂起的时候,你运行的第二个 `curl` 命令依然能够连接至服务器,因为内核中用来存放即将接收的连接请求的队列依然拥有足够大的可用空间。
我还想再提一件事:不知道你有没有注意到,在我们的第二个轮询服务器 `webserver3b.py` 中,当你的服务器休眠 60 秒的过程中,你仍然可以通过第二个 `curl` 命令连接至服务器。当然 `curl` 命令并没有立刻输出任何内容而是挂在哪里,但是既然服务器没有接受连接,那它为什么不立即拒绝掉连接,而让它还能够继续与服务器建立连接呢?这个问题的答案是:当我在调用套接字对象的 `listen` 方法时,我为该方法提供了一个 `BACKLOG` 参数,在代码中用 `REQUEST_QUEUE_SIZE` 量来表示。`BACKLOG` 参数决定了在内核中为存放即将到来的连接请求所创建的队列的大小。当服务器 `webserver3b.py` 在睡眠的时候,你运行的第二个 `curl` 命令依然能够连接至服务器,因为内核中用来存放即将接收的连接请求的队列依然拥有足够大的可用空间。
尽管增大 `BACKLOG` 参数并不能神奇地使你的服务器同时处理多个请求,但当你的服务器很繁忙时,将它设置为一个较大的值还是相当重要的。这样,在你的服务器调用 `accept` 方法时,不需要再等待一个新的连接建立,而可以立刻直接抓取队列中的第一个客户端连接,并不加停顿地立刻处理它。
@ -297,7 +293,7 @@ hello
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_checkpoint.png)
- 迭代服务器
- 轮询服务器
- 服务端套接字创建流程(创建套接字,绑定,监听及接受)
- 客户端连接创建流程(创建套接字,连接)
- 套接字对
@ -308,6 +304,8 @@ hello
- 文件描述符
- 套接字的 `listen` 方法中,`BACKLOG` 参数的含义
### 如何并发处理多个请求
现在,我可以开始回答第二部分中的那个问题了:“你该如何让你的服务器在同一时间处理多个请求呢?”或者换一种说法:“如何编写一个并发服务器?”
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc2_service_clients.png)
@ -368,13 +366,13 @@ def serve_forever():
while True:
client_connection, client_address = listen_socket.accept()
pid = os.fork()
if pid == 0: # 子进程
listen_socket.close() # 关闭子进程中复制的套接字对象
if pid == 0: ### 子进程
listen_socket.close() ### 关闭子进程中复制的套接字对象
handle_request(client_connection)
client_connection.close()
os._exit(0) # 子进程在这里退出
else: # 父进程
client_connection.close() # 关闭父进程中的客户端连接对象,并循环执行
os._exit(0) ### 子进程在这里退出
else: ### 父进程
client_connection.close() ### 关闭父进程中的客户端连接对象,并循环执行
if __name__ == '__main__':
serve_forever()
@ -386,13 +384,13 @@ if __name__ == '__main__':
$ python webserver3c.py
```
然后,像我们之前测试轮询服务器那样,运行两个 `curl` 命令,来看看这次的效果。现在你可以看到,即使子进程在处理客户端请求后会休眠 60 秒,但它并不会影响其它客户端连接,因为他们都是由完全独立的进程来处理的。你应该看到你的 `curl` 命令立即输出了“Hello, World!”然后挂起 60 秒。你可以按照你的想法运行尽可能多的 `curl` 命令(好吧,并不能运行特别特别多 ^_^所有的命令都会立刻输出来自服务器的响应“Hello, World!”,并不会出现任何可被察觉到的延迟行为。试试看吧。
然后,像我们之前测试轮询服务器那样,运行两个 `curl` 命令,来看看这次的效果。现在你可以看到,即使子进程在处理客户端请求后会休眠 60 秒,但它并不会影响其它客户端连接,因为他们都是由完全独立的进程来处理的。你应该看到你的 `curl` 命令立即输出了“Hello, World!”然后挂起 60 秒。你可以按照你的想法运行尽可能多的 `curl` 命令(好吧,并不能运行特别特别多 `^_^`),所有的命令都会立刻输出来自服务器的响应 “Hello, World!”,并不会出现任何可被察觉到的延迟行为。试试看吧。
如果你要理解 `fork()`,那最重要的一点是:你调用了它一次,但是它会返回两次:一次在父进程中,另一次是在子进程中。当你创建了一个新进程,那么 `fork()` 在子进程中的返回值是 0。如果是在父进程中`fork()` 函数会返回子进程的 PID。
如果你要理解 `fork()`,那最重要的一点是:**你调用了它一次,但是它会返回两次** —— 一次在父进程中,另一次是在子进程中。当你创建了一个新进程,那么 `fork()` 在子进程中的返回值是 0。如果是在父进程中`fork()` 函数会返回子进程的 PID。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc2_how_fork_works.png)
我依然记得在第一次看到它并尝试使用 `fork()` 的时候,我是多么的入迷。它在我眼里就像是魔法一样。这就好像我在读一段顺序执行的代码,然后“砰”地一声,代码变成了两份,然后出现了两个实体,同时并行地运行相同的代码。讲真,那个时候我觉得它真的跟魔法一样神奇。
我依然记得在第一次看到它并尝试使用 `fork()` 的时候,我是多么的入迷。它在我眼里就像是魔法一样。这就好像我在读一段顺序执行的代码,然后“砰”地一声,代码变成了两份,然后出现了两个实体,同时并行地运行相同的代码。讲真,那个时候我觉得它真的跟魔法一样神奇。
当父进程创建出一个新的子进程时,子进程会复制从父进程中复制一份文件描述符:
@ -401,38 +399,39 @@ $ python webserver3c.py
你可能注意到,在上面的代码中,父进程关闭了客户端连接:
```
else: # parent
else: ### parent
client_connection.close() # close parent copy and loop over
```
不过,既然父进程关闭了这个套接字,那为什么子进程仍然能够从来自客户端的套接字中读取数据呢?答案就在上面的图片中。内核会使用描述符引用计数器来决定是否要关闭一个套接字。当你的服务器创建一个子进程时,子进程会复制父进程的所有文件描述符,内核中描述符的引用计数也会增加。如果只有一个父进程及一个子进程,那客户端套接字的文件描述符引用数应为 2当父进程关闭客户端连接的套接字时内核只会减少它的引用计数将其变为 1但这仍然不会使内核关闭该套接字。子进程也关闭了父进程中 `listen_socket` 的复制实体,因为子进程不需要关注新的客户端连接,而只需要处理已建立的客户端连接中的请求。
不过,既然父进程关闭了这个套接字,那为什么子进程仍然能够从来自客户端的套接字中读取数据呢?答案就在上面的图片中。内核会使用描述符引用计数器来决定是否要关闭一个套接字。当你的服务器创建一个子进程时,子进程会复制父进程的所有文件描述符,内核中描述符的引用计数也会增加。如果只有一个父进程及一个子进程,那客户端套接字的文件描述符引用数应为 2当父进程关闭客户端连接的套接字时内核只会减少它的引用计数将其变为 1但这仍然不会使内核关闭该套接字。子进程也关闭了父进程中 `listen_socket` 的复制实体,因为子进程不需要关注新的客户端连接,而只需要处理已建立的客户端连接中的请求。
```
listen_socket.close() # 关闭子进程中的复制实体
listen_socket.close() ### 关闭子进程中的复制实体
```
我们将会在后文中讨论,如果你不关闭那些重复的描述符,会发生什么。
你可以从你的并发服务器源码看到,父进程的主要职责为:接受一个新的客户端连接,复制出一个子进程来处理这个连接,然后继续循环来接受另外的客户端连接,仅此而已。服务器父进程并不会处理客户端连接——子进程才会做这件事。
你可以从你的并发服务器源码看到,父进程的主要职责为:接受一个新的客户端连接,复制出一个子进程来处理这个连接,然后继续循环来接受另外的客户端连接,仅此而已。服务器父进程并不会处理客户端连接——子进程才会做这件事。
打个岔:当我们说两个事件并发执行时,我们在说什么?
A little aside. What does it mean when we say that two events are concurrent?
打个岔:当我们说两个事件并发执行时,我们所要表达的意思是什么?
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc2_concurrent_events.png)
当我们说“两个事件并发执行”时,它通常意味着这两个事件同时发生。简单来讲,这个定义没问题,但你应该记住它的严格定义:
> 如果你阅读代码时,无法判断两个事件的发生顺序,那这两个事件就是并发执行的。[2][5]
> 如果你不能在代码中判断两个事件的发生顺序,那这两个事件就是并发执行的。(引自[《信号系统简明手册 (第二版): 并发控制深入浅出及常见错误》][5]
好的,现在你又该回顾一下你刚刚学过的知识点了。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_checkpoint.png)
- 在 Unix 中,编写一个并发服务器的最简单的方式——使用 `fork()` 系统调用;
- 当一个进程复制出另一个进程时,它会变成刚刚复制出的进程的父进程;
- 当一个进程分叉(`fork`)出另一个进程时,它会变成刚刚分叉出的进程的父进程;
- 在进行 `fork` 调用后,父进程和子进程共享相同的文件描述符;
- 系统内核通过描述符引用计数来决定是否要关闭该描述符对应的文件或套接字;
- 服务器父进程的主要职责:现在它做的只是从客户端接受一个新的连接,复制出子进程来处理这个客户端连接,然后开始下一轮循环,去接收新的客户端连接。
- 系统内核通过描述符的引用计数来决定是否要关闭该描述符对应的文件或套接字;
- 服务器父进程的主要职责:现在它做的只是从客户端接受一个新的连接,分叉出子进程来处理这个客户端连接,然后开始下一轮循环,去接收新的客户端连接。
### 进程分叉后不关闭重复的套接字会发生什么?
我们来看看,如果我们不在父进程与子进程中关闭重复的套接字描述符会发生什么。下面是刚才的并发服务器代码的修改版本,这段代码(`webserver3d.py` 中,服务器不会关闭重复的描述符):
@ -470,15 +469,15 @@ def serve_forever():
clients = []
while True:
client_connection, client_address = listen_socket.accept()
# 将引用存储起来,否则在下一轮循环时,他们会被垃圾回收机制销毁
### 将引用存储起来,否则在下一轮循环时,他们会被垃圾回收机制销毁
clients.append(client_connection)
pid = os.fork()
if pid == 0: # 子进程
listen_socket.close() # 关闭子进程中多余的套接字
if pid == 0: ### 子进程
listen_socket.close() ### 关闭子进程中多余的套接字
handle_request(client_connection)
client_connection.close()
os._exit(0) # 子进程在这里结束
else: # 父进程
os._exit(0) ### 子进程在这里结束
else: ### 父进程
# client_connection.close()
print(len(clients))
@ -503,7 +502,7 @@ Hello, World!
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc3_child_is_active.png)
所以,为什么 `curl` 不终止呢?原因就在于多余的文件描述符。当子进程关闭客户端连接时,系统内核会减少客户端套接字的引用计数,将其变为 1。服务器子进程退出了但客户端套接字并没有被内核关闭因为该套接字的描述符引用计数并没有变为 0所以这就导致了连接终止包在 TCP/IP 协议中称作 `FIN`)不会被发送到客户端,所以客户端会一直保持连接。这里就会出现另一个问题:如果你的服务器在长时间运行,并且不关闭重复的文件描述符,那么可用的文件描述符会被消耗殆尽:
所以,为什么 `curl` 不终止呢?原因就在于文件描述符的副本。当子进程关闭客户端连接时,系统内核会减少客户端套接字的引用计数,将其变为 1。服务器子进程退出了但客户端套接字并没有被内核关闭因为该套接字的描述符引用计数并没有变为 0所以这就导致了连接终止包在 TCP/IP 协议中称作 `FIN`)不会被发送到客户端,所以客户端会一直保持连接。这里也会出现另一个问题:如果你的服务器长时间运行,并且不关闭文件描述符的副本,那么可用的文件描述符会被消耗殆尽:
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc3_out_of_descriptors.png)
@ -529,7 +528,7 @@ virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
```
你可以从上面的结果看到,在我的 Ubuntu box 中,系统为我的服务器进程分配的最大可用文件描述符(文件打开)数为 1024。
你可以从上面的结果看到,在我的 Ubuntu 机器中,系统为我的服务器进程分配的最大可用文件描述符(文件打开)数为 1024。
现在我们来看一看,如果你的服务器不关闭重复的描述符,它会如何消耗可用的文件描述符。在一个已有的或新建的终端窗口中,将你的服务器进程的最大可用文件描述符设为 256
@ -607,15 +606,18 @@ if __name__ == '__main__':
$ python client3.py --max-clients=300
```
过一会,你的服务器就该爆了。这是我的环境中出现的异常截图:
过一会,你的服务器进程就该爆了。这是我的环境中出现的异常截图:
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc3_too_many_fds_exc.png)
这个例子很明显——你的服务器应该关闭重复的描述符。但是,即使你关闭了多余的描述符,你依然没有摆脱险境,因为你的服务器还有一个问题,这个问题在于“僵尸”!
这个例子很明显——你的服务器应该关闭描述符副本。
#### 僵尸进程
但是即使你关闭了描述符副本你依然没有摆脱险境因为你的服务器还有一个问题这个问题在于“僵尸zombies
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc3_zombies.png)
没错,这个服务器代码确实在制造僵尸进程。我们来看看怎么回事。重新运行你的服务器:
```
@ -636,13 +638,13 @@ vagrant 9099 0.0 1.2 31804 6256 pts/0 S+ 16:33 0:00 python webserve
vagrant 9102 0.0 0.0 0 0 pts/0 Z+ 16:33 0:00 [python] <defunct>
```
你看到第二行中pid 为 9102状态为 Z+,名字里面有个 `<defunct>` 的进程了吗?那就是我们的僵尸进程。这个僵尸进程的问题在于:你无法将它杀掉
你看到第二行中pid 为 9102状态为 `Z+`,名字里面有个 `<defunct>` 的进程了吗?那就是我们的僵尸进程。这个僵尸进程的问题在于:你无法将它杀掉
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc3_kill_zombie.png)
就算你尝试使用 `kill -9` 来杀死僵尸进程,它们仍旧会存活。自己试试看,看看结果。
这个僵尸到底是什么,为什么我们的服务器会造出它们呢?一个僵尸进程是一个已经结束的进程,但它的父进程并没有等待它结束,并且也没有收到它的终结状态。如果一个进程在父进程退出之前退出,系统内核会把它变为一个僵尸进程,存储它的部分信息,以便父进程读取。内核保存的进程信息通常包括进程 ID进程终止状态以及进程的资源占用情况。OK所以僵尸进程确实有存在的意义但如果服务器不管这些僵尸进程你的系统调用将会被阻塞。我们来看看这个要如何发生。首先,关闭你的服务器;然后,在一个新的终端窗口中,使用 `ulimit` 命令将最大用户进程数设为 400同时要确保你的最大可用描述符数大于这个数字我们在这里设为 500
这个僵尸到底是什么,为什么我们的服务器会造出它们呢?一个僵尸进程zombie是一个已经结束的进程,但它的父进程并没有等待`waited`它结束,并且也没有收到它的终结状态。如果一个进程在父进程退出之前退出,系统内核会把它变为一个僵尸进程,存储它的部分信息,以便父进程读取。内核保存的进程信息通常包括进程 ID进程终止状态以及进程的资源占用情况。OK所以僵尸进程确实有存在的意义但如果服务器不管这些僵尸进程你的系统将会被壅塞。我们来看看这个会如何发生。首先,关闭你运行的服务器;然后,在一个新的终端窗口中,使用 `ulimit` 命令将最大用户进程数设为 400同时要确保你的最大可用描述符数大于这个数字我们在这里设为 500
```
$ ulimit -u 400
@ -661,33 +663,35 @@ $ python webserver3d.py
$ python client3.py --max-clients=500
```
然后,过一会,你的服务器应该会再次爆炸,它会在创建新进程时抛出一个 `OSError: 资源暂时不可用` 异常。但它并没有达到系统允许的最大进程数。这是我的环境中输出的异常信息截图:
然后,过一会,你的服务器进程应该会再次爆了,它会在创建新进程时抛出一个 `OSError: 资源暂时不可用` 异常。但它并没有达到系统允许的最大进程数。这是我的环境中输出的异常信息截图:
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc3_resource_unavailable.png)
你可以看到,如果服务器不管僵尸进程,它们会引发问题。我会简单探讨一下僵尸进程问题的解决方案。
你可以看到,如果服务器不管僵尸进程,它们会引发问题。接下来我会简单探讨一下僵尸进程问题的解决方案。
我们来回顾一下你刚刚掌握的知识点:
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_checkpoint.png)
- 如果你不关闭重复的描述符,客户端就不会在请求处理完成后终止,因为客户端连接没有被关闭;
- 如果你不关闭重复的描述符,长久运行的服务器最终会把可用的文件描述符(最大文件打开数)消耗殆尽;
- 当你创建一个新进程,而父进程不等待子进程,也不在子进程结束后收集它的终止状态,它会变为一个僵尸进程;
- 僵尸通常都会吃东西,在我们的例子中,僵尸进程会占用资源。如果你的服务器不管僵尸进程,它最终会消耗掉所有的可用进程(最大用户进程数);
- 你不能杀死僵尸进程,你需要等待它。
- 如果你不关闭文件描述符副本,客户端就不会在请求处理完成后终止,因为客户端连接没有被关闭;
- 如果你不关闭文件描述符副本,长久运行的服务器最终会把可用的文件描述符(最大文件打开数)消耗殆尽;
- 当你创建一个新进程,而父进程不等待`wait`子进程,也不在子进程结束后收集它的终止状态,它会变为一个僵尸进程;
- 僵尸通常都会吃东西,在我们的例子中,僵尸进程会吃掉资源。如果你的服务器不管僵尸进程,它最终会消耗掉所有的可用进程(最大用户进程数);
- 你不能杀死`kill`僵尸进程,你需要等待`wait`它。
所以,你需要做什么来处理僵尸进程呢?你需要修改你的服务器代码,来等待僵尸进程,并收集它们的终止信息。你可以在代码中使用系统调用 `wait` 来完成这个任务。不幸的是,这个方法里理想目标还很远,因为在没有终止的子进程存在的情况下调用 `wait` 会导致程序阻塞,这会阻碍你的服务器处理新的客户端连接请求。那么,我们有其他选择吗?嗯,有的,其中一个解决方案需要结合信号处理以及 `wait` 系统调用。
### 如何处理僵尸进程?
所以,你需要做什么来处理僵尸进程呢?你需要修改你的服务器代码,来等待(`wait`)僵尸进程,并收集它们的终止信息。你可以在代码中使用系统调用 `wait` 来完成这个任务。不幸的是,这个方法离理想目标还很远,因为在没有终止的子进程存在的情况下调用 `wait` 会导致服务器进程阻塞,这会阻碍你的服务器处理新的客户端连接请求。那么,我们有其他选择吗?嗯,有的,其中一个解决方案需要结合信号处理以及 `wait` 系统调用。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc4_signaling.png)
这是它的工作流程。当一个子进程退出时,内核会发送 `SIGCHLD` 信号。父进程可以设置一个信号处理器,它可以异步响应 `SIGCHLD` 信号,并在信号响应函数中等待子进程收集终止信息,从而阻止了僵尸进程的存在。
这是它的工作流程。当一个子进程退出时,内核会发送 `SIGCHLD` 信号。父进程可以设置一个信号处理器,它可以异步响应 `SIGCHLD` 信号,并在信号响应函数中等待`wait`子进程收集终止信息,从而阻止了僵尸进程的存在。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part_conc4_sigchld_async.png)
顺便,异步事件意味着父进程无法提前知道事件的发生时间。
顺便说一下,异步事件意味着父进程无法提前知道事件的发生时间。
修改你的服务器代码,设置一个 `SIGCHLD` 信号处理器在信号处理器中等待终止的子进程。修改后的代码如下webserver3e.py
修改你的服务器代码,设置一个 `SIGCHLD` 信号处理器,在信号处理器中等待`wait`终止的子进程。修改后的代码如下webserver3e.py
```
#######################################################
@ -722,7 +726,7 @@ HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
# 挂起进程,来允许父进程完成循环,并在 "accept" 处阻塞
### 挂起进程,来允许父进程完成循环,并在 "accept" 处阻塞
time.sleep(3)
@ -738,12 +742,12 @@ def serve_forever():
while True:
client_connection, client_address = listen_socket.accept()
pid = os.fork()
if pid == 0: # 子进程
listen_socket.close() # 关闭子进程中多余的套接字
if pid == 0: ### 子进程
listen_socket.close() ### 关闭子进程中多余的套接字
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # 父进程
else: ### 父进程
client_connection.close()
if __name__ == '__main__':
@ -766,7 +770,7 @@ $ curl http://localhost:8888/hello
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc4_eintr.png)
刚刚发生了什么?`accept` 调用失败了,错误信息为 `EINTR`
刚刚发生了什么?`accept` 调用失败了,错误信息为 `EINTR`
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc4_eintr_error.png)
@ -822,20 +826,20 @@ def serve_forever():
client_connection, client_address = listen_socket.accept()
except IOError as e:
code, msg = e.args
# 若 'accept' 被打断,那么重启它
### 若 'accept' 被打断,那么重启它
if code == errno.EINTR:
continue
else:
raise
pid = os.fork()
if pid == 0: # 子进程
listen_socket.close() # 关闭子进程中多余的描述符
if pid == 0: ### 子进程
listen_socket.close() ### 关闭子进程中多余的描述符
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # 父进程
client_connection.close() # 关闭父进程中多余的描述符,继续下一轮循环
else: ### 父进程
client_connection.close() ### 关闭父进程中多余的描述符,继续下一轮循环
if __name__ == '__main__':
@ -854,7 +858,7 @@ $ python webserver3f.py
$ curl http://localhost:8888/hello
```
看到了吗?没有 EINTR 异常出现了。现在检查一下,确保没有僵尸进程存活,调用 `wait` 函数的 `SIGCHLD` 信号处理器能够正常处理被终止的子进程。我们只需使用 `ps` 命令,然后看看现在没有处于 Z+ 状态(或名字包含 `<defunct>` )的 Python 进程就好了。很棒!僵尸进程没有了,我们很安心。
看到了吗?没有 EINTR 异常出现了。现在检查一下,确保没有僵尸进程存活,调用 `wait` 函数的 `SIGCHLD` 信号处理器能够正常处理被终止的子进程。我们只需使用 `ps` 命令,然后看看现在没有处于 `Z+` 状态(或名字包含 `<defunct>` )的 Python 进程就好了。很棒!僵尸进程没有了,我们很安心。
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_checkpoint.png)
@ -862,6 +866,8 @@ $ curl http://localhost:8888/hello
- 使用 `SIGCHLD` 信号处理器可以异步地等待子进程终止,并收集其终止状态;
- 当使用事件处理器时,你需要牢记,系统调用可能会被打断,所以你需要处理这种情况发生时带来的异常。
#### 正确处理 SIGCHLD 信号
好的,一切顺利。是不是没问题了?额,几乎是。重新尝试运行 `webserver3f.py` 但我们这次不会只发送一个请求,而是同步创建 128 个连接:
```
@ -882,7 +888,7 @@ $ ps auxw | grep -i python | grep -v grep
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc5_signals_not_queued.png)
这个问题的解决方案依然是设置 `SIGCHLD` 事件处理器。但我们这次将会用 `WNOHANG` 参数循环调用 `waitpid`,来保证所有处于终止状态的子进程都会被处理。下面是修改后的代码,`webserver3g.py`
这个问题的解决方案依然是设置 `SIGCHLD` 事件处理器。但我们这次将会用 `WNOHANG` 参数循环调用 `waitpid` 来替代 `wait`,以保证所有处于终止状态的子进程都会被处理。下面是修改后的代码,`webserver3g.py`
```
#######################################################
@ -904,13 +910,13 @@ def grim_reaper(signum, frame):
while True:
try:
pid, status = os.waitpid(
-1, # 等待所有子进程
os.WNOHANG # 无终止进程时,不阻塞进程,并抛出 EWOULDBLOCK 错误
-1, ### 等待所有子进程
os.WNOHANG ### 无终止进程时,不阻塞进程,并抛出 EWOULDBLOCK 错误
)
except OSError:
return
if pid == 0: # 没有僵尸进程存在了
if pid == 0: ### 没有僵尸进程存在了
return
@ -939,20 +945,20 @@ def serve_forever():
client_connection, client_address = listen_socket.accept()
except IOError as e:
code, msg = e.args
# 若 'accept' 被打断,那么重启它
### 若 'accept' 被打断,那么重启它
if code == errno.EINTR:
continue
else:
raise
pid = os.fork()
if pid == 0: # 子进程
listen_socket.close() # 关闭子进程中多余的描述符
if pid == 0: ### 子进程
listen_socket.close() ### 关闭子进程中多余的描述符
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # 父进程
client_connection.close() # 关闭父进程中多余的描述符,继续下一轮循环
else: ### 父进程
client_connection.close() ### 关闭父进程中多余的描述符,继续下一轮循环
if __name__ == '__main__':
serve_forever()
@ -974,13 +980,15 @@ $ python client3.py --max-clients 128
![](https://ruslanspivak.com/lsbaws-part3/lsbaws_part3_conc5_no_zombies.png)
恭喜!你刚刚经历了一段很长的旅程,我希望你能够喜欢它。现在你拥有了自己的建议并发服务器,并且这段代码能够为你在继续研究生产级 Web 服务器的路上奠定基础。
### 大功告成
恭喜!你刚刚经历了一段很长的旅程,我希望你能够喜欢它。现在你拥有了自己的简易并发服务器,并且这段代码能够为你在继续研究生产级 Web 服务器的路上奠定基础。
我将会留一个作业:你需要将第二部分中的 WSGI 服务器升级,将它改造为一个并发服务器。你可以在[这里][12]找到更改后的代码。但是,当你实现了自己的版本之后,你才应该来看我的代码。你已经拥有了实现这个服务器所需的所有信息。所以,快去实现它吧 ^_^
然后要做什么呢?乔希·比林斯说过:
> “我们应该做一枚邮票——专注于一件事,不达目的不罢休。”
> “就像一枚邮票一样——专注于一件事,不达目的不罢休。”
开始学习基本知识。回顾你已经学过的知识。然后一步一步深入。
@ -990,13 +998,13 @@ $ python client3.py --max-clients 128
下面是一份书单,我从这些书中提炼出了这篇文章所需的素材。他们能助你在我刚刚所述的几个方面中发掘出兼具深度和广度的知识。我极力推荐你们去搞到这几本书看看:从你的朋友那里借,在当地的图书馆中阅读,或者直接在亚马逊上把它买回来。下面是我的典藏秘籍:
1. [UNIX网络编程 (卷1):套接字联网API (第3版)][6]
2. [UNIX环境高级编程 (第3版)][7]
3. [Linux/UNIX系统编程手册][8]
4. [TCP/IP详解 (卷1):协议 (第2版) (爱迪生-韦斯莱专业编程系列)][9]
5. [信号系统简明手册 (第二版): 并发控制深入浅出及常见错误][10]. 这本书也可以从[作者的个人网站][11]中买到。
1. [《UNIX 网络编程 卷1套接字联网 API 第3版][6]
2. [《UNIX 环境高级编程第3版][7]
3. [Linux/UNIX 系统编程手册][8]
4. [《TCP/IP 详解 卷1协议第2版][9]
5. [信号系统简明手册 (第二版): 并发控制深入浅出及常见错误》][10],这本书也可以从[作者的个人网站][11]中免费下载到。
顺便,我在撰写一本名为《搭个 Web 服务器:从头开始》的书。这本书讲解了如何从头开始编写一个基本的 Web 服务器,里面包含本文中没有的更多细节。订阅邮件列表,你就可以获取到这本书的最新进展,以及发布日期。
顺便,我在撰写一本名为《搭个 Web 服务器:从头开始》的书。这本书讲解了如何从头开始编写一个基本的 Web 服务器,里面包含本文中没有的更多细节。订阅[原文下方的邮件列表][13],你就可以获取到这本书的最新进展,以及发布日期。
--------------------------------------------------------------------------------
@ -1004,7 +1012,7 @@ via: https://ruslanspivak.com/lsbaws-part3/
作者:[Ruslan][a]
译者:[StdioA](https://github.com/StdioA)
校对:[校对者ID](https://github.com/校对者ID)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
@ -1013,12 +1021,13 @@ via: https://ruslanspivak.com/lsbaws-part3/
[1]: https://github.com/rspivak/lsbaws/blob/master/part3/
[2]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3a.py
[3]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3b.py
[4]: https://ruslanspivak.com/lsbaws-part3/#fn:1
[5]: https://ruslanspivak.com/lsbaws-part3/#fn:2
[6]: http://www.amazon.com/gp/product/0131411551/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0131411551&linkCode=as2&tag=russblo0b-20&linkId=2F4NYRBND566JJQL
[7]: http://www.amazon.com/gp/product/0321637739/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0321637739&linkCode=as2&tag=russblo0b-20&linkId=3ZYAKB537G6TM22J
[8]: http://www.amazon.com/gp/product/1593272200/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1593272200&linkCode=as2&tag=russblo0b-20&linkId=CHFOMNYXN35I2MON
[9]: http://www.amazon.com/gp/product/0321336313/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0321336313&linkCode=as2&tag=russblo0b-20&linkId=K467DRFYMXJ5RWAY
[4]: http://www.epubit.com.cn/book/details/1692
[5]: http://www.amazon.com/gp/product/1441418687/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1441418687&linkCode=as2&tag=russblo0b-20&linkId=QFOAWARN62OWTWUG
[6]: http://www.epubit.com.cn/book/details/1692
[7]: http://www.epubit.com.cn/book/details/1625
[8]: http://www.epubit.com.cn/book/details/1432
[9]: http://www.epubit.com.cn/book/details/4232
[10]: http://www.amazon.com/gp/product/1441418687/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1441418687&linkCode=as2&tag=russblo0b-20&linkId=QFOAWARN62OWTWUG
[11]: http://greenteapress.com/semaphores/
[12]: https://github.com/rspivak/lsbaws/blob/master/part3/webserver3h.py
[13]: https://ruslanspivak.com/lsbaws-part1/