2019年4月5日 星期五

在樹莓派 Nginx 伺服器上執行 Python 網頁應用程式

完成樹莓派 Nginx 伺服器安裝測試後, 我迫不及待想要測試如何在 Nginx 上執行 Python 網頁程式, 但我發現這牽涉到 Python 網頁程式開發背後的技術, 即 WSGI 介面規範, 所以得先搞清楚整個運作機制才行.

以下測試主要是參考下列兩本書 :

# 一次搞定所有 Python Web 框架開發百科全書 (佳魁, 劉長龍) 第 5.3 節
科班出身的MVC網頁開發 : 使用Python + Django (佳魁, 王友釗)

以及下面這幾篇文章 :

化整為零的次世代網頁開發標準: WSGI
python中WSGI是什麼,Python應用WSGI詳解
Python Web开发最难懂的WSGI协议,到底包含哪些内容?
做python Web开发你要理解:WSGI & uwsgi
理解Python WSGI

要在 Web 伺服器上跑 Python 應用程式必須先了解 WSGI 介面, 這是專為 Python 而定義的後端通訊協定, 相關知識摘要整理如下 :


一. WSGI 規範 (協定) :

WSGI (Web Server Gateway Interface) 是源自 CGI 的 Python 網頁介面標準, 問世於 2003 年, 它定義了 Python 網頁應用程式 (web application) 與 Web 伺服器 (web server) 的溝通方式, 讓 Python 網頁應用程式可以跟 Web 伺服器溝通. 對於 Client-Server 運作模式而言, 所謂的溝通其實就是伺服器如何傳遞請求與應用程式如何回應罷了.

WSGI 是在 CGI 的基礎上專門為 Python 語言制定的後端通訊標準, 功能上可說是 CGI 的延伸, 但 WSGI 為 Python 做了更多的定義, 其規格定義詳見 Python 文件 PEP333 :

https://www.python.org/dev/peps/pep-0333/

WSGI 協定包括了 Server 與 Application 兩部分 :
  1. WSGI Server :
    接收客戶端請求打包後轉發給 Application; 然後接收 Application 的回應, 將其回傳給客戶端.
  2. WSGI Application :
    接收 server 轉發的客戶端請求, 處理後將結果傳回 Server. 這部分可以是單獨的 Application, 也可以是包覆著 Application 的多個 middleware 堆疊. 
架構如下圖所示 :




注意, WSGI 只是一個分別定義了 Server 與 Application 介面的規範, 本身不是一個實作軟體. 關於 middleware 的妙用, 可參考下面這篇 :

化整為零的次世代網頁開發標準: WSGI

WSGI 的兩個部分可以分別實作, 實作 Application 部分的就是各式各樣的 Python 網頁框架, 例如 Django, Flask, Tornado 等都是. 而實作 Server 部分的稱為 WSGI Server, 例如 Python 3 內建的 wsgiref, 介接 Apache 的 mod_wsgi, 或移植自 Unix 的 Gunicorn 等等都是 WSGI server 的實作. 更多 WSGI Server 參考 :

Servers which support WSGI


二. Python 3 內建的 WSGI 伺服器 wsgiref :

由上面的概念圖可知 WSGI Server 的介面有兩個 :
  1. 與 Web 伺服器的介面
  2. 與 Python 應用程式的介面
其中與 Web 伺服器的介面由 WSGI Server 負責, Python 網頁應用程式開發者不需要處理. 此介面用來介接 Web 伺服器, 因為一般網頁伺服器例如 Apache 或 Nginx 必須支援各種程式語言, 因此它們不會直接與網頁應用程式溝通, 而是透過 WSGI 伺服器代勞, 例如 wsgiref, uWSGI, 或 mod_wsgi 等等. 除了介接外, WSGI 伺服器本身也會內建基本的 Web 伺服器功能, 但那只是做為開發測試用, 其功能太少且效能無法應付上線的流量.

網頁程式開發者需要處理的是 WSGI 伺服器的第二個介面, 即面向網頁應用程式的介面. WSGI 標準定義了一個簡單且形式固定的函數做為 Web 伺服器與 Python 網頁應用程式的溝通介面, 開發者只要按此介面撰寫網頁應用程式即可 :

def app_name(environ, start_response): 

函數名稱 app_name 是自訂的, 兩個傳入參數 environ 與 start_response 即應用程式與 Web 伺服器的單向雙工溝通管道, environ 表示來訊; 而 start_response() 表示回訊 :
  1. environ : (來訊用)
    字典型態的環境變數, 儲存 Web 伺服器傳來的請求相關資訊 
  2. start_response(status, response_headers) : (回訊用)
    回呼函數, 可讓應用程式起始一個回應, 其第一參數為回應狀態 (字串), 第二參數是回應標頭 (串列), 回應的本體 (body) 則用 return 傳回 HTML 代碼.
所謂環境變數是指包含客戶端請求訊息與 Web 伺服器相關資訊的字典變數. WSGI 與 CGI 一樣, 都是透過環境變數從伺服器取得外部資訊, 例如請求資訊 REQUEST_METHOD, HTTP_XXX, 與 PATH_INFO 等屬性, 伺服器資訊如 SERVER_NAME, 以及 WSGI 資訊如版本等. 當伺服器接到用戶端請求時, 會依據 HTTP 協定從 socket 串流剖析客戶端的請求資訊, 加上伺服器資訊打包成環境變數傳給應用程式處理.

例如 :

#myapp.py
def application(environ, start_response):
    status='200 OK'
    response_headers=[('Content-type','text/plain')]
    start_response(status, response_headers)
    return [b'Hello World!n']

將此函數存成 myapp.py 即成為一個會回應 "Hello World!" 的應用程式了.

另外是 WSGI 伺服器部分, Python 3 的內建模組 wsgiref 的 simple_server 類別可用來實作一個簡易的 WSGI+HTTP 伺服器, 在測試開發階段可以用它做為 Web 伺服器的代用品. 只要呼叫 simple_server 的 make_server() 方法並傳入 host, port, 以及處理函數即可建立一個 Web 伺服器 :

wsgiref.simple_server.make_server(host, port, app) 

詳細文件參考 :

https://docs.python.org/2/library/wsgiref.html#module-wsgiref.simple_server

例如下面程式就實作了一個 Web 伺服器 :

#wsgi_server.py    
from wsgiref.simple_server import make_server   
from myapp import application    

http_server=make_server('', 8000, application)
http_server.serve_forever()  

將此檔案存成 wsgi_server.py, 與上面的應用程式 myapp.py 放在同一目錄下, 開啟命令提示字元視窗, 執行 WSGI 伺服器程式 wsgi_server.py, 然後開啟瀏覽器視窗, 連線 localhost:8000 即可看到網頁輸出 "Hello World!" :




注意, 在 Python 3, 傳回的回應字串必須加 b 以轉成 byte string, 否則會出現 "AttributeError: 'NoneType' object has no attribute 'split'" 錯誤.

也可以將上面的 Web 伺服器程式 wsgi_server.py 與應用程式 myapp.py 合併寫在單一檔案裡面, 例如 :

#helloworld.py
from wsgiref.simple_server import make_server

def application(environ, start_response):
    status='200 OK'
    response_headers=[('Content-type','text/plain')]
    start_response(status, response_headers)
    return [b'Hello World!']

http_server=make_server('localhost', 8000, application)
http_server.serve_forever()

執行結果是一樣的 :

D:\Python\test>python helloworld.py
127.0.0.1 - - [04/Apr/2019 20:09:20] "GET / HTTP/1.1" 200 12
127.0.0.1 - - [04/Apr/2019 20:09:20] "GET /favicon.ico HTTP/1.1" 200 12


三. uWSGI 伺服器 :

上面提到有很多實作 WSGI Server 介面的 WSGI 伺服器, 在上線運營的 Python 網站中常見的配置是 :
  1. Nginx + uWSGI 
  2. Nginx + Gunicorn
  3. Apache + mod_wsgi
Nginx 的高效能是有目共睹的, 它的最佳搭檔 uWSGI 也是常見的 WSGI 伺服器, 它不僅實作了獨家的 uwsgi 協定, 也實作了 WSGI 與 HTTP 協定. 注意, 小寫的 uwsgi 指的是協定, 而開頭小寫的 uWSGI 則是其實作. 關於 uwsgi 參考:

https://uwsgi-docs.readthedocs.io/en/latest/Protocol.html

我在 Win10 上用 pip3 安裝結果失敗 :

D:\Python\test>pip3 install uwsgi 
Collecting uwsgi
  Downloading https://files.pythonhosted.org/packages/e7/1e/3dcca007f974fe4eb369bf1b8629d5e342bb3055e2001b2e5340aaefae7a/uwsgi-2.0.18.tar.gz (801kB)
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "C:\Users\User\AppData\Local\Temp\pip-install-0i8q8nuo\uwsgi\setup.py", line 3, in <module>
        import uwsgiconfig as uc
      File "C:\Users\User\AppData\Local\Temp\pip-install-0i8q8nuo\uwsgi\uwsgiconfig.py", line 8, in <module>
        uwsgi_os = os.uname()[0]
    AttributeError: module 'os' has no attribute 'uname'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in C:\Users\User\AppData\Local\Temp\pip-install-0i8q8nuo\uwsgi\

查詢網路, 似乎 uWSGI 不支援 Windows :

https://github.com/unbit/uwsgi/issues/1930
Windows 10安装uWSGI:不可行、失败了

在樹莓派上就能安裝了 :

pi@raspberrypi:~ $ pip3 install uwsgi 
Collecting uwsgi
  Using cached https://files.pythonhosted.org/packages/e7/1e/3dcca007f974fe4eb369bf1b8629d5e342bb3055e2001b2e5340aaefae7a/uwsgi-2.0.18.tar.gz
Building wheels for collected packages: uwsgi
  Running setup.py bdist_wheel for uwsgi ... done
  Stored in directory: /home/pi/.cache/pip/wheels/2d/0c/b0/f3ba1bbce35c3766c9dac8c3d15d5431cac57e7a8c4111c268
Successfully built uwsgi
Installing collected packages: uwsgi
Successfully installed uwsgi-2.0.18

安裝完畢用 pip3 show 顯示版本訊息為 2.0.18 版 :

pi@raspberrypi:~ $ pip3 show uwsgi 
Name: uWSGI
Version: 2.0.18
Summary: The uWSGI server
Home-page: https://uwsgi-docs.readthedocs.io/en/latest/
Author: Unbit
Author-email: info@unbit.it
License: GPLv2+
Location: /home/pi/.local/lib/python3.5/site-packages
Requires: 

安裝好後, 我在 /home/pi 底下編輯了一個網頁檔 myapp.py :

pi@raspberrypi:~ $ nano myapp.py 
pi@raspberrypi:~ $ cat myapp.py 
def application(environ, start_response):
    status='200 OK'
    response_headers=[('Content-type','text/plain')]
    start_response(status, response_headers)
    return [b'Hello World!']

然後在命令列開啟 uWSGI 伺服器讀取網頁 : 

pi@raspberrypi:~ $ uwsgi --http :8080 --wsgi-file myapp.py

卻出現 "bash : uwsgi command not found" 錯誤訊息, why?  

後來在葉難的 "在Raspbian上安裝nginx、PHP、Django與uWSGI" 看到它安裝的是 uWSGI, 難道就是上面說的 uwsgi 是協定, 而 uWSGI 是伺服器的差別嗎? 所以就安裝 uWSGI :

pi@raspberrypi:~ $ sudo pip3 install uWSGI
Collecting uWSGI
  Using cached https://files.pythonhosted.org/packages/e7/1e/3dcca007f974fe4eb369bf1b8629d5e342bb3055e2001b2e5340aaefae7a/uwsgi-2.0.18.tar.gz
Building wheels for collected packages: uWSGI
  Running setup.py bdist_wheel for uWSGI ... done
  Stored in directory: /root/.cache/pip/wheels/2d/0c/b0/f3ba1bbce35c3766c9dac8c3d15d5431cac57e7a8c4111c268
Successfully built uWSGI
Installing collected packages: uWSGI
Successfully installed uWSGI-2.0.18

用 pip3 show 顯示套件資訊 :

pi@raspberrypi:~ pip3 show uWSGI  
Name: uWSGI
Version: 2.0.18
Summary: The uWSGI server
Home-page: https://uwsgi-docs.readthedocs.io/en/latest/
Author: Unbit
Author-email: info@unbit.it
License: GPLv2+
Location: /home/pi/.local/lib/python3.5/site-packages
Requires: 
pi@raspberryp

看起來跟 uwsgi 的幾乎一樣, 就大小寫不同而已. 再試試看用 uwsgi 指令啟動伺服器, 結果沒再出現 "bash : uwsgi command not found" 錯誤訊息, 而是出現 "uwsgi: unrecognized option" 錯誤訊息 :

pi@raspberrypi:~ $ uwsgi --http:8000 --wsgi-file myapp.py 
uwsgi: unrecognized option '--http:8000'
getopt_long() error

查詢官網文件, 應該加上 "--http-websockets", 參考 :


果然加上 proxy 後就可以順利啟動伺服器了 : 

pi@raspberrypi:~ $ uwsgi --http :8080 --http-websockets --wsgi-file myapp.py 
*** Starting uWSGI 2.0.18 (32bit) on [Fri Apr  5 11:30:48 2019] ***
compiled with version: 6.3.0 20170516 on 05 April 2019 02:47:26
os: Linux-4.14.79+ #1159 Sun Nov 4 17:28:08 GMT 2018
nodename: raspberrypi
machine: armv6l
clock source: unix
detected number of CPU cores: 1
current working directory: /home/pi
detected binary path: /usr/local/bin/uwsgi
!!! no internal routing support, rebuild with pcre support !!!
*** WARNING: you are running uWSGI without its master process manager ***
your processes number limit is 3400
your memory page size is 4096 bytes
detected max file descriptor number: 1024
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uWSGI http bound on :8080 fd 4
spawned uWSGI http 1 (pid: 21648)
uwsgi socket 0 bound to TCP address 127.0.0.1:37339 (port auto-assigned) fd 3
Python version: 3.5.3 (default, Sep 27 2018, 17:25:39)  [GCC 6.3.0 20170516]
*** Python threads support is disabled. You can enable it with --enable-threads ***
Python main interpreter initialized at 0x1766130
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 64392 bytes (62 KB) for 1 cores
*** Operational MODE: single process ***
WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x1766130 pid: 21647 (default app)
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 21647, cores: 1)
[pid: 21647|app: 0|req: 1/1] 127.0.0.1 () {36 vars in 665 bytes} [Fri Apr  5 11:31:18 2019] GET / => generated 12 bytes in 1 msecs (HTTP/1.1 200) 1 headers in 45 bytes (1 switches on core 0)
[pid: 21647|app: 0|req: 2/2] 127.0.0.1 () {36 vars in 644 bytes} [Fri Apr  5 11:31:21 2019] GET /favicon.ico => generated 12 bytes in 1 msecs (HTTP/1.1 200) 1 headers in 45 bytes (1 switches on core 0)

瀏覽 localhost:8080 成功顯示網頁 :





sudo -u www-data uwsgi uwsgi_config.ini

參考 :

葉難: 在Raspbian上安裝nginx、PHP、Django與uWSGI
Setting up Nginx and uWSGI for CGI scripting
NGINX and uWSGI work but don't begin at startup

沒有留言 :