新聞中心
[[173107]]

創(chuàng)新互聯(lián)是一家專注于成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作和成都服務(wù)器托管的網(wǎng)絡(luò)公司,有著豐富的建站經(jīng)驗(yàn)和案例。
在本系列的第二部分中,你創(chuàng)造了一個(gè)可以處理基本 HTTP GET 請(qǐng)求的、樸素的 WSGI 服務(wù)器。當(dāng)時(shí)我問(wèn)了一個(gè)問(wèn)題:“你該如何讓你的服務(wù)器在同一時(shí)間處理多個(gè)請(qǐng)求呢?”在這篇文章中,你會(huì)找到答案。系好安全帶,我們要認(rèn)真起來(lái),全速前進(jìn)了!你將會(huì)體驗(yàn)到一段非常快速的旅程。準(zhǔn)備好你的 Linux、Mac OS X(或者其他 *nix 系統(tǒng)),還有你的 Python。本文中所有源代碼均可在 GitHub 上找到。
服務(wù)器的基本結(jié)構(gòu)及如何處理請(qǐng)求
首先,我們來(lái)回顧一下 Web 服務(wù)器的基本結(jié)構(gòu),以及服務(wù)器處理來(lái)自客戶端的請(qǐng)求時(shí),所需的必要步驟。你在第一部分及第二部分中創(chuàng)建的輪詢服務(wù)器只能夠一次處理一個(gè)請(qǐng)求。在處理完當(dāng)前請(qǐng)求之前,它不能夠接受新的客戶端連接。所有請(qǐng)求為了等待服務(wù)都需要排隊(duì),在服務(wù)繁忙時(shí),這個(gè)隊(duì)伍可能會(huì)排的很長(zhǎng),一些客戶端可能會(huì)感到不開(kāi)心。
這是輪詢服務(wù)器 webserver3a.py 的代碼:
- #####################################################################
- # 輪詢服務(wù)器 - webserver3a.py #
- # #
- # 使用 Python 2.7.9 或 3.4 #
- # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測(cè)試通過(guò) #
- #####################################################################
- 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()
為了觀察到你的服務(wù)器在同一時(shí)間只能處理一個(gè)請(qǐng)求的行為,我們對(duì)服務(wù)器的代碼做一點(diǎn)點(diǎn)修改:在將響應(yīng)發(fā)送至客戶端之后,將程序阻塞 60 秒。這個(gè)修改只需要一行代碼,來(lái)告訴服務(wù)器進(jìn)程暫停 60 秒鐘。
這是我們更改后的代碼,包含暫停語(yǔ)句的服務(wù)器 webserver3b.py:
- ######################################################################
- # 輪詢服務(wù)器 - webserver3b.py #
- # #
- # 使用 Python 2.7.9 或 3.4 #
- # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測(cè)試通過(guò) #
- # #
- # - 服務(wù)器向客戶端發(fā)送響應(yīng)之后,會(huì)阻塞 60 秒 #
- ######################################################################
- 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) ### 睡眠語(yǔ)句,阻塞該進(jìn)程 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))
- while True:
- client_connection, client_address = listen_socket.accept()
- handle_request(client_connection)
- client_connection.close()
- if __name__ == '__main__':
- serve_forever()
用以下命令啟動(dòng)服務(wù)器:
- $ python webserver3b.py
現(xiàn)在,打開(kāi)一個(gè)新的命令行窗口,然后運(yùn)行 curl 語(yǔ)句。你應(yīng)該可以立刻看到屏幕上顯示的字符串“Hello, World!”:
- $ curl http://localhost:8888/hello
- Hello, World!
然后,立刻打開(kāi)第二個(gè)命令行窗口,運(yùn)行相同的 curl 命令:
- $ curl http://localhost:8888/hello
如果你在 60 秒之內(nèi)完成了以上步驟,你會(huì)看到第二條 curl 指令不會(huì)立刻產(chǎn)生任何輸出,而只是掛在了哪里。同樣,服務(wù)器也不會(huì)在標(biāo)準(zhǔn)輸出流中輸出新的請(qǐng)求內(nèi)容。這是這個(gè)過(guò)程在我的 Mac 電腦上的運(yùn)行結(jié)果(在右下角用黃色框標(biāo)注出來(lái)的窗口中,我們能看到第二個(gè) curl 指令被掛起,正在等待連接被服務(wù)器接受):
當(dāng)你等待足夠長(zhǎng)的時(shí)間(60 秒以上)后,你會(huì)看到第一個(gè) curl 程序完成,而第二個(gè) curl 在屏幕上輸出了“Hello, World!”,然后休眠 60 秒,進(jìn)而終止。
這樣運(yùn)行的原因是因?yàn)樵诜?wù)器在處理完第一個(gè)來(lái)自 curl 的請(qǐng)求之后,只有等待 60 秒才能開(kāi)始處理第二個(gè)請(qǐng)求。這個(gè)處理請(qǐng)求的過(guò)程按順序進(jìn)行(也可以說(shuō),迭代進(jìn)行),一步一步進(jìn)行,在我們剛剛給出的例子中,在同一時(shí)間內(nèi)只能處理一個(gè)請(qǐng)求。
現(xiàn)在,我們來(lái)簡(jiǎn)單討論一下客戶端與服務(wù)器的交流過(guò)程。為了讓兩個(gè)程序在網(wǎng)絡(luò)中互相交流,它們必須使用套接字。你應(yīng)當(dāng)在本系列的前兩部分中見(jiàn)過(guò)它幾次了。但是,套接字是什么?
套接字socket是一個(gè)通訊通道端點(diǎn)endpoint的抽象描述,它可以讓你的程序通過(guò)文件描述符來(lái)與其它程序進(jìn)行交流。在這篇文章中,我只會(huì)單獨(dú)討論 Linux 或 Mac OS X 中的 TCP/IP 套接字。這里有一個(gè)重點(diǎn)概念需要你去理解:TCP 套接字對(duì)socket pair。
TCP 連接使用的套接字對(duì)是一個(gè)由 4 個(gè)元素組成的元組,它確定了 TCP 連接的兩端:本地 IP 地址、本地端口、遠(yuǎn)端 IP 地址及遠(yuǎn)端端口。一個(gè)套接字對(duì)唯一地確定了網(wǎng)絡(luò)中的每一個(gè) TCP 連接。在連接一端的兩個(gè)值:一個(gè) IP 地址和一個(gè)端口,通常被稱作一個(gè)套接字。(引自《UNIX 網(wǎng)絡(luò)編程 卷1:套接字聯(lián)網(wǎng) API (第3版)》)
所以,元組 {10.10.10.2:49152, 12.12.12.3:8888} 就是一個(gè)能夠在客戶端確定 TCP 連接兩端的套接字對(duì),而元組 {12.12.12.3:8888, 10.10.10.2:49152} 則是在服務(wù)端確定 TCP 連接兩端的套接字對(duì)。在這個(gè)例子中,確定 TCP 服務(wù)端的兩個(gè)值(IP 地址 12.12.12.3 及端口 8888),代表一個(gè)套接字;另外兩個(gè)值則代表客戶端的套接字。
一個(gè)服務(wù)器創(chuàng)建一個(gè)套接字并開(kāi)始建立連接的基本工作流程如下:
1. 服務(wù)器創(chuàng)建一個(gè) TCP/IP 套接字。我們可以用這條 Python 語(yǔ)句來(lái)創(chuàng)建:
- listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2. 服務(wù)器可能會(huì)設(shè)定一些套接字選項(xiàng)(這個(gè)步驟是可選的,但是你可以看到上面的服務(wù)器代碼做了設(shè)定,這樣才能夠在重啟服務(wù)器時(shí)多次復(fù)用同一地址):
- listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
3. 然后,服務(wù)器綁定一個(gè)地址。綁定函數(shù) bind 可以將一個(gè)本地協(xié)議地址賦給套接字。若使用 TCP 協(xié)議,調(diào)用綁定函數(shù) bind 時(shí),需要指定一個(gè)端口號(hào),一個(gè) IP 地址,或兩者兼有,或兩者全無(wú)。(引自《UNIX網(wǎng)絡(luò)編程 卷1:套接字聯(lián)網(wǎng) API (第3版)》)
- listen_socket.bind(SERVER_ADDRESS)
4. 然后,服務(wù)器開(kāi)啟套接字的監(jiān)聽(tīng)模式。
- listen_socket.listen(REQUEST_QUEUE_SIZE)
監(jiān)聽(tīng)函數(shù) listen 只應(yīng)在服務(wù)端調(diào)用。它會(huì)通知操作系統(tǒng)內(nèi)核,表明它會(huì)接受所有向該套接字發(fā)送的入站連接請(qǐng)求。
以上四步完成后,服務(wù)器將循環(huán)接收來(lái)自客戶端的連接,一次循環(huán)處理一條。當(dāng)有連接可用時(shí),接受請(qǐng)求函數(shù)accept 將會(huì)返回一個(gè)已連接的客戶端套接字。然后,服務(wù)器從這個(gè)已連接的客戶端套接字中讀取請(qǐng)求數(shù)據(jù),將數(shù)據(jù)在其標(biāo)準(zhǔn)輸出流中輸出出來(lái),并向客戶端回送一條消息。然后,服務(wù)器會(huì)關(guān)閉這個(gè)客戶端連接,并準(zhǔn)備接收一個(gè)新的客戶端連接。
這是客戶端使用 TCP/IP 協(xié)議與服務(wù)器通信的必要步驟:
下面是一段示例代碼,使用這段代碼,客戶端可以連接你的服務(wù)器,發(fā)送一個(gè)請(qǐng)求,并輸出響應(yīng)內(nèi)容:
- import socket
- ### 創(chuàng)建一個(gè)套接字,并連接值服務(wù)器
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.connect(('localhost', 8888))
- ### 發(fā)送一段數(shù)據(jù),并接收響應(yīng)數(shù)據(jù)
- sock.sendall(b'test')
- data = sock.recv(1024)
- print(data.decode())
在創(chuàng)建套接字后,客戶端需要連接至服務(wù)器。我們可以調(diào)用連接函數(shù) connect 來(lái)完成這個(gè)操作:
- sock.connect(('localhost', 8888))
客戶端只需提供待連接的遠(yuǎn)程服務(wù)器的 IP 地址(或主機(jī)名),及端口號(hào),即可連接至遠(yuǎn)端服務(wù)器。
你可能已經(jīng)注意到了,客戶端不需要調(diào)用 bind 及 accept 函數(shù),就可以與服務(wù)器建立連接??蛻舳瞬恍枰{(diào)用 bind 函數(shù)是因?yàn)榭蛻舳瞬恍枰P(guān)注本地 IP 地址及端口號(hào)。操作系統(tǒng)內(nèi)核中的 TCP/IP 協(xié)議棧會(huì)在客戶端調(diào)用 connect 函數(shù)時(shí),自動(dòng)為套接字分配本地 IP 地址及本地端口號(hào)。這個(gè)本地端口被稱為臨時(shí)端口ephemeral port,即一個(gè)短暫開(kāi)放的端口。
服務(wù)器中有一些端口被用于承載一些眾所周知的服務(wù),它們被稱作通用well-known端口:如 80 端口用于 HTTP 服務(wù),22 端口用于 SSH 服務(wù)。打開(kāi)你的 Python shell,與你在本地運(yùn)行的服務(wù)器建立一個(gè)連接,來(lái)看看內(nèi)核給你的客戶端套接字分配了哪個(gè)臨時(shí)端口(在嘗試這個(gè)例子之前,你需要運(yùn)行服務(wù)器程序 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)
在上面的例子中,內(nèi)核將臨時(shí)端口 60589 分配給了你的套接字。
在我開(kāi)始回答我在第二部分中提出的問(wèn)題之前,我還需要快速講解一些概念。你很快就會(huì)明白這些概念為什么非常重要。這兩個(gè)概念,一個(gè)是進(jìn)程,另外一個(gè)是文件描述符。
什么是進(jìn)程?進(jìn)程就是一個(gè)程序執(zhí)行的實(shí)體。舉個(gè)例子:當(dāng)你的服務(wù)器代碼被執(zhí)行時(shí),它會(huì)被載入內(nèi)存,而內(nèi)存中表現(xiàn)此次程序運(yùn)行的實(shí)體就叫做進(jìn)程。內(nèi)核記錄了進(jìn)程的一系列有關(guān)信息——比如進(jìn)程 ID——來(lái)追蹤它的運(yùn)行情況。當(dāng)你在執(zhí)行輪詢服務(wù)器 webserver3a.py 或 webserver3b.py 時(shí),你其實(shí)只是啟動(dòng)了一個(gè)進(jìn)程。
我們?cè)诮K端窗口中運(yùn)行 webserver3b.py:
- $ python webserver3b.py
在另一個(gè)終端窗口中,我們可以使用 ps 命令獲取該進(jìn)程的相關(guān)信息:
- $ ps | grep webserver3b | grep -v grep
- 7182 ttys003 0:00.04 python webserver3b.py
ps 命令顯示,我們剛剛只運(yùn)行了一個(gè) Python 進(jìn)程 webserver3b.py。當(dāng)一個(gè)進(jìn)程被創(chuàng)建時(shí),內(nèi)核會(huì)為其分配一個(gè)進(jìn)程 ID,也就是 PID。在 UNIX 中,所有用戶進(jìn)程都有一個(gè)父進(jìn)程;當(dāng)然,這個(gè)父進(jìn)程也有進(jìn)程 ID,叫做父進(jìn)程 ID,縮寫(xiě)為 PPID。假設(shè)你默認(rèn)使用 BASH shell,那當(dāng)你啟動(dòng)服務(wù)器時(shí),就會(huì)啟動(dòng)一個(gè)新的進(jìn)程,同時(shí)被賦予一個(gè) PID,而它的父進(jìn)程 PID 會(huì)被設(shè)為 BASH shell 的 PID。
自己嘗試一下,看看這一切都是如何工作的。重新開(kāi)啟你的 Python shell,它會(huì)創(chuàng)建一個(gè)新進(jìn)程,然后在其中使用系統(tǒng)調(diào)用 os.getpid() 及 os.getppid() 來(lái)獲取 Python shell 進(jìn)程的 PID 及其父進(jìn)程 PID(也就是你的 BASH shell 的 PID)。然后,在另一個(gè)終端窗口中運(yùn)行 ps 命令,然后用 grep 來(lái)查找 PPID(父進(jìn)程 ID,在我的例子中是 3148)。在下面的屏幕截圖中,你可以看到一個(gè)我的 Mac OS X 系統(tǒng)中關(guān)于進(jìn)程父子關(guān)系的例子,在這個(gè)例子中,子進(jìn)程是我的 Python shell 進(jìn)程,而父進(jìn)程是 BASH shell 進(jìn)程:
另外一個(gè)需要了解的概念,就是文件描述符。什么是文件描述符?文件描述符是一個(gè)非負(fù)整數(shù),當(dāng)進(jìn)程打開(kāi)一個(gè)現(xiàn)有文件、創(chuàng)建新文件或創(chuàng)建一個(gè)新的套接字時(shí),內(nèi)核會(huì)將這個(gè)數(shù)返回給進(jìn)程。你以前可能聽(tīng)說(shuō)過(guò),在 UNIX 中,一切皆是文件。內(nèi)核會(huì)按文件描述符來(lái)找到一個(gè)進(jìn)程所打開(kāi)的文件。當(dāng)你需要讀取文件或向文件寫(xiě)入時(shí),我們同樣通過(guò)文件描述符來(lái)定位這個(gè)文件。Python 提供了高層次的操作文件(或套接字)的對(duì)象,所以你不需要直接通過(guò)文件描述符來(lái)定位文件。但是,在高層對(duì)象之下,我們就是用它來(lái)在 UNIX 中定位文件及套接字,通過(guò)這個(gè)整數(shù)的文件描述符。
一般情況下,UNIX shell 會(huì)將一個(gè)進(jìn)程的標(biāo)準(zhǔn)輸入流(STDIN)的文件描述符設(shè)為 0,標(biāo)準(zhǔn)輸出流(STDOUT)設(shè)為 1,而標(biāo)準(zhǔn)錯(cuò)誤打印(STDERR)的文件描述符會(huì)被設(shè)為 2。
我之前提到過(guò),即使 Python 提供了高層次的文件對(duì)象或類文件對(duì)象來(lái)供你操作,你仍然可以在對(duì)象上使用fileno() 方法,來(lái)獲取與該文件相關(guān)聯(lián)的文件描述符?;氐?Python shell 中,我們來(lái)看看你該怎么做到這一點(diǎn):
- >>> import sys
- >>> sys.stdin
', mode 'r' at 0x102beb0c0> - >>> sys.stdin.fileno()
- 0
- >>> sys.stdout.fileno()
- 1
- >>> sys.stderr.fileno()
- 2
當(dāng)你在 Python 中操作文件及套接字時(shí),你可能會(huì)使用高層次的文件/套接字對(duì)象,但是你仍然有可能會(huì)直接使用文件描述符。下面有一個(gè)例子,來(lái)演示如何用文件描述符做參數(shù)來(lái)進(jìn)行一次寫(xiě)入的系統(tǒng)調(diào)用:
- >>> import sys
- >>> import os
- >>> res = os.write(sys.stdout.fileno(), 'hello\n')
- hello
下面是比較有趣的部分——不過(guò)你可能不會(huì)為此感到驚訝,因?yàn)槟阋呀?jīng)知道在 Unix 中,一切皆為文件——你的套接字對(duì)象同樣有一個(gè)相關(guān)聯(lián)的文件描述符。和剛才操縱文件時(shí)一樣,當(dāng)你在 Python 中創(chuàng)建一個(gè)套接字時(shí),你會(huì)得到一個(gè)對(duì)象而不是一個(gè)非負(fù)整數(shù),但你永遠(yuǎn)可以用我之前提到過(guò)的 fileno() 方法獲取套接字對(duì)象的文件描述符,并可以通過(guò)這個(gè)文件描述符來(lái)直接操縱套接字。
- >>> import socket
- >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- >>> sock.fileno()
- 3
我還想再提一件事:不知道你有沒(méi)有注意到,在我們的第二個(gè)輪詢服務(wù)器 webserver3b.py 中,當(dāng)你的服務(wù)器休眠 60 秒的過(guò)程中,你仍然可以通過(guò)第二個(gè) curl 命令連接至服務(wù)器。當(dāng)然 curl 命令并沒(méi)有立刻輸出任何內(nèi)容而是掛在哪里,但是既然服務(wù)器沒(méi)有接受連接,那它為什么不立即拒絕掉連接,而讓它還能夠繼續(xù)與服務(wù)器建立連接呢?這個(gè)問(wèn)題的答案是:當(dāng)我在調(diào)用套接字對(duì)象的 listen 方法時(shí),我為該方法提供了一個(gè) BACKLOG參數(shù),在代碼中用 REQUEST_QUEUE_SIZE 常量來(lái)表示。BACKLOG 參數(shù)決定了在內(nèi)核中為存放即將到來(lái)的連接請(qǐng)求所創(chuàng)建的隊(duì)列的大小。當(dāng)服務(wù)器 webserver3b.py 在睡眠的時(shí)候,你運(yùn)行的第二個(gè) curl 命令依然能夠連接至服務(wù)器,因?yàn)閮?nèi)核中用來(lái)存放即將接收的連接請(qǐng)求的隊(duì)列依然擁有足夠大的可用空間。
盡管增大 BACKLOG 參數(shù)并不能神奇地使你的服務(wù)器同時(shí)處理多個(gè)請(qǐng)求,但當(dāng)你的服務(wù)器很繁忙時(shí),將它設(shè)置為一個(gè)較大的值還是相當(dāng)重要的。這樣,在你的服務(wù)器調(diào)用 accept 方法時(shí),不需要再等待一個(gè)新的連接建立,而可以立刻直接抓取隊(duì)列中的第一個(gè)客戶端連接,并不加停頓地立刻處理它。
歐耶!現(xiàn)在你已經(jīng)了解了一大塊內(nèi)容。我們來(lái)快速回顧一下我們剛剛講解的知識(shí)(當(dāng)然,如果這些對(duì)你來(lái)說(shuō)都是基礎(chǔ)知識(shí)的話,那我們就當(dāng)復(fù)習(xí)好啦)。
- 輪詢服務(wù)器
- 服務(wù)端套接字創(chuàng)建流程(創(chuàng)建套接字,綁定,監(jiān)聽(tīng)及接受)
- 客戶端連接創(chuàng)建流程(創(chuàng)建套接字,連接)
- 套接字對(duì)
- 套接字
- 臨時(shí)端口及通用端口
- 進(jìn)程
- 進(jìn)程 ID(PID),父進(jìn)程 ID(PPID),以及進(jìn)程父子關(guān)系
- 文件描述符
- 套接字的 listen 方法中,BACKLOG 參數(shù)的含義
如何并發(fā)處理多個(gè)請(qǐng)求
現(xiàn)在,我可以開(kāi)始回答第二部分中的那個(gè)問(wèn)題了:“你該如何讓你的服務(wù)器在同一時(shí)間處理多個(gè)請(qǐng)求呢?”或者換一種說(shuō)法:“如何編寫(xiě)一個(gè)并發(fā)服務(wù)器?”
在 UNIX 系統(tǒng)中編寫(xiě)一個(gè)并發(fā)服務(wù)器最簡(jiǎn)單的方法,就是使用系統(tǒng)調(diào)用 fork()。
下面是全新出爐的并發(fā)服務(wù)器 webserver3c.py 的代碼,它可以同時(shí)處理多個(gè)請(qǐng)求(和我們之前的例子webserver3b.py 一樣,每個(gè)子進(jìn)程都會(huì)休眠 60 秒):
- #######################################################
- # 并發(fā)服務(wù)器 - webserver3c.py #
- # #
- # 使用 Python 2.7.9 或 3.4 #
- # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測(cè)試通過(guò) #
- # #
- # - 完成客戶端請(qǐng)求處理之后,子進(jìn)程會(huì)休眠 60 秒 #
- # - 父子進(jìn)程會(huì)關(guān)閉重復(fù)的描述符 #
- # #
- #######################################################
- 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: ### 子進(jìn)程
- listen_socket.close() ### 關(guān)閉子進(jìn)程中復(fù)制的套接字對(duì)象
- handle_request(client_connection)
- client_connection.close()
- os._exit(0) ### 子進(jìn)程在這里退出
- else: ### 父進(jìn)程
- client_connection.close() ### 關(guān)閉父進(jìn)程中的客戶端連接對(duì)象,并循環(huán)執(zhí)行
- if __name__ == '__main__':
- serve_forever()
在深入研究代碼、討論 fork 如何工作之前,先嘗試運(yùn)行它,自己看一看這個(gè)服務(wù)器是否真的可以同時(shí)處理多個(gè)客戶端請(qǐng)求,而不是像輪詢服務(wù)器 webserver3a.py 和 webserver3b.py 一樣。在命令行中使用如下命令啟動(dòng)服務(wù)器:
- $ python webserver3c.py
然后,像我們之前測(cè)試輪詢服務(wù)器那樣,運(yùn)行兩個(gè) curl 命令,來(lái)看看這次的效果?,F(xiàn)在你可以看到,即使子進(jìn)程在處理客戶端請(qǐng)求后會(huì)休眠 60 秒,但它并不會(huì)影響其它客戶端連接,因?yàn)樗麄兌际怯赏耆?dú)立的進(jìn)程來(lái)處理的。你應(yīng)該看到你的 curl 命令立即輸出了“Hello, World!”然后掛起 60 秒。你可以按照你的想法運(yùn)行盡可能多的 curl 命令(好吧,并不能運(yùn)行特別特別多 ^_^),所有的命令都會(huì)立刻輸出來(lái)自服務(wù)器的響應(yīng) “Hello, World!”,并不會(huì)出現(xiàn)任何可被察覺(jué)到的延遲行為。試試看吧。
如果你要理解 fork(),那最重要的一點(diǎn)是:你調(diào)用了它一次,但是它會(huì)返回兩次 —— 一次在父進(jìn)程中,另一次是在子進(jìn)程中。當(dāng)你創(chuàng)建了一個(gè)新進(jìn)程,那么 fork() 在子進(jìn)程中的返回值是 0。如果是在父進(jìn)程中,那fork() 函數(shù)會(huì)返回子進(jìn)程的 PID。
我依然記得在第一次看到它并嘗試使用 fork() 的時(shí)候,我是多么的入迷。它在我眼里就像是魔法一樣。這就好像我在讀一段順序執(zhí)行的代碼,然后“砰!”地一聲,代碼變成了兩份,然后出現(xiàn)了兩個(gè)實(shí)體,同時(shí)并行地運(yùn)行相同的代碼。講真,那個(gè)時(shí)候我覺(jué)得它真的跟魔法一樣神奇。
當(dāng)父進(jìn)程創(chuàng)建出一個(gè)新的子進(jìn)程時(shí),子進(jìn)程會(huì)復(fù)制從父進(jìn)程中復(fù)制一份文件描述符:
你可能注意到,在上面的代碼中,父進(jìn)程關(guān)閉了客戶端連接:
- else: ### 父進(jìn)程
- client_connection.close() # 關(guān)閉父進(jìn)程的副本并循環(huán)
不過(guò),既然父進(jìn)程關(guān)閉了這個(gè)套接字,那為什么子進(jìn)程仍然能夠從來(lái)自客戶端的套接字中讀取數(shù)據(jù)呢?答案就在上面的圖片中。內(nèi)核會(huì)使用描述符引用計(jì)數(shù)器來(lái)決定是否要關(guān)閉一個(gè)套接字。當(dāng)你的服務(wù)器創(chuàng)建一個(gè)子進(jìn)程時(shí),子進(jìn)程會(huì)復(fù)制父進(jìn)程的所有文件描述符,內(nèi)核中該描述符的引用計(jì)數(shù)也會(huì)增加。如果只有一個(gè)父進(jìn)程及一個(gè)子進(jìn)程,那客戶端套接字的文件描述符引用數(shù)應(yīng)為 2;當(dāng)父進(jìn)程關(guān)閉客戶端連接的套接字時(shí),內(nèi)核只會(huì)減少它的引用計(jì)數(shù),將其變?yōu)?1,但這仍然不會(huì)使內(nèi)核關(guān)閉該套接字。子進(jìn)程也關(guān)閉了父進(jìn)程中 listen_socket 的復(fù)制實(shí)體,因?yàn)樽舆M(jìn)程不需要關(guān)注新的客戶端連接,而只需要處理已建立的客戶端連接中的請(qǐng)求。
- listen_socket.close() ### 關(guān)閉子進(jìn)程中的復(fù)制實(shí)體
我們將會(huì)在后文中討論,如果你不關(guān)閉那些重復(fù)的描述符,會(huì)發(fā)生什么。
你可以從你的并發(fā)服務(wù)器源碼中看到,父進(jìn)程的主要職責(zé)為:接受一個(gè)新的客戶端連接,復(fù)制出一個(gè)子進(jìn)程來(lái)處理這個(gè)連接,然后繼續(xù)循環(huán)來(lái)接受另外的客戶端連接,僅此而已。服務(wù)器父進(jìn)程并不會(huì)處理客戶端連接——子進(jìn)程才會(huì)做這件事。
打個(gè)岔:當(dāng)我們說(shuō)兩個(gè)事件并發(fā)執(zhí)行時(shí),我們所要表達(dá)的意思是什么?
當(dāng)我們說(shuō)“兩個(gè)事件并發(fā)執(zhí)行”時(shí),它通常意味著這兩個(gè)事件同時(shí)發(fā)生。簡(jiǎn)單來(lái)講,這個(gè)定義沒(méi)問(wèn)題,但你應(yīng)該記住它的嚴(yán)格定義:
如果你不能在代碼中判斷兩個(gè)事件的發(fā)生順序,那這兩個(gè)事件就是并發(fā)執(zhí)行的。(引自《信號(hào)系統(tǒng)簡(jiǎn)明手冊(cè) (第二版): 并發(fā)控制深入淺出及常見(jiàn)錯(cuò)誤》)好的,現(xiàn)在你又該回顧一下你剛剛學(xué)過(guò)的知識(shí)點(diǎn)了。
- 在 Unix 中,編寫(xiě)一個(gè)并發(fā)服務(wù)器的最簡(jiǎn)單的方式——使用 fork() 系統(tǒng)調(diào)用;
- 當(dāng)一個(gè)進(jìn)程分叉(fork)出另一個(gè)進(jìn)程時(shí),它會(huì)變成剛剛分叉出的進(jìn)程的父進(jìn)程;
- 在進(jìn)行 fork 調(diào)用后,父進(jìn)程和子進(jìn)程共享相同的文件描述符;
- 系統(tǒng)內(nèi)核通過(guò)描述符的引用計(jì)數(shù)來(lái)決定是否要關(guān)閉該描述符對(duì)應(yīng)的文件或套接字;
- 服務(wù)器父進(jìn)程的主要職責(zé):現(xiàn)在它做的只是從客戶端接受一個(gè)新的連接,分叉出子進(jìn)程來(lái)處理這個(gè)客戶端連接,然后開(kāi)始下一輪循環(huán),去接收新的客戶端連接。
進(jìn)程分叉后不關(guān)閉重復(fù)的套接字會(huì)發(fā)生什么?
我們來(lái)看看,如果我們不在父進(jìn)程與子進(jìn)程中關(guān)閉重復(fù)的套接字描述符會(huì)發(fā)生什么。下面是剛才的并發(fā)服務(wù)器代碼的修改版本,這段代碼(webserver3d.py 中,服務(wù)器不會(huì)關(guān)閉重復(fù)的描述符):
- #######################################################
- # 并發(fā)服務(wù)器 - webserver3d.py #
- # #
- # 使用 Python 2.7.9 或 3.4 #
- # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測(cè)試通過(guò) #
- #######################################################
- 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()
- ### 將引用存儲(chǔ)起來(lái),否則在下一輪循環(huán)時(shí),他們會(huì)被垃圾回收機(jī)制銷毀
- clients.append(client_connection)
- pid = os.fork()
- if pid == 0: ### 子進(jìn)程
- listen_socket.close() ### 關(guān)閉子進(jìn)程中多余的套接字
- handle_request(client_connection)
- client_connection.close()
- os._exit(0) ### 子進(jìn)程在這里結(jié)束
- else: ### 父進(jìn)程
- # client_connection.close()
- print(len(clients))
- if __name__ == '__main__':
- serve_forever()
用以下命令來(lái)啟動(dòng)服務(wù)器:
- $ python webserver3d.py
用 curl 命令連接服務(wù)器:
- $ curl http://localhost:8888/hello
- Hello, World!
好,curl 命令輸出了來(lái)自并發(fā)服務(wù)器的響應(yīng)內(nèi)容,但程序并沒(méi)有退出,而是仍然掛起。到底發(fā)生了什么?這個(gè)服務(wù)器并不會(huì)掛起 60 秒:子進(jìn)程只處理客戶端連接,關(guān)閉連接然后退出,但客戶端的 curl 命令并沒(méi)有終止。
所以,為什么 curl 不終止呢?原因就在于文件描述符的副本。當(dāng)子進(jìn)程關(guān)閉客戶端連接時(shí),系統(tǒng)內(nèi)核會(huì)減少客戶端套接字的引用計(jì)數(shù),將其變?yōu)?1。服務(wù)器子進(jìn)程退出了,但客戶端套接字并沒(méi)有被內(nèi)核關(guān)閉,因?yàn)樵撎捉幼值拿枋龇糜?jì)數(shù)并沒(méi)有變?yōu)?0,所以,這就導(dǎo)致了連接終止包(在 TCP/IP 協(xié)議中稱作 FIN)不會(huì)被發(fā)送到客戶端,所以客戶端會(huì)一直保持連接。這里也會(huì)出現(xiàn)另一個(gè)問(wèn)題:如果你的服務(wù)器長(zhǎng)時(shí)間運(yùn)行,并且不關(guān)閉文件描述符的副本,那么可用的文件描述符會(huì)被消耗殆盡:
使用 Control-C 關(guān)閉服務(wù)器 webserver3d.py,然后在 shell 中使用內(nèi)置命令 ulimit 來(lái)查看系統(tǒng)默認(rèn)為你的服務(wù)器進(jìn)程分配的可用資源數(shù):
- $ 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
你可以從上面的結(jié)果看到,在我的 Ubuntu 機(jī)器中,系統(tǒng)為我的服務(wù)器進(jìn)程分配的最大可用文件描述符(文件打開(kāi))數(shù)為 1024。
現(xiàn)在我們來(lái)看一看,如果你的服務(wù)器不關(guān)閉重復(fù)的描述符,它會(huì)如何消耗可用的文件描述符。在一個(gè)已有的或新建的終端窗口中,將你的服務(wù)器進(jìn)程的最大可用文件描述符設(shè)為 256:
- $ ulimit -n 256
在你剛剛運(yùn)行 ulimit -n 256 的終端窗口中運(yùn)行服務(wù)器 webserver3d.py:
- $ python webserver3d.py
然后使用下面的客戶端 client3.py 來(lái)測(cè)試你的服務(wù)器。
- #######################################################
- # 測(cè)試客戶端 - client3.py #
- # #
- # 使用 Python 2.7.9 或 3.4 #
- # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測(cè)試通過(guò) #
- #######################################################
- 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)
在一個(gè)新建的終端窗口中,運(yùn)行 client3.py 然后讓它與服務(wù)器同步創(chuàng)建 300 個(gè)連接:
- $ python client3.py --max-clients=300 分享名稱:如何搭建Web服務(wù)器(三)
標(biāo)題鏈接:http://m.fisionsoft.com.cn/article/cohsgeh.html


咨詢
建站咨詢
