WEB編程基礎(chǔ)

談?wù)揥EB編程的時候常說天天在寫CGI逾雄,那么CGI是什么呢爱咬?可能很多時候并不會去深究這些基礎(chǔ)概念停忿,再比如除了CGI還有FastCGI, wsgi, uwsgi等比被,那這些又有什么區(qū)別呢胯究?為了總結(jié)這些這些WEB編程基礎(chǔ)知識稍计,于是寫了此文,如有錯誤裕循,懇請指正臣嚣,示例代碼見 web-basis 净刮。

1 CGI

1.1 CGI原理

在說明CGI是什么之前,我們先來說說CGI不是什么硅则。

  • CGI不是一門編程語言淹父。它的實現(xiàn)對編程語言沒有限定,你可以用python怎虫,php暑认,perl,shell大审,C語言等蘸际。
  • CGI不是一個編程模式。你可以使用任何你熟悉的方式實現(xiàn)它饥努。
  • CGI也不復(fù)雜捡鱼,不需要你是一個編程老鳥,菜鳥一樣可以愉快的寫自己的CGI酷愧。

那么CGI到底是什么?CGI全稱是Common Gateway Interface缠诅,即通用網(wǎng)關(guān)接口溶浴。我們可能對API(Application Programming Interface)會很熟悉,CGI就是WEB服務(wù)器的API管引。WEB服務(wù)器顧名思義士败,就是發(fā)送網(wǎng)頁給瀏覽器的軟件,瀏覽器稱之為web client褥伴,WEB服務(wù)器是web server谅将。瀏覽器作為客戶端,它做的工作就是向WEB服務(wù)器請求文件(比如HTML文檔重慢,圖片饥臂,樣式文件以及任何其他文件等),一般的WEB服務(wù)器的功能就是發(fā)送存儲在服務(wù)器上的靜態(tài)文件給發(fā)送請求的客戶端似踱。

那么問題來了隅熙,有些時候,我們需要發(fā)送動態(tài)的數(shù)據(jù)給客戶端核芽,這就需要我們寫程序來動態(tài)生成數(shù)據(jù)并返回囚戚,這就是CGI用處所在。需要強(qiáng)調(diào)的是轧简,WEB服務(wù)器和客戶端之間是不能交互的驰坊,CGI程序不能要求用戶輸入一些參數(shù),處理并返回輸出哮独,然后要求用戶繼續(xù)輸入拳芙,這也是CGI能夠保持簡單的原因之一假勿。CGI程序每次只能最多獲取一次用戶輸入,然后處理并返回一次輸出态鳖。那么CGI如何獲取用戶輸入呢转培?

CGI程序獲取用戶輸入依賴瀏覽器發(fā)送請求的方式。一般來說浆竭,瀏覽器的HTTP請求會以GET或者POST的方式發(fā)送浸须。瀏覽器使用HTML表單獲取用戶輸入,HTML表單可以指定瀏覽器發(fā)送請求的方法是GET還是POST邦泄,它們不同在于GET方法會將用戶輸入?yún)?shù)作為URL一部分删窒,而POST的優(yōu)勢在于:

  • 你可以發(fā)送更多的數(shù)據(jù)(URL長度是有限制的)
  • 發(fā)送數(shù)據(jù)不會在URL中被記錄(例如你要發(fā)送密碼放到URL中是不太安全的),也不會出現(xiàn)在瀏覽器的地址欄中顺囊。

那么CGI程序如何知道客戶端請求是哪種方法呢肌索?在WEB服務(wù)器加載你的CGI程序前,會設(shè)置一些環(huán)境變量讓CGI程序知道去哪里獲取用戶輸入數(shù)據(jù)以及數(shù)據(jù)大小特碳。比如 REQUEST_METHOD這個環(huán)境變量會設(shè)置為客戶端的請求方法如GET/POST/HEAD等诚亚。而CONTENT_LENGTH環(huán)境變量會告訴你應(yīng)該從stdin中讀取多少字節(jié)數(shù)據(jù)。CONTENT_TYPE則是告訴你客戶端數(shù)據(jù)類型午乓,是來自表單還是其他來源站宗。

當(dāng)CGI程序讀取到了用戶輸入數(shù)據(jù)后,可以處理數(shù)據(jù)并將響應(yīng)發(fā)送到stdout益愈。CGI程序可以返回HTML數(shù)據(jù)或者其他類型數(shù)據(jù)如GIF圖片等梢灭。這也是為什么你在返回數(shù)據(jù)前要先在第一行說明你返回數(shù)據(jù)的類型,如Content-type: text/html蒸其,然后加兩個CRLF后(HTTP協(xié)議的規(guī)定)敏释,再返回真正的輸出數(shù)據(jù)。

1.2 CGI實現(xiàn)

在現(xiàn)實應(yīng)用中摸袁,WEB服務(wù)器常用的有nginx和apache钥顽。apache提供了很多模塊,可以直接加載CGI程序但惶,和上一章提到的方式基本一致耳鸯。而nginx是不能加載CGI程序的,必須另外單獨運行一個CGI程序處理器來處理CGI請求膀曾,先來看下CGI實現(xiàn)县爬,WEB服務(wù)器代碼cgi.c。編譯并運行:

$ gcc -o cgi cgi.c
$ ./cgi

CGI程序如下添谊,可以為C語言編寫财喳,如 cgi_hello.c,也可以是shell,python等其他語言耳高,如 cgi_hello.sh扎瓶。編譯cgi_hello.c,放到cgi.c同一個目錄下面泌枪。

$ gcc -o cgi_hello cgi_hello.c

使用C實現(xiàn)一個cgi服務(wù)器概荷,其實就是WEB服務(wù)器并附帶調(diào)用cgi程序功能。根據(jù)URL中的路徑獲取cgi程序名碌燕,并執(zhí)行該cgi程序獲取返回結(jié)果并返回給客戶端误证。注意,是在WEB服務(wù)器程序中設(shè)置的環(huán)境變量修壕,通過execl執(zhí)行cgi程序愈捅,cgi程序因為是fork+exec執(zhí)行的,子進(jìn)程是會復(fù)制父進(jìn)程環(huán)境變量表到自己的進(jìn)程空間的慈鸠,所以可以讀取環(huán)境變量QUERY_STRING蓝谨。在瀏覽器輸入 http://192.168.56.18:6006/cgi_hello?name=ssj(測試機(jī)ip為192.168.56.18)
可以看到返回 Hello: ssj

2 FastCGI協(xié)議

2.1 FastCGI原理

如前面提到的青团,nginx是不能直接加載CGI程序的譬巫,由此需要一個專門的CGI程序管理器,nginx通過unix-socket或tcp-socket與CGI程序管理器通信壶冒。如php常用php-fpm缕题,python常用uWSGI等,不過它們的協(xié)議不同胖腾,php-fpm用的是fastcgi協(xié)議,而uWSGI用的是uwsgi協(xié)議瘪松。nginx對這兩種協(xié)議都支持咸作,nginx配置文件/etc/nginx/fastcgi_params/etc/nginx/uwsgi_params就是分別針對這兩種協(xié)議的。

先來看看FastCGI協(xié)議宵睦。顧名思義记罚,F(xiàn)astCGI協(xié)議不過是CGI協(xié)議的變種,不同之處僅僅在于WEB服務(wù)器和CGI程序的交互方式壳嚎。CGI協(xié)議中WEB服務(wù)器和CGI程序是通過環(huán)境變量來傳遞信息桐智,WEB服務(wù)器fork+exec來執(zhí)行CGI程序,CGI程序?qū)⑤敵龃蛴〉綐?biāo)準(zhǔn)輸出烟馅,執(zhí)行完成后即退出说庭。而FastCGI做的事情幾乎和CGI一樣,不同點在于FastCGI是通過進(jìn)程間通信來傳遞信息郑趁,比如unix socket或tcp socket刊驴。那么,如果只是這么小的不同,F(xiàn)astCGI協(xié)議的意義何在呢捆憎?FastCGI的意義在于可以讓W(xué)EB應(yīng)用程序架構(gòu)完全變化舅柜,CGI協(xié)議下,應(yīng)用程序的生命周期是一次http請求躲惰,而在FastCGI協(xié)議里面致份,應(yīng)用程序可以一直存在,處理多個http請求再退出础拨,大幅提升了WEB應(yīng)用程序性能氮块。

FastCGI協(xié)議是一個交互協(xié)議,盡管底層傳輸機(jī)制是面向連接的太伊,但是它本身不是面向連接的雇锡。WEB服務(wù)器和CGI程序管理器之間通過FastCGI的消息通信,消息由header和body兩部分組成僚焦。其中header包含的字段如下:

Version: FastCGI協(xié)議版本號锰提,目前一般是1.
Type: 標(biāo)識消息類型。后面會有提到芳悲。
Request ID: 標(biāo)識消息數(shù)據(jù)包所屬的請求立肘。
Content Length: 該數(shù)據(jù)包中body長度

FastCGI主要的消息類型如下:

  • BEGIN_REQUEST:WEB服務(wù)器 => 應(yīng)用程序,請求開始時發(fā)送名扛。
  • ABORT_REQUEST:WEB服務(wù)器 => 應(yīng)用程序谅年,準(zhǔn)備終止正在運行的請求時發(fā)送。常見情況是用戶點擊了瀏覽器的停止按鈕肮韧。
  • END_REQUEST:應(yīng)用程序 => WEB服務(wù)器融蹂,請求處理完成后發(fā)送。這種消息的body會包含一個return code弄企,標(biāo)識請求成功還是失敗超燃。
  • PARAMS:WEB服務(wù)器 => 應(yīng)用程序,稱之為“stream packet”拘领,一個請求里面可能發(fā)送多個PARAMS類型的消息意乓。最后一個body長度為0的消息標(biāo)識這類消息結(jié)束。PARAMS類型消息里面包含的數(shù)據(jù)正是CGI里面設(shè)置到環(huán)境變量里面的那些變量约素。
  • STDIN: WEB服務(wù)器 => 應(yīng)用程序届良,這也是一個“stream packet”,POST相關(guān)數(shù)據(jù)會在STDIN消息中發(fā)送圣猎。在發(fā)送完P(guān)OST數(shù)據(jù)后士葫,會發(fā)送一個空的STDIN消息以標(biāo)識STDIN類型消息結(jié)束。
  • STDOUT: 應(yīng)用程序 => WEB服務(wù)器样漆,這也是一個“stream packet”为障,是應(yīng)用程序發(fā)送給WEB服務(wù)器的包含用戶請求對應(yīng)的響應(yīng)數(shù)據(jù)。響應(yīng)數(shù)據(jù)發(fā)送完成后,也會發(fā)送一個空的STDOUT消息以標(biāo)識STDOUT類型消息結(jié)束鳍怨。

WEB服務(wù)器和FastCGI應(yīng)用程序之間交互流程通常是這樣的:

  • WEB服務(wù)器接收到一個需要FastCGI應(yīng)用程序處理的客戶端請求呻右。因此,WEB服務(wù)器通過unix-socket或者TCP-socket連接到FastCGI程序鞋喇。
  • FastCGI程序看到了到來的連接声滥,它可以選擇拒絕或者接收該連接。若接收連接侦香,則FastCGI程序開始從連接的數(shù)據(jù)流中讀取數(shù)據(jù)包落塑。
  • 如果FastCGI程序沒有在預(yù)期時間內(nèi)接收連接,則請求失敗罐韩。否則憾赁,WEB服務(wù)器會發(fā)送一個 BEGIN_REQUEST 的消息給FastCGI程序,該消息有一個唯一的請求ID散吵。接下來的消息都用這個在header中聲明的同樣的ID龙考。接著,WEB服務(wù)器會發(fā)送一定數(shù)目的PARAMS消息給FastCGI程序矾睦,當(dāng)變量都發(fā)送完成時晦款,WEB服務(wù)器再發(fā)送一個空的PARAMS消息關(guān)閉PARAMS數(shù)據(jù)流。而且枚冗,WEB服務(wù)器會將收到的來自客戶端的POST數(shù)據(jù)通過STDIN消息傳給FastCGI程序缓溅,當(dāng)所有POST數(shù)據(jù)傳輸完成,一樣也會發(fā)送一個空的STDIN類型的消息以標(biāo)識結(jié)束赁温。
  • 同時坛怪,當(dāng)FastCGI程序接收到BEGIN_REQUEST包后,它可以回復(fù)一個END_REQUEST包拒絕該請求股囊,也可以接收并處理該請求酝陈。如果接收請求,則它會等到PARAMSSTDIN包都接收完成再一起處理毁涉,響應(yīng)結(jié)果會通過STDOUT包發(fā)送回WEB服務(wù)器,最終會發(fā)送END_REQUEST包給WEB服務(wù)器讓其知道請求是成功還是失敗了锈死。

有人可能會有點奇怪贫堰,為什么消息頭中需要一個Request ID,如果一個請求一個連接待牵,那這個字段是多余的其屏。也許你猜到了,一個連接可能包含多個請求缨该,這樣就需要標(biāo)識消息數(shù)據(jù)包是屬于哪個請求偎行,這也是FastCGI為什么要采用面向數(shù)據(jù)包的協(xié)議,而不是面向數(shù)據(jù)流的協(xié)議。一個連接中可能混合多個請求蛤袒,在軟件工程里面也稱之為多路傳輸熄云。由于每個數(shù)據(jù)包都有一個請求ID,所以WEB服務(wù)器可以在一個連接中同時傳輸任意個數(shù)據(jù)包給FastCGI應(yīng)用程序妙真。而且缴允,F(xiàn)astCGI程序可以同時接收大量的連接,每個連接可以同時包含多個請求珍德。

此外练般,上面描述的通信流程并不是順序的。也就是說锈候,WEB服務(wù)器可以先發(fā)送20個BEGIN_REQUEST包薄料,然后再發(fā)送一些PARAMS包,接著發(fā)送一些STDIN包泵琳,然后又發(fā)送一些PARAMS包等等摄职。

2.2 FastCGI實例分析

測試環(huán)境配置和抓包

FastCGI實現(xiàn)方式很多,如PHP的php-fpm虑稼,或者比較簡單的fcgiwrap琳钉,在這里,我用fcgiwrap這個比較簡單的實現(xiàn)來分析FastCGI協(xié)議蛛倦,驗證上一節(jié)說的原理歌懒。

先安裝fcgiwrap,可以源碼安裝溯壶,如果是ubuntu/debian系統(tǒng)也可以直接apt-get安裝及皂。通過/etc/init.d/fcgiwrap start啟動fcgiwrap默認(rèn)會以unix-socket方式運行,如果要改成tcp-socket運行且改,可以fcgiwrap -f -s tcp:ip:port這樣運行验烧。

# sudo apt-get install fcgiwrap

在測試的nginx配置的server段里面添加一行

include /etc/nginx/fcgi.conf;

其中fcgi.conf文件內(nèi)容見 fcgi.conf

測試用的cgi程序都放在 /usr/share/nginx/cgi-bin目錄下面又跛。測試cgi程序為 fcgi_hello.sh

在瀏覽器輸入http://192.168.56.18/cgi-bin/fcgi_hello.sh?foo=bar可以看到返回結(jié)果碍拆。

為了避免其他干擾,我沒用tcp-socket運行fcgiwrap慨蓝,這樣為了抓unix-socket的包感混,需要使用socat這個工具。為了抓包礼烈,需要簡單改下nginx的配置弧满,將 /etc/nginx/fcgi.conf中的fastcgi_pass這一行修改下,如下所示此熬。

# fastcgi_pass  unix:/var/run/fcgiwrap.socket;
fastcgi_pass  unix:/var/run/fcgiwrap.socket.socat;

reload nginx并在命令行打開socat命令

socat -t100 -x -v UNIX-LISTEN:/var/run/fcgiwrap.socket.socat,mode=777,reuseaddr,fork UNIX-CONNECT:/var/run/fcgiwrap.socket

此時庭呜,在瀏覽器輸入http://192.168.56.18/cgi-bin/fcgi_hello.sh?foo=bar可以看到socat命令會有輸出如下:

> 2018/01/30 06:16:42.309659  length=960 from=0 to=959
01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00  ................
01 04 00 01 03 92 06 00 0c 07 51 55 45 52 59 5f  ..........QUERY_
53 54 52 49 4e 47 66 6f 6f 3d 62 61 72 0e 03 52  STRINGfoo=bar..R
45 51 55 45 53 54 5f 4d 45 54 48 4f 44 47 45 54  EQUEST_METHODGET
......
66 72 3b 71 3d 30 2e 36 00 00 00 00 00 00 01 04  fr;q=0.6........
00 01 00 00 00 00 01 05 00 01 00 00 00 00        ..............
--
< 2018/01/30 06:16:42.312909  length=136 from=0 to=135
01 06 00 01 00 61 07 00 53 74 61 74 75 73 3a 20  .....a..Status: 
32 30 30 0d 0a                                   200..
43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 74 65  Content-Type: te
78 74 2f 70 6c 61 69 6e 0d 0a                    xt/plain..
0d 0a                                            ..
52 45 51 55 45 53 54 20 4d 45 54 48 4f 44 3a 20  REQUEST METHOD: 
20 47 45 54 0a                                    GET.
50 41 54 48 5f 49 4e 46 4f 3a 20 0a              PATH_INFO: .
51 55 45 52 59 5f 53 54 52 49 4e 47 3a 20 20 66  QUERY_STRING:  f
6f 6f 3d 62 61 72 0a                             oo=bar.
00 00 00 00 00 00 00 01 06 00 01 00 00 00 00 01  ................
03 00 01 00 08 00 00 00 00 00 00 00 00 00 00     ...............

在ubuntu/debian上通過 sudo apt-get install libfcgi-dev后滑进,可以在/usr/local/fastcgi.h中找到各個類型的消息的定義,接下來我們對照上一節(jié)說的FastCGI類型逐個分析下募谎。

分析

WEB服務(wù)器和FastCGI之間通常的交互流程是這樣的扶关,下面會通過抓包詳細(xì)分析。

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\007QUERY_STRINGfoo=bar"}
{FCGI_PARAMS,          1, ""}
{FCGI_STDIN,           1, "id=1&name=ssj"}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

WEB服務(wù)器發(fā)送給FastCGI程序的數(shù)據(jù)包:

  • 第一個消息是 BEGIN_REQUEST近哟,可以看到第1個字節(jié)為01驮审,也就是version為1,第2個字節(jié)為01吉执,即消息類型是 BEGIN_REQUEST疯淫,接著3-4字節(jié)0001是requestId為1。再接著5-6字節(jié)0008是消息體長度為8戳玫。然后7-8字節(jié)0000是保留字段和填充字段熙掺。接著8個字節(jié)就是消息體了,9-10字節(jié)0001為role值咕宿,表示FCGI_RESPONDER币绩,也就是這是一個需要響應(yīng)的消息。11字節(jié)00為flag府阀,表示應(yīng)用在本次請求后關(guān)閉連接缆镣。然后12-16的5個字節(jié)0000000000為保留字段。

  • 第二個消息的第1個字節(jié)是01试浙,也是version為1董瞻,第2個字節(jié)為04,表示消息類型為PARAMS田巴。接著3-4字節(jié)為0001是requestId也是1钠糊。5-6字節(jié)0x0392消息體長度為914字節(jié)。后面7-8是0600位填充字段6字節(jié)壹哺。后面的為消息體內(nèi)容抄伍,也就是QUERY_STRING, REQUEST_METHOD這些在CGI中設(shè)置到環(huán)境變量中的變量和值。接下來是PARAMS消息體管宵。PARAMS消息用的是Name-Value對這種形式組織的數(shù)據(jù)結(jié)構(gòu)截珍,先是變量名稱長度,然后是變量值長度箩朴,接著才是名字和值的具體數(shù)據(jù)笛臣。注意,名和值的長度如果超過1字節(jié)隧饼,則用4個字節(jié)來存儲,具體是1字節(jié)還是4字節(jié)根據(jù)長度值的第一個字節(jié)的最高位來區(qū)分静陈,如果為1則是4字節(jié)燕雁,如果為0則是1字節(jié)诞丽。如此可以分析PARAMS消息體了,頭兩個字節(jié)0c07表示名字長度為12拐格,值長度為7僧免,然后就是13個字節(jié)的變量名QUERY_STRING,7字節(jié)的值foo=bar捏浊,以此類推懂衩,接著的2個字節(jié)0e03就是名字長度為14,值長度為3金踪,變量名是REQUEST_METHOD浊洞,值為GET...后續(xù)數(shù)據(jù)就是剩下的其他變量。最后面的6個字節(jié)000000000000是填充字節(jié)胡岔。

  • 第三個消息也是PARAMS法希,這是一個空的PARAMS消息。第1字節(jié)為01靶瘸,第2字節(jié)為04表示PARAMS苫亦,3-4字節(jié)0001是requestId為1,5-6字節(jié)0000表示消息體長度為0怨咪,7-8字節(jié)0000表示填充和保留字節(jié)為0屋剑。

  • 第四個消息為STDIN,第1個字節(jié)01是version诗眨,第2個字節(jié)05表示類型為STDIN唉匾,接下來是3-4字節(jié)0001是requestId為1,5-6字節(jié)表示消息體長度為0辽话,因為我們沒有POST數(shù)據(jù)肄鸽。后面7-8字節(jié)為0。(如果有POST數(shù)據(jù)油啤,則STDIN這里消息體長度不為0典徘,而它的消息體就是POST的數(shù)據(jù),注意STDIN不是Name-Value對益咬,它是直接將POST的數(shù)據(jù)字段連在一起的逮诲,如這樣id=1&name=ssj)。到此幽告,WEB服務(wù)器發(fā)送給FastCGI程序的數(shù)據(jù)包結(jié)束梅鹦。

FastCGI程序發(fā)送給WEB服務(wù)器的數(shù)據(jù)包:

  • 第一個消息是 STDOUT 。第1個字節(jié)還是01為version冗锁,第2個字節(jié)06表示類型為STDOUT齐唆,接著3-4字節(jié)0001還是requestId,5-6字節(jié)0061為消息體長度97冻河,7-8字節(jié)0700表示填充字段為7字節(jié)箍邮。接下來消息體就是返回的內(nèi)容Status: 200\r\n...茉帅。

  • 第二個消息還是 STDOUT,不過是空的STDOUT消息锭弊,用來標(biāo)識STDOUT消息結(jié)束堪澎。

  • 第三個消息是 END_REQUEST。第1個字節(jié)01還是version味滞,第2個字節(jié)03標(biāo)識類型 END_REQUEST樱蛤,3-4字節(jié)為requestId為1,5-6字節(jié)為消息體大小為8剑鞍,7-8字節(jié)0000為填充字節(jié)長度昨凡。后面消息體內(nèi)容為8個0字節(jié)。也就是說appStatus為0攒暇,protocolStatus也為0.其中protocalStatus是協(xié)議級的狀態(tài)碼土匀,為0表示 REQUEST_COMPLETE,即請求正常完成形用。

// 消息類型定義
#define FCGI_BEGIN_REQUEST       1
#define FCGI_ABORT_REQUEST       2
#define FCGI_END_REQUEST         3
#define FCGI_PARAMS              4
#define FCGI_STDIN               5
#define FCGI_STDOUT              6
#define FCGI_STDERR              7
#define FCGI_DATA                8
#define FCGI_GET_VALUES          9
#define FCGI_GET_VALUES_RESULT  10
#define FCGI_UNKNOWN_TYPE       11
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)

2.3 fcgiwrap分析

fcgiwrap用到了libfcgi庫就轧,libfcgi庫提供了一些函數(shù)封裝,以方便實現(xiàn)fastcgi管理器田度。fcgiwrap啟動參數(shù)如下:

fcgiwrap -f -s unix:/var/run/fcgiwrap.socket -c 2

其中-s指定socket類型妒御,若要用tcp-socket則用 -s tcp:ip:port。-c參數(shù)指定子進(jìn)程數(shù)目镇饺,這里為2個乎莉。

fcgiwrap的核心代碼如下,即先創(chuàng)建一個listen socket奸笤,然后將該socket通過dup2復(fù)制到文件描述符0惋啃,因為libfcgi庫里面固定從fd 0來監(jiān)聽網(wǎng)絡(luò)數(shù)據(jù)。prefork是創(chuàng)建參數(shù)指定數(shù)目的子進(jìn)程數(shù)目监右,然后父進(jìn)程通過pause()調(diào)用停止運行边灭,接著每個子進(jìn)程繼續(xù)往下執(zhí)行fcgiwrap_main()函數(shù)。

int main(int argc, char **argv) {
    fd = setup_socket(socket_url);
    prefork(nchildren);
    fcgiwrap_main();
}

fcgiwrap_main()核心代碼如下健盒,即不停的通過 FCGI_Accept()函數(shù)監(jiān)聽連接并處理請求绒瘦。其中FCGI_Accept()函數(shù)是libfcgi庫提供的,主要作用就是監(jiān)聽listen socket上的請求扣癣,然后根據(jù)fastcgi協(xié)議讀取數(shù)據(jù)并解析為方便處理的結(jié)構(gòu)惰帽,設(shè)置環(huán)境變量environ等,這樣handle_fcgi_request()就能跟cgi程序一樣通過讀取環(huán)境變量還獲取cgi文件名等內(nèi)容父虑。

static void fcgiwrap_main(void)
{
   ...... //略去了一些信號處理代碼
   inherited_environ = environ;

    while (FCGI_Accept() >= 0 && !sigint_received) {
        handle_fcgi_request();
    }
}

handle_fcgi_request()就是處理請求的函數(shù)了该酗,先是fork出子進(jìn)程去執(zhí)行CGI程序,將執(zhí)行結(jié)果寫入到管道中士嚎,而父進(jìn)程則讀取管道中的數(shù)據(jù)并返回給WEB服務(wù)器垂涯。這里有幾點注意下:

  • 子進(jìn)程中代碼dup2(pipe_in[0], 0)執(zhí)行后烁焙,子進(jìn)程從pipe_in[0]作為標(biāo)準(zhǔn)輸入,而父進(jìn)程設(shè)置了 fc.fd_stdin = pipe_in[1]耕赘,在函數(shù)fcgi_pass()中,會先調(diào)用子函數(shù)fcgi_pass_request()讀取FCGI_stdin中的數(shù)據(jù)(也就是前一節(jié)提到的STDIN類型的消息膳殷,也就是POST中的表單數(shù)據(jù))并寫入fc.fd_stdin操骡,也就是寫入到了pipe_in管道中,則子進(jìn)程此時就可以從標(biāo)準(zhǔn)輸入中(因為前面的dup2)讀取到數(shù)據(jù)赚窃。同理册招,子進(jìn)程中代碼dup2(pipe_out[1], 1)即說明子進(jìn)程的標(biāo)準(zhǔn)輸出會輸出到管道pipe_out中,父進(jìn)程在fcgi_pass()中同理可以通過管道讀取到子進(jìn)程的運行輸出結(jié)果(這里fcgi_pass()使用了select()方式來輪詢fd_stdout和fd_stderr文件描述符)勒极。父進(jìn)程讀取到輸出結(jié)果后是掰,返回STDOUTFCGI_END_REQUEST消息給nginx服務(wù)器,完成本次請求辱匿。
static void handle_fcgi_request(void)
{
    int pipe_in[2];
    int pipe_out[2];
    int pipe_err[2];
    char *filename;
    char *last_slash;
    char *p;
    pid_t pid;

    struct fcgi_context fc;

    switch((pid = fork())) {
        case -1:
            goto err_fork;

        case 0: /* child */
            close(pipe_in[1]);
            close(pipe_out[0]);
            close(pipe_err[0]);

            dup2(pipe_in[0], 0);
            dup2(pipe_out[1], 1);
            dup2(pipe_err[1], 2);

            close(pipe_in[0]);
            close(pipe_out[1]);
            close(pipe_err[1]);

            close(FCGI_fileno(FCGI_stdout));

            signal(SIGCHLD, SIG_DFL);
            signal(SIGPIPE, SIG_DFL);

            filename = get_cgi_filename();
            inherit_environment();
            ...... //省略了檢查文件是否存在和文件權(quán)限的代碼

            execl(filename, filename, (void *)NULL);
            cgi_error("502 Bad Gateway", "Cannot execute script", filename);

        default: /* parent */
            close(pipe_in[0]);
            close(pipe_out[1]);
            close(pipe_err[1]);

            fc.fd_stdin = pipe_in[1];
            fc.fd_stdout = pipe_out[0];
            fc.fd_stderr = pipe_err[0];
            fc.reply_state = REPLY_STATE_INIT;
            fc.cgi_pid = pid;

            fcgi_pass(&fc);
    }
    return;

   ...... // 省略部分錯誤處理代碼
    FCGI_puts("Status: 502 Bad Gateway\nContent-type: text/plain\n");
    FCGI_puts("System error");
}

實際應(yīng)用中键痛,像php-fpm(fpm是fastcgi process manager的意思)這種Fastcgi進(jìn)程管理器,它會有master進(jìn)程和worker進(jìn)程匾七,然后統(tǒng)一由master進(jìn)程來分發(fā)請求管理worker絮短,但是用的都是fastcgi協(xié)議,與本文分析的一致昨忆。

3 WSGI

3.1 WSGI規(guī)范

WSGI是Web服務(wù)器網(wǎng)關(guān)接口(Python Web Server Gateway Interface丁频,縮寫為WSGI)是為Python語言定義的WEB服務(wù)器和WEB應(yīng)用程序或框架之間的一種簡單而通用的接口,它與CGI類似邑贴,它不是一種框架席里,也不是模塊,而是一種服務(wù)器(Web Server)和應(yīng)用程序(Web Application)之間規(guī)范拢驾。WSGI協(xié)議實際上是定義了一種WEB服務(wù)器與WEB框架解耦的規(guī)范奖磁,開發(fā)者可以選擇任意的WEB 服務(wù)器和WEB應(yīng)用組合實現(xiàn)自己的web應(yīng)用。例如常用的uWSGI和Gunicorn都是實現(xiàn)了WSGI Server協(xié)議的服務(wù)器(uWSGI還兼有進(jìn)程管理器独旷,監(jiān)控署穗,日志,插件嵌洼,網(wǎng)關(guān)等功能)顿膨,F(xiàn)lask是實現(xiàn)了WSGI Application協(xié)議的應(yīng)用框架(當(dāng)然Flask也自帶有一個簡單的WEB服務(wù)器酌予,雖然我們通常是用nginx來處理靜態(tài)文件),可以根據(jù)項目情況搭配使用。

WSGI分為兩端:服務(wù)器/網(wǎng)關(guān)端 和 應(yīng)用/框架端瞬场,服務(wù)器端調(diào)用應(yīng)用端提供的可調(diào)用的對象。可調(diào)用對象可以是函數(shù)、方法、類或者實現(xiàn)了__call__方法的實例低飒,這取決于服務(wù)器和應(yīng)用選擇哪種實現(xiàn)技術(shù)。除了純正的服務(wù)器和應(yīng)用懂盐,也可以使用中間件技術(shù)來實現(xiàn)該規(guī)范褥赊。

應(yīng)用/框架

應(yīng)用對象就是一個接受兩個參數(shù)的可調(diào)用對象,它可以是函數(shù)莉恼,方法拌喉,類等。應(yīng)用對象必須可以被多次調(diào)用俐银。雖然我們稱之為應(yīng)用對象尿背,但這并不意味著應(yīng)用開發(fā)者要用WSGI作為WEB編程API。應(yīng)用開發(fā)者可以繼續(xù)使用已經(jīng)存在的捶惜、高級框架服務(wù)去開發(fā)他們的應(yīng)用田藐。WSGI 是一個為框架開發(fā)者和服務(wù)器開發(fā)者準(zhǔn)備的工具,應(yīng)用開發(fā)者不需要直接使用 WSGI吱七。

app.py是包含兩個應(yīng)用對象的示例汽久,其中一個是用函數(shù)實現(xiàn),另一個是用類實現(xiàn)陪捷。

服務(wù)器/網(wǎng)關(guān)

服務(wù)器/網(wǎng)關(guān)每次從 HTTP 客戶端收到一個請求回窘,就調(diào)用一次應(yīng)用對象。為了便于說明市袖,這里有個簡單的CGI網(wǎng)關(guān)的例子 server.py啡直,接收請求并調(diào)用應(yīng)用對象app處理請求,實際負(fù)責(zé)處理請求的地方在handles.py中苍碟。

中間件:可以扮演兩種角色

中間件是這樣一種對象酒觅,它既可以作為服務(wù)器端跟應(yīng)用端交互,也可以作為應(yīng)用端跟服務(wù)器端交互微峰。中間件組件通常具備下面幾個功能:

  • 在重寫了環(huán)境變量后舷丹,根據(jù)目標(biāo)URL將請求路由到不同的應(yīng)用對象。
  • 允許多個應(yīng)用或框架在同一個進(jìn)程中依次執(zhí)行蜓肆。
  • 通過轉(zhuǎn)發(fā)請求和響應(yīng)颜凯,支持負(fù)載均衡和遠(yuǎn)程處理。
  • 支持對內(nèi)容進(jìn)行后續(xù)處理仗扬。

中間件的存在對于接口的“服務(wù)器/網(wǎng)關(guān)”和“應(yīng)用/框架”這兩端是透明的症概,并不需要特別的支持。大多數(shù)情況下早芭,中間件必須符合WSGI的服務(wù)器和應(yīng)用程序端的限制和要求彼城。

3.2 WSGI細(xì)節(jié)

規(guī)范細(xì)節(jié)

應(yīng)用對象必須接受兩個位置參數(shù)。為了便于說明,我們將參數(shù)命名為environ和start_response募壕,當(dāng)然你也可以用其他的名稱调炬。服務(wù)器/網(wǎng)關(guān)必須使用位置參數(shù)(非關(guān)鍵字參數(shù))調(diào)用應(yīng)用對象,如result = application(environ, start_response)舱馅。

environ參數(shù)是一個字典對象缰泡,包含CGI風(fēng)格的環(huán)境變量。這個對象必須是一個內(nèi)置的Python字典(不是子類代嗤、UserDict等)匀谣,并允許應(yīng)用程序修改字典。字典還必須包含某些WSGI必需的變量(在后面的章節(jié)中介紹)资溃,還可能包含特定的服務(wù)器的擴(kuò)展變量,按照約定方式進(jìn)行命名烈炭。

start_response參數(shù)是一個可調(diào)用的對象溶锭,它接受兩個必填的位置參數(shù)和一個可選參數(shù)。這三個參數(shù)通常命名為status符隙,response_headers和exc_info趴捅。應(yīng)用程序通常通過start_response(status,response_headers)方式調(diào)用它霹疫。

status參數(shù)是形式為“200 OK”這樣的狀態(tài)字符串拱绑,response_headers是描述HTTP響應(yīng)頭的(header_name,header_value)元組列表丽蝎×圆Γ可選的exc_info參數(shù)僅在應(yīng)用程序捕獲錯誤并嘗試向瀏覽器顯示錯誤消息時使用。start_response必須返回一個write(body_data)的可調(diào)用對象屠阻,它接受一個位置參數(shù)红省,該參數(shù)作為HTTP響應(yīng)主體的一部分。

當(dāng)被服務(wù)器調(diào)用時国觉,應(yīng)用對象必須返回一個產(chǎn)生零個或多個字節(jié)串的迭代吧恃,比如一個Python列表。如果應(yīng)用程序返回的迭代對象具有close()方法麻诀,則服務(wù)器/網(wǎng)關(guān)在結(jié)束當(dāng)前請求前必須調(diào)用該方法痕寓,無論請求是正常完成還是由于迭代期間的因為瀏覽器斷開連接產(chǎn)生了應(yīng)用程序錯誤而提前終止。調(diào)用close()方法是為了釋放應(yīng)用程序的資源蝇闭。

環(huán)境變量

environ字典中必須包含CGI規(guī)范中定義的變量呻率,包括下面這些:

  • REQUEST_METHOD
  • SCRIPT_NAME
  • PATH_INFO
  • QUERY_STRING
  • CONTENT_TYPE
  • CONTENT_LENGTH
  • SERVER_NAME, SERVER_PORT
  • SERVER_PROTOCOL
  • HTTP_Variables

除了CGI定義的環(huán)境變量之外,environ字典中還要包含下面幾個變量:

  • wsgi.version:WSGI版本丁眼,元組(1筷凤,0)表示版本為1.0.
  • wsgi.url_scheme:URL模式,值通常為http或者h(yuǎn)ttps。
  • wsgi.input:可以讀取HTTP請求體的輸入流藐守。(當(dāng)被應(yīng)用對象請求時挪丢,服務(wù)器/網(wǎng)關(guān)執(zhí)行 read ,可以預(yù)讀取請求體卢厂,緩存到內(nèi)存或者磁盤中乾蓬,或者用其他處理輸入流的技術(shù))
  • wsgi.errors:錯誤輸出流。在許多服務(wù)器中慎恒,wsgi.errors通常是服務(wù)器的日志任内。
  • wsgi.multithread:應(yīng)用對象如果支持多線程,則設(shè)置為true融柬。
  • wsgi.multiprocess:應(yīng)用對象如果支持多進(jìn)程死嗦,則設(shè)置為true。
  • wsgi.run_once:如果服務(wù)器/網(wǎng)關(guān)希望應(yīng)用對象在包含它的進(jìn)程中僅執(zhí)行一次這個請求粒氧,它的值為true越除。正常情況下,只有是基于CGI的網(wǎng)關(guān)才是true外盯。

最后摘盆,environ字典也可能包含服務(wù)器定義的變量。這些變量只能使用小寫字母饱苟,數(shù)字孩擂,點和下劃線來命名,并且應(yīng)該用該服務(wù)器/網(wǎng)關(guān)唯一的名稱作為前綴箱熬。例如类垦,mod_python可能會定義名稱為mod_python.some_variable的變量。

輸入流和錯誤流

服務(wù)器提供的輸入流和錯誤流需提供如下方法:

方法
read(size) input
readline() input
readlines(hint) input
iter() input
flush() errors
write(str) errors
writelines(seq) errors

方法含義可以在標(biāo)準(zhǔn)庫中查找坦弟,不過有幾點要注意:

  • 服務(wù)器不能讀取超過客戶端指定的Content-Length的數(shù)據(jù)护锤,而如果應(yīng)用對象嘗試讀取超過Content-Length的內(nèi)容,服務(wù)器應(yīng)該模擬已經(jīng)讀到文件結(jié)束酿傍。服務(wù)器應(yīng)該允許read()在沒有參數(shù)的情況下被調(diào)用烙懦,并返回客戶端輸入流的其余部分。從一個空的或者已經(jīng)讀完的輸入流讀取時赤炒,服務(wù)器應(yīng)該返回空字節(jié)串氯析。

  • 服務(wù)器應(yīng)該支持readline()的可選“size”參數(shù),但是在WSGI 1.0中莺褒,服務(wù)器不支持該參數(shù)也是可以的掩缓。

  • 錯誤流一般不會重讀,因此服務(wù)器/網(wǎng)關(guān)可以直接轉(zhuǎn)發(fā)寫入操作遵岩,而不需要緩沖你辣。在這種情況下巡通,flush()方法可能是沒有操作的。但是舍哄,便攜式應(yīng)用程序不能假定輸出是無緩沖的或者flush()是無操作的宴凉。如果他們需要確保輸出已經(jīng)被寫入,他們必須調(diào)用flush()表悬,而不管flush()具體做了什么弥锄。

  • 上表中列出的方法必須得到符合本規(guī)范的所有服務(wù)器的支持。符合本規(guī)范的應(yīng)用程序不得使用輸入流或錯誤流對象的任何其他方法或?qū)傩泽∧L貏e是籽暇,應(yīng)用程序不能嘗試關(guān)閉這些流,即使它們擁有close()方法饭庞。

start_response

start_response是傳遞給應(yīng)用對象的第二個參數(shù)是可調(diào)用對象(通常就是個函數(shù))戒悠,start_response(status,response_headers舟山,exc_info = None)救崔。 (與所有的WSGI可調(diào)用對象參數(shù)一樣,這里必須是位置參數(shù)捏顺,不能用關(guān)鍵字參數(shù))。start_response用于開始HTTP響應(yīng)纬黎,它必須返回一個write(body_data)的可調(diào)用對象幅骄。

status參數(shù)就是"200 OK"或者"404 Not Found"這種狀態(tài)字符串,由狀態(tài)碼和狀態(tài)說明組成的字符串本今,由一個空格分開拆座,沒有周圍的空格或其他字符(更多請參見RFC2616第6.1.1節(jié))。字符串不能包含控制字符冠息,也不能以回車挪凑、換行符或它們的組合結(jié)束。

response_headers參數(shù)是(header_name逛艰,header_value)元組的列表躏碳。它必須是一個Python列表類型,并且服務(wù)器可以任意改變其內(nèi)容散怖。每個header_name必須是一個有效的HTTP頭部字段名稱(由RFC2616菇绵,第4.2節(jié)定義)。header_nameheader_value不能包含任何控制字符(包括回車符或換行符)镇眷。服務(wù)器/網(wǎng)關(guān)負(fù)責(zé)確保向客戶端發(fā)送正確的響應(yīng)頭部:如果應(yīng)用對象省略了HTTP響應(yīng)所需的頭部咬最,則服務(wù)器/網(wǎng)關(guān)必須添加它。例如欠动,HTTP的Date和Server頭部通常由服務(wù)器/網(wǎng)關(guān)提供永乌。(注意:HTTP頭部字段不區(qū)分大小寫,因此在檢查應(yīng)用程序提供的頭部時務(wù)必考慮這一點!)翅雏。禁止應(yīng)用對象使用 HTTP 1.1的 hop-by-hop 特性或者頭(如Keep-Alive)圈驼,以及任何在 HTTP/1.0中等價的特性,或任何影響客戶端到 web服務(wù)器端持久化連接的頭部枚荣。

服務(wù)器應(yīng)該在調(diào)用start_response的時候檢查頭文件中的錯誤碗脊,以便應(yīng)用程序仍在運行時拋出錯誤。但是橄妆,start_response實際上并不傳輸響應(yīng)頭衙伶。相反,它必須將它們存儲在服務(wù)器/網(wǎng)關(guān)上害碾,以便僅在應(yīng)用返回值時或者在應(yīng)用首次調(diào)用write()時傳輸矢劲。響應(yīng)頭傳輸?shù)倪@種延遲是為了確保緩沖和異步應(yīng)用程序可以用錯誤輸出來替換它們原來預(yù)期的輸出,直到最后的可能時刻慌随。例如芬沉,如果在應(yīng)用程序緩沖區(qū)內(nèi)生成正文時發(fā)生錯誤,則應(yīng)用程序可能需要將status從“200 OK”更改為“500 Internal Error”阁猜。

exc_info參數(shù)(如果提供)必須是Python sys.exc_info()元組丸逸。只有在錯誤處理程序調(diào)用start_response的情況下,應(yīng)用程序才能提供此參數(shù)剃袍。如果提供了exc_info黄刚,并且還沒有發(fā)送HTTP頭,start_response應(yīng)該用新提供的頭部替換當(dāng)前存儲的HTTP頭部民效,從而允許應(yīng)用程序在發(fā)生錯誤時“改變主意”憔维。但是,如果此時已經(jīng)發(fā)送了HTTP頭部畏邢,則start_response必須再次拋出異常业扒。

處理Content-Length頭部

如果應(yīng)用程序提供Content-Length頭,則服務(wù)器不應(yīng)該發(fā)送比Content-Length更多的字節(jié)舒萎,并且應(yīng)該在發(fā)送完Content-Length字節(jié)后停止發(fā)送響應(yīng)程储,如果應(yīng)用程序此時還繼續(xù)嘗試寫入,則應(yīng)該拋出錯誤)臂寝。

如果應(yīng)用程序不提供Content-Length頭部虱肄,則服務(wù)器或網(wǎng)關(guān)可以選擇幾種方法之一來處理它。最簡單的是在響應(yīng)完成時關(guān)閉客戶端連接交煞。但是咏窿,在某些情況下,服務(wù)器/網(wǎng)關(guān)可以生成一個Content-Length頭部來避免關(guān)閉客戶端連接素征。注意:應(yīng)用程序和中間件的輸出中不能使用任何類型的Transfer-Encoding集嵌,例如chunking或gzip萝挤,這些傳輸編碼是Web服務(wù)器/網(wǎng)關(guān)的職責(zé)

緩沖和流

一般來說根欧,應(yīng)用程序通過緩沖輸出并一次發(fā)送全部數(shù)據(jù)來實現(xiàn)最佳吞吐量怜珍。在Zope等現(xiàn)有框架中,這是一種常見的方法:輸出緩存在一個StringIO或類似的對象中凤粗,然后與響應(yīng)頭一起傳輸酥泛。

3.3 實現(xiàn)

總體上看來,WSGI服務(wù)器端就是接收請求嫌拣,設(shè)置好環(huán)境變量柔袁,然后調(diào)用應(yīng)用對象處理請求。而應(yīng)用對象調(diào)用start_response函數(shù)設(shè)置頭部(注意异逐,此時還沒有返回響應(yīng)給客戶端)捶索,然后應(yīng)用對象返回一個可迭代對象(如Python的列表)給服務(wù)器端。服務(wù)端對應(yīng)用對象返回的迭代數(shù)據(jù)進(jìn)行輸出灰瞻,輸出前會先調(diào)用send_headers()來發(fā)送響應(yīng)頭部腥例。

完整的示例代碼參見 web-basis-wsgi,代碼基本來源于Python自帶的wsgiref和http相關(guān)模塊酝润。

4 uWSGI和uwsgi協(xié)議

4.1 uWSGI安裝配置

uWSGI是一個WEB服務(wù)器燎竖,它實現(xiàn)了WSGI協(xié)議、uwsgi協(xié)議要销、http協(xié)議等底瓣。這里要區(qū)分下:uWSGI是WEB服務(wù)器,而小寫的uwsgi是協(xié)議蕉陋。安裝uWSGI的步驟比較簡單,如下:

# sudo apt-get install build-essential python-dev
# sudo pip install uwsgi

然后我們可以編寫一個簡單的符合WSGI規(guī)范的python程序:

# foobar.py
def application(env, start_response):
    start_response('200 OK', [('Content-Type','text/html')])
    return [b"Hello World"]

運行:

# uwsgi --http :9090 --wsgi-file foobar.py

此時拨扶,我們就可以在瀏覽器輸入 http://127.0.0.1:9090來訪問了凳鬓。指定參數(shù)--http則是以HTTP服務(wù)器方式運行,在實際項目中患民,通常會以socket的方式運行缩举,nginx負(fù)責(zé)處理靜態(tài)資源,動態(tài)請求則由nginx通過uwsgi協(xié)議與uWSGI服務(wù)器交互匹颤。

配置nginx如下:

# /etc/nginx/sites-enabled/uwsgi
server {
    listen 9090;
    location / {
        include uwsgi_params;
         uwsgi_pass 127.0.0.1:3031;
    }
}

以socket的方式運行uWSGI如下(加了進(jìn)程和線程數(shù)配置):

uwsgi --socket 127.0.0.1:3031 --wsgi-file foobar.py --master --processes 4 --threads 2

為了方便仅孩,可以將啟動參數(shù)放到配置文件 config.ini中,然后 uwsgi config.ini即可印蓖。

## config.ini示例
[uwsgi]
uid = nobody
gid = nogroup
socket = 127.0.0.1:3031
chdir = /home/vagrant/project/uwsgi
wsgi-file = foobar.py
processes = 4
threads = 2

如果nginx里面配置的是proxy_pass http://127.0.0.1:3031辽慕,則此時需要將uwsgi以 http-socket的方式運行,即

uwsgi --http-socket 127.0.0.1:3031 --wsgi-file foobar.py --master --processes 4 --threads 2 

4.2 uwsgi協(xié)議

前面我們分析過fastcgi和wsgi協(xié)議赦肃,而uwsgi是uWSGI獨有的用于與WEB服務(wù)器通信的協(xié)議溅蛉,它是一個字節(jié)協(xié)議公浪,可以傳輸任意數(shù)據(jù)類型,nginx也已經(jīng)支持uwsgi協(xié)議船侧,如我們前面用到的uwsgi_pass欠气。

uwsgi協(xié)議的包頭如下:共32位,前面8位為標(biāo)識镜撩,中間16位是數(shù)據(jù)包大小预柒,最后8位也為標(biāo)識。我這里分析的是modifier1為0的情況袁梗,即數(shù)據(jù)包為WSGI變量宜鸯,datasize為WSGI塊變量大小(不包括請求體)。其他選項詳細(xì)含義可以參見 uwsgi-protocol.

struct uwsgi_packet_header {
    uint8_t modifier1;
    uint16_t datasize;
    uint8_t modifier2;
};

struct uwsgi_var {
    uint16_t key_size;
    uint8_t key[key_size];
    uint16_t val_size;
    uint8_t val[val_size];
}

實例分析:

以前面例子說明围段,我們使用tcpdump命令查看nginx和uWSGI之間的通信包

tcpdump -i lo port 3031 -n -X -vvvv

運行 curl -i http://127.0.0.1:9090
可以看到如下輸出:

 127.0.0.1.36705 > 127.0.0.1.3031
    0x0000:  4500 0193 9511 4000 4006 a651 7f00 0001  E.....@.@..Q....
    0x0010:  7f00 0001 8f61 0bd7 441d 5d4e c7f6 50e7  .....a..D.]N..P.
    0x0020:  8018 02ab ff87 0000 0101 080a 0009 c842  ...............B
    0x0030:  0009 c842 005b 0100 0c00 5155 4552 595f  ...B.[....QUERY_
    0x0040:  5354 5249 4e47 0000 0e00 5245 5155 4553  STRING....REQUES
    0x0050:  545f 4d45 5448 4f44 0300 4745 540c 0043  T_METHOD..GET..C
    ...
    
127.0.0.1.3031 > 127.0.0.1.36705
   ...
    0x0030:  0009 c842 4854 5450 2f31 2e31 2032 3030  ...BHTTP/1.1.200
    0x0040:  204f 4b0d 0a43 6f6e 7465 6e74 2d54 7970  .OK..Content-Typ
    0x0050:  653a 2074 6578 742f 6874 6d6c 0d0a 0d0a  e:.text/html....
    0x0060:  4865 6c6c 6f20 576f 726c 64              Hello.World

除去前面ip和tcp包頭顾翼,可以看到實際內(nèi)容從 005b 0100開始,即uwsgi包的長度為 0x015b共347字節(jié)奈泪,后面則是實際內(nèi)容适贸,如QUERY_STRING這個變量key長度為12(0c00),后面緊跟key涝桅,而value長度為0(0000)拜姿,沒有內(nèi)容。后面REQUEST_METHOD的key長度為14(0e00)冯遂,value為GET蕊肥,長度為3(0300),依此類推蛤肌,跟fastcgi協(xié)議有點相似壁却,這些變量來自/etc/nginx/uwsgi_params中定義和nginx添加的HTTP_USER_AGENT等HTTP變量。uWSGI發(fā)送給nginx的響應(yīng)報文則是標(biāo)準(zhǔn)的HTTP響應(yīng)裸准。

4.3 uWSGI與應(yīng)用框架組合使用

uWSGI可以與Flask展东,Django,web2py等框架組合使用炒俱,在我們的實際項目中盐肃,架構(gòu)通常是nginx + uWSGI + Flask,即靜態(tài)請求由nginx處理权悟,動態(tài)請求轉(zhuǎn)發(fā)到uWSGI砸王,然后組合使用Flask框架來編寫業(yè)務(wù)邏輯,當(dāng)然里面通常還會用到gevent等峦阁。

uWSGI與Flask組合使用也很簡單谦铃。因為Flask框架將WSGI的可調(diào)用應(yīng)用對象暴露出來了,我們只要在啟動參數(shù)中指明入口的app即可榔昔。先安裝flask模塊:

# sudo pip install flask

然后編寫flask程序如下:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return "<span style='color:red'>I am app 1</span>"

然后啟動uWSGI(比之前只是多一個 --callable 參數(shù)):

uwsgi --socket 127.0.0.1:3031 --wsgi-file flaskapp.py --callable app --processes 4 --threads 2

5 總結(jié)

從CGI荷辕,F(xiàn)astCGI凿跳,WSGI到uWSGI,涉及內(nèi)容很基礎(chǔ)也很繁雜疮方,本文只是拋磚引玉控嗜,說出了我對這些概念的基本理解,如有錯漏骡显,懇請指正疆栏。實際項目中用到的架構(gòu)如 nginx + uWSGI + Flask (配合gevent,mysql線程池等)惫谤,后面有時間再做總結(jié)壁顶。

6 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市溜歪,隨后出現(xiàn)的幾起案子若专,更是在濱河造成了極大的恐慌,老刑警劉巖蝴猪,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件调衰,死亡現(xiàn)場離奇詭異,居然都是意外死亡自阱,警方通過查閱死者的電腦和手機(jī)嚎莉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沛豌,“玉大人趋箩,你說我怎么就攤上這事〖优桑” “怎么了叫确?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長芍锦。 經(jīng)常有香客問我竹勉,道長,這世上最難降的妖魔是什么醉旦? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮桨啃,結(jié)果婚禮上车胡,老公的妹妹穿的比我還像新娘。我一直安慰自己照瘾,他們只是感情好匈棘,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著析命,像睡著了一般主卫。 火紅的嫁衣襯著肌膚如雪逃默。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天簇搅,我揣著相機(jī)與錄音完域,去河邊找鬼。 笑死瘩将,一個胖子當(dāng)著我的面吹牛吟税,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姿现,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼肠仪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了备典?” 一聲冷哼從身側(cè)響起异旧,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎提佣,沒想到半個月后吮蛹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡镐依,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年匹涮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片槐壳。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡然低,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出务唐,到底是詐尸還是另有隱情雳攘,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布枫笛,位于F島的核電站吨灭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏刑巧。R本人自食惡果不足惜喧兄,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望啊楚。 院中可真熱鬧吠冤,春花似錦、人聲如沸恭理。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颜价。三九已至涯保,卻和暖如春诉濒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背夕春。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工未荒, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撇他。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓茄猫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親困肩。 傳聞我的和親對象是個殘疾皇子划纽,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容