翻译:misterliwei
原文:What an ISAPI extension is?(http://www.codeproject.com/isapi/isapi_extensions.asp)
介绍
如果不是孤陋寡闻,在浏览某些网站时,你一定遇到过URL地址栏的结尾是一个脚本目录下的DLL文件,就像下面的这个假设的URL地址:
http://www.mydomain.com/script/example.dll?ID=p05874&Tx=870250AZT6
这些DLL文件可以干什么呢?它和本文有什么关系呢?
这些DLL是使用ISAPI(Internet Server API)建立的。ISAPI是一种比CGI更有优势的技术。虽然使用CGI脚本开发出的新网站功能确实令人惊讶,但是ISAPI DLL有CGI所不具备的新功能。
本文从每个ISAPI程序员都必须知道的底层细节开始,这有助于开发出更好的程序。接着,我们将手把手教你开发一个实用的ISAPI扩展程序,这是一个信用卡卡号的校验程序。常有人问我关于信用卡卡号校验的算法问题,开发这个程序也算是对他们的一种回应。好,我们开始吧!
什么是ISAPI?
互联网服务应用程序编程接口(Internet Server Application Programming Interface,简称ISAPI)是一组API函数。它是一种用来开发扩展IIS程序的强有力的方法。虽然ISAPI扩展绝不仅仅局限于IIS的开发,但是实际上它用的最广泛的还是开发一些与IIS合作的程序。
CGI vs. ISAPI
用C/C++或PERL开发出CGI程序将会生成EXE文件。不管什么时候,即使用户一次又一次点击相同的页,这些EXE文件都将被执行并结束一次。而这样的后果就是导致内存的过度使用。
这种内存的过度使用将会导致服务器的完全当机,而这种问题在ISAPI扩展下能被完美解决。每个ISAPI扩展程序都是一个导出3个固定函数的DLL文件,这个DLL由调用进程(如IIS)来调用。这样,不管在同一时间有多少客户调用它,DLL只需加载进内存一次。(如果你阅读一下《VSIUAL C++6.0 BIBLE》的第18章:内存管理,将是非常有益的。)
ISAPI原理
因为ISAPI扩展和调用它的进程(IIS)在同一的进程地址空间中,这样它们就可以互相直接联系。这种方式一个最大的隐患就是会导致整个IIS当机,而且在有些时候是整个网站当掉。看一下下面的图:
你看到,如果ISAPI扩展程序遇到问题且处理不当,将会影响整个网站的服务进程。就像上图所示,ISAPI扩展和IIS的通讯是通过一个ECB(Extension Control Block)结构的指针,其结构然如下:
typedef struct _EXTENSION_CONTROL_BLOCK
{
DWORD cbSize; // size of this struct.
DWORD dwVersion; // version info of this spec
HCONN ConnID; // Context number not to be modified!
DWORD dwHttpStatusCode; // HTTP Status code
CHAR lpszLogData[HSE_LOG_BUFFER_LEN];// null terminated log info
LPSTR lpszMethod; // REQUEST_METHOD
LPSTR lpszQueryString; // QUERY_STRING
LPSTR lpszPathInfo; // PATH_INFO
LPSTR lpszPathTranslated; // PATH_TRANSLATED
DWORD cbTotalBytes; // Total bytes indicated from client
DWORD cbAvailable; // Available number of bytes
LPBYTE lpbData; // pointer to cbAvailable bytes
LPSTR lpszContentType; // Content type of client data
BOOL (WINAPI * GetServerVariable) (HCONN hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSize );
BOOL (WINAPI * WriteClient) (HCONN ConnID,
LPVOID Buffer,
LPDWORD lpdwBytes,
DWORD dwReserved );
BOOL (WINAPI * ReadClient) (HCONN ConnID,
LPVOID lpvBuffer,
LPDWORD lpdwSize );
BOOL (WINAPI * ServerSupportFunction)( HCONN hConn,
DWORD dwHSERequest,
LPVOID lpvBuffer,
LPDWORD lpdwSize,
LPDWORD lpdwDataType );
}EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
无论是调用进程还是ISAPI扩展,它们之间的任何信息都是通过EBC来要传递给对方的。我们已简单的看了一下ECB结构。现在,我们来看看IIS是如何通过与ISAPI扩展进行通讯,来为网站访问者服务的。
当一个ISAPI扩展被访问(比如:http://www.mydomain.com/script/example.dll? ID=p05874 & Tx=870250AZT6)时,IIS会检查example.dll是否已经被加载进内存中。如果没有加载,IIS会加载。一旦DLL被加载进内存,一个工作线程便开始运行我们的ISAPI扩展程序(example.dll)。先是DLL的入口函数DllMain被调用。调用完成后,IIS开始调用GetExtensionVersion函数,这个函数主要实现了下面两个功能:
- 报告ISAPI可以实现的扩展服务
- 取得一个简短的描述扩展的字符串。
然后IIS开始调用HttpExtensionProc函数。这个函数会传递一个ECB指针给ISAPI扩展以开始真正的调用。通过这个函数ISAPI可以向客户端(如浏览器)回写数据。过会儿我们会检查。
第三个也是最后一个ISAPI扩展的入口函数是TerminateExtension函数。它在当ISAPI扩展从内存中卸载的时候调用。所有的清除代码可以在这个函数中实现。
简言之,一个ISAPI扩展是一个导出三个函数的非常规范的DLL。
- GetExtensionVersion
- HttpExtensionProc
- TerminateExtension (可选)
手头有了这些资料,我们开始DllMain的代码编写。它是所有的DLL的入口点函数。
DLLMain——入口点函数
就像微软说的,DllMain是每个DLL的入口函数,它是可选的。如DLL中定义了DllMain,在进程或线程初始化/中止时,或者当使用LoadLibrary加载和FreeLibrary卸载时调用这个入口点函数。这个如何函数原型如下:
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwCallReason, LPVOID lpReserved);
如果你的ISAPI扩展程序中提供这个过程,那么它将在初始化和结束时被被调用。dwCallReason参数可以是下面预定义值之一:
- DLL_PROCESS_ATTACHED
- DLL_THREAD_ATTACH
- DLL_THREAD_DETACH
- DLL_PROCESS_DETACH
对每个参数的详细描述超出了本文讨论范围,有兴趣的读者可以到MSDN上了解更多关于本函数的信息。
再有,我们可以保存hMoudle参数以便以后的使用。最后只是简单的返回一个TURE值。我们在开发扩展程序时,通常在这个函数中不做什么事。
GetExtensionVersion——真正的入口函数
这个函数是IIS调用的第一个入口函数,是ISAPI扩展用来向IIS提供信息用的。为了进一步的了解这个函数,我们来看一下这个函数的原型:
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer);
在这个函数的调用时,我们假定使用pver参数来填写扩展信息,HSE_VERSION_INFO结构如下:
typedef struct _HSE_VERSION_INFO
{
DWORD dwExtensionVersion;
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN];
}HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
dwExtensionVersion是扩展的版本号,lpszExtensionDescription是扩展的描述。如果返回TRUE便表示我们告诉IIS我们的扩展程序准备好了,可以使用了;而返回FALSE则表示IIS不可以使用本扩展。
HttpExtensionProc——主要入口函数
一个ISAPI扩展的令人着迷部分是HttpExtensionProc。目前你所能想到的是这个函数可以用来向客户端回写数据。为了看明白这是怎么发生的,我们来一下这个函数原型:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);
这里pECB是一个扩展控制块(ECB)的指针。这个结构用来解决IIS和ISAPI扩展之间的通讯问题。在这个函数中,你可以决定你的网页中包含什么内容,如何返回给用户,怎样做?还想起来这个结构的成员了?ECB成员中包含了一个方法,原型如下:
BOOL WriteClient(HCONN ConnID, LPVOID Buffer, LPDWORD lpdwBytes, DWORD dwSync);
使用这个函数,你可以将数据放入缓冲区中,呈现给用ConnId标识的客户端。比如说,要想发送 “A BIG RED ROSE”这几个字给客户,你可以简单地进行下面的调用:
char szBigRedRos[] =
"<font color='#FF0000' size='3'><b>A BIG RED ROSE</b></font>";
DWORD dwSize = strlen(szBigRedRos);
pECB->WriteClient(pECB->ConnID, szBigRedRose, dwSize, 0);
哈哈,到此为止。我想你已经拥有最基础的知识来开发你自己的第一个ISAPI扩展程序了。让我们开始吧。。。
项目需求
1. 一点点耐性
2. 一个MSVC++6编译器
3. 一个装有IIS地MS WINDOWS2000的操作系统
4. 一个网页浏览器
目标
我们决定不用MFC而用WIN32 API来开发开发一个ISAPI扩展程序。这个程序的功能是检查Master Card (万事达)卡号是否有效。我们给这个ISAPI扩展DLL命名为Validate.dll。在这个扩展程序中将简单地往客户端写一串字符以告诉客户:你所提供的卡号是否有效。当然,我们一定会检查,如果用户使用下面的URL地址:
http://mydomain/script/validate.dll?some%20string
或
http://mydomain/script/validate.dll?
或者相似的无效URL地址的情况(当然,所谓“无效”是从我们程序员的角度来看的,使用者可不这么想)。
当上面这种无效URL地址的情况发生时,我们将简单地回复下面一段文字到客户端:What you have entered is an invalid Master Card #.
上面就是我们的ISAPI扩展所能做的一切。
为什么不用MFC呢?
虽然说使用MFC可以大大地简化查询字符串的分析过程。但是它也会大大地增加你的扩展程序的文件大小。另一方面,如果你知道了使用SDK编写扩展的细节,我想这将会帮你写出一个更好的MFC扩展程序的。所以,我决定在我们的第一个ISAPI中避免使用MFC。
Luhn算法
现在我们的问题是,我们怎样来检测一个给定的卡号是否有效。这涉及到一种叫做Luhn算法。考虑假如有这样一个数字——5168254236021548。需要通过下面这四个步骤来检测其是否为有效的卡号。
1.从最左面开始,我们简单地第1,3,5,7…数字乘以2。所以,我们需要将下面黑体数字分别乘以2:
5168254236021548
1. 5 * 2 = 10
2. 6 * 2 = 12
3. 2 * 2 = 4
4. 4 * 2 = 8
5. 3 * 2 = 6
6. 0 * 2 = 0
7. 1 * 2 = 2
8. 4 * 2 = 8
2.将乘积的数字相加。
1 + 0 + 1 + 2 + 4 + 8 + 6 + 0 + 2 + 8 = 32
3.将第1步中所有不受影响的数字相加。
1 + 8 + 5 + 2 + 6 + 2 + 5 + 8 = 37
4.将第2、3两个步骤的结果相加并整除10。
32 + 37 = 69
69 % 10 = 9
如果最终的结果是0表示这是一个有效的卡号,否则就不是。从这个算法来看5168254236021548是个无效的卡号。
进一步的检查
知道luhn算法后,我们还可以根据信用卡类型(如下表)做进一步的功能扩展:
信用卡类型 | 前缀数字 | 数字长度 |
Master Card | 51-55 | 16 |
VISA | 4 | 13, 16 |
American Express | 34, 37 | 15 |
然而,对于我们的例子,我们只是假设所给的卡是Master Card卡号。当然我们会检查所给的卡号是否为的Master Card卡号。比如,我们会简单地检查所给的卡号是否为16位。如果不是16位,我们就简单地返回所给的卡号不是有效的卡号。所以,你可以根据上表在这一基础上,对它的功能进行扩展。
如何用C实现Luhn检查
这里,我们开发了一个函数。如果所给的卡号是有效的Master Card卡号,就返回0;否则返回其他值。
#define ERR_WRONG_NUMBER_OF_DIGITS 1
#define ERR_NOT_A_MASTERCARD 2
#define ERR_INVALID_CC 3
#define ERR_INVALID_INPUT 4
BYTE CheckCC(const char *pszNumber)
{
if(strlen(pszNumber) != 16)
return ERR_WRONG_NUMBER_OF_DIGITS;
for(int i = 0; i < 16; i++)
if(!isdigit(pszNumber[i]))
return ERR_INVALID_INPUT;
if(pszNumber[0] != '5' || pszNumber[1] < '1' || pszNumber[1] > '5')
return ERR_NOT_A_MASTERCARD;
int nSum;
for(i = 0, nSum = 0; i < 16; i += 2)
{
int nDigit = (pszNumber[i] - 48) * 2;
nSum += (nDigit < 10 ? nDigit : nDigit / 10 + nDigit % 10)
+ (pszNumber[i + 1] - 48);
}
if(nSum % 10)
return ERR_INVALID_CC;
return 0;
}
有了这个函数后,我们可以简单地进行如下的调用来检查一个给定的号码是否位有效的Master Card卡号。
BYTE byRet = CheckCC("1269875230210254");
if(!byRet)
{
//this is a valid master card #
}
else
{
//An invalid master card#, byRet shows the error code!
}
开始我们的ISAPI扩展
有前面的信息在手中,是开发我们的ISAPI扩展程序的时候了。打开你地VC++编译器。从[File]菜单,选择[New]命令。在Project标签中选择一个Win32 Dynamic-Link Library,在Project Name 处,写上有效的名称,并点击OK。在Win32 Dynamic-Link Library对话框中,选择第二个选项A simple DLL project,点击FINISH按钮。此时,我们有了一个DLL PROJECT了。
因为我们对ul_reason_for_call 和 hModule这些DllMain函数的参数不感兴趣,所以在这个入口点处,我们只是简单地返回TRUE。我们的DllMain函数实现如下:
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
现在,我们从DllMain换到我们的第一个真正的入口点——GetExtensionVersion。 从上文我们知道,我们必须实现这个过程——用来完成两个任务。所以我们在我们的Project中增加下面的代码:
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = HSE_VERSION;
strncpy(pVer->lpszExtensionDesc,
"Validate ISAPI Extension", HSE_MAX_EXT_DLL_NAME_LEN);
return TRUE;
}
这里的HSE_VERSION宏在httpext.h定义如下:
#define HSE_VERSION MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR)
所以在validate.cpp文件头部加上上面提及的头文件(httpext.h)。最后,我们返回TRUE以告诉IIS可以运行我们的ISAPI扩展程序。
下一步是什么呢?现在是要找到一条途径从IIS取得查询字符串的时候了。但是,怎么办呢?你可能已想起来了IIS可以通过ECB指针和我们的DLL交流,ECB指针作为HttpExetnsionProc的唯一参数来传递:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);
pECB中包含了一个数据成员——lpszQueryString,它就指向我们要取的字符串。换句话说,如果用户通过http://mydomain.com/validate.dll?12345这个URL来调用执行Validate.dll,那么pECB->lpszQueryString便指向字符串“ 12345” ——就是在?号后面的字符串。
还有一件事我们必须先解决,就是学会如何利用pECB来向客户端(如浏览器)返回一个字符串(包括Html、Javascript等等)。这是通过ECB结构中的WriteClient方法来实现的,函数原型如下:
BOOL (WINAPI *WriteClient)(HCONN ConnID, LPVOID Buffer,
LPDWORD lpdwBytes, DWORD dwReserved);
这里ConnID是一个连接标识,它用来标识我们返回数据的客户端。Buffer指向我们要返回的数据,而lpdwBytes是Buffer的长度,dwreserved是没用。
有了上文的知识,我们来完成我们的HttpExtensionProc如下:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
StartContext(pECB);
BYTE byRet = CheckCC(pECB->lpszQueryString);
if(!byRet)
{
//this is a valid master card, echo a suitable string to the client
WriteContext(pECB,
"<p><b><font face='Verdana'
color='#008000'>Congratulations!</font>");
WriteContext(pECB,
"</b></p><p><font size='2' face='Verdana'>");
WriteContext(pECB,
"%s is a valid master card #</font></p>/r/n",
pECB->lpszQueryString);
}
else
{
//this is an invalid master card, echo a proper string to the client!
WriteContext(pECB,
"<p><b><font face='Verdana'
color='#800000'>Sorry!</font></b></p>");
WriteContext(pECB,
"<p><font size='2' face='Verdana'>What you have entered is an ");
WriteContext(pECB,
"invalid master card#</font></p>/r/n");
}
EndContext(pECB);
return HSE_STATUS_SUCCESS;
}
那么StartContext, EndContext, and WriteContext functions过程是干什么呢? WriteContext只是一个简单处理返回字符串到客户端的函数,实现如下:
void WriteContext(EXTENSION_CONTROL_BLOCK *pECB, char *pszFormat, ...)
{
char szBuffer[1024];
va_list arg_ptr;
va_start(arg_ptr, pszFormat);
vsprintf(szBuffer, pszFormat, arg_ptr);
va_end(arg_ptr);
DWORD dwSize = strlen(szBuffer);
pECB->WriteClient(pECB->ConnID, szBuffer, &dwSize, 0);
}
这样,我们就不必输入在返回字符串时一次又一次输入字符串的的长度或者其他没有用的信息了。而且,我们可以像使用MFC的CString::Format(...)函数一样来使用,例如:
WriteContext(pECB, "5 + 6 = %d", 5 + 6);
这样就返回一个5 + 6 = 11到客户端了。
另一个函数StartContect用来提供必要的头部HTML代码的回应给客户端。同样的,EndContext返回必要的尾部HTML给客户端。
void StartContext(EXTENSION_CONTROL_BLOCK *pECB)
{
WriteContext(pECB, "<html>/r/n<body>/r/n");
}
void EndContext(EXTENSION_CONTROL_BLOCK *pECB)
{
WriteContext(pECB, "</body>/r/n</html>");
}
现在编译我们的project得到到Release DLL。下面我们来看看如何装载和使用我们的扩展程序。
假定你正在使用Windows2000,在[Program]菜单中选择[Administrative Tools]中的[Internet Services Manager],并展开至叶节点。在左栏中,选择你想将DLL放入的网站目录,一般是在放在scripts目录中。因此,我们在articls目录下建立一个scripts目录:
在scripts目录上单击右键,选择属性项打开下面的对话框:
对这个对话框进行必要的改变,就像上图所示然后按Apply按钮。现在,拷贝你的Validate.dll文件从RELEASE目录到SCRIPTS目录中,然后打开浏览器,试着存取这个DLL。并输入一串数字作为查询字符串。
http://myth/articles/scripts/validate.dll?1234567890125436
请注意要将上面的URL地址改变成适合你要求的域名。运行后,你会发现一个很熟悉的出错窗口:
哦,怎么回事?是什么原因导致了上面的错误?如果你再思考一下,你可能会想起每个ISPAI扩展DLL需要导出3个函数(我们的Validate.dll中只使用了两个):GetExtensionVersion 和 HttpExtensionProc。我们回头来更正这个错误:
在VC++环境中,建立一个空文件validate.def然后完成如下:
; Validate.def : Declares the module parameters for the DLL.
LIBRARY "Validate"
DESCRIPTION 'Validate ISAPI Extension'
EXPORTS
; Explicit exports can go here
HttpExtensionProc @1
GetExtensionVersion @2
然后,从[project]菜单,选择Add to project,安后选择[FILE]项将如我们早已建好的DEF文件加入Project。重新编译,然后生成的dll重新拷贝至Scripts目录中。再次使用浏览器调用它,这次你会看到下面的网页:
现在试着用一个有效的Master Card卡号来调用,你会看到一个欢迎网页。然后试着删除SCRIPT目录下的VALIDATE.DLL,会有什么事发生?啊!你会见到一个DLL正在使用且不能被删除的警告窗口。如果我们要升级更新我们的DLL该怎么办呢?我们是要重启服务器,还是关闭客户浏览器,还是……?不用,不用这么麻烦。
你所要做的事是停止IIS。这在[Internet Information Service]管理单元中完成。运行它之后,再服务器名称上单击右键,选择resert。在“what do you want IIS to do?”下来框(combine box)中,选择“stop Internet services on server”,按OK按钮。几秒钟,一切便搞定J。现在你可以删除或者更改你的ISAPI扩展DLL了。
结束语
这就是ISAPI扩展能做的。它们扩展了IIS的功能。通过这篇文章,我再三重复“扩展”这个词。其实ISAPI程序是分成两类的:ISAPI扩展和ISAPI过滤程序。
ISAPI过滤程序——不像ISAPI扩展——在所有调用WEB服务器的时候它都会被调用。换句话说,因为这些DLL被一遍又一遍地调用,所以会大大降低处理器的速度。然而,在下面这些时候它却绝对有用:如建立一个日志服务,或者做一些特定工作。因为要详细描述ISAPI过滤程序需要另一片文章,读者可以自行了解它是如何工作的。
无论如何,这是我们今天完成的最简单的一个ISAPI扩展程序,它只是告诉你什么是ISAPI扩展程序以及它是如何工作的。这是一个同步的,单线程的最简单的DLL。虽然实际的ISAPI扩展并不是这么简单——你得面对多线程、链接池和其他高级话题。这就需要你进一步学习了,而本文仅仅是一个起点