技术开发 频道

如何扩展IIS的架构满足不同需求



【IT168 专稿】

摘要
   本文讨论了如何使用ISAPI来扩展IIS。主要从ISAPI的概念、结构、编写ISAPI的步骤来阐述实现ISAPI的过程,并给出了一个动态网络配置文件的实例来具体说明如何使用ISAPI来扩展IIS。
 
一、ISAPI简介
    ISAPI可以理解为直接和IIS交互的dll。ISAPI分为ISAPI扩展(ISAPI Extension)和ISAPI过滤器(ISAPI filter)。 ISAPI扩展在一些特定的url请求时调用,而ISAPI过滤器是在所有的url请求时调用。本文采用的是第一种ISAPI,即ISAPI扩展。
    ISAPI扩展可以通过Get或Post方法直接调用,也可以通过将特定文件映射到这个ISAPI上,进行间接调用。如在IIS下运行了php,其中一种运行方式就是以ISAPI提供的。将*.php映射到ISAPI扩展上,所以在浏览器中输入*.php才可以运行php程序。因此,ISAPI扩展是最常用的ISAPI。
 
二、动态网络配置文件概述
现在有许多应用程序都有配置文件,但如果想统一控制程序的行为,配置文件就需要从网络上获得,即所有的程序在启动时使用HTTP协议从网上下载配置文件,然后再读取它的内容。但这样有一个问题,由于同一个程序以不同的用户进入可能需要不同的配置文件。当然,实现这些功能有很多方式,可以用php或asp.net在服务端进行处理。在这里我们讨论另外一种实现方式,即ISAPI扩展。ISAPI可以使用C++实现,由于使用的是C++,可以很容易做到php或asp.net不容易做到的事,另外,在发布时,可以只带一个dll,同时也可以起到保密的作用(php、c#等程序生成的目标代码很容易被反编译)。


三、ISAPI的结构和实现步骤
ISAPI本身是一种API,即然是API,就会有相应的接口。接口可以看做是规范的另一种称呼,在定义了标准接口后,任何能实现API的开发工具都可以实现ISAPI。
ISAPI主要有两个接口函数:GetExtensionVersion和HttpExtensionProc,在IIS启动时首先调用ISAPI的GetExtensionVersion以获得版本号, 并与自己的版本号进行比较,以保证版本兼容。
其中GetExtensionVersion函数有一个HSE_VERSION_INFO类型的参数,用来设置ISAPI的版本号和版本信息。HSE_VERSION_INFO类型的定义如下。
typedef struct   _HSE_VERSION_INFO {
 
    DWORD dwExtensionVersion;
    CHAR   lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN];
 
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
HttpExtensionProc函数是ISAPI的入口点,相当于控制台程序的main函数。当有请求时,IIS就会调用这个函数来处理请求。
在本文中使用了VS2005做为开发平台,下面就以一个具体的例子详细讲述ISAPI扩展的开发过程。


四、使用vs2005建立ISAPI工程
使用vs2005建立一个默认的dll(也就是ISAPI工程)工程需要两步。
1 启动vs2005,选中“Win32项目”,在“名称”文本框中输入first_isapi,如图1所示。



1

2 点击确定后选择建立dll,如图2所示。



在点击完成后,出现一个默认的dll工程。然后找到DllMain函数,将其删掉。因为ISAPI的入口函数是HttpExtensionProc,而不是DllMain,因此DllMain是没用的。在建立完工程后,加入一个first_isapi.def文件,这个文件用来导出两个ISAPI接口函数。First_isapi.def的内容如下。
LIBRARY "first_isapi"
 
EXPORTS
 HttpExtensionProc
GetExtensionVersion



五、获得url请求信息
这是实现ISAPI的第一步。从客户端发送来的url请求中含有一些有用的信息,在这个例子中需要获得url请求所指的文件在服务端的绝对路径和查询字符串(即url请求?后面的部分)。这些信息通过HttpExtensionProc函数的参数传入。GetExtensionVersion函数的定义如下:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
其中绝对路径和查询字符串分别由pECB->lpszPathTranslated和pECB->lpszQueryString得到。如url请求为:http://localhost/a.my?name=xyz&age=16。绝对路径可能为d:\isapi\a.my,查询字符串为name=xyz&age=16。由于查询字符串是连在一起了,无法直接使用,因此这就需要做一个函数将其分成单独的查询串。实现拆分查询串功能的函数叫AnalyzeQuery,下面是它的实现代码。
 
//将连在一起的查询串分开,如将aa=xyz&bb=uu分成aa xyz,bb uu
void AnalyzeQuery(string query_original, map<string, string> &query)
{
    string s;
    int last_pos = -1, new_pos = -1;
    while(true)
    {
        new_pos = query_original.find('&', last_pos + 1);
        if(new_pos == query_original.npos)
            break;
        else
        {
            last_pos++;
            s = query_original.substr(last_pos, new_pos - last_pos);
            //处理每一个query
            int pos = s.find('=');
            if(pos == s.npos)
                query[s] = "";
            else
            {
                string q, v;
                q = s.substr(0, pos);
                v = s.substr(pos + 1, s.length() - pos - 1);
                query[q] = v;
            }
            last_pos = new_pos;
        }
    }
    last_pos++;
    s = query_original.substr(last_pos);
    if(s != "")
    {
      //处理每一个query
        int pos = s.find('=');
        if(pos == s.npos)
            query[s] = "";
        else
        {
            string q, v;
            q = s.substr(0, pos);
            v = s.substr(pos + 1, s.length() - pos - 1);
            query[q] = v;
        }
    }
}
这个函数是通过一个map的引用返回结果的,返回的结果如下。
name xyz
age 16


六、动态网络配置文件的格式
从理论上说,动态网络配置文件可以采用任何格式。为了简便,本文采用xml做为动态网络配置文件的格式。这个配置文件很简单,就是根据url请求的查询字符串决定输出哪一段配置文件内容。为了更灵活,在配置文件中加入了if语句,因此,如果要想改变输出,只需要改变if语句的条件即可。一个配置文件的例子如下如示。
<?xml version="1.0" encoding="gb2312" ?>
<root>
    <%if name=xyz & age = 16%>
    <table1>
    </table1>
    <%endif%>
    <%if name=b | age = 17%>
    <table2>
    </table2>
    <%endif%>   
</root>
 
其中if语句的语法开始部分和php类似,如<%if name=xyz & age = 16%>。if的语句格式为<%if query1=value1 &/| query2=value2 …&/| … %>(query1和query2是查询的字段,value1和value2是查询的值,即url查询串“=”前和“=”后的部分),最后以<%endif%>结束,如果if的条件为true,那么输出if和endif中间的字符,否则,忽略它们之间的字符。


七、对配置文件进行分析
即然配置文件有if语句,那么就需要对其进行分析。由于if语句采用类似于xml的格式,因此无需使用编译原理的相关知识进行分析。在分析之前,需要打开绝对路径所指向的文件,并进行读取等操作。这些功能由四个函数来完成,它们是OpenFile、CloseFile、GetNextChar和PutBackChar。这四个函数的实现如下。
ifstream *p_ifs;
//打开文件,如果成功,返回true,失败,返回false
bool OpenFile(string fn)
{
    p_ifs = new ifstream(fn.c_str());
    if(p_ifs->is_open())
        return true;
    else
        return false;
}
//关闭被打开的文件
void CloseFile()
{
 p_ifs->close();
 delete p_ifs;
}
//从这个被打开的文件中得到一个字符
char GetNextChar()
{
    char c;
    p_ifs->read(&c, 1);
    if(p_ifs->eof())
        return 0;
    else
        return c;
}
//向文件流回退一个字符
void PutBackChar(char c)
{
    p_ifs->putback(c);
}
本例中将得到最终配置文件内容的功能单独封装在GetFileContent函数中,函数实现如下。
string GetFileContent(string query_original)
{
    string content = ""//通过对if语句的判断,得到最终的文件内容
    map<string, string> query;
    int state = 0;
    char c;
    string syntax;
    AnalyzeQuery(query_original, query);
    while((c = GetNextChar()) != 0)
    {
        switch(state)
        {
            case 0: 
// 未遇到if语句,直接将当前字符加入到content中
                if(c != '<')
                    content.push_back(c);
                else
                {
                    char c1;
                    c1 = GetNextChar();
                    if(c1 != '%')
                    {
                        content.push_back(c);
                        content.push_back(c1);
                    }
                    else
                    {
                        string cmd = GetKeyword();
                        if(cmd.compare("if") != 0)
                        {                          
                            return "if语句出错!";
                        }
                        else
                        {
                            state = 1;   //转到处理if语句条件的状态
                        }
                    }
                }
                break;
            case 1:   // 处理if后的条件,得到true或false
                syntax = GetSyntax();
                if(syntax == "")
                    return "语法错误!";
                else
                {
//这个条件满足,转到状态
 
                  if(VerifyCondition(syntax, query))                
                 {
                        state = 2;
                  }
                  else   //不满足这个条件,转到状态
                  {
                        state = 3;
                }
            }
                break;
            case 2:   //将满足条件的部分加入到content中,在遇到<%endif终止
                if(c != '<')
                    content.push_back(c);
                else
                {
                    char c1;
                    c1 = GetNextChar();
                    if(c1 != '%')
                    {
                        content.push_back(c);
                        content.push_back(c1);
                    }
                    else
                    {
                        string cmd = GetKeyword();
                        if(cmd.compare("endif") == 0)
                        {
                            state = 0;
                        }
                        else
                        {
                            return "if语句错误!";
                        }
 
                    }
                }
                break;
            case 3:   //忽略不满足条件的部分
                if(c == '<')
                {
                    char c1;
                    c1 = GetNextChar();
                    if(c1 == '%')
                    {
                        string cmd = GetKeyword();
                        if(cmd.compare("endif") == 0)
                            state = 0; //回到开始状态
                        else
                            return "if语句错误!";
                    }
                }
                break;
        }
    }
        return content;
}
这个函数有一个参数:query_original,表示一个连在一起的查询字符串,如name=xyz&age=16。GetFileContent通过对原始字符串以及配置文件的内容进行扫描,返回最终送到客户端的字符串。
GetFileContent的基本原理是利用一个状态state。当GetNextChar得到一个字符时,判断是否以<%开头,如果不是,则按原样输出,否则,判断是否为if或endif语句,如果一开始遇到的是if语句,就得到它的语法部分,进行解析,判断这个if语句是否为true,如果为true,返回if和endif之间的内容,否则略过if和endif之间的内容,接着扫描endif后的内容。如果if语句没有以endif结尾,那么返回“if语句错误!”信息。
GetFileContent中有几个函数需要解释一下,其中GetKeyword的功能是得到关键字,即得到if和endif。在调用GetKeyword时,文件流指针已经指向<%的下一个字符。因此调用GetKeyword读到的第一个字符就是if和endif的第一个字符。关键字和后面的语法部分用空格、tab键或%(只有endif是以%结尾的)隔开。GetKeyword的函数实现如下。
string GetKeyword() //得到关键字,有个关键字,if, endif
{
    string keyword = "";
    char c;
    while((c = GetNextChar()) != 0)
    {
//命令后可用空格或tab键和后面的语句隔开
        if(c != ' ' && c != 9 && c != '%')    
   {
            keyword.push_back(c);
        }
        else
            break;
    }
    if(c == '%')
        GetNextChar();
    else if(c == ' ' || c == 9)
        PutBackChar(c);    
    return keyword;
}
和GetKeyword相对应的就是GetSyntax函数,这个函数的功能是得到if语句的语法部分,语法部分以%>结尾。GetSyntax的实现如下。
string GetSyntax() //得到关键字的语法字符串,
{
    string syntax = "";
    char c;
    while((c = GetNextChar()) != 0)
    {
        if(c != '%')
        {
            syntax.push_back(c);
        }
        else
        {
            char c1;
            c1 = GetNextChar();
            if(c1 != '>')
            {
                syntax.push_back(c);
                if(c1 != 0)
                    syntax.push_back(c1);
                else
                    return "";
            }
            else //语法部分结束,返回相应的语句
                break;
        }
    }
        return syntax;
}
在这个ISAPI中最关键的要算VerifyCondition函数了,它的功能是验证if语句后面的条件是true还是false。它的实现代码如下。
bool VerifyCondition(string condition, map<string, string> &query)
{
    vector<string> condition_set;
    vector<char> relation_set;
    bool result;
    if(condition == "") return false;
    ProcessCondition(condition, condition_set, relation_set);    
    result = VerifySingleCondition(condition_set[0], query);
    for(size_t i = 1, j = 0; i < condition_set.size() && j < relation_set.size(); i++, j++)
    {
        if(relation_set[j] == '&')
            result = result && VerifySingleCondition(condition_set[i], query);
        else
            result = result || VerifySingleCondition(condition_set[i], query);
    }
    return result;
}
在处理if语句的条件时首先要将条件字符串分解,完成这个功能的函数是ProcessCondition,它将条件保存在condition_set中,而将它们之间的关系(&代表and,|代表or)放到relation_set中。ProcessCondition的实现代码如下。
void ProcessCondition(string condition, vector<string> &condition_set, vector<char> &relation_set)
{
    string s;
    int last_pos = -1, new_pos = 0;
    while(true)
    {
        new_pos = Find(condition, "&|", last_pos + 1);
        if(new_pos == condition.npos)
        {
            break;
        }
        else
        {
            relation_set.push_back(condition[new_pos]);
            last_pos++;
            condition_set.push_back(condition.substr(last_pos, new_pos - last_pos));
            last_pos = new_pos;
        }
    }
    s = condition.substr(++last_pos);
    if(s != "")
    {
        condition_set.push_back(s);
    }
}
condition_set中的每一个值是单独的条件串,型如name=xyz。因此,将判断每一个条件串是否为true的功能单独封装到VerifySingleCondition函数中。然后在VerifyCondition函数中利用relation_set中保存的关系将每个条件串的逻辑值组合起来。VerifySingleCondition函数有两个参数,第一个是条件串,第二个是查询字符串集。如果条件满足查询串,返回true,否则返回false。VerifySingleCondition函数的实现如下。
bool VerifySingleCondition(string condition, map<string, string> &query)
{
    int pos = condition.find('=');
    if(pos != condition.npos)
    {
        string q, v;
        vector<string> values;
        q = Trim(condition.substr(0, pos));
        v = condition.substr(pos + 1);
        SeparateString(v, ',', values);
        if(query.find(q) == query.end()) return false;
        for(size_t i = 0; i < values.size(); i++)
        {
            if(query[q].compare(values[i]) == 0)
                return true;
        }
    }
    return false;
}
在编写完以上函数后,在HttpExtensionProc接口函数中利用pECB的lpszPathTranslated属性打开文件后,将lpszQueryString属性的值传入GetFileContent函数中,最后用WriteClient函数将GetFileContent的返回值发送到客户端。



八、ISAPI的发布和运行
将ISAPI扩展的源程序编译成dll后,就可以将其发布了。发布过程分为两步。
1 在IIS中建立一个虚拟目录(假设虚拟目录名为isapi),进入属性对话框,在“虚拟目录”标签中点击“配置”按钮,出现如图3所示的对话框。



点击“添加”按钮,将ISAPI映射到相应的文件扩展名上(假设映射的扩展名为my),保存设置。
2 在“Web服务扩展”(只针对win2003)中将相应的ISAPI扩展权限设为“允许”。有两种方法可以设置ISAPI扩展权限,第一种是将所有未知ISAPI扩展的权限设为“允许”,第二种是设置指定的ISAPI扩展的权限为“允许”。一般选用第二种,因为要是设置了所有ISAPI扩展为“允许”,将会带来一些安全性问题。点击“Web服务扩展”后出现如图4所示的界面。

 

发布完ISAPI扩展后,在IE中输入http://localhost/isapi/isapi.my?name=xyz&age=16,如果使用上述的配置文件内容,将会出现如下内容。
<?xml version="1.0" encoding="gb2312" ?>
<root>
    <table1>
    </table1>
</root>
 
九、总结
ISAPI扩展的用途非常广泛,可以将其当成服务端的程序,就像php、asp.net、asp或jsp一样使用、也可以对特定的文件进行过滤(本文所举的例子就是根据不同的查询字符串对xml文件进行过滤)、如果对编译原理等相关知识比较熟悉的话,甚至可以做成像php一样的脚本语言,或是满足某些特殊需求的脚本语言。从技术角度来说,ISAPI扩展具有以下优势。
1  比CGI、asp、jsp等脚本语言拥有更高的性能,因为ISAPI扩展在第一次运行时就加载到内存中,因此第二次运行的速度将非常快。
2  ISAPI可以使用任何能够生成API的语言编写,因此可以满足不同程序员的需要。
3  由于ISAPI可以用C++、delphi(pascal)等强大的语言编写,因此可以做到一些脚本语言很难做到的事。
4  由于ISAPI是以本地二进制形式发布的,因此很难被反编译,从而具有很好的保密性。
总之,合理利用ISAPI扩展,可以充分利用IIS的构架来满足不同的需求。
 
0
相关文章