ASP.NET AJAX 脚本方法访问的客户端缓存






4.38/5 (4投票s)
作为 ASP.NET AJAX 的关键功能之一,在构建使用 ASP.NET 的 AJAX 应用程序时,从客户端调用脚本方法将非常常用。虽然存在服务器端缓存机制,但如何将响应缓存到客户端以获得更好的性能呢?
背景
缓存是构建高性能和可伸缩 Web 应用程序最重要的方面之一。作为 ASP.NET AJAX 的关键功能之一,在构建使用 ASP.NET 的 AJAX 应用程序时,从客户端调用脚本方法将非常常用。以下是一个示例,展示如何启用服务器端缓存。
<asp:ScriptManager ID="ScriptManager1" runat="server" ScriptMode="Debug">
<Services>
<asp:ServiceReference Path="CacheService.asmx" />
</Services>
</asp:ScriptManager>
<script language="javascript" type="text/javascript">
var count = 0;
function getServerTime()
{
window.count ++;
CacheService.GetServerTime(onSucceeded);
}
function onSucceeded(result)
{
Sys.Debug.trace(result.format("HH:mm:ss"));
if (count < 6)
{
window.setTimeout(getServerTime, 3000);
}
elses
{
window.count = 0;
}
}
</script>
<input type="button" value="GetCurrentTime" onclick="getServerTime()" />
<br /><br />
<textarea cols="20" rows="10" id="TraceConsole"></textarea>
这里有一个 ScriptManager
控件,其 ScirptMode
属性设置为 Debug
,以便我们可以使用 Sys.Debug.trace
方法在 ID 为 "TraceConsole
" 的 TextArea
元素中显示消息。当我们点击按钮时,我们将访问服务器上的方法六次以获取服务器时间,并且每次连续调用之间有 3 秒的间隔。脚本方法在服务器端定义如下:
[ScriptService]
public class CacheService : System.Web.Services.WebService
{
[WebMethod]
public DateTime GetServerTime()
{
return DateTime.Now;
}
}
打开页面并点击按钮,我们将在页面上看到以下结果:
服务器端缓存
ASP.NET AJAX 中的脚本方法访问具有内置的服务器端缓存机制,但似乎只有少数开发人员意识到这一点。他们通常会将数据缓存到 HttpContext.Cache
对象或其他位置,并在必要时从缓存中获取数据。这是开发 ASP.NET 应用程序时常见的缓存方式之一,但有时我们可以使用更方便、更高效的功能。请看下面的代码,了解如何启用此功能:
[WebMethod(CacheDuration=10)]
public DateTime GetServerTime()
{
return DateTime.Now;
}
就像在 ASP.NET 中构建 Web 服务一样,我们使用相同的方式让 ASP.NET 缓存对具有相同内容的相同资源的请求,方法是设置 WebMethodAttribute
的 CacheDuration
属性。如上面的代码片段所示,方法的返回值将在 10 秒内被缓存。System.Web.Extensions.dll 中的 System.Web.Script.Services.RestHandler
类的静态 InitializeCachePolicy
方法显示了如果我们这样做会发生什么:
private static void InitializeCachePolicy(WebServiceMethodData methodData,
HttpContext context)
{
int cacheDuration = methodData.CacheDuration;
if (cacheDuration > 0)
{
context.Response.Cache.SetCacheability(HttpCacheability.Server);
context.Response.Cache.SetExpires(DateTime.Now.AddSeconds(
(double) cacheDuration));
context.Response.Cache.SetSlidingExpiration(false);
context.Response.Cache.SetValidUntilExpires(true);
if (methodData.ParameterDatas.Count > 0)
{
context.Response.Cache.VaryByParams["*"] = true;
}
else
{
context.Response.Cache.VaryByParams.IgnoreParams = true;
}
}
else
{
context.Response.Cache.SetNoServerCaching();
context.Response.Cache.SetMaxAge(TimeSpan.Zero);
}
}
如果 ASP.NET AJAX 检测到要执行的方法设置了 CacheDuration
,它将向当前 HttpCachePolicy
对象的 SetCacheability
方法传递 HttpCacheabibity.Server
,以便请求的响应可以缓存以供将来使用。如果执行的方法包含参数,则当前 HttpCachePolicy
的 VaryByParams
属性将通过 HttpCacheVaryByParams
对象将 "*" 项目设置为 true
,以便 ASP.NET 为不同的参数组合缓存单独的响应。
让我们看看缓存的效果。
与以编程方式缓存数据相比,设置 CacheDuration
属性以启用缓存的最重要优点是它非常易于使用。现在我们可以更多地专注于方法的实现,而无需处理缓存带来的麻烦(例如同步)。它还略微提高了性能,因为不需要将结果序列化为 JSON 字符串,而 ASP.NET 会负责将缓存的数据发送到客户端。但有时,自行缓存数据会更合适,因为它会节省许多资源。例如,这里有一个接受四个参数的方法,其中第二个参数指示其余参数将被忽略或不被忽略。
public string GetResult(int key, bool ignoreRest, string args1, string args2) { ... }
在上述场景中,几乎所有程序员都会仅为 key
参数的不同值缓存数据,而不会关心第二个参数设置为 true
时其余参数是什么。但是 ASP.NET 无法理解参数的含义,因此它会为任何组合缓存不同的数据副本,尽管结果相同。
客户端缓存
我使用了 HttpWatch 基础版 来捕获客户端和服务器之间的通信。这是快照。
每次访问脚本方法时,相同的内容都会发送到服务器,并接收相同的响应。尽管响应会被缓存,但我们只节省了方法的执行时间,每次调用方法时仍然需要往返通信。这意味着,如果响应的大小很大或带宽较低,访问脚本方法对用户来说仍然是耗时的任务。因此,如果我们能够将响应缓存到客户端,以便用户在再次调用相同方法并使用相同参数时可以立即获得响应,那就更好了——即使网络连接丢失。
开始吧。
起初,如果我们想让浏览器为我们缓存响应,我们只能使用 HTTP GET 方法来访问脚本方法。
[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime() { ... }
我们将使用传统方式将响应缓存到客户端。
[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime()
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;
cache.SetCacheability(HttpCacheability.Private);
cache.SetExpires(DateTime.Now.AddSeconds((double)10));
cache.SetMaxAge(new TimeSpan(0, 0, 10));
return DateTime.Now;
}
我们将 Cacheabilitiy
设置为 public
(如果您想阻止响应被缓存并在不同客户端之间共享,private
也可以),并指定 10 秒的到期时间。由于 ASP.NET AJAX 已将此值设置为零(TimeSpan.Zero
),因此我们也将其 MaxAge
设置为 10 秒。让我们检查一下效果……从未缓存?让我们看看其中一个响应中设置的标头。
Cache-Control public, max-age=0
Date Fri, 29 Jun 2007 00:44:14 GMT
Expires Fri, 29 Jun 2007 00:44:24 GMT
问题是 Cache-Control
中设置的 max-age
值。我们已经将其设置为 10 秒,但由于 HttpCachePolicy
类的 SetMaxAge
方法的实现,它仍然是零。
public void SetMaxAge(TimeSpan delta)
{
if (delta < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException("delta");
}
if (s_oneYear < delta)
{
delta = s_oneYear;
}
if (!this._isMaxAgeSet || (delta < this._maxAge))
{
this.Dirtied();
this._maxAge = delta;
this._isMaxAgeSet = true;
}
}
调用 SetMaxAge
方法一次后,_isMaxAgeSet
标志被设置为 true
,阻止 _maxAge
被设置为小于当前值的值。执行脚本方法时,_isMaxAgeSet
为 true
,_maxAge
为 TimeSpan.Zero
,因此我们无法将其设置为任何其他值。是时候使用反射了。我们应该直接设置 _maxAge
值。
[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime()
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;
cache.SetCacheability(HttpCacheability.Private);
cache.SetExpires(DateTime.Now.AddSeconds((double)10));
FieldInfo maxAgeField = cache.GetType().GetField(
"_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
maxAgeField.SetValue(cache, new TimeSpan(0, 0, 10));
return DateTime.Now;
}
让我们检查一下效果。
似乎与之前的没有区别,但 HttpWatch 可以告诉我们这一点:
没有发送和接收任何内容,因此缓存的响应确实已被“缓存”,并且客户端和服务器之间的往返通信已节省!请注意,缓存也是“特定于参数组合的”,这意味着如果您使用不同的参数,由于请求 URL(查询字符串)已更改,您将获得新响应。
客户端缓存的成功得益于浏览器收到的响应标头。请关注 Cache-Control
项中的 max-age
值。
Cache-Control public, max-age=10
Date Fri, 29 Jun 2007 00:54:32 GMT
Expires Fri, 29 Jun 2007 00:54:42 GMT
在客户端缓存响应的最重要优点是它显着提高了性能。但它也有缺点。也许其中最严重的是反射的使用。反射操作在某些主机环境中有一定的限制。如果您想使用反射,您应该遵循以下三种方式之一。但实际上,在虚拟 Web 托管中几乎没有机会应用其中任何一种。
- 在您的 Web 应用程序中使用完全信任。
- 使用带有
ReflectionPermission
的自定义信任级别。 - 将使用反射的代码放入单独的程序集中,并将其注册到 GAC。
客户端缓存响应的另一个缺点是,不同的客户端在缓存之前至少会执行一次服务器方法以获取响应。在这方面,服务器端缓存效果更好,因为服务器方法只会被执行一次(对于相同的参数组合),并且缓存的响应可以发送到所有客户端的请求。但我们可以最大程度地减少方法的执行时间,例如,像我们一直做的那样,在服务器端以编程方式缓存响应。
结论
在本文中,我们讨论了三种用于提高 ASP.NET AJAX 中脚本方法访问性能的缓存方法。现在是时候得出结论了。
- 以编程方式缓存到服务器端: 这是三种方法中最灵活的一种。我们可以缓存任何我们想要的东西,并选择任何缓存策略。
- 通过设置
CacheDuration
属性缓存到服务器端: 这是最简单的缓存方法。方法将只执行一次(对于相同的参数组合),并且缓存的响应可以发送到所有客户端的请求。有时,这效率不高,因为 ASP.NET 会缓存比我们实际需要更多的数据副本。 - 客户端缓存: 缓存后,这可以提供最佳性能,因为节省了客户端和服务器之间的往返通信。但不同的客户端在缓存之前至少会执行一次服务器方法以获取响应。