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