請(qǐng)求從 Nginx 到 uwsgi 到 django 交互概覽
作為python web開(kāi)發(fā),,我們首先要弄清楚,,到底一個(gè)請(qǐng)求過(guò)來(lái),,發(fā)生了什么事,,請(qǐng)求的傳遞是怎么樣完成的,由nginx是怎么轉(zhuǎn)發(fā)到uwsgi, uwsgi又是怎樣把請(qǐng)求傳給到我們的框架(django or falsk)由我們自己寫(xiě)的代碼處理,,返回?cái)?shù)據(jù)給客戶(hù)端的,。因此我作了以下一個(gè)粗略的流程圖:
uwsgi 處理過(guò)程.png
以下我會(huì)逐個(gè)步驟從下往上詳細(xì)講解,并附上代碼和配置,,
WSGI 協(xié)議
從上面的圖看得出 wsgi server (比如uwsgi) 要和 wsgi application(比如django )交互,,uwsgi需要將過(guò)來(lái)的請(qǐng)求轉(zhuǎn)給django 處理,那么uwsgi 和 django的交互和調(diào)用就需要一個(gè)統(tǒng)一的規(guī)范,,這個(gè)規(guī)范就是WSGI WSGI(Web Server Gateway Interface) ,,WSGI是 Python PEP333中提出的一個(gè) Web 開(kāi)發(fā)統(tǒng)一規(guī)范。
Web 應(yīng)用的開(kāi)發(fā)通常都會(huì)涉及到 Web 框架(django, flask)的使用,,各個(gè) Web 框架內(nèi)部由于實(shí)現(xiàn)不同相互不兼容,,給用戶(hù)的學(xué)習(xí),使用和部署造成了很多麻煩,。 正是有了WSGI這個(gè)規(guī)范,,它約定了wsgi server 怎么調(diào)用web應(yīng)用程序的代碼,,web 應(yīng)用程序需要符合什么樣的規(guī)范,,只要 web 應(yīng)用程序和 wsgi server 都遵守 WSGI 協(xié)議,那么,,web 應(yīng)用程序和 wsgi server就可以隨意的組合,。 比如uwsgi+django , uwsgi+flask, gunicor+django, gunicor+flask 這些的組合都可以任意組合,,因?yàn)樗麄冏裱薟SGI規(guī)范。
WSGI 標(biāo)準(zhǔn)
WSGI 標(biāo)準(zhǔn)中主要定義了兩種角色:
- “server” 或 “gateway” 端
- “application” 或 “framework” 端
wsgi.png
為了方便理解,,我們可以把server具體成 uwsgi,, application具體成django
這里可以看到,WSGI 服務(wù)器需要調(diào)用應(yīng)用程序的一個(gè)可調(diào)用對(duì)象,,這個(gè)可調(diào)用對(duì)象(callable object)可以是一個(gè)函數(shù),,方法,類(lèi)或者可調(diào)用的實(shí)例,,總之是可調(diào)用的,。
下面是一個(gè) callable object 的示例,這里的可調(diào)用對(duì)象是一個(gè)函數(shù):
def simple_app(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/html')]
start_response(status, response_headers)
return ['Hello World']
這里,,我們首先要注意,,這個(gè)對(duì)象接收兩個(gè)參數(shù):
environ :請(qǐng)求的環(huán)境變量,它是一個(gè)字典,,包含了客戶(hù)端請(qǐng)求的信息,,如 HTTP 請(qǐng)求的首部,方法等信息,,可以認(rèn)為是請(qǐng)求上下文,,
start_response :一個(gè)用于發(fā)送HTTP響應(yīng)狀態(tài)(HTTP status ),、響應(yīng)頭(HTTP headers)的回調(diào)函數(shù)。在返回內(nèi)容之前必須先調(diào)用這個(gè)回掉函數(shù)
上面的 start_response 這個(gè)回調(diào)函數(shù)的作用是用于讓 WSGI Server 返回響應(yīng)的 HTTP 首部和 HTTP 狀態(tài)碼,。這個(gè)函數(shù)有兩個(gè)必須的參數(shù),,返回的狀態(tài)碼和返回的響應(yīng)首部組成的元祖列表。返回狀態(tài)碼和首部的這個(gè)操作始終都應(yīng)該在響應(yīng) HTTP body 之前執(zhí)行,。
還需要注意的是,,最后的返回結(jié)果,應(yīng)該是一個(gè)可迭代對(duì)象,,這里是將返回的字符串放到列表里,。如果直接返回字符串可能導(dǎo)致 WSGI 服務(wù)器對(duì)字符串進(jìn)行迭代而影響響應(yīng)速度。
當(dāng)然,,這個(gè)函數(shù)是一個(gè)最簡(jiǎn)單的可調(diào)用對(duì)象,,它也可以是一個(gè)類(lèi)或者可調(diào)用的類(lèi)實(shí)例。
WSGI 實(shí)例
- wsgi application 的代碼 app.py
def application(env, start_response):
start_response('200 OK', [('Content-Type', 'text/html'), ('X-Coder', 'Cooffeeli')])
return ['<h1>你好??!世界</h1>']
- wsgi server 代碼 wsgi_server.py
我們可以借助 python 的 wsgiref 庫(kù)運(yùn)行一個(gè) WSGI 服務(wù)器(當(dāng)然這個(gè) WSGI 服務(wù)器同時(shí)也是 Web 服務(wù)器),用它來(lái)運(yùn)行我們的 application
from wsgiref.simple_server import make_server
from app import application
# 啟動(dòng) WSGI 服務(wù)器
httpd = make_server (
'localhost',
9000,
application # 這里指定我們的 application object)
)
# 開(kāi)始處理請(qǐng)求
httpd.handle_request()
python wsgiref_server.py
運(yùn)行上面的程序,,并訪(fǎng)問(wèn) http://localhost:9000 ,, 將返回此次請(qǐng)求所有的首部信息。 這里,,我們利用 environ 字典,,獲取了請(qǐng)求中所有的變量信息,構(gòu)造成相應(yīng)的內(nèi)容返回給客戶(hù)端,。 environ 這個(gè)參數(shù)中包含了請(qǐng)求的首部,,URL,請(qǐng)求的地址,,請(qǐng)求的方法等信息,。可以參考 PEP3333來(lái)查看 environ 字典中必須包含哪些 CGI 變量,。
自己實(shí)現(xiàn)WSGI Server
既然我們知道了WSGI的規(guī)范,,我們完全可以自己實(shí)現(xiàn)一個(gè)WSGI Server 根據(jù)這個(gè)規(guī)范,我們可以總結(jié)WSGI Server需要實(shí)現(xiàn)以下功能:
- 監(jiān)聽(tīng)端口,,接收請(qǐng)求
- 接受HTTP請(qǐng)求后,,解析HTTP協(xié)議
- 根據(jù)HTTP內(nèi)容,生成
env 參數(shù),,該參數(shù)包括HTTP,wsgi信息,,可以看作是請(qǐng)求上下文
- 實(shí)現(xiàn)一個(gè)
start_response 函數(shù),,作為調(diào)用application的參數(shù),,用作application回調(diào)函數(shù),負(fù)責(zé)http相應(yīng)頭
實(shí)現(xiàn)代碼: WSGIServer.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import sys
import StringIO
from app import application
from datetime import datetime
class WSGIServer(object):
def __init__(self, server_address):
"""初始構(gòu)造函數(shù), 創(chuàng)建監(jiān)聽(tīng)socket"""
self.listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.listen_sock.bind(server_address)
self.listen_sock.listen(5)
(host, port) = self.listen_sock.getsockname()
self.server_port = port
self.server_name = socket.getfqdn(host)
def set_application(self, application):
"""設(shè)置wsgi application, 供server 調(diào)用"""
self.application = application
def get_environ(self):
"""構(gòu)造WSGI環(huán)境變量,,傳給application的env參數(shù)"""
self.env = {
'wsgi.version': (1, 0),
'wsgi.url_scheme': 'http',
'wsgi.errors': sys.stderr,
'wsgi.multithread': False,
'wsgi.run_once': False,
'REQUEST_METHOD': self.request_method,
'PATH_INFO': self.request_path,
'SERVER_NAME': self.server_name,
'SERVER_PORT': str(self.server_port),
'wsgi.input': StringIO.StringIO(self.request_data),
}
return self.env
def start_response(self, http_status, http_headers):
"""構(gòu)造WSGI響應(yīng),, 傳給application的start_response"""
self.http_status = http_status
self.http_headers = dict(http_headers)
headers = {
'Date': datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
'Server': 'WSGIServer 1.0'
}
self.http_headers.update(headers)
def parse_request(self, text):
"""獲取http頭信息,用于構(gòu)造env參數(shù)"""
request_line = text.splitlines()[0]
request_info = request_line.split(' ')
(self.request_method,
self.request_path,
self.request_version) = request_info
def get_http_response(self, response_data):
"""完成response 內(nèi)容"""
res = 'HTTP/1.1 {status} \r\n'.format(status=self.http_status)
for header in self.http_headers.items():
res += '{0}: {1} \r\n'.format(*header)
res += '\r\n'
res_body = ''
for val in response_data:
res_body += val
res += res_body
return res
def handle_request(self):
"""處理請(qǐng)求"""
# 初始版本,,只接受一個(gè)請(qǐng)求
conn, addr = self.listen_sock.accept()
# 獲取http 請(qǐng)求的request內(nèi)容
self.request_data = conn.recv(1024)
self.parse_request(self.request_data)
# 構(gòu)造調(diào)用application需要的兩個(gè)參數(shù) env, start_response
env = self.get_environ()
start_response = self.start_response
# 調(diào)用application, 并獲取需要返回的http response內(nèi)容
response_data = self.application(env, start_response)
# 獲取完整http response header 和 body, 通過(guò)socket的sendall返回到客戶(hù)端
res = self.get_http_response(response_data)
conn.sendall(res)
# 腳本運(yùn)行完畢也會(huì)結(jié)束
conn.close()
def make_server(server_address, application):
"""創(chuàng)建WSGI Server 負(fù)責(zé)監(jiān)聽(tīng)端口,,接受請(qǐng)求"""
wsgi_server = WSGIServer(server_address)
wsgi_server.set_application(application)
return wsgi_server
SERVER_ADDRESS = (HOST, PORT) = '', 8124
wsgi_server = make_server(SERVER_ADDRESS, application)
wsgi_server.handle_request()
上面的 WSGI 服務(wù)器運(yùn)行過(guò)程為:
- 初始化,創(chuàng)建套接字,,綁定端口
- 接收客戶(hù)端請(qǐng)求
- 解析 HTTP 協(xié)議
- 構(gòu)造 WSGI 環(huán)境變量(environ)
- 調(diào)用 application
- 回調(diào)函數(shù) start_response 設(shè)置好響應(yīng)的狀態(tài)碼和首部
- 返回信息
至此,, wsgi server -> wsgi application 的交互講解完畢, 下面我們繼續(xù)看nginx->uwsgi交互過(guò)程
啟動(dòng) uwsgi
上面說(shuō)了我們自己實(shí)現(xiàn)WSGI Server的過(guò)程,,現(xiàn)在我們用uwsgi 來(lái)作為Server 運(yùn)行監(jiān)聽(tīng)請(qǐng)求uwsgi
uwsgi --http :9090 --wsgi-file foobar.py --master --processes 4 --threads 2
執(zhí)行這個(gè)命令會(huì)產(chǎn)生4個(gè)uwsgi進(jìn)程(每個(gè)進(jìn)程2個(gè)線(xiàn)程),,1個(gè)master進(jìn)程,當(dāng)有子進(jìn)程死掉時(shí)再產(chǎn)生子進(jìn)程,,1個(gè) the HTTP router進(jìn)程,,一個(gè)6個(gè)進(jìn)程。
這個(gè)Http route進(jìn)程的地位有點(diǎn)類(lèi)似nginx,,(可以認(rèn)為與nginx同一層)負(fù)責(zé)路由http請(qǐng)求給worker, Http route進(jìn)程和worker之間使用的是uwsgi協(xié)議
FastCgi協(xié)議,, uwsgi協(xié)議, http協(xié)議有什么用,?
在構(gòu)建 Web 應(yīng)用時(shí),,通常會(huì)有 Web Server (nginx)和 Application Server(wsgi server eg:uwsgi) 兩種角色。其中 Web Server 主要負(fù)責(zé)接受來(lái)自用戶(hù)的請(qǐng)求,,解析 HTTP 協(xié)議,,并將請(qǐng)求轉(zhuǎn)發(fā)給 Application Server,Application Server 主要負(fù)責(zé)處理用戶(hù)的請(qǐng)求,,并將處理的結(jié)果返回給 Web Server,,最終 Web Server 將結(jié)果返回給用戶(hù)。
由于有很多動(dòng)態(tài)語(yǔ)言和很多種 Web Server,,他們彼此之間互不兼容,,給程序員造成了很大的麻煩。因此就有了 CGI/FastCGI ,,uwsgi 協(xié)議,,定義了 Web Server 如何通過(guò)輸入輸出與 Application Server 進(jìn)行交互,將 Web 應(yīng)用程序的接口統(tǒng)一了起來(lái),。
總而言之,, 這些協(xié)議就是進(jìn)程交互的一種溝通方式。 舉個(gè)例子:美國(guó)人和中國(guó)人溝通必須要有一個(gè)公共的語(yǔ)言:英語(yǔ),, 這時(shí)候英語(yǔ)就是兩個(gè)人溝通的協(xié)議,, 不然,,一個(gè)說(shuō)英語(yǔ)(uwsgi協(xié)議), 一個(gè)說(shuō)中文(fastcgi協(xié)議)是肯定會(huì)亂碼的,,處理不成功的,。用同一個(gè)協(xié)議,大家都知道該如何解析過(guò)來(lái)的內(nèi)容,。 所以,,nginx 和 uwsgi交互就必須使用同一個(gè)協(xié)議,而上面說(shuō)了uwsgi支持fastcgi,uwsgi,http協(xié)議,,這些都是nginx支持的協(xié)議,,只要大家溝通好使用哪個(gè)協(xié)議,就可以正常運(yùn)行了,。
將uwsgi 放在nginx 后面
將uwsgi 放在nginx后面,,讓nginx反向代理請(qǐng)求到uwsgi
uwsgi 原生支持HTTP, FastCGI,, SCGI,,以及特定的uwsgi協(xié)議, 性能最好的明顯時(shí)uwsgi, 這個(gè)協(xié)議已經(jīng)被nginx支持,。
所以u(píng)wsgi 配置使用哪個(gè)協(xié)議,,nginx 要使用對(duì)應(yīng)協(xié)議
# 使用http協(xié)議
uwsgi --http-socket 127.0.0.1:9000 --wsgi-file app.py
# nginx配置
lcation / {
proxy_pass 127.0.0.1:9000;
}
更多協(xié)議
[uwsgi]
# 使用uwsgi協(xié)議 socket, uwsgi-socket 都是uwsgi協(xié)議
# bind to the specified UNIX/TCP socket using default protocol
# UNIX/TCP 意思時(shí)可以UNIX: xx.sock, 或者 TCP: 127.0.0.1:9000 他們是都可以的
# UNIX 沒(méi)有走TCP協(xié)議,不是面向連接, 而是直接走文件IO
# nginx 使用uwsgi_pass
socket = 127.0.0.1:9000
socket = /dev/shm/owan_web_uwsgi.sock
uwsgi-socket = /dev/shm/owan_web_uwsgi.sock
# nginx 使用 uwsgi_pass /dev/shm/owan_web_uwsgi.sock;
# 使用fastcgi協(xié)議 fastcgi-socket
# bind to the specified UNIX/TCP socket using FastCGI protocol
# nginx 就可以好象PHP那樣配置 使用fastcgi_pass
fastcgi-socket = /dev/shm/owan_web_uwsgi.sock
# nginx 使用fastcgi_pass /dev/shm/owan_web_uwsgi.sock;
# 使用http協(xié)議 http-socket
# bind to the specified UNIX/TCP socket using HTTP protocol
# nginx 使用proxy_pass
# 原來(lái)proxy_pass 是http協(xié)議,,但不一定要用TCP
# proxy_pass http://unix:/dev/shm/owan_web_uwsgi.sock;
http-socket = /dev/shm/owan_web_uwsgi.sock
# nginx 使用 proxy_pass /dev/shm/owan_web_uwsgi.sock;
chdir = /data/web/advance_python/uwsgi/
wsgi-file = app.py
processes = 4
threads = 2
master = true
...
結(jié)束
至此,,nginx ->uwsgi ->web 框架 以及 WSGI的相關(guān)知識(shí)已經(jīng)講解完了。 需要補(bǔ)充的是,,我們自己實(shí)現(xiàn)的WSGI Server只能支持一個(gè)請(qǐng)求,,在之后的日子,我會(huì)再寫(xiě)一些教程,,關(guān)于socket IO 復(fù)用 和線(xiàn)程池 讓我們自己寫(xiě)server支持多請(qǐng)求,,多并發(fā)的功能
|