技术开发 频道

ASP.NET AJAX之内部揭秘

浏览器只允许同一时间内有两个调用,此时不会执行其他任何命令

    浏览器在同一时间内只能对一个域名处理两个并发的AJAX调用。如果你有5个AJAX调用,那么浏览器首先将会处理两个,然后等其中一个完成后,再处理另一个调用,直到剩下的4个调用都被完成。此外,你不要指望调用的执行顺序会与你处理调用的顺序相同,这是为什么呢?

    正如你所见,调用3需要下载比较大的数据,所以它所需的时间就会比调用5要长,事实上,调用5会在调用3之前执行完。

   所以,在HTTP的世界里,这些都是不可预知的。

    当队列里有多于两个调用的时候浏览器将不会响应

    尝试这样做,在首次访问时打开任何一个加载了大量RSS的页(如Pageflakes, Netvibes, Protopage),在加载期间,你可以尝试着单击一个链接到另一个站点或者试着直接访问另一个站点,那么你就会发现浏览器不会有任何响应。直到浏览器里所有队列的AJAX调用都完成之后,浏览器才能接受另一个活动。这是IE的一个比较糟糕的地方,但是Firefox和Opera就不会有此问题。
    这个问题是,当你有大量的AJAX调用的时候,浏览器会将所有的调用放到一个队列里,在同一时间内只执行其中的两个。所以,如果你单击了某个链接或者转向另一个站点,那么浏览器必须等待在得到另一个调用之前正在执行的调用完成之后才会去处理。解决这个问题的办法就是防止浏览器在同一时间内有多于两个的调用在队列里。我们需要维持一个自己的队列,然后从我们的队列里将一个一个调用的发到浏览器的队列中。

    这个解决方案是很棒,它可以防止调用间的冲突:
var GlobalCallQueue = {
_callQueue : [], // 保存web method的调用列表
_callInProgress : 0, // 浏览器目前处理的web method的编号
_maxConcurrentCall : 2, // 同一时间内执行调用的最大数
_delayBetweenCalls : 50, // 调用执行之间的延迟
call : function(servicePath, methodName, useGet,
params, onSuccess, onFailure, userContext, timeout)
{
var queuedCall = new QueuedCall(servicePath, methodName, useGet,
params, onSuccess, onFailure, userContext, timeout);

Array.add(GlobalCallQueue._callQueue,queuedCall);
GlobalCallQueue.run();
},
run : function()
{
/// 从队列里执行一个调用

if( 0 == GlobalCallQueue._callQueue.length ) return;
if( GlobalCallQueue._callInProgress <
GlobalCallQueue._maxConcurrentCall )
{
GlobalCallQueue._callInProgress ++;
// 得到第一个调用队列
var queuedCall = GlobalCallQueue._callQueue[0];
Array.removeAt( GlobalCallQueue._callQueue, 0 );

// 调用web method
queuedCall.execute();
}
else
{
// 达到最大并发数,不能运行另一个调用
// 处理中的webservice method
}
},
callComplete : function()
{
GlobalCallQueue._callInProgress --;
GlobalCallQueue.run();
}
};

QueuedCall = function( servicePath, methodName, useGet, params,
onSuccess, onFailure, userContext, timeout )
{
this._servicePath = servicePath;
this._methodName = methodName;
this._useGet = useGet;
this._params = params;

this._onSuccess = onSuccess;
this._onFailure = onFailure;
this._userContext = userContext;
this._timeout = timeout;
}

QueuedCall.prototype =
{
execute : function()
{
Sys.Net.WebServiceProxy.original_invoke(
this._servicePath, this._methodName, this._useGet, this._params,
Function.createDelegate(this, this.onSuccess), // 调用处理完成
Function.createDelegate(this, this.onFailure), // 调用处理完成
this._userContext, this._timeout );
},
onSuccess : function(result, userContext, methodName)
{
this._onSuccess(result, userContext, methodName);
GlobalCallQueue.callComplete();
},
onFailure : function(result, userContext, methodName)
{
this._onFailure(result, userContext, methodName);
GlobalCallQueue.callComplete();
}
};
QueueCall封装了一个web method调用,它拥有真实web服务调用的所有参数,并且重写了onSuccess和onFailure回调函数。我们想知道当一个调用完成或者失败了的时候,如何从我们的队列里调出另一个调用。GlobalCallQueue保存了web服务调用的列表。无论何时,当一个web method被调用时,我们先要对GlobalCallQueue中的调用进行排队,并从我们自己的队列里一个一个的执行调用。这样就可以保证浏览器在相同的时间里不会有多于两个的调用,所以浏览器就不会停止响应。

为了确保队列是基于调用的,我们需要像之前那样再次重写ASP.NET AJAX的web method
Sys.Net.WebServiceProxy.original_invoke = Sys.Net.WebServiceProxy.invoke;
Sys.Net.WebServiceProxy.invoke =
function Sys$Net$WebServiceProxy$invoke(servicePath, methodName,
useGet, params, onSuccess, onFailure, userContext, timeout)
{
GlobalCallQueue.call(servicePath, methodName, useGet, params,
onSuccess, onFailure, userContext, timeout);
}

    在浏览器中缓存web服务响应可以显著节省带宽

    浏览器可以在用户的硬盘里缓存图片、JavaScript、CSS文件,如果XML HTTP调用是一个HTTP GET的话也是可以缓存的。这个缓存是基于URL的。如果是相同URL,且保存在同一个电脑里,那么数据将从缓存里加载,而不会向服务器再次请求。基本上,浏览器可以缓存任何HTTP GET请求并且返回基于URL的被缓存数据。如果你把一个XML HTTP调用作为HTTP Get方式的话,那么服务端将返回一些特殊的头信息,用于通知浏览器对相应做缓存,之后再次调用相同的内容,结果就会立即从缓存中被返回,从而减少了网络传输延迟和下载时间。

    在Pageflakes中,我们对用户的状态做了缓存,所以当用户再次访问的时候会从浏览器的缓存里立即得到缓存数据,而不用通过服务端。因此第二次加载时间会变得非常快。我们也缓存了用户的某些行为所产生的结果。当用户再次做相同行为时,缓存结果就会立即从用户的本地缓存中加载,从而减少了网络传输时间。用户会体验到一个快速加载和高响应的站点,获得速度会有明显增长。

    这个方法就是处理Atlas web service调用时要使用HTTP GET方式,并且要返回一些明确的HTTP头信息告知浏览器具体要缓存多长时间。如果在响应期间你返回了一个“Expires”头信息,那么浏览器就会缓存这个XML HTTP结果。这里你需要返回两个头信息去通知浏览器缓存结果。
HTTP/1.1 200 OK
Expires: Fri, 1 Jan 2030
Cache-Control: public
    该信息将通知浏览器要缓存结果直到2030年1月1日。在你处理具有相同参数的同一个XML HTTP调用的时候,就将从电脑的缓存中加载数据,而不会通过服务端。这里还有更多的控制缓存的高级方法。例如,有一个头信息通知浏览器缓存60秒,那么浏览器要在60秒之后才能接触到服务端并获取新的结果。当60秒后浏览器本地缓存过期的时候,它也会防止从代理服务器端获得已缓存的响应。
HTTP/1.1 200 OK
Cache-Control: private, must-revalidate, proxy-revalidate, max-age=60
    让我们来尝试着在一个ASP.NET web service方法中产生这样的头信息:
[WebMethod][ScriptMethod(UseHttpGet=true)]
public string CachedGet()
{
TimeSpan cacheDuration = TimeSpan.FromMinutes(1);
Context.Response.Cache.SetCacheability(HttpCacheability.Public);
Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
Context.Response.Cache.SetMaxAge(cacheDuration);
Context.Response.Cache.AppendCacheExtension(
"must-revalidate, proxy-revalidate");

return DateTime.Now.ToString();
}
    “Expires”头信息被正确的设置。但是问题发生在“Cache-Control”,它显示了“max-age”的值被设置成零,这将阻止浏览器从任何缓存中读取数据。如果你真的想禁用缓存,当然要发送这样一条Cache-Control头信息。结果像是发生了相反的事情。

   不能改变“max-age”头信息是ASP.NET 2.0中的bug。因为“max-age”被设置成零,而“max-age”的值等于零就意味着不需要缓存,所以ASP.NET 2.0才把“Cache-Control”设置为“private”。所以使ASP.NET 2.0返回正确的缓存响应的头信息是不可行的。 

    不知何故,this._maxAge的值会被设置成零,看一下这段代码“if (!this._isMaxAgeSet || (delta < this._maxAge))”,它用于防止_maxAge被设置得过大。由于这个问题,我们需绕过SetMaxAge函数,然后使用反射去直接的设置_maxAge的值。
[WebMethod][ScriptMethod(UseHttpGet=true)]
public string CachedGet2()
{
TimeSpan cacheDuration = TimeSpan.FromMinutes(1);

FieldInfo maxAge = Context.Response.Cache.GetType().GetField("_maxAge",
BindingFlags.Instance|BindingFlags.NonPublic);
maxAge.SetValue(Context.Response.Cache, cacheDuration);

Context.Response.Cache.SetCacheability(HttpCacheability.Public);
Context.Response.Cache.SetExpires(DateTime.Now.Add(cacheDuration));
Context.Response.Cache.AppendCacheExtension(
"must-revalidate, proxy-revalidate");

return DateTime.Now.ToString();
}

0
相关文章