<menu id="ycqsw"></menu><nav id="ycqsw"><code id="ycqsw"></code></nav>
<dd id="ycqsw"><menu id="ycqsw"></menu></dd>
  • <nav id="ycqsw"></nav>
    <menu id="ycqsw"><strong id="ycqsw"></strong></menu>
    <xmp id="ycqsw"><nav id="ycqsw"></nav>
  • 服務器是干什么用的,服務器的作用介紹


    有天一個女人出去散步,她經過建筑工地時看到三個人正在干活。她上去問第一個人,你在干什么呢?第一個人覺得這問題很惱人,厲聲道,你看不到我在砌磚頭嗎?不甚滿意的女人又問第二個人他在做什么。第二個人答道,我在砌一道磚墻。然后他看了下第一個人,喊道,嘿,你超過墻的長度了,把最后一塊磚拿下來。女人還是不滿意這個答案,他問第三個人。這個人呢,他一邊看著天一邊跟她說,我在建這世上從未有過的大教堂。在他抬頭望天的時候,另兩個人在為磚頭的對錯爭吵不休。這人轉向那兩人說,伙計們,別為那一塊磚當心了。這是個內墻,它會被粉刷沒人能看到磚頭的。把它放到另一層去吧。

    這個故事的寓意是,當你知道整個系統,了解不同組件如何相互配合(磚,墻壁,教堂),你能快速找到和快速解決問題(磚)。

    它對你從頭開始建web服務器有什么啟示呢?

    我相信要成為好的開發者,你必須對日常使用的軟件底層系統有更好的理解,這包括編程語言,編譯器和解釋器,數據庫和操作系統,web服務器和web框架。而為了能更好更深的理解這些系統,你必須從頭開始重建他們,從一磚一瓦開始。

    老夫子有言曰:

    我聽見了,我就忘了;

    自己寫一個Web服務器

    我看見了,我就記得了;

    自己寫一個Web服務器

    我做過了,我就理解了。

    自己寫一個Web服務器

    希望你同意這點,我們重新建構軟件系統是學習他們怎樣運作的好方法。

    在這個分為三部分的系列中,我將展示給你怎樣搭建你自己的web服務器。我們開始吧。

    自己寫一個Web服務器

    簡而言之,這是一個運行在物理服務器上的網絡服務器,它等待客戶端發送的請求。當它收到一個請求,它會生成一個回復并傳回到客戶端。一個客戶端和服務器的通信時通過HTTP協議實現??蛻舳丝梢允悄愕臑g覽器或任何其他應用HTTP的軟件。

    一個簡單的web服務器是什么樣呢?這是我給出的答案。這個例子是用Python的,但即使你不懂Python也能通過下面的代碼和解釋理解這些概念。

    自己寫一個Web服務器

    將上面代碼保存為’websever1.py’,或者直接在GitHub上下載,然后想下面那樣在命令行中運行

    $ python webserver1.py

    Serving HTTP on port 8888 …

    現在在瀏覽器地址欄輸入http://localhost:8888/hello,按Enter鍵,奇跡就發生率。你應該能在瀏覽器看到“Hello,World”,如圖所示:

    自己寫一個Web服務器

    看到了吧。讓我們來看看它到底是怎么做到的。

    從你鍵入的網址開始。它是一個URL下面是他的基本結構:

    自己寫一個Web服務器

    這是你告訴瀏覽器找尋web服務器和連接的地址,也是你要獲取的服務器上的頁面(路徑)。在你的瀏覽器發送HTTP請求前,它需要和web服務器建立一個TCP連接。然后它通過TCP連接發出HTTP請求,接著等待服務器返回一個HTTP響應。當瀏覽器收到響應后顯示出來,在這個例子中它顯示為“Hello World!”

    再來詳細看看客戶端和服務器怎樣在HTTP請求響應之前建立TCP連接的。要做到這個,他們都用到了socket。不要直接用瀏覽器,你用telnet命令行來手動模擬瀏覽器。

    在同一臺電腦上通過telnet會話運行web服務器,指定localhost和8888端口,按下Enter:

    $ telnet localhost 8888

    Trying 127.0.0.1 …

    Connected to localhost.

    這樣你就已經跟服務器建立了TCP連接,它運行在本地主機準備好發送和接收HTTP消息。下面的圖片中你可以看到服務器必須經過一個標準的程序才可能接受一個新的TCP連接。

    自己寫一個Web服務器

    在同一telnet會話中鍵入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!

    你剛才手動模擬了瀏覽器!你發送了一個HTTP請求然后收到了一個HTTP響應。這就是HTTP請求的基本結構:

    自己寫一個Web服務器

    HTTP請求由HTTP方法(GET,因為我們要求服務器返回給我們寫東西),路徑/hello指向服務器的一個“頁面”和協議版本。

    為了簡單的找到我們的web服務器,這個例子完全無視上面的命令行。你可以用任何沒意義的東西替換“GET /hello HTTP/1.1”,還是會得到“Hello, World!”

    一旦你輸入了請求而且按下了Enter,客戶端就像服務器發出了請求,服務器讀取請求,打印它然后做出適當的響應。

    這是HTTP響應,服務器傳送到你的客戶端的過程(這里是telnet):

    自己寫一個Web服務器

    我們來剖析它。響應由一個狀態行 HTTP/1.1 200 OK, 接著一個空白行,然后是HTTP響應主體。

    狀態行 HTTP/1.1 200 OK, 由HTTP版本,HTTP狀態代碼和HTTP狀態代碼指示短語 OK 組成。當瀏覽器收到響應,它顯示出響應主體,這就是為什么你在瀏覽器中看到“Hello, World!”

    這就是一個web服務器運行的基本模型??偨Y起來:web服務器創建一個監聽socket持續地接受新的連接??蛻舳税l起一個TCP連接,然后成功建立連接,客戶端發出一個HTTP請求給服務器,服務器用HTTP響應來做回復,最后呈現給用戶。建立TCP連接的過程中客戶端和服務器都使用了 socket。

    現在你有了一個基本的服務器,可以在瀏覽器和其他HTTP客戶端去測試。如你所見,你也可以做個人肉HTTP客戶端,用telnet同時手動鍵入hTTP請求就行。

    那么問題來了:你怎么在你剛建立的web服務器上運行一個Django應用,Flask應用和Pyramid應用,如何不做任何改變而適應不同的web架構呢?

    在以前,你選擇 Python web 架構會受制于可用的web服務器,反之亦然。如果架構和服務器可以協同工作,那你就走運了:

    自己寫一個Web服務器

    但你有可能面對(或者曾有過)下面的問題,當要把一個服務器和一個架構結合起來是發現他們不是被設計成協同工作的:

    自己寫一個Web服務器

    基本上你只能用可以一起運行的而非你想要使用的。

    那么,你怎么可以不修改服務器和架構代碼而確??梢栽诙鄠€架構下運行web服務器呢?答案就是 Python Web Server Gateway Interface (或簡稱 WSGI,讀作“wizgy”)。

    自己寫一個Web服務器

    WSGI允許開發者將選擇web框架和web服務器分開?,F在你可以混合匹配web服務器和web框架,選擇一個適合你需要的配對。比如,你可以在Gunicorn 或者 Nginx/uWSGI 或者 Waitress上運行 Django, Flask, 或 Pyramid。真正的混合匹配,得益于WSGI同時支持服務器和架構:

    自己寫一個Web服務器

    WSGI是第一篇和這篇開頭又重復問道問題的答案。你的web服務器必須具備

    WSGI接口,所有的現代Python Web框架都已具備WSGI接口,它讓你不對代碼作修改就能使服務器和特點的web框架協同工作。

    現在你知道WSGI由web服務器支持,而web框架允許你選擇適合自己的配對,但它同樣對于服務器和框架開發者提供便利使他們可以專注于自己偏愛的領域和專長而不至于相互牽制。其他語言也有類似接口:java有Servlet API,Ruby 有 Rack。

    說這么多了,你肯定在喊,給我看代碼!好吧,看看這個極簡的WSGI服務器實現:

    自己寫一個Web服務器
    自己寫一個Web服務器
    自己寫一個Web服務器
    自己寫一個Web服務器
    自己寫一個Web服務器

    這比第一篇的代碼長的多,但也足夠短(只有150行)來讓你理解而避免在細節里越陷越深。上面的服務器可以做更多——可以運行你鐘愛web框架所寫基本的web應用,Pyramid, Flask, Django, 或其他 Python WSGI 框架.

    不相信我?你自己試試看。保存上面的代碼為webserver2.py或者直接在Github下載。如果你不傳入任何參數它會提醒然后推出。

    $ python webserver2.py

    Provide a WSGI application object as module:callable

    它需要為web應用服務,這樣才會有意思。運行服務器你唯一要做的就是按照python。但是要運行 Pyramid, Flask, 和 Django 寫的應用你得先按照這些框架。我們索性三個都安裝好了。我偏愛用virtualenv。只要按照下面的步驟創建一個虛擬環境然后按照這三個web框架。

    $ [sudo] pip install virtualenv

    $ mkdir ~/envs

    $ virtualenv ~/envs/lsbaws/

    $ cd ~/envs/lsbaws/

    $ ls

    bin include lib

    $ source bin/activate

    (lsbaws) $ pip install pyramid

    (lsbaws) $ pip install flask

    (lsbaws) $ pip install django

    這時你要建立一個web應用了。我們從Pyramid開始。在webserver2.py所在的文件夾保存下面代碼為pyramidapp.py,也可以直接在Githhub下載:

    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

    你的服務器已經為你的 Pyramid 應用準備好了:

    (lsbaws) $ python webserver2.py pyramidapp:app

    WSGIServer: Serving HTTP on port 8888 …

    你告訴服務器載入一個來自python ‘pyramidapp’模塊的‘app’,然后做好準備接收請求并傳給你的 Pyramid 應用。這個應用只處理一個路徑: /hello 路徑。在瀏覽器中輸入地址 http://localhost:8888/hello,按下Enter,就看到結果:

    自己寫一個Web服務器

    你也可以用’curl‘在命令行中測試服務器:

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

    接著是 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 或者從 GitHub下載,然后運行服務器:

    (lsbaws) $ python webserver2.py flaskapp:app

    WSGIServer: Serving HTTP on port 8888 …

    在瀏覽器中輸入地址 http://localhost:8888/hello,按下Enter:

    自己寫一個Web服務器

    繼續,用’curl‘看看服務器返回 Flask 應用生成的消息:

    這個服務器能處理 Django 應用嗎?試試看!這會更復雜一點,我建議克隆整個repo,用djangoapp.py, 它是GitHub repository的一部分。這里給出代碼,只是添加Django ’helloworld‘工程到當前python路徑然后導入它的WSGI應用。

    import sys

    sys.path.insert(0, ‘./helloworld’)

    from helloworld import wsgi

    app = wsgi.application

    保存代碼為 djangoapp.py 然后在服務器上運行 Django 應用:

    (lsbaws) $ python webserver2.py djangoapp:app

    WSGIServer: Serving HTTP on port 8888 …

    輸入地址,按下Enter:

    自己寫一個Web服務器

    同樣的就像你以及試過的幾次,再用命令行試試看,確認這個 Django 應用也是可以處理你的請求:

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

    你試了嗎?你確定這個服務器和三個框架都能工作嗎?要是沒有,快去做吧。看文章很重要,但這個系列是重建,就是說你得身體力行。去試試,我會等你的,別擔心。你必須自己驗證,最好能自己寫所用的東西以確保它能達到預期。

    好了,你已經體驗了WSGI的強大:它讓你混合匹配web服務器和架構。WSGI為python web服務器和框架提供一個最小的接口。它非常簡單且容易應用到服務器和框架兩端。下面的代碼段是服務器和框架端的接口:

    自己寫一個Web服務器

    它這樣工作:

    • 這個框架提供一個可調用的’application’(WSGI規范沒有規定如何應實現)
    • 服務器從HTTP客戶端接收請求,并調用’application’。它把包含 WSGI/CGI 變量的字典‘environ’和‘start_response’ 調用作為參數傳給 ‘application’ 。
    • 框架/應用生成一個HTTP狀態和HTTP響應頭,將他們傳入‘start_response’ 讓服務器來存儲??蚣?應用同時返回響應體。
    • 服務器將狀態,響應頭和響應提結合成HTTP響應并且傳送到客戶端(這一步不是標準中的部分,但卻是合乎邏輯的下一步,為明了起見我加在這里)

    這里是界面的可視化表示:

    自己寫一個Web服務器

    到此為止,你看到了Pyramid, Flask, 和 Django Web 應用,看到了服務器端實現WSGI規范的代碼。你看到了沒用任何框架的WSGI應用代碼。

    問題是你在用這些框架寫web應用時是在一個更高的層級并不直接接觸WSGI,但我知道你也好奇框架端的WSGI接口,當然也因為你在看這篇文章。所以,我們來建一個極簡的WSGI web應用/框架,不用Pyramid, Flask, 或Django,在你的服務器上運行:

    自己寫一個Web服務器

    保存上面代碼為wsgiapp.py,或者在GitHub下載,想下面這樣運行:

    自己寫一個Web服務器

    輸入地址,按下Enter,你就看到結果:

    自己寫一個Web服務器

    回去看服務器傳了什么給客戶端 。這里是你用HTTP客戶端調用你的Pyramid應用時服務器生成的HTTP響應:

    自己寫一個Web服務器

    這個響應有些部分和第一篇看到的相似,但也有些新的東西。它有四個你之前沒看到過的HTTP頭:內容類型,內容長度,日期和服務器。這些頭飾一個web服務器響應通常應該有的。即便沒有一個是必須的,它沒的作用是發送HTTP請求/響應的附加信息。

    你對WSGI接口有了更多的了解,這里還有些信息關于這條HTTP響應是那部分產生的:

    自己寫一個Web服務器

    我還沒說過‘environ’字典的任何東西,它基本上就是必須包含由WSGI規范規定的明確WSGI和CGI參數的python字典。服務器解析請求后從請求中取出參數放入字典。這是字典中包含內容的樣子:

    自己寫一個Web服務器

    web框架用字典中的信息決定通過特點路徑的呈現,響應方式去,哪里去讀取響應體和哪里去寫入錯誤,如果有的話。

    你創建了自己的WSGI web服務器,你用不同框架寫了自己的web應用。你也創建了自己基本的web應用/框架。這是一個 heck 之旅。來回顧一下你的WSGI服務器對應用都要做些什么:

    • 首先,服務器啟動然后載入你的web框架/應用提供的‘application’調用
    • 然后,服務器讀取請求
    • 然后,服務器解析它
    • 然后,它根據請求數據創建一個‘environ’ 字典
    • 然后,它用‘environ’ 字典調用‘application’,‘start_response’ 做參數同時得到一個響應體
    • 然后, 通過‘start_response’ 用‘application’返回的數據和狀態及響應頭創建一個HTTP響應。

    最后,服務器將HTTP響應傳回到客戶端。

    自己寫一個Web服務器

    必須發明時我們學的最好——Piaget

    在第二篇你建了一個極簡的WSGI服務器,可以出來基本的HTTP GET請求。結束時我問了個問題,你怎么保證你的服務器能同時處理多個請求?在這篇文章中你會找到答案。所以,系好安全帶,換高檔位,你將會超高速行駛。準備好你的Linux,Mac OS X(或其他*nix系統)和python。這篇文章的所有代碼都在GitHub。

    首先讓我們回憶一個基本的web服務器是什么樣子,它需要對客戶端的請求做什么。在第一篇和第二篇中你建是一個的迭代服務器,一次處理一個客戶端請求。它不能接受新的連接直到處理完當前客戶端請求。一些客戶端可能會不高興,因為他們必須排隊等待,而一些忙碌的服務器這隊就太長了。

    自己寫一個Web服務器

    這是迭代服務器的代碼webserver3a.py:

    自己寫一個Web服務器
    自己寫一個Web服務器

    仔細看你的服務器一次只處理一個請求,稍微修改這個服務器在給客戶端發送響應加上一個60秒的延時。這個改變只有一行來告訴服務器進程休眠60秒。

    自己寫一個Web服務器

    這是可休眠服務器的代碼 webserver3b.py:

    自己寫一個Web服務器
    自己寫一個Web服務器

    啟動服務器:

    $ python webserver3b.py

    現在打開一個新的終端窗口然后運行curl命令。你應該會立即看到“Hello, World!”被打印在屏幕上:

    $ curl http://localhost:8888/hello

    Hello, World!

    不要等待打卡第二個終端運行同樣curl命令:

    $ curl http://localhost:8888/hello

    你要是在60秒內做完了,那第二個curl不會立即有任何顯示而只停在那里。服務器也不會打印出一個新請求的標準輸出。在我的Mac上是這個樣子的(在右下方高亮的窗口顯示了第二個curl命令掛起,等待連接被服務器接受):

    等待時間足夠長之后(多余60秒)你應該看到第一個curl終止,第二個curl的窗口打印出“Hello, World!”,然后掛起60秒,然后終止:

    自己寫一個Web服務器

    它的工作方式是這樣的,服務器處理完第一個curl客戶端請求后休眠60秒然后開始處理第二個請求。這些都是按順序一步步來,或者在這個例子中一個時刻,一個客戶端請求。

    我們討論一下客戶端和服務器之間的通信。要讓兩個程序通過網絡彼此通訊,他們需要用到socket。你在第一篇和第二篇都看到了socket,但socket是什么呢?

    自己寫一個Web服務器

    socket是一個通信終端的抽象,它允許你的程序通過描述文件與另一個程序通信。在這篇文章中我會談到Linux/Mac OS X上典型的TCP/IP socket一個重要的概念是TCP socket對。

    TCP連接的socket對是有4個值的tuple用來標識TCP連接的兩個端點:本地IP地址,本地端口,外部IP地址,外部端口。socket對唯一標識網絡上的每個TCP連接。這兩個成對的值標識各自端點,一個IP地址和一個端口號,通常被稱為一個socket。

    自己寫一個Web服務器

    tuple {10.10.10.2:49152, 12.12.12.3:8888} 是客戶端上一個唯一標識兩個TCP連接終端的socket, {12.12.12.3:8888, 10.10.10.2:49152} 是客戶端上一個唯一標識相同的兩個TCP連接終端的socket。IP地址12.12.12.3和端口8888在TCP連接中用來識別服務器端點(同樣適用于客戶端)。

    標準的服務器創建一個socket然后接受客戶端連接的流程如下圖所示:

    自己寫一個Web服務器

    服務器創建一個TCP/IP socket。用下面的python語句:

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

    服務器可能會設置一些socket選項(這是可選的,單絲你看到上面的代碼多次使用相同的地址,如果你想停止它那就馬上重啟服務器)。

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

    3、然后,服務器綁定地址。bind函數給socket分配一個本地地址。在TCP中,調用bind允許你指定端口號,IP地址,要么兩個要么就沒有。

    listen_socket.bind(SERVER_ADDRESS)

    接著服務器讓這個socket成為監聽socket

    listen_socket.listen(REQUEST_QUEUE_SIZE)

    listen方法只供服務器調用。它告訴內核應該接受給這個socket傳入的連接請求

    這些完成后,服務器開始逐個接受客戶端連接。當一個連接可用accept返回要連接的客戶端socket。然后服務器讀從客戶端socket取請求數據,打印出響應標準輸出然后給客戶端socket傳回消息。然后服務器關閉客戶端連接,準備接受一個新的客戶端連接。

    下圖就是在TCP/IP中客戶端與服務器通信需要做的:

    自己寫一個Web服務器

    這里有同樣的代碼用來連接客戶端和服務器,發出一個請求然后打印出響應:

    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)

    創建socket之后,客戶端需要連接服務器。這是通過connect調用來完成的:

    sock.connect((‘localhost’, 8888))

    客戶端只需提供服務器的遠程地址或是主機名和遠程端口號來連接。

    你可能已經注意到客戶端沒有調用bind和accept。其原因是客戶端不關心本地IP地址和端口號??蛻舳苏{用connect時內核中的TCP/IP socket會自動分配本地IP地址和端口號。本地端口被稱為臨時端口,一個短命的端口。

    自己寫一個Web服務器

    客戶端連接用以獲取已知服務的服務器端口成為已知端口(例如80是HTTP,2

    2是SSH)。打開python shell在本地主機開啟一個客戶端連接,看看內核給你的socket分配了哪個臨時端口(先啟動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)

    上面的例子內核分配給socket的臨時端口是60589.

    還有一些重要概念我需要在回答第二篇的問題前先做說明。你很快就會看到為什么這是非常重要的。這兩個概念是一個是進程,一個是文件描述符。

    什么事進程?進程是執行程序的實例。當服務器代碼開始執行,比如,它要載入內存執行程序就會調用一個進程。內核記錄一系列關于進程的信息——比如進程ID——用來追蹤它。當你運行webserver3a.py 或 webserver3b.py你只運行了一個進程。

    自己寫一個Web服務器

    在一個終端中運行webserver3b.py:

    $ python webserver3b.py

    在另一個終端中用ps命令獲取有關進程的信息:

    $ ps | grep webserver3b | grep -v grep

    7182 ttys003 0:00.04 python webserver3b.py

    ps命令表明你卻是只運行了一個python進程webserver3b。當一個進程產生內核就會給他分配進程ID,PID。在UNIX中每個用戶進程還有一個父進程,當然也有自己的ID叫做父進程ID,或者簡寫成PPID。

    我當你是在用默認BASH,那么啟動服務器一個進程被創建同時一個PID被設定,同時一個PPID在BASH中被設定。

    自己寫一個Web服務器

    你自己試試看它是怎么做的。再次打開python shell,它就產生了一個新進程,然后用ow.getpid和os.getppid這戀歌系統調用查看PID和PPID。接著在另一個終端窗口運行ps命令同時grep搜索這個PPID(我這里是3148).在下面的截屏中你看到一個關于我Moc OS X系統上BASH進程和python shell進程的父子關系:

    自己寫一個Web服務器

    另一個必須知道的的概念是文件描述符。那什么是文件描述符呢?是當一個進程打開現有的文件,創建一個新文件,或者當它創建一個新的socket時,內核返回給它的一個非負整數。你應該知道在UNIX中所有東西都是文件。內核通過文件描述符指向一個打開的文件。當你需要讀寫文件是就用文件描述符來識別。python給你跟高級別的對象來處理文件,你不需要直接用文件描述符來識別文件,但在底層,UNIX中文件和socket的識別是用他們的整數文件描述符。

    自己寫一個Web服務器

    UNIX shell默認分配文件描述符0給標準輸入進程,1是標準輸出,2是標準錯誤。

    自己寫一個Web服務器

    前面說到的,python給你跟高層級文件或類文件對象,你還是可以用fileno方法來得到相關聯文件的文件描述符?;氐絧ython shell看看怎么做到:

    >>> 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時,你通常會使用一個高層次的文件/ Socket對象,但也有可能,你需要直接使用文件描述符。這里給出一個例子,你用write系統調用給標準輸出寫入一個字符串,它將文件描述符作為一個參數:

    這是一個有趣的部分——你不會在感到驚訝,因為你已經知道在UNIX中所有的都是文件——你的socket也有一個與其關聯的文件描述符。繼續,我前面說到那樣創建一個socket你得到一個對象和一個非負整數,你總是可以直接通過fileno方法獲取文件描述符。

    還有一件事情:你有沒有注意到注意到在第二個迭代服務器webserver3b.py的例子中,服務器在60秒休眠中你還可以通過第二個curl命令連接到服務器。當然curl沒有立即有任何輸出,它掛起了,但是為什么服務器沒有在那時就接受連接,也沒有立即拒絕客戶端,而是允許它連接到服務器呢?答案是socket對象的listen方法和它的BACKLOG 參數,在代碼中是REQUEST_QUEUE_SIZE。BACKLOG 參數決定內核處理進來連接請求隊列的長度。服務器 webserver3b.py休眠時,第二個curl能夠連接到服務器是因為內核有足夠的空間給進來的連接請求。

    增加BACKLOG 參數不能讓你的服務器理解神奇到可以同時處理多個客戶端請求。要繁忙的服務器不必等待繼而接受一個新的連接,而是立即從消息隊列中抓取新的連接同時沒有延遲的開始一個客戶端響應進程,一個相當大的BACKLOG參數是非常重要的。

    你已經了解夠多了。來快速回顧一下你目前為止所學的(或者復習一下你的基礎)。

    自己寫一個Web服務器
    • 迭代服務器
    • 服務器socket創建過程(socket,bind,listen,accept)
    • 客戶端socket創建過程(socket,connect)
    • socket對
    • socket
    • 臨時端口和已知端口
    • 進程
    • 進程ID(PID),父進程ID(PPID),和父子關系
    • 文件描述符
    • 監聽socket的BSCKLOG參數的意義

    現在我準備回答第二篇的問題:你怎么保證你的服務器能同時處理多個請求?或者換個方式,如何編寫并發服務器?

    自己寫一個Web服務器

    在UNIX下最簡單的方法是用一個fork系統調用。

    自己寫一個Web服務器

    這是你新的兵法服務器的代碼[webserver3c.py](https://github.com/rspivak/lsbaws/blob/master/part3/webserver3c.py),它可以同時處理多個客戶端請求(跟在迭代服務器webserver3b.py一樣,每個子進程休眠60秒):

    自己寫一個Web服務器
    自己寫一個Web服務器

    在討論fork怎么工作前,你自己看看這個服務器卻是同時處理多個請求,而不像前連個。在命令行中用下面命令:

    $ python webserver3c.py

    接著運行連個curl命令,下載即便服務器子進程處理客戶端請求后休眠60秒,卻并不影響其他客戶端,因為他們是完全不同的獨立進程了。你可以運行你盡可能多的curl命令(你想多少就多少),每一個都會沒有明顯延遲立即打印出服務器響應“Hello, World” 。

    理解fork最重要的一點是你調用forl一次但是他返回兩次:一次是父進程,一次是子進程。你fork你個新的進程返回子進程的ID是0,返回父進程的是子進程的PID。

    自己寫一個Web服務器

    我還記得第一次看到并嘗試fork時是有多著迷。我正在看循序代碼突然一聲響:代碼復制了自己成為兩個同時運行的實例。我覺得這就是魔法,真的。

    父進程fork出一個新子進程,這個子進程得到一個父進程文件描述符:

    自己寫一個Web服務器

    你可能注意到上面代碼的父進程關閉了客戶端連接:

    else: # parent

    client_connection.close # close parent copy and loop over

    那么子進程怎么能繼續讀取客戶端socket數據,如果父進程已經關閉了和它的連接?答案就在上面的圖片中。內核根據文件描述符的值來決定是否關閉連接socket,只有其值為0才會關閉。服務器產生一個子進程,子進程拷貝父進程文件描述符,內核增加引用描述符的值。在一個父進程一個子進程的例子中,描述符引用值就是2,當父進程關閉連接socket,它只會把引用值減為1,不會小島讓內核關閉socket。子進程也關閉了父進程監聽socket的重復拷貝,是因為它不關心接受新的客戶端連接,而只在乎處理已連接客戶端的響應:

    listen_socket.close # close child copy

    我會在這篇文章的后面談到你不取消重復描述符會發生什么。

    如你從當前服務器代碼中看到的,父進程的唯一職責是接受客戶端連接,fork一個子進程去處理客戶端請求,然后繼續接受另一個客戶端請求,沒別的。父進程不對客戶端請求做處理——子進程來做。

    我們說兩個事件并發是什么意思呢?

    自己寫一個Web服務器

    說兩事件并發通常是指他們在同一時間發生。作為一個簡短的定義是好的,但是你應該記住嚴格的定義:

    如果你不能通過看這個程序來告訴你,這兩個事件是并發的。

    又到了回顧概念和理念的時間了:

    • 在UNIX下寫并發服務器最簡單的方法是用fork系統調用。
    • 一個進程fork出一個新進程,它就變成新進程的父進程
    • 調用fork后,父進程和子進程公用同樣的文件描述符
    • 內核用文件描述符應用值來決定關閉或打開文件/socket
    • 服務器父進程的角色:從客戶端接受新的連接,fork一個子進程去處理請求,繼續接受新的連接。

    我們來看看不取消父進程和子進程建重復描述符會發生什么。對個當前服務器代碼稍作修改,webserver3d.py:

    自己寫一個Web服務器
    自己寫一個Web服務器

    啟動服務器:

    $ python webserver3d.py

    用curl連接服務器:

    $ curl http://localhost:8888/hello

    Hello, World!

    curl打印出了并發服務器的響應但沒有終止然后保持掛起。發生了什么?服務器不再休眠60秒:它的子進程積積德處理了客戶端請求,關閉客戶端連接和退出,但curl仍然不終止。

    自己寫一個Web服務器

    為什么curl不終止?答案是重復的文件描述符。子進程關閉了客戶端連接,內核將socket引用值減為1。子進程退出,客戶端socket還不關閉是因為socket的引用值還不是0,結果就是終止包(在TCP/IP中叫FIN)沒有被發送到客戶端,客戶端就持續連接。還有一個問題,你一直運行服務器而不關閉重復的文件描述符,最終會用完可用的文件描述符。

    自己寫一個Web服務器

    用Control-C停止你的服務器,在shell通過內置命令ulimit檢查你服務器可用的默認資源:

    自己寫一個Web服務器

    從上面你可以看到,在我的Ubuntu上open files文件描述符(打開多少文件)的最大可用數值是1024.

    來看看不關閉重復描述符服務器怎樣用完可用文件描述符。在終端窗口設置open files描述符為256:

    $ ulimit -n 256

    在同一終端啟動服務器 webserver3d.py:

    $ python webserver3d.py

    用下面的客戶端client3.py來測試服務器。

    自己寫一個Web服務器
    自己寫一個Web服務器

    在一個新的終端窗口開啟client3.py然后告訴他創建300個與服務器的并發連接:

    $ python client3.py –max-clients=300

    很快你的服務器就會爆掉。這是我的異常報告截屏:

    自己寫一個Web服務器

    教訓是明顯的——服務器應該關閉重復描述符。但即使關閉重復描述符你也沒有走出困境,你這服務器還有一個問題,這個問題是僵尸!

    自己寫一個Web服務器

    真的,你的代碼創造了僵尸進程??聪略趺椿厥?,再次啟動服務器:

    $ python webserver3d.py

    在另一個終端運行下面的curl命令:

    $ curl http://localhost:8888/hello

    接著運行ps命令看看運行的python進程。下面是我在Ubuntu上的樣子:

    $ 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的進程是Z+,而且進程名是了嗎?這就是我們的僵尸進程。僵尸進程的問題是你不能殺死他們。

    自己寫一個Web服務器

    即使你用’$ kill -9’來殺僵尸進程,他們還會復活,你自己試試看。

    什么是僵尸進程而我們的服務器為什么會產生他們?僵尸進程是一個已經終止進程,但是他的父進程沒有等待并收到它的終止狀態。

    一個子進程先于它的父進程退出,內核將其轉為僵尸進程并存儲器父進程的一些信息用來以后恢復。通常存儲進程ID,終止狀態,進程使用的資源。所以僵尸進程是有用的,但你的服務器不處理好這些僵尸進程就會造成系統阻塞。看看吧,先停止運行的服務器,然后再新的終端窗口用ulimit命令設定你的最大用戶進程為400(確定open files是更大的數,就500吧):

    $ ulimit -u 400

    $ ulimit -n 500

    在剛運行’$ ulimit -u 400’ 命令的終端啟動服務器webserver3d.py:

    $ python webserver3d.py

    在新的終端窗口,啟動client3.py產生500個同時到服務器的連接:

    $ python client3.py –max-clients=500

    很快你瘋服務器就會出現在創建新的子進程時OSError: Resource temporarily unavailable異常,因為它已經達到了允許子進程數的上限。下面是我的異常截圖:

    自己寫一個Web服務器

    我會簡要說明服務器該怎樣對待僵尸進程問題。

    再回顧一下主要內容:

    • 你不關閉重復描述符,客戶端就不終止因為客戶端連接沒有關閉
    • 你不關閉重復描述符,長時間運行的服務器最終會用完可用文件描述符
    • 你fork的子進程退出了但是其父進程沒等待和回收它的終止狀態,那它就成了僵尸進程
    • 你不能殺死僵尸進程,你需要等待它

    那么你要做什么來對付僵尸進程?你要修改服務器代碼來等待僵尸進程回收他們的終止狀態。你可以用系統調用wait來修改服務器。不幸的是這太不理想,因為調用wait而沒有終止的子進程的話,wait調用會鎖住服務器,從而阻止服務器從處理新的客戶端連接請求。有別的辦法嗎?有,其中一個是將一個信號處理器和wait調用結合。

    自己寫一個Web服務器

    它的工作原理是,一個子進程退出,內核發出一個SIGCHLD信號。父進程可以建一個信號處理器異步接收SIGCHLD信號,然后它就等待并回收子進程終止狀態,從而防止留下僵尸進程。

    順便說下,異步事件意味著父進程不會提前知道該事件將要發生。

    修改服務器代碼,設置SIGCHLD事件處理器等待終止的子進程。代碼是webserver3e.py:

    自己寫一個Web服務器
    自己寫一個Web服務器

    啟動服務器:

    $ python webserver3e.py

    用curl給修改過的服務器發送請求:

    $ curl http://localhost:8888/hello

    看看服務器怎么樣:

    自己寫一個Web服務器

    發生了什么?因為錯誤EINTR調用accept失敗。

    自己寫一個Web服務器

    子進程退出出發SIGCHLD事件然后父進程在調用accept時被鎖住,父進程激活信號處理器完成工作后導致了系統調用accept中斷:

    自己寫一個Web服務器

    別擔心,這是個很好解決的問題。你需要的只是重啟系統調用accept。這是修改的服務器用 webserver3f.py 來解決那個問題:

    自己寫一個Web服務器
    自己寫一個Web服務器

    啟動webserver3f.py:

    $ python webserver3f.py

    用curl給服務器發送請求:

    $ curl http://localhost:8888/hello

    看到了吧?沒有EINTR異常了?,F在,確定沒有僵尸進程同時SIGCHLD事件處理器等待并處理子進程終止。運行ps命令不會在意python進程是Z+狀態(沒有進程)。太好了,沒有僵尸進程就安全了。

    • 如果你fork一個子進程卻沒有等待它,它會變僵尸進程
    • 用SIGCHLD事件處理器異步等待終止的子進程回收它的終止狀態
    • 用事件處理器時你要記住系統調用可能終止,您需要為此做好準備方案

    目前為止沒什么問題,對嗎?嗯,基本上是。在試試看webserver3f.py 但是不要用curl只發一個請求,用client3.py 發出128個同時連接:

    $ python client3.py –max-clients 128

    再次運行ps命令:

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

    看到了吧,天吶,僵尸進程又回來了!

    自己寫一個Web服務器

    這次是哪出錯了?當運行128個鏈接且連接成功,服務器子進程處理請求并推出基本在同一時間,造成了SIGCHLD信號的洪流傳向父進程。問題在于這些信號不排隊,你的服務器就漏掉了一些信號,留下幾個僵尸進程亂跑沒人管:

    自己寫一個Web服務器

    解決方法是設一個SIGCHLD事件處理器但用WNOHANG來代替系統調用waitpid來排一個隊,以確保所有終止進程都被處理。修改后代碼 webserver3g.py:

    自己寫一個Web服務器
    自己寫一個Web服務器
    自己寫一個Web服務器

    啟動服務器:

    $ python webserver3g.py

    用測試客戶端client3.py:

    $ python client3.py –max-clients 128

    現在確認有沒有僵尸進程。 好極了!生活是美好的:)

    自己寫一個Web服務器

    恭喜!這是一個相當漫長的旅程,但我希望你喜歡它。現在你有了簡單的并發服務器,這代碼可以作為在高曾次web服務器進一步的工作取的基礎。

    我把第二篇中WSGI服務器升級成并發服務器留給你當練習。你在這里可以找到修改的版本.但是只能在自己實現了之后看。你用完成它的所有信息,那就做吧: )

    下來時什么呢?就像Josh Billings說的

    要像一張郵票,堅持一件事情直到你到達目的地。

    從掌握的基楚開始,質疑你已經知道的,始終深入。

    自己寫一個Web服務器

    如果你僅僅學習方法,你將被被你的方法束縛。但是如果你學習原則,你可以設計自己的方法?!?Ralph Waldo Emerson

    下面是我在這篇文章引用素材的書單。他們會幫你擴大并深入我在文章中提到的知識。

    版權聲明:本文內容由互聯網用戶自發貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如發現本站有涉嫌抄襲侵權/違法違規的內容, 請發送郵件至 舉報,一經查實,本站將立刻刪除。

    發表評論

    登錄后才能評論
    国产精品区一区二区免费