自己動手開發網路伺服器(一)

程式設計派發表於2015-12-25

這個譯文共分為三個部分,內容比較長,介紹瞭如何利用 Python 語言從頭開發一個網路伺服器,其中涉及瞭如何讓伺服器同時支援 Flask 、 Pyramid 等多種框架,以及讓伺服器同時處理多個客戶端請求等問題。我覺得對於理解網路伺服器的實現有較大的幫助。譯文如果有什麼不對的地方,麻煩大家指正。

有一天,一位女士散步時經過一個工地,看見有三個工人在幹活。她問第一個人,“你在做什麼?”第一個人有點不高興,吼道“難道你看不出來我在砌磚嗎?”女士對這個答案並不滿意,接著問第二個人他在做什麼。第二個人回答道,“我正在建造一堵磚牆。”然後,他轉向第一個人,說道:“嘿,你砌的磚已經超過牆高了。你得把最後一塊磚拿下來。”女士對這個答案還是不滿意,她接著問第三個人他在做什麼。第三個人抬頭看著天空,對她說:“我在建造這個世界上有史以來最大的教堂”。就在他望著天空出神的時候,另外兩個人已經開始爭吵多出的那塊磚。他慢慢轉向前兩個人,說道:“兄弟們,別管那塊磚了。這是一堵內牆,之後還會被刷上石灰的,沒人會注意到這塊磚。接著砌下層吧。”

這個故事的寓意在於,當你掌握了整個系統的設計,明白不同的元件是以何種方式組合在一起的(磚塊,牆,教堂)時候,你就能夠更快地發現並解決問題(多出的磚塊)。

但是,這個故事與從頭開發一個網路伺服器有什麼關係呢?

在我看來,要成為一名更優秀的程式設計師,你必須更好地理解自己日常使用的軟體系統,而這就包括了程式語言、編譯器、直譯器、資料庫與作業系統、網路伺服器和網路開發框架。而要想更好、更深刻地理解這些系統,你必須從頭重新開發這些系統,一步一個腳印地重來一遍。

孔子曰:不聞不若聞之,聞之不若見之,見之不若知之,知之不若行之。

不聞不若聞之

聽別人說怎麼做某事

聞之不若見之

看別人怎麼做某事

見之不若知之,知之不若行之。

自己親自做某事

譯者注:上面原作者所引用的那段話在國外的翻譯是:I hear and I forget, I see and I remember, I do and I understand。外國人普遍認為出自孔子,但在查詢這句英文的出處時,查到有篇博文稱這句話的中文實際出自荀子的《儒效篇》,經查確實如此。

我希望你讀到這裡的時候,已經認可了通過重新開發不同軟體系統來學習其原理這種方式。

《自己動手開發網路伺服器》會分為三個部分,將介紹如何從頭開發一個簡易網路伺服器。我們這就開始吧。

首先,到底什麼是網路伺服器?

HTTP請求/響應

簡而言之,它是在物理伺服器上搭建的一個網路連線伺服器(networking server),永久地等待客戶端傳送請求。當伺服器收到請求之後,它會生成響應並將其返回至客戶端。客戶端與伺服器之間的通訊,是以HTTP協議進行的。客戶端可以是瀏覽器,也可以是任何支援HTTP協議的軟體。

那麼,網路伺服器的簡單實現形式會是怎樣的呢?下面是我對此的理解。示例程式碼使用Python語言實現,不過即使你不懂Python語言,你應該也可以從程式碼和下面的解釋中理解相關的概念:

:::python
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 'Serving HTTP on port %s ...' % PORT
while True:
    client_connection, client_address = listen_socket.accept()
    request = client_connection.recv(1024)
    print request

    http_response = """\
    HTTP/1.1 200 OK

    Hello, World!
    """

    client_connection.sendall(http_response)
    client_connection.close()

將上面的程式碼儲存為webserver1.py,或者直接從我的Github倉庫下載,然後通過命令列執行該檔案:

$ python webserver1.py
Serving HTTP on port 8888 …

接下來,在瀏覽器的位址列輸入這個連結:http://localhost:8888/hello,然後按下Enter鍵,你就會看見神奇的一幕。在瀏覽器中,應該會出現“Hello, World!”這句話:

瀏覽器返回“Hello World""

是不是很神奇?接下來,我們來分析背後的實現原理。

首先,我們來看你所輸入的網路地址。它的名字叫URL(Uniform Resource Locator,統一資源定位符),其基本結構如下:

URL的基本結構

通過URL,你告訴了瀏覽器它所需要發現並連線的網路伺服器地址,以及獲取伺服器上的頁面路徑。不過在瀏覽器傳送HTTP請求之前,它首先要與目標網路伺服器建立TCP連線。然後,瀏覽器再通過TCP連線傳送HTTP請求至伺服器,並等待伺服器返回HTTP響應。當瀏覽器收到響應的時候,就會在頁面上顯示響應的內容,而在上面的例子中,瀏覽器顯示的就是“Hello, World!”這句話。

那麼,在客戶端傳送請求、伺服器返回響應之前,二者究竟是如何建立起TCP連線的呢?要建立起TCP連線,伺服器和客戶端都使用了所謂的套接字(socket)。接下來,我們不直接使用瀏覽器,而是在命令列使用telnet手動模擬瀏覽器。

在執行網路伺服器的同一臺電腦商,通過命令列開啟一次telnet會話,將需要連線的主機設定為localhost,主機的連線埠設定為8888,然後按Enter鍵:

$ telnet localhost 8888
Trying 127.0.0.1 …
Connected to localhost.

完成這些操作之後,你其實已經與本地執行的網路伺服器建立了TCP連線,隨時可以傳送和接收HTTP資訊。在下面這張圖片裡,展示的是伺服器接受新TCP連線所需要完成的標準流程。

伺服器接受TCP連線的標準流程

在上面那個telnet會話中,我們輸入GET /hello HTTP/1.1,然後按下回車:

$ 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請求的基本結構:

HTTP請求的基本結構

HTTP請求行包括了HTTP方法(這裡使用的是GET方法,因為我們希望從伺服器獲取內容),伺服器頁面路徑(/hello)以及HTTP協議的版本。

為了儘量簡化,我們目前實現的網路伺服器並不會解析上面的請求,你完全可以輸入一些沒有任何意義的程式碼,也一樣可以收到"Hello, World!"響應。

在你輸入請求程式碼並按下Enter鍵之後,客戶端就將該請求傳送至伺服器了,伺服器則會解析你傳送的請求,並返回相應的HTTP響應。

下面這張圖顯示的是伺服器返回至客戶端的HTTP響應詳情:

HTTP響應解析

我們來分析一下。響應中包含了狀態行HTTP/1.1 200 OK,之後是必須的空行,然後是HTTP響應的正文。

響應的狀態行HTTP/1.1 200 OK中,包含了HTTP版本、HTTP狀態碼以及與狀態碼相對應的原因短語(Reason Phrase)。瀏覽器收到響應之後,會顯示響應的正文,這就是為什麼你會在瀏覽器中看到“Hello, World!”這句話。

這就是網路伺服器基本的工作原理了。簡單回顧一下:網路伺服器首先建立一個偵聽套接字(listening socket),並開啟一個永續迴圈接收新連線;客戶端啟動一個與伺服器的TCP連線,成功建立連線之後,向伺服器傳送HTTP請求,之後伺服器返回HTTP響應。要建立TCP連線,客戶端和伺服器都使用了套接字。

現在,你已經擁有了一個基本可用的簡易網路伺服器,你可以使用瀏覽器或其他HTTP客戶端進行測試。正如上文所展示的,通過telnet命令並手動輸入HTTP請求,你自己也可以成為一個HTTP客戶端。

下面給大家佈置一道思考題:如何在不對伺服器程式碼作任何修改的情況下,通過該伺服器執行Djando應用、Flask應用和Pyramid應用,同時滿足這些不同網路框架的要求?

答案將在《自己動手開發網路伺服器》系列文章的第二部分揭曉。

相關文章