Python網路應用也需要網路協議的相關知識,可參考協議森林
**注意,在Python 3.x中,BaseHTTPServer, SimpleHTTPServer, CGIHTTPServer整合到http.server包,SocketServer改名為socketserver,請注意查閱官方文件。
在上一篇文章中(用socket寫一個Python伺服器),我使用socket介面,製作了一個處理HTTP請求的Python伺服器。任何一臺裝有作業系統和Python直譯器的計算機,都可以作為HTTP伺服器使用。我將在這裡不斷改寫上一篇文章中的程式,引入更高階的Python包,以寫出更成熟的Python伺服器。
支援POST
我首先增加該伺服器的功能。這裡增添了表格,以及處理表格提交資料的”POST”方法。如果你已經讀過用socket寫一個Python伺服器,會發現這裡只是增加很少的一點內容。
原始程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# Written by Vamei # A messy HTTP server based on TCP socket import socket # Address HOST = '' PORT = 8000 text_content = ''' HTTP/1.x 200 OK Content-Type: text/html <head> <title>WOW</title> </head> <html> <p>Wow, Python Server</p> <IMG src="test.jpg"/> <form name="input" action="/" method="post"> First name:<input type="text" name="firstname"><br> <input type="submit" value="Submit"> </form> </html> ''' f = open('test.jpg','rb') pic_content = ''' HTTP/1.x 200 OK Content-Type: image/jpg ''' pic_content = pic_content + f.read() # Configure socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((HOST, PORT)) # Serve forever while True: s.listen(3) conn, addr = s.accept() request = conn.recv(1024) # 1024 is the receiving buffer size method = request.split(' ')[0] src = request.split(' ')[1] print 'Connected by', addr print 'Request is:', request # if GET method request if method == 'GET': # if ULR is /test.jpg if src == '/test.jpg': content = pic_content else: content = text_content # send message conn.sendall(content) # if POST method request if method == 'POST': form = request.split('\r\n') idx = form.index('') # Find the empty line entry = form[idx:] # Main content of the request value = entry[-1].split('=')[-1] conn.sendall(text_content + '\n <p>' + value + '</p>') ###### # More operations, such as put the form into database # ... ###### # close connection conn.close() |
伺服器進行的操作很簡單,即從POST請求中提取資料,再顯示在螢幕上。
執行上面Python伺服器,像上一篇文章那樣,使用一個瀏覽器開啟。
頁面新增了表格和提交(submit)按鈕。在表格中輸入aa並提交,頁面顯示出aa。
我下一步要用一些高階包,來簡化之前的程式碼。
使用SocketServer
首先使用SocketServer包來方便的架設伺服器。在上面使用socket的過程中,我們先設定了socket的型別,然後依次呼叫bind(),listen(),accept(),最後使用while迴圈來讓伺服器不斷的接受請求。上面的這些步驟可以通過SocketServer包來簡化。
SocketServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# Written by Vamei # use TCPServer import SocketServer HOST = '' PORT = 8000 text_content = ''' HTTP/1.x 200 OK Content-Type: text/html <head> <title>WOW</title> </head> <html> <p>Wow, Python Server</p> <IMG src="test.jpg"/> <form name="input" action="/" method="post"> First name:<input type="text" name="firstname"><br> <input type="submit" value="Submit"> </form> </html> ''' f = open('test.jpg','rb') pic_content = ''' HTTP/1.x 200 OK Content-Type: image/jpg ''' pic_content = pic_content + f.read() # This class defines response to each request class MyTCPHandler(SocketServer.BaseRequestHandler): def handle(self): # self.request is the TCP socket connected to the client request = self.request.recv(1024) print 'Connected by',self.client_address[0] print 'Request is', request method = request.split(' ')[0] src = request.split(' ')[1] if method == 'GET': if src == '/test.jpg': content = pic_content else: content = text_content self.request.sendall(content) if method == 'POST': form = request.split('\r\n') idx = form.index('') # Find the empty line entry = form[idx:] # Main content of the request value = entry[-1].split('=')[-1] self.request.sendall(text_content + '\n <p>' + value + '</p>') ###### # More operations, such as put the form into database # ... ###### # Create the server server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler) # Start the server, and work forever server.serve_forever() |
我建立了一個TCPServer物件,即一個使用TCP socket的伺服器。在建立TCPServe的同時,設定該伺服器的IP地址和埠。使用server_forever()方法來讓伺服器不斷工作(就像原始程式中的while迴圈一樣)。
我們傳遞給TCPServer一個MyTCPHandler類。這個類定義瞭如何操作socket。MyTCPHandler繼承自BaseRequestHandler。改寫handler()方法,來具體規定不同情況下伺服器的操作。
在handler()中,通過self.request來查詢通過socket進入伺服器的請求 (正如我們在handler()中對socket進行recv()和sendall()操作),還使用self.address來引用socket的客戶端地址。
經過SocketServer的改造之後,程式碼還是不夠簡單。 我們上面的通訊基於TCP協議,而不是HTTP協議。因此,我們必須手動的解析HTTP協議。我們將建立基於HTTP協議的伺服器。
SimpleHTTPServer: 使用靜態檔案來回應請求
HTTP協議基於TCP協議,但增加了更多的規範。這些規範,雖然限制了TCP協議的功能,但大大提高了資訊封裝和提取的方便程度。
對於一個HTTP請求(request)來說,它包含有兩個重要資訊:請求方法和URL。
請求方法(request method) URL 操作
GET / 傳送text_content
GET /text.jpg 傳送pic_content
POST / 分析request主體中包含的value(實際上是我們填入表格的內容); 傳送text_content和value
根據請求方法和URL的不同,一個大型的HTTP伺服器可以應付成千上萬種不同的請求。在Python中,我們可以使用SimpleHTTPServer包和CGIHTTPServer包來規定針對不同請求的操作。其中,SimpleHTTPServer可以用於處理GET方法和HEAD方法的請求。它讀取request中的URL地址,找到對應的靜態檔案,分析檔案型別,用HTTP協議將檔案傳送給客戶。
SimpleHTTPServer
我們將text_content放置在index.html中,並單獨儲存text.jpg檔案。如果URL指向index_html的母資料夾時,SimpleHTTPServer會讀取該資料夾下的index.html檔案。
我在當前目錄下生成index.html檔案:
1 2 3 4 5 6 7 8 9 10 11 |
<head> <title>WOW</title> </head> <html> <p>Wow, Python Server</p> <IMG src="test.jpg"/> <form name="input" action="/" method="post"> First name:<input type="text" name="firstname"><br> <input type="submit" value="Submit"> </form> </html> |
改寫Python伺服器程式。使用SimpleHTTPServer包中唯一的類SimpleHTTPRequestHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Written by Vamei # Simple HTTPsERVER import SocketServer import SimpleHTTPServer HOST = '' PORT = 8000 # Create the server, SimpleHTTPRequestHander is pre-defined handler in SimpleHTTPServer package server = SocketServer.TCPServer((HOST, PORT), SimpleHTTPServer.SimpleHTTPRequestHandler) # Start the server server.serve_forever() |
這裡的程式不能處理POST請求。我會在後面使用CGI來彌補這個缺陷。值得注意的是,Python伺服器程式變得非常簡單。將內容存放於靜態檔案,並根據URL為客戶端提供內容,這讓內容和伺服器邏輯分離。每次更新內容時,我可以只修改靜態檔案,而不用停止整個Python伺服器。
這些改進也付出代價。在原始程式中,request中的URL只具有指導意義,我可以規定任意的操作。在SimpleHTTPServer中,操作與URL的指向密切相關。我用自由度,換來了更加簡潔的程式。
CGIHTTPServer:使用靜態檔案或者CGI來回應請求
CGIHTTPServer包中的CGIHTTPRequestHandler類繼承自SimpleHTTPRequestHandler類,所以可以用來代替上面的例子,來提供靜態檔案的服務。此外,CGIHTTPRequestHandler類還可以用來執行CGI指令碼。
、
CGIHTTPServer
先看看什麼是CGI (Common Gateway Interface)。CGI是伺服器和應用指令碼之間的一套介面標準。它的功能是讓伺服器程式執行指令碼程式,將程式的輸出作為response傳送給客戶。總體的效果,是允許伺服器動態的生成回覆內容,而不必侷限於靜態檔案。
支援CGI的伺服器程接收到客戶的請求,根據請求中的URL,執行對應的指令碼檔案。伺服器會將HTTP請求的資訊和socket資訊傳遞給指令碼檔案,並等待指令碼的輸出。指令碼的輸出封裝成合法的HTTP回覆,傳送給客戶。CGI可以充分發揮伺服器的可程式設計性,讓伺服器變得“更聰明”。
伺服器和CGI指令碼之間的通訊要符合CGI標準。CGI的實現方式有很多,比如說使用Apache伺服器與Perl寫的CGI指令碼,或者Python伺服器與shell寫的CGI指令碼。
為了使用CGI,我們需要使用BaseHTTPServer包中的HTTPServer類來構建伺服器。Python伺服器的改動很簡單。
CGIHTTPServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Written by Vamei # A messy HTTP server based on TCP socket import BaseHTTPServer import CGIHTTPServer HOST = '' PORT = 8000 # Create the server, CGIHTTPRequestHandler is pre-defined handler server = BaseHTTPServer.HTTPServer((HOST, PORT), CGIHTTPServer.CGIHTTPRequestHandler) # Start the server server.serve_forever() |
CGIHTTPRequestHandler預設當前目錄下的cgi-bin和ht-bin資料夾中的檔案為CGI指令碼,而存放於其他地方的檔案被認為是靜態檔案。因此,我們需要修改一下index.html,將其中form元素指向的action改為cgi-bin/post.py。
1 2 3 4 5 6 7 8 9 10 11 |
<head> <title>WOW</title> </head> <html> <p>Wow, Python Server</p> <IMG src="test.jpg"/> <form name="input" action="cgi-bin/post.py" method="post"> First name:<input type="text" name="firstname"><br> <input type="submit" value="Submit"> </form> </html> |
我建立一個cgi-bin的資料夾,並在cgi-bin中放入如下post.py檔案,也就是我們的CGI指令碼:
1 2 3 4 5 6 7 8 9 10 11 |
#!/usr/bin/env python # Written by Vamei import cgi form = cgi.FieldStorage() # Output to stdout, CGIHttpServer will take this as response to the client print "Content-Type: text/html" # HTML is following print # blank line, end of headers print "<p>Hello world!</p>" # Start of content print "<p>" + repr(form['firstname']) + "</p>" |
(post.py需要有執行許可權,見評論區)
第一行說明了指令碼所使用的語言,即Python。 cgi包用於提取請求中包含的表格資訊。指令碼只負責將所有的結果輸出到標準輸出(使用print)。CGIHTTPRequestHandler會收集這些輸出,封裝成HTTP回覆,傳送給客戶端。
對於POST方法的請求,它的URL需要指向一個CGI指令碼(也就是在cgi-bin或者ht-bin中的檔案)。CGIHTTPRequestHandler繼承自SimpleHTTPRequestHandler,所以也可以處理GET方法和HEAD方法的請求。此時,如果URL指向CGI指令碼時,伺服器將指令碼的執行結果傳送到客戶端;當此時URL指向靜態檔案時,伺服器將檔案的內容傳送到客戶端。
更進一步,我可以讓CGI指令碼執行資料庫操作,比如將接收到的資料放入到資料庫中,以及更豐富的程式操作。相關內容從略。
總結
我使用了Python標準庫中的一些高階包簡化了Python伺服器。最終的效果分離靜態內容、CGI應用和伺服器,降低三者之間的耦合,讓程式碼變得簡單而容易維護。
希望你享受在自己的電腦上架設伺服器的過程。