使用MFC開發ISAPI Extensions程序

  本文主要介紹瞭如何運用一些Web 服務器所支持的Internet Server API (ISAPI) 編程接口來創建交互式的Web 應用程序(Internet Server Applications, 或者簡稱爲ISAs),以及如何調試ISAPI Extension 程序。在閱讀本文時,雖然不要求讀者對Web/CGI 開發有很深的瞭解,但是必須具有使用Visual C++ v4.1 以上版本的MFC 開發應用程序的經驗。本文中的示例程序就是在Windows NT 下用Visual C++ v5.0 創建的。

1.引言

  1 .1 ISAPI 與CGI

  通用網關接口Common Gateway Interface (CGI) 很早就作爲交互式的Web 應用程序的一個標準廣泛應用在Internet 之中。CGI 腳本允許人們用多種編程語言( 如Basic、C、Perl、Shell 等等) 來編寫簡單的應用程序。這些腳本運行在Web 服務器上,而在客戶的Web 瀏覽器上輸出運行結果。客戶的輸入通過環境變量或者標準輸入設備來進行傳遞,然後CGI 程序根據需要完成特定的功能,並通過標準輸出設備送回HTML 格式的結果顯示在客戶的瀏覽器中。CGI 的這一特性—設計簡單,再加上它支持多種編程語言,使得開發CGI 應用程序非常簡單。儘管如此,人們在使用中還是發現了CGI 應用程序的一個很大的缺點:性能不高。雖然有不少辦法來使CGI 應用程序運行得更快一些(如把它們變成編譯好的二進制代碼,而不用Perl 腳本),但執行速度仍然是一個問題。每當通過Web 訪問一個CGI 程序時,CGI 執行文件(或者腳本的解釋器)都要爲每一個請求創建一個新的進程。對於一個信息量比較大的站點來說,這無疑給服務器增加了一個沉重的負擔。

  當微軟開始開發自己的Web 服務器(Microsoft Internet Information Server 或簡稱爲IIS) 時,意識到了CGI 的這一大的缺陷,於是他們就引入了ISAPI。

  ISAPI 使用動態鏈接庫DLL 而不是可執行代碼。這些DLLs 被裝入到服務器的內存空間。由於代碼在內存中緩存起來了,而不是每次接收到請求時再裝入到內存中,因此這一技術極大地提高了交互式的Web 應用程序的性能。

  ISAPI 程序分爲兩種:一種就是我們要介紹的ISAPI Extensions,它提供了一種比CGI 更好的實現方法;另一種稱作ISAPI Filter,它可以對服務器上進入或出去的數據進行過濾。

  總的來說,ISAPI 優於CGI 之處包括:

  ①速度快:ISAPI 在性能上有很大的提高;

  ②功能強:ISAPI 還可以創建Filter 以對數據進行預處理。並且它完全與MFC 集成在一起了。

  相反,ISAPI 的不足之處有:

  ①標準化不夠:目前只有一部分服務器支持ISAPI;

  ②開發困難:相關資料很少,並且調試很麻煩。

  1 .2 ISAPI 基礎

  ISAs 開發主要基於MFC 的CHttpServer 類。該類控制了所有與服務器的交互操作,同時還包含了用於客戶請求的所有函數。儘管一個ISA 只能有CHttpServer 類的一個實例,但每個ISA 仍然可以同時處理多個請求。這是通過CHttpServer 類爲每個請求創建一個CHttpServerContext 類來實現的。CHttpServerContext 類包含了每個特定請求的所有數據以及由ISA 返回到客戶的所有HTML 代碼。

  ISAPI DLLs 的調用方法和CGI 一樣:在客戶端使用GET 或POST 方法。例如,當客戶作出下列請求時:

  http://www.mysite.com/myisa.dll?name=fisherman&id=12345

  "name" 域和"id" 域以及與它們相關的數據都被傳遞給ISA。ISAPI 在使用這些相關的數據之前把它們存放在相應的數據結構中,這是通過一個請求映射系統來實現的。

  每一個請求都有一個解析映射表。通過定義服務器從客戶接收的數據的類型和順序,該解析映射表可以把數據傳遞到合適的數據結構中。例如,對於請求"name=fisherman&id=12345",解析映射表將顯示一個字符串和一個整型數,並且" fisherman " and "12345" 將被解析出來存放到各自的數據結構中。

  解析映射系統還有另外一個功能:ISAPI 可以把請求傳遞給ISA 內特定的成員函數。請求字符串可以包含一個命令,解析映射表就使用該命令把請求傳遞給ISA 內正確的成員函數。

  由於ISAPI 使用命令驅動機制來處理請求,因此在開始開發ISA 程序時可能會覺得有些麻煩,但是一旦學會了,用戶就會發現這是一個非常強大的處理請求的方法。

  2 .使用MFC 開發ISA 程序

  2 .1 建立工程

  開發ISA 的第一步工作是建立一個工程。和創建其它Visual C++ (VC++) 工程一樣,創建ISA 也有一個wizard 來指導用戶完成初始的步驟。打開VC++,在File 菜單中選擇New,然後在對話框中選擇Projects 面板,在下面的列表中選擇"ISAPI Extension Wizard" 工程類型,選擇適當的路徑,並把它命名爲"HelloWeb"。

  接着選擇Ok 按鈕,於是出現一個對話框讓用戶選擇預創建的ISAPI 程序類型,缺省情況下是ISA 程序,同時MFC 被設置爲動態鏈接。如果用戶開發的服務器上有了這些MFC DLLs,這當然是可以的。但是如果沒有安裝Developer Studio,通常情況下這些DLLs 是沒有的,這樣用戶的ISA 將無法運行。此時應該把工程設置爲靜態鏈接。我們建議用戶這樣設置。

  接着選擇Finish 按鈕。VC++ 將顯示一個對話框給用戶一些關於新工程的提示信息。在此對話框中選擇OK。

  現在工程已經創建好了,現在該處理一些複雜點的問題了。我們在前面提到過,ISA 在運行時是IIS 的一部分,而IIS 又作爲NT 的一個服務而運行。這一事實使用調試過程變得複雜了,因爲在IIS 運行時,VC++ 的調試器不能夠接管ISA。爲了解決這個問題,微軟公司以兩種形式發行了IIS:作爲一項服務,以及作爲一個單獨的可執行程序。對於後一種情況,我們就可以在命令行上來控制服務器。雖然這樣可以解決上述問題並使得開發過程變得容易一些,但實現起來顯得很繁瑣。下面我們來介紹這個過程。

  當用戶處於debug 調試模式時,VC++ ( 以及IIS) 將在用戶的帳號和權限下運行。由於通常IIS 完成的一些工作是不允許大多數用戶有相應的權限的,因此用戶(或用戶的系統管理員)需要做以下工作:

  ①在桌面上選擇“開始/ 程序/ 管理工具(公用)/ 域用戶管理器”, 打開域用戶管理器;

  ②在“規則”菜單中選擇“用戶權限”;

  ③選擇“顯示高級用戶權限”檢查框;

  ④在“權限”下拉列表中選擇“以操作系統方式操作”;

  ⑤選擇“添加”按鈕得到“添加用戶及組”對話框,選擇“顯示用戶”按鈕,並在“名稱”列表中選擇用戶使用的帳號,然後選擇“添加”按鈕;

  ⑥選擇“確定”按鈕;

  ⑦對“產生安全審覈”權限重複上述步驟。

  爲了使這些設置生效,用戶必須先退出登錄,然後再登錄回來。

  IIS 中包含了三項服務:FTP Publishing Service, Gopher Publishing Service 和World Wide Web。由於調試器要在命令行上運行IIS,所以所有這三項服務都必須停止。這可以通過“控制面板”中的“服務”程序或者使用IIS 的“Internet 服務管理器”來實現。如果需要進行大量的調試工作,我們建議用戶通過“控制面板”中的“服務”程序來關閉IIS 服務並禁止它們自動啓動,這樣可以避免用戶每次啓動計算機時都要進行關閉服務的操作。

  接下來就必須對工程進行一些配置了:

  ①在Project 菜單中選擇Settings 菜單項;

  ②選擇Debug 面板,並在Category 下拉列表中選擇General;

  ③在Executable for debug session 框中輸入或者尋找IIS 執行文件的路徑(通常情況下位於WINNT/system32/inetsrv/inetinfo.exe);

  ④在Program arguments 框中輸入-e w3svc;

  ⑤選擇Link 面板;

  ⑥在Output filename 框中輸入被編譯後的DLL 將被放置的路徑和文件名。這個路徑必須位於Web 服務器的根目錄下或者某個虛擬目標下,以便客戶可以通過URL 來訪問。例如,我們的Web 服務器的根目錄是c:/InetPub/wwwroot/,我們把helloweb.dll 放置在該目錄下,這樣客戶就可以使用下面的URL 來訪問它:

  http://www.mysite.com/helloweb.dll

  如果用戶現在還沒有退出登錄以改變權限,請現在行動,然後再登錄回來。

  ISAPI Extension Wizard 所產生的缺省代碼已經包含了一個可工作的ISA 所需要的一切,雖然該ISA 還沒有任何功能,但我們不妨先試着進行編譯一下。

  按下F5 鍵以運行ISA,如果彈出對話框問是否創建工程時選擇Yes。幾秒鐘之後調試器就已經工作了,此時IIS 應該運行在後臺。現在一切就緒了,用戶可以在自己喜歡的瀏覽器中輸入上面所提到的URL,並在最後面加上一個問號?,如下所示:

  http://www.mysite.com/helloweb.dll?

  請注意把域名換成正確的值。

  第一次連接到ISA 時可能要花幾秒鐘的時間,此後該DLL 被緩存到了內存中,連接速度就會顯著提高。當DLL 被裝入後,在瀏覽器中應該顯示如下信息:

  This default message was produced by the Internet Server DLL Wizard. Edit your CHelloWebExtension::Default() implementation to change it.

  我們的第一個ISA 已經工作了。

  2 .2 分析源代碼

  一個ISA 包含兩個主要組成部分:解析映射表和命令處理函數。

  當一個請求來自EXTENSION_CONTROL_BLOCK(此結構用於在服務器和ISA 之間進行通信)時,它被傳遞到命令解析映射表。解析映射表由一些宏組成,如HelloWeb 工程(HELLOWEB.CPP) 中的:



BEGIN_PARSE_MAP(CHelloWebExtension, CHttpServer)

// TODO: insert your ON_PARSE_COMMAND() and

// ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

// For example:

ON_PARSE_COMMAND(Default, CHelloWebExtension, ITS_EMPTY)

DEFAULT_PARSE_COMMAND(Default, CHelloWebExtension)

END_PARSE_MAP(CHelloWebExtension)

  BEGIN_PARSE_MAP 宏標誌着解析映射表的開始,它以ISA 的CHttpServer 類和基類作爲參數;ON_PARSE_COMMAND 宏則把一個特定的請求或命令映射到一個命令處理函數中。它的參數包括命令處理函數名、函數的類以及請求的格式;DEFAULT_PARSE_COMMAND 宏確定了當請求爲空或者與解析映射表不匹配時調用的函數。它的參數包括函數名和函數的類。

  命令處理函數是解析映射表中調用的主CHttpServer 類的成員函數。下面就是HelloWeb 工程中的Default 命令處理函數:



void CHelloWebExtension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt < < _T("This default message was produced by the Internet");

*pCtxt < < _T

("Server DLL Wizard. Edit your CHelloWebExtension::Default()");

*pCtxt < < _T("implementation to change it./r/n");

EndContent(pCtxt);

}






  當請求爲空或者包含Default 時此函數被調用。首先它通過參數得到請求的CHttpServerContext(命令處理函數的第一個參數必須是一個CHttpServerContext),StartContent 把< HTML >< BODY > 標誌放到pCtxt 中,然後WriteTitle 放置< TITLE > 標誌。緊接着下面的三行語句把缺省的消息寫入pCtxt,後者指向一個CHtmlStream 類。當ISA 結束時,該HTML 流緩衝區被髮送到客戶端。

  2 .3 修改HelloWeb

  下面我們將把缺省的消息換成" 我會編ISAPI 程序了!"。

  找到CHelloWebExtension 類的成員函數Default,修改成如下形式:

void CHelloWebExtension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt < < _T(" 我會編ISAPI 程序了!");

EndContent(pCtxt);

}


  然後按照前面方法編譯並運行該DLL,並在瀏覽器中重裝或者刷新URL,用戶即可看到顯示的消息已經變了。

  然而,如果用戶看到"Server Error 500: Specified module not found." 這樣的錯誤信息,則表示用戶的工程是動態鏈接的,而所必需的DLLs 不存在。爲了修正這個錯誤,需要到Project 菜單中選擇Settings 命令,然後選擇General 面板,在Microsoft Foundation Classes 下拉列表種選擇Use MFC in a Static Library,然後重新編譯該工程。

  3.深入理解ISA 編程

  3.1  進一步分析解析映射表

  在解析映射表中使用了五個宏:

  BEGIN_PARSE_MAP:開始解析映射表的定義;

  ON_PARSE_COMMAND:解析客戶的命令;

  ON_PARSE_COMMAND_PARAMS:把請求的數據映射到相應的數據結構中;

  DEFAULT_PARSE_COMMAND:定義缺省命令;

  END_PARSE_MAP:結束解析映射表的定義。

  我們在來看一看HelloWeb 中的解析映射表:


BEGIN_PARSE_MAP(CHelloWebExtension, CHttpServer)

// TODO: insert your ON_PARSE_COMMAND()and

// ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

// For example:

ON_PARSE_COMMAND(Default, CHelloWebExtension, ITS_EMPTY)

DEFAULT_PARSE_COMMAND(Default, CHelloWebExtension)

END_PARSE_MAP(CHelloWebExtension)


  此映射表定義了兩個命令:一個空的請求和缺省的Default 命令。空的請求格式如下:

  http://www.mysite.com/helloweb.dll?

  它是由DEFAULT_PARSE_COMMAND 宏處理的。

  然而,如果Default 命令放在了問號的後面:

  http://www.mysite.com/helloweb.dll?Default

  則此命令由ON_PARSE_COMMAND 宏處理。該宏的第一個參數定義了命令的名稱,同時它也是處理該命令的函數的名字。第二個參數是函數的類名。第三個參數用於解析與命令相關的數據,由於在HelloWeb 示例中只傳送了命令而沒有數據,因此它被設置爲ITS_EMPTY。

  如果命令後面還有數據,則需要把它解析到合適的數據類型。例如,如果作出了下面的請求:

  http://www.mysite.com/myisapi.dll?Add&name=fisherman&id=12345

  則"name" 域需要放置到一個字符串中,"id" 域需要放置到一個整型數中。爲此,ISAPI 定義了6個數據標識:


ITS_EMPTY:沒有數據;

ITS_PSTR:字符串;

ITS_I2:short 整型;

ITS_I4:long 整型;

ITS_R4:float 浮點數;

ITS_R8:double 浮點數。


  因此,在上面的例子中應該有一個ITS_PSTR 數據標識和一個ITS_I4 數據標識。ON_PARSE_COMMAND 宏將變成:

  ON_PARSE_COMMAND(Add, CMyISAPIExtension, ITS_PSTR ITS_I4)

  僅僅這樣還不能正確的工作,因爲ISAPI 把& 分隔符之間的所有字符都解析成一個域。因此,"name=fisherman" 將被放置到一個字符串中,"id=15248" 域被放置到一個整型數中。這個問題可以通過使用ON_PARSE_COMMAND_PARAMS 宏來解決。它應緊跟在ON_PARSE_COMMAND 宏後面,並創建請求中的域名的一個映射表。因此上面的例子應該這樣表示:

  ON_PARSE_COMMAND(Add, CMyISAPIExtension, ITS_PSTR ITS_I4)

  ON_PARSE_COMMAND_PARAMS("name id")

  這樣就表明了名爲"name" 的域應該與解析映射表中的第一個數據類型相聯繫,"id" 域應該與第二個數據類型相聯繫。

  當創建與解析映射表交互的HTML 窗體時,要確保窗體中的動作包含了該命令,並且窗體的方法是post。例如,上面的解析映射表對應的窗體應該是這樣的:



< FORM ACTION="myisapi.dll?Add" METHOD=POST >

< INPUT NAME="name" >

< INPUT NAME="id" >

< INPUT TYPE=SUBMIT >

< /FORM >


  3.2  編寫SimpleCalc 示例

  爲了進一步加深認識,我們再舉一個例子。SimpleCalc 是一個簡單的基於Web 的計算器,它可以進行加、減、乘、除運算。缺省情況下SimpleCalc ISA 會顯示一個窗體,其中包含兩個用於輸入數字的編輯框和一個選擇運算模式的選擇框。當此窗體被提交時,它將傳送一個"Calc" 命令,服務器計算答案,最後在客戶端顯示結果。

  首先按照前面介紹的步驟來創建一個名爲SimpleCalc 的新工程。接着需要建立解析映射表。該表要處理一個缺省的命令和一個Calc 命令。對於後者,編輯域num1 和num2 必須映射到double 浮點型數,選擇框mode 必須映射到字符串。如下所示:


BEGIN_PARSE_MAP(CSimpleCalcExtension, CHttpServer)

//Handle "Calc" command.

ON_PARSE_COMMAND(Calc, CSimpleCalcExtension, ITS_R8 ITS_R8 ITS_PSTR)

//Maps "num1" and "num2" to the ITS_R8s, and "mode" to the ITS_PSTR.

ON_PARSE_COMMAND_PARAMS("num1 num2 mode")

//Display form if request is empty.

ON_PARSE_COMMAND(Default, CSimpleCalcExtension, ITS_EMPTY)

DEFAULT_PARSE_COMMAND(Default, CSimpleCalcExtension)

END_PARSE_MAP(CSimpleCalcExtension)


  下一步需要改變default 命令處理函數以便顯示Calc 窗體。代碼如下:


void CSimpleCalcExtension::Default(CHttpServerContext* pCtxt)

{

//Print the < HTML > < BODY > tags.

StartContent(pCtxt);

//Print the title.

WriteTitle(pCtxt);

//The next six lines print the default calc form.

//For this form to work correctly the action must contain the "Calc"

//command, and the method must be POST.

*pCtxt < < _T("< H3 >SimpleCalc< /H3< BR >< BR >");

*pCtxt < < _T("< FORM ACTION=/"simplecalc.dll?Calc/" METHOD=POST >");

*pCtxt < < _T("< INPUT NAME=/"num1/" SIZE=5 > ");

*pCtxt < < _T("< SELECT NAME=/"mode/" >< OPTION >+< OPTION >-");

*pCtxt < < _T("< OPTION >*< OPTION >/< /SELECT >");

*pCtxt < < _T("< INPUT NAME=/"num2/" SIZE=5 >< BR >< BR >");

*pCtxt < < _T("< INPUT TYPE=SUBMIT >< /FORM >");

//Print < /HTML > < /BODY > tags.

EndContent(pCtxt);

}


  現在需要編寫Calc 命令處理函數。此函數必須決定用戶選擇的操作符並相應計算num1 和num2 的運算結果,然後返回該結果。其實現代碼如下:



void CSimpleCalcExtension::Calc(CHttpServerContext* pCtxt,

double num1, double num2, LPTSTR mode)

{

double result;

//Prints the < HTML > < BODY > tags.

StartContent(pCtxt);

//Prints the title.

WriteTitle(pCtxt);

//Determine the operator.

switch( mode[0] )

{

//Add

case '+' :

result = num1 + num2;

//Print result.

*pCtxt < < num1 < < _T(" + ") < < num2 < < _T(" = ") < < result;

break;

//Subtract

case '-' :

result = num1 - num2;

//Print result.

*pCtxt < < num1 < < _T(" - ") < < num2 < < _T(" = ") < < result;

break;

//Multiply

case '*' :

result = num1 * num2;

//Print result.

*pCtxt < < num1 < < _T(" * ") < < num2 < < _T(" = ") < < result;

break;

//Divide

case '/' :

result = num1 * num2;

//Print result.

*pCtxt < < num1 < < _T(" / ") < < num2 < < _T(" = ") < < result;

break;

}

//Print < /HTML > < /BODY > tags.

EndContent(pCtxt);

}

  最後重新編譯並運行此工程。當用戶在瀏覽器中裝入此ISA 時將會顯示窗體。在此窗體中輸入兩個數字,並選擇一個操作符,然後按下submit 按鈕。片刻之後瀏覽器中將顯示如下的結果:

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