CGI應用程序開發基礎

CGI 腳本結構
計劃你的CGI腳本
標準CGI環境變量
CGI 腳本的移植性
CGI 庫
CGI 的侷限
1.CGI 腳本結構
當腳本被服務器引發時,服務器常常以兩種途徑之一向腳本傳遞信息:GET或POST。這兩種方法被稱爲請求方法。所使用的請求方法是通過環境變量傳給腳本,該環境變量叫作REQUEST_METHOD(還定義了另外兩種請求方法一HEAD和PUT,但它們不是特別應用於CGI,並且不鼓勵使用它們)。
1)GET是對數據的一個請求——同樣的方法被用於獲得靜態文檔。GET方法以附加在URL後面的參數發送請求信息。這些參數將放在環境變量QUERY_STRING中傳給CGI程序。例如,有一個叫作Myprog.exe的腳本,從如下的鏈接啓動它:
REQUEST_METHOD是GET,QUERY_STRING包含lname=b1ow&fname=joe。在“URL一編碼”中將討論QUERY_STRING的格式。
問號從QUERY_STRING的起始處分隔開腳本名字。在一些服務器上,問號是強制性的,即使後面沒有跟着QUERY_STRING。另一些服務器則允許用一個正斜槓代替問號或與之附加在一起。如果使用斜槓,服務器則用PATH_INFO而不是QUERY_STRING變量將信息傳給腳本。(URL解碼)
2)當瀏覽器將數據從一個填寫表單傳給服務器時,發生POST操作。對於POST,QUERY一STRING可能爲空或不空,這有賴於服務器。如果有信息,則其如GET的情況一樣被格式化和傳遞。
來自POST查詢的數據使用STDIN從服務器傳到腳本。由於STDIN是一個源,腳本需要知道有多少有效數據。於是服務器還提供了另一個變量,CONTENT_LENGTH,以指出到來數據的字節數。而POST的數據格式爲:
variable1=value1&variable2=value2&etc
你的程序必須檢查REQUEST_METHOD環境變量以知道是否要讀取STDIN。CONTENT_LENGTH變量一般只在REOUEST_METHOD爲POST時有用。
CGI應用的基本結構既簡單又直接明瞭:初始化、處理、輸出和終止。由於討論的是概念、數據源、編程規則,所以在例子中將使用僞碼而不是使用某種特定語言。
理想情況下,一個腳本具有如下形式(do-initialize,do-process和do-output代表恰當的子例程):
程序開始
調用 do-initialize
調用 do-proces
調用 do一output
程序結束。
實際情況並非這麼簡單。
1.1 初始化
腳本啓動後必須做的第一件事是確定其輸入、環境和狀態。基本操作系統環境信息能以通常方式得到:在Windows NT或windows95中從系統註冊區得到,在Unix系統中從標準環境變量得到,在別的Windows版本中從INI文件得到,等等。
狀態信息來自於輸入,而不是操作環境或靜態變量。記住:每當CGI腳本被引發時,它都好象此前從未被引發過。腳本不在調用之間持續運行,所有的東西都必須從頭初始化,如下:
1.確定腳本是如何被引發的
典型情況下,這涉及讀取REQUEST_METHOOD環境變量並分析其中的單詞GET或POST。
注意
儘管當前定義應用於coi的操作只有GET和posT,你或許會時不時地遇到PUT或HEAD,假王口你的服務器支持它並且用戶的剜覽器或一個機器人使用它就可能發生這種情況。 PUl7k作爲PosT的另選提供,但從未得多(認可的RFC資格,一般不被使用。HEAD被一些剜覽器fotL器人(自動剜覽器賬用,僅用於提取HTML文在的頭部,不適用於C6路程。此外還有一些古怪的請求方法。你的代碼應該檢查是否爲GET和PosT,拒絕任何其他方法,不要假設請求方法如果不是GET便是PosT,或者相反。
2.提取輸入數據
如果方法是GET,必須獲得、分析、解碼QUERY_STRING環境變量。如果方法是POST,必須檢查QUERY_STRING並還要分析STDIN。如果CONTENT_TYPE環境變量是設爲application/x-www-form-urlencoded,來自STDIN的源也需要解碼。
1.2處理
腳本通過讀取和分析其輸入從而對環境初始化之後,便準備進入工作。在此階段發生的事情則遠沒有初始化階段那樣確定。在初始化時,參數是知道的(或是可以被發現),所要做的任務對於各個腳本都多多少少地相同。然而,處理階段是腳本的核心,在此時要做的事情幾乎完全依賴於腳本的目標。
1.處理輸入數據
此時做什麼取決於腳本。例如,你可以忽略全部輸入而僅僅輸出數據,可能以有條理格式化的HTML將輸入在吐出去,或許會在一個數據庫中獵取信息在將其顯示出來,或者是從前沒有想到的任何事情。處理數據一般意味着,以某種方式對其進行轉換。在傳統的數據處理術語中,這叫做轉換步驟,因爲,在面向批作業的處理中,程序讀取一個記錄並對其施加一些規則(轉換它),然後將其寫回。CGI 程序很少被看作傳統的數據處理,但思想是一樣的。程序的處理數據階段不同的CGI 程序,——在數據處理階段,你拿到輸入,並從其中做出一些新的東西來。
2.輸出結果
在一個簡單的CGI腳本中,輸出常常只是一個頭部和一些HTML。更復雜些的腳本可能;輸出圖形、圖形與文本的混和,或者爲了用一些附加信息再次調用腳本而必要的全部信息。一個常用並且更精巧的技術是使用GET調用腳本一次,這可以用一個標準的標記做到。腳本可以感知它是用GET調用的,並動態地創建HTML表單一一包括隱藏變量和再次用POST調用腳本所需的代碼。
兼容性問題
在UNIX世界中,字符流是一種特殊的文件。默認地,STDIN和STDOUT是字符流。操作系統很有幫助地爲你分析流,確保所通過的全是正確的7-bitASCII碼,或者是認可的控制碼。
7-bit?是的。對於HTML,這沒有問題。然而,如果你的腳本發送圖形數據,使用面向字符的流則意味着立即失敗。解決方法是將流切換到二進制模式。在C語言中,可以使用setmode函數:setmode(fileno(stdout),O_BINARY)。通過setmode(fi1eno(stdout),O_TEXT)在流當中進行切換。一個典型的圖形腳本以字符模式輸出頭部,而後切換到二進制模式用於圖形數據。
在windows NT世界中,爲了兼容性,流有着同樣方式的行爲。輸出中的一個簡單\n,當寫到STDOUT時,被變換爲/r/n。一般的windows NT調用,如write Fi1e(),不發生上述變換,如果同時想要一個回車和一個換行,則必須顯式地指出/r/n。
字符模式和二進制模式的另一種說法是cooked和raw,知道這兩個名詞的人或許會使用它們,而不是更常見的說法。不管使用什麼詞,在什麼平臺上,關於流存在着另一問題:默認情況下,它們是有緩衝區的,意思是操作系統掛起數據,直至看見一個行結束符、緩衝區滿或者流被關閉。這意味着,你如果將有緩衝區的prinif()語句同無緩衝區的fwriie()或fprintf()語句混合在一起,事情可能就變得混亂了,儘管它們都會是寫到STDOUT。printf()有緩衝區地將數據寫到流,面向文件的例程則無緩衝區地輸出數據。結果是亂序的一團糟。
你可能將此歸咎於後向兼容性。除了許多老程序之外,流實在沒理由將默認定爲有緩衝區和cooked。這應當是在需要時可以打開的選項,而不是在不要時關閉。幸運的是,你能夠用setvbuf(stdout,NULL,_IONBF,0)解決這一困難,這個函數關閉UTDOUT流的全部緩衝區。
另一個解決是避免混和不同類型的輸出語句,即使這樣,也不能使cooked輸出變成raw。所以最好是關閉所有緩衝區。許多服務器和瀏覽器不喜歡接收單調乏味的輸入。
注意
那些常把UNIX掛在嘴邊的人可能會對名詞CRLF(回車與換行)皺眉,而那些在其他平臺上編程的人也許不認識/n或/r/n。CRLF等於/r/n。C編程者用/r表示一個回車(CR)符號,用/n表示一個換行(LF)符。(對於Basic編程,LF是Chr$(10,CR是Chr$(13)。)
1.3 終止
終止就是清理和退出。你如果對任何文件加了鎖,則必須在程序結束前釋放它們。你如果分配了內存、信號量或其他對象,也必須進行釋放。不正確完成這些會導致腳本“曇花只能一現”。即腳本在第一次調用時能工作,而在以後的調用中就會崩潰。更有甚者,腳本由於沒有正確釋放資源和鎖,將會妨礙甚至破壞其他腳本或服務器本身。
在一些平臺上一Windows NT最顯著, UNIX次之——文件句柄和內存對象在進程終止時會被關閉和收回。即使這樣,依賴操作系統爲你清理垃圾也非明智之舉。例如,在Windows NT上,如果一個程序對一個文件全部或部分加鎖,而後不釋放鎖便終止,則文件系統的行爲將是不確定的。
必須確保你的出錯一退出例程——如果有(也應該有)——瞭解腳本的資源並能象主退出例程一樣徹底地對它們進行清理。
2.計劃腳本
現在讀者已經看到了一個腳本的基本結構,下面將要學習如何從頭計劃一個腳本。按照如下基本步驟進行:
用一些時間定義程序的任務。整體、周到地考慮一下,把它寫下來並描繪程序邏輯。當你已經很好理解了輸入、輸出和必須做的轉換處理之後,纔可以往下繼續。
預訂好食物和飲料,把自己關在屋裏一晚上,第二天便可以帶着完成的程序出來了。如果前面第1步做得好,那麼實際編寫程序是算不了什麼的(編寫代碼時不要忘記爲它做文檔)。
測試、測試、測試。試一試各種知道的瀏覽器和各種能想到的輸入。尤其檢測一下諸如用戶在一個10字節字段中輸入32KB數據,或者在期望爲簡單文字的地方輸入控制代碼等等這些情況。
將整個程序文檔作爲一個整體——不僅僅針對其中的單個步驟——以便讓其他人維護或改編代碼時能夠理解你的意圖。
當然,本節的話題是上面的第一步,因此下面讓我們更深入地看一看此過程:
如果你的腳本處理表單變量,計劃出每個變量的名字、預期長度、數據類型。
當你從QUERY_STRING或STDIN拷貝變量時,檢查類型和長度是否正確。UNIX破壞者的一個慣用技倆是蓄意讓緩衝區溢出,鑑於一些腳本語言(顯著的是sh和bash)爲變量分配內存的方式,使得破壞者能夠訪問本應受到保護的內存,他們能在你腳本的堆或棧空間放置可執行指令。
使用有意義的變量名字。一個指向環境變量QUERY_STRING的指針應該叫作類似PQueryString的名字,而不是P2。這不僅在一開始有助於調試,也能簡化維護和修改工作。不管代碼有多漂亮,也免不了在一年後想不起來P1是指向CONTENT_TYPE而P2指向QUERY_STRING。
區分系統級參數和用戶級參數。前者影響程序如何操作而後者提供實例特定的信息,例如,在一個發送電子郵件的腳本中,不要讓用戶指定SMTP主機的IP號。這個信息甚至不應該出現在隱藏變量裏的表單上。它是實例無關的,因而應當是一個系統級參數。在Windows NT中,將該信息存在註冊區裏,在UNIX中,將它放入一個配置文件或系統環境變量。
如果你的腳本退出外殼(shell out)而到系統去加載另一程序或腳本,不要傳遞未經檢查的用戶給出的變量——尤其在UNIX系統中,那裏system()調用可包含管道或重定向符使不經檢查的變量可引起災難。聰明的用戶和惡意的竊入者會用這種方式拷貝敏感信息或破壞數據。你如果不能完全避免system()調用,則要小心地計劃。確切定義什麼能作爲一個參數傳遞,並且知道哪些bit來自用戶。在程序中包含一個分析可疑字符串並將其排斥掉的算法。
如果你的腳本存取外部文件,則要對如何處理併發做出計劃。你可能會加鎖部分或全部數據文件,建立一個信號量,或者使用一個文件作爲一個信號量,決不要假想你的腳本是存取某一給定文件的唯一程序而毫不顧慮併發問題。你的腳本的5個拷貝可能會同時運行用以滿足來自5個不同用戶的請求。
注意
編程者使用信號量來同步多個程序、同一程序的多個實例,甚至是單個程序內的多個例程。一些操作系統具有對信號量的內置支持,另一些則要求編程者建立信號量策略。
以最簡單的含義,信號量象一個開關,它的狀態可以被檢測:開關是打開的嗎?如果是,則這樣做;否則那樣做,文件經常被用作信號量(文件存在否?存在則這樣做,否則那樣做)。一個更復雜的方法是向文件加鎖以實現互斥存取(如果能得到鎖,這樣做,否則,等待一會兒並重試)。
在CGI編程中,信號量經常被用於同步同一CGI腳本的多個實例。舉例來說,如果你的腳本必須更新一個文件,它不能假設文件隨時可以得到。如果恰好該腳本的另一實例正在更新文件之中呢?第二個進程則必須等待,直至前一個完成,否則文件會被致命地破壞掉。解決的辦法是使用一個信號量。要檢測你的腳本以確保信號量被清掉。如果沒有,它進入一個短循環,間隔地檢測信號量。當信號量已被清掉,應設置信號量以避免其他程序介入,而後,便執行其臨界區一一在這種情況下,寫入文件一一然後再次清除信號量,使其他實例又能得到機會。信號量就是這樣提供了一種管理併發安全性的方式。
如果需要加鎖文件,則應使用最小限定。當僅僅讀取一個數據文件時,對寫加鎖,並在讀完之後立即釋放鎖。當更新一條記錄時,只對記錄(或一定範圍的字節加鎖。理想情況下,鎖邏輯應該緊緊圍繞實際I/O調用。不要在程序一開頭便打開一個文件並鎖住它直至終止。如果必要,可以立刻打開文件但不要加鎖,直到真正要用它時,這樣能讓其他應用或者你腳本的其他實例能工作平滑和快速。
爲意外事件準備良好的退出。舉例來說,如果你的程序要求互斥地存取一個特定資源,準備好等待一段合理時間而後優雅地退出。決不要編寫一個永久等待的調用。當你的程序從一個致命錯誤消亡時,要確保它在臨終前對錯誤進行報告。錯誤報告應該使用簡單明白的語言。如果可能,還應將錯誤寫進一個日誌文件,使得系統管理員能夠知道它。
你如果在使用一種GUI語言來編寫CGI腳本,不要把捕獲的錯誤表現爲一個屏幕上的消息盒。這是一個服務器應用,錯誤將很少被人注意到並且清除,你的程序會擋在那裏直至系統管理員偶爾路過。埋藏所有的錯誤,在能夠存活的地方工作,把其他的都看成天災吧。
在啓動代碼編輯器之前,爲你的例程寫僞碼,至少到一般邏輯結構一級。這常常有助於建立存根例程,使你能在開發中在程序時使用實際調用。存根例程(stub routine)是一個權宜之計,它並不實際做任何處理,僅僅接收最終例程期待的輸入,返回結果一致的代碼。
對於複雜的項目,一個數據流圖將大有稗益。數據流應該有別於邏輯源。數據在程序中按照某條路徑流動,爲各個程序片段所擁有,不管它是如何被子例程變換的。
儘量封裝私有數據和處理。你的例程應該有一個確定的輸入和輸出——一個門進,一個門出,並且要知道通過大門的是什麼。你的例程如何完成其任務,這不是調用例程的事。這叫作“黑匣子方法”。從外面不應該看到箱子裏面發生了什麼,也不應對它產生影響。例如,一個正確封裝的使用平面文件表的查找例程可以被置換爲一個與後端數據庫打交道的例程,而不用對程序的其餘部分做何改變。
在進行中爲程序做文檔。自組織文檔的代碼是最好的方法,帶有一般的註釋和用於分隔代碼的額外空行,如果使用了含義明確,很有說明性的變量和函數名,則事情已經做了一半。但好的文檔不僅僅指出一般代碼是做什麼的,還要說明爲什麼這樣做。例如:“給REQUEST-METHOD賦值pRequestMethod”指出代碼是做什麼的,“確定是否由GET或POST引發”則說明爲何編寫該段代碼,並且,更理想的是引出下一段代碼和文檔:“如果由 GET啓動,做這個”或“如果由POST啓動,做這個。”
象計劃輸入那樣仔細地定義輸出。你給用戶的消息應該是標準化的。例如,不要象這樣報告一個文件加鎖問題:“Couldn't obtain lock,Please try again later”,而報告一個棧溢出錯誤爲“ERR4332”,成功消息也應該具有一致性,不要一次返回:
you are the first visitor to this site since l/1/96
而下次回返:
you are visitor number 2 since 01-01-96
如果按邏輯分類數據流和分組函數,則每種類型的消息應由相應於那種類型的例程產生。如果你把帶有錯誤消息和ear1y-out成功消息的代碼加入到程序的邏輯流中,那麼終端用戶來說,你的程序看上去不太一致,而對任何維護你的代碼的人來說它是一團糟。
注意
early-out算法是一種用預先定義好的答案來檢測異常和無意義情況並退出的算法,它不是以執行算法來決定答案的。例如,除法算法通常以兩個操作檢測一個除,並做一個移位而非除。
3 標準CGI環境變量
這裏對常遇到的標準環境變量作一簡要總結。各個服務器一致地實現了其中大部分, 但也有變化、例外和附加的情況。一般地,你更可能找到一個新的、沒有歸檔的變量而非一個省略的歸檔變量。那麼,唯一用來確認的辦法就是檢查你的服務器文獻。本節內容來自於NCSA規範 ,是你所能找到的最接近“標準”的規範。NCSA CGI規範的URL如下:
http://www.w3.org/hypertext/WWW/CGI/
每當服務器加載腳本的一個實例時下述環境變量被設置,並且是私有和特定於該實例的:
AUTH_TYPE
如果服務器支持基本的認證並且如果腳本被保護,此變量提供認證類型,此信息是特定於協議和服務器的。AUTH_TYPE的一個例子是BASIC。
CONTENT_LENGTH
如果請求通過POST方法包括數據,此變量被設置爲提供通過STDIN的字節的合法數據的長度——如,72。
CONTENT_TYPE
如果請求包括數據,此變量指定數據類型爲一個MIME頭一一例如,application/x-www-form-urlencoded
GATEWAY_INTERFACE
它提供被服務器支持的CGI接口的版本數,其格式爲CGI/版本數:如CGI/1.1。
HTTP_ACCEPT
提供由逗號分開並被客戶服務器可接受的MIME類型的列表,如image/gif,image/x-xbitmap,image/jpeg,image/pjpeg和*/*。此列表實際上來自瀏覽器本身,服務器只是將它傳到CGI腳本。
HTTP_USER_AGENT
提供可能包含版本數或其他專有數據的客戶沏覽器名,如 Mozilla/2.0b3(Win NT;I)。
PATH_INFO
顯示由客戶提供並附在虛擬路徑尾的任何附加的路徑信息。它通常被用作腳本的參數 。例如,在URL
http://www.yourcompany.com/cgi-bin/myscript.pl/dir1/dir2中,腳本爲myscript.pl,PATH_INFO爲/dirl/dlr2。
PATH_TRANSLATED
僅由部分服務器支持,此變量包含由虛擬路徑到被執行腳本的轉換(即虛擬路徑到物理路徑的映射)。例如,如果到你的Web服務器根的絕對路徑爲/usr/local/etc/httpd/htdocs,並且你的cgi-bin文件夾在Web服務器的根水平上(即,http://www.mycorp.com/cgi- bin),一個具有URL http://www.mycorp.com/cgi-bin/search.cgi的腳本將變量PATH_TRANSLATED 設置爲/usr/local/etc/httpd/htdocs/cgi-bin/search.cgi。
QUERY_STRING
顯示由客戶提供的附在URL尾並用一個問號與腳本名分開的任何附加信息。例如,
htt p://www.yourcompany.com/hello.html?name=joe&id=id=45中的name=joe&id=45爲QUERY_STRING。
10)REMOTE_ADDR
它提供發請求客戶的IP地址——如,199.1.166.171。此信息一直可用。
11)REMOTE_HO8T
它提供已分解的發請求客戶的主機名。如dial-up102.abc.def.com。此信息通常不可用,這是由於兩種原因:調用者的IP沒能通過DNS正確映射到一個主機名,或是你的站點的Web管理員屏蔽了IP查找,Web管理員通常關閉查找是因爲它們意味着在每次連接之後服務器要進行額外的步驟,這將降低服務器的運行效率。
EMOTE_IDENT
如果服務器和客戶支持RFC931,此變量將包含由遠程用戶的計算機提供的識別信息。很少有服務器和客戶還支持這種協議。這種信息也沒什麼價值,因爲用戶可把此信息設置爲他們想要的任何東西。即使你的服務器支持也不要用這個變量。
REMOTE_USER
如果AUTH_TYPE被設置,此變量將包含用戶提供並由服務器確認的用戶名。
注意
AUTH_TYPE和REMOTE_USER僅在用戶成功地使其標識在服務器上得到認證後(通常通過用戶名和口令)才被設置,因此,這些變量僅在建立限定區域時並且僅在此區域中有用。
REQUEST_METHOD
它提供腳本被調用的方法。對於使用HTTP/1.0協議的腳本,僅GET和POST有意義。
SCRIPT_NAME
這是被調用腳本文件的名字,它對於自引用腳本很有用。例如,可用此變量產生一個通過GET方法被調用腳本的URL來產生並輸出一個表單,這個表單被提交時通過POST法調用同樣的腳本。通過使用此變量而非硬編碼腳本名或位置將更容易做到維護——如,/cgi-bin/myscript.exe。當移動或更名腳本,當重新配置服務器而改變cgi-bin目錄,或是在另外一臺機器上安裝腳本時,你不必改變代碼。
SERVER_NAME
這是你的Web服務器的主機名、別名或IP地址。它對於在運行時產生指向服務器的URL是可靠的——如,www.ourcompany.com。
SERVER_PORT
這是本連接的端口號——如,80。
SERVER_PROTOCOL
這是本請求所用協議的名字/版本。如, HTTP/1.0。
SERVER一S0FTWARE
這是運行腳本的HTTP服務器的名字/版本。如,HTTPS/1.1。
4 CGI腳本可移植性
CGI程序員面臨兩種可移植性問題:平臺獨立性和服務器獨立性。平臺獨立性是指代碼不必修改就可以在不同於爲其而寫的硬件平臺或操作系統上運行的能力。服務器獨立性是指代碼不必修改就可以在使用相同操作系統的另一臺服務器上運行。
4.1 平臺獨立性
保持腳本可移植的最好辦法就是要使用通用的語言,並且要避免使用平臺特有的代碼。聽上去很簡單,是嗎?實際上,這就意味着要麼用C語言要麼用Perl語言,並且不不能做超出格式文本的事,也不能輸出圖形。
這是否就意味着不必考慮使用VisualBasic,AppleScript和Unix shell等語言呢?是的,我認爲目前是這樣的。然而,平臺獨立性並非是選擇一個CGI平臺時所考慮的唯一準則,還要考慮如代碼速度、維護的簡易性和執行所選擇任務的能力等因素。
某此類型的操作是不可移植的。例如,如果你開發16位Windows程序,將很難在其他平臺上找到所用函數VBX和DLL等效函數。如果開發的是32位windows,NT程序,你將會發現在UNIX環境下,所有異步Winsock調用都毫無意義。如果你的shell腳本調用一個System()來運行grep、並以管道形式將輸出回送到你的程序,你將會發現,在windows NT或Windows 95環境下沒有類似的東西。
如果你的指令之一是以最少的修改在平臺之間移動代碼的能力,你可能會發現用C語言將會取得最大的成功。用ANSI C庫中的標準函數寫代碼並且要避免其他的操作系統調用。不幸的是,遵循這樣的規則將會限制腳本的功能。然而,如果你將依賴於例程自帶的代碼包含平臺,你就使需要從一個平臺移至到另一平臺的工作最小化了。如果在前面部分“計劃腳本”中所看到的一樣,當談及封裝性時,一個設計良好的程序在其整體中的任何模塊被替換後不影響到程序的其他部分。運用這些原則,你可能不得不替換一兩個好程序,而且當然也得重新編譯,但是,你的程序將是可移植的。
Perl 腳本當然要比C程序更易維護,主要是因爲沒有編譯這一步。在知道什麼該修改時,可迅速修改程序。而事情難就難在這裏:Perl今人惱火地遲鈍,並且它的庫比C語言的庫更不一致——即使是在同平臺上的不同版本之間也是如此。另外,windows NT下的perl相當新並且仍很奇特(似乎任何與Perl相關的東西都比其他部分更爲奇特)。這個問題正在解決,但是不要在不理解Perl時就去使用它,幾乎不可能直接從書上或聯機資源上覆制一個腳本而不需做任何修改就可在你的系統上運行。
一旦識別出依賴於平臺的部分並且找到(或寫出)能得到標準函數的庫,在平臺之間 移動代碼就不會有太大的麻煩了。
4.2 服務器獨立性
比平臺獨立性更爲重要的是服務器獨立性(除非你只是因愛好而寫腳本)。服務器獨立性相當容易實現,但是因爲某些原因,它對初寫腳本的人來說也有點難纏。要做到服務器獨立性,你的腳本必須不做任何修改就可在使用相同操作系統的任何服務器上運行。只有獨立於服務器的程序作爲共享軟件或免費軟件才真正有用,並且毫無疑問,服務器獨立性對於商業軟件是必須的。
大多數編程人員考慮到的都是一些明顯的問題,如不假定服務器有靜態IP地址。接下來是服務器獨立性的其他一些規則,儘管一旦指出來也很明顯,但它還是一次次被忽略了:
不要假定你的環境
例如,不要因爲你的開發環境上temp的目錄爲C:/TEMP就假定在腳本運行的其他地方目錄也一樣。不要將目錄和文件名寫死了,這將使Perl腳本的可讀性更差,而這裏對正確編程的曲解也時常發生。如果你的Perl腳本排除一定範圍內IP地址的全部或部訪問,就不要將此地址硬編碼到你的程序中,也不要在註解中寫上“改變此行”這樣的話,應該用一個配置文件。
不要假定特權
在UNIX上,服務器(及你的腳本)可能是以用戶nobody或root運行,或是以它們之間任何的特權水平運行的。在裝有windows NT的機器上,CGI程序也繼承了服務器的安全屬性。檢測訪問權限並仔細檢查返回代碼,以便萬一因不能訪問其資源而使腳本執行失敗是能提供給用戶明瞭的錯誤信息。
不要假定CGI變量的一致性
一些服務器傳遞一些規定的CGI環境變量(如PATH和LIB變量),然而,它們傳遞的變量要依賴於運行時的環境。服務器配置也可影響CGI變量的數目和格式。如果有依賴於環境的輸入,那麼你的程序採取有相應的措施。
不要假定特定版本的信息
檢測工作環境或檢測告訴用戶更改什麼和爲什麼要更改的錯誤信息,服務器和操作系統版本都可影響腳本的環境。
不要假定LAN或WAN配置
在Windows NT環境下,服務器可能會是一個Windows NT工作站或是Windows NT服務器;它可能是獨立的、工作組的一部分或是域的一部分。DNS(域名服務)可能或不能使用;查找可能會被限制在靜態窗主機文件上。在UNIX環境下,不要假定任何關於如inetd、sendmail等守護程序的配置或系統環境,也不要假定目錄名。對不能用系統調用找到的各項用一個配置文件,並給腳本維護人員指令以編輯腳本。
不要假定系統目標的可用性
用相應的特權,檢測諸如數據庫、消息隊列、硬驅等目標的存在,並在找不到或配置錯誤時輸出顯式消息。沒有什麼比下載一個新腳本、安裝而最後只得到“Run time error#203”的輸出更令人惱火的了。
5 CGI庫
通常談及的CGI庫有兩種可能:一種是用戶自己開發,並希望在其他項目中使用的代碼庫,另一種是公用的程序、例程和消息庫。
5.1 個人庫
如果你採納了“計劃腳本”中有關用黑匣子方式寫代碼的建議,你就會發現你正在創建一個將要反覆使用的例程庫。例如,在解決了如何解碼URL編碼數據這個問題後,就不必再去做這項工作。當你寫好一個基本的main()函數後,該函數將可能爲你所寫的每一個CGI程序服務。這對一般的例程也一樣,如查詢數據庫、解碼輸出、報告運行的錯誤。
如何管理個人庫取決於所用的編程語言。用C語言和彙編語言可以將代碼預編譯進實際的1ib文件,然後可用它來鏈接程序。儘管也有可能,但這種方法對CGI來說是不必要的,而且它對於解釋性語言(如Perl和VisualBasic)是無效的(儘管Perl和VB可以調用已編譯好的庫,但是不能用同使用C語言一樣的靜態方式來鏈接它們)。使用已編譯好的庫的好處是當改變庫中的代碼時不必重新編譯所有的程序。如果庫是在運行時(一個DLL)裝入,就不必修改任何東西。如果庫是被靜態鏈接,所需做的只是重新鏈接。
另一種解決辦法是保留獨立的源文件,並在每個項目中包括這件文件。你可以將最爲常用的例程放人一個非常大的文件中,而把其他不太常用的例程放到各自獨立的文件中。以源文件格式保留文件會增加編譯時間,但不必擔心——尤其是同節省寫代碼的時間相比時。這種方法的不利之處是當修改庫代碼時,必須重新編譯你的所有程序才能利用修改後的好處。
沒有什麼可以阻止你把公共域例程併入你的個人庫中。一旦確定了版權和特許允許使用和修改源代碼而不必付費或沒有其他條件,你就可以將感興趣的例程篩選進你的庫中。
設計的歸檔良好的程序爲新的程序提供了基礎。如果仔細地將特定的程序組成部分分離成爲例程,就沒有什麼理由不把整個程序的結構拆用到其他項目中。
也可以開發某些例程特定於平臺的版本,並且,如果編譯程序允許的話,可自動爲建立的每種類型包括進正確的例程。最壞的情況下就得手工指定需要的例程。
使代碼可重用的關鍵是儘量使代碼通用,但也不是絕對通用。例如,美元紙幣打印例程不需要通用到可同時處理美元和日元,但至少任何打印美元總量的程序都可調用它。在升級、增加功能甚至修改例程內容時,要保持每個函數的輸入和輸出不變。這就是實際上的黑匣子方法。通過保持調用約定和參數不變,就可自由升級任何代碼段而不必擔心破壞調用你的函數的程序。
另外一種要考慮的技術是使用函數框架。假定你最終決定打印日元和美元兩者的單個例程實際上是最有效的方法。但是你已經有了分開的例程,並且舊的程序不知道如何將增加的參數傳遞給新的例程。你不必回頭去修改調用舊例程的每個程序,你只需使用庫中例程的框架,這樣程序唯一要做的只是用正確的參數調用新的組合例程。在一些語言中,可通過重新定義例程聲明部分而實現這一點;在其他一些語言中,就需要編碼一個調用並要付出一些額外系統開銷的代價。但既使這樣,這個代價也遠小於所有的舊程序遭到破壞的代價。
5.2公共庫
Internet具有豐富的公共域範例代碼、庫和預編譯程序。儘管你所能找到的這些大部分是面向UNIX的(因爲它出現得時間較長),然而並不缺乏面向Windows NT的例程。
下面的序列是Internet上的一些最好的站點及對在每個站點上能找到什麼的簡要描述。這個序列中包含一部分站點。成百上千的站點致力於或是包含有關CGI編程的信息。打開你的web瀏覽器和最喜歡的搜索引擎並告訴它搜來“CGI”或“CGI Libraries”,你就會看到我說的那些東西了。爲了使你免去那些乏味的擊點,我已爲你把它們都找了出來。請見免費資源—免費CGI源碼
6 CGI的侷限
CGI的最大侷限是它的“無狀態性”。一個HTTP服務器是不會記住兩個請求組成——這些請求要麼全是到同一服務器的,要麼是到一些不同的服務器。每種情況下,服務器完成請求後,就掛起並忘記曾順便訪問過的用戶。
能夠記住一個呼叫者上次接通時做了什麼的能力叫做“記住用戶的狀態”。HTTP以及CGI都沒有自動保留狀態信息,在Web事務中與狀態信息最相近的東西是用戶的瀏覽器高速緩衝區和CGI程序的智能。例如,如果一個用戶在填寫一個表單時漏填了一個必須的字段,CGI程序不能彈出一個警告框也不能拒絕接收輸入。那麼,這個程序唯一的選擇是要麼輸出一條警告信息,告訴用戶單擊瀏覽器的back按鈕,要麼再次輸出整個表單,填入提供的字段值並讓用戶重試,或者修改錯誤或者提供遺漏的信息。
有幾種解決這個問題的辦法,但都不是很令人滿意。有一種想法是保留一個包含來自所有用戶最新信息的文件,當一個新的請求傳來時,在文件中找到這個用戶並假定基於用戶上一次所做事情的正確的程序狀態。這種想法的問題是很難識別一個Web用戶,即一個用戶可能在今天沒有完成操作而第二天又因其他目的再次訪問這個站點。這種方法大量的精力都花費到了保留狀態的算法上了,而這只是爲了節省有限的一點時間。所以,這種解決問題的方法效率很低,並且它們忽視了其他的問題:即首先要識別用戶。
你不能依靠用戶來提供他或她的標識。不僅那些想匿名的用戶,而且即使那些想讓你知道他們名字的用戶都可能一次又一次地將名字拼寫錯。那麼,用IP地址作爲用戶標識又如何呢?也不好。每個通過同一代理的用戶都使用相同的IP地址。在某一時刻,是公司內哪個僱員在呼叫呢?你說不出來的。不僅如此,現在許多人在每一次撥號時都使IP地址動態分配。你當然不會因爲這一次John Joe和Jane Joe的IP地址相同,就給John Joe訪問Jane Joe的數據的特權。
標識映射唯一可靠的形式是由服務器提供的,它運用名字和口令模式。即使這樣,用戶還是不能忍受每次請求時都需輸入名字和口令,所以,服務器緩存數據並用前面提到的算法判斷緩衝區何時變得非法。
假定你們單位的CEO沒有使用其名字或其他可猜得到的東西作爲口令,並且也沒有人洗劫他祕書的抽屜,也沒看過他監視器上的黃色便箋,那麼在服務器告訴你他是CEO時你就有理由肯定他就是CEO。那麼接下來做什麼呢?你的CGI程序仍必須通過一些環節來防止CEO在查你的數據庫時重複回答相同的問題。你的CGI程序的每個響應都必須包含從那點開始程序向前或向後進行的所有信息。這很麻煩但卻很有必要。
第二個繼承到CGI程序中的主要侷限性是與圍繞發送文檔的設計HTTP規範的方式有關。HTTP不傾向於長交換和長交互性。這意味着當你的CGI程序要做一些像生成一個服務器推送的圖形時,它必須保持連接打開。它是通過把各種圖像都真正當成同一圖像的組成部分而實現這一點的。
可憐的用戶例覽器還堅持顯示“連接活動”的信號,以爲它正在檢索單個的文檔。從瀏覽器的觀點來看,文檔只是偶爾有點過長。但從腳本的觀點來看,文檔實際上是由幾十個(也許是成百個)獨立的圖像組成的,每個圖像都是按順序通過管道傳輸的,並且它被做爲一個巨大文件的部分被標記,而這個文件實際上並不存在。
也許當下一代HTTP規範出現,並且例覽器和服務器被更新以能利用保持有效的協議時,我們將會看到一些真正的革新。同時, CGI就會成爲真正的CGI。儘管CGI偶爾不那麼優雅,但它還是非常有用——並且很有意思。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章