介紹html
在過去20幾年裏,網絡已經在各個方面改變了咱們的生活,可是它的核心卻幾乎沒有什麼改變。多數的系統依然遵循着Tim Berners-Lee在上個世紀發佈的規則。大多數的web服務器都在用一樣的方式處理消息python
背景web
多數在web上的服務器都是運行在IP協議標準上。在這協議家族裏面咱們關心的成員就是TCP,這個協議使得計算機之間的通訊看起來像是在讀寫文件。shell
項目經過套接字來使用IP通訊。每一個套接字都是一個點對點的通訊信道,一個套接字包含IP地址,端口來標識具體的機器。IP地址包含4個8Bit的數字,好比174.136.14.108;DNS將這些數字匹配到更加容易識別的名字好比aosabook.org,這樣更加便於人們記住。數據庫
HTTP是一種能夠在IP之上傳輸數據的方式。HTTP很是簡單:客戶端在套接字鏈接上發送一個請求指示須要什麼樣的信息,而後服務端就發送響應。數據能夠是從硬盤上的文件拷貝過來,程序動態生成,或者是二者結合編程
HTTP請求中最重要的就是文本:任何項目均可以創造或者解析一個文本。爲了便於理解,文本有圖中所示的部分瀏覽器
HTTP方法通常採用」GET」(去獲取信息)或者」POST」(去提交表單數據或者上傳文件)。URL指明瞭客戶端想要的;通常是硬件上文件的路徑,好比/research/experiments.html
,可是這一切都取決於服務器端如何去作。
HTTP
版本通常是
"HTTP/1.0"
或者
"HTTP/1.1"
;咱們並不關心這二者的差異。
安全
HTTP
的頭是像下面的成對鍵值:
服務器
Accept: text/htmlAccept-Language: en, frIf-Modified-Since: 16-May-2005
和哈希表中的鍵值不同的是,鍵值在
HTTP
頭中能夠出現任意的次數。這就使得請求能夠去指定它願意接受的幾種類型。
cookie
最後,請求的主體是與請求相關聯的任何額外數據。這些將被用在經過表單提交數據,上傳文件等等。
在最後一個標頭和主體的開始之間必須有空白行以表示標頭的結束。
一個被稱爲
Content-length
的頭,用來告訴在請求數據中指望讀取多數個字節。
HTTP
響應也和
HTTP
請求是同樣的格式
版本
,頭信息和主體都是一樣的格式。狀態碼是一個數字用來指示請求處理時發生了什麼:
200
意味着正常工做,
404
意味着沒有找到,其餘的碼也有不一樣的意思。
對於這章節
,咱們只須要知道
HTTP
的其餘兩件事。
第一個就是無狀態
:每一個請求都處理本身的,而且服務器端。服務器不會記住當前請求和下一個請求之間的內容。若是應用想跟蹤好比用戶身份的信息,就必須本身處理。
一般採用的方法是用
cookie
,
cookie
是服務器發送給客戶端的字符流,而後客戶端返回給服務器。當一個用戶須要實如今不一樣請求之間保持狀態的時候,服務器會建立
cookie
,存儲在數據庫裏,而後發送給瀏覽器。每次瀏覽器把
cookie
值發送回來的時候,服務器都會用來去查找信息來知道用戶在幹什麼。
第二個咱們須要瞭解關於
HTTP
的就是
URL
能夠經過提供參數來提供更多的信息
。好比,若是咱們在使用搜索引擎,咱們必須指定搜索術語。咱們能夠加入到
URL
的路徑中,可是咱們通常都是加入到
URL
的參數中。咱們在
URL
中增長
?,
後面跟隨
key=value
而且用
&
符號分割來達到這個目的。好比
URL
http://www.google.ca?q=Python
就告訴
Google
去搜索
Python
相關的網頁。鍵值是字母
q
,值是
Python
。更長的查詢
http://www.google.ca/search?q=Python&client=Firefox
告訴
Google
咱們正在使用
Firefox
等等
。咱們能夠傳輸任何咱們須要的參數。可是使用哪個,如何解釋這些參數取決於應用。
固然
,若是
?
和
&
特殊的字符,那麼必須有一種方法去規避,
正如必須有一種方法將雙引號字符放入由雙引號分隔的字符串中同樣
。
URL
的編碼標準用
%
後面跟
2
個字節碼的方式來表示特殊字符,用
+
來代替空格。因此爲了在
Google
上搜索」
grade=A+」
,咱們可使用的
URL
爲
http://www.google.ca/search?q=grade+%3D+A%2B
建立
sockets
,構建
HTTP
請求,解析響應是很是枯燥的事情。因此人們更可能是使用庫函數來完成大部分的工做。
Python
附帶了一個
urllib2
的庫,可是它暴露了不少人根本不關心的管道。
Request
庫是能夠替代
urllib2
而且更加好使用的庫。下面是一個從
AOA
網站下載網頁的例子。
import requestsresponse = requests.get('http://aosabook.org/en/500L/web-server/testpage.html')print 'status code:', response.status_codeprint 'content length:', response.headers['content-length']print response.textstatus code: 200content length: 61<html><body><p>Test page.</p></body></html>
requests.get
發送一個
HTTP GET
請求到服務器而後返回一個包含響應的對象。對象的
status_code
成員是響應的狀態碼;
content_length
成員是響應數據的長度,
text
是真是的數據
(
在這個例子中,是
HTTP
網頁
)
你好
,web
如今咱們準備去寫第一個
簡單的
web
服務器。
1
等待某人鏈接到服務器上而且發送一個請求
2
解析請求
3
指出要求獲取的東西
4
獲取數據(或者動態的產生)
5
將數據格式化爲
HTML
格式
6
發送回去
1,2,6
步對於各類不一樣的應用來講都是同樣的,
Python
標準庫有一個模塊稱爲
BaseHTTPServer
爲咱們作完成這些。咱們須要完成的是步驟
3
到步驟
5.
這一部分只須要不多的工做
這個函數假設被容許web服務器正在運行的目錄或者目錄下的任何文件(經過os.getcwd來獲取)。程序會將URL中包含的路徑和當前的路徑組裝起來(URL中的路徑放在self.path變量中,初始化的時候都是’/’)來獲得用戶須要的文件路徑 若是路徑不存在,或者不是個文件,函數將會經過產生並捕獲一個異常來報告錯誤。若是路徑和文件匹配,則會調用handle_file函數來讀取並返回內容。這個函數讀取文件而且使用send_content來發送給客戶端
import BaseHTTPServerclass RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):'''Handle HTTP requests by returning a fixed 'page'.'''# Page to send back.Page = '''\<html><body><p>Hello, web!</p></body></html>'''# Handle a GET request.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(self.Page)if __name__ == '__main__':
serverAddress = ('', 8080)
server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)
server.serve_forever()
if __name__ == '__main__':serverAddress = ('', 8080)server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)server.serve_forever()BaseHTTPRequestHandler庫會解析傳入的HTTP請求而後決定裏面包含的方法。若是方法是GET,類就會調用do_GET的函數。咱們本身的類RequestHandler重寫了這個方法來動態生成網頁:文本text存儲在類級別的參數page,Page將會在發送了200響應碼後發送給客戶端,Content-Type頭告訴客戶端用HTML的方式來解析數據以及網頁的長度(end_headers方法在咱們的頭和網頁之間插入空白行)可是RequestHandler並非整個的工程:咱們依然須要最後的三行啓動服務器。第一行用一個元組的方式來定義服務器的地址:空字符意味着運行在本機上,8080是端口。而後咱們用整個地址和RequestHandler做爲參數來建立BaseHTTPServer.HTTPServer實例,而後讓程序永遠運行(在實際中,除非用Control-C中止整個程序)若是咱們在命令行中運行整個項目,不會顯示任何東西$ 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 -第一行是直截了當的:由於咱們並無要求獲取具體的文件,瀏覽器要求獲取」/」(服務器運行的根目錄)。第二行出現是由於瀏覽器自動發送第二個請求去獲取圖片文件/favicon.ico,它將在地址欄中顯示爲圖標。顯示數值讓咱們修改下web服務器使得能夠顯示在HTTP請求中的內容(未來在調試的過程當中咱們常常會作這件事,因此咱們先練習下)爲了保持咱們的代碼乾淨,咱們將發送和建立頁面分開class RequestHandler(BaseHTTPServer.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的代碼和以前的同樣def send_page(self, page):self.send_response(200)self.send_header("Content-type", "text/html")self.send_header("Content-Length", str(len(page)))self.end_headers()self.wfile.write(page)想要顯示的網頁模板是一個字符串
,其中包含了
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
程序的主體並無改變
:和以前同樣,建立了一個
HTTPServer
類實例
,其中包含地址和請求,而後服務器就永遠工做。若是咱們開始運行而且從瀏覽器中發送請求
http://localhost:8080/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
即便
something.html
網頁不在網頁上,咱們也沒有發現
404
異常。這是由於服務器只是一個程序,
當收到請求時,它能夠作任何它想作的事:發送回前一個請求中命名的文件,提供隨機選擇的維基百科頁面,或者咱們對它進行編程的任何其餘內容。 靜態網頁 下一步就是從硬盤上的網頁開始啓動而不是隨機產生一個。咱們能夠重寫do_GET想要顯示的網頁模板是一個字符串,其中包含了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程序的主體並無改變:和以前同樣,建立了一個HTTPServer類實例,其中包含地址和請求,而後服務器就永遠工做。若是咱們開始運行而且從瀏覽器中發送請求http://localhost:8080/something.html。咱們將獲得:Date and time Mon, 24 Feb 2014 17:17:12 GMTClient host 127.0.0.1Client port 54548Command GETPath /something.html即便something.html網頁不在網頁上,咱們也沒有發現404異常。這是由於服務器只是一個程序,def do_GET(self):try:# Figure out what exactly is being requested.full_path = os.getcwd() + self.path# It doesn't exist...if not os.path.exists(full_path):raise ServerException("'{0}' not found".format(self.path))# ...it's a file...elif os.path.isfile(full_path):self.handle_file(full_path)# ...it's something we don't handle.else:raise ServerException("Unknown object '{0}'".format(self.path))# Handle errors.except Exception as msg:self.handle_error(msg)
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)
注意到咱們用二進制的方式來打開文件--’rb’中的’b’. 這樣Python就不會幫咱們經過過改變看起來像Windows行結尾的字節序列。而且在運行的時候,將整個的文件讀進內存是個很糟糕的主意。像視頻文件有多是好幾個G的大小。可是處理那樣的狀況不在本章節的考慮以內。 爲了完成這個類,咱們還須要寫一個異常處理方法以及錯誤報告的網頁模板
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)self.send_content(content)
這個程序能夠工做了,可是咱們仔細看會發現問題。問題在與老是返回200的狀態碼,即便被請求的的網頁不存在。是的,在這種狀況下,發送回的頁面包含錯誤信息,可是瀏覽器不能閱讀英文,因此也不知道request是成功仍是失敗。爲了讓這種狀況更清晰,咱們須要修改handle_error和send_content。# Handle unknown objects.
def handle_error(self, msg):
content = self.Error_Page.format(path=self.path, msg=msg)
self.send_content(content, 404)
# Send actual content.
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)
在一個文件沒被找到的時候咱們沒有拋出ServerException
異常
,而是產生了一個錯誤的頁面。
ServerException
是爲了在咱們本身搞錯的時候發送一個內部錯誤的信號。
handle_error
建立的異常網頁,只會在用戶發生錯誤的時候發生。好比發送
URL
中的文件並不存在。
顯示目錄
# Handle unknown objects.def handle_error(self, msg):content = self.Error_Page.format(path=self.path, msg=msg)self.send_content(content, 404)# Send actual content.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)ServerException異常,而是產生了一個錯誤的頁面。ServerException是爲了在咱們本身搞錯的時候發送一個內部錯誤的信號。handle_error建立的異常網頁,只會在用戶發生錯誤的時候發生。好比發送URL中的文件並不存在。顯示目錄下一步,咱們將教會服務器當URL是一個目錄而不是文件的時候顯示路徑的內容。咱們還能夠走遠一點在路徑中去尋找index.html文件並顯示出來,而且在文件不存在的時候顯示路徑的內容。可是在do_GET中創建這些規則將會是個錯誤,由於所獲得的方法將是一長串控制特殊行爲的if語句。正確的解決方法是退後並解決通常性問題,那就是指出URL將要發生的動做。下面是對do_GET的重寫。def do_GET(self):try:# Figure out what exactly is being requested.self.full_path = os.getcwd() + self.path# Figure out how to handle it.for case in self.Cases:handler = case()if handler.test(self):handler.act(self)break# Handle errors.except Exception as msg:self.handle_error(msg)第一步都是同樣的:指出請求的全路徑。儘管如此,代碼仍是看起來不同,不是一堆的內聯測試,這個版本查找存儲在列表中的事件集合。每一個事件對象都有2個方法:test,用來告訴咱們是否能夠處理這個請求以及act,用來實際執行動做。一旦咱們找到了正確的事件,咱們就開始處理請求而且跳出循環。下面三個對象事件從新塑造了服務器的行爲:class case_no_file(object):'''File or directory does not exist.'''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):'''File exists.'''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):'''Base case if nothing else worked.'''def test(self, handler):return Truedef act(self, handler):raise ServerException("Unknown object '{0}'".format(handler.path))在RequestHandler類的開始的時候,咱們將將創建事件處理列表。class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):'''If the requested path maps to a file, that file is served.If anything goes wrong, an error page is constructed.'''Cases = [case_no_file(),case_existing_file(),case_always_fail()]...everything else as before...如今服務器代碼變得愈來愈複雜:代碼行數從74變成了99,還有一個額外的間接級別且沒有函數。當咱們回到本章開始的任務,並試圖教咱們的服務器在index.html頁面上提供一個目錄(若是有的話)以及目錄列表(若是沒有的話)時,就會獲得好處。以前的處理以下:class case_directory_index_file(object):'''Serve index.html page for a directory.'''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方法構建到index.html的路徑;將其放入case處理程序能夠防止主RequestHandler中的混亂,測試檢查路徑是不是包含index.html頁面的目錄,act請求主請求程序去爲該網頁提供服務。RequestHandler惟一的變化是在Cases列表中添加case_directory_index_file對象。Cases = [case_no_file(),case_existing_file(),case_directory_index_file(),case_always_fail()]若是路徑中不包含index.html網頁?測試和上面的同樣,僅僅是插入了一個not語句,可是act方法如何處理?它應該作什麼class case_directory_no_index_file(object):'''Serve listing for a directory without an index.html page.'''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方法應該建立,返回路徑列表,可是咱們的代碼不容許這樣:RequestHandler.do_GET調用act,可是並無指望去處理和返回值。如今,讓咱們在RequestHandler加一個方法去生成路徑列表,而後從事件的處理器act中去調用。class case_directory_no_index_file(object):'''Serve listing for a directory without an index.html page.'''# ...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))self.send_content(page)except OSError as msg:msg = "'{0}' cannot be listed: {1}".format(self.path, msg)self.handle_error(msg)CGI協議固然,多數的人都不想去編輯web服務器的源代碼來增長新的功能。爲了避免給開發者增長更多的工做量,服務器老是支持稱爲CGI的機制,這爲服務器提供了一種標準的方法去運行外部程序來知足需求。好比,加入咱們想服務器可以在HTML網頁上顯示當地時間。咱們能夠在程序中增長几行代碼from datetime import datetimeprint '''\<html><body><p>Generated {0}</p></body></html>'''.format(datetime.now())爲了讓服務器運行程序,咱們增長了事件處理器:class case_cgi_file(object):'''Something runnable.'''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)測試樣例:這個路徑是不是以.py結尾?若是是,RequestHandler運行這個程序def run_cgi(self, full_path):cmd = "python " + full_pathchild_stdin, child_stdout = os.popen2(cmd)child_stdin.close()data = child_stdout.read()child_stdout.close()self.send_content(data)這樣很是的不安全:若是有人知道了服務器上的Python文件路徑,咱們就容許去運行這些程序而沒有去關心傳入了些什麼數據,是否包含了死循環或者其餘的。先無論上面的這些,咱們的核心觀點很簡單:1 在子進程中運行程序2 捕獲子進程發送到標準輸出的任何數據3 將輸出發送回觸發請求的客戶端完成的CGI程序比這個更加負責----總的來講,它容許服務器將URL中的參數傳遞給正在運行的程序,可是這些細節並不影響系統的架構。RequestHandler有一個初始函數,handle_file用來處理內容。咱們如今以list_dir和run_cgi的形式增長了2個特殊的事件。這三個方法並不屬於當前的位置,由於它們是被其餘地方調用解決辦法很簡單:爲全部的事件處理建立一個父類,若是其餘方法被多個處理器共享使用就將其移入到類中。當咱們完成的時候,RequestHandler類看起來以下:class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):Cases = [case_no_file(),case_cgi_file(),case_existing_file(),case_directory_index_file(),case_directory_no_index_file(),case_always_fail()]# How to display an error.Error_Page = """\<html><body><h1>Error accessing {path}</h1><p>{msg}</p></body></html>"""# Classify and handle request.def do_GET(self):try:# Figure out what exactly is being requested.self.full_path = os.getcwd() + self.path# Figure out how to handle it.for case in self.Cases:if case.test(self):case.act(self)break# Handle errors.except Exception as msg:self.handle_error(msg)# Handle unknown objects.def handle_error(self, msg):content = self.Error_Page.format(path=self.path, msg=msg)self.send_content(content, 404)# Send actual content.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):'''Parent for case handlers.'''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(full_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.'處理存在文件的代碼以下:class case_existing_file(base_case):'''File exists.'''def test(self, handler):return os.path.isfile(handler.full_path)def act(self, handler):self.handle_file(handler, handler.full_path)
版本
,頭信息和主體都是一樣的格式。狀態碼是一個數字用來指示請求處理時發生了什麼:
200
意味着正常工做,
404
意味着沒有找到,其餘的碼也有不一樣的意思。
對於這章節
,咱們只須要知道
HTTP
的其餘兩件事。
第一個就是無狀態
:每一個請求都處理本身的,而且服務器端。服務器不會記住當前請求和下一個請求之間的內容。若是應用想跟蹤好比用戶身份的信息,就必須本身處理。
一般採用的方法是用
cookie
,
cookie
是服務器發送給客戶端的字符流,而後客戶端返回給服務器。當一個用戶須要實如今不一樣請求之間保持狀態的時候,服務器會建立
cookie
,存儲在數據庫裏,而後發送給瀏覽器。每次瀏覽器把
cookie
值發送回來的時候,服務器都會用來去查找信息來知道用戶在幹什麼。
第二個咱們須要瞭解關於
HTTP
的就是
URL
能夠經過提供參數來提供更多的信息
。好比,若是咱們在使用搜索引擎,咱們必須指定搜索術語。咱們能夠加入到
URL
的路徑中,可是咱們通常都是加入到
URL
的參數中。咱們在
URL
中增長
?,
後面跟隨
key=value
而且用
&
符號分割來達到這個目的。好比
URL
http://www.google.ca?q=Python
就告訴
Google
去搜索
Python
相關的網頁。鍵值是字母
q
,值是
Python
。更長的查詢
http://www.google.ca/search?q=Python&client=Firefox
告訴
Google
咱們正在使用
Firefox
等等
。咱們能夠傳輸任何咱們須要的參數。可是使用哪個,如何解釋這些參數取決於應用。
固然
,若是
?
和
&
特殊的字符,那麼必須有一種方法去規避,
正如必須有一種方法將雙引號字符放入由雙引號分隔的字符串中同樣
。
URL
的編碼標準用
%
後面跟
2
個字節碼的方式來表示特殊字符,用
+
來代替空格。因此爲了在
Google
上搜索」
grade=A+」
,咱們可使用的
URL
爲
http://www.google.ca/search?q=grade+%3D+A%2B
建立
sockets
,構建
HTTP
請求,解析響應是很是枯燥的事情。因此人們更可能是使用庫函數來完成大部分的工做。
Python
附帶了一個
urllib2
的庫,可是它暴露了不少人根本不關心的管道。
Request
庫是能夠替代
urllib2
而且更加好使用的庫。下面是一個從
AOA
網站下載網頁的例子。
import requestsresponse = requests.get('http://aosabook.org/en/500L/web-server/testpage.html')print 'status code:', response.status_codeprint 'content length:', response.headers['content-length']print response.textstatus code: 200content length: 61<html><body><p>Test page.</p></body></html>
requests.get
發送一個
HTTP GET
請求到服務器而後返回一個包含響應的對象。對象的
status_code
成員是響應的狀態碼;
content_length
成員是響應數據的長度,
text
是真是的數據
(
在這個例子中,是
HTTP
網頁
)
你好
,web
如今咱們準備去寫第一個
簡單的
web
服務器。
1
等待某人鏈接到服務器上而且發送一個請求
2
解析請求
3
指出要求獲取的東西
4
獲取數據(或者動態的產生)
5
將數據格式化爲
HTML
格式
6
發送回去
1,2,6
步對於各類不一樣的應用來講都是同樣的,
Python
標準庫有一個模塊稱爲
BaseHTTPServer
爲咱們作完成這些。咱們須要完成的是步驟
3
到步驟
5.
這一部分只須要不多的工做
當收到請求時,它能夠作任何它想作的事:發送回前一個請求中命名的文件,提供隨機選擇的維基百科頁面,或者咱們對它進行編程的任何其餘內容。 靜態網頁 下一步就是從硬盤上的網頁開始啓動而不是隨機產生一個。咱們能夠重寫do_GET 這個函數假設被容許web服務器正在運行的目錄或者目錄下的任何文件(經過os.getcwd來獲取)。程序會將URL中包含的路徑和當前的路徑組裝起來(URL中的路徑放在self.path變量中,初始化的時候都是’/’)來獲得用戶須要的文件路徑 若是路徑不存在,或者不是個文件,函數將會經過產生並捕獲一個異常來報告錯誤。若是路徑和文件匹配,則會調用handle_file函數來讀取並返回內容。這個函數讀取文件而且使用send_content來發送給客戶端 注意到咱們用二進制的方式來打開文件--’rb’中的’b’. 這樣Python就不會幫咱們經過過改變看起來像Windows行結尾的字節序列。而且在運行的時候,將整個的文件讀進內存是個很糟糕的主意。像視頻文件有多是好幾個G的大小。可是處理那樣的狀況不在本章節的考慮以內。 爲了完成這個類,咱們還須要寫一個異常處理方法以及錯誤報告的網頁模板 這個程序能夠工做了,可是咱們仔細看會發現問題。問題在與老是返回200的狀態碼,即便被請求的的網頁不存在。是的,在這種狀況下,發送回的頁面包含錯誤信息,可是瀏覽器不能閱讀英文,因此也不知道request是成功仍是失敗。爲了讓這種狀況更清晰,咱們須要修改handle_error和send_content。 在一個文件沒被找到的時候咱們沒有拋出 import BaseHTTPServerclass RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):'''Handle HTTP requests by returning a fixed 'page'.'''# Page to send back.Page = '''\<html><body><p>Hello, web!</p></body></html>'''# Handle a GET request.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(self.Page)if __name__ == '__main__':serverAddress = ('', 8080)server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler)server.serve_forever()BaseHTTPRequestHandler庫會解析傳入的HTTP請求而後決定裏面包含的方法。若是方法是GET,類就會調用do_GET的函數。咱們本身的類RequestHandler重寫了這個方法來動態生成網頁:文本text存儲在類級別的參數page,Page將會在發送了200響應碼後發送給客戶端,Content-Type頭告訴客戶端用HTML的方式來解析數據以及網頁的長度(end_headers方法在咱們的頭和網頁之間插入空白行)可是RequestHandler並非整個的工程:咱們依然須要最後的三行啓動服務器。第一行用一個元組的方式來定義服務器的地址:空字符意味着運行在本機上,8080是端口。而後咱們用整個地址和RequestHandler做爲參數來建立BaseHTTPServer.HTTPServer實例,而後讓程序永遠運行(在實際中,除非用Control-C中止整個程序)若是咱們在命令行中運行整個項目,不會顯示任何東西$ 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 -第一行是直截了當的:由於咱們並無要求獲取具體的文件,瀏覽器要求獲取」/」(服務器運行的根目錄)。第二行出現是由於瀏覽器自動發送第二個請求去獲取圖片文件/favicon.ico,它將在地址欄中顯示爲圖標。顯示數值讓咱們修改下web服務器使得能夠顯示在HTTP請求中的內容(未來在調試的過程當中咱們常常會作這件事,因此咱們先練習下)爲了保持咱們的代碼乾淨,咱們將發送和建立頁面分開class RequestHandler(BaseHTTPServer.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的代碼和以前的同樣def send_page(self, page):self.send_response(200)self.send_header("Content-type", "text/html")self.send_header("Content-Length", str(len(page)))self.end_headers()self.wfile.write(page)想要顯示的網頁模板是一個字符串,其中包含了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程序的主體並無改變:和以前同樣,建立了一個HTTPServer類實例,其中包含地址和請求,而後服務器就永遠工做。若是咱們開始運行而且從瀏覽器中發送請求http://localhost:8080/something.html。咱們將獲得:Date and time Mon, 24 Feb 2014 17:17:12 GMTClient host 127.0.0.1Client port 54548Command GETPath /something.html即便something.html網頁不在網頁上,咱們也沒有發現404異常。這是由於服務器只是一個程序,def do_GET(self):try:# Figure out what exactly is being requested.full_path = os.getcwd() + self.path# It doesn't exist...if not os.path.exists(full_path):raise ServerException("'{0}' not found".format(self.path))# ...it's a file...elif os.path.isfile(full_path):self.handle_file(full_path)# ...it's something we don't handle.else:raise ServerException("Unknown object '{0}'".format(self.path))# Handle errors.except Exception as msg:self.handle_error(msg)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)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)self.send_content(content)# Handle unknown objects.def handle_error(self, msg):content = self.Error_Page.format(path=self.path, msg=msg)self.send_content(content, 404)# Send actual content.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)ServerException異常,而是產生了一個錯誤的頁面。ServerException是爲了在咱們本身搞錯的時候發送一個內部錯誤的信號。handle_error建立的異常網頁,只會在用戶發生錯誤的時候發生。好比發送URL中的文件並不存在。顯示目錄