日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
如何搭建Web服務(wù)器(三)

[[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 的代碼:

 
 
  1. ##################################################################### 
  2. # 輪詢服務(wù)器 - webserver3a.py                                       # 
  3. #                                                                   # 
  4. # 使用 Python 2.7.9 或 3.4                                          # 
  5. # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測試通過                        # 
  6. ##################################################################### 
  7. import socket 
  8. SERVER_ADDRESS = (HOST, PORT) = '', 8888 
  9. REQUEST_QUEUE_SIZE = 5 
  10. def handle_request(client_connection): 
  11.     request = client_connection.recv(1024) 
  12.     print(request.decode()) 
  13.     http_response = b"""\ 
  14. HTTP/1.1 200 OK 
  15. Hello, World! 
  16. """ 
  17.     client_connection.sendall(http_response) 
  18. def serve_forever(): 
  19.     listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
  20.     listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
  21.     listen_socket.bind(SERVER_ADDRESS) 
  22.     listen_socket.listen(REQUEST_QUEUE_SIZE) 
  23.     print('Serving HTTP on port {port} ...'.format(port=PORT)) 
  24.     while True: 
  25.         client_connection, client_address = listen_socket.accept() 
  26.         handle_request(client_connection) 
  27.         client_connection.close() 
  28. if __name__ == '__main__': 
  29.     serve_forever() 

為了觀察到你的服務(wù)器在同一時間只能處理一個請求的行為,我們對服務(wù)器的代碼做一點(diǎn)點(diǎn)修改:在將響應(yīng)發(fā)送至客戶端之后,將程序阻塞 60 秒。這個修改只需要一行代碼,來告訴服務(wù)器進(jìn)程暫停 60 秒鐘。

 這是我們更改后的代碼,包含暫停語句的服務(wù)器 webserver3b.py:

 
 
  1. ###################################################################### 
  2. # 輪詢服務(wù)器 - webserver3b.py                                         # 
  3. #                                                                    # 
  4. # 使用 Python 2.7.9 或 3.4                                            # 
  5. # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測試通過                           # 
  6. #                                                                    # 
  7. # - 服務(wù)器向客戶端發(fā)送響應(yīng)之后,會阻塞 60 秒                             # 
  8. ###################################################################### 
  9. import socket 
  10. import time 
  11. SERVER_ADDRESS = (HOST, PORT) = '', 8888 
  12. REQUEST_QUEUE_SIZE = 5 
  13. def handle_request(client_connection): 
  14.     request = client_connection.recv(1024) 
  15.     print(request.decode()) 
  16.     http_response = b"""\ 
  17. HTTP/1.1 200 OK 
  18. Hello, World! 
  19. """ 
  20.     client_connection.sendall(http_response) 
  21.     time.sleep(60)  ### 睡眠語句,阻塞該進(jìn)程 60 秒 
  22. def serve_forever(): 
  23.     listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
  24.     listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
  25.     listen_socket.bind(SERVER_ADDRESS) 
  26.     listen_socket.listen(REQUEST_QUEUE_SIZE) 
  27.     print('Serving HTTP on port {port} ...'.format(port=PORT)) 
  28.     while True: 
  29.         client_connection, client_address = listen_socket.accept() 
  30.         handle_request(client_connection) 
  31.         client_connection.close() 
  32. if __name__ == '__main__': 
  33.     serve_forever() 

用以下命令啟動服務(wù)器:

 
 
  1. $ python webserver3b.py 

現(xiàn)在,打開一個新的命令行窗口,然后運(yùn)行 curl 語句。你應(yīng)該可以立刻看到屏幕上顯示的字符串“Hello, World!”:

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

然后,立刻打開第二個命令行窗口,運(yùn)行相同的 curl 命令:

 
 
  1. $ 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)建:

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

2. 服務(wù)器可能會設(shè)定一些套接字選項(xiàng)(這個步驟是可選的,但是你可以看到上面的服務(wù)器代碼做了設(shè)定,這樣才能夠在重啟服務(wù)器時多次復(fù)用同一地址):

 
 
  1. 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版)》)

 
 
  1. listen_socket.bind(SERVER_ADDRESS) 

4. 然后,服務(wù)器開啟套接字的監(jiān)聽模式。

 
 
  1. 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)容:

 
 
  1. import socket 
  2. ### 創(chuàng)建一個套接字,并連接值服務(wù)器 
  3. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
  4. sock.connect(('localhost', 8888)) 
  5. ### 發(fā)送一段數(shù)據(jù),并接收響應(yīng)數(shù)據(jù) 
  6. sock.sendall(b'test') 
  7. data = sock.recv(1024) 
  8. print(data.decode()) 

在創(chuàng)建套接字后,客戶端需要連接至服務(wù)器。我們可以調(diào)用連接函數(shù) connect 來完成這個操作:

 
 
  1. 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):

 
 
  1. >>> import socket 
  2. >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
  3. >>> sock.connect(('localhost', 8888)) 
  4. >>> host, port = sock.getsockname()[:2] 
  5. >>> host, port 
  6. ('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:

 
 
  1. $ python webserver3b.py 

在另一個終端窗口中,我們可以使用 ps 命令獲取該進(jìn)程的相關(guān)信息:

 
 
  1. $ ps | grep webserver3b | grep -v grep 
  2. 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):

 
 
  1. >>> import sys 
  2. >>> sys.stdin 
  3. ', mode 'r' at 0x102beb0c0> 
  4. >>> sys.stdin.fileno() 
  5. >>> sys.stdout.fileno() 
  6. >>> sys.stderr.fileno() 

當(dāng)你在 Python 中操作文件及套接字時,你可能會使用高層次的文件/套接字對象,但是你仍然有可能會直接使用文件描述符。下面有一個例子,來演示如何用文件描述符做參數(shù)來進(jìn)行一次寫入的系統(tǒng)調(diào)用:

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

下面是比較有趣的部分——不過你可能不會為此感到驚訝,因?yàn)槟阋呀?jīng)知道在 Unix 中,一切皆為文件——你的套接字對象同樣有一個相關(guān)聯(lián)的文件描述符。和剛才操縱文件時一樣,當(dāng)你在 Python 中創(chuàng)建一個套接字時,你會得到一個對象而不是一個非負(fù)整數(shù),但你永遠(yuǎn)可以用我之前提到過的 fileno() 方法獲取套接字對象的文件描述符,并可以通過這個文件描述符來直接操縱套接字。 

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

我還想再提一件事:不知道你有沒有注意到,在我們的第二個輪詢服務(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 秒):

  

 
 
  1. ####################################################### 
  2. # 并發(fā)服務(wù)器 - webserver3c.py                          # 
  3. #                                                     # 
  4. # 使用 Python 2.7.9 或 3.4                             # 
  5. # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測試通過            # 
  6. #                                                     # 
  7. # - 完成客戶端請求處理之后,子進(jìn)程會休眠 60 秒             # 
  8. # - 父子進(jìn)程會關(guān)閉重復(fù)的描述符                           # 
  9. #                                                     # 
  10. ####################################################### 
  11. import os 
  12. import socket 
  13. import time 
  14. SERVER_ADDRESS = (HOST, PORT) = '', 8888 
  15. REQUEST_QUEUE_SIZE = 5 
  16. def handle_request(client_connection): 
  17.     request = client_connection.recv(1024) 
  18.     print( 
  19.         'Child PID: {pid}. Parent PID {ppid}'.format( 
  20.             pid=os.getpid(), 
  21.             ppid=os.getppid(), 
  22.         ) 
  23.     ) 
  24.     print(request.decode()) 
  25.     http_response = b"""\ 
  26. HTTP/1.1 200 OK 
  27. Hello, World! 
  28. """ 
  29.     client_connection.sendall(http_response) 
  30.     time.sleep(60) 
  31. def serve_forever(): 
  32.     listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
  33.     listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
  34.     listen_socket.bind(SERVER_ADDRESS) 
  35.     listen_socket.listen(REQUEST_QUEUE_SIZE) 
  36.     print('Serving HTTP on port {port} ...'.format(port=PORT)) 
  37.     print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid())) 
  38.     while True: 
  39.         client_connection, client_address = listen_socket.accept() 
  40.         pid = os.fork() 
  41.         if pid == 0:  ### 子進(jìn)程 
  42.             listen_socket.close()  ### 關(guān)閉子進(jìn)程中復(fù)制的套接字對象 
  43.             handle_request(client_connection) 
  44.             client_connection.close() 
  45.             os._exit(0)  ### 子進(jìn)程在這里退出 
  46.         else:  ### 父進(jìn)程 
  47.             client_connection.close()  ### 關(guān)閉父進(jìn)程中的客戶端連接對象,并循環(huán)執(zhí)行 
  48. if __name__ == '__main__': 
  49.     serve_forever() 

在深入研究代碼、討論 fork 如何工作之前,先嘗試運(yùn)行它,自己看一看這個服務(wù)器是否真的可以同時處理多個客戶端請求,而不是像輪詢服務(wù)器 webserver3a.py 和 webserver3b.py 一樣。在命令行中使用如下命令啟動服務(wù)器:

 
 
  1. $ 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)閉了客戶端連接:

 
 
  1. else:  ### 父進(jìn)程 
  2.     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)注新的客戶端連接,而只需要處理已建立的客戶端連接中的請求。

 
 
  1. 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ù)的描述符):

 
 
  1. ####################################################### 
  2. # 并發(fā)服務(wù)器 - webserver3d.py                          # 
  3. #                                                     # 
  4. # 使用 Python 2.7.9 或 3.4                             # 
  5. # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測試通過            # 
  6. ####################################################### 
  7. import os 
  8. import socket 
  9. SERVER_ADDRESS = (HOST, PORT) = '', 8888 
  10. REQUEST_QUEUE_SIZE = 5 
  11. def handle_request(client_connection): 
  12.     request = client_connection.recv(1024) 
  13.     http_response = b"""\ 
  14. HTTP/1.1 200 OK 
  15. Hello, World! 
  16. """ 
  17.     client_connection.sendall(http_response) 
  18. def serve_forever(): 
  19.     listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
  20.     listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
  21.     listen_socket.bind(SERVER_ADDRESS) 
  22.     listen_socket.listen(REQUEST_QUEUE_SIZE) 
  23.     print('Serving HTTP on port {port} ...'.format(port=PORT)) 
  24.     clients = [] 
  25.     while True: 
  26.         client_connection, client_address = listen_socket.accept() 
  27.         ### 將引用存儲起來,否則在下一輪循環(huán)時,他們會被垃圾回收機(jī)制銷毀 
  28.         clients.append(client_connection) 
  29.         pid = os.fork() 
  30.         if pid == 0:  ### 子進(jìn)程 
  31.             listen_socket.close()  ### 關(guān)閉子進(jìn)程中多余的套接字 
  32.             handle_request(client_connection) 
  33.             client_connection.close() 
  34.             os._exit(0)  ### 子進(jìn)程在這里結(jié)束 
  35.         else:  ### 父進(jìn)程 
  36.             # client_connection.close() 
  37.             print(len(clients)) 
  38. if __name__ == '__main__': 
  39.     serve_forever() 

用以下命令來啟動服務(wù)器:

 
 
  1. $ python webserver3d.py 

用 curl 命令連接服務(wù)器:

 
 
  1. $ curl http://localhost:8888/hello 
  2. 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ù):

 
 
  1. $ ulimit -a 
  2. core file size          (blocks, -c) 0 
  3. data seg size           (kbytes, -d) unlimited 
  4. scheduling priority             (-e) 0 
  5. file size               (blocks, -f) unlimited 
  6. pending signals                 (-i) 3842 
  7. max locked memory       (kbytes, -l) 64 
  8. max memory size         (kbytes, -m) unlimited 
  9. open files                      (-n) 1024 
  10. pipe size            (512 bytes, -p) 8 
  11. POSIX message queues     (bytes, -q) 819200 
  12. real-time priority              (-r) 0 
  13. stack size              (kbytes, -s) 8192 
  14. cpu time               (seconds, -t) unlimited 
  15. max user processes              (-u) 3842 
  16. virtual memory          (kbytes, -v) unlimited 
  17. 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: 

 
 
  1. $ ulimit -n 256 

在你剛剛運(yùn)行 ulimit -n 256 的終端窗口中運(yùn)行服務(wù)器 webserver3d.py:

 
 
  1. $ python webserver3d.py 

然后使用下面的客戶端 client3.py 來測試你的服務(wù)器。

 
 
  1. ####################################################### 
  2. # 測試客戶端 - client3.py                              # 
  3. #                                                     # 
  4. # 使用 Python 2.7.9 或 3.4                             # 
  5. # 在 Ubuntu 14.04 及 Mac OS X 環(huán)境下測試通過            # 
  6. ####################################################### 
  7. import argparse 
  8. import errno 
  9. import os 
  10. import socket 
  11. SERVER_ADDRESS = 'localhost', 8888 
  12. REQUEST = b"""\ 
  13. GET /hello HTTP/1.1 
  14. Host: localhost:8888 
  15. """ 
  16. def main(max_clients, max_conns): 
  17.     socks = [] 
  18.     for client_num in range(max_clients): 
  19.         pid = os.fork() 
  20.         if pid == 0: 
  21.             for connection_num in range(max_conns): 
  22.                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
  23.                 sock.connect(SERVER_ADDRESS) 
  24.                 sock.sendall(REQUEST) 
  25.                 socks.append(sock) 
  26.                 print(connection_num) 
  27.                 os._exit(0) 
  28. if __name__ == '__main__': 
  29.     parser = argparse.ArgumentParser( 
  30.         description='Test client for LSBAWS.', 
  31.         formatter_class=argparse.ArgumentDefaultsHelpFormatter, 
  32.     ) 
  33.     parser.add_argument( 
  34.         '--max-conns', 
  35.         type=int, 
  36.         default=1024, 
  37.         help='Maximum number of connections per client.' 
  38.     ) 
  39.     parser.add_argument( 
  40.         '--max-clients', 
  41.         type=int, 
  42.         default=1, 
  43.         help='Maximum number of clients.' 
  44.     ) 
  45.     args = parser.parse_args() 
  46.     main(args.max_clients, args.max_conns) 

在一個新建的終端窗口中,運(yùn)行 client3.py 然后讓它與服務(wù)器同步創(chuàng)建 300 個連接:

 
 
  1. $ python client3.py --max-clients=300 網(wǎng)頁標(biāo)題:如何搭建Web服務(wù)器(三)
    網(wǎng)站路徑:http://www.5511xx.com/article/cohsgeh.html