1. 程式人生 > >Web基礎(三)Python Web

Web基礎(三)Python Web

Python Web基礎

在這裡插入圖片描述

Web應用的本質: 1. 瀏覽器傳送一個HTTP請求 2. 伺服器收到請求,生成一個HTML文件 3. 伺服器把HTML文件作為HTTP響應的Body傳送給瀏覽器 4. 瀏覽器收到HTTP響應,從HTTP Body取出HTML文件並顯示

所以,最簡單的Web應用就是先把HTML用檔案儲存好,用一個現成的HTTP伺服器軟體,接收使用者請求,從檔案中讀取HTML,返回。Apache、Nginx、Lighttpd等這些常見的靜態伺服器就是幹這件事情的。

如果要動態生成HTML,就需要把上述步驟自己來實現。不過,接受HTTP請求、解析HTTP請求、傳送HTTP響應都是苦力活,如果我們自己來寫這些底層程式碼,還沒開始寫動態HTML呢,就得花個把月去讀HTTP規範。正確的做法是底層程式碼由專門的伺服器軟體實現,我們用Python專注於生成HTML文件。

1. WSGI

Web伺服器閘道器介面(Python Web Server Gateway Interface,縮寫為WSGI)是為Python語言定義的Web伺服器和Web應用程式或框架之間的一種簡單而通用的介面。自從WSGI被開發出來以後,許多其它語言中也出現了類似介面。

以前,如何選擇合適的Web應用程式框架成為困擾Python初學者的一個問題,這是因為,一般而言,Web應用框架的選擇將限制可用的Web伺服器的選擇,反之亦然。那時的Python應用程式通常是為CGI,FastCGI,mod_python中的一個而設計,甚至是為特定Web伺服器的自定義的API介面而設計的。

WSGI(有時發音作’wiz-gee’)是作為Web伺服器與Web應用程式或應用框架之間的一種低級別的介面,以提升可移植Web應用開發的共同點。WSGI是基於現存的CGI標準而設計的。WSGI沒有官方的實現, 因為WSGI更像一個協議

。只要遵照這些協議,WSGI應用(Application)都可以在任何伺服器(Server)上執行, 反之亦然。WSGI就是Python的CGI包裝,相對於Fastcgi是PHP的CGI包裝

1.1 概述

WSGI區分為兩個部分 1. 為“伺服器”或“閘道器”。它用於接收、整理客戶端傳送的請求 2. 為“應用程式”或“應用框架”。處理伺服器程式傳遞過來的請求

在這裡插入圖片描述

如上圖,Web伺服器即第一部分,接收、整理客戶端傳送的請求;Web框架即為第二部分,即所謂的Web應用程式。開發Web應用程式的時候,通常會把常用的功能封裝起來,成為各種框架,比如Flask,Django,Tornado(使用某框架進行web開發,相當於開發服務端的應用程式,處理後臺邏輯)。但是,伺服器程式和應用程式互相配合才能給使用者提供服務,而不同應用程式(不同框架)會有不同的函式、功能。 此時,我們就需要一個標準,讓伺服器程式和應用程式都支援這個標準,那麼,二者就能很好的配合了,這個標準就是 WSGI

在處理一個WSGI請求時,伺服器會為應用程式提供環境資訊及一個回撥函式(Callback Function)。當應用程式完成處理請求後,透過前述的回撥函式,將結果回傳給伺服器。

所謂的 WSGI 中介軟體同時實現了API的兩方,因此可以在WSGI伺服器和WSGI應用之間起調解作用。從Web伺服器的角度來說,中介軟體扮演應用程式,而從應用程式的角度來說,中介軟體扮演伺服器。“中介軟體”元件可以執行以下功能: 1. 重寫環境變數後,根據目標URL,將請求訊息路由到不同的應用物件。 2. 允許在一個程序中同時執行多個應用程式或應用框架。 3. 負載均衡和遠端處理,通過在網路上轉發請求和響應訊息。 4. 進行內容後處理,例如應用XSLT樣式表。

1.2 實現原理

WSGI 將 Web 元件分為三類

  • web伺服器
  • web中介軟體
  • web應用程式

wsgi基本處理模式為: WSGI Server -> WSGI Middleware -> WSGI Application

1、WSGI Server/gateway

wsgi server可以理解為一個符合wsgi規範的web server,接收request請求,封裝一系列環境變數,按照wsgi規範呼叫註冊的wsgi app,最後將response返回給客戶端。以python自帶的wsgiref為例,wsgiref是按照wsgi規範實現的一個簡單wsgi server。它的程式碼不復雜。

在這裡插入圖片描述

  1. 伺服器建立socket,監聽埠,等待客戶端連線。
  2. 當有請求來時,伺服器解析客戶端資訊放到環境變數environ中,並呼叫繫結的handler來處理請求。
  3. handler解析這個http請求,將請求資訊例如method,path等放到environ中。
  4. wsgi handler再將一些伺服器端資訊也放到environ中,最後伺服器資訊,客戶端資訊,本次請求資訊全部都儲存到了環境變數environ中。
  5. wsgi handler 呼叫註冊的wsgi app,並將environ和回撥函式傳給wsgi app
  6. wsgi app 將reponse header/status/body 回傳給wsgi handler
  7. 最終handler還是通過socket將response資訊塞回給客戶端。

2、WSGI Application

wsgi application就是一個普通的callable物件,當有請求到來時,wsgi server會呼叫這個wsgi app。這個物件接收兩個引數,通常為environ,start_response。environ就像前面介紹的,可以理解為環境變數,跟一次請求相關的所有資訊都儲存在了這個環境變數中,包括伺服器資訊,客戶端資訊,請求資訊。start_response是一個callback函式,wsgi application通過呼叫start_response,將response headers/status 返回給wsgi server。此外這個wsgi app會return 一個iterator物件 ,這個iterator就是response body。這麼空講感覺很虛,對著下面這個簡單的例子看就明白很多了。

3、WSGI MiddleWare

有些功能可能介於伺服器程式和應用程式之間,例如,伺服器拿到了客戶端請求的URL, 不同的URL需要交由不同的函式處理,這個功能叫做 URL Routing,這個功能就可以放在二者中間實現,這個中間層就是 middleware。middleware對伺服器程式和應用是透明的,也就是說,伺服器程式以為它就是應用程式,而應用程式以為它就是伺服器。這就告訴我們,middleware需要把自己偽裝成一個伺服器,接受應用程式,呼叫它,同時middleware還需要把自己偽裝成一個應用程式,傳給伺服器程式。

論是伺服器程式、middleware 還是應用程式,都在服務端,為客戶端提供服務,之所以把他們抽象成不同層,就是為了控制複雜度,使得每一次都不太複雜,各司其職。

1.3 測試 WSGI伺服器

原理說得太多未免過於抽象,現在使用Python內建的純Python程式碼編寫的wsgiref伺服器來體驗一把WSGI伺服器是如何工作的

  • 編寫hello.py 作為一個Web應用程式

    def application(environ, start_response):
        start_response('200 OK', [('Content-Type', 'text/html')])
        return [b'<h1>Hello, World!</h1>']
    
  • 編寫server.py作為一個WSGI伺服器

    from wsgiref.simple_server import make_server
    # 匯入編寫的application函式
    from hello import application
    
    # 建立一個伺服器,IP地址為空,埠是8000,傳入函式application
    httpd = make_server('', 8000, application)
    print('Serving HTTP on port 8000...')
    # 開始監聽HTTP請求:
    httpd.serve_forever()
    
  • 啟動WSGI伺服器

    python server.py
    
  • 使用客戶端訪問 開啟瀏覽器,輸入http://localhost:8000/ ,在瀏覽器正常顯示“Hello, World!”

在這裡插入圖片描述

程式碼簡析

上面的application()函式就是符合WSGI標準的一個HTTP處理函式,它接收兩個引數:

  • environ:一個包含所有HTTP請求資訊的dict物件
  • start_response:一個傳送HTTP響應的函式

而在application()函式中又呼叫了start_response函式

該函式傳送了HTTP響應的Header,注意Header只能傳送一次,也就是隻能呼叫一次start_response()函式。start_response()函式接收兩個引數,一個是HTTP響應碼,一個是一組list表示的HTTP Header,每個Header用一個包含兩個strtuple表示。

通常情況下,都應該把Content-Type頭髮送給瀏覽器。其他很多常用的HTTP Header也應該傳送。然後,函式的返回值b'<h1>Hello, web!</h1>'將作為HTTP響應的Body傳送給瀏覽器。

有了WSGI,我們關心的就是如何從environ這個dict物件拿到HTTP請求資訊,然後構造HTML,通過start_response()傳送Header,最後返回Body。

整個application()函式本身沒有涉及到任何解析HTTP的部分,也就是說,底層程式碼不需要我們自己編寫,我們只負責在更高層次上考慮如何響應請求就可以了。

需要注意的是,application()函式必須由WSGI伺服器來呼叫。有很多符合WSGI規範的伺服器,我們可以挑選一個來用。但是我們僅將內建的wsgiref伺服器用於測試,使我們編寫的Web應用程式立馬跑起來。

1.4 實現WSGI伺服器

為了瞭解wsgi的工作原理,我們可以參照wsgiref原始碼,使用Python簡單實現一個WSGI伺服器

import socket
import StringIO
import sys

class WSGIServer(object):

    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM
    request_queue_size = 1

    def __init__(self, server_address):
        # 建立socket,利用socket獲取客戶端的請求
        self.listen_socket = listen_socket = socket.socket(self.address_family, self.socket_type)
        # 設定socket的工作模式
        listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 繫結socket地址
        listen_socket.bind(server_address)
        # socket active, 監聽檔案描述符
        listen_socket.listen(self.request_queue_size)

        # 獲得serve的host name和port
        host, port = self.listen_socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port

        self.headers_set = []

    def set_app(self, application):
        self.application = application 

    #啟動WSGI server服務,不停的監聽並獲取socket資料。
    def serve_forever(self):
        listen_socket = self.listen_socket
        while True:
            self.client_connection, client_address = listen_socket.accept() #接受客戶端請求
            #處理請求
            self.handle_one_request()

    def handle_one_request(self):
        self.request_data = request_data = self.client_connection.recv(1024)
        self.parse_request(request_data)

        # Construct environment dictionary using request data
        env = self.get_environ()

        #給flask\tornado傳遞兩個引數,environ,start_response
        result = self.application(env, self.start_response)
        self.finish_response(result)

    #處理socket的http協議
    def parse_request(self, data):
        format_data = data.splitlines()
        if len(format_data):
            request_line = data.splitlines()[0]
            request_line = request_line.rstrip('\r\n')
            (self.request_method, self.path, self.request_version) = request_line.split() ## ['GET', '/', 'HTTP/1.1']

    # 獲取environ資料並設定當前server的工作模式
    def get_environ(self):
        env = {}
        env['wsgi.version']      = (1, 0)
        env['wsgi.url_scheme']   = 'http'
        env['wsgi.input']        = StringIO.StringIO(self.request_data)
        env['wsgi.errors']       = sys.stderr
        env['wsgi.multithread']  = False
        env['wsgi.multiprocess'] = False
        env['wsgi.run_once']     = False
        # Required CGI variables
        env['REQUEST_METHOD']    = self.request_method    # GET
        env['PATH_INFO']         = self.path              # /hello
        env['SERVER_NAME']       = self.server_name       # localhost
        env['SERVER_PORT']       = str(self.server_port)  # 8888
        return env

    def start_response(self, status, response_headers, exc_info=None):
        server_headers = [('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'), ('Server', 'WSGIServer 0.2')]
        self.headers_set = [status, response_headers + server_headers]

    #把application返回給WSGI的資料返回給客戶端。
    def finish_response(self, result):
        try:
            status, response_headers = self.headers_set
            response = 'HTTP/1.1 {status}\r\n'.format(status=status)
            for header in response_headers:
                response += '{0}: {1}\r\n'.format(*header)
            response += '\r\n'
            for data in result:
                response += data
            self.client_connection.sendall(response)
            print(''.join(['> {line}\n'.format(line=line) for line in response.splitlines()]))
        finally:
            self.client_connection.close()

SERVER_ADDRESS = (HOST, PORT) = '', 8888

def make_server(server_address, application):
    server = WSGIServer(server_address)
    server.set_app(application)
    return server


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit('Provide a WSGI application object as module:callable')
    app_path = sys.argv[1]
    module, application = app_path.split(':') # 第一個引數是檔名,第二個引數時長檔案內app的命名
    module = __import__(module)
    application = getattr(module, application) # getattr(object, name[, default]) -> value
    httpd = make_server(SERVER_ADDRESS, application)
    print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT))
    httpd.serve_forever()

1.5 生產環境中的Web伺服器

每個web框架都不是專注於實現伺服器方面的,因此,在生產環境部署的時候,使用的伺服器也不會簡單的使用web框架自帶的伺服器,那麼用於生產環境的伺服器有哪些呢?

Gunicorn(從Ruby下面的Unicorn得到的啟發)應運而生:依賴Nginx的代理行為,同Nginx進行功能上的分離。由於不需要直接處理使用者來的請求(都被Nginx先處理),Gunicorn不需要完成相關的功能,其內部邏輯非常簡單:接受從Nginx來的動態請求,處理完之後返回給Nginx,由後者返回給使用者。

由於功能定位很明確,Gunicorn得以用純Python開發:大大縮短了開發時間的同時,效能上也不會很掉鏈子。同時,它也可以配合Nginx的代理之外的別的Proxy模組工作,其配置也相應比較簡單

uWSGI

使用C語言開發,和底層接觸的更好,配置也比較方便,目前和gunicorn兩個算是部署時的唯二之選。由於其可擴充套件的架構,它能夠被無限制的擴充套件用來支援更多的平臺和語言。目前,可以使用C,C++和Objective-C來編寫外掛

uWSGI 既不使用wsgi協議也不用FastCGI協議,而是自創了一個uwsgi的協議,uwsgi協議是一個uWSGI伺服器自有的協議,它用於定義傳輸資訊的型別(type of information),每一個uwsgi packet前4byte為傳輸資訊型別描述,它與WSGI相比是兩樣東西。據說該協議大約是fcgi協議的10倍那麼快

主要特點如下:

  • 超快的效能
  • 低記憶體佔用(實測為apache2的mod_wsgi的一半左右)
  • 多app管理
  • 詳盡的日誌功能(可以用來分析app效能和瓶頸)
  • 高度可定製(記憶體大小限制,服務一定次數後重啟等)

uWSGI 伺服器自己實現了基於uwsgi協議的server部分,因此我們只需要在uwsgi的配置檔案中指定application的地址,uWSGI 就能直接和應用框架中的WSGI application通訊

是一個用C語言編寫的,快速超輕量級的 Python WSGI伺服器。 它是最快速的,最小的並且是最輕量級的WSGI伺服器。有以下特性:

  • 1000 行的C程式碼
  • 佔用記憶體 600KB
  • 單執行緒沒有其他協同程式
  • 可以繫結到TCP主機:埠地址和Unix套接字
  • 支援HTTP1.0/1.1,包含支援HTTP1.1的分塊響應

如果單純追求效能,那uWSGI會更好一點,而Gunicorn則會更易安裝和結合gevent。在阻塞響應較多的情況下,Gunicorn的gevent模式無疑效能會更加強大。功能實現方面,uWSGI會更多一些,配置也會更加複雜一些。

2. Web應用開發

常見的Python Web應用框架:

  • Django:全能型Web框架
  • Flask:一個使用Python編寫的輕量級Web框架
  • web.py:一個小巧的Web框架
  • Bottle:和Flask類似的Web框架
  • Tornado:Facebook的開源非同步Web框架

2.1 伺服器架構

在這裡插入圖片描述

2.1.1 Nginx

Nginx(發音同engine x)是一個非同步框架的 Web伺服器,也可以用作反向代理,負載平衡器 和 HTTP快取。該軟體由 Igor Sysoev 建立,並於2004年首次公開發布。同名公司成立於2011年,以提供支援。

Nginx是一款免費的開源軟體,根據類BSD許可證的條款釋出。一大部分Web伺服器使用Nginx,通常作為負載均衡器。

Nginx是一款面向效能設計的HTTP伺服器,相較於Apache、lighttpd具有佔有記憶體少,穩定性高等優勢。與舊版本(<=2.2)的Apache不同,Nginx不採用每客戶機一執行緒的設計模型,而是充分使用非同步邏輯從而削減了上下文排程開銷,所以併發服務能力更強。整體採用模組化設計,有豐富的模組庫和第三方模組庫,配置靈活。 在Linux作業系統下,Nginx使用epoll事件模型,得益於此,Nginx在Linux作業系統下效率相當高。同時Nginx在OpenBSD或FreeBSD作業系統上採用類似於epoll的高效事件模型kqueue。

Nginx在官方測試的結果中,能夠支援五萬個並行連線,而在實際的運作中,可以支援二萬至四萬個並行連線

反向代理

在這裡插入圖片描述

正向代理是指瀏覽器主動請求代理伺服器,代理伺服器轉發請求到對應的目標伺服器。而反向代理則部署在Web伺服器上,代理所有外部網路對內部網路的訪問。瀏覽器訪問伺服器,必須經過這個代理,是被動的。正向代理的主動方是客戶端,反向代理的主動方是Web伺服器

在Python的Web開發中,較為成熟穩定的伺服器架構一般是Nginx + uWSGI + Django。而實際上Nginx伺服器並不是必須的,直接使用uWSGI + Djang完全是可以的,但這樣一來,直接將uWSGI伺服器暴露給了瀏覽器客戶端,由此會導致諸多隱患。

Nginx的優勢
  1. 安全問題。客戶端對Web伺服器的訪問需要先經過反向代理伺服器,Nginx則只需開放某個介面,uWSGI本身是內網介面,這樣可以防止外部程式對Web伺服器的直接攻擊。
  2. 負載均衡。反向代理伺服器可以根據Web伺服器的負載情況,動態地把HTTP請求交給不同的Web伺服器來處理,前提是要有多個Web伺服器。
  3. 提升Web伺服器的IO效能。一個HTTP請求的資料,從客戶端傳輸給伺服器,是需要時間的,例如N秒,如果直接傳給Web伺服器,Web伺服器就需要讓一個程序阻塞N秒,來接收IO,這樣會降低Web伺服器的效能。如果使用Nginx作為反向代理伺服器,先讓反向代理伺服器接收完整個HTTP請求,再把請求發給Web伺服器,就能提升Web伺服器的效能。將靜態資源傳送(js、css、圖片等)、動態請求轉發以及結果的回覆交給Nginx處理,就不需要經過Web伺服器。

附錄

支援WSGI的Web應用框架有很多

  • BlueBream
  • bobo
  • Bottle
  • CherryPy
  • Django
  • Flask
  • Google App Engine’s webapp2
  • Gunicorn
  • prestans
  • Pylons
  • Pyramid
  • restlite
  • Tornado
  • Trac
  • TurboGears
  • Uliweb
  • web2py
  • weblayer
  • Werkzeug

參考