由淺入深 | 如何一步步地搭建一個(gè)Web服務(wù)器

魚相信要成為一個(gè)更好的開發(fā)人員敞峭,你必須更好地理解你每天使用的底層軟件系統(tǒng)踊谋,包括編程語言、編譯器和解釋器旋讹、數(shù)據(jù)庫和操作系統(tǒng)殖蚕、web服務(wù)器和web框架轿衔。

引言

有一天,一個(gè)女人出去散步到一個(gè)建筑工地睦疫,看見三個(gè)男人在工作呀枢。

她問第一個(gè)男人:“你在干什么?”第一個(gè)男人被這個(gè)問題惹惱了笼痛,叫道:“你沒看見我在砌磚嗎?”

她對(duì)回答不滿意琅拌,問第二個(gè)人在做什么缨伊。第二個(gè)男人回答說:“我在建一堵磚墻〗Γ”然后刻坊,他把注意力轉(zhuǎn)向第一個(gè)男人,說:“嘿党晋,你剛砌過了墻的盡頭谭胚。你需要把最后一塊磚頭摘下來∥床#”

她又一次對(duì)答案不滿意灾而,問第三個(gè)人在干什么。那人抬頭望著天空對(duì)她說:“我正在建造世界上最大的大教堂扳剿∨蕴耍”當(dāng)他站在那里仰望天空時(shí),另外兩個(gè)人開始為那塊亂七八糟的磚頭爭(zhēng)吵起來庇绽。男人轉(zhuǎn)身對(duì)前兩個(gè)人說:“嘿锡搜,伙計(jì)們,別擔(dān)心那塊磚頭瞧掺。這是一個(gè)內(nèi)墻耕餐,它會(huì)被粉刷過,沒有人會(huì)看到那塊磚辟狈。換一層吧肠缔。”

這個(gè)故事的寓意是上陕,當(dāng)你了解整個(gè)系統(tǒng)桩砰,了解不同部分(磚塊、墻壁释簿、大教堂)如何組合在一起時(shí)亚隅,你可以更快地識(shí)別和解決問題(錯(cuò)誤的磚塊)。

文章這樣的開頭庶溶,與《創(chuàng)建一個(gè)簡(jiǎn)單的Web服務(wù)器》有什么關(guān)系煮纵?

魚相信要成為一個(gè)更好的開發(fā)人員懂鸵,你必須更好地理解你每天使用的底層軟件系統(tǒng),包括編程語言行疏、編譯器和解釋器匆光、數(shù)據(jù)庫和操作系統(tǒng)、web服務(wù)器和web框架酿联。而且终息,為了更好更深入地了解這些系統(tǒng),你必須從頭開始贞让,一塊一塊地周崭,一堵墻一堵墻地重新構(gòu)建它們。

孔子這樣說:

聽而易忘

image

見而易記

image

做而易懂

image

魚希望你可以相信喳张,對(duì)不同的軟件系統(tǒng)進(jìn)行造輪子续镇,來學(xué)習(xí)它們的工作方式,是一個(gè)好辦法销部。

在這個(gè)由三部分組成的系列中摸航,魚將向你展示如何構(gòu)建自己的基本W(wǎng)eb服務(wù)器。Here we go舅桩!

初識(shí)Web服務(wù)器

首先酱虎,什么是Web服務(wù)器?

image

簡(jiǎn)單滴說江咳,它是一個(gè)網(wǎng)絡(luò)服務(wù)器逢净,位于物理服務(wù)器之上(沒看錯(cuò),就是服務(wù)器上的服務(wù)器)歼指,然后它等待客戶端發(fā)送請(qǐng)求爹土。當(dāng)它接收到請(qǐng)求時(shí),它生成一個(gè)響應(yīng)并將其發(fā)送回客戶端踩身≌鸵穑客戶端和服務(wù)器之間的通信使用HTTP協(xié)議進(jìn)行⌒瑁客戶端可以是你的瀏覽器琼娘,也可以是任何其他講HTTP的軟件。

最簡(jiǎn)單的Web服務(wù)器

一個(gè)非常簡(jiǎn)單的Web服務(wù)器實(shí)現(xiàn)是什么樣子的附鸽?

可以看下魚的這個(gè)例子脱拼,這個(gè)例子是用Python編寫的(在Python3.7+上進(jìn)行了測(cè)試),但是即使你不了解Python(這是一種很容易掌握的語言坷备,請(qǐng)嘗試Oㄅā)你仍然應(yīng)該能夠從下面的代碼和解釋中理解概念:

 # Python3.7+
import socket

HOST, PORT = '', 8888

listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind((HOST, PORT))
listen_socket.listen(1)
print(f'Serving HTTP on port {PORT} ...')
while True:
    client_connection, client_address = listen_socket.accept()
    request_data = client_connection.recv(1024)
    print(request_data.decode('utf-8'))

    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)
    client_connection.close()

將以上代碼保存為webserver1.py,然后使用以下命令運(yùn)行它:

$ python webserver1.py
Serving HTTP on port 8888 …

現(xiàn)在省撑,在Web瀏覽器的地址欄輸入http://localhost:8888/hello赌蔑,按Enter俯在,然后你應(yīng)該看到“Hello World!”顯示在瀏覽器中娃惯,如下所示:

image

快去實(shí)踐一遍吧跷乐,簡(jiǎn)單地一匹!

分析Web服務(wù)器工作原理

魚們來分析一下它到底是如何工作的趾浅。

首先讓魚們從你輸入的網(wǎng)址開始愕提。它被稱為URL,其基本結(jié)構(gòu)如下

image

這個(gè)就是你要告訴瀏覽器的地址皿哨,相當(dāng)于你要求瀏覽器查找和連接到的Web服務(wù)器的地址揪荣,以及服務(wù)器上要為你獲取的頁面(路徑)。

但是往史,在瀏覽器發(fā)送HTTP請(qǐng)求之前,它首先需要與Web服務(wù)器建立TCP連接佛舱。然后瀏覽器通過TCP連接向服務(wù)器發(fā)送HTTP請(qǐng)求椎例,并等待服務(wù)器發(fā)送HTTP響應(yīng)。

當(dāng)你的瀏覽器收到來自服務(wù)器的應(yīng)答時(shí)请祖,它會(huì)顯示出來订歪,在這種情況下,它會(huì)顯示“Hello, World!”

現(xiàn)在肆捕,讓魚們來更詳細(xì)地探討刷晋,客戶端和服務(wù)器在發(fā)送HTTP請(qǐng)求和響應(yīng)之前如何建立TCP連接。為此慎陵,魚使用所謂的套接字進(jìn)行模擬眼虱。你將通過在命令行上使用telnet手動(dòng)模擬瀏覽器,而不是直接使用瀏覽器席纽。

在運(yùn)行Web服務(wù)器的同一臺(tái)計(jì)算機(jī)上捏悬,在命令行上啟動(dòng)telnet會(huì)話,指定要連接到本地主機(jī)和8888的端口润梯,然后按Enter鍵:

$ telnet localhost 8888
Trying 127.0.0.1 …
Connected to localhost.

此時(shí)过牙,你已經(jīng)與本地主機(jī)上運(yùn)行的服務(wù)器建立了TCP連接,并準(zhǔn)備發(fā)送和接收HTTP消息纺铭。在下面的圖片中寇钉,你可以看到一個(gè)服務(wù)器必須經(jīng)過的標(biāo)準(zhǔn)過程,才能接受新的TCP連接舶赔。

image

更多地關(guān)注造殼扫倡。

同時(shí),魚們繼續(xù)試驗(yàn)顿痪,在同一個(gè)telnet會(huì)話中镊辕,輸入“GET /hello HTTP/1.1”油够,然后按Enter:

$ telnet localhost 8888
Trying 127.0.0.1 …
Connected to localhost.
GET /hello HTTP/1.1

HTTP/1.1 200 OK
Hello, World!

就在此時(shí),你手動(dòng)模擬了你的瀏覽器征懈!你發(fā)送了一個(gè)HTTP請(qǐng)求并得到了一個(gè)HTTP響應(yīng)石咬。這是HTTP請(qǐng)求的基本結(jié)構(gòu):

image

HTTP請(qǐng)求由這些元素組成:

  1. HTTP方法(GET)
  2. 表示所需服務(wù)器上的“頁面”的路徑(/hello)
  3. 協(xié)議版本(HTTP/1.1)

為了簡(jiǎn)單起見,魚們的Web服務(wù)器此時(shí)完全忽略了上面的請(qǐng)求數(shù)據(jù)卖哎。你也可以輸入任何垃圾而不是“GET/hello HTTP/1.1”鬼悠,然后你仍然會(huì)得到一個(gè)“hello,World亏娜!”回應(yīng)焕窝。

輸入請(qǐng)求行并按Enter鍵后,客戶機(jī)將請(qǐng)求發(fā)送到服務(wù)器维贺,服務(wù)器讀取請(qǐng)求行它掂,打印請(qǐng)求行并返回正確的HTTP響應(yīng)。

以下是服務(wù)器發(fā)送回客戶端的HTTP響應(yīng)(在本例中為telnet):

image

讓魚們解剖一下溯泣。HTTP響應(yīng)由這幾個(gè)元素組成:

  1. 響應(yīng)狀態(tài)行(協(xié)議版本+狀態(tài)碼/返回碼)虐秋,HTTP/1.1 200 OK,
  2. 一個(gè)必需的空行垃沦,
  3. HTTP響應(yīng)體客给。

響應(yīng)狀態(tài)行HTTP/1.1 200 OK由HTTP協(xié)議版本、HTTP狀態(tài)碼和HTTP狀態(tài)碼原因短語OK組成肢簿。當(dāng)瀏覽器得到響應(yīng)時(shí)靶剑,它會(huì)顯示響應(yīng)的主體,這就是為什么你會(huì)看到“Hello池充,World桩引!”在你的瀏覽器中。

這就是Web服務(wù)器工作的基本模式收夸〔郏總而言之:

  1. Web服務(wù)器創(chuàng)建一個(gè)監(jiān)聽套接字,并開始在循環(huán)中接受新連接咱圆。
  2. 客戶端啟動(dòng)一個(gè)TCP連接笛辟,并在成功建立它之后,客戶端向服務(wù)器發(fā)送一個(gè)HTTP請(qǐng)求序苏,服務(wù)器返回HTTP響應(yīng)結(jié)果手幢,其中包含了展示給用戶看的響應(yīng)內(nèi)容。

要建立TCP連接忱详,客戶端和服務(wù)器都使用套接字围来。

現(xiàn)在你有了一個(gè)非常基本的工作Web服務(wù)器,你可以使用瀏覽器或其他HTTP客戶端進(jìn)行測(cè)試监透。正如你已經(jīng)看到并希望嘗試過的那樣桶错,通過使用telnet并手動(dòng)鍵入HTTP請(qǐng)求,你也可以成為一個(gè)人工HTTP客服端胀蛮。

接下來魚們要提出一個(gè)問題:

“在不對(duì)服務(wù)器進(jìn)行任何更改的前提下院刁,你如何在新開發(fā)的Web服務(wù)器下,運(yùn)行/適配不同的Django應(yīng)用程序粪狼、Flask應(yīng)用程序和pyrampid應(yīng)用程序”

解耦Web服務(wù)器和Python應(yīng)用程序 —— WSGI

在過去退腥,你對(duì)Python Web框架的選擇會(huì)限制你對(duì)可用Web服務(wù)器的選擇,反之亦然再榄。如果框架和服務(wù)器設(shè)計(jì)為協(xié)同工作狡刘,那么一般是可行的:

image

但是,當(dāng)你嘗試將服務(wù)器和非設(shè)計(jì)為協(xié)同工作的框架結(jié)合在一起時(shí)困鸥,可能會(huì)遇到不match的問題:

image

基本上嗅蔬,你必須使用協(xié)同工作的東西,但有可能不是你想要使用的東西疾就。比如你希望用ServerA的某個(gè)特性和FrameworkB的某個(gè)功能倦青,但是FrameworkA不能滿足你浴鸿。

那么专缠,如何確倍炝洌可以使用多個(gè)Web框架運(yùn)行Web服務(wù)器吴趴,而不必對(duì)Web服務(wù)器或Web框架進(jìn)行代碼更改呢漆诽?這個(gè)問題的答案就是Python Web服務(wù)器網(wǎng)關(guān)接口(簡(jiǎn)稱WSGI,發(fā)音為wizgy)锣枝。

image

WSGI允許開發(fā)人員將Web框架與Web服務(wù)器解耦∠崾茫現(xiàn)在,你可以混合和匹配Web服務(wù)器和Web框架撇叁,并選擇適合你需要的配對(duì)供鸠。例如,可以使用Gunicorn陨闹、Nginx/uWSGI或Waitress運(yùn)行Django楞捂、Flask或Pyramid。這樣的解耦趋厉,得益于服務(wù)器和框架中的WSGI支持:

image

因此寨闹,WSGI就是問題的答案。你的Web服務(wù)器必須實(shí)現(xiàn)WSGI接口的服務(wù)器部分君账,并且要求所有的python web框架都已經(jīng)實(shí)現(xiàn)了WSGI接口的框架端繁堡。這樣,就可以將它們混合使用,而無需修改服務(wù)器代碼以適應(yīng)特定的Web框架椭蹄。

現(xiàn)在你知道闻牡,Web服務(wù)器和Web框架對(duì)WSGI的支持允許你選擇適合你的配對(duì),但這也有利于服務(wù)器和框架開發(fā)人員绳矩,因?yàn)樗麄兛梢詫W⒂谧约合矚g的專業(yè)領(lǐng)域罩润,而不是相互干涉。其他語言也有類似的接口:例如埋酬,Java有Servlet API哨啃,Ruby有Rack。

編寫自己的WSGI服務(wù)器

理想很美好写妥,但凡事都得“Show me the code”拳球。魚們來看看這個(gè)非常簡(jiǎn)單的WSGI服務(wù)器實(shí)現(xiàn):

# Tested with Python 3.7+ (Mac OS X)
import io
import socket
import sys


class WSGIServer(object):

    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM
    request_queue_size = 1

    def __init__(self, server_address):
        # Create a listening socket
        self.listen_socket = listen_socket = socket.socket(
            self.address_family,
            self.socket_type
        )
        # Allow to reuse the same address
        listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # Bind
        listen_socket.bind(server_address)
        # Activate
        listen_socket.listen(self.request_queue_size)
        # Get server host name and port
        host, port = self.listen_socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port
        # Return headers set by Web framework/Web application
        self.headers_set = []

    def set_app(self, application):
        self.application = application

    def serve_forever(self):
        listen_socket = self.listen_socket
        while True:
            # New client connection
            self.client_connection, client_address = listen_socket.accept()
            # Handle one request and close the client connection. Then
            # loop over to wait for another client connection
            self.handle_one_request()

    def handle_one_request(self):
        request_data = self.client_connection.recv(1024)
        self.request_data = request_data = request_data.decode('utf-8')
        # Print formatted request data a la 'curl -v'
        print(''.join(
            f'< {line}\n' for line in request_data.splitlines()
        ))

        self.parse_request(request_data)

        # Construct environment dictionary using request data
        env = self.get_environ()

        # It's time to call our application callable and get
        # back a result that will become HTTP response body
        result = self.application(env, self.start_response)

        # Construct a response and send it back to the client
        self.finish_response(result)

    def parse_request(self, text):
        request_line = text.splitlines()[0]
        request_line = request_line.rstrip('\r\n')
        # Break down the request line into components
        (self.request_method,  # GET
         self.path,            # /hello
         self.request_version  # HTTP/1.1
         ) = request_line.split()

    def get_environ(self):
        env = {}
        # The following code snippet does not follow PEP8 conventions
        # but it's formatted the way it is for demonstration purposes
        # to emphasize the required variables and their values
        #
        # Required WSGI variables
        env['wsgi.version']      = (1, 0)
        env['wsgi.url_scheme']   = 'http'
        env['wsgi.input']        = io.StringIO(self.request_data)
        env['wsgi.errors']       = sys.stderr
        env['wsgi.multithread']  = False
        env['wsgi.multiprocess'] = False
        env['wsgi.run_once']     = False
        # Required CGI variables
        env['REQUEST_METHOD']    = self.request_method    # GET
        env['PATH_INFO']         = self.path              # /hello
        env['SERVER_NAME']       = self.server_name       # localhost
        env['SERVER_PORT']       = str(self.server_port)  # 8888
        return env

    def start_response(self, status, response_headers, exc_info=None):
        # Add necessary server headers
        server_headers = [
            ('Date', 'Mon, 15 Jul 2019 5:54:48 GMT'),
            ('Server', 'WSGIServer 0.2'),
        ]
        self.headers_set = [status, response_headers + server_headers]
        # To adhere to WSGI specification the start_response must return
        # a 'write' callable. We simplicity's sake we'll ignore that detail
        # for now.
        # return self.finish_response

    def finish_response(self, result):
        try:
            status, response_headers = self.headers_set
            response = f'HTTP/1.1 {status}\r\n'
            for header in response_headers:
                response += '{0}: {1}\r\n'.format(*header)
            response += '\r\n'
            for data in result:
                response += data.decode('utf-8')
            # Print formatted response data a la 'curl -v'
            print(''.join(
                f'> {line}\n' for line in response.splitlines()
            ))
            response_bytes = response.encode()
            self.client_connection.sendall(response_bytes)
        finally:
            self.client_connection.close()


SERVER_ADDRESS = (HOST, PORT) = '', 8888


def make_server(server_address, application):
    server = WSGIServer(server_address)
    server.set_app(application)
    return server


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit('Provide a WSGI application object as module:callable')
    app_path = sys.argv[1]
    module, application = app_path.split(':')
    module = __import__(module)
    application = getattr(module, application)
    httpd = make_server(SERVER_ADDRESS, application)
    print(f'WSGIServer: Serving HTTP on port {PORT} ...\n')
    httpd.serve_forever()

代碼還是比較簡(jiǎn)單的(不到150行),大家都可以理解珍特,應(yīng)該不會(huì)出現(xiàn)陷入細(xì)節(jié)泥潭的情況祝峻。上面的服務(wù)器還可以做更多的事情——它可以運(yùn)行用你喜愛的Web框架編寫的基本W(wǎng)eb應(yīng)用程序,無論是Pyramid扎筒、Flask莱找、Django還是其他Python WSGI框架。

讓魚們來運(yùn)行試試看嗜桌。將上述代碼保存為webserver2.py奥溺。如果你試圖在沒有任何參數(shù)的情況下運(yùn)行它,它會(huì)提醒錯(cuò)誤并退出骨宠。

$ python webserver2.py
Provide a WSGI application object as module:callable

它真的需要你的Web應(yīng)用服務(wù)浮定。要運(yùn)行服務(wù)器,只需要安裝Python层亿。但是要運(yùn)行使用Pyramid桦卒、Flask和Django編寫的應(yīng)用程序,你需要先安裝這些框架匿又,讓魚們把這三個(gè)都安裝好方灾。魚首選的方法是使用venv創(chuàng)建一個(gè)虛擬環(huán)境,以免影響現(xiàn)有環(huán)境(venv在Python3.3版本以及以上默認(rèn)自帶)碌更。只需按照下面的步驟創(chuàng)建并激活一個(gè)虛擬環(huán)境裕偿,然后安裝所有三個(gè)Web框架。

$ python3 -m venv lsbaws
$ ls lsbaws
bin   include   lib   pyvenv.cfg
$ source lsbaws/bin/activate
(lsbaws) $ pip install -U pip
(lsbaws) $ pip install pyramid
(lsbaws) $ pip install flask
(lsbaws) $ pip install django

搭建WSGI + Pyramid應(yīng)用程序

此時(shí)痛单,你需要?jiǎng)?chuàng)建一個(gè)Web應(yīng)用程序击费。魚們先從Pyramid開始。將以下代碼另存為金字塔app.py保存到同一目錄webserver2.py

from pyramid.config import Configurator
from pyramid.response import Response


def hello_world(request):
    return Response(
        'Hello world from Pyramid!\n',
        content_type='text/plain',
    )

config = Configurator()
config.add_route('hello', '/hello')
config.add_view(hello_world, route_name='hello')
app = config.make_wsgi_app()

現(xiàn)在桦他,你可以使用自己的Web服務(wù)器蔫巩,來啟動(dòng)你的Pyramid應(yīng)用程序了:

(lsbaws) $ python webserver2.py pyramidapp:app
WSGIServer: Serving HTTP on port 8888 ...

你剛才告訴服務(wù)器從python模塊“Pyramid app”加載可調(diào)用的“app”谆棱,你的服務(wù)器現(xiàn)在可以接收請(qǐng)求并將它們轉(zhuǎn)發(fā)到你的pyramid中叫app的應(yīng)用程序。

同時(shí)圆仔,從webserver2.py代碼中可以看出垃瞧,應(yīng)用程序現(xiàn)在只處理一個(gè)路由:/hello路由。在瀏覽器網(wǎng)站上輸入http://localhost:8888/hello地址坪郭,按回車鍵个从,然后觀察結(jié)果:

image

當(dāng)然,你也可以在命令行中使用curl工具歪沃,也能達(dá)到同樣的效果:

$ curl -v http://localhost:8888/hello
...

你可以看下服務(wù)器和curl都輸出了些什么嗦锐。

搭建WSGI + Flask應(yīng)用程序

現(xiàn)在魚們移步到Flask,跟著魚一起做:

from flask import Flask
from flask import Response
flask_app = Flask('flaskapp')


@flask_app.route('/hello')
def hello_world():
    return Response(
        'Hello world from Flask!\n',
        mimetype='text/plain'
    )

app = flask_app.wsgi_app

將上述代碼保存為flaskapp.py沪曙,然后允許它:

(lsbaws) $ python webserver2.py flaskapp:app
WSGIServer: Serving HTTP on port 8888 ...

在瀏覽器網(wǎng)站上輸入http://localhost:8888/hello地址奕污,按回車鍵:

image

一樣的,你也可以在命令行中使用curl工具:

$ curl -v http://localhost:8888/hello
...

搭建WSGI + Django應(yīng)用程序

服務(wù)器還可以處理Django應(yīng)用程序嗎液走?當(dāng)然可以碳默,試試看!不過缘眶,它涉及的內(nèi)容稍微多一些嘱根,魚建議復(fù)制整個(gè)repo并使用djangoapp.py

下面的源代碼基本上將Django helloworld項(xiàng)目(使用Django的Django-admin.py startproject命令預(yù)先創(chuàng)建)添加到當(dāng)前Python路徑巷懈,然后導(dǎo)入項(xiàng)目的WSGI應(yīng)用程序该抒。

import sys
sys.path.insert(0, './helloworld')
from helloworld import wsgi


app = wsgi.application

將這段代碼保存為djangoapp.py,然后運(yùn)行起來:

(lsbaws) $ python webserver2.py djangoapp:app
WSGIServer: Serving HTTP on port 8888 ...

在瀏覽器網(wǎng)站上輸入地址顶燕,按回車鍵:

image

雖然已經(jīng)運(yùn)行過很多次凑保,但是你依然可以在命令行中使用curl工具,為了驗(yàn)證Django:

$ curl -v http://localhost:8888/hello
...

ok割岛,到目前為止,魚們把三個(gè)服務(wù)器都輪了一遍犯助,如果你還沒親手試過癣漆,最好動(dòng)下手,看是沒啥用的剂买。

WSGI程序分析

好吧惠爽,你已經(jīng)體驗(yàn)過WSGI的強(qiáng)大功能:它允許你混合匹配你的Web服務(wù)器和Web框架。

WSGI在Python Web服務(wù)器和pythonweb框架之間提供了一個(gè)最小的接口瞬哼。WSGI很簡(jiǎn)單婚肆,而且很容易在服務(wù)器端和框架端實(shí)現(xiàn)。

魚們來分析下魚們之前實(shí)現(xiàn)的WSGI的代碼坐慰。

以下代碼段顯示了服務(wù)器和接口的框架端:

def run_application(application):
    """Server code."""
    # This is where an application/framework stores
    # an HTTP status and HTTP response headers for the server
    # to transmit to the client
    headers_set = []
    # Environment dictionary with WSGI/CGI variables
    environ = {}

    def start_response(status, response_headers, exc_info=None):
        headers_set[:] = [status, response_headers]

    # Server invokes the ‘a(chǎn)pplication' callable and gets back the
    # response body
    result = application(environ, start_response)
    # Server builds an HTTP response and transmits it to the client
    ...

def app(environ, start_response):
    """A barebones WSGI app."""
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b'Hello world!']

run_application(app)

它的工作原理如下:

  1. 框架提供了一個(gè)可調(diào)用的“應(yīng)用程序”(WSGI規(guī)范沒有規(guī)定應(yīng)該如何實(shí)現(xiàn))较性,指的就是魚們的業(yè)務(wù)代碼
  2. 服務(wù)器為從HTTP客戶端接收的每個(gè)請(qǐng)求調(diào)用可調(diào)用的“應(yīng)用程序”。它將包含WSGI/CGI變量的字典“environ”和可作為參數(shù)調(diào)用的“start_response”傳遞給可調(diào)用的“application”。
  3. 框架/應(yīng)用程序生成一個(gè)HTTP狀態(tài)和HTTP響應(yīng)頭赞咙,并將它們傳遞給服務(wù)器可調(diào)用的“start_response”來存儲(chǔ)它們责循。框架/應(yīng)用程序還返回一個(gè)響應(yīng)體攀操。
  4. 服務(wù)器將狀態(tài)院仿、響應(yīng)頭和響應(yīng)體組合成一個(gè)HTTP響應(yīng)并將其傳輸?shù)娇蛻舳耍ù瞬襟E不是規(guī)范的一部分,但它是流中的下一個(gè)邏輯步驟速和,一般都是需要的)

下面是界面的可視化表示:

image

到目前為止歹垫,魚們已經(jīng)輪過了Pyramid、Flask和Django Web應(yīng)用程序颠放,還看到了實(shí)現(xiàn)WSGI規(guī)范的服務(wù)器端的服務(wù)器代碼排惨。

當(dāng)你使用這些框架之一編寫Web應(yīng)用程序時(shí),你可以在更高的級(jí)別上工作慈迈,而不直接使用WSGI若贮。

但魚知道你對(duì)WSGI接口的框架方面也很好奇,因?yàn)槟阏陂喿x本文痒留。

更多地關(guān)注造殼谴麦。

因此,讓魚們創(chuàng)建一個(gè)極簡(jiǎn)的WSGI Web應(yīng)用程序/Web框架伸头,而不使用Pyramid匾效、Flask或Django,并在服務(wù)器上運(yùn)行它:

def app(environ, start_response):
    """A barebones WSGI application.

    This is a starting point for your own Web framework :)
    """
    status = '200 OK'
    response_headers = [('Content-Type', 'text/plain')]
    start_response(status, response_headers)
    return [b'Hello world from a simple WSGI application!\n']

將上述代碼保存為wsgiapp.py恤磷,運(yùn)行它:

(lsbaws) $ python webserver2.py wsgiapp:app
WSGIServer: Serving HTTP on port 8888 ...

在瀏覽器網(wǎng)站上輸入地址面哼,按回車鍵:

image

這樣,魚們就實(shí)現(xiàn)了一個(gè)極簡(jiǎn)的WSGI Web“框架”扫步,沒錯(cuò)魔策,就是這么簡(jiǎn)單。

現(xiàn)在河胎,讓魚們回到服務(wù)器向客戶端傳輸?shù)膬?nèi)容闯袒。

下面是服務(wù)器在使用HTTP客戶端調(diào)用金字塔應(yīng)用程序時(shí)生成的HTTP響應(yīng):

image

這個(gè)響應(yīng)有一些是你在前文中能看到的熟悉部分,但它也有一些新的內(nèi)容游岳。例如政敢,它有四個(gè)你以前從未見過的HTTP頭:內(nèi)容類型、內(nèi)容長度胚迫、日期和服務(wù)器喷户。其實(shí)這些是Web服務(wù)器響應(yīng)通常應(yīng)該具有的頭。不過访锻,這些都不是嚴(yán)格要求的褪尝。報(bào)頭的目的是傳輸關(guān)于HTTP請(qǐng)求/響應(yīng)的附加信息闹获。

現(xiàn)在你已經(jīng)了解了更多關(guān)于WSGI接口的信息,同樣的恼五,下面HTTP響應(yīng)昌罩,其中包含了有關(guān)生成它的部分的更多信息:

image

魚還沒有提到“environ”字典,但基本上它是一個(gè)Python字典灾馒,必須包含WSGI規(guī)范指定的某些WSGI和CGI變量茎用。服務(wù)器在分析請(qǐng)求后從HTTP請(qǐng)求中獲取字典的值。這就是字典的內(nèi)容:

image

Web框架使用該字典中的信息睬罗,根據(jù)指定的路由轨功、請(qǐng)求方法等信息,決定使用哪個(gè)視圖容达、從何處讀取請(qǐng)求體以及在何處寫入錯(cuò)誤(如果有的話)古涧。

現(xiàn)在,你已經(jīng)創(chuàng)建了自己的WSGI Web服務(wù)器花盐,并使用不同的Web框架編寫了Web應(yīng)用程序羡滑。而且,你還創(chuàng)建了一個(gè)簡(jiǎn)單的Web應(yīng)用程序/Web框架算芯。

讓魚們回顧一下你的WSGI Web服務(wù)器必須做些什么來服務(wù)針對(duì)WSGI應(yīng)用程序的請(qǐng)求:

首先柒昏,服務(wù)器啟動(dòng)并加載Web框架/應(yīng)用程序提供的可調(diào)用的“應(yīng)用程序”;
然后熙揍,服務(wù)器讀取一個(gè)請(qǐng)求职祷;
然后,服務(wù)器解析它届囚;
然后有梆,它使用請(qǐng)求數(shù)據(jù)構(gòu)建一個(gè)“environ”字典;
然后意系,它用“environ”字典調(diào)用“application”泥耀,用“start_response”作為參數(shù)調(diào)用“start_response”,并返回一個(gè)響應(yīng)體蛔添;
然后痰催,服務(wù)器使用對(duì)“application”對(duì)象的調(diào)用返回的數(shù)據(jù)以及可調(diào)用的“start_response”設(shè)置的狀態(tài)和響應(yīng)頭來構(gòu)造HTTP響應(yīng);
最后作郭,服務(wù)器將HTTP響應(yīng)發(fā)送回客戶端陨囊。

image

就是這些步驟弦疮,貫穿了魚們的整個(gè)服務(wù)流程夹攒。

現(xiàn)在你有了一個(gè)可以工作的WSGI服務(wù)器,它可以為使用符合WSGI的Web框架(如Django胁塞、Flask咏尝、Pyramid或你自己的WSGI框架)編寫的基本W(wǎng)eb應(yīng)用程序提供服務(wù)压语。最理想的,是服務(wù)器可以與多個(gè)Web框架一起使用编检,而不需要對(duì)服務(wù)器代碼庫進(jìn)行任何更改胎食。

但這還不夠完美,甚至還有明顯的缺點(diǎn)允懂。

魚們來思考一下:“為了提高你的程序的性能厕怜,你如何讓你的服務(wù)器一次處理多個(gè)請(qǐng)求?”

創(chuàng)建并發(fā)服務(wù)器

在前面蕾总,魚們創(chuàng)建了一個(gè)極簡(jiǎn)的WSGI服務(wù)器粥航,它可以處理基本的HTTP GET請(qǐng)求。但是生百,它是一個(gè)“迭代服務(wù)器”递雀,一次處理一個(gè)客戶機(jī)請(qǐng)求。在處理完當(dāng)前客戶端請(qǐng)求之前蚀浆,它無法接受新連接缀程。有些客戶端可能不滿意,因?yàn)樗麄儗⒉坏貌慌抨?duì)等候市俊,而對(duì)于繁忙的服務(wù)器杨凑,排隊(duì)現(xiàn)象尤其嚴(yán)重。

image

串行的“迭代服務(wù)器”

魚們來看一眼魚們的“迭代服務(wù)器”秕衙,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()

為了更直觀地觀察服務(wù)器一次只處理一個(gè)請(qǐng)求蠢甲,魚們稍微“降低一下性能”,修改服務(wù)器并在向客戶端發(fā)送響應(yīng)后添加60秒延遲据忘。

image

“降低性能”的代碼保存為webserver3b.py:

#########################################################################
# Iterative server - webserver3b.py                                     #
#                                                                       #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X      #
#                                                                       #
# - Server sleeps for 60 seconds after sending a response to a client   #
#########################################################################
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)  # sleep and block the process for 60 seconds


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()

然后運(yùn)行起來:

$ python webserver3b.py

在命令行中請(qǐng)求一下鹦牛,你馬上就能看到“Hello, World!”:

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

然后,緊接著勇吊,魚們趕緊發(fā)起第二個(gè)請(qǐng)求:

$ curl http://localhost:8888/hello

如果你足夠地“趕緊”曼追,在60秒內(nèi)完成了發(fā)起了這兩個(gè)請(qǐng)求,那么第二個(gè)請(qǐng)求應(yīng)該不會(huì)馬上產(chǎn)生任何輸出汉规,而應(yīng)該只是hung在那里礼殊。服務(wù)器也不應(yīng)該在其標(biāo)準(zhǔn)輸出上打印新的請(qǐng)求體。下面是在魚的Mac上的情況(右下角以黃色突出顯示的窗口顯示第二個(gè)curl命令掛起针史,等待服務(wù)器接受連接):

image

當(dāng)你等了60秒到了晶伦,你就會(huì)看到第二個(gè)“Hello, World!”出現(xiàn)了,然后服務(wù)器繼續(xù)hung住60秒啄枕。

image

魚們看到服務(wù)器完成對(duì)第一個(gè)curl客戶機(jī)請(qǐng)求的服務(wù)婚陪,然后僅在第二個(gè)請(qǐng)求休眠60秒后才開始處理它。這一切都是因?yàn)椤暗?wù)器”是按順序或迭代地進(jìn)行的频祝,一步一個(gè)步驟泌参,或者在魚們的情況下脆淹,一次處理一個(gè)請(qǐng)求。

Socket沽一、進(jìn)程盖溺、文件描述符是什么

為了更好地分析魚們?cè)趺唇鉀Q這個(gè)性能問題,魚們來談?wù)効蛻舳撕头?wù)器之間的通信铣缠。

魚們?yōu)榱俗寖蓚€(gè)程序通過網(wǎng)絡(luò)相互通信烘嘱,必須使用套接字/Socket。前面的代碼魚們使用了Socker蝗蛙,那么什么是Socket拙友?

image

Socket是通信端點(diǎn)的抽象,它允許你的程序使用文件描述符與另一個(gè)程序通信歼郭。在本文中遗契,魚將特別討論Linux/Mac OS X上的TCP/IP socket。

其中病曾,魚們需要理解牍蜂,什么是Socket Pair(套接字對(duì))?

TCP連接的Socket Pair是一個(gè)4元組泰涂,用于標(biāo)識(shí)TCP連接的兩個(gè)端點(diǎn):本地IP地址鲫竞、本地端口、外部IP地址和外部端口逼蒙。Socket Pair唯一標(biāo)識(shí)網(wǎng)絡(luò)上的每個(gè)TCP連接从绘。標(biāo)識(shí)每個(gè)連接點(diǎn)的兩個(gè)值(IP地址和端口號(hào))通常稱為Socket。

image

所以是牢,元組{10.10.10.2:49152, 12.12.12.3:8888}是一個(gè)Socket Pair僵井,它唯一地標(biāo)識(shí)出客戶端上兩個(gè)終端的TCP連接;而元組 {12.12.12.3:8888, 10.10.10.2:49152}也是一個(gè)Socket Pair驳棱,標(biāo)識(shí)出服務(wù)器上兩個(gè)終端的TCP連接批什。地址12.12.12.3和端口8888兩個(gè)值,能標(biāo)識(shí)TCP連接的服務(wù)器端點(diǎn)社搅,在這里魚們稱之為Socket(客戶端依然)驻债。

服務(wù)器創(chuàng)建Socket并開始接受客戶端連接的標(biāo)準(zhǔn)順序如下:

image
  1. 服務(wù)器創(chuàng)建TCP/IP Socket。這是通過Python語句完成的:
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  1. 服務(wù)器可能會(huì)設(shè)置一些Socket參數(shù)選項(xiàng)(這是可選的參數(shù)形葬。魚們可以看到上面實(shí)現(xiàn)過的服務(wù)器代碼合呐,使用的是REUSEADDR,正是為了能夠反復(fù)使用同一地址)笙以。
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  1. 然后淌实,服務(wù)器綁定地址。bind函數(shù)為Socket分配一個(gè)本地協(xié)議地址。對(duì)于TCP翩伪,調(diào)用bind允許你指定端口號(hào)、IP地址谈息,或者兩者都指定缘屹,或者都不指定。
listen_socket.bind(SERVER_ADDRESS)
  1. 然后侠仇,監(jiān)聽轻姿。
listen_socket.listen(REQUEST_QUEUE_SIZE)

listen方法僅由服務(wù)器調(diào)用,客戶端沒有逻炊。它告訴內(nèi)核應(yīng)該接受這個(gè)Socket的傳入連接請(qǐng)求互亮。

完成后,服務(wù)器開始在一個(gè)循環(huán)中一次接受一個(gè)客戶端連接余素。當(dāng)有可用的連接時(shí)豹休,accept調(diào)用返回已連接的客戶端Socket。然后桨吊,服務(wù)器從連接的客戶端Socket中讀取請(qǐng)求數(shù)據(jù)威根,在其標(biāo)準(zhǔn)輸出上打印數(shù)據(jù),并將消息發(fā)送回客戶端。然后,服務(wù)器關(guān)閉客戶端連接嘹锁,并準(zhǔn)備再次接受新的客戶端連接誊锭。

魚們?cè)賮砜纯纯蛻舳送ㄟ^TCP/IP與服務(wù)器通信所需執(zhí)行的操作:

image

魚們?cè)倏纯纯蛻舳诉B接服務(wù)器再打印返回的代碼,比較簡(jiǎn)單:

import socket

 # create a socket and connect to a server
 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 sock.connect(('localhost', 8888))

 # send and receive some data
 sock.sendall(b'test')
 data = sock.recv(1024)
 print(data.decode())

客戶端創(chuàng)建完Socket之后竿音,需要連接服務(wù)端,這是通過connect函數(shù)做到的:

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

客戶端只需提供遠(yuǎn)程IP地址或主機(jī)名以及要連接到的服務(wù)器的遠(yuǎn)程端口號(hào)即可。

你可能已經(jīng)注意到客戶端沒有調(diào)用bind和accept谎砾,是的,客戶端不需要調(diào)用bind捧颅,因?yàn)榭蛻舳瞬魂P(guān)心本地IP地址和本地端口號(hào)棺榔。

當(dāng)客戶端調(diào)用connect時(shí),內(nèi)核中的TCP/IP堆棧會(huì)自動(dòng)分配本地IP地址和本地端口隘道。本地端口稱為臨時(shí)端口症歇,一般來說很快就釋放了。

image

一般谭梗,常用服務(wù)的端口稱為常用端口忘晤,如HTTP服務(wù)的80端口,SSH服務(wù)的22端口激捏。

如果你想知道你的客服端的本地端口是什么设塔,可一啟動(dòng)Python shell并與在本地主機(jī)上運(yùn)行的服務(wù)器建立客戶端連接,然后查看內(nèi)核為你創(chuàng)建的套接字分配的臨時(shí)端口(在嘗試以下示例之前啟動(dòng)服務(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分配給Socket闰蛔。

除了Socket之外痕钢,魚需要快速介紹一些其他重要的概念,分別是進(jìn)程和文件描述符序六。你很快就會(huì)明白這些概念為什么很重要任连。

什么是進(jìn)程?進(jìn)程只是執(zhí)行程序的一個(gè)實(shí)例例诀。例如随抠,當(dāng)服務(wù)器代碼被執(zhí)行時(shí),它被加載到內(nèi)存中繁涂,執(zhí)行程序的一個(gè)實(shí)例稱為進(jìn)程拱她。內(nèi)核記錄了一堆關(guān)于進(jìn)程的信息,它的進(jìn)程ID就是一個(gè)例子扔罪,用來跟蹤它秉沼。當(dāng)你運(yùn)行迭代服務(wù)器webserver3a.py或webserver3b.py時(shí),你只是運(yùn)行一個(gè)進(jìn)程矿酵。

image

在終端中啟動(dòng)webserver3b.py:

$ python webserver3b.py

在另一個(gè)終端中氧猬,使用ps命令查看這個(gè)進(jìn)程:

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

ps命令顯示你實(shí)際上只運(yùn)行了一個(gè)Python進(jìn)程webserver3b。當(dāng)創(chuàng)建一個(gè)進(jìn)程時(shí)坏瘩,內(nèi)核會(huì)為它分配一個(gè)進(jìn)程ID盅抚,即PID。在UNIX中倔矾,每個(gè)用戶進(jìn)程都有一個(gè)父進(jìn)程妄均,該父進(jìn)程又有自己的進(jìn)程ID,稱為父進(jìn)程ID哪自,簡(jiǎn)稱PPID丰包。魚假設(shè)你在默認(rèn)情況下運(yùn)行BASH shell,當(dāng)你啟動(dòng)服務(wù)器時(shí)壤巷,會(huì)創(chuàng)建一個(gè)帶有PID的新進(jìn)程邑彪,其父PID設(shè)置為bashshell的PID。

image

再次啟動(dòng)Python shell胧华,它將創(chuàng)建一個(gè)新進(jìn)程寄症,然后魚們使用os.getpid() 和os.getppid() 系統(tǒng)調(diào)用獲取pythonshell進(jìn)程的PID和父PID(bashshell的PID)。

然后矩动,在另一個(gè)終端窗口中為PPID(父進(jìn)程ID有巧,在魚的例子中是3148)運(yùn)行ps命令和grep。在下面的屏幕截圖中悲没,你可以看到魚的Mac OS X上的子Python shell進(jìn)程和父BASH shell進(jìn)程之間的父子關(guān)系示例:

image

另一個(gè)需要知道的重要概念是文件描述符篮迎。那么什么是文件描述符?是一個(gè)非負(fù)整數(shù)。

甜橱?什么鬼非負(fù)整數(shù)逊笆?

內(nèi)核在打開現(xiàn)有文件、創(chuàng)建新文件或創(chuàng)建新Socket時(shí)返回給進(jìn)程一個(gè)非負(fù)整數(shù)岂傲,這個(gè)非負(fù)整數(shù)就是文件描述符难裆。

你可能聽說過,在UNIX中譬胎,一切都是文件。內(nèi)核通過文件描述符引用進(jìn)程的打開文件命锄。當(dāng)你需要讀或?qū)懸粋€(gè)文件時(shí)堰乔,你可以用文件描述符來識(shí)別它。

Python為你提供了處理文件(和socket)的高級(jí)對(duì)象脐恩,你不必直接使用文件描述符來標(biāo)識(shí)文件镐侯,但實(shí)際上,在UNIX中驶冒,文件和socket是通過它們的整數(shù)文件描述符來標(biāo)識(shí)的苟翻。

image

默認(rèn)情況下,unixshell將文件描述符0分配給進(jìn)程的標(biāo)準(zhǔn)輸入骗污,將文件描述符1分配給進(jìn)程的標(biāo)準(zhǔn)輸出崇猫,將文件描述符2分配給標(biāo)準(zhǔn)錯(cuò)誤。

image

如前所述需忿,盡管Python提供了一個(gè)高級(jí)文件或類似文件的對(duì)象诅炉,但你始終可以對(duì)該對(duì)象使用 fileno() 方法來獲取與該文件關(guān)聯(lián)的文件描述符∥堇澹回到Python shell涕烧,看看魚們?nèi)绾巫龅竭@一點(diǎn):

>>> import sys
>>> sys.stdin
<open file '<stdin>', mode 'r' at 0x102beb0c0>
>>> sys.stdin.fileno()
0
>>> sys.stdout.fileno()
1
>>> sys.stderr.fileno()
2

在Python中處理文件和Socket時(shí),通常會(huì)使用高級(jí)文件/Socket對(duì)象汗洒,但有時(shí)可能需要直接使用文件描述符议纯。

下面是一個(gè)例子,說明了如何使用以文件描述符整數(shù)為參數(shù)的write系統(tǒng)調(diào)用將字符串寫入標(biāo)準(zhǔn)輸出:

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

這里有一個(gè)有趣的事溢谤,不過對(duì)你來說不再奇怪瞻凤,因?yàn)槟阋呀?jīng)知道所有東西都是Unix中的一個(gè)文件,你的socket也有一個(gè)與之相關(guān)的文件描述符世杀。同樣鲫构,在Python中創(chuàng)建一個(gè)socket時(shí),會(huì)返回一個(gè)對(duì)象玫坛,而不是一個(gè)非負(fù)整數(shù)结笨,但你始終可以使用前面提到的fileno() 方法直接訪問socket的整數(shù)文件描述符。

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

魚還想提一件事:你是否注意到在“迭代服務(wù)器”webserver3b.py中,當(dāng)服務(wù)器進(jìn)程休眠60秒時(shí)炕吸,你仍然可以使用第二個(gè)curl命令連接到服務(wù)器伐憾?當(dāng)然,curl沒有立即輸出任何內(nèi)容赫模,它只是掛在那里树肃,但為什么服務(wù)器當(dāng)時(shí)不接受連接,客戶端也沒有立即被拒絕瀑罗,而是能夠連接到服務(wù)器胸嘴?答案是socket對(duì)象的listen方法及其BACKLOG參數(shù),魚在代碼中稱之為REQUEST-QUEUE-SIZE斩祭。BACKLOG參數(shù)確定內(nèi)核中傳入連接請(qǐng)求的隊(duì)列大小劣像。當(dāng)服務(wù)器webserver3b.py處于睡眠狀態(tài)時(shí),你運(yùn)行的第二個(gè)curl命令能夠連接到服務(wù)器摧玫,因?yàn)閮?nèi)核在服務(wù)器套接字的傳入連接請(qǐng)求隊(duì)列中有足夠的可用空間耳奕。

雖然增加BACKLOG參數(shù)并不能神奇地將你的服務(wù)器轉(zhuǎn)變?yōu)橐淮慰梢蕴幚矶鄠€(gè)客戶機(jī)請(qǐng)求的服務(wù)器,但是對(duì)于繁忙的服務(wù)器诬像,有一個(gè)相當(dāng)大的backlog參數(shù)是很重要的屋群,這樣accept調(diào)用就不必等待建立新連接,而是可以立即從隊(duì)列中獲取新連接坏挠,并立即開始處理客戶端請(qǐng)求芍躏。

到目前為止,文章以上的內(nèi)容覆蓋了很多知識(shí)點(diǎn)降狠,魚們復(fù)習(xí)下:

image
  • 迭代服務(wù)器
  • 服務(wù)器Socket創(chuàng)建序列 (socket, bind, listen, accept)
  • 客戶端Socket創(chuàng)建序列 (socket, connect)
  • Socket Pair(套接字對(duì))
  • Socket
  • 臨時(shí)端口和常用端口
  • 進(jìn)程
  • 進(jìn)程id(PID)纸肉,父進(jìn)程id(PPID),父子進(jìn)程關(guān)系
  • 文件描述符
  • BACKLOG參數(shù)的含義

使用fork編寫并發(fā)服務(wù)器

現(xiàn)在魚們已經(jīng)準(zhǔn)備好回答那一個(gè)問題:“為了提高你的程序的性能喊熟,你如何讓你的服務(wù)器一次處理多個(gè)請(qǐng)求柏肪?”

或者魚們換一個(gè)問法:“你怎么寫一個(gè)并發(fā)的服務(wù)器呢?”

image

寫一個(gè)并發(fā)服務(wù)器最簡(jiǎn)單的方法芥牌,是在Unix系統(tǒng)下使用fork()系統(tǒng)調(diào)用烦味。

image

下面是新的閃亮登場(chǎng)的并發(fā)服務(wù)器的代碼,命名為webserver3c.py,它可以同時(shí)處理多個(gè)客戶端請(qǐng)求(在魚們的迭代服務(wù)器示例webserver3b.py中壁拉,每個(gè)子進(jìn)程睡眠60秒):

image
###########################################################################
# Concurrent server - webserver3c.py                                      #
#                                                                         #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X        #
#                                                                         #
# - Child process sleeps for 60 seconds after handling a client's request #
# - Parent and child processes close duplicate descriptors                #
#                                                                         #
###########################################################################
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:  # child
            listen_socket.close()  # close child copy
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)  # child exits here
        else:  # parent
            client_connection.close()  # close parent copy and loop over

if __name__ == '__main__':
    serve_forever()

在深入討論fork是如何工作的之前谬俄,魚們先嘗試一下并親眼確認(rèn)服務(wù)器確實(shí)可以同時(shí)處理多個(gè)客戶機(jī)請(qǐng)求。

不同于webserver3a.pywebserver3b.py弃理,使用以下命令行啟動(dòng)服務(wù)器:

$ python webserver3c.py

在迭代服務(wù)器上嘗試同樣的兩個(gè)curl命令溃论,然后魚們可以看到,即使服務(wù)器子進(jìn)程在服務(wù)客戶機(jī)請(qǐng)求后睡眠60秒痘昌,它也不會(huì)影響其他客戶端的請(qǐng)求钥勋,因?yàn)樗鼈冇刹煌耐耆?dú)立的進(jìn)程提供服務(wù)炬转。你應(yīng)該看到curl命令輸出“Hello,World算灸!”馬上就hung住60秒扼劈。你可以繼續(xù)運(yùn)行任意數(shù)量的curl命令(當(dāng)然不能大于fd的上限),所有這些命令都將立即輸出服務(wù)器的響應(yīng)“Hello菲驴,World”荐吵,不會(huì)有任何明顯的延遲。

關(guān)于fork()要注意一點(diǎn)赊瞬,一個(gè)代碼里面調(diào)用fork一次先煎,它會(huì)返回兩次:一次在父進(jìn)程中,一次在子進(jìn)程中巧涧。派生新進(jìn)程時(shí)薯蝎,返回給子進(jìn)程的進(jìn)程ID為0。當(dāng)fork在父進(jìn)程中返回時(shí)褒侧,它返回子進(jìn)程的PID良风。

image

魚依稀記得當(dāng)魚第一次讀到并嘗試fork的時(shí)候魚是多么的迷谊迄,現(xiàn)在這在魚看來依然很神奇闷供。

更多地關(guān)注造殼

當(dāng)父進(jìn)程派生新的子進(jìn)程時(shí)统诺,子進(jìn)程將獲取父進(jìn)程的文件描述符的副本:

image

你可能注意到上面代碼中的父進(jìn)程關(guān)閉了客戶端連接:

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

那么歪脏,當(dāng)一個(gè)子進(jìn)程的父進(jìn)程關(guān)閉了同一個(gè)套接字,它為什么還能從客戶端socket里面讀取數(shù)據(jù)呢粮呢?

答案如上圖所示婿失。內(nèi)核使用描述符引用計(jì)數(shù)來決定是否關(guān)閉socket。它只在其描述符引用計(jì)數(shù)變?yōu)?時(shí)啄寡,關(guān)閉套接字豪硅。

當(dāng)服務(wù)器創(chuàng)建子進(jìn)程時(shí),子進(jìn)程獲取父進(jìn)程文件描述符的副本挺物,內(nèi)核增加這些描述符的引用計(jì)數(shù)懒浮。在一個(gè)父進(jìn)程和一個(gè)子進(jìn)程的情況下,客戶端socket的描述符引用計(jì)數(shù)為2识藤,當(dāng)上面代碼中的父進(jìn)程關(guān)閉客戶端連接套接字時(shí)砚著,它只會(huì)減少其引用計(jì)數(shù),該計(jì)數(shù)將變?yōu)?痴昧,不足以導(dǎo)致內(nèi)核關(guān)閉socket稽穆。

另外,子進(jìn)程還關(guān)閉父進(jìn)程偵聽套接字的副本赶撰,因?yàn)樽舆M(jìn)程不關(guān)心接受新的客戶端連接舌镶,它只關(guān)心處理來自已建立的客戶端連接的請(qǐng)求:

listen_socket.close()  # close child copy

魚將在本文后面討論如果不關(guān)閉重復(fù)的描述符會(huì)發(fā)生什么柱彻。

從并發(fā)服務(wù)器的源代碼中可以看到,服務(wù)器父進(jìn)程現(xiàn)在的唯一角色是接受一個(gè)新的客戶端連接乎折,派生一個(gè)新的子進(jìn)程來處理該客戶端請(qǐng)求绒疗,并循環(huán)接受另一個(gè)客戶端連接,僅此而已骂澄。服務(wù)器父進(jìn)程不處理客戶端請(qǐng)求吓蘑,而是讓其子進(jìn)程處理。

魚們先討論另一個(gè)問題坟冲,魚們所說的兩個(gè)事件同時(shí)發(fā)生是什么意思磨镶?

image

當(dāng)魚們說兩個(gè)事件同時(shí)發(fā)生時(shí),通常是指它們同時(shí)發(fā)生健提。這個(gè)定義很好琳猫,但你應(yīng)該記住嚴(yán)格的定義:

如果你看不出哪個(gè)程序會(huì)先發(fā)生,那么兩個(gè)事件是并發(fā)的私痹。

再次重申一下脐嫂,現(xiàn)在是時(shí)候回顧一下你迄今為止所涉及的主要思想和概念了。

image
  • 在Unix中編寫并發(fā)服務(wù)器的最簡(jiǎn)單方法是使用fork()系統(tǒng)調(diào)用
  • 當(dāng)進(jìn)程分叉新進(jìn)程時(shí)紊遵,它將成為該新分叉子進(jìn)程的父進(jìn)程账千。
  • 在調(diào)用fork之后,父級(jí)和子級(jí)共享相同的文件描述符暗膜。
  • 內(nèi)核使用描述符引用計(jì)數(shù)來決定是否關(guān)閉文件/socket
  • 服務(wù)器父進(jìn)程的角色:它現(xiàn)在所做的只是接受來自客戶端的新連接匀奏,派生子進(jìn)程來處理客戶端請(qǐng)求,然后循環(huán)接受新的客戶端連接学搜。

回收文件描述符

讓魚們看看如果不關(guān)閉父進(jìn)程和子進(jìn)程中的重復(fù)套接字描述符娃善,將會(huì)發(fā)生什么。webserver3d.py是并發(fā)服務(wù)器的修改版本瑞佩,其中服務(wù)器不關(guān)閉重復(fù)的描述符:

###########################################################################
# Concurrent server - webserver3d.py                                      #
#                                                                         #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X        #
###########################################################################
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()
        # store the reference otherwise it's garbage collected
        # on the next loop run
        clients.append(client_connection)
        pid = os.fork()
        if pid == 0:  # child
            listen_socket.close()  # close child copy
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)  # child exits here
        else:  # parent
            # client_connection.close()
            print(len(clients))

if __name__ == '__main__':
    serve_forever()

運(yùn)行起來:

$ python webserver3d.py

使用curl請(qǐng)求服務(wù)器:

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

curl打印了并發(fā)服務(wù)器的響應(yīng)聚磺,但它沒有終止并一直掛起。服務(wù)器不再休眠60秒:其子進(jìn)程主動(dòng)處理客戶端請(qǐng)求炬丸,關(guān)閉客戶端連接并退出瘫寝,但客戶端這邊的curl仍然沒有終止。

image

為什么curl不終止御雕?原因是文件描述符還有余矢沿。

當(dāng)子進(jìn)程關(guān)閉客戶端連接時(shí),內(nèi)核減少了該客戶端套接字的引用計(jì)數(shù)酸纲,計(jì)數(shù)變?yōu)?捣鲸。服務(wù)器子進(jìn)程已退出,但客戶端套接字未被內(nèi)核關(guān)閉闽坡,因?yàn)樵撎捉幼置枋龇囊糜?jì)數(shù)不是0栽惶。因此愁溜,終止數(shù)據(jù)包(在TCP/IP術(shù)語中稱為FIN)未發(fā)送到客戶端,客戶端保持在線外厂。

如果長時(shí)間運(yùn)行的服務(wù)器沒有關(guān)閉重復(fù)的文件描述符冕象,它最終將耗盡可用的文件描述符:

image

使用Control-C終止你webserver3d.py程序,檢查下你所在服務(wù)器上的默認(rèn)可用資源汁蝶,可以使用ulimit命令:

$ 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

如上所示渐扮,在魚的Ubuntu機(jī)器上,服務(wù)器進(jìn)程可以使用的打開文件描述符(打開的文件)的最大數(shù)量是1024個(gè)掖棉。
現(xiàn)在讓魚們看看如果服務(wù)器不關(guān)閉重復(fù)的描述符墓律,它將如何耗盡可用的文件描述符。在現(xiàn)有或新的終端窗口中幔亥,將服務(wù)器的最大打開文件描述符數(shù)設(shè)置為256:

$ ulimit -n 256

在同一個(gè)終端中啟動(dòng)服務(wù):

$ python webserver3d.py

然后使用以下的自動(dòng)化代碼client3.py耻讽,模擬請(qǐng)求數(shù)量比較多的客戶端:

#####################################################################
# Test client - client3.py                                          #
#                                                                   #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X  #
#####################################################################
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è)新的終端中,啟動(dòng)client3.py帕棉,請(qǐng)求量設(shè)定為300:

$ python client3.py --max-clients=300

很快你的服務(wù)器就會(huì)爆炸针肥。這是魚實(shí)驗(yàn)時(shí)異常的截圖:

image

試驗(yàn)給魚們帶來的教訓(xùn)很清楚:服務(wù)器應(yīng)該關(guān)閉重復(fù)的描述符。

僵尸進(jìn)程的危害

但即使你關(guān)閉了重復(fù)的描述符香伴,你還沒有走出困境慰枕,因?yàn)槟愕姆?wù)器還有一個(gè)問題,那就是僵尸進(jìn)程瞒窒!

image

是的捺僻,其實(shí)你的服務(wù)器代碼實(shí)際上創(chuàng)建了僵尸進(jìn)程乡洼。讓魚們看看怎么做崇裁。魚們先重新啟動(dòng)服務(wù)器:

$ python webserver3d.py

在另一個(gè)終端中,使用curl請(qǐng)求:

$ curl http://localhost:8888/hello

現(xiàn)在運(yùn)行ps命令來顯示正在運(yùn)行的Python進(jìn)程束昵。這是魚的Ubuntu上ps輸出的例子:

$ ps auxw | grep -i python | grep -v grep
vagrant   9099  0.0  1.2  31804  6256 pts/0    S+   16:33   0:00 python webserver3d.py
vagrant   9102  0.0  0.0      0     0 pts/0    Z+   16:33   0:00 [python] <defunct>

你是否看到上面的第二行拔稳,其中顯示PID 9102的進(jìn)程的狀態(tài)是Z+,進(jìn)程的名稱是<deflunct>锹雏?那是魚們的僵尸進(jìn)程巴比。甚至魚們還不能殺死他們。

image

即使魚們使用kill -9礁遵,他們依然會(huì)存在轻绞。

什么是僵尸進(jìn)程?為什么魚們的服務(wù)器要?jiǎng)?chuàng)建僵尸進(jìn)程佣耐?

僵尸進(jìn)程是已終止的進(jìn)程政勃,但其父進(jìn)程尚未等待它,也尚未收到其終止?fàn)顟B(tài)兼砖。當(dāng)子進(jìn)程在其父進(jìn)程之前退出時(shí)奸远,內(nèi)核會(huì)將子進(jìn)程變成僵尸進(jìn)程既棺,并存儲(chǔ)一些有關(guān)該進(jìn)程的信息,供其父進(jìn)程以后檢索懒叛。存儲(chǔ)的信息通常是進(jìn)程ID丸冕、進(jìn)程終止?fàn)顟B(tài)和進(jìn)程的資源使用情況。

所以說薛窥,僵尸進(jìn)程是有目的的胖烛,但是如果你的服務(wù)器不處理這些僵尸進(jìn)程,你的系統(tǒng)最終就會(huì)被阻塞诅迷。

讓魚們?cè)囼?yàn)看看不清理僵尸進(jìn)程會(huì)怎么樣洪己。

首先停止正在運(yùn)行的服務(wù)器,并在新的終端窗口中竟贯,使用ulimit命令將max user processess設(shè)置為400(確保將open files設(shè)置為一個(gè)很高的數(shù)字答捕,也就是說500):

$ ulimit -u 400
$ ulimit -n 500

在剛剛運(yùn)行$ulimit-u 400命令的同一終端中,啟動(dòng)服務(wù)器webserver3d.py

$ python webserver3d.py

在新的終端窗口中屑那,啟動(dòng)client3.py并告訴它創(chuàng)建500個(gè)到服務(wù)器的同時(shí)連接:

$ python client3.py --max-clients=500

很快拱镐,你的服務(wù)器就會(huì)出現(xiàn)一個(gè)OSError:Resource temporary unavailable異常,當(dāng)它試圖創(chuàng)建一個(gè)新的子進(jìn)程時(shí)持际,創(chuàng)建失敗沃琅,因?yàn)樗呀?jīng)達(dá)到了允許創(chuàng)建的最大子進(jìn)程數(shù)的限制。這是魚機(jī)器上異常的截圖:

image

如你所見蜘欲,如果不處理僵尸進(jìn)程益眉,就會(huì)給長期運(yùn)行的服務(wù)器帶來問題。魚將后面會(huì)討論服務(wù)器應(yīng)該如何處理僵尸問題姥份。

魚們?cè)倩仡櫼幌轮R(shí)點(diǎn):

image
  • 如果不關(guān)閉重復(fù)的描述符郭脂,則客戶端不會(huì)終止,因?yàn)榭蛻舳诉B接不會(huì)關(guān)閉澈歉。
  • 如果不關(guān)閉重復(fù)的描述符展鸡,長期運(yùn)行的服務(wù)器最終將耗盡可用的文件描述符(最大打開文件數(shù))。
  • 當(dāng)派生子進(jìn)程并退出埃难,而父進(jìn)程不等待它莹弊,也不收集其終止?fàn)顟B(tài)時(shí),它將成為一個(gè)僵尸進(jìn)程涡尘。
  • 僵尸需要吃點(diǎn)東西忍弛,在魚們的例子中,這是記憶考抄。如果服務(wù)器不處理僵尸進(jìn)程细疚,那么它最終將耗盡可用進(jìn)程(最大用戶進(jìn)程)。
  • 你不能殺僵尸座泳,你需要等待惠昔。

解決僵尸進(jìn)程:信號(hào)處理程序 + 等待系統(tǒng)調(diào)用

那么你需要做什么來處理掉僵尸進(jìn)程呢幕与?你需要修改服務(wù)器代碼,以等待僵尸進(jìn)程镇防,獲得其終止?fàn)顟B(tài)啦鸣。然后可以通過修改服務(wù)器來調(diào)用等待系統(tǒng)調(diào)用來完成此操作。

不幸的是来氧,這遠(yuǎn)不是理想的诫给,因?yàn)槿绻{(diào)用wait,將阻塞服務(wù)器啦扬,從而有效地阻止服務(wù)器處理新的客戶端連接請(qǐng)求中狂。還有其他選擇嗎?是的扑毡,有胃榕,其中一個(gè)是信號(hào)處理程序和等待系統(tǒng)調(diào)用的組合。

image

魚們來看一下工作原理瞄摊。當(dāng)子進(jìn)程退出時(shí)勋又,內(nèi)核發(fā)送一個(gè)SIGCHLD信號(hào)。父進(jìn)程可以設(shè)置一個(gè)信號(hào)處理程序换帜,以異步通知該SIGCHLD事件楔壤,然后它可以等待子進(jìn)程收集其終止?fàn)顟B(tài),從而防止僵尸進(jìn)程留在周圍惯驼。

image

順便說一下蹲嚣,異步事件意味著父進(jìn)程不能提前知道事件將要發(fā)生。

修改服務(wù)器代碼以設(shè)定SIGCHLD事件祟牲,并在事件處理程序中等待終止的子進(jìn)程隙畜。修改代碼得webserver3e.py文件:

###########################################################################
# Concurrent server - webserver3e.py                                      #
#                                                                         #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X        #
###########################################################################
import os
import signal
import socket
import time

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5


def grim_reaper(signum, frame):
    pid, status = os.wait()
    print(
        'Child {pid} terminated with status {status}'
        '\n'.format(pid=pid, status=status)
    )


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)
    # sleep to allow the parent to loop over to 'accept' and block there
    time.sleep(3)


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))

    signal.signal(signal.SIGCHLD, grim_reaper)

    while True:
        client_connection, client_address = listen_socket.accept()
        pid = os.fork()
        if pid == 0:  # child
            listen_socket.close()  # close child copy
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)
        else:  # parent
            client_connection.close()

if __name__ == '__main__':
    serve_forever()

啟動(dòng)服務(wù)器:

$ python webserver3e.py

再請(qǐng)求一次:

$ curl http://localhost:8888/hello

看服務(wù)器終端:

image

發(fā)生了什么呢?accept調(diào)用失敗了疲眷,返回了EINTR錯(cuò)誤禾蚕。

image

當(dāng)子進(jìn)程退出導(dǎo)致SIGCHLD事件后您朽,父進(jìn)程accept調(diào)用會(huì)被阻塞,然后導(dǎo)致激活了信號(hào)處理程序,當(dāng)信號(hào)處理程序完成時(shí)摄职,accept系統(tǒng)調(diào)用被中斷:

image

Don’t worry, it’s a pretty simple problem to solve, though. All you need to do is to re-start the accept system call. Here is the modified version of the server webserver3f.py that handles that problem:
不過這是個(gè)很簡(jiǎn)單的問題脸候,只需要重新啟動(dòng)accept系統(tǒng)調(diào)用。修改版本webserver3f.py解決了這個(gè)問題:

###########################################################################
# Concurrent server - webserver3f.py                                      #
#                                                                         #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X        #
###########################################################################
import errno
import os
import signal
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024


def grim_reaper(signum, frame):
    pid, status = os.wait()


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))

    signal.signal(signal.SIGCHLD, grim_reaper)

    while True:
        try:
            client_connection, client_address = listen_socket.accept()
        except IOError as e:
            code, msg = e.args
            # restart 'accept' if it was interrupted
            if code == errno.EINTR:
                continue
            else:
                raise

        pid = os.fork()
        if pid == 0:  # child
            listen_socket.close()  # close child copy
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)
        else:  # parent
            client_connection.close()  # close parent copy and loop over


if __name__ == '__main__':
    serve_forever()

啟動(dòng)升級(jí)后的webserver3f.py:

$ python webserver3f.py

請(qǐng)求一下:

$ curl http://localhost:8888/hello

現(xiàn)在不再有EINTR異常了讯屈。

接下來魚們?cè)衮?yàn)證是否不再有僵尸進(jìn)程蛋哭,以及帶有wait調(diào)用的SIGCHLD事件處理程序是否處理已終止的子進(jìn)程。

要做到這一點(diǎn)涮母,只需運(yùn)行ps命令谆趾,并親自查看不再有Z+狀態(tài)的Python進(jìn)程(不再有<definct>進(jìn)程)就ok了躁愿。

image
  • 如果你用fork創(chuàng)建一個(gè)子進(jìn)程而不wait它,它就會(huì)變成僵尸進(jìn)程沪蓬;
  • 處理僵尸進(jìn)程的方法:使用SIGCHLD事件處理程序彤钟,異步等待已終止的子進(jìn)程獲取其終止?fàn)顟B(tài);
  • 當(dāng)使用事件處理程序時(shí)跷叉,你需要記住系統(tǒng)調(diào)用可能會(huì)被中斷逸雹,并且你需要為該場(chǎng)景做好準(zhǔn)備。

好吧云挟,到目前為止還不錯(cuò)梆砸。沒問題吧?好吧园欣,差不多了帖世。再次嘗試webserver3f.py,但不要使用curl發(fā)出一個(gè)請(qǐng)求沸枯,而是使用client3.py創(chuàng)建128個(gè)同時(shí)連接:

$ python client3.py --max-clients 128

運(yùn)行下ps命令看下:

$ ps auxw | grep -i python | grep -v grep

然而狮暑,僵尸進(jìn)程還在。

image

這次是什么問題呢辉饱?當(dāng)你同時(shí)運(yùn)行128個(gè)客戶端并建立128個(gè)連接時(shí)搬男,服務(wù)器上的子進(jìn)程處理這些請(qǐng)求并幾乎同時(shí)退出,導(dǎo)致大量SIGCHLD信號(hào)被發(fā)送到父進(jìn)程彭沼。問題是缔逛,信號(hào)沒有排隊(duì),服務(wù)器進(jìn)程錯(cuò)過了幾個(gè)信號(hào)姓惑,導(dǎo)致幾個(gè)僵尸進(jìn)程無人值守:

image

解決這個(gè)問題的方法是褐奴,設(shè)置一個(gè)SIGCHLD事件處理程序,但是不要等待于毙,而是在循環(huán)中使用帶WNOHANG選項(xiàng)的waitpid系統(tǒng)調(diào)用敦冬,以確保所有終止的子進(jìn)程都得到處理。以下是修改后的服務(wù)器代碼webserver3g.py

###########################################################################
# Concurrent server - webserver3g.py                                      #
#                                                                         #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X        #
###########################################################################
import errno
import os
import signal
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024


def grim_reaper(signum, frame):
    while True:
        try:
            pid, status = os.waitpid(
                -1,          # Wait for any child process
                 os.WNOHANG  # Do not block and return EWOULDBLOCK error
            )
        except OSError:
            return

        if pid == 0:  # no more zombies
            return


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))

    signal.signal(signal.SIGCHLD, grim_reaper)

    while True:
        try:
            client_connection, client_address = listen_socket.accept()
        except IOError as e:
            code, msg = e.args
            # restart 'accept' if it was interrupted
            if code == errno.EINTR:
                continue
            else:
                raise

        pid = os.fork()
        if pid == 0:  # child
            listen_socket.close()  # close child copy
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)
        else:  # parent
            client_connection.close()  # close parent copy and loop over

if __name__ == '__main__':
    serve_forever()

啟動(dòng)服務(wù):

$ python webserver3g.py

發(fā)起請(qǐng)求:

$ python client3.py --max-clients 128

現(xiàn)在可以確認(rèn)沒有僵尸進(jìn)程了唯沮。

image

總結(jié)

恭喜脖旱!代碼的旅途很漫長,但總算結(jié)束了介蛉。

現(xiàn)在你擁有了自己的簡(jiǎn)單并發(fā)服務(wù)器萌庆,代碼可以作為你進(jìn)一步面向生產(chǎn)級(jí)Web服務(wù)器的基礎(chǔ)。

接下來是什么币旧?正如喬霞眨·比林斯所說,

“就像一張郵票,堅(jiān)持一件事巍虫,直到你到達(dá)那里彭则。”

開始掌握基本知識(shí)占遥,質(zhì)疑你已經(jīng)知道的贰剥,然后總是深入挖掘。

image

“如果你只學(xué)方法筷频,你就會(huì)被方法束縛住蚌成。但如果你學(xué)會(huì)了原則,你就可以設(shè)計(jì)出自己的方法凛捏〉S牵” —— 愛默生。

下面是魚為這篇文章中的大部分內(nèi)容而繪制的書籍列表坯癣。它們將幫助你拓寬和加深你對(duì)魚所涉及主題的知識(shí)瓶盛。魚強(qiáng)烈建議你以某種方式去買那些書:從你的朋友那里借,從你當(dāng)?shù)氐膱D書館里看示罗,或者在亞馬遜上買:

  1. Unix Network Programming, Volume 1: The Sockets Networking API (3rd Edition)
  2. Advanced Programming in the UNIX Environment, 3rd Edition
  3. The Linux Programming Interface: A Linux and UNIX System Programming Handbook
  4. TCP/IP Illustrated, Volume 1: The Protocols (2nd Edition) (Addison-Wesley Professional Computing Series)
  5. The Little Book of SEMAPHORES (2nd Edition): The Ins and Outs of Concurrency Control and Common Mistakes. Also available for free on the author’s site here.

先這樣吧

原文惩猫,若有錯(cuò)誤之處請(qǐng)指出,更多地關(guān)注造殼蚜点。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末轧房,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子绍绘,更是在濱河造成了極大的恐慌奶镶,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陪拘,死亡現(xiàn)場(chǎng)離奇詭異厂镇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)左刽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門捺信,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人欠痴,你說我怎么就攤上這事迄靠。” “怎么了斋否?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵梨水,是天一觀的道長。 經(jīng)常有香客問我茵臭,道長,這世上最難降的妖魔是什么舅世? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任旦委,我火速辦了婚禮奇徒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缨硝。我一直安慰自己摩钙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布查辩。 她就那樣靜靜地躺著胖笛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宜岛。 梳的紋絲不亂的頭發(fā)上长踊,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音萍倡,去河邊找鬼身弊。 笑死,一個(gè)胖子當(dāng)著我的面吹牛列敲,可吹牛的內(nèi)容都是我干的阱佛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼戴而,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼凑术!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起所意,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤麦萤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后扁眯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壮莹,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年姻檀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了命满。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绣版,死狀恐怖胶台,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情杂抽,我是刑警寧澤诈唬,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站缩麸,受9級(jí)特大地震影響铸磅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一阅仔、第九天 我趴在偏房一處隱蔽的房頂上張望吹散。 院中可真熱鬧,春花似錦八酒、人聲如沸空民。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽界轩。三九已至,卻和暖如春衔瓮,著一層夾襖步出監(jiān)牢的瞬間浊猾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國打工报辱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留与殃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓碍现,卻偏偏與公主長得像幅疼,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子昼接,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353