使用MFC开发ISAPI Extensions程序

发表于:2007-07-04来源:作者:点击数: 标签:
第二炮兵工程学院 俞俊平 余安萍 本文主要介绍了如何运用一些Web 服务器 所支持的Internet Server API (ISAPI) 编程接口来创建交互式的Web 应用程序(Internet Server Applications, 或者简称为ISAs),以及如何调试ISAPI Extension 程序。在阅读本文时,虽然不
第二炮兵工程学院 俞俊平 余安萍

  本文主要介绍了如何运用一些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

back.gif (1185 字节)

原文转自:http://www.ltesting.net