好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

用程序实现HTTP压缩和缓存

用程序实现HTTP压缩和缓存

用 Asp.Net 开发 Web 应用时,为了减少请求次数和流量,可以在 IIS 里配置 gzip 压缩以及开启客户端缓存。园子里已经有很多文章介绍了如何在 IIS 里开启压缩和缓存,但我想搞清楚该如何自己写代码来实现 http 压缩或者缓存,这样做的原因主要有下面两点:

1.IIS 的版本不同,启用 IIS 的 http 压缩的方式也不同, IIS7 还好一些,但对于 IIS6 来说,稍微麻烦一点;

2. 如果我把应用部署在虚拟空间上,是没办法去设置虚拟主机的 IIS 的

      所以了解如何用程序实现 http 压缩和缓存还是很有必要的。

实现压缩:在 .net 的 System.IO.Compression 命名空间里,有两个类可以帮助我们压缩 response 中的内容: DeflateStream 和 GZIPStream ,分别实现了 deflate 和 gzip 压缩,可以利用这两个类来实现 http 压缩。

实现缓存:通过在 response 的 header 中加入 ETag 、 Expires 或 LastModified ,即可启用浏览器缓存。

      下面我们创建一个小小的 Asp.net Mvc2 App ,然后逐步为它加入压缩和缓存。

      首先新建一个 Asp.net Mvc2 的 web application ,建好后整个 solution 如下图:

    

实现缓存

      要缓存的文件包括 js 、 css 、图片等静态文件。我在上面已经提到了,要使浏览器能够缓存这些文件,需要在 response 的 header 中加入相应的标记。要做到这一点,我们首先要使我们的程序可以控制到这些文件的 response 输出。用 mvc 的 controller 是一个不错的方法,所以首先在 Global.asax.cs 中加入下面的路由规则:

   public    static  void   RegisterRoutes(RouteCollection routes)   
  {  
         routes.IgnoreRoute("    {resource}.axd/{*pathInfo}  ");   
         routes.MapRoute(   
             "    Default  ",   //      路由名称  
             "    {controller}/{action}/{id}  ",   //       带有参数的     URL   
               new   { controller = "  Home  ", action = "  Index  ", id = UrlParameter.Optional }   //      参数默认值  
         );   
          routes.MapRoute(    
              "    Cache  ",   //        路由名称   
              "    Cache/{action}/{version}/{resourceName}  ",    
               new   
              {    
                  controller = "    Cache  ",    
                  action = "    Css  ",    
                  resourceName = "",    
                  version = "    1  "    
              }    //       参数默认值   
              );    
  }  

上面加粗的代码增加了一条 url 路由规则,匹配以 Cache 开头的 url ,并且指定了 Controller 为 Cache 。参数 action 指定请求的是 css 还是 js , resourceName 指定请求的资源的文件名, version 是 css 或 js 文件的版本。加入这个 version 参数的目的是为了刷新客户端的缓存,当 css 或 js 文件做了改动时,只需要在 url 中改变这个 version 值,客户端浏览器就会认为这是一个新的资源,从而请求服务器获取最新版本。

可能你会有疑问,加了这个路由规则之后,在 View 中引用 css 和 js 的方法是不是得变一下才行呢?没错,既然我要用程序控制 js 或 css 的输出,那么在 View 中引用 js 和 css 的方式也得做些改变。引用 js 和 css 的常规方法如下:

           <  link  href  =  "http://www.cnblogs.com/Content/Site.css"  rel  =  "stylesheet"  type  =  "text/css"   />  
           <  script  src  =  "http://www.cnblogs.com/Scripts/jquery-1.4.1.js"  language  =  "javascript"  type  =  "text/javascript"></  script   >  

这种引用方式是不会匹配到我们新加的路由的,所以在 View 中,要改成如下的方式:

           <  link  href  =  "/Cache/Css/1/site"  rel  =  "Stylesheet"  type  =  "text/css"   />  
           <  script  src  =  "/Cache/Js/1/jquery-1.4.1"  language  =  "javascript"  type  =  "text/javascript"></  script   >  

 

下面我们先实现这个 CacheController 。添加一个新的 Controller ,名为 CacheController ,并为它添加两个 Action:

   using    System.Web.Mvc;  
   namespace    MvcApplication1.Controllers  
  {  
           public  class   CacheController : Controller   
         {   
               public   ActionResult Css(  string   resourceName,   string   version)   
             {   
                   throw  new   System.NotImplementedException();   
             }   
               public   ActionResult Js(  string   resourceName,   string   version)   
             {   
                   throw  new   System.NotImplementedException();   
             }   
         }   
  }  

 

添加的两个 Action 为 Css 和 Js ,分别用于处理对 css 和 js 的请求。其实对 css 和对 js 请求的逻辑是差不多的,都是读取服务器上相应资源的文件内容,然后发送到客户端,不同的只是 css 和 js 文件所在的目录不同而已,所以我们添加一个类来处理对资源的请求。

在 Controllers 下添加一个类,名为 ResourceHandler ,代码如下:

   using    System;  
   using    System.IO;  
   using    System.Web;  
   namespace    MvcApplication1.Controllers  
  {  
           public  class   ResourceHandler   
         {   
               private  static  readonly   TimeSpan CacheDuration = TimeSpan.FromDays(30);   
               private  string   _contentType;   
               private  string   _resourcePath;   
               private   HttpContextBase _context;   
               public   ResourceHandler(  string   resourceName,   string   resourceType, HttpContextBase context)   
             {   
                 ParseResource(resourceName, resourceType, context);   
             }   
               public  string   PhysicalResourcePath {   get  ;   private  set  ; }   
               public   DateTime LastModifiedTime {   get  ;   private  set  ; }   
               private  void   ParseResource(  string   resourceName,   string   resourceType, HttpContextBase context)   
             {   
                     if   (resourceType.ToLower() == "  css  ")   
                 {   
                     _contentType = @"    text/css  ";   
                     _resourcePath =     string  .Format("  ~/Content/{0}.css  ", resourceName);   
                 }   
                   if   (resourceType.ToLower() == "  js  ")   
                 {   
                   _contentType = @"    text/javascript  ";   
                     _resourcePath =     string  .Format("  ~/Scripts/{0}.js  ", resourceName);   
                 }   
                 _context = context;   
                 PhysicalResourcePath = context.Server.MapPath(_resourcePath);   
                 LastModifiedTime = File.GetLastWriteTime(PhysicalResourcePath);   
             }   
               public  void   ProcessRequest()   
             {   
                   if   (IsCachedOnBrowser())   return  ;   
                   byte  [] bts = File.ReadAllBytes(PhysicalResourcePath);   
                 WriteBytes(bts);   
             }   
               protected  bool   IsCachedOnBrowser()   
             {   
                 var ifModifiedSince = _context.Request.Headers["    If-Modified-Since  "];   
                   if   (!  string  .IsNullOrEmpty(ifModifiedSince))   
                 {   
                     var time = DateTime.Parse(ifModifiedSince);   
                      //     加  1  秒的原因是  request  的  header  里的  modified time  没有精确到毫秒,而  _lastModified    是精确到毫秒的   
                       if   (time.AddSeconds(1) >= LastModifiedTime)   
                     {   
                         var response = _context.Response;   
                         response.ClearHeaders();   
                         response.Cache.SetLastModified(LastModifiedTime);   
                         response.Status = "    304 Not Modified  ";   
                         response.AppendHeader("    Content-Length  ", "  0  ");   
                             return  true  ;   
                     }   
                 }   
                   return  false  ;   
             }   
               private  void   WriteBytes(  byte  [] bytes)   
             {   
                 var response = _context.Response;   
                 response.AppendHeader("    Content-Length  ", bytes.Length.ToString());   
                 response.ContentType = _contentType;   
                 response.Cache.SetCacheability(HttpCacheability.Public);   
                 response.Cache.SetExpires(DateTime.Now.Add(CacheDuration));   
                 response.Cache.SetMaxAge(CacheDuration);   
                 response.Cache.SetLastModified(LastModifiedTime);   
                 response.OutputStream.Write(bytes, 0, bytes.Length);   
                 response.Flush();   
             }   
         }   
  }  

 

在上面的代码中, ProecesRequest 负责处理对 css 和 js 的请求,先判断资源是否在客户端浏览器中缓存了,如果没有缓存,再读取 css 或 js 文件,并在 header 中加入和缓存相关的 header ,发送到客户端。

在这里有必要解释一下 IsCachedOnBrowser 这个方法。你可能会质疑这个方法是否有存在的必要:既然浏览器已经缓存了某个资源,那么在缓存过期之前,浏览器就不会再对服务器发出请求了,所以这个方法是不会被调用的。这个方法一旦被调用,那说明浏览器在重新请求服务器,再次读取资源文件不就行了吗,为什么还要判断一次呢?

其实,即使客户端缓存的资源没有过期,浏览器在某些时候也会重新请求服务器的,例如按 F5 刷新的时候。用户按了浏览器的刷新按钮之后,浏览器就会重新请求服务器,并利用 LastModified 或 ETag 来询问服务器资源是否已经改变,所以 IsCachedOnBrowser 这个方法就是用来处理这种情况的:读出 Request 中的 If-Modified-Since ,然后和资源的最后修改时间做比较,如果资源没被修改,则直接返回 304 的代码,告知浏览器只需要从缓存里取就行了。

下面在 CacheController 中使用这个 ResourceHandler 。先增加一个 CacheResult 的类,继承自 ActionReult :

   using    System;  
   using    System.Web.Mvc;  
   namespace    MvcApplication1.Controllers  
  {  
           public  class   CacheResult : ActionResult   
         {   
               private  readonly  string   _resourceName;   
               private  readonly  string   _type;   
               public   CacheResult(  string   resourceName,   string   type)   
             {   
                 _resourceName = resourceName;   
                 _type = type;   
             }   
               public  override  void   ExecuteResult(ControllerContext context)   
             {   
                   if   (context ==   null  )   
                       throw  new   ArgumentNullException("  context  ");   
                    var handler =   new   ResourceHandler(_resourceName, _type, context.HttpContext);    
                  handler.ProcessRequest();    
             }   
         }   
  }  

修改 CacheController 如下:

   using    System.Web.Mvc;  
   namespace    MvcApplication1.Controllers  
  {  
           public  class   CacheController : Controller   
         {   
               public   ActionResult Css(  string   resourceName,   string   version)   
             {   
                   return  new   CacheResult(resourceName, "  css  ");   
             }   
               public   ActionResult Js(  string   resourceName,   string   version)   
             {   
                   return  new   CacheResult(resourceName, "  js  ");   
             }   
         }   
  }  

可以看到,由于 version 只是用来改变 url 更新缓存的,对于我们处理资源的请求是没用的,所以我们在这两个 Action 中都忽略了这两个参数。

缓存的逻辑到这里就完成大部分了,下面我们为 UrlHelper 加两个扩展方法,方便我们在 View 中使用。增加一个 UrlHelperExtensions 的类,代码如下:

   using    System.Web.Mvc;  
   namespace    MvcApplication1  
  {  
           public  static  class   UrlHelperExtensions   
         {   
               public  static  string   CssCache(  this   UrlHelper helper,   string   fileName)   
             {   
                   return   helper.Cache("  Css  ", fileName);   
             }   
               public  static  string   JsCache(  this   UrlHelper helper,   string   fileName)   
             {   
                   return   helper.Cache("  Js  ", fileName);   
             }   
               private  static  string   Cache(  this   UrlHelper helper,   string   resourceType,   string   resourceName)   
             {   
                 var version = System.Configuration.ConfigurationManager.AppSettings["    ResourceVersion  "];   
                 var action = helper.Action(resourceType, "    Cache  ");   
                   return  string  .Format("  {0}/{1}/{2}  ", action, version, resourceName);   
             }   
         }   
  }  

version 配置在 web.config 的 appSettings 节点下。然后修改 Site.Master 中对 css 和 js 的引用:

           <  link  href  =  "<%=Url.CssCache("  site  ") %>"   rel  =  "Stylesheet"  type  =  "text/css"   />  
           <  script  src  =  "<%=Url.JsCache("  jquery  -  1  .  4  .  1  ") %>"   language  =  "javascript"  type  =  "text/javascript"></  script   >  

这样,缓存基本上算是完成了,但我们还漏了一个很重要的问题,那就是 css 中对图片的引用。假设在 site.css 中有下面一段 css :

  body  
  {  
           background-image  :  url(images/bg.jpg)  ;   
  }  

然后再访问 ~/Home/Index 时就会有一个 404 的错误,如下图:

由于 css 中对图片的链接采用的是相对路径,所以浏览器自动计算出 http://localhost:37311/Cache/Css/12/images/bg.jpg 这个路径,但服务器上并不存在这个文件,所以就有了 404 的错误。解决这个问题的方法是再加一个路由规则:

         routes.MapRoute(   
             "    CacheCssImage  ",   //      路由名称  
             "    Cache/Css/{version}/images/{resourceName}  ",   
              new  
             {   
                 controller = "    Cache  ",   
                 action = "    CssImage  ",   
                 resourceName = "",   
                 version = "    1  ",   
                 image = ""   
             }    //     参数默认值  
             );   

这样就把对 ~/Cache/Css/12/images/bg.jpg 的请求路由到了 CacheController 的 CssImage 这个 Action 上。下面我们为 CacheController 加上 CssImage 这个 Action :

   using    System.Web.Mvc;  
   namespace    MvcApplication1.Controllers  
  {  
           public  class   CacheController : Controller   
         {   
               public   ActionResult Css(  string   resourceName,   string   version)   
             {   
                   return  new   CacheResult(resourceName, "  css  ");   
             }   
               public   ActionResult Js(  string   resourceName,   string   version)   
             {   
                   return  new   CacheResult(resourceName, "  js  ");   
             }   
                public   ActionResult CssImage(  string   resourceName,   string   version)    
              {    
                    return  new   CacheResult(resourceName, "  image  ");    
              }    
         }   
  }  

然后修改 ResourceHandler 类,让他支持 image 资源的处理如下:

   using    System;  
   using    System.IO;  
   using    System.Web;  
   namespace    MvcApplication1.Controllers  
  {  
           public  class   ResourceHandler   
         {   
             ...   
               private  void   ParseResource(  string   resourceName,   string   resourceType, HttpContextBase context)   
             {   
                   if   (resourceType.ToLower() == "  css  ")   
                 {   
                     _contentType = @"    text/css  ";   
                     _resourcePath =     string  .Format("  ~/Content/{0}.css  ", resourceName);   
                 }   
                   if   (resourceType.ToLower() == "  js  ")   
                 {   
                     _contentType = @"    text/javascript  ";   
                     _resourcePath =     string  .Format("  ~/Scripts/{0}.js  ", resourceName);   
                 }   
                    if   (resourceType.ToLower() == "  image  ")    
                  {    
                        string   ext = Path.GetExtension(resourceName);    
                        if   (  string  .IsNullOrEmpty(ext))    
                      {    
                          ext = "    .jpg  ";    
                      }    
                      _contentType =     string  .Format("  image/{0}  ", ext.Substring(1));    
                      _resourcePath =     string  .Format("  ~/Content/images/{0}  ", resourceName);    
                  }    
                 ...   
             }   
             ...   
         }   
  }  

再次访问 ~/Home/Index ,可以看到 css 中的 image 已经正常了:

到这里,缓存的实现可以说已经完成了,但总觉得还有个问题很纠结,那就是在修改 css 或 js 之后,如何更新缓存?上面的代码中,可以修改 web.config 中的一个配置来改变 version 值,从而达到更新缓存的目的,但这是一个全局的配置,改变这个配置后,所有的 css 和 js 的 url 都会跟着变。这意味着即使我们只改动其中一个 css 文件,所有的资源文件的缓存都失效了,因为 url 都变了。为了改进这一点,我们需要修改 version 的取值方式,让他不再读取 web.config 中的配置,而是以资源的最后修改时间作为 version 值,这样一旦某个资源文件的最后修改时间变了,该资源的缓存也就跟着失效了,但并不影响其他资源的缓存。修改 UrlHelperExtensions 的 Cache 方法如下:

               private  static  string   Cache(  this   UrlHelper helper,   string   resourceType,   string   resourceName)   
             {   
                  //var version = System.Configuration.ConfigurationManager.AppSettings["ResourceVersion"];  
                    var handler =   new   ResourceHandler(resourceName, resourceType, helper.RequestContext.HttpContext);    
                  var version = handler.LastModifiedTime.Ticks;    
                 var action = helper.Action(resourceType, "    Cache  ");   
                   return  string  .Format("  {0}/{1}/{2}  ", action, version, resourceName);   
             }   

实现 HTTP 压缩

在文章的开头已经提到, DeflateStream 和 GZIPStream 可以帮助我们实现 Http 压缩。让我们来看一下如何使用这两类。

首先要清楚的是我们要压缩的是文本内容,例如 css 、 js 以及 View(aspx) ,图片不需要压缩。

为了压缩 css 和 js ,需要修改 ResourceHandler 类:

   using    System;  
   using    System.IO;  
   using    System.IO.Compression;  
   using    System.Web;  
   namespace    MvcApplication1.Controllers  
  {  
           public  class   ResourceHandler   
         {   
               private  static  readonly   TimeSpan CacheDuration = TimeSpan.FromDays(30);   
               private  string   _contentType;   
               private  string   _resourcePath;   
               private   HttpContextBase _context;   
                private  bool   _needCompressed =   true  ;    
               public   ResourceHandler(  string   resourceName,   string   resourceType, HttpContextBase context)   
             {   
                 ParseResource(resourceName, resourceType, context);   
             }   
               public  string   PhysicalResourcePath {   get  ;   private  set  ; }   
               public   DateTime LastModifiedTime {   get  ;   private  set  ; }   
               private  void   ParseResource(  string   resourceName,   string   resourceType, HttpContextBase context)   
             {   
                     if   (resourceType.ToLower() == "  css  ")   
                 {   
                     _contentType = @"    text/css  ";   
                     _resourcePath =     string  .Format("  ~/Content/{0}.css  ", resourceName);   
                 }   
                   if   (resourceType.ToLower() == "  js  ")   
                 {   
                     _contentType = @"    text/javascript  ";   
                     _resourcePath =     string  .Format("  ~/Scripts/{0}.js  ", resourceName);   
                 }   
                   if   (resourceType.ToLower() == "  image  ")   
                 {   
                       string   ext = Path.GetExtension(resourceName);   
                       if   (  string  .IsNullOrEmpty(ext))   
                     {   
                         ext = "    .jpg  ";   
                     }   
                     _contentType =     string  .Format("  image/{0}  ", ext.Substring(1));   
                     _resourcePath =     string  .Format("  ~/Content/images/{0}  ", resourceName);   
                        _needCompressed =   false  ;    
                 }   
                 _context = context;   
                 PhysicalResourcePath = context.Server.MapPath(_resourcePath);   
                 LastModifiedTime = File.GetLastWriteTime(PhysicalResourcePath);   
             }   
               public  void   ProcessRequest()   
             {   
                   if   (IsCachedOnBrowser())   return  ;   
                   byte  [] bts = File.ReadAllBytes(PhysicalResourcePath);   
                 WriteBytes(bts);   
             }   
               public  static  bool   CanGZip(HttpRequestBase request)   
             {   
                   string   acceptEncoding = request.Headers["  Accept-Encoding  "];   
                   if   (!  string  .IsNullOrEmpty(acceptEncoding) && (acceptEncoding.Contains("  gzip  ")))   
                         return  true  ;   
                   return  false  ;   
             }   
               protected  bool   IsCachedOnBrowser()   
             {   
                 var ifModifiedSince = _context.Request.Headers["    If-Modified-Since  "];   
                   if   (!  string  .IsNullOrEmpty(ifModifiedSince))   
                 {   
                     var time = DateTime.Parse(ifModifiedSince);   
                      //     加  1  秒的原因是  request  的  header  里的  modified time  没有精确到毫秒,而  _lastModified    是精确到毫秒的   
                       if   (time.AddSeconds(1) >= LastModifiedTime)   
                     {   
                         var response = _context.Response;   
                         response.ClearHeaders();   
                         response.Cache.SetLastModified(LastModifiedTime);   
                         response.Status = "    304 Not Modified  ";   
                         response.AppendHeader("    Content-Length  ", "  0  ");   
                           return  true  ;   
                     }   
                 }   
                   return  false  ;   
             }   
               private  void   WriteBytes(  byte  [] bytes)   
             {   
                 var response = _context.Response;   
                  var needCompressed = CanGZip(_context.Request) && _needCompressed;    
                    if   (needCompressed)    
                  {    
                      response.AppendHeader("    Content-Encoding  ", "  gzip  ");    
                      var stream =     new   MemoryStream();    
                      var writer =     new   GZipStream(stream, CompressionMode.Compress);    
                      writer.Write(bytes, 0, bytes.Length);    
                      bytes = stream.ToArray();    
                  }    
                 response.AppendHeader("    Content-Length  ", bytes.Length.ToString());   
                 response.ContentType = _contentType;   
                 response.Cache.SetCacheability(HttpCacheability.Public);   
                 response.Cache.SetExpires(DateTime.Now.Add(CacheDuration));   
                 response.Cache.SetMaxAge(CacheDuration);   
                 response.Cache.SetLastModified(LastModifiedTime);   
                 response.OutputStream.Write(bytes, 0, bytes.Length);   
                 response.Flush();   
             }   
         }   
  }  

 

加粗的代码是修改的内容,并且只用了 gzip 压缩,并没有用 deflate 压缩,有兴趣的同学可以改一改。

为了压缩 View ( aspx ),我们需要添加一个 ActionFilter ,代码如下:

   using    System.IO.Compression;  
   using    System.Web;  
   using    System.Web.Mvc;  
   namespace    MvcApplication1.Controllers  
  {  
           public  class   CompressFilterAttribute : ActionFilterAttribute   
         {   
               public  override  void   OnActionExecuting(ActionExecutingContext filterContext)   
             {   
                 var response = filterContext.HttpContext.Response;   
                 HttpRequestBase request = filterContext.HttpContext.Request;   
                   if   (!ResourceHandler.CanGZip(request))   return  ;   
                 response.AppendHeader("    Content-encoding  ", "  gzip  ");   
                 response.Filter =     new   GZipStream(response.Filter, CompressionMode.Compress);   
             }   
         }   
  }  

然后为 HomeController 添加这个 Filter :

   using    System.Web.Mvc;  
   namespace    MvcApplication1.Controllers  
  {  
         [HandleError]   
          [CompressFilterAttribute]    
           public  class   HomeController : Controller   
         {   
               public   ActionResult Index()   
             {   
                 ViewData["    Message  "] = "      欢迎使用   ASP.NET MVC!     ";  
                   return   View();   
             }   
               public   ActionResult About()   
             {   
                   return   View();   
             }   
         }   
  }  

这样就可以压缩 View 了。

最终的效果如下图:

第一次访问:

第二次访问:

作者:明年我18 
出处: http://www.cnblogs.com/default  
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。

 

作者: Leo_wl

    

出处: http://www.cnblogs.com/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于用程序实现HTTP压缩和缓存的详细内容...

  阅读:41次