HTTP格式
HTTP協議是基於TCP和IP協議的。HTTP協議是一種文本協議。
每個HTTP請求和響應都遵循相同的格式,一個HTTP包含Header和Body兩部分,其中Body是可選的。
HTTP請求格式:
GET:
GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
POST:
POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
Header部分每行用\r\n
換行,每行里鍵名和鍵值之間以:
分割,注意冒號后有個空格。
當遇到\r\n\r\n
時,Header部分結束,后面的數據全部是Body。
HTTP響應格式:
200 OK
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
HTTP響應如果包含body,也是通過\r\n\r\n
來分隔的。
請再次注意,Body的數據類型由
Content-Type
頭來確定,如果是網頁,Body就是文本,如果是圖片,Body就是圖片的二進制數據。
Body數據是可以被壓縮的,如果看到Content-Encoding
,說明網站使用了壓縮。最常見的壓縮方式是gzip。
WSGI接口
了解了HTTP協議的格式后,我們可以理解一個Web應用的本質:
1、瀏覽器發送HTTP請求給服務器;
2、服務器接收請求后,生成HTML;
3、服務器把生成的HTML作為HTTP響應的body返回給瀏覽器;
4、瀏覽器接收到HTTP響應后,解析HTTP里body並顯示。
接受HTTP請求、解析HTTP請求、發送HTTP響應實現起來比較復雜,有專門的服務器軟件來實現,例如Nginx,Apache。我們要做的就是專注於生成HTML文檔。
Python里也提供了一個比較底層的WSGI
(Web Server Gateway Interface)接口來實現TCP連接、HTTP原始請求和響應格式。實現了該接口定義的內容,就可以實現類似Nginx、Apache等服務器的功能。
WSGI
接口定義要求Web開發者實現一個函數,就可以響應HTTP請求,示例:
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return [b'<h1>Hello, web!</h1>']
這是一個簡單的文本版本的Hello, web!
。
上面的application()
函數就是符合WSGI
標准的一個HTTP處理函數,它接收兩個參數:
environ:一個包含所有HTTP請求信息的dict對象;
start_response:一個發送HTTP響應的函數。
有了WSGI,我們關心的就是如何從environ
這個dict對象拿到HTTP請求信息,然后構造HTML,通過start_response()
發送Header,最后返回Body。
整個application()
函數本身沒有涉及到任何解析HTTP的部分,即底層代碼不需要自己編寫,只負責在更高層次上考慮如何響應請求就可以了。
但是,application()
函數由誰來調用呢?因為這里的參數environ
、start_response
我們沒法提供,返回的bytes也沒法發給瀏覽器。
application()
函數必須由WSGI
服務器來調用。
有很多符合WSGI規范的服務器,Python提供了一個最簡單的WSGI
服務器,可以把我們的Web應用程序跑起來。這個模塊叫wsgiref
,它是用純Python編寫的WSGI
服務器的參考實現。所謂“參考實現”是指該實現完全符合WSGI
標准,但是不考慮任何運行效率,僅供開發和測試使用。
運行WSGI服務
有了wsgiref
,我們可以非常快的實現一個簡單的web服務器:
# coding: utf-8
from wsgiref.simple_server import make_server
def application(environ, start_response):
print(environ)
start_response('200 OK', [('Content-Type', 'text/html')])
return [b'<h1>Hello web!</h1>']
print('HTTP server is running on http://127.0.0.1:9999')
# 創建一個服務器,IP地址可以為空,端口是9999,處理函數是application:
httpd = make_server('', 9999, application)
httpd.serve_forever()
運行后訪問http://127.0.0.1:9999/
,會看到:
Hello web!
擴展知識:
make_server()
里第一個參數如果為空,實際等效於0.0.0.0
,表示監聽本地所有ip地址(包括127.0.0.1
)。
通過Chrome瀏覽器的控制台,我們可以查看到瀏覽器請求和服務器響應信息:
# 請求信息:
GET / HTTP/1.1
Host: 127.0.0.1:9999
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: _ga=GA1.1.948200530.1463673425
# 響應信息:
HTTP/1.0 200 OK
Date: Sun, 12 Feb 2017 05:20:31 GMT
Server: WSGIServer/0.2 CPython/3.4.3
Content-Type: text/html
Content-Length: 19
<h1>Hello web!</h1>
我們再看終端的輸出信息:
$ python user_wsgiref_server.py
HTTP server is running on http://127.0.0.1:9999
127.0.0.1 - - [12/Feb/2017 13:18:38] "GET / HTTP/1.1" 200 19
127.0.0.1 - - [12/Feb/2017 13:18:39] "GET /favicon.ico HTTP/1.1" 200 19
如果我們打印environ
參數信息,會看到如下值:
{
"SERVER_SOFTWARE": "WSGIServer/0.1 Python/2.7.5",
"SCRIPT_NAME": "",
"REQUEST_METHOD": "GET",
"SERVER_PROTOCOL": "HTTP/1.1",
"HOME": "/root",
"LANG": "en_US.UTF-8",
"SHELL": "/bin/bash",
"SERVER_PORT": "9999",
"HTTP_HOST": "dev.banyar.cn:9999",
"HTTP_UPGRADE_INSECURE_REQUESTS": "1",
"XDG_SESSION_ID": "64266",
"HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"wsgi.version": "0",
"wsgi.errors": "",
"HOSTNAME": "localhost",
"HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.8,en;q=0.6",
"PATH_INFO": "/",
"USER": "root",
"QUERY_STRING": "",
"PATH": "/usr/local/php/bin:/usr/local/php/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin",
"HTTP_USER_AGENT": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"HTTP_CONNECTION": "keep-alive",
"SERVER_NAME": "localhost",
"REMOTE_ADDR": "192.168.0.101",
"wsgi.url_scheme": "http",
"CONTENT_LENGTH": "",
"GATEWAY_INTERFACE": "CGI/1.1",
"CONTENT_TYPE": "text/plain",
"REMOTE_HOST": "",
"HTTP_ACCEPT_ENCODING": "gzip, deflate, sdch"
}
為顯示方便,已精簡部分信息。有了環境變量信息,我們可以對程序做些修改,可以動態顯示內容:
def application(environ, start_response):
print(environ['PATH_INFO'])
start_response('200 OK', [('Content-Type', 'text/html')])
body = '<h1>Hello %s!</h1>' % (environ['PATH_INFO'][1:] or 'web' )
return [body.encode('utf-8')]
以上使用了environ
里的PATH_INFO
的值。我們在瀏覽器輸入http://127.0.0.1:9999/python
,瀏覽器會顯示:
Hello python!
終端的輸出信息:
$ python user_wsgiref_server.py
HTTP server is running on http://127.0.0.1:9999
/python
127.0.0.1 - - [12/Feb/2017 13:54:57] "GET /python HTTP/1.1" 200 22
/favicon.ico
127.0.0.1 - - [12/Feb/2017 13:54:58] "GET /favicon.ico HTTP/1.1" 200 27
web框架
實際項目開發中,我們不可能使用swgiref
來實現服務器,因為WSGI提供的接口雖然比HTTP接口高級了不少,但和Web App的處理邏輯比,還是比較低級。我們需要使用成熟的web框架。
由於用Python開發一個Web框架十分容易,所以Python有上百個開源的Web框架。部分流行框架:
Flask:輕量級Web應用框架;
Django:全能型Web框架;
web.py:一個小巧的Web框架;
Bottle:和Flask類似的Web框架;
Tornado:Facebook的開源異步Web框架
Flask
Flask是一個使用 Python 編寫的輕量級 Web 應用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎則使用 Jinja2 。
安裝非常簡單:
pip install flask
控制台輸出:
Collecting flask
Downloading Flask-0.12-py2.py3-none-any.whl (82kB)
100% |████████████████████████████████| 92kB 163kB/s
Collecting itsdangerous>=0.21 (from flask)
Downloading itsdangerous-0.24.tar.gz (46kB)
100% |████████████████████████████████| 51kB 365kB/s
Collecting click>=2.0 (from flask)
Downloading click-6.7-py2.py3-none-any.whl (71kB)
100% |████████████████████████████████| 71kB 349kB/s
Collecting Jinja2>=2.4 (from flask)
Downloading Jinja2-2.9.5-py2.py3-none-any.whl (340kB)
100% |████████████████████████████████| 348kB 342kB/s
Collecting Werkzeug>=0.7 (from flask)
Downloading Werkzeug-0.11.15-py2.py3-none-any.whl (307kB)
100% |████████████████████████████████| 317kB 194kB/s
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
Downloading MarkupSafe-0.23.tar.gz
Building wheels for collected packages: itsdangerous, MarkupSafe
Running setup.py bdist_wheel for itsdangerous ... done
Successfully built itsdangerous MarkupSafe
Installing collected packages: itsdangerous, click, MarkupSafe, Jinja2, Werkzeug, flask
Successfully installed Jinja2-2.9.5 MarkupSafe-0.23 Werkzeug-0.11.15 click-6.7 flask-0.12 itsdangerous-0.24
安裝完flask會同時安裝依賴模塊:itsdangerous
, click
, MarkupSafe
, Jinja2
, Werkzeug
。
現在我們來寫個簡單的登錄功能,主要是三個頁面:
- 首頁,顯示
home
字樣; - 登錄頁,地址
/login
,有登錄表單; - 登錄后的歡迎頁面,如果登錄成功,提示歡迎語,否則提示用戶名不正確。
那么一共有3個URL:
- GET /:首頁,返回Home;
- GET /login:登錄頁,顯示登錄表單;
- POST /login:處理登錄表單,顯示登錄結果。
user_flask_app.py
# coding: utf-8
from flask import Flask
from flask import request
app = Flask(__name__)
# 首頁
@app.route('/', methods=['GET', 'POST'])
def home():
return '<h1>Home</h1><p><a href="/login">去登錄</a></p>'
# 登錄頁
@app.route('/login', methods=['get'])
def login():
return '''<form action="/login" method="post">
<p>用戶名:<input name="username"></p>
<p>密碼:<input name="password" type="password"></p>
<p><button type="submit">登錄</button></p>
</form>'''
# 登錄頁處理
@app.route('/login', methods=['post'])
def do_login():
# 從request對象讀取表單內容:
param = request.form
if(param['username'] == 'yjc' and param['password'] == 'yjc'):
return '歡迎您 %s !' % param['username']
else:
return '用戶名或密碼不正確。'
pass
if __name__ == '__main__':
# run()方法參數可以都為空,使用默認值
app.run('', 5000)
我們可以打開:http://localhost:5000/ 看效果。實際的Web App應該拿到用戶名和口令后,去數據庫查詢再比對,來判斷用戶是否能登錄成功。
通過代碼我們可以發現,Flask通過Python的裝飾器在內部自動地把URL和函數給關聯起來。
注意代碼里同一個URL/login
分別有GET
和POST
兩種請求,可以映射到兩個處理函數中。
使用模板
Web框架讓我們從編寫底層WSGI接口拯救出來了,極大的提高了我們編寫程序的效率。
但代碼里嵌套太多的html讓整個代碼易讀性變差,使程序變得復雜。我們需要將后端代碼邏輯與前端html分離出來。這就是傳說中的MVC
:Model-View-Controller,中文名“模型-視圖-控制器”。
Controlle
r負責業務邏輯,比如檢查用戶名是否存在,取出用戶信息等等;
View
負責顯示邏輯,通過簡單地替換一些變量,View最終輸出的就是用戶看到的HTML。
'Model'負責數據的獲取,如從數據庫查詢用戶信息等。Model簡單可以理解為數據。
那么就是:Model
獲取數據,Controlle
處理業務邏輯,View
顯示數據。
現在,我們把上次直接輸出字符串作為HTML的例子用MVC模式改寫一下:
# coding: utf-8
from flask import Flask,request,render_template
app = Flask(__name__)
# 首頁
@app.route('/', methods=['GET', 'POST'])
def home():
return render_template('home.html')
# 登錄頁
@app.route('/login', methods=['get'])
def login():
return render_template('login.html', param = [])
# 登錄頁處理
@app.route('/login', methods=['post'])
def do_login():
param = request.form
if(param['username'] == 'yjc' and param['password'] == 'yjc'):
return render_template('welcome.html', username = param['username'])
else:
return render_template('login.html', msg = '用戶名或密碼不正確。', param = param)
pass
if __name__ == '__main__':
app.run('', 5000)
Flask通過render_template()
函數來實現模板的渲染。和Web框架類似,Python的模板也有很多種。Flask默認支持的模板是jinja2
。
模板頁面:
home.html
<h1>Home</h1><p><a href="/login">去登錄</a></p>
login.html
{% if msg %}
<p style="color:red;">{{ msg }}</p>
{% endif %}
<form action="/login" method="post">
<p>用戶名:<input name="username" value="{{ param.username }}"></p>
<p>密碼:<input name="password" type="password"></p>
<p><button type="submit">登錄</button></p>
</form>
welcome.html
<p>歡迎您, {{ username }} !</p>
項目目錄:
user_flask_app
|-- templates
|-- home.html
|-- login.html
|-- welcome.html
|-- user_flask_app.py
render_template()
函數第一個參數是模板名,默認是templates
目錄下。后面的參數是傳給模板的變量。變量的值可以是數字、字符串、列表等等。
在Jinja2模板中,我們用{{ name }}
表示一個需要替換的變量。很多時候,還需要循環、條件判斷等指令語句,在Jinja2中,用{% ... %}
表示指令。
比如循環輸出頁碼:
{% for i in page_list %}
<a href="/page/{{ i }}">{{ i }}</a>
{% endfor %}
除了Jinja2
,常見的模板還有:
Mako:用<% ... %>和${xxx}的一個模板;
Cheetah:也是用<% ... %>和${xxx}的一個模板;
Django:Django是一站式框架,內置一個用{% ... %}和{{ xxx }}的模板。