目标
实现对Web请求的动态内容进行字符串过滤,比如去掉所有注释和空行(可自行配置),亦可压缩HTML输出流,减小流量消耗。
前言
关于什么是HttpModule及其作用,可自行查找相关文章。本文旨在通过对HttpModule的实际应用,加深对服务器动态处理请求过程的理解。其中参考了网上几位大侠的处理思路,同时结合自身实际,编织出了自己的一套方法。当然肯定有更适合的处理办法,希望有缘能看到这篇文章的朋友们不吝赐教,并且对于其中的不当之处,如能指出,感激不尽。
创建HttpModule
- 开始第一步,向项目中添加一个新类HttpFilterModule,实现IHttpModule类的接口,命名为HttpFilterModule.cs,默认位于App_Code文件夹中。
- 在IHttpModule中需要实现两个接口函数,一个是Init(HttpApplication application),其中以HttpApplication类型作为传入参数,需要具体实现,也是主要实现方法;一个是Dispose(),无参数,这里无需具体实现方法。HttpFilterModule.cs文件内容如下:
主要HttpModule建立好了,现在需要的是实现方法。不过现在涉及到另一个很重要的对象:Response.Filter,通过IntelliSense的快速信息我们可以看到,Response.Filter对象属于System.IO.Stream对象,解释是“获取或设置一个包装筛选器对象,该对象用于在传输之前修改HTTP实体主体。”因此可以看出通过修改(设置)改对象可达到过滤HTTP实体主体内容的目的。using System; using System.Web; using System.Web.Configuration; /// <summary> /// HTTP页面字符串过滤 /// </summary> public class HttpFilterModule : IHttpModule { public HttpFilterModule(){} public void Init(HttpApplication application) { //TODO: 这里实现具体过滤方法 } public void Dispose() { } }
创建RawFilter过滤器
既然Response.Filter对象属于System.IO.Stream对象,要修改它就需要建立一个原始过滤对象,继承Stream对象,然后重写Stream对象的Write()方法修改其输出对象,最后将重写后的Stream流对象赋给Response.Filter对象即可。具体操作:新建 RawFilter.cs文件,默认也位于App_Code文件夹中,双击打开输入以下代码:using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml.XPath;
/// <summary>
/// 自定义原始过滤器,用于处理原始数据流
/// </summary>
public class RawFilter : Stream
{
Stream responseStream;
HttpRequest request;
long position;
StringBuilder responseHtml;
/// <summary>
/// 原始过滤器
/// </summary>
/// <param name="inputStream">输入流</param>
public RawFilter(Stream inputStream, HttpRequest httpRequest)
{
responseStream = inputStream;
responseHtml = new StringBuilder();
request = httpRequest;
}
//关键的点,在HttpResponse 输出内容的时候,输出一定会调用此方法数据,所以要在此方法内截获数据(重写)
public override void Write(byte[] buffer, int offset, int count)
{
string strBuffer = System.Text.UTF8Encoding.UTF8.GetString(buffer, offset, count);
//采用正则表达式,检查输入是否有页面结束符</html>,有即表示页面流输出完毕
Regex eof = new Regex("</html>", RegexOptions.IgnoreCase);
if (!eof.IsMatch(strBuffer))
{
//页面没有输出完毕,继续追加内容
responseHtml.Append(strBuffer);
}
else
{
//页面输出已经完毕,截获内容
responseHtml.Append(strBuffer);
string finalHtml = responseHtml.ToString();
//谨慎选择注释以下内容,因为网页内容/排版可能会因此而改变
//finalHtml = Regex.Replace(finalHtml, "<!--.*-->", string.Empty, RegexOptions.Compiled | RegexOptions.Multiline);
//finalHtml = Regex.Replace(finalHtml, "^\\s*", string.Empty, RegexOptions.Compiled | RegexOptions.Multiline);
//finalHtml = Regex.Replace(finalHtml, "\\r\\n", string.Empty, RegexOptions.Compiled | RegexOptions.Multiline);
//注释以上内容的原因那里把过滤内容写死了,以后手动修改很麻烦,
//因此我另建FilterString()方法通过读取XML配置文件来动态更新过滤方法
finalHtml = FilterString(finalHtml); //过滤字符串
//继续传递要发出的内容写入流
byte[] data = System.Text.UTF8Encoding.UTF8.GetBytes(finalHtml);
responseStream.Write(data, 0, data.Length);
}
}
#region 过滤字符串主函数
/// <summary>
/// 过滤字符串
/// </summary>
/// <param name="InputString">输入源字符串</param>
/// <returns>返回已过滤的字符串</returns>
protected string FilterString(string InputString)
{
string configPath = HttpContext.Current.Server.MapPath("~/XML/FilterConfig.xml");
string finalString = InputString;
try
{
if (!System.IO.File.Exists(configPath)) //如果配置文件不存在,则创建默认配置文件
{
return finalString; //不存在则原样返回
}
else
{
XPathDocument xpDoc = new XPathDocument(configPath); //载入只读配置文件
XPathNavigator xpNav = xpDoc.CreateNavigator();
XPathNodeIterator xpNt = xpNav.Select("//Rules/FilterRule");
while (xpNt.MoveNext())
{
XPathNavigator xpNav2 = xpNt.Current.Clone();
string lookFor = ""; //查询规则,为正则表达式
string sendTo = ""; //重写规则,为正则表达式
XPathNodeIterator xpNt2 = xpNav2.Select("LookFor");
while (xpNt2.MoveNext())
{
lookFor = xpNt2.Current.Value;
break;
}
xpNt2 = xpNav2.Select("SendTo");
while (xpNt2.MoveNext())
{
sendTo = xpNt2.Current.Value;
break;
}
if (lookFor != string.Empty)
{
finalString = Regex.Replace(finalString, lookFor, sendTo, RegexOptions.Compiled | RegexOptions.Multiline);
}
} //END WHILE
return finalString;
}
}
catch (Exception ex)
{
return finalString;
}
}
#endregion
#region 实现 Stream 抽象方法
public override bool CanRead{get{return true;}}
public override bool CanSeek{ get { return true; }}
public override bool CanWrite{ get{ return true; }}
public override void Close(){ responseStream.Close(); }
public override void Flush(){ responseStream.Flush(); }
public override long Length{ get{ return 0; }}
public override long Position{ get { return position; } set { position = value; }}
public override int Read(byte[] buffer, int offset, int count){ return responseStream.Read(buffer, offset, count);}
public override long Seek(long offset, SeekOrigin origin){ return responseStream.Seek(offset, origin); }
public override void SetLength(long length) { responseStream.SetLength(length);}
#endregion
}
仔细观察以上代码,整个思路是:
- 该类继承Stream类,注意Stream是抽象类,派生类需要实现它的所有抽象方法,因此最下面的抽象方法实现不能遗漏(虽然看似不参与主要功能);
- 构造函数用于初始化输入流和请求对象;
- 主要方法Write重写了基类中的方法,首先将字节流转换成字符串,通过正则表达式匹配内容来判断页面输出是否完毕,完毕后再整合原始HTML字符串;
- 通过自定义函数对输入HTML字符流进行过滤,主要是在XML配置文件中读取自定义配置节,节点内容是正则表达式的原始匹配字符串和目标字符串,然后进行字符串替换;
- 最后再将字符串转换成字节数组进行Write输出;
<?xml version="1.0" encoding="utf-8" ?>
<!-- 字符串过滤配置文件 -->
<FilterConfig>
<!-- 过滤规则 -->
<Rules>
<!-- 注释标记 -->
<FilterRule>
<LookFor><!--.*--></LookFor>
<SendTo></SendTo>
</FilterRule>
<!-- 空行 -->
<FilterRule>
<LookFor>^\s*</LookFor>
<SendTo></SendTo>
</FilterRule>
</Rules>
</FilterConfig>
我用正则表达式专门匹配了注释标记和空行,<LookFor>是要匹配的原始字符串,也就是注释标记格式,HTML注释格式为:<!-- --> ,利用正则表达式很方便,这里说个题外话,正则表达式可是个好东西,但是学习起来会有很大的起伏过程,正则表达式测试工具有很多,VS的插件也有,比如RegexTester等,正则表达式很值得探索。<SendTo>配置节内容是匹配之后想要替换成的内容,这里将注释和空行都替换为空字符,也就达到了去掉的目的。
装配RawFilter过滤器
好了,现在要做的就是原始过滤器的装配了,也就是修改(设置)之前的Response.Filter对象。回到 HttpFilterModule.cs文件,其中新建自定义函数 application_ReleaseRequestState用于在HTTP请求结束后进行过滤(关于请求处理过程各个不同状态及不同功能可查阅相关资料,如BeginRequest在刚开始发送请求时发生), application_ReleaseRequestState函数内容如下:/// <summary>
/// 对此HTTP请求处理的过程全部结束
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void FilterStream(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
//针对网页类型过滤
if (application.Response.ContentType.ToLowerInvariant().Contains("text/html"))
{
//针对ASPX页面进行拦截
string reqPath = application.Request.CurrentExecutionFilePath;
if (reqPath.Contains("aspx"))
{
//装配过滤器
application.Response.Filter = new RawFilter(application.Response.Filter, application.Request);
}
}
}
仔细观察以上代码,我只针对后缀名为aspx的文件进行处理,可根据实际情况进行更改。
这样只是完成了一个函数的设计,现在要进行调用,这里就涉及到在什么时候进行调用的问题,关于HttpModule处理请求的不同过程需要了解,这里是要过滤请求完成后的内容,因此需要等待请求结束释放后再进行处理,注意到刚开始建立的
HttpFilterModule.cs文件中的TODO(待完成,备忘)标记了麽,是的,在
HttpFilterModule实现
IHttpModule接口中有个很重要的接口
Init()初始化方法,模块启动时将会调用该方法,其中传入的参数是整个
HttpApplication对象(拿到了这个,还有什么不能干^_^)。因此,在Init()方法中为应用程序对象(HttpApplication)中的释放请求状态事件
ReleaseRequestState添加一个处理事件,也就是
application_ReleaseRequestState处理。
using System;
using System.Web;
using System.Web.Configuration;
/// <summary>
/// HTTP页面字符串过滤
/// </summary>
public class HttpFilterModule : IHttpModule
{
public HttpFilterModule(){}
public void Init(HttpApplication application)
{
//加入释放请求事件
application.ReleaseRequestState += new EventHandler(FilterStream);
}
public void Dispose() { }
}
配置Web.config文件
整个过程就完成了,捏了一把汗。不过你以为这样就可以成功了麽,那你真是Too young to simple, sometimes naive. 细心地你会问为什么程序知道要执行 HttpFilterModule这个模块呢?问的不错,她真的不知道,所以你要告诉她。其实到了这一步就很简单了,但很关键(很容易忘掉),那就是要在 Web.config文件中添加相应配置,也就是告诉网站程序,我在这里安了一道门,进出都要从我这里过,并且经过我的审核。具体配置节位于 system.web/httpModules中,没有的话需要自行添加。[注:在IIS7及以上版本的服务器中可能需要配置在 system.webServer/Modules配置节中,详情请参考IIS帮助说明]<system.web>
<httpModules>
<add name="HttpFilterModule" type="HttpFilterModule" />
</httpModules>
</system.web>
注意以上name的内容为模块名,也就是新建的类名,如果有命名空间的要改为“命名空间名.模块名”形式。
以上实现[内容过滤]的问题就完成了,现在可以试试运行了,我们来看看效果。
1. 源代码编辑器中原始代码(含空行和注释)
2. 不加HttpModule模块时运行后查看源代码效果(依然含很多空行和注释)
3. 加上HttpModule模块后运行查看源代码效果(所有空行和注释都已消除)
关于去除注释和空格有什么用处,大家可以自己权衡,我只是提供了一种思路和方法,想法是无穷大的,大家有什么好的创意可以交流交流。比如还可以去除标签外所有空白,将网页压缩成就好像一团乱麻一样(看过百度首页源代码的人应该知道)。效果如下:
就像压缩js文件一样,貌似挺酷炫的 [呵呵]