1. 程式人生 > >自己動手開發一個 Web 伺服器(三)

自己動手開發一個 Web 伺服器(三)

第二部分中,你開發了一個能夠處理HTTPGET請求的簡易WSGI伺服器。在上一篇的最後,我問了你一個問題:“怎樣讓伺服器一次處理多個請求?”讀完本文,你就能夠完美地回答這個問題。接下來,請你做好準備,因為本文的內容非常多,節奏也很快。文中的所有程式碼都可以在Github倉庫下載。

首先,我們簡單回憶一下簡易網路伺服器是如何實現的,伺服器要處理客戶端的請求需要哪些條件。你在前面兩部分文章中開發的伺服器,是一個迭代式伺服器(iterative server),還只能一次處理一個客戶端請求。只有在處理完當前客戶端請求之後,它才能接收新的客戶端連線。這樣,有些客戶端就必須要等待自己的請求被處理了,而對於流量大的伺服器來說,等待的時間就會特別長。

客戶端逐個等待伺服器響應

客戶端逐個等待伺服器響應

下面是迭代式伺服器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秒。

讓伺服器睡眠60秒

讓伺服器睡眠60秒

下面就是修改之後的伺服器程式碼:

  1. #########################################################################
  2. #Iterative server - webserver3b.py #
  3. ##
  4. #TestedwithPython2.7.9&Python
    3.4 on Ubuntu14.04&Mac OS X #
  5. ##
  6. #-Server sleeps for60 seconds after sending a response to a client #
  7. #########################################################################
  8. import socket
  9. importtime
  10. SERVER_ADDRESS =(HOST, PORT)='',8888
  11. REQUEST_QUEUE_SIZE =5
  12. def handle_request(client_connection):
  13. request = client_connection.recv(1024)
  14. print(request.decode())
  15. http_response = b"""\
  16. HTTP/1.1 200 OK
  17. Hello, World!
  18. """
  19. client_connection.sendall(http_response)
  20. time.sleep(60)#sleepand block the process for60 seconds
  21. def serve_forever():
  22. listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  23. listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
  24. listen_socket.bind(SERVER_ADDRESS)
  25. listen_socket.listen(REQUEST_QUEUE_SIZE)
  26. print('Serving HTTP on port {port} ...'.format(port=PORT))
  27. whileTrue:
  28. client_connection, client_address = listen_socket.accept()
  29. handle_request(client_connection)
  30. client_connection.close()
  31. if __name__ =='__main__':
  32. serve_forever()

接下來,我們啟動伺服器:

  1. $ python webserver3b.py

現在,我們開啟一個新的終端視窗,並執行curl命令。你會立刻看到螢幕上打印出了“Hello, World!”這句話:

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

接著我們立刻再開啟一個終端視窗,並執行curl命令:

  1. $ curl http://localhost:8888/hello

如果你在60秒了完成了上面的操作,那麼第二個curl命令應該不會立刻產生任何輸出結果,而是處於掛死(hang)狀態。伺服器也不會在標準輸出中列印這個新請求的正文。下面這張圖就是我在自己的Mac上操作時的結果(右下角那個邊緣高亮為黃色的視窗,顯示的就是第二個curl命令掛死):

Mac上操作時的結果

Mac上操作時的結果

當然,你等了足夠長時間之後(超過60秒),你會看到第一個curl命令結束,然後第二個curl命令會在螢幕上打印出“Hello, World!”,之後再掛死60秒,最後才結束:

curl命令演示

curl命令演示

這背後的實現方式是,伺服器處理完第一個curl客戶端請求後睡眠60秒,才開始處理第二個請求。這些步驟是線性執行的,或者說迭代式一步一步執行的。在我們這個例項中,則是一次一個請求這樣處理。

接下來,我們簡單談談客戶端與伺服器之間的通訊。為了讓兩個程式通過網路進行通訊,二者均必須使用套接字。你在前兩章中也看到過套接字,但到底什麼是套接字?

什麼是套接字

什麼是套接字

套接字是通訊端點(communication endpoint)的抽象形式,可以讓一個程式通過檔案描述符(file descriptor)與另一個程式進行通訊。在本文中,我只討論Linux/Mac OS X平臺上的TCP/IP套接字。其中,尤為重要的一個概念就是TCP套接字對(socket pair)。

TCP連線所使用的套接字對是一個4元組(4-tuple),包括本地IP地址、本地埠、外部IP地址和外部埠。一個網路中的每一個TCP連線,都擁有獨特的套接字對。IP地址和埠號通常被稱為一個套接字,二者一起標識了一個網路端點。

套接字對合套接字

套接字對合套接字

因此,{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,它們在這裡被稱為一個套接字(同理,客戶端端點的兩個值也是一個套接字)。

伺服器建立套接字並開始接受客戶端連線的標準流程如下:

伺服器建立套接字並開始接受客戶端連線的標準流程

伺服器建立套接字並開始接受客戶端連線的標準流程

  1. 伺服器建立一個TCP/IP套接字。通過下面的Python語句實現:

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

  2. 伺服器可以設定部分套接字選項(這是可選項,但你會發現上面那行伺服器程式碼就可以確保你重啟伺服器之後,伺服器會繼續使用相同的地址)。

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

  3. 然後,伺服器繫結地址。繫結函式為套接字指定一個本地協議地址。呼叫繫結函式時,你可以單獨指定埠號或IP地址,也可以同時指定兩個引數,甚至不提供任何引數也沒問題。

    listen_socket.bind(SERVER_ADDRESS)

  4. 接著,伺服器將該套接字變成一個偵聽套接字:

    listen_socket.listen(REQUEST_QUEUE_SIZE)

listen方法只能由伺服器呼叫,執行後會告知伺服器應該接收針對該套接字的連線請求。

完成上面四步之後,伺服器會開啟一個迴圈,開始接收客戶端連線,不過一次只接收一個連線。當有連線請求時,accept方法會返回已連線的客戶端套接字。然後,伺服器從客戶端套接字讀取請求資料,在標準輸出中列印資料,並向客戶端返回訊息。最後,伺服器會關閉當前的客戶端連線,這時伺服器又可以接收新的客戶端連線了。

要通過TCP/IP協議與伺服器進行通訊,客戶端需要作如下操作:

客戶端與伺服器進行通訊所需要的操作

客戶端與伺服器進行通訊所需要的操作

下面這段示例程式碼,實現了客戶端連線至伺服器,傳送請求,並列印響應內容的過程:

  1. import socket
  2. # create a socket and connect to a server
  3. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  4. sock.connect(('localhost',8888))
  5. # send and receive some data
  6. sock.sendall(b'test')
  7. data = sock.recv(1024)
  8. print(data.decode())

在建立套接字之後,客戶端需要與伺服器進行連線,這可以通過呼叫connect方法實現:

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

客戶端只需要提供遠端IP地址或主機名,以及伺服器的遠端連線埠號即可。

你可能已經注意到,客戶端不會呼叫bindaccept方法。不需要呼叫bind方法,是因為客戶端不關心本地IP地址和本地埠號。客戶端呼叫connect方法時,系統核心中的TCP/IP棧會自動指定本地IP地址和本地埠。本地埠也被稱為臨時埠(ephemeral port)。

本地埠——臨時埠號

本地埠——臨時埠號

伺服器端有部分埠用於連線熟知的服務,這種埠被叫做“熟知埠”(well-known port),例如,80用於HTTP傳輸服務,22用於SSH協議傳輸。接下來,我們開啟Python shell,向在本地執行的伺服器發起一個客戶端連線,然後檢視系統核心為你建立的客戶端套接字指定了哪個臨時埠(在進行下面的操作之前,請先執行webserver3a.pywebserver3b.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)

在上面的示例中,我們看到核心為套接字指定的臨時埠是60589。

在開始回答第二部分最後提的問題之前,我需要快速介紹一些其他的重要概念。稍後你就會明白我為什麼要這樣做。我要介紹的重要概念就是程序(process)和檔案描述符(file descriptor)。

什麼是程序?程序就是正在執行的程式的一個例項。舉個例子,當伺服器程式碼執行的時候,這些程式碼就被載入至記憶體中,而這個正在被執行的伺服器的例項就叫做程序。系統核心會記錄下有關程序的資訊——包括程序ID,以便進行管理。所以,當你執行迭代式伺服器webserver3a.pywebserver3b.py時,你也就開啟了一個程序。

伺服器程序

伺服器程序

我們在終端啟動webserver3a.py伺服器:

  1. $ python webserver3b.py

然後,我們在另一個終端視窗中,使用ps命令來獲取上面那個伺服器程序的資訊:

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

ps命令的結果,我們可以看出你的確只運行了一個Python程序webserver3b。程序建立的時候,核心會給它指定一個程序ID——PID。在UNIX系統下,每個使用者程序都會有一個父程序(parent process),而這個父程序也有自己的程序ID,叫做父程序ID,簡稱PPID。在本文中,我預設大家使用的是BASH,因此當你啟動伺服器的時候,系統會建立伺服器程序,指定一個PID,而伺服器程序的父程序PID則是BASH shell程序的PID。

程序ID與父程序ID

程序ID與父程序ID

接下來請自己嘗試操作一下。再次開啟你的Python shell程式,這會建立一個新程序,然後我們通過os.gepid()os.getppid()這兩個方法,分別獲得Python shell程序的PID及它的父程序PID(即BASH shell程式的PID)。接著,我們開啟另一個終端視窗,執行ps命令,grep檢索剛才所得到的PPID(父程序ID,本操作時的結果是3148)。在下面的截圖中,你可以看到我在Mac OS X上的操作結果:

Mac OS X系統下程序ID與父程序ID演示

Mac OS X系統下程序ID與父程序ID演示

另一個需要掌握的重要概念就是檔案描述符(file descriptor)。那麼,到底什麼是檔案描述符?檔案描述符指的就是當系統開啟一個現有檔案、建立一個新檔案或是建立一個新的套接字之後,返回給程序的那個正整型數。系統核心通過檔案描述符來追蹤一個程序所開啟的檔案。當你需要讀寫檔案時,你也通過檔案描述符說明。Python語言中提供了用於處理檔案(和套接字)的高層級物件,所以你不必直接使用檔案描述符來指定檔案,但是從底層實現來看,UNIX系統中就是通過它們的檔案描述符來確定檔案和套接字的。

檔案描述符

檔案描述符

一般來說,UNIX shell會將檔案描述符0指定給程序的標準輸出,檔案描述富1指定給程序的標準輸出,檔案描述符2指定給標準錯誤。

標準輸入的檔案描述符

標準輸入的檔案描述符

正如我前面提到的那樣,即使Python語言提供了高層及的檔案或類檔案物件,你仍然可以對檔案物件使用fileno()方法,來獲取該檔案相應的檔案描述符。我們回到Python shell中來試驗一下。

  1. >>>import sys
  2. >>> sys.stdin
  3. <open file'<stdin>', mode 'r' at 0x102beb0c0>
  4. >>> sys.stdin.fileno()
  5. 0
  6. >>> sys.stdout.fileno()
  7. 1
  8. >>> sys.stderr.fileno()
  9. 2

在Python語言中處理檔案和套接字時,你通常只需要使用高層及的檔案/套接字物件即可,但是有些時候你也可能需要直接使用檔案描述符。下面這個示例演示了你如何通過write()方法向標準輸出中寫入一個字串,而這個write方法就接受檔案描述符作為自己的引數:

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

還有一點挺有意思——如果你知道Unix系統下一切都是檔案,那麼你就不會覺得奇怪了。當你在Python中建立一個套接字後,你獲得的是一個套接字物件,而不是一個正整型數,但是你還是可以和上面演示的一樣,通過fileno()方法直接訪問這個套接字的檔案描述符。

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

我還想再說一點:不知道大家有沒有注意到,在迭代式伺服器webserver3b.py的第二個示例中,我們的伺服器在處理完請求後睡眠60秒,但是在睡眠期間,我們仍然可以通過curl命令與伺服器建立連線?當然,curl命令並沒有立刻輸出結果,只是出於掛死狀態,但是為什麼伺服器既然沒有接受新的連線,客戶端也沒有立刻被拒絕,而是仍然繼續連線至伺服器呢?這個問題的答案在於套接字物件的listen方法,以及它使用的BACKLOG引數。在示例程式碼中,這個引數的值被我設定為REQUEST_QUEQUE_SIZEBACKLOG引數決定了核心中外部連線請求的佇列大小。當webserver3b.py伺服器睡眠時,你執行的第二個curl命令之所以能夠連線伺服器,是因為連線請求佇列仍有足夠的位置。

雖然提高BACKLOG引數的值並不會讓你的伺服器一次處理多個客戶端請求,但是業務繁忙的伺服器也應該設定一個較大的BACKLOG引數值,這樣accept函式就可以直接從佇列中獲取新連線,立刻開始處理客戶端請求,而不是還要花時間等待連線建立。

嗚呼!到目前為止,已經給大家介紹了很多知識。我們現在快速回顧一下之前的內容。

  • 迭代式伺服器
  • 伺服器套接字建立流程(socket, bind, listen, accept)
  • 客戶端套接字建立流程(socket, connect)
  • 套接字對(Socket pair)
  • 套接字
  • 臨時埠(Ephemeral port)與熟知埠(well-known port)
  • 程序
  • 程序ID(PID),父程序ID(PPID)以及父子關係
  • 檔案描述符(File descriptors)
  • 套接字物件的listen方法中BACKLOG引數的意義

現在,我可以開始回答第二部分留下的問題了:如何讓伺服器一次處理多個請求?換句話說,如何開發一個併發伺服器?

併發伺服器手繪演示

併發伺服器手繪演示

在Unix系統中開發一個併發伺服器的最簡單方法,就是呼叫系統函式fork()

fork()系統函式呼叫

fork()系統函式呼叫

下面就是嶄新的webserver3c.py併發伺服器,能夠同時處理多個客戶端請求:

  1. ###########################################################################
  2. #Concurrent server - webserver3c.py #
  3. ##
  4. #TestedwithPython2.7.9&Python3.4 on Ubuntu14.04&Mac OS X #
  5. ##
  6. #-Child process sleeps for60 seconds after handling a client's request #
  7. # - Parent and child processes close duplicate descriptors #
  8. # #
  9. ###########################################################################
  10. import os
  11. import socket
  12. import time
  13. SERVER_ADDRESS = (HOST, PORT) = '', 8888
  14. REQUEST_QUEUE_SIZE = 5
  15. def handle_request(client_connection):
  16. request = client_connection.recv(1024)
  17. print(
  18. 'Child PID:{pid}.Parent PID {ppid}'.format(
  19. pid=os.getpid(),
  20. ppid=os.getppid(),
  21. )
  22. )
  23. print(request.decode())
  24. http_response = b"""\
  25. HTTP/1.1 200 OK
  26. Hello, World!
  27. """
  28. client_connection.sendall(http_response)
  29. time.sleep(60)
  30. def serve_forever():
  31. listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  32. listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  33. listen_socket.bind(SERVER_ADDRESS)
  34. listen_socket.listen(REQUEST_QUEUE_SIZE)
  35. print('Serving HTTP on port {port}...'.format(port=PORT))
  36. print('Parent PID (PPID):{pid}\n'.format(pid=os.getpid()))
  37. while True:
  38. client_connection, client_address = listen_socket.accept()
  39. pid = os.fork()
  40. if pid == 0: # child
  41. listen_socket.close() # close child copy
  42. handle_request(client_connection)
  43. client_connection.close()
  44. os._exit(0) # child exits here
  45. else: # parent
  46. client_connection.close() # close parent copy and loop over
  47. if __name__ == '__main__':
  48. serve_forever()

在討論fork的工作原理之前,請測試一下上面的程式碼,親自確認一下伺服器是否能夠同時處理多個客戶端請求。我們通過命令列啟動上面這個伺服器:

  1. $ python webserver3c.py

然後輸入之前迭代式伺服器示例中的兩個curl命令。現在,即使伺服器子程序在處理完一個客戶端請求之後會睡眠60秒,但是並不會影響其他客戶端,因為它們由不同的、完全獨立的程序處理。你應該可以立刻看見curl命令輸出“Hello, World”,然後掛死60秒。你可以繼續執行更多的curl命令,所有的命令都會輸出伺服器的響應結果——“Hello, World”,不會有任何延遲。你可以試試。

關於fork()函式有一點最為重要,就是你呼叫fork一次,但是函式卻會返回兩次:一次是在父程序裡返回,另一次是在子程序中返回。當你fork一個程序時,返回給子程序的PID是0,而fork返回給父程序的則是子程序的PID。

fork函式

fork函式

我還記得,第一次接觸並使用fork函式時,自己感到非常不可思議。我覺得這就好像一個魔法。之前還是一個線性的程式碼,突然一下子克隆了自己,出現了並行執行的相同程式碼的兩個例項。我當時真的覺得這和魔法也差不多了。

當父程序fork一個新的子程序時,子程序會得到父程序檔案描述符的副本:

當父程序fork一個新的子程序時,子程序會得到父程序檔案描述符的副本

當父程序fork一個新的子程序時,子程序會得到父程序檔案描述符的副本

你可能也注意到了,上面程式碼中的父程序關閉了客戶端連線:

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

那為什麼父程序關閉了套接字之後,子程序卻仍然能夠從客戶端套接字中讀取資料呢?答案就在上面的圖片裡。系統核心根據檔案描述符計數(descriptor reference counts)來決定是否關閉套接字。系統只有在描述符計數變為0時,才會關閉套接字。當你的伺服器建立一個子程序時,子程序就會獲得父程序檔案描述符的副本,系統核心則會增加這些檔案描述符的計數。在一個父程序和一個子程序的情況下,客戶端套接字的檔案描述符計數為2。當上面程式碼中的父程序關閉客戶端連線套接字時,只是讓套接字的計數減為1,還不夠讓系統關閉套接字。子程序同樣關閉了父程序偵聽套接字的副本,因為子程序不關心要不要接收新的客戶端連線,只關心如何處理連線成功的客戶端所發出的請求。

  1. listen_socket.close()# close child copy

稍後,我會給大家介紹如果不關閉重複的描述符的後果。

從上面並行伺服器的原始碼可以看出,伺服器父程序現在唯一的作用,就是接受客戶端連線,fork一個新的子程序來處理該客戶端連線,然後回到迴圈的起點,準備接受其他的客戶端連線,僅此而已。伺服器父程序並不會處理客戶端請求,而是由它的子程序來處理。

談得稍遠一點。我們說兩個事件是並行時,到底是什麼意思?

並行事件

並行事件

我們說兩個事件是並行的,通常指的是二者同時發生。這是簡單的定義,但是你應該牢記它的嚴格定義:

如果你不能分辨出哪個程式會先執行,那麼二者就是並行的。

現在又到了回顧目前已經介紹的主要觀點和概念。

checkpoint

checkpoint

  • Unix系統中開發並行伺服器最簡單的方法,就是呼叫fork()函式
  • 當一個程序fork新程序時,它就成了新建立程序的父程序
  • 在呼叫fork之後,父程序和子程序共用相同的檔案描述符
  • 系統核心通過描述符計數來決定是否關閉檔案/套接字
  • 伺服器父程序的角色:它現在所做的只是接收來自客戶端的新連線,fork一個子程序來處理該客戶端的請求,然後回到迴圈的起點,準備接受新的客戶端連線

接下來,我們看看如果不關閉父程序和子程序中的重複套接字描述符,會發生什麼情況。下面的並行伺服器(webserver3d.py)作了一些修改,確保伺服器不關閉重複的:

  1. ###########################################################################
  2. #Concurrent server - webserver3d.py #
  3. ##
  4. #TestedwithPython2.7.9&Python3.4 on Ubuntu14.04&Mac OS X #
  5. ###########################################################################
  6. import os
  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. http_response = b"""\
  13. HTTP/1.1 200 OK
  14. Hello, World!
  15. """
  16. client_connection.sendall(http_response)
  17. def serve_forever():
  18. listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  19. listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
  20. listen_socket.bind(SERVER_ADDRESS)
  21. listen_socket.listen(REQUEST_QUEUE_SIZE)
  22. print('Serving HTTP on port {port} ...'.format(port=PORT))
  23. clients =[]
  24. whileTrue:
  25. client_connection, client_address = listen_socket.accept()
  26. # store the reference otherwise it's garbage collected
  27. # on the next loop run
  28. clients.append(client_connection)
  29. pid = os.fork()
  30. if pid == 0: # child
  31. listen_socket.close() # close child copy
  32. handle_request(client_connection)
  33. client_connection.close()
  34. os._exit(0) # child exits here
  35. else: # parent
  36. # client_connection.close()
  37. print(len(clients))
  38. if __name__ == '__main__':
  39. serve_forever()

啟動伺服器:

  1. $ python webserver3d.py

然後通過curl命令連線至伺服器:

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

我們看到,curl命令列印了並行伺服器的響應內容,但是並沒有結束,而是繼續掛死。伺服器出現了什麼不同情況嗎?伺服器不再繼續睡眠60秒:它的子程序會積極處理客戶端請求,處理完成後就關閉客戶端連線,然後結束執行,但是客戶端的curl命令卻不會終止。

伺服器不再睡眠,其子程序積極處理客戶端請求

伺服器不再睡眠,其子程序積極處理客戶端請求

那麼為什麼curl命令會沒有結束執行呢?原因在於重複的檔案描述符(duplicate file descriptor)。當子程序關閉客戶端連線時,系統核心會減少客戶端套接字的計數,變成了1。伺服器子程序結束了,但是客戶端套接字並沒有關閉,因為那個套接字的描述符計數並沒有變成0,導致系統沒有向客戶端傳送終止包(termination packet)(用TCP/IP的術語來說叫做FIN),也就是說客戶端仍然線上。但是還有另一個問題。如果你一直執行的伺服器不去關閉重複的檔案描述符,伺服器最終就會耗光可用的檔案伺服器:

檔案描述符

檔案描述符

按下Control-C,關閉webserver3d.py伺服器,然後通過shell自帶的ulimit命令檢視伺服器程序可以使用的預設資源:

  1. $ ulimit -a
  2. core filesize(blocks,-c)0
  3. data seg size(kbytes,-d) unlimited
  4. scheduling priority (-e)0
  5. filesize(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

從上面的結果中,我們可以看到:在我這臺Ubuntu電腦上,伺服器程序可以使用的檔案描述符(開啟的檔案)最大數量為1024。

現在,我們來看看如果伺服器不關閉重複的檔案描述符,伺服器會不會耗盡可用的檔案描述符。我們在現有的或新開的終端窗口裡,將伺服器可以使用的最大檔案描述符數量設定為256:

  1. $ ulimit -n 256

在剛剛運行了$ ulimit -n 256命令的終端裡,我們開啟webserver3d.py伺服器:

  1. $ python webserver3d.py

然後通過下面的client3.py客戶端來測試伺服器。

  1. #####################################################################
  2. #Test client - client3.py #
  3. ##
  4. #TestedwithPython2.7.9&Python3.4 on Ubuntu14.04&Mac OS X #
  5. #####################################################################
  6. import argparse
  7. import errno
  8. import os
  9. import socket
  10. SERVER_ADDRESS ='localhost',8888
  11. REQUEST = b"""\
  12. GET /hello HTTP/1.1
  13. Host: localhost:8888
  14. """
  15. def main(max_clients, max_conns):
  16. socks =[]
  17. for client_num in range(max_clients):
  18. pid = os.fork()
  19. if pid ==0:
  20. for connection_num in range(max_conns):
  21. sock = socket.socket(socket.AF_INET, socket.

    相關推薦

    自己動手開發一個 Web 伺服器

    在第二部分中,你開發了一個能夠處理HTTPGET請求的簡易WSGI伺服器。在上一篇的最後,我問了你一個問題:“怎樣讓伺服器一次處理多個請求?”讀完本文,你就能夠完美地回答這個問題。接下來,請你做好準備,因為本文的內容非常多,節奏也很快。文中的所有程式碼都可以在Github倉庫下載。 首先,我們簡單回憶一下

    手把手教你用nginx開發自己伺服器------利用nginx開發一個helloWorld程式

    之前兩篇文章已經說明了過程,今天稍微把過程說細一點,畢竟知其然還要知其所以然嘛,整個呼叫的邏輯是怎完整的呢?其實上兩篇文章看似簡單的將nginx處理一個請求的過程說出來了,但實際過程一點也不簡單,一個連線處理的過程,主要是複雜在準備階段(也就是各種回撥函式的掛載,上下文的準備

    自己動手實現java資料結構

    自己動手實現java資料結構(三) 棧 1.棧的介紹   在許多演算法設計中都需要一種"先進後出(First Input Last Output)"的資料結構,因而一種被稱為"棧"的資料結構被抽象了出來。   棧的結構類似一個罐頭:只有一個開口;先被放進去的東西沉在底下,後放進去的東西被

    一起寫一個Web伺服器3

    轉自:http://python.jobbole.com/81820/“發明創造時,我們學得最多” —— Piaget在本系列第二部分,你已經創造了一個可以處理基本的 HTTP GET 請求的 WSGI 伺服器。我還問了你一個問題,“怎麼讓伺服器在同一時間處理多個請求?”在本

    一起寫一個 Web 伺服器1

    轉自:http://python.jobbole.com/81524/有天一個女士出門散步,路過一個建築工地,看到三個男人在幹活。她問第一個男人,“你在幹什麼呢?”,第一個男人被問得很煩,咆哮道,“你沒看到我在碼磚嗎?”。她對回答不滿意,然後問第二個男人他在幹什麼。第二個男人

    一起寫一個 Web 伺服器2

    轉自:http://python.jobbole.com/81523/還記得嗎?在本系列第一部分我問過你:“怎樣在你的剛完成的WEB伺服器下執行 Django 應用、Flask 應用和 Pyramid 應用?在不單獨修改伺服器來適應這些不同的WEB框架的情況下。”往下看,來找

    java小白自己動手開發一個網站之域名的申請第4回

    新手小白,大神們看到什麼問題,請多多指出   目錄   域名的申請 域名的申請   之前想做部落格,聽說朋友用的阿里雲的域名很便宜,於是就過去申請了一個 登入賬號就是淘寶的賬號 地址: https://wanwang.al

    java小白自己動手開發一個網站之技術選型第3回

    新手小白,大神們看到什麼問題,請多多指出 目錄     MyWeb技術選型 一、域名 二 、網站空間 三 、開發環境: 四、框架選擇 1.前段 2.後端 五、資料庫 六、伺服器 MyWeb技術選型 一、域名 來

    java小白自己動手開發一個網站之搭建一個網站需要啥第2回

    新手小白,大神們看到什麼問題,請多多指出 目錄 新手小白,大神們看到什麼問題,請多多指出 搭建網站的流程 註冊域名 購買空間 製作網站 搭建網站的流程 搭建網站有哪些流程 1\註冊域名  2\購買空間  3\製作網站  &

    java小白自己動手開發一個網站之立項第1回

    新手小白,大神們看到什麼問題,請多多指出   MyWeb專案立項     修訂記錄表 修訂人 修訂版本 修訂描述 修訂時間 備註

    java小白自己動手開發一個網站之建立專案及域名訪問第5回

    新手小白,大神們看到什麼問題,請多多指出 目錄 一、建立專案  1.建立web專案,新增一個index.html頁面, 2.建立一個本地服務tomcat,並配置(檢驗tomcat是否成功,http://localhost:8080/) 3.後將專案新增進去,本地測試

    手把手教你用nginx開發自己伺服器------利用nginx開發一個helloWorld程式

    能開始學習nginx的你,肯定也擼了不少程式碼了,相信你學習程式碼都是從helloWorld開始的,那麼,今天我們就用nginx開發一個helloWorld,我們將要實現的功能就是當瀏覽器來訪問你的伺服器時,你的終端列印一個helloWorld。先別急著開始擼程式碼,先聊一聊

    手把手教你用nginx開發自己伺服器------利用nginx開發一個helloWorld程式

    現在我們正式開始編寫nginx的helloWorld功能,該從哪下手呢?別急,我們在上一篇文章中提到了事件驅動對吧。nginx是怎麼樣事件驅動的呢?我們來看看ngx_worker_process_cycle()這個函式的一部分for ( ;; ) { if

    自己動手開發Socks5代理伺服器

      一、Socks5協議簡介 socks5是基於傳輸層的協議,客戶端和伺服器經過兩次握手協商之後服務端為客戶端建立一條到目標伺服器的通道,在傳輸層轉發TCP/UDP流量。 關於socks5協議規範,到處都可以找到,我再重複一遍也沒啥意思,因此不再贅述,可以參見rfc1928(英文),或者查閱維

    從零寫一個Java WEB框架Dao層優化

    該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github

    python 實戰之模仿開發QQ聊天軟體TCP/IP伺服器與客戶端建設

    無論是p2p還是c/s還是b/s,只要用到通訊,必然是要用到今天寫的這個。 TCP/IP是網路軟體最核心的部分,缺少這個你只能當做單機遊戲玩。 TCP/IP,只需要搞清楚udp和tcp這兩個就可以了。 兩者的區別在於 udp每次傳送資訊都需要傳送ip和埠號,可以比

    自己動手一個Vue外掛MD.7

    造不完的輪子,封不完的外掛。網上什麼都有,但是有那找的功夫,自己都寫完了。漫島仍然在向前推進,只是你們看不到最新的更新內容了,剩餘的不會展示,等以後上線了再去看把。 講一下如何寫一個的Vue外掛,(以一個極其簡單的loading效果為例),會了這個其他不愁。 第一步,在compon

    windows下利用Node.js開發後臺伺服器

    三.為前端做資料介面 1.在專案資料夾下新建app.js檔案作為專案主入口檔案2.專案需要用到koa\koa-bodbparser\kou-router\kou-cors模組,先requrie進去 const Koa = require('koa'); const body

    一步一步開發Game伺服器載入指令碼和伺服器熱更新

    大家可能對遊戲伺服器的執行不太理解或者說不太清楚一些機制。 但是大家一定會明白一點,當程式在執行的時候出現一些bug,必須及時更新,但是不能重啟程式的情況下。 這裡牽涉到一個問題。比如說在遊戲裡面,,如果一旦開服,錯非完全致命性bug,否則是不能頻繁重啟伺服器程式的, 你重啟一次就可能流失一部分玩家。那

    一步一步開發Game伺服器載入指令碼和伺服器熱更新完整版

    可是在使用過程中,也許有很多會發現,動態載入dll其實不方便,應為需要預先編譯程式碼為dll檔案。便利性不是很高。 那麼有麼有辦法能做到動態實時更新呢???? 官方提供了這兩個物件,動態編譯原始檔。 提供對 C# 程式碼生成器和程式碼編譯器的例項的訪問。 CSharpCodeProvider