天天看點

python wsgi架構_WSGI——python-Web架構基礎 from test from test

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協定。

python wsgi架構_WSGI——python-Web架構基礎 from test from test
python wsgi架構_WSGI——python-Web架構基礎 from test from test
python wsgi架構_WSGI——python-Web架構基礎 from test from test
python wsgi架構_WSGI——python-Web架構基礎 from test from test

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