預備知識:
關於http 協議的基礎請參考這里。
關於socket 基礎函數請參考這里。
關於python 網絡編程基礎請參考這里。
一、python socket 實現的簡單http服務器
廢話不多說,前面實現過使用linux c 或者python 充當客戶端來獲取http 響應,也利用muduo庫實現過一個簡易http服務器,現在來實現一個python版
的簡易http服務器,代碼改編自 http://www.cnblogs.com/vamei/ and http://www.liaoxuefeng.com/
httpServer.py
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
#!/usr/bin/env python #coding=utf-8 import socket import re HOST = '' PORT = 8000 #Read index.html, put into HTTP response data index_content = ''' HTTP/1.x 200 ok Content-Type: text/html ''' file = open('index.html', 'r') index_content += file.read() file.close() #Read reg.html, put into HTTP response data reg_content = ''' HTTP/1.x 200 ok Content-Type: text/html ''' file = open('reg.html', 'r') reg_content += file.read() file.close() #Read picture, put into HTTP response data file = open('T-mac.jpg', 'rb') pic_content = ''' HTTP/1.x 200 ok Content-Type: image/jpg ''' pic_content += file.read() file.close() #Configure socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind((HOST, PORT)) sock.listen(100) #infinite loop while True: # maximum number of requests waiting conn, addr = sock.accept() request = conn.recv(1024) method = request.split(' ')[0] src = request.split(' ')[1] print 'Connect by: ', addr print 'Request is:\n', request #deal wiht GET method if method == 'GET': if src == '/index.html': content = index_content elif src == '/T-mac.jpg': content = pic_content elif src == '/reg.html': content = reg_content elif re.match('^/\?.*$', src): entry = src.split('?')[1] # main content of the request content = 'HTTP/1.x 200 ok\r\nContent-Type: text/html\r\n\r\n' content += entry content += '<br /><font color="green" size="7">register successs!</p>' else: continue #deal with POST method elif method == 'POST': form = request.split('\r\n') entry = form[-1] # main content of the request content = 'HTTP/1.x 200 ok\r\nContent-Type: text/html\r\n\r\n' content += entry content += '<br /><font color="green" size="7">register successs!</p>' ###### # More operations, such as put the form into database # ... ###### else: continue conn.sendall(content) #close connection conn.close() |
chmod +x httpServer.py, 並運行./httpServer.py
使用瀏覽器當做客戶端訪問服務器
在httpServer.py 所在目錄有index.html, reg.html, T-mac.jpg
1、訪問目錄: http://192.168.56.188:8000/index.html
服務器輸出:
Connect by: ('192.168.56.1', 6274)
Request is:
GET /index.html HTTP/1.1
Host: 192.168.56.188:8000
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/33.0.1750.146 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4
回顧代碼可知我們給客戶端的響應是頭部+index.html, index.html如下:
1
2 3 4 5 6 7 8 9 10 |
<html> <head> <title>Jinan University</title> </head> <body> <p>Python HTTP Server</p> <img src="T-mac.jpg" /> </body> </html> |
進而進一步訪問T-mac.jpg,由於我們在實現服務器時使用短連接,即響應一次就關掉連接,所以客戶端會再發起一次連接,如下:
Connect by: ('192.168.56.1', 6275)
Request is:
GET /T-mac.jpg HTTP/1.1
Host: 192.168.56.188:8000
Connection: keep-alive
Accept: image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/33.0.1750.146 Safari/537.36
Referer: http://192.168.56.188:8000/index.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4
同樣地,服務器響應頭部+圖片的二進制數據,如下圖所示:
當然你也可以直接訪問 http://192.168.56.188:8000/T-mac.jpg
2、訪問目錄:http://192.168.56.188:8000/reg.html
服務器輸出:
Connect by: ('192.168.56.1', 6282)
Request is:
GET /reg.html HTTP/1.1
Host: 192.168.56.188:8000
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/33.0.1750.146 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4
同樣地,我們把頭部+reg.html 響應過去,reg.html 是注冊表單如下:
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 |
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=GBK"> <!--meta http-equiv="refresh" content="3;url=http://www.sina.com.cn" /--> <title>register page</title> </head> <body> <form action="http://192.168.56.188:8000" method="post"> <table border="1" bordercolor="#0000ff" cellpadding=10 cellspacing=0 width=600> <tr> <th colspan="2">注冊表單</th> </tr> <tr> <td>用戶名稱:</td> <td><input type="text" name="user" /></td> </tr> <tr> <td>輸入密碼:</td> <td><input type="password" name="psw" /></td> </tr> <tr> <td>確認密碼:</td> <td><input type="password" name="repsw" /></td> </tr> <tr> <td>選擇性別:</td> <td> <input type="radio" name="sex" value="nan" />男 <input type="radio" name="sex" value="nv" />女 </td> </tr> <tr> <td>選擇技術:</td> <td> <input type="checkbox" name="tech" value="java" />JAVA <input type="checkbox" name="tech" value="html" />HTML <input type="checkbox" name="tech" value="css" />CSS </td> </tr> <tr> <td>選擇國家:</td> <td> <select name="country"> <option value="none">--選擇國家--</option> <option value="usa">--美國--</option> <option value="en">--英國--</option> <option value="cn">--中國--</option> </select> </td> </tr> <tr> <th colspan="2"> <input type="reset" value="清除數據" /> <input type="submit" value="提交數據" /> </th> </tr> </table> </form> </body> </html> |
我們隨便填一些信息上去然后點擊提交數據,如下圖:
此時瀏覽器會訪問 http://192.168.56.188:8000/
服務器輸出為:
Connect by: ('192.168.56.1', 6578)
Request is:
POST / HTTP/1.1
Host: 192.168.56.188:8000
Connection: keep-alive
Content-Length: 59
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://192.168.56.188:8000
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/33.0.1750.146 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Referer: http://192.168.56.188:8000/reg.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4
user=simba&psw=1990&repsw=1990&sex=nan&tech=java&country=cn
注意:即表單中的name=value,以&分隔。
回顧代碼,我們只是將瀏覽器提交的數據體直接發回去,再輸出register success! 瀏覽器輸出如下圖:
如果我們把 表單中的 <form action="http://192.168.56.188:8000" method="post"> method 改成get,會是怎樣的呢?
此時瀏覽器會訪問 http://192.168.56.188:8000/?user=simba&psw=1990&repsw=1990&sex=nan&tech=java&country=cn
服務器輸出為:
Connect by: ('192.168.56.1', 6382)
Request is:
GET /?user=simba&psw=1990&repsw=1990&sex=nan&tech=java&country=cn HTTP/1.1
Host: 192.168.56.188:8000
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/33.0.1750.146 Safari/537.36
Referer: http://192.168.56.188:8000/reg.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4
因為我們應答回去的數據跟post一致,故瀏覽器看到的輸出也是一樣的。
在這里可以總結一下post 跟 get 提交的一些區別:
get提交,提交的信息都顯示在地址欄中;對於敏感數據不安全;由於地址欄存儲體積有限而不能提交大容量數據;將信息封裝到了請求消息的請求行
中,而post 提交將信息封裝到了請求體中。
二、CGIHTTPServer:使用靜態文件或者CGI來回應請求
先看看什么是CGI (Common Gateway Interface)。CGI是服務器和應用腳本之間的一套接口標准。它的功能是當客戶端訪問cgi腳本文件時讓服務
器程序運行此腳本程序,將程序的輸出作為response發送給客戶。總體的效果,是允許服務器動態的生成回復內容,而不必局限於靜態文件。
支持CGI的服務器程序接收到客戶的請求,根據請求中的URL,運行對應的腳本文件。服務器會將HTTP請求的信息通過環境變量的方式傳遞給腳本文
件,並等待腳本的輸出。腳本的輸出封裝成合法的HTTP回復,發送給客戶。CGI可以充分發揮服務器的可編程性,讓服務器變得“更聰明”。服務器和
CGI 腳本之間的通信要符合CGI標准。CGI的實現方式有很多,比如說使用Apache 服務器與Perl 寫的CGI腳本,或者Python 服務器與shell寫的
CGI 腳本。
為了使用CGI,我們需要使用 BaseHTTPServer 包中的 HTTPServer 類來構建服務器。
# 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。
<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腳本:
#!/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 中的 WSGI
WSGI, Web Server Gateway Interface。WSGI 是Python 對CGI 進行的一種包裝,核心使用Python實現,具體實現通常來說也需要使用Python,目前Django 等web框架都實現了WSGI。
WSGI 接口定義非常簡單,它只要求Web 開發者實現一個函數,就可以響應HTTP請求。來看一個最簡單的Web版本的“Hello, web!”:
def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return '<h1>Hello, web!</h1>'
上面的application()
函數就是符合WSGI標准的一個HTTP處理函數,它接收兩個參數:
-
environ:一個包含所有HTTP請求信息的
dict
對象; -
start_response:一個發送HTTP響應的函數。
在application()
函數中,調用:
start_response('200 OK', [('Content-Type', 'text/html')])
就發送了HTTP響應的Header,注意Header只能發送一次,也就是只能調用一次start_response()
函數。start_response()
函數接收兩個參數,一個是HTTP響應碼,一個是一組list
表示的HTTP頭部,每個Header用一個包含兩個str
的tuple
表示。
Python內置了一個WSGI 服務器,這個模塊叫 wsgiref,它是用純 Python 編寫的WSGI 服務器的參考實現。所謂“參考實現”是指該實現完全符合WSGI 標准,但是不考慮任何運行效率,僅供開發和測試使用。
運行WSGI服務
我們先編寫hello.py
,實現Web應用程序的WSGI 處理函數:
# hello.py def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
然后,再編寫一個server.py
,負責啟動WSGI服務器,加載application()
函數:
# server.py # 從wsgiref模塊導入: from wsgiref.simple_server import make_server # 導入我們自己編寫的application函數: from hello import application # 創建一個服務器,IP地址為空,端口是8000,處理函數是application: httpd = make_server('', 8000, application) print "Serving HTTP on port 8000..." # 開始監聽HTTP請求: httpd.serve_forever()
訪問 http://localhost:8000/simba 會打印 Hello, simba!
四、MVC:Model-View-Controller
(一)中我們需要自己監聽端口,接收http 請求,解析 http 請求,發送http 響應(包括靜態文件和訪問 cgi),就好象實現了一個極簡版的
apache/lighttpd/nginx;
(二/三)中利用已有的 cgi/wsgi 服務器,我們只需要實現如何響應http 請求即可。
但如上的方式需要對每一個不同的請求(參數)都實現一個函數來響應,作為真正的web 后台實現來說肯定是不適用的,我們需要將同個目錄的請求
統一處理。比如一些python web 框架如 web.py 會自己實現一個 wsgi 服務器,並留出接口,讓開發者更好地實現 web 應用的功能,將 url 映射
到各個不同的 python class,在 class 內會定義 GET/POST 方法,用戶訪問url 則由對應的 class 類來處理,如下所示。當然具體展示給用戶的html 肯
定是根據得到的變量值替換而成,即模板化,不同模板有不同的語法。在Jinja2 模板中,用{{ name }}
表示一個需要替換的變量;很多時候,
還需要循環、條件判斷等指令語句,在Jinja2中用{% ... %}
表示指令。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
urls = (
"/index", ViewIndex, "/help", ViewHelp, '/authority/manager', ViewAuthorityView, '/authority/edit', ViewAuthorityEdit, '/authority/delete', ViewAuthorityDelete, '/rule/view', ViewRuleView, '/rule/result', ViewRuleResult, '/rule/edit', ViewRuleEdit, '/rule/edit_advanced', ViewRuleEditAdvanced, '/rule/manager', ViewRuleManager, '/rule/delete', ViewRuleDelete, '/rule/operator', ViewRuleOperator, '/module/show', ViewModuleShow, '/.*', ViewIndex, ) |
這就是傳說中的MVC:Model-View-Controller,中文名“模型-視圖-控制器”。
Python 處理URL的函數就是C:Controller,Controller 負責業務邏輯,比如檢查用戶名是否存在,取出用戶信息等等;
包含變量{{ name }}
的模板就是V:View,View 負責顯示邏輯,通過簡單地替換一些變量,View最終輸出的就是用戶看到的HTML。
MVC 中的Model在哪?Model是用來傳給View 的,這樣View 在替換變量的時候,就可以從Model 中取出相應的數據。
上面的例子中,Model 就是一個dict
:
{ 'name': 'Michael' }
只是因為Python支持關鍵字參數,很多Web框架允許傳入關鍵字參數,然后,在框架內部組裝出一個dict
作為Model。
在實際應用中往往也會把數據庫表的操作認為是Model,通過從請求獲取的信息,進而在庫表查詢出需要替換url 的變量值。
注意:一般一個表一個Model,而且把表操作函數中的表名寫死,這樣如果表有字段變動,只需改動此Model,而無需修改其他調用此表操作
的地方。此外,在一個Web App 中,有多個用戶會同時訪問,假設以多線程模式來處理每個用戶的請求,每個線程在訪問數據庫時都必須創建僅屬於
自身的連接,對別的線程不可見,否則就會造成數據庫操作混亂,此時可能需要用到 threading.local 對象。