500 lines or less學(xué)習(xí)筆記(三)——簡易 Web 服務(wù)器(web server)

Web 服務(wù)器是我們開發(fā)中經(jīng)常碰到的,本文對使用 Python 實現(xiàn)了一個簡單的 Web 服務(wù)器。原文基于 Python 2 實現(xiàn),我將其改為了 Python 3,并將其中使用的 Python 模塊改為了 Python3 中的 http.server。

原文作者

Greg Wilson 是 Software Carpentry(一個為科學(xué)家和工程師開設(shè)的計算技能速成班)的創(chuàng)始人。他在工業(yè)界和學(xué)術(shù)界工作了 30 年,著有幾本關(guān)于計算的書籍,包括 2008 年的 Jolt 獎得主《Beautiful Code》和《The Architecture of Open Source Applications》的前兩卷。Greg 于 1993 年獲得愛丁堡大學(xué)計算機(jī)科學(xué)博士學(xué)位。

引言

在過去的二十年里,網(wǎng)絡(luò)以無數(shù)的方式改變了社會,但它的核心卻幾乎沒有改變。大多數(shù)系統(tǒng)仍然遵循25年前 Tim Berners-Lee 制定的規(guī)則。特別是,大多數(shù) web 服務(wù)器仍然以相同的方式處理著相同類型的消息。

本文將探討它們是如何做到這一點的。同時,還將探討開發(fā)人員如何添加新功能而不重寫軟件系統(tǒng)。

背景

網(wǎng)絡(luò)上幾乎每個程序都運行在一系列稱為因特網(wǎng)協(xié)議(IP)的通信標(biāo)準(zhǔn)上。傳輸控制協(xié)議(TCP/IP)是這個家族中的一員,它使計算機(jī)之間的通信看起來就像讀寫文件一樣。

使用 IP 的程序通過套接字進(jìn)行通信。每個套接字都是點對點通信通道的一端,就像電話是電話通訊的一端一樣。套接字由標(biāo)識特定計算機(jī)的 IP 地址和該計算機(jī)上的端口號組成。IP 地址由四個8位數(shù)字組成,例如174.136.14.108;域名系統(tǒng)(DNS)將這些數(shù)字與符號名稱匹配,如 aosabook.org 網(wǎng)站,這對人類來說更容易記住。

端口號是 0 - 65535 范圍內(nèi)的一個數(shù)字,它唯一地標(biāo)識主機(jī)上的套接字。(如果 IP 地址比作公司的電話號碼,則端口號類則類似分機(jī)號。)端口 0 - 1023 保留給操作系統(tǒng)使用;其余端口任何人都可以使用。

超文本傳輸協(xié)議(HTTP)描述了程序通過 IP 交換數(shù)據(jù)的一種方式。HTTP 設(shè)計的很簡單:客戶機(jī)通過套接字連接發(fā)送一個請求,指定它想要什么,服務(wù)器發(fā)送一些數(shù)據(jù)作為響應(yīng)(如下圖)。數(shù)據(jù)可以從磁盤上的文件復(fù)制,也可以由程序動態(tài)生成,或者兩者混合。

http-cycle.png

關(guān)于 HTTP 請求最重要的是它只由文本組成:任何程序都可以對其創(chuàng)建或解析。但是,為了被正確解析,該文本必須包含下圖所示的部分。

http-request.png

HTTP 方法大部分是 GET(獲取信息)或 POST(提交表單數(shù)據(jù)或上傳文件)。URL 指定客戶端需要什么;它通常是指向磁盤上文件的路徑,例如 /research/experiments.html,但是(這是關(guān)鍵部分)完全由服務(wù)器決定如何處理它。HTTP 版本通常是 HTTP/1.0 或 HTTP/1.1;兩者之間的區(qū)別對我們來說并不重要。

HTTP 首部是鍵/值對,如下所示:

Accept: text/html
Accept-Language: en, fr
If-Modified-Since: 16-May-2005

與哈希表中的鍵不同,HTTP 首部中的鍵可以出現(xiàn)任意次數(shù)。這使請求更加的靈活,比如指定它愿意接受多種類型的內(nèi)容。

最后,請求的主體是與請求相關(guān)聯(lián)的任何數(shù)據(jù),在通過 web 表單提交數(shù)據(jù)、上傳文件等時使用。首部的末尾和正文開頭之間必須有一個空白行,以指示首部的結(jié)尾。

首部中,Content-Length 告訴服務(wù)器請求主體中預(yù)期讀取的字節(jié)數(shù)。

HTTP 響應(yīng)的格式和 HTTP 請求類似:

http-response.png

版本、首部和主體具有相同的格式和含義。狀態(tài)碼是一個數(shù)字,表示在處理請求時發(fā)生了什么:200表示“一切正?!?,404表示“未找到”,其他代碼也有各自的含義。狀態(tài)短語以易讀的形式重復(fù)該信息,如“OK”或“not found”。

在本部分我們主要了解關(guān)于 HTTP 的另外兩方面。

第一個是它是無狀態(tài)的:每個請求都是獨立處理的,服務(wù)器不記得兩個請求之間的任何內(nèi)容。如果應(yīng)用程序想要跟蹤用戶的身份等信息,它必須自己實現(xiàn)。

通常的實現(xiàn)方法是使用 cookie,cookie 是服務(wù)器發(fā)送給客戶端的一個短字符串,然后由客戶端返回到服務(wù)器。當(dāng)用戶執(zhí)行某個功能,需要在多個請求之間保存狀態(tài)時,服務(wù)器會創(chuàng)建一個新的 cookie,將其存儲在數(shù)據(jù)庫中,并將其發(fā)送到瀏覽器。每次瀏覽器返回 cookie 時,服務(wù)器都會使用它來查找有關(guān)用戶行為的信息。

第二方面是 URL 可以添加參數(shù)以提供更多的信息。例如,如果我們使用搜索引擎,我們必須指定我們的搜索詞。我們可以將這些添加到 URL 的路徑中,但是更加合適的方式是向 URL 添加參數(shù)。我們通過在 URL后面添加“?”和以“&”分隔的“key=value”對。例如,URL http://www.google.ca?q=Python 要求 Google 搜索與 Python 相關(guān)的頁面:鍵是字母“q”,值是“Python”。較長的查詢 http://www.google.ca/search?q=Python&client=Firefox 告訴 Google 我們正在使用 Firefox,諸如此類。我們可以傳遞我們想要的任何參數(shù),不過,使用哪些參數(shù)以及如何解釋這些參數(shù)完全取決于運行在 Web 站點上的應(yīng)用程序。

當(dāng)然,如果“?”和“&”是特殊字符,必須有一種轉(zhuǎn)義它們的方法,就像必須有一種方法將雙引號字符放入由雙引號分隔的字符串中一樣。URL 編碼標(biāo)準(zhǔn)使用“%”后跟2位代碼表示特殊字符,并將空格替換為“+”字符。因此,要在 Google 上搜索“grade = A+”(帶空格),我們要使用 URL http://www.google.ca/search?q=grade+%3D+A%2B。

打開套接字、構(gòu)造 HTTP 請求和解析響應(yīng)繁瑣而無趣,因此大多數(shù)人使用庫來完成大部分工作。Python 附帶了一個名為 urllib2 的庫(因為它是早期 urllib 庫的替代品),但是它暴漏了許多大多數(shù)人不想關(guān)心的管道。Requests 庫是 urllib2 更易于使用的替代選擇。下面是一個使用它從 AOSA book 站點下載頁面的示例:

import requests
response = requests.get('http://aosabook.org/en/500L/web-server/testpage.html')
print 'status code:', response.status_code
print 'content length:', response.headers['content-length']
print response.text
status code: 200
content length: 61
<html>
  <body>
    <p>Test page.</p>
  </body>
</html>

request.get 向服務(wù)器發(fā)送 HTTP GET 請求,返回一個包含響應(yīng)的對象。該對象的 status_code 是響應(yīng)的狀態(tài)碼;content_length 是響應(yīng)數(shù)據(jù)中的字節(jié)數(shù),text 是實際數(shù)據(jù)(在本例中,它是一個 HTML 頁面)。

你好,Web

我們現(xiàn)在準(zhǔn)備編寫第一個簡單的 Web 服務(wù)器?;舅悸泛芎唵危?/p>

  1. 等待用戶連接到我們的服務(wù)器并發(fā)送過來一個 HTTP 請求;
  2. 解析該請求;
  3. 弄清楚它在請求什么;
  4. 獲取數(shù)據(jù)(或動態(tài)生成);
  5. 將數(shù)據(jù)格式化為 HTML;
  6. 返回數(shù)據(jù)。

步驟1、2、6 在不同的應(yīng)用程序中是相同的,因此 Python 標(biāo)準(zhǔn)庫有一個名為 http.server 的模塊,它為我們完成這些操作。我們只需完成步驟3-5,這是下面的小程序中要做的:

import http.server

#-------------------------------------------------------------------------------

class RequestHandler(http.server.BaseHTTPRequestHandler):
    '''通過返回固定頁面來處理HTTP請求'''

    # 返回頁面
    Page = '''<html>\
    <body>
    <p>Hello, Web!</p>
    </body>
    </html>
    '''

    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(len(self.Page)))
        self.end_headers()
        self.wfile.write(bytes(self.Page, 'utf-8'))

#-------------------------------------------------------------------------------

if __name__ == '__main__':
    serverAddress = ('', 8080)
    server = http.server.HTTPServer(serverAddress, RequestHandler)
    server.serve_forever()

庫中的 BaseHTTPRequestHandler 類負(fù)責(zé)解析傳入的 HTTP 請求并判斷它包含什么方法。如果方法是 GET,則類將調(diào)用名為 do_GET 的方法。我們的類 RequestHandler 重寫這個方法來動態(tài)生成一個簡單的頁面:文本存儲在類級別的變量 Page 中,我們在發(fā)送給客戶端 200 的響應(yīng)碼、首部 Content-Type 字段告訴客戶端將我們的數(shù)據(jù)解釋為 HTML,以及頁面的長度后將其發(fā)送回客戶端。(end_headers 方法調(diào)用插入空行以分隔首部和頁面本身。)

然而 RequestHandler 并不是全部:我們?nèi)匀恍枰詈笕写a來真正啟動服務(wù)器。第一行使用元組定義服務(wù)器的地址:空字符串表示“在當(dāng)前主機(jī)上運行”,8080是端口。然后使用該地址和請求處理程序類的名稱作為參數(shù)創(chuàng)建 http.server.HTTPServer 的實例,然后請求它一直運行(這意味著一直運行到我們用 Ctrl-C 殺死它為止)。

如果我們從命令行運行此程序,它不會顯示任何內(nèi)容:

$ python server.py

如果我們在瀏覽器中訪問 http://localhost:8080 ,我們可以在瀏覽器中看到:

Hello, web!

同時在 shell 中看到:

127.0.0.1 - - [24/Feb/2014 10:26:28] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [24/Feb/2014 10:26:28] "GET /favicon.ico HTTP/1.1" 200 -

第一行很簡單:因為我們沒有請求特定的文件,所以我們的瀏覽器輸入“/”(服務(wù)器文件的根目錄)。出現(xiàn)第二行是因為瀏覽器會自動發(fā)送第二個對名為 /favicon.ico 圖像文件的請求,如果存在,它將在地址欄中顯示為圖標(biāo)。

展示值

讓我們修改 Web 服務(wù)器以展示 HTTP 請求中的值。(在調(diào)試時,我們會經(jīng)常這樣做,所以我們不妨進(jìn)行一些練習(xí)。)為了保持代碼的整潔,我們將把創(chuàng)建頁面與發(fā)送頁面分開:

class RequestHandler(http.server.BaseHTTPRequestHandler):

    # ...page template...

    def do_GET(self):
        page = self.create_page()
        self.send_page(page)

    def create_page(self):
        # ...fill in...

    def send_page(self, page):
        # ...fill in...

send_page 比之前的內(nèi)容多了很多:

    def send_page(self, page):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(len(self.Page)))
        self.end_headers()
        self.wfile.write(bytes(self.Page, 'utf-8'))

我們要顯示的頁面模板只是一個字符串,其中包含一個帶有一些格式占位符的HTML表格:

    Page = '''\
    <html>
    <body>
    <table>
    <tr>  <td>Header</td>         <td>Value</td>          </tr>
    <tr>  <td>Date and time</td>  <td>{date_time}</td>    </tr>
    <tr>  <td>Client host</td>    <td>{client_host}</td>  </tr>
    <tr>  <td>Client port</td>    <td>{client_port}s</td> </tr>
    <tr>  <td>Command</td>        <td>{command}</td>      </tr>
    <tr>  <td>Path</td>           <td>{path}</td>         </tr>
    </table>
    </body>
    </html>
'''

填充表格的方法如下:

    def create_page(self):
        values = {
            'date_time'   : self.date_time_string(),
            'client_host' : self.client_address[0],
            'client_port' : self.client_address[1],
            'command'     : self.command,
            'path'        : self.path
        }
        page = self.Page.format(**values)
        return page

程序的主體沒有改變:和以前一樣,它創(chuàng)建了一個 HTTPServer 類的實例,并將地址和這個請求處理程序作為參數(shù),然后永遠(yuǎn)為請求提供服務(wù)。如果我們運行它并從瀏覽器發(fā)送請求 http://localhost:8000/something.html,我們將得到:

  Date and time  Mon, 24 Feb 2014 17:17:12 GMT
  Client host    127.0.0.1
  Client port    54548
  Command        GET
  Path           /something.html

注意,我們沒有得到 404 錯誤,即使 something.html 頁面并不存在。這是因為 Web 服務(wù)器只是一個程序,當(dāng)它收到一個請求時,它可以做任何它想做的事情:返回前一個請求中提到的文件,提供一個隨機(jī)選擇的 Wikipedia 頁面,或者我們編程時讓它做的任何事情。

提供靜態(tài)頁面

顯而易見的下一步是從磁盤開始提供頁面,而不是動態(tài)生成頁面。我們將從重寫 do_GET 開始:

    def do_GET(self):
        try:
            # 檢查清楚需求
            full_path = os.getcwd() + self.path

            # 如果不存在
            if not os.path.exists(full_path):
                raise ServerException("'{0} not found".format(self.path))

            # 如果是文件
            elif os.path.isfile(full_path):
                self.handle_file(full_path)

            # 其它無法處理
            else:
                raise ServerException("unkown object '{0}'".format(self.path))
        # 處理異常
        except Exception as msg:
            self.handle_error(msg)

這個方法假設(shè)允許訪問 Web 服務(wù)器下的任何目錄( 通過 os.getcwd)。它將其與 URL 中提供的路徑相結(jié)合(庫會自動將其放入 self.path,并始終以前導(dǎo)“/”開頭),以獲取用戶所需文件的路徑。

如果路徑不存在或者它不是一個文件,則該方法通過引發(fā)并捕獲異常來報告錯誤。另一方面,如果路徑與文件匹配,則調(diào)用名為 handle_file 的輔助方法來讀取并返回內(nèi)容。此方法只讀取文件并使用現(xiàn)有的 send_content 將其發(fā)送回客戶端:

    def handle_file(self, full_path):
        try:
            with open(full_path, 'rb') as reader:
                content = reader.read()
            self.send_content(content)
        except IOError as msg:
            msg = "'{0}' cannot be read: {1}".format(self.path, msg)
            self.handle_error(msg)

請注意,我們以二進(jìn)制模式打開文件,即“rb”中的“b”,這樣 Python 就不會試圖通過改變看起來像 Windows 行尾的字節(jié)序列來“幫助”我們。還請注意,在實際應(yīng)用中,在提供服務(wù)時將整個文件讀入內(nèi)存不是一個好主意,因為文件可能是幾 GB 的視頻數(shù)據(jù)。處理這種情況不在本文范圍之內(nèi)。

為了完成這個類,我們需要編寫錯誤處理方法和錯誤報告頁面的模板:

    Error_Page = """\
        <html>
        <body>
        <h1>Error accessing {path}</h1>
        <p>{msg}</p>
        </body>
        </html>
        """

    def handle_error(self, msg):
        content = self.Error_Page.format(path=self.path, msg=msg).encode('utf-8')
        self.send_content(content)

這個程序當(dāng)我們不仔細(xì)看的時候感覺有效,但問題是,即使請求的頁面不存在,它也總是返回200的狀態(tài)碼。是的,在這種情況下發(fā)送回的頁面包含錯誤消息,但是由于瀏覽器并不認(rèn)識英語,因此不知道請求實際上失敗。為了明確這一點,我們需要修改 handle_errorsend_content 如下:

    # 處理錯誤對象
    def handle_error(self, msg):
        content = self.Error_Page.format(path=self.path, msg=msg).encode('utf-8')
        self.send_content(content, 404)

    # 發(fā)送的內(nèi)容
    def send_content(self, content, status=200):
        self.send_response(status)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(len(content)))
        self.end_headers()
        self.wfile.write(content)

請注意,當(dāng)找不到文件時,我們不會引發(fā) ServerException,而是生成一個錯誤頁。ServerException 意味著服務(wù)器代碼中有一個內(nèi)部錯誤,也就是說,我們出錯了。另一方面,handle_error 創(chuàng)建的錯誤頁面會在用戶出錯時出現(xiàn),例如,向我們發(fā)送了一個不存在的文件的URL。[1]

列表目錄

下一步,我們可以教 Web 服務(wù)器在 URL 中的路徑是目錄而不是文件時顯示目錄內(nèi)容的列表。我們甚至可以更進(jìn)一步,讓它在目錄中查找 index.html 文件來顯示,并且僅在該文件不存在時顯示目錄的內(nèi)容。

但是最好不要將這些規(guī)則構(gòu)建到 do_GET 中,因為生成的方法將與控制特殊行為的 if 語句混在了一起。正確的解決方案是退后一步來解決一般問題,即弄清楚如何處理 URL。 這是 do_GET 方法的重寫:

    def do_GET(self):
        try:
            self.full_path = os.getcwd() + self.path
            print(self.full_path)

            for case in self.Cases:
                if case.test(self):
                    case.act(self)
                    break
        # 處理異常
        except Exception as msg:
            self.handle_error(msg)

第一步是相同的:找出被請求的完整路徑。不過,在那之后,代碼看起來就完全不同了。這個版本不是一堆內(nèi)聯(lián)測試,而是循環(huán)遍歷存儲在列表中的一組 case。每個 case 都是一個有兩個方法的對象:test,它告訴我們是否能夠處理請求;act,它實際上對請求進(jìn)行操作。一旦找到正確的情況,我們就讓它處理請求并跳出循環(huán)。

這三個 case 類重現(xiàn)了我們前面服務(wù)器的行為:

class case_no_file(object):
    '''文件或目錄不存在'''
    def test(self, handler):
        return not os.path.exists(handler.full_path)

    def act(self, handler):
        raise ServerException("'{0}' not found".format(handler.path))

class case_existing_file(object):
    '''文件存在'''

    def test(self, handler):
        return os.path.isfile(handler.full_path)

    def act(self, handler):
        handler.handle_file(handler.full_path)
class case_always_fail(object):
    '''工作的基礎(chǔ)場景'''
    def test(self, handler):
        return True
    
    def act(self, handler):
        raise ServerException("Unkown object '{0}'".format(handler.path))

下面是我們?nèi)绾卧?RequestHandler 類的頂部構(gòu)造案例處理程序列表:

class RequestHandler(http.server.BaseHTTPRequestHandler):
    '''
    如果請求的路徑映射到一個文件,則使用該文件服務(wù)。
    如果出現(xiàn)任何錯誤,將構(gòu)造一個錯誤頁。
    '''

    Cases = [case_no_file(),
             case_existing_file(),
             case_always_fail()]

    ...everything else as before...

現(xiàn)在,從表面上看,這使我們的服務(wù)器變得更加復(fù)雜,而不是更少:文件從 74 行增加到 99 行,并且在沒有任何新功能的情況下增加了一個額外的層級。當(dāng)我們回到本章節(jié)開始的任務(wù)并嘗試教我們的服務(wù)器提供 index.html: 如果目錄下有這個頁面,則返回該頁面;如果沒有,則顯示該目錄的文件列表。前者的處理程序為:

class case_directory_index_file(object):
    '''處理包含 index.html 的目錄'''
    def index_path(self, handler):
        return os.path.join(handler.full_path, 'index.html')

    def test(self, handler):
        return os.path.isdir(handler.full_path) and \
            os.path.isfile(self.index_path(handler))

    def act(self, handler):
        handler.handle_file(self.index_path(handler))

這里,輔助方法 index_path 構(gòu)造指向 index.html 文件的路徑;將其放入案例處理程序可防止 RequestHandler 中出現(xiàn)混亂。test 方法檢查路徑是否是包含 index.html 的目錄,act 方法請求主請求處理程序為該頁面提供服務(wù)。

RequestHandler 的唯一更改是將一個 case_directory_index_file 對象添加到我們的“案例”列表中:

    Cases = [case_no_file(),
             case_existing_file(),
             case_directory_index_file(),
             case_always_fail()]

如果目錄不包含 index.html 頁呢?test 和上面未執(zhí)行策略性插入的 test 一樣,但是 act 方法呢?它應(yīng)該做什么?

class case_directory_no_index_file(object):
    '''處理沒有 index.html 頁面的目錄'''
    def index_path(self, handler):
        return os.path.join(handler.full_path, 'index.html')

    def test(self, handler):
        return os.path.isdir(handler.full_path) and \
            not os.path.isfile(self.index_path(handler))
    
    def act(self, handler):
        ???

看來我們陷入了困境。從邏輯上講,act 方法應(yīng)該創(chuàng)建并返回目錄列表,但是我們現(xiàn)有的代碼不允許這樣做:RequestHandler.do_GET 調(diào)用 act,但不處理它的返回值?,F(xiàn)在,讓我們向 RequestHandler 添加一個方法來生成一個目錄列表,并從case handler的 act 中調(diào)用它:

class case_directory_no_index_file(object):
    '''處理沒有 index.html 頁面的目錄'''

    # ...index_path and test as above...

    def act(self, handler):
        handler.list_dir(handler.full_path)


class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    # ...all the other code...

    # How to display a directory listing.
    Listing_Page = '''\
        <html>
        <body>
        <ul>
        {0}
        </ul>
        </body>
        </html>
        '''

    def list_dir(self, full_path):
        try:
            entries = os.listdir(full_path)
            bullets = ['<li>{0}</li>'.format(e) for e in  entries if not e.startswith('.')]
            page = self.Listing_Page.format('\n'.join(bullets)).encode('utf-8')
            self.send_content(page)
        except OSError as msg:
            msg = "'{0}' cannot be listed: {1}".format(self.path, msg)
            self.handle_error(msg)

CGI 協(xié)議

當(dāng)然,大多數(shù)人不會為了添加新功能而編輯 Web 服務(wù)器的源代碼。為了避免它們不得不這樣做,服務(wù)器一直支持一種稱為公共網(wǎng)關(guān)接口(CGI)的機(jī)制,該機(jī)制為 Web 服務(wù)器運行外部程序以滿足請求提供了一種標(biāo)準(zhǔn)方法。

例如,我們想在服務(wù)器上顯示一個 HTML 頁面。我們只需幾行代碼就可以在獨立程序中執(zhí)行此操作:

from datetime import datetime
print '''\
<html>
<body>
<p>Generated {0}</p>
</body>
</html>'''.format(datetime.now())

為了讓 Web 服務(wù)器為我們運行此程序,我們添加了以下案例處理程序:

class case_cgi_file(object):
    '''可以運行的文件'''

    def test(self, handler):
        return os.path.isfile(handler.full_path) and \
            handler.full_path.endswith('.py')

    def act(self, handler):
        handler.run_cgi(handler.full_path)

test 方法很簡單:文件路徑是否以 .py 結(jié)尾?如果是這樣,RequestHandler 將運行該程序。

    def run_cgi(self, full_path):
        cmd = "python " + full_path
        child_stdout = os.popen(cmd)
        data = child_stdout.read()
        child_stdout.close()
        self.send_content(data.encode('utf-8'))

這是非常不安全的:如果有人知道我們服務(wù)器上 Python 文件的路徑,我們就讓他們運行它,而不必?fù)?dān)心它可以訪問哪些數(shù)據(jù),它是否可能包含一個無限循環(huán)或其他東西。

拋開這些,核心思想很簡單:

  1. 在子進(jìn)程中運行程序。
  2. 捕獲子進(jìn)程發(fā)送到標(biāo)準(zhǔn)輸出的任何內(nèi)容。
  3. 把它發(fā)送回提出請求的客戶機(jī)。

完整的 CGI 協(xié)議比這個更豐富,特別是它允許 URL 中帶有參數(shù),服務(wù)器將這些參數(shù)傳遞給正在運行的程序,但是這些細(xì)節(jié)不會影響系統(tǒng)的整體架構(gòu)。

這又一次變得相當(dāng)糾結(jié)。RequestHandler最初有一個方法 handle_file,用于處理內(nèi)容。我們現(xiàn)在添加了兩個特殊情況,分別是list_dirrun_cgi。這三種方法并不真正屬于它們所處的位置,因為它們主要被其他人使用。

解決方法很簡單:為我們所有的案例處理程序創(chuàng)建一個父類,如果(并且僅當(dāng))其他方法被兩個或多個處理程序共享時,將它們移動到該類中。完成后,RequestHandler 類如下所示:

class RequestHandler(http.server.BaseHTTPRequestHandler):
    '''
    如果請求的路徑映射到一個文件,則使用該文件服務(wù)。
    如果出現(xiàn)任何錯誤,將構(gòu)造一個錯誤頁。
    '''

    Cases = [case_no_file(),
        case_cgi_file(),
        case_existing_file(),
        case_directory_index_file(),
        case_directory_no_index_file(),
        case_always_fail()]

    # 展示錯誤的頁面
    Error_Page = """\
        <html>
        <body>
        <h1>Error accessing {path}</h1>
        <p>{msg}</p>
        </body>
        </html>
    """

    # 分類處理請求
    def do_GET(self):
        try:
            self.full_path = os.getcwd() + self.path

            for case in self.Cases:
                if case.test(self):
                    case.act(self)
                    break
        # 處理異常
        except Exception as msg:
            self.handle_error(msg)

    # 處理錯誤對象
    def handle_error(self, msg):
        content = self.Error_Page.format(path=self.path, msg=msg).encode('utf-8')
        self.send_content(content, 404)

    # 發(fā)送的內(nèi)容
    def send_content(self, content, status=200):
        self.send_response(status)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(len(content)))
        self.end_headers()
        self.wfile.write(content)

我們的案例處理程序的父類是:

class base_case(object):
    '''case handler 的父類'''

    def handle_file(self, handler, full_path):
        try:
            with open(full_path, 'rb') as reader:
                content = reader.read()
            handler.send_content(content)
        except IOError as msg:
            msg = "'{0}' cannot be read: {1}".format(self.path, msg)
            handler.handle_error(msg)

    def index_path(self, handler):
        return os.path.join(handler.full_path, 'index.html')

    def test(self, handler):
        assert False, "Not implemented."

    def act(self, handler):
        assert False, 'Not implemented.'

現(xiàn)有文件的處理程序(只是隨機(jī)選取一個示例)是:

class case_existing_file(base_case):
    '''文件存在'''

    def test(self, handler):
        return os.path.isfile(handler.full_path)

    def act(self, handler):
        self.handle_file(handler, handler.full_path)

總結(jié)

原始代碼與重構(gòu)版本之間的差異反映了兩個重要的思想。第一種方法是將類視為相關(guān)服務(wù)的集合。RequestHandlerbase_case 不做決定或采取行動;它們提供了其他類可以用來做這些事情的工具。

第二個是可擴(kuò)展性:人們可以通過編寫一個外部 CGI 程序或添加一個 case handler 類來向我們的 Web 服務(wù)器添加新功能。后者確實需要對 RequestHandler 進(jìn)行一行更改(在Cases列表中插入case處理程序),但是我們可以通過讓 Web 服務(wù)器讀取配置文件并從中加載處理程序類來消除這種情況。在這兩種情況下,它們都可以忽略最低級的細(xì)節(jié),就像 BaseHTTPRequestHandler 類的作者允許我們忽略處理套接字連接和解析 HTTP 請求的細(xì)節(jié)一樣。

這些想法通常是有用的;看看你是否能找到在你自己的項目中使用它們的方法。


  1. 在本文中,我們將多次使用 handle_error,包括一些狀態(tài)碼 404 不合適的情況。當(dāng)你繼續(xù)閱讀時,請嘗試考慮如何擴(kuò)展此程序,以便在每種情況下都可以輕松地提供響應(yīng)狀態(tài)碼。 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容