Greg Wilson是Software Carpentry(為科學(xué)家和工程師提供在計(jì)算技能方面的速成課程)的創(chuàng)始人。他已經(jīng)在學(xué)術(shù)界和工業(yè)界工作了30年,是幾本計(jì)算方面的書,包括獲得2008年jolt獎(jiǎng)的《代碼之美》和《開源應(yīng)用程序體系結(jié)構(gòu)》的前兩卷的作者或者編輯。Greg于1993在愛丁堡大學(xué)獲得了計(jì)算機(jī)科學(xué)博士學(xué)位。 介紹 在過去的二十年里,網(wǎng)絡(luò)已經(jīng)改變了社會(huì)的方方面面,但是它的核心變化很少。大多數(shù)系統(tǒng)仍然遵守Tim Berners-Lee在25年前提出的規(guī)則。特別是,大多數(shù)web服務(wù)器仍然按照以前同樣的方式處理同種類的消息。 本章將探討他們?nèi)绾巫龅竭@一點(diǎn)。與此同時(shí),它將探索開發(fā)人員如何創(chuàng)建不需要重新寫入來添加新特性的軟件系統(tǒng)。 背景 幾乎網(wǎng)絡(luò)上的每一個(gè)程序都在以互聯(lián)網(wǎng)協(xié)議(IP)為通信標(biāo)準(zhǔn)運(yùn)行。我們關(guān)心的其中一個(gè)通信標(biāo)準(zhǔn)是傳輸控制協(xié)議(TCP/ IP),它使得計(jì)算機(jī)之間的通信看起來像讀寫文件。 使用IP網(wǎng)絡(luò)協(xié)議的程序通過sockets(套接字)進(jìn)行通信。每個(gè)socket(套接字)是一個(gè)點(diǎn)對(duì)點(diǎn)通信信道的一端,就像一個(gè)電話機(jī)是一個(gè)電話呼叫的一端。一個(gè)socket(套接字)由一個(gè)定義了特定機(jī)器和其端口號(hào)的IP地址構(gòu)成。 IP地址由4個(gè)8位的數(shù)字構(gòu)成,例如174.136.14.108(10100100.10001000.00001110.01101100);域名系統(tǒng)(DNS)把這些數(shù)字匹配給像這樣便于人們記憶的符號(hào)名。 端口號(hào)是0-65535范圍內(nèi)的數(shù)字,定義了主機(jī)上的唯一的socket(套接字)。 (如果IP地址比作一個(gè)公司的電話號(hào)碼,那么端口號(hào)就像是分機(jī)。)端口0-1023被保留用于操作系統(tǒng)的使用;任何人都可以使用剩余的端口。 超文本傳輸協(xié)議(HTTP)描述了一種程序通過IP交換數(shù)據(jù)的方法。 HTTP很簡(jiǎn)單:客戶端發(fā)送一個(gè)請(qǐng)求,指明它想通過socket(套接字)連接獲得的內(nèi)容,然后服務(wù)器發(fā)送一些數(shù)據(jù)作為響應(yīng)(如圖22.1)。數(shù)據(jù)可以從磁盤上的文件復(fù)制,或由程序動(dòng)態(tài)生成,或兩者的結(jié)合。
HTTP頭文件是“鍵/值”對(duì),如下面三行所示: Accept: text/html Accept-Language: en, fr If-Modified-Since: 16-May-2005 不像在哈希表中的鍵,鍵可能會(huì)在HTTP頭文件中出現(xiàn)任意次數(shù)。這允許request指定它想要接受的多種類型的內(nèi)容。 最后,請(qǐng)求的主體是任何與請(qǐng)求相關(guān)以外的數(shù)據(jù)。這用于通過網(wǎng)頁表單提交數(shù)據(jù),上傳文件等等的時(shí)候。在最后一個(gè)頭和主體開頭之間必須有一個(gè)空白行表示頭文件的結(jié)束。 一個(gè)叫Content-Length的頭文件,在請(qǐng)求主體中告訴服務(wù)器請(qǐng)求讀取多少字節(jié)。 HTTP響應(yīng)格式類似于HTTP請(qǐng)求(如圖22.2):
作為本章的目的,我們只需要了解HTTP的兩點(diǎn)。 首先,它是無狀態(tài)的:每個(gè)請(qǐng)求是自己處理自己,服務(wù)器不記得一個(gè)請(qǐng)求與下一個(gè)請(qǐng)求之間的任何事情。如果應(yīng)用程序想要跟蹤什么如一個(gè)用戶的id,就必須自己跟蹤。 追蹤常用的方法是使用cookie,它是一個(gè)從服務(wù)器端發(fā)送到客戶端然后客戶端稍后再返回到服務(wù)器端的短字符串。當(dāng)用戶執(zhí)行一些需要保存跨越多個(gè)請(qǐng)求的狀態(tài)的功能時(shí),服務(wù)器會(huì)創(chuàng)建一個(gè)新的cookie,并存儲(chǔ)在數(shù)據(jù)庫中,并且將其發(fā)送到她的瀏覽器。每次她的瀏覽器將cookie發(fā)送回去,服務(wù)器會(huì)用它來查看用戶正在做什么。 我們需要了解HTTP的第二件事是,一個(gè)URL可以用參數(shù)來補(bǔ)充,以提供更多的信息。舉個(gè)例子,如果我們使用搜索引擎,我們不得不指定我們的搜索詞是什么。我們可以把這些添加到URL路徑中,但我們應(yīng)該做的是把參數(shù)添加到URL。我們通過添加 “?”做到這點(diǎn),后面跟著以'&'分隔的“key=value”對(duì)。例如,URL http://www.?q=Python要求谷歌來搜索Python相關(guān)的網(wǎng)頁:key值是字母“q”,value值為“Python。較長(zhǎng)的查詢http://www./search?q=Python&client=Firefox告訴Google我們正在使用Firefox,等等。我們可以傳遞任何我們想要的傳遞的參數(shù),但同樣,它是由在網(wǎng)站上運(yùn)行的應(yīng)用來決定要注意哪些參數(shù),以及如何解釋它們。 當(dāng)然,如果 '?'和'&'是特殊字符,必須有方法避開他們,就是必須有個(gè)方法來把一個(gè)雙引號(hào)字符放進(jìn)用雙引號(hào)限定的字符串內(nèi)。 URL編碼標(biāo)準(zhǔn)用%后面跟著2位的代碼代表特殊字符,并以'+'字符代替空格。因此,搜索Google“grade= A+”(有空格),我們應(yīng)該使用URL http://www./search?q=grade+%3D+A%2B。 打開sockets,構(gòu)建HTTP請(qǐng)求,解析響應(yīng)是乏味的,所以大多數(shù)人使用庫來完成大部分的工作。 Python提供了這樣一個(gè)稱作urllib2的庫(因?yàn)樗且粋€(gè)較早版本的庫urllib的升級(jí)版),但它暴露了很多大多數(shù)人永遠(yuǎn)不想關(guān)注的底層編碼。Requests庫比urllib2更容易使用。下面是一個(gè)使用它從AOSA圖書網(wǎng)站下載頁面的例子:
hello,web 現(xiàn)在,我們準(zhǔn)備寫我們的第一個(gè)簡(jiǎn)單的Web服務(wù)器?;舅枷敕浅:?jiǎn)單: 1.等待有人來連接我們的服務(wù)器,發(fā)送HTTP請(qǐng)求; 2.解析請(qǐng)求; 3.找出它的要求; 4.獲取數(shù)據(jù)(或動(dòng)態(tài)生成它); 5.把數(shù)據(jù)格式化為HTML; 6.回發(fā)。 從一個(gè)應(yīng)用程序到另一個(gè),步驟1,2和6是相同的,所以Python標(biāo)準(zhǔn)庫有一個(gè)名為BaseHTTPServer的模塊為我們做這些步驟。我們只需要關(guān)注步驟3-5,這是我們?cè)谙旅娴男〕绦蚶镒龅模?/p>
但RequestHandler并不是故事的全部:我們還需要最后三行來使服務(wù)器開始運(yùn)行。其中第一行定義服務(wù)器的地址為一個(gè)元組:空字符串意味著“在當(dāng)前計(jì)算機(jī)上運(yùn)行”,8080是端口號(hào)。然后,我們創(chuàng)建一個(gè)BaseHTTPServer.HTTPServer的實(shí)例,實(shí)例中含有服務(wù)器地址和我們的請(qǐng)求處理類作為參數(shù)的名稱,然后要求它永久運(yùn)行(這實(shí)際上意味著,直到我們使用Control-C殺死它才停止運(yùn)行)。 如果我們?cè)诿钚兄羞\(yùn)行這個(gè)程序,它不顯示任何內(nèi)容: $ python server.py 然后如果我們用我們的瀏覽器訪問http://localhost:8080,那么,在瀏覽器中我們得到這樣的內(nèi)容: Hello, web! 在我們的shell中是這樣的內(nèi)容: 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 - 第一行是直觀的:因?yàn)槲覀儧]有要求特定文件,我們的瀏覽器就要求'/'(服務(wù)器提供所有的根目錄)。出現(xiàn)第二行是因?yàn)槲覀兊臑g覽器會(huì)自動(dòng)發(fā)送第二個(gè)請(qǐng)求來請(qǐng)求一個(gè)名為/favicon.ico的圖像文件,如果圖像文件存在,它會(huì)在地址欄顯示為一個(gè)圖標(biāo)。 顯示數(shù)值 讓我們修改我們的Web服務(wù)器來顯示一些包含在HTTP請(qǐng)求中的值。 (在調(diào)試的時(shí)候,我們會(huì)相當(dāng)頻繁的做這個(gè),所以我們不妨做一些練習(xí))。為了保持我們的代碼簡(jiǎn)潔,我們將把創(chuàng)建頁面和發(fā)送頁面分開:
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 請(qǐng)注意,我們沒有收到一個(gè)404錯(cuò)誤,即使something.html頁面文件在磁盤上不存在。這是因?yàn)閃eb服務(wù)器只是一個(gè)程序,當(dāng)它獲得一個(gè)請(qǐng)求時(shí),它可以做任意想做的事:發(fā)回在先前的請(qǐng)求里被命名的文件、提供隨機(jī)選擇的一個(gè)維基百科頁面,或我們編寫的任意的東西。 提供靜態(tài)頁面服務(wù) 明顯的,下一步是從磁盤啟動(dòng)服務(wù)頁面,而不是動(dòng)態(tài)生成。我們會(huì)通過改寫do_GET來開始:
如果路徑不存在,或者如果它不是一個(gè)文件,該方法通過拋出和捕獲異常報(bào)告錯(cuò)誤。如果路徑匹配一個(gè)文件,在另一方面,它調(diào)用一個(gè)幫助者方法命名為handle_file來讀取和返回內(nèi)容。這種方法只會(huì)讀取文件,并使用我們現(xiàn)有的send_content將其發(fā)送回客戶端:
為了完成這個(gè)類,我們需要編寫錯(cuò)誤處理方法和錯(cuò)誤報(bào)告頁面模板:
列出目錄 作為我們的下一步,當(dāng)URL中的路徑是一個(gè)目錄而不是文件時(shí),我們可以教Web服務(wù)器顯示目錄列表。我們甚至可以更進(jìn)一步,讓它在那個(gè)目錄中以index.html文件顯示,并且文件不存在時(shí),只顯示目錄內(nèi)容的列表。 但是,把這些規(guī)則寫進(jìn)do_GET將是一個(gè)錯(cuò)誤,因?yàn)榉椒ㄗ罱K將會(huì)成為長(zhǎng)長(zhǎng)一大團(tuán)控制特定行為的if聲明。正確的解決辦法是退后一步,解決一般性問題,指出如何處理URL。這里是重寫的do_GET方法:
這三個(gè)case類復(fù)制我們之前服務(wù)器的行為:
唯一需要RequestHandler改變的是把一個(gè)case_directory_index_file對(duì)象添加到我們的Cases列表中:
當(dāng)然,大多數(shù)人都不會(huì)想要編輯他們的網(wǎng)絡(luò)服務(wù)器的源代碼來增加新的功能。為了從這種不得不這么做的泥沼中解脫出來,服務(wù)器一直支持一個(gè)叫做通用網(wǎng)關(guān)接口(CGI)的機(jī)制,這為Web服務(wù)器為了滿足請(qǐng)求去運(yùn)行外部程序提供了一種標(biāo)準(zhǔn)方法。 例如,假設(shè)我們希望服務(wù)器能在HTML頁面中顯示本地時(shí)間。我們?cè)谥挥袔仔写a的獨(dú)立的程序中就可以做到:
先不管那個(gè),核心的思想很簡(jiǎn)單: 1.在子進(jìn)程中運(yùn)行程序。 2.捕獲任何子進(jìn)程發(fā)送到標(biāo)準(zhǔn)輸出的內(nèi)容。 3.把內(nèi)容發(fā)送回發(fā)出請(qǐng)求的客戶端。 完整的CGI協(xié)議比這個(gè)要豐富的多-尤其是它允許url中服務(wù)器傳遞給程序的參數(shù)運(yùn)行-但這些細(xì)節(jié)不影響系統(tǒng)的整體架構(gòu)...這再次變得相當(dāng)亂。 RequestHandler中最初有一個(gè)方法handle_file,用于處理內(nèi)容?,F(xiàn)在,我們已經(jīng)在list_dir和run_cgi表中加入了兩種特殊情況。這三個(gè)方法并不真正在所屬類起作用,因?yàn)樗麄冎饕善渌椒ㄊ褂谩?/p> 修補(bǔ)程序很簡(jiǎn)單:為所有的case handler創(chuàng)建一個(gè)父類,當(dāng)(且僅當(dāng))他們由兩個(gè)或多個(gè)handler共享時(shí),把其他方法移入這個(gè)類。當(dāng)我們完成后,RequestHandler類看起來是這樣的: 與此同時(shí)case handler的父類是:
我們?cè)即a和重構(gòu)版本之間的差異反映了兩個(gè)重要思想。首先是考慮類作為相關(guān)服務(wù)的集合。services.RequestHandler和base_case不作決定或采取行動(dòng);它們提供讓其他類使用去做這些事的工具。 第二個(gè)是可擴(kuò)展性:人們可以通過編寫一個(gè)外部CGI程序,或通過添加一個(gè)case處理器的類,來給我們的Web服務(wù)器添加新功能。后者確實(shí)需要更改RequestHandler的一行(在Cases列表插入case handler),但我們可以通過Web服務(wù)器讀取一個(gè)配置文件和負(fù)載handler類來避免。在這兩種情況下,它們可以忽略更低層級(jí)的細(xì)節(jié),正如BaseHTTPRequestHandler類的作者已經(jīng)允許我們忽略處理套接字連接和解析HTTP請(qǐng)求的細(xì)節(jié)。 這些想法通常是有用的;看看你能不能想辦法在自己的項(xiàng)目中使用它們。 ------------------------------------------------------------------------------------------------------------------- 1.在本章中,包括一些情況下,狀態(tài)代碼404不合適的地方,我們會(huì)好幾次使用handle_error。當(dāng)你繼續(xù)閱讀,試著想想你將如何擴(kuò)展程序使?fàn)顟B(tài)響應(yīng)代碼可以很容易地在每種情況下被提供 2.我們的代碼還使用了popen2庫函數(shù),該函數(shù)為支持subprocess模塊已經(jīng)被棄用了。然而,在這個(gè)例子中,popen2是較少分散使用的工具。 英文原文:http:///en/500L/a-simple-web-server.html |
|