https://blog.csdn.net/a519640026/article/details/76157976
請求從 Nginx 到 uwsgi 到 django 交互概覽
作為python web開發,我們首先要弄清楚,到底一個請求過來,發生了什么事,請求的傳遞是怎么樣完成的,由nginx是怎么轉發到uwsgi, uwsgi又是怎樣把請求傳給到我們的框架(django or falsk)由我們自己寫的代碼處理,返回數據給客戶端的。因此我作了以下一個粗略的流程圖:
WSGI 協議
從上面的圖看得出 wsgi server (比如uwsgi) 要和 wsgi application(比如django )交互,uwsgi需要將過來的請求轉給django 處理,那么uwsgi 和 django的交互和調用就需要一個統一的規范,這個規范就是WSGI WSGI(Web Server Gateway Interface) ,WSGI是 Python PEP333中提出的一個 Web 開發統一規范
Web 應用的開發通常都會涉及到 Web 框架(django, flask)的使用,各個 Web 框架內部由於實現不同相互不兼容,給用戶的學習,使用和部署造成了很多麻煩。
正是有了WSGI這個規范,它約定了wsgi server 怎么調用web應用程序的代碼,web 應用程序需要符合什么樣的規范,只要 web 應用程序和 wsgi server 都遵守 WSGI 協議,那么,web 應用程序和 wsgi server就可以隨意的組合。 比如uwsgi+django , uwsgi+flask, gunicor+django, gunicor+flask 這些的組合都可以任意組合,因為他們遵循了WSGI規范。
WSGI 標准
- “server” 或 “gateway” 端
- “application” 或 “framework” 端
為了方便理解,我們可以把server具體成 uwsgi, application具體成django
這里可以看到,WSGI 服務器需要調用應用程序的一個可調用對象,這個可調用對象(callable object)可以是一個函數,方法,類或者可調用的實例,總之是可調用的。
下面是一個 callable object 的示例,這里的可調用對象是一個函數:
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']
這里,我們首先要注意,這個對象接收兩個參數:
environ:請求的環境變量,它是一個字典,包含了客戶端請求的信息,如 HTTP 請求的首部,方法等信息,可以認為是請求上下文,
start_response:一個用於發送HTTP響應狀態(HTTP status )、響應頭(HTTP headers)的回調函數。在返回內容之前必須先調用這個回掉函數
上面的 start_response 這個回調函數的作用是用於讓 WSGI Server 返回響應的 HTTP 首部和 HTTP 狀態碼。這個函數有兩個必須的參數,返回的狀態碼和返回的響應首部組成的元祖列表。返回狀態碼和首部的這個操作始終都應該在響應 HTTP body 之前執行。
還需要注意的是,最后的返回結果,應該是一個可迭代對象,這里是將返回的字符串放到列表里。如果直接返回字符串可能導致 WSGI 服務器對字符串進行迭代而影響響應速度。
當然,這個函數是一個最簡單的可調用對象,它也可以是一個類或者可調用的類實例。
WSGI 實例<應用端代碼 (web application)>
- 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 庫運行一個 WSGI 服務器(當然這個 WSGI 服務器同時也是 Web 服務器),用它來運行我們的 application
1 from wsgiref.simple_server import make_server 2 from app import application 3 4 # 啟動 WSGI 服務器 5 httpd = make_server ( 6 'localhost', 7 9000, 8 application # 這里指定我們的 application object) 9 ) 10 # 開始處理請求 11 httpd.handle_request() 12
python wsgiref_server.py
運行上面的程序,並訪問 http://localhost:9000 , 將返回此次請求所有的首部信息。
這里,我們利用 environ 字典,獲取了請求中所有的變量信息,構造成相應的內容返回給客戶端。
environ 這個參數中包含了請求的首部,URL,請求的地址,請求的方法等信息。可以參考 PEP3333來查看 environ 字典中必須包含哪些 CGI 變量。
自己實現WSGI Server
既然我們知道了WSGI的規范,我們完全可以自己實現一個WSGI Server
根據這個規范,我們可以總結WSGI Server需要實現以下功能:
監聽端口,接收請求
接受HTTP請求后,解析HTTP協議
根據HTTP內容,生成env參數,該參數包括HTTP,wsgi信息,可以看作是請求上下文
實現一個start_response函數,作為調用application的參數,用作application回調函數,負責http相應頭
實現代碼: WSGIServer.py
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 import socket 4 import sys 5 import StringIO 6 from app import application 7 from datetime import datetime 8 9 class WSGIServer(object): 10 11 def __init__(self, server_address): 12 """初始構造函數, 創建監聽socket""" 13 self.listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 14 self.listen_sock.bind(server_address) 15 self.listen_sock.listen(5) 16 (host, port) = self.listen_sock.getsockname() 17 self.server_port = port 18 self.server_name = socket.getfqdn(host) 19 20 21 def set_application(self, application): 22 """設置wsgi application, 供server 調用""" 23 self.application = application 24 25 26 def get_environ(self): 27 """構造WSGI環境變量,傳給application的env參數""" 28 self.env = { 29 'wsgi.version': (1, 0), 30 'wsgi.url_scheme': 'http', 31 'wsgi.errors': sys.stderr, 32 'wsgi.multithread': False, 33 'wsgi.run_once': False, 34 'REQUEST_METHOD': self.request_method, 35 'PATH_INFO': self.request_path, 36 'SERVER_NAME': self.server_name, 37 'SERVER_PORT': str(self.server_port), 38 'wsgi.input': StringIO.StringIO(self.request_data), 39 } 40 return self.env 41 42 43 def start_response(self, http_status, http_headers): 44 """構造WSGI響應, 傳給application的start_response""" 45 self.http_status = http_status 46 self.http_headers = dict(http_headers) 47 headers = { 48 'Date': datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'), 49 'Server': 'WSGIServer 1.0' 50 } 51 self.http_headers.update(headers) 52 53 54 def parse_request(self, text): 55 """獲取http頭信息,用於構造env參數""" 56 request_line = text.splitlines()[0] 57 request_info = request_line.split(' ') 58 (self.request_method, 59 self.request_path, 60 self.request_version) = request_info 61 62 63 def get_http_response(self, response_data): 64 """完成response 內容""" 65 res = 'HTTP/1.1 {status} \r\n'.format(status=self.http_status) 66 for header in self.http_headers.items(): 67 res += '{0}: {1} \r\n'.format(*header) 68 69 res += '\r\n' 70 71 res_body = '' 72 for val in response_data: 73 res_body += val 74 75 res += res_body 76 77 return res 78 79 80 def handle_request(self): 81 """處理請求""" 82 # 初始版本,只接受一個請求 83 conn, addr = self.listen_sock.accept() 84 85 # 獲取http 請求的request內容 86 self.request_data = conn.recv(1024) 87 self.parse_request(self.request_data) 88 89 # 構造調用application需要的兩個參數 env, start_response 90 env = self.get_environ() 91 start_response = self.start_response 92 93 # 調用application, 並獲取需要返回的http response內容 94 response_data = self.application(env, start_response) 95 96 # 獲取完整http response header 和 body, 通過socket的sendall返回到客戶端 97 res = self.get_http_response(response_data) 98 conn.sendall(res) 99 100 # 腳本運行完畢也會結束 101 conn.close() 102 103 104 def make_server(server_address, application): 105 """創建WSGI Server 負責監聽端口,接受請求""" 106 wsgi_server = WSGIServer(server_address) 107 wsgi_server.set_application(application) 108 109 return wsgi_server 110 111 112 SERVER_ADDRESS = (HOST, PORT) = '', 8124 113 wsgi_server = make_server(SERVER_ADDRESS, application) 114 wsgi_server.handle_request() 115
上面的 WSGI 服務器運行過程為:
- 初始化,創建套接字,綁定端口
- 接收客戶端請求
- 解析 HTTP 協議
- 構造 WSGI 環境變量(environ)
- 調用 application
- 回調函數 start_response 設置好響應的狀態碼和首部
- 返回信息
WSGI SERVER---> WSGI APPLICATION
至此, wsgi server -> wsgi application 的交互講解完畢, 下面我們繼續看nginx->uwsgi交互過程
啟動 uwsgi
上面說了我們自己實現WSGI Server的過程,現在我們用uwsgi 來作為Server
運行監聽請求uwsgi
uwsgi --http :9090 --wsgi-file foobar.py --master --processes 4 --threads 2
執行這個命令會產生4個uwsgi進程(每個進程2個線程),1個master進程,當有子進程死掉時再產生子進程,1個 the HTTP router進程,一個6個進程。
這個Http route進程的地位有點類似nginx,(可以認為與nginx同一層)負責路由http請求給worker, Http route進程和worker之間使用的是uwsgi協議
FastCgi協議, uwsgi協議, http協議有什么用?
在構建 Web 應用時,通常會有 Web Server (nginx)和 Application Server(wsgi server eg:uwsgi) 兩種角色。其中 Web Server 主要負責接受來自用戶的請求,解析 HTTP 協議,並將請求轉發給 Application Server,Application Server 主要負責處理用戶的請求,並將處理的結果返回給 Web Server,最終 Web Server 將結果返回給用戶。
由於有很多動態語言和很多種 Web Server,他們彼此之間互不兼容,給程序員造成了很大的麻煩。因此就有了 CGI/FastCGI ,uwsgi 協議,定義了 Web Server 如何通過輸入輸出與 Application Server 進行交互,將 Web 應用程序的接口統一了起來。
總而言之, 這些協議就是進程交互的一種溝通方式。
舉個例子:美國人和中國人溝通必須要有一個公共的語言:英語, 這時候英語就是兩個人溝通的協議, 不然,一個說英語(uwsgi協議), 一個說中文(fastcgi協議)是肯定會亂碼的,處理不成功的。用同一個協議,大家都知道該如何解析過來的內容。
所以,nginx 和 uwsgi交互就必須使用同一個協議,而上面說了uwsgi支持fastcgi,uwsgi,http協議,這些都是nginx支持的協議,只要大家溝通好使用哪個協議,就可以正常運行了。
將uwsgi 放在nginx 后面
將uwsgi 放在nginx后面,讓nginx反向代理請求到uwsgi
uwsgi 原生支持HTTP, FastCGI, SCGI,以及特定的uwsgi協議, 性能最好的明顯時uwsgi, 這個協議已經被nginx支持。
所以uwsgi 配置使用哪個協議,nginx 要使用對應協議
# 使用http協議 uwsgi --http-socket 127.0.0.1:9000 --wsgi-file app.py
# nginx配置 lcation / { proxy_pass 127.0.0.1:9000; }
更多協議
[uwsgi] # 使用uwsgi協議 socket, uwsgi-socket 都是uwsgi協議 # bind to the specified UNIX/TCP socket using default protocol # UNIX/TCP 意思時可以UNIX: xx.sock, 或者 TCP: 127.0.0.1:9000 他們是都可以的 # UNIX 沒有走TCP協議,不是面向連接, 而是直接走文件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協議 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協議 http-socket # bind to the specified UNIX/TCP socket using HTTP protocol # nginx 使用proxy_pass # 原來proxy_pass 是http協議,但不一定要用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
結束 :
至此,nginx ->uwsgi ->web 框架 以及 WSGI的相關知識已經講解完了。 需要補充的是,我們自己實現的WSGI Server只能支持一個請求,在之后的日子,我會再寫一些教程,關於socket IO 復用 和線程池 讓我們自己寫server支持多請求,多並發的功能