1. 簡介
WSGI
WSGI:web伺服器網關接口,這是python中定義的一個網關協定,規定了Web Server如何跟應用程式互動。可以了解為一個web應用的容器,通過它可以啟動應用,進而提供HTTP服務。
它最主要的目的是保證在Python中所有的Web Server程式或者說Gateway程式,能夠通過統一的協定跟Web架構或者說Web應用進行互動。
uWSGI
uWGSI:是一個web伺服器,或者wsgi server伺服器,他的任務就是接受使用者請求,由于使用者請求是通過網絡發過來的,其中使用者到伺服器端之間用的是http協定,是以我們uWSGI要想接受并且正确解出相關資訊,我們就需要uWSGI實作http協定,沒錯,uWSGI裡面就實作了http協定。是以現在我們uWSGI能準确接受到使用者請求,并且讀出資訊。現在我們的uWSGI伺服器需要把資訊發給Django,我們就需要用到WSGI協定,剛好uWSGI實作了WSGI協定,是以。uWSGI把接收到的資訊作一次簡單封裝傳遞給Django,Django接收到資訊後,再經過一層層的中間件,于是,對資訊作進一步處理,最後比對url,傳遞給相應的視圖函數,視圖函數做邏輯處理......後面的就不叙述了,然後将處理後的資料通過中間件一層層傳回,到達Djagno最外層,然後,通過WSGI協定将傳回資料傳回給uWSGI伺服器,uWSGI伺服器通過http協定将資料傳遞給使用者。這就是整個流程。
這個過程中我們似乎沒有用到uwsgi協定,但是他也是uWSGI實作的一種協定,魯迅說過,存在即合理,是以說,他肯定在某個地方用到了。我們過一會再來讨論
我們可以用這條指令:python manage.py runserver,啟動Django自帶的伺服器。DJango自帶的伺服器(runserver 起來的 HTTPServer 就是 Python 自帶的 simple_server)。是預設是單程序單多線程的,對于同一個http請求,總是先執行一個,其他等待,一個一個串行執行。無法并行。而且django自帶的web伺服器性能也不好,隻能在開發過程中使用。于是我們就用uWSGI代替了。
為什麼有了WSGI為什麼還需要nginx?
因為nginx具備優秀的靜态内容處理能力,然後将動态内容轉發給uWSGI伺服器,這樣可以達到很好的用戶端響應。支援的并發量更高,友善管理多程序,發揮多核的優勢,提升性能。這時候nginx和uWSGI之間的溝通就要用到uwsgi協定。
2. 簡單的Web Server
在了解WSGI協定之前,首先看一個通過socker程式設計實作的Web服務的代碼。
import socket
EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
body = """hello world
from test
"""
response_params = [
'HTTP/1.0 200 OK',
'DATE: Sun, 27 may 2019 01:01:01 GMT',
'Content-Type:text/plain; charset=utf-8',
'Content-Length: {}\r\n'.format(len(body.encode())),
body,
]
response = '\r\n'.join(response_params)
def handle_connection(conn, addr):
request = b""
print('new conn', conn, addr)
import time
time.sleep(100)
while EOL1 not in request and EOL2 not in request:
request += conn.recv(1024)
print(request)
conn.send(response.encode())
conn.close()
def main():
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('127.0.0.1', 8000))
serversocket.listen(5)
print('http://127.0.0.1:8000')
try:
while True:
conn, address = serversocket.accept()
handle_connection(conn, address)
finally:
serversocket.close()
if __name__ == '__main__':
main()
多線程
import errno
import socket
import threading
import time
EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
body = """hello world
from test
"""
response_params = [
'HTTP/1.0 200 OK',
'DATE: Sun, 27 may 2019 01:01:01 GMT',
'Content-Type:text/plain; charset=utf-8',
'Content-Length: {}\r\n'.format(len(body.encode())),
body,
]
response = '\r\n'.join(response_params)
def handle_connection(conn, addr):
print(conn, addr)
time.sleep(60)
request = b""
while EOL1 not in request and EOL2 not in request:
request += conn.recv(1024)
print(request)
current_thread = threading.currentThread()
content_length = len(body.format(thread_name=current_thread.name).encode())
print(current_thread.name)
conn.send(response.format(thread_name=current_thread.name, length=content_length).encode())
conn.close()
def main():
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(('127.0.0.1', 8000))
serversocket.listen(10)
print('http://127.0.0.1:8000')
serversocket.setblocking(True) # 設定socket為阻塞模式
try:
i = 0
while True:
try:
conn, address = serversocket.accept()
except socket.error as e:
if e.args[0] != errno.EAGAIN:
raise
continue
i += 1
print(i)
t = threading.Thread(target=handle_connection, args=(conn, address), name='thread-%s'%i)
t.start()
finally:
serversocket.close()
if __name__ == '__main__':
main()
3. 簡單的WSGI Application
該協定分為兩個部分:
Web Server 或者Gateway
監聽在某個端口上接收外部的請求
Web Application
Web Server接收請求之後,會通過WSGI協定規定的方式把資料傳遞給Web Application,在Web Application中處理完之後,設定對應的狀态和header,之後傳回body部分。Web Server拿到傳回的資料之後,再進行HTTP協定的封裝,最終傳回完整的HTTPResponse資料。
下面我們來實作一個簡單的應用:
app.py
def simple_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [b'Hello world! -by test \n']
我們需要一個腳本運作上面這個應用:
import os
import sys
from app import simple_app
def wsgi_to_bytes(s):
return s.encode()
def run_with_cgi(application):
environ = dict(os.environ.items())
environ['wsgi.input'] = sys.stdin.buffer
environ['wsgi.errors'] = sys.stderr
environ['wsgi.version'] = (1, 0)
environ['wsgi.multithread'] = False
environ['wsgi.multiprocess'] = True
environ['wsgi.run_once'] = True
if environ.get('HTTPS', 'off') in ('on', '1'):
environ['wsgi.url_scheme'] = 'https'
else:
environ['wsgi.url_scheme'] = 'http'
headers_set = []
headers_sent = []
def write(data):
out = sys.stdout.buffer
if not headers_set:
raise AssertionError('Write() before start_response()')
elif not headers_sent:
# 在輸出第一行資料之前,先發送響應頭
status, response_headers = headers_sent[:] = headers_set
out.write(wsgi_to_bytes('Status: %s\r\n' % status))
for header in response_headers:
out.write(wsgi_to_bytes('%s: %s\r\n' % header))
out.write(wsgi_to_bytes('\r\n'))
out.write(data)
out.flush()
def start_response(status, response_headers, exc_info=None):
if exc_info:
try:
if headers_sent:
# 如果已經發送了header,則重新抛出原始異常資訊
raise (exc_info[0], exc_info[1], exc_info[2])
finally:
exc_info = None
elif headers_set:
raise AssertionError('*Headers already set!')
headers_set[:] = [status, response_headers]
return write
result = application(environ, start_response)
try:
for data in result:
if data:
write(data)
if not headers_sent:
write('')
finally:
if hasattr(result, 'close'):
result.close()
if __name__ == '__main__':
run_with_cgi(simple_app)
運作結果:
Status: 200 OK
Content-type: text/plain
Hello world! -by test
如果不是windows系統,還可以采用另一種方式運作:
pip install gunicorn
gunicorn app:simle_app
4. 了解
對于上述代碼我們隻需要關注一點,result = application(environ, start_response),我們要實作的Application,隻需要能夠接收一個環境變量以及一個回調函數即可。但處理完請求之後,通過回調函數(start_response)來設定response的狀态和header,最終傳回結果,也就是body。
WSGI協定規定,application必須是一個可調用對象,這意味這個對象既可以是Python中的一個函數,也可以是一個實作了__call__方法的類的執行個體,比如:
樣例一
class AppClass(object):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
def __call__(self, environ, start_response):
print(environ, start_response)
start_response(self.status, self.response_headers)
return [b'Hello AppClass.__call__\n']
application = AppClass()
gunicorn app: application運作上述檔案
樣例二
除此之外,我們還可以通過另一種方式實作WSGI協定,從上面的simple_app和這裡的AppClass.__call__的傳回值來看,WSGI Server隻需要傳回一個可疊代的對象就行
class AppClassIter(object):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
def __init__(self, environ, start_response):
self.environ = environ
self.start_response = start_response
def __iter__(self):
self.start_response(self.status, self.response_headers)
yield b'Hello AppClassIter\n'
gunicorn app: AppClassIter運作上述檔案
這裡的啟動指令并不是一個類的執行個體,而是類本身。通過上面兩個代碼,我們可以看到能夠被調用的方法會傳environ和start_response過來,而現在這個實作沒有可調用的方式,是以就需要在執行個體化的時候通過參數傳遞進來,這樣在傳回body之前,可以先調用start_response方法。
是以,可以推測出WSGI Server是如何調用WSGI Application的,大概代碼如下:
def start_response(status, headers):
# 僞代碼
set_status(status)
for k, v in headers:
set_header(k, v)
def handle_conn(conn):
# 調用我們定義的application(也就是上面的simple_app, 或者是AppClass的執行個體,或者是AppClassIter本身)
app = application(environ, start_response)
# 周遊傳回的結果,生成response
for data in app:
response += data
conn.sendall(response)
5. WSGI中間件和Werkzeug
WSGI中間件可以了解為Python中的一個裝飾器,可以在不改變原方法的情況下對方法的輸入和輸出部分進行處理。
類似這樣:
def simple_app(enbiron, start_response):
response = Response('Hello World', start_response=start_response)
response.set_header('Content-Type', 'text/plain') # 這個函數裡面調用start_response
return response
這樣就看起來更加自然一點。
是以,就存在Werkzeug這樣的WSGI工具集,讓你能夠跟WSGI協定更加友好的互動。從理論上來看,我們可以直接通過WSGI協定的簡單實作寫一個Web服務。但是有了Werkzeug之後,我們可以寫的更加容易。
6. 雜談
django 的并發能力真的是令人擔憂,這裡就使用 nginx + uwsgi 提供高并發
nginx 的并發能力超高,單台并發能力過萬(這個也不是絕對),在純靜态的 web 服務中更是突出其優越的地方,由于其底層使用 epoll 異步IO模型進行處理,使其深受歡迎
做過運維的應該都知道,
Python需要使用nginx + uWSGI 提供靜态頁面通路,和高并發
php 需要使用 nginx + fastcgi 提供高并發,
java 需要使用 nginx + tomcat 提供 web 服務
django 原生為單線程式,當第一個請求沒有完成時,第二個請求輝阻塞,知道第一個請求完成,第二個請求才會執行。
Django就沒有用異步,通過線程來實作并發,這也是WSGI普遍的做法,跟tornado不是一個概念
官方文檔解釋django自帶的server預設是多線程
django開兩個接口,第一個接口sleep(20),另一個接口不做延時處理(大概耗時幾毫秒)
先請求第一個接口,緊接着請求第二個接口,第二個接口傳回資料,第一個接口20秒之後傳回資料
證明django的server是預設多線程
啟動uWSGI伺服器
# 在django項目目錄下 Demo工程名
uwsgi --http 0.0.0.0:8000 --file Demo/wsgi.py
經過上述的步驟測試,發現在這種情況下啟動django項目,uWSGI也是單線程,通路接口需要"排隊"
不給uWSGI加程序,uWSGI預設是單程序單線程
uwsgi --http 0.0.0.0:8000 --file Demo/wsgi.py --processes 4 --threads 2
# processes: 程序數 # processes 和 workers 一樣的效果 # threads : 每個程序開的線程數
經過測試,接口可以"同時"通路,uWSGI提供多線程
Python因為GIL的存在,在一個程序中,隻允許一個線程工作,導緻單程序多線程無法利用多核
多程序的線程之間不存在搶GIL的情況,每個程序有一個自己的線程鎖,多程序多GIL