新聞中心
[[173107]]

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


咨詢
建站咨詢
