標準的CGI程序是通過環境變量和標準輸入輸出來與Web服務器交換信息的。任 何一個被系統激活的進程都擁有標準輸入和輸出這兩個文件句柄,CGI程序的進程 也不例外。不過,當CGI程序被Web服務器激活以后,它的標準輸入STDIN被連接到 Web服務器的標準輸出STDOUT上,而CGI程序的標準輸出STDOUT則被連到服務器的標 準輸入STDIN上。因此,CGI程序從標準輸入讀取信息(也就是從Web服務器的標準 輸出讀信息),而它向標準輸出寫信息(也就是向Web服務器的標準輸入寫信息)。
Web服務器一般將客戶機傳送來的信息放在它的標準輸出和相關環境變量中而CGI程序則從環境變量和它的標準輸入(也就是Web服務器的標準輸出)獲取所 需的信息,程序的最終輸出結果則被寫向它的標準輸出STDOUT(也就是Web服務器的標準輸入)。Web服務器將從它的標準輸入STDIN(也就是CGI程序的標準輸 出)獲取CGI程序的輸出結果并將它傳送給客戶機。顯然,Web服務器就像是客戶機和CGI程序間的中介。
Web服務器、CGI程序間的這種標準框架在Unix系統下和微軟Windows環境中的字符方式下可以工作得非常好,因為此時系統產生的所有進程都可以存取標準輸入和標準輸出。但對于微軟Windows圖形方式下的程序就不行了,因為它們無法存取標準輸入和標準輸出。為了解決這一問題,微軟在Win32系統中創建了另一類型的標準輸入和標準輸出,程序可以通過調用Win32 API函數來存取標準輸入和標準輸出,不過,這就意味著使用這類標準輸入和標準輸出的CGI程序都必須是32位的。
微軟Windows環境下的其它一些Web服務器(例如Website)則使用另外一種特殊的技術(即利用INI文件)來實現Web服務器和CGI程序間的數據交流。采用這種被稱為“Win-CGI”規范編寫的CGI程序通常只能在部分Web服務器上運行。一般地,支持Win-CGI的Web服務器將客戶端的輸入以及有關的狀態信息寫入到一個INI文件中,而CGI程序則從該INI文件中獲取相關信息,這類程序的執行效率沒有標準CGI
程序高。
在進行CGI編程時,只要使CGI程序從標準輸入和環境變量中獲取客戶機提供的信息,并將要傳送給客戶機的輸出結果寫入標準輸出,剩下的信息傳遞工作將由Web服務器自動完成。CGI只是規定了一個標準的接口規范,只要遵守這個標準規范,程序開發人員就可以利用各種編程工具(如Perl、C、FORTRAN、Visual Basic等)進行CGI編程了??紤]到VisualBasic的強大的數據庫處理能力、客戶機/服務器模
式的編程能力以及字符串處理能力,所以本文主要向大家介紹如何使用VB編寫標準的CGI程序。
一、輸入輸出的處理
一個CGI程序被激活以后,它首先要做的事情就是確定系統平臺、Web服務器和客戶端瀏覽器的狀態信息以及客戶端用戶的輸入數據。此外,它還必須能夠將相關信息傳送給客戶端,否則它將一事無成。這些操作都是通過存取環境變量和標準輸入輸出來完成的。用VB編寫的CGI程序通過調用函數Environ( 來獲取相關環境變量的值。存取標準輸入輸出就要在程序中使用Win32API函數GetStdHandle( )、ReadFile( )和WriteFile( ),在使用這些函數時首先必須在程序中聲明它們,寫聲明語句時可以借助于VB提供的API文本查看器。
以下的CGI程序說明了在VB-CGI程序中如何處理環境變量和標準輸入輸出。該CGI程序非常簡單,可將標準輸入中的信息不經任何處理就返回給客戶端,它可被任何表單用POST方法激活:
Declare Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As Long
Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any,
ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long,lpOverlapped As Any) As Long
Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long, ByVal lpBuffer As String,ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long,lpOverlapped As Any) As Long
Public Const STD_INPUT_HANDLE = -10&
Public Const STD_OUTPUT_HANDLE = -11&
Public Const FILE_BEGIN = 0&
Public hStdIn As Long ' 標準輸入文件句柄
Public hStdOut As Long ' 標準輸出文件句柄
Sub Main()
Dim CGI_ContentLength As String,CGI_QueryString As String
Dim lContentLength As Long ' 標準輸入中的字符串的長度
Dim sBuff As String ' 用于存儲標準輸入中的字符串
Dim lBytesRead As Long ' 實際讀入的字符個數
Dim rc As Long
Dim sFormData As String
'調用系統函數生成標準輸入輸出文件句柄
hStdIn = GetStdHandle(STD_INPUT_HANDLE)
hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)
'獲取環境變量CONTENT_LENGTH的值,并將它轉換為整型
CGI_ContentLength = Environ("CONTENT_LENGTH")
lContentLength = Val(CGI_ContentLength)
sBuff = String(lContentLength, Chr$(0))
'從標準輸入中讀數據
rc = ReadFile(hStdIn, ByVal sBuff, lContentLength, lBytesRead, ByVal 0&)
sFormData = Left$(sBuff, lBytesRead)
OutPut "Content-type: text/html" & vbCrLf
OutPut ""
OutPut ""
OutPut "表單傳送數據的方法POST "
OutPut "本CGI 程序使用Visual Basic編制! "
OutPut "POST方法傳送的數據: "
OutPut "" & sBuff
OutPut ""
End Sub
Sub OutPut(s As String) ' 定義一個向標準輸出寫信息的函數
Dim lBytesWritten As Long
s = s & vbCrLf
WriteFile hStdOut, s, Len(s), lBytesWritten, ByVal 0&
End Sub
一般地,用VB編譯生成的CGI程序不能正確處理中文信息。這主要表現在CGI程序向STDOUT輸出的中文在Web頁面上無法正確顯示,可通過在該中文字符串后跟著輸出一些空格來解決這個問題。當使用HTML標識符、對Web頁面進行排版時,瀏覽器在顯示該Web頁面時會吃掉多余的空格而只保留一個。在這種情況下,這些空格對Web頁面的外觀基本上沒有什么影響。如果使用HTML標識符、對Web頁面進行排版,則由于空格不能被瀏覽器吃掉,所以Web頁面的外觀將會受到較大的影響。不過,這時可用HTML的表格、來代替對Web頁面進行排版。
注意:整個CGI程序的主體必須放在MAIN()函數中。
二、URL譯碼與解碼
由于Web服務器和瀏覽器不能正確處理一些特殊的字符,Web服務器和瀏覽器之間可能會因此而產生某種程度的誤會,所以在數據被傳送之前,瀏覽器都要對表單內客戶輸入的數據中的特殊字符進行URL譯碼。
例如,Web系統用“=”分解表單各元素的NAME和VALUE屬性,用“&”分解不同表單元素的輸入數據。如果在表單的輸入數據中包含這些特殊的字符,并且表單的數據在傳送給Web服務器前不作任何處理,則Web服務器將無法知道哪一個“=”、“&”是用戶輸入的,哪一個是瀏覽器加上的。在由表單屬性ACTION定義的URL中,也可能會出現一些特殊的字符,當在CGI程序的名稱和路徑信息(Path Information)中出現“=”、“&”和“?”時,都會影響數據的正確傳送。
URL譯碼(URL Encoding)就是將Web服務器所不能正確處理的特殊字符轉換成它的十六進制數的形式,比如將“%”轉換成“%25”、“=”轉換成“%3D”等等。這些特殊的字符通常被稱作Web系統的保留字符。在Web系統上無論是用GET方法還是用POST方法傳送的數據都要進行URL譯碼。CGI程序要想處理表單傳送來的數據,還必須對瀏覽器URL譯碼過的數據進行解碼。
因此,理解URL譯碼對于我們進行CGI編程是非常重要的。URL譯碼一般包括以下步驟:
1、瀏覽器將所傳送的數據根據表單所包含的元素分解成“NAME=VALUE”形式,NAME和VALUE分別是表單元素的屬性。其中,VALUE屬性中存儲客戶機在表單中輸入的數據:如果客戶機沒有輸入數據,則VALUE存儲的是表單定義的缺省值;如果缺省值也沒有定義,則VALUE值為空。
2、代表表單中各元素的各個“NAME=VALUE”對被瀏覽器用“&”連接起來。
3、VALUE屬性中存放的數據若含有空格,則被轉換成“+”。
4、URL和輸入數據中所包含的Web系統的保留字符必須被譯碼成其十六進制數形式。
5、被譯碼后的字符被表示成一個“%”和它們的十六進制數形式(即%HH)。
CGI程序從環境變量“QUERY_STRING”或標準輸入中讀入的數據是經過瀏覽器URL譯碼過的,故在使用這些數據以前還必須對它們進行URL解碼。解碼的目的是將數據還原成客戶端用戶在Web頁面上輸入時的形式。本文已經介紹了URL譯碼過程,URL解碼過程與它正好相反,它一般包括以下步驟:
1、從瀏覽器用GET或POST方法所傳送來的數據中找出代表各個表單元素所儲存數據的“NAME=VALUE”對。
2、VALUE屬性中所存放的數據若含有“+”,則被轉換成空格。
3、將VALUE屬性中所存放的數據的十六進制數“%HH”轉換成相應的字符。
Web系統將漢字當成特殊的字符,對它也要進行URL譯碼。對于一個特殊的單字節字符(比如“/”),瀏覽器通常將它譯碼成十六進制數的形式(比如%2F), “%”表示它后面跟的是兩位十六進制數。當VB程序對其進行處理時調用Chr$函數就可以將其恢復為原貌。而一個漢字則被瀏覽器譯碼成四位十六進制數(比如%D5%C5)。如果CGI程序還像以前那樣分別調用Chr(D5)和Chr(C5),則由于D5、C5都不是正常的單字節十六進制數碼,故Chr函數返回空,漢字將無法正確還原。正確的做法應該是將有關漢字的四位十六進制數一起傳給函數Chr(如Chr(D5C5)),此時漢字才能被正確還原。
因此,可以讓CGI程序對四位連續的十六進制數一起進行譯碼,以便使漢字能夠被正確還原。但在這種情況下,當客戶端用戶輸入了兩個連續的Web系統保留字符時,CGI程序又可能把它們當成漢字來處理。這時可以讓CGI程序在需要對四位連續的十六進制數進行譯碼時首先檢查前面兩位是否為Web系統的保留字符,如果是則仍然按照單字節的字符處理。不過如果客戶端用戶在表單內填寫了很多漢字,則CGI程序的負擔將會大大加重。事實上,在大多數情況下,客戶端用戶很少會使用兩個連續的Web系統的保留字符,所以可以只讓CGI程序對最容易出現的情形如“://”(當客戶端用戶在表單中輸入某一URL時會出現這種情況)進行檢查,本文下節提供的函數UrlDecode( )可以實現對漢字和Web系統保留字符的URL解碼。
三、CGI編程實例
本節將用VB編寫一個處理主頁客戶留言簿的CGI程序。除了要調用本文前面所介紹的Win32API函數外,程序中還調用了Win32API函數GetTempFileName() 來獲得一個唯一的臨時文件名。程序中的函數UrlDecode()用來對客戶端的輸入進行URL譯碼。函數GetCgiValue()則用來分解字符串,根據表單元素的NAME屬性獲取其VALUE值,并調用UrlDecode()函數對其進行URL譯碼。
本程序要求在留言簿文件guests.html中使用一個定位串“”,將文件的開始部分和具體的客戶留言部分分開。CGI程序將在“”所在的位置插入客戶新的留言。guests.html應具有如下所示的樣式:
……………………….
這種樣式將保證最后的留言出現在留言簿的最前面。如果要想使最后的留言出現在留言簿的最后面,則只需將留言簿文件中的定位字符串“>”移到留言簿文件中客戶留言部分和HTML文件結尾部分之間的位置就行了。
整個程序的完整代碼如下所示:
'guestbook.bas
Declare Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As Long
Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any,ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, lpOverlapped As Any) As Long
Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long,ByVal lpBuffer As String, ByVal nNumberOfBytesToWrite As Long,lpNumberOfBytesWritten As Long, lpOverlapped As Any) As Long
Declare Function GetTempFileName Lib "kernel32" Alias "GetTempFileNameA"(ByVal lpszPath As String, ByVal lpPrefixString As String, ByVal wUnique As Long, ByVal lpTempFileName As String) As Long
Public Const STD_INPUT_HANDLE = -10&
Public Const STD_OUTPUT_HANDLE = -11&
Public Const FILE_BEGIN = 0&
Public hStdIn As Long ' 標準輸入文件句柄
Public hStdOut As Long ' 標準輸出文件句柄
Public sFormData As String ' 用于存儲沒有經過URL譯碼的用戶輸入數據
Public lContentLength As Long
Public CGI_RequestMethod As String
Sub Main()
Dim CGI_ContentLength As String, CGI_QueryString As String, sBuff As String, chinesetail As String
Dim lBytesRead As Long, rc As Long,I As Long
Dim sEmail As String, sName As String, sURL As String, sfrom As String, tempstring As String
Dim sComment As String, tempFileName As String, guestbook As String
'CGI程序的初始化工作
hStdIn = GetStdHandle(STD_INPUT_HANDLE)
hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)
CGI_RequestMethod = Environ("REQUEST_METHOD")
CGI_QueryString = Environ("QUERY_STRING")
CGI_ContentLength = Environ("CONTENT_LENGTH")
lContentLength = Val(CGI_ContentLength)
sBuff = String(lContentLength, Chr$(0))
OutPut "Content-type: text/html" & vbCrLf ' 輸出MIME類型
OutPut ""
If CGI_RequestMethod = "POST" Then
sBuff = String(lContentLength, Chr$(0))
rc = ReadFile(hStdIn, ByVal sBuff, lContentLength, lBytesRead, ByVal 0&)
sFormData = Left$(sBuff, lBytesRead)
ElseIf CGI_RequestMethod = "GET" Then
sFormData = CGI_QueryString
Else
OutPut "Unknow Form Method !"
End If
chinesetail = String(400, " ")
'為了在頁面上正確顯示中文,生成一個空格串以獲取客戶端用戶的輸入
sName = GetCgiValue("name")
sEmail = GetCgiValue("email")
sURL = GetCgiValue("URL")
sfrom = GetCgiValue("from")
sComment = GetCgiValue("URL_Comment")
'對客戶端用戶的輸入進行檢查
If Len(sName) = 0 Then
OutPut "非常抱歉!您還沒有填寫姓名!" & chinesetail
Exit Sub
End If
If Len(sComment) = 0 Then
OutPut "非常抱歉!您還沒有提出建議!" & chinesetail
Exit Sub
End If
'獲取唯一的臨時文件名和留言簿文件并打開它們
tempFileName = TempFile("c:windowstemp", "gbk")
guestbook = "e:netscapeserverdocsguests.html"
Open tempFileName For Output As #1
Open guestbook For Input As #2
Do '本循環體用于將留言簿中字符串""前面的內容寫入臨時文件
Line Input #2, tempstring
Print #1, tempstring
Loop While tempstring <> "" And Not EOF(2)
'向臨時文件中插入客戶端用戶的留言
Print #1, "--------------------------------------------------------------------------------" & vbCrLf
Print #1, "" & vbCrLf
Print #1, "留言時間:" & Date$ & " " & Time$ & vbCrLf
Print #1, "姓名: " & sName & vbCrLf
If Len(sEmail) <> 0 Then
Print #1, "E-mail: " & sEmail & "" & vbCrLf
End If
If Len(sURL) <> 0 Then
Print #1, "我的主頁: " & sURL & "" & vbCrLf
End If
If Len(sfrom) <> 0 Then
Print #1, "我來自: " & sfrom & vbCrLf
End If
Print #1, "我的建議: " & vbCrLf
Print #1, sComment & vbCrLf
Print #1, "" & vbCrLf
Do '本循環體用于將留言簿剩余的東西寫入留言簿
Line Input #2, tempstring
Print #1, tempstring
Loop While Not EOF(2)
Close #1
Close #2
Kill guestbook '刪除舊的留言簿
Name tempFileName As guestbook '將臨時文件改成新的留言簿
OutPut "非常感謝您的留言!" & chinesetail
OutPut "歡迎您經常光顧本主頁!" & chinesetail
OutPut ""
End Sub
Sub OutPut(s As String) ' 本子程序用于向標準輸出寫信息
Dim lBytesWritten As Long
s = s & vbCrLf
WriteFile hStdOut, s, Len(s), lBytesWritten, ByVal 0&
End Sub
Public Function GetCgiValue(cgiName As String) As String
' 本子程序可以獲取表單上某一元素的數據
Dim delim2 As Long ' position of "="
Dim delim1 As Long ' position of "&"
Dim n As Integer
Dim pointer1 As Long,pointer2 As Long,length As Long,length1 As Long
Dim tmpstring1 As String,tmpstring2 As String
pointer1 = 1
pointer2 = 1
delim2 = InStr(pointer2, sFormData, "=")
pointer2 = delim2 + 1
Do
length = delim2 - pointer1
tmpstring1 = Mid(sFormData, pointer1, length)
delim1 = InStr(pointer1, sFormData, "&")
pointer1 = delim1 + 1
length1 = delim1 - pointer2
If delim1 = 0 Then length1 = lContentLength + 1 - pointer2
If tmpstring1 = cgiName Then
tmpstring2 = Mid$(sFormData, pointer2, length1)
GetCgiValue = UrlDecode(tmpstring2)
Exit Do
End If
If delim1 = 0 Then
Exit Do
End If
delim2 = InStr(pointer2, sFormData, "=")
pointer2 = delim2 + 1
Loop
End Function
Public Function UrlDecode(ByVal sEncoded As String) As String
' 本函數可以對用戶輸入的數據進行URL解碼
Dim pointer As Long ' sEncoded position pointer
Dim pos As Long ' position of InStr target
Dim temp As String
If sEncoded = "" Then Exit Function
pointer = 1
Do '本循環體用于將"+"轉換成空格
pos = InStr(pointer, sEncoded, "+")
If pos = 0 Then Exit Do
Mid$(sEncoded, pos, 1) = " "
pointer = pos + 1
Loop
pointer = 1
Do
'本循環體用于將%XX轉換成字符。對于兩個連續的%XX,如果第一個%XX 不是某些特指的Web系統保留字符,將把它們轉換成漢字
pos = InStr(pointer, sEncoded, "%")
If pos = 0 Then Exit Do
temp = Chr$("&H" & (Mid$(sEncoded, pos + 1, 2)))
If Mid(sEncoded, pos + 3, 1) = "%" And (temp <> ":") And (temp <> "/") _
And (temp <> "(") And (temp <> ")") And (temp <> ".") And (temp <> ",") _
And (temp <> ";") And (temp <> "%") Then
Mid$(sEncoded, pos, 2) = Chr$("&H" & (Mid$(sEncoded, pos + 1, 2)) _
& (Mid$(sEncoded, pos + 4, 2)))
sEncoded = Left$(sEncoded, pos) & Mid$(sEncoded, pos + 6)
pointer = pos + 1
Else
Mid$(sEncoded, pos, 1) = temp
sEncoded = Left$(sEncoded, pos) & Mid$(sEncoded, pos + 3)
pointer = pos + 1
End If
Loop
UrlDecode = sEncoded
Exit Function
End Function
Public Function TempFile(sPath As String, sPrefix As String) As String
'本函數可以獲得一個唯一的臨時文件名
Dim x As Long,rc As Long
TempFile = String(127, Chr$(0))
rc = GetTempFileName(sPath, sPrefix, ByVal 0&, TempFile)
x = InStr(TempFile, Chr$(0))
If x > 0 Then TempFile = Left$(TempFile, x - 1)
End Function
雖然目前已經有很多可以取代CGI且其性能較CGI要高的技術(例如ASP、ISAPI、NSAPI等),但使用它們時需要用到專門的知識和工具,并且利用這些技術所編制的程序只適用于特定的Web服務器或系統平臺??紤]到CGI編程具有易用易學性、跨服務器平臺特性等優點,因此,CGI程序還將在WWW上占有一席之地。