避免 10 个常见 ASP.NET 缺陷, 使网站平稳运行

ASP.NET 成功的其中一个原因在于它降低了 Web 开发人员的门槛。即便您不是计算机科学博士也可以编写 ASP.NET 代码。我在工作中遇到的许多 ASP.NET 开发人员都是自学成材的,他们在编写 C# 或 Visual Basic 之前都在编写 Microsoft Excel 电子表格。现在,他们在编写 Web 应用程序,总的来说,他们所做的工作值得表扬。

但是与能力随之而来的还有责任,即使是经验丰富的 ASP.NET 开发人员也难免会出错。在多年的 ASP.NET 项目咨询工作中,我发现某些错误特别容易导致缺陷不断发生。其中某些错误会影响性能。其他错误会抑制可伸缩性。有些错误还会使开发团队耗费宝贵的时间来跟踪错误和意外的行为。

下面是会导致 ASP.NET 生产应用程序的发布过程中出现问题的 10 个缺陷以及可避免它们的方法。所有示例均来自我对真实的公司构建真实的 Web 应用程序的亲身体验,在某些情况下,我会通过介绍 ASP.NET 开发团队在开发过程中遇到的一些问题来提供相关的背景。

LoadControl 和输出缓存

极少有不使用用户控件的 ASP.NET 应用程序。在出现母版页之前,开发人员使用用户控件来提取公用内容,如页眉和页脚。即使在 ASP.NET 2.0 中,用户控件也提供了有效的方法来封装内容和行为以及将页面分为多个区域,这些区域的缓存能力可以独立于作为整体的页面进行控制(一种称为段缓存的特殊输出缓存形式)。

用户控件可以采用声明的方式加载,也可以强制加载。强制加载依赖于 Page.LoadControl,它实例化用户控件并返回控件引用。如果用户控件包含自定义类型的成员(例如,公共属性),则您可以转换该引用并从您的代码访问自定义成员。图 1 中的用户控件实现名为 BackColor 的属性。以下代码加载用户控件并向 BackColor 分配一个值:

protected void Page_Load(object sender, EventArgs e)
{
// 加载用户控件并将其添加到页面中
Control control = LoadControl("~/MyUserControl.ascx");
PlaceHolder1.Controls.Add(control);

// 设置其背景色
((MyUserControl)control).BackColor = Color.Yellow;
}

以上代码实际上很简单,但却是一个等待粗心的开发人员掉进去的陷阱。您能找出其中的破绽吗?

如果您猜到该问题与输出缓存有关,那么您是正确的。正如您所看到的一样,上述代码示例编译和运行都正常,但是如果尝试将以下语句(完全合法)添加到 MyUserControl.ascx 中:

<%@ OutputCache Duration="5" VaryByParam="None" %>

则当您下一次运行该页面时,您将看到 InvalidCastException (oh joy!) 和以下错误消息:

“无法将类型为‘System.Web.UI.PartialCachingControl’的对象转换为类型‘MyUserControl’。”

因此,此代码在没有 OutputCache 指令时运行正常,但如果添加了 OutputCache 指令就会出错。ASP.NET 不应该以这种方式运行。页面(和控件)对于输出缓存应该是不可知的。那么,这代表什么意思?

问题在于为用户控件启用输出缓存时,LoadControl 不再返回对控件实例的引用;相反,它返回对 PartialCachingControl 实例的引用,而 PartialCachingControl 可能会也可能不会包装控件实例,具体取决于控件的输出是否被缓存。因此,如果开发人员调用 LoadControl 以动态加载用户控件并且为了访问控件特定的方法和属性而转换控件引用,他们必须注意进行该操作的方式,以便不管是否具有 OutputCache 指令,代码都可以运行。

图 2 说明动态加载用户控件以及转换返回的控件引用的正确方法。以下是其工作原理概要:

"

如果 ASCX 文件缺少 OutputCache 指令,则 LoadControl 返回一个 MyUserControl 引用。Page_Load 将该引用转换为 MyUserControl 并设置控件的 BackColor 属性。

"

如果 ASCX 文件包括一个 OutputCache 指令并且控件的输出没有被缓存,则 LoadControl 返回一个对 PartialCachingControl 的引用,此 PartialCachingControl 的 CachedControl 属性包含对基础 MyUserControl 的引用。Page_Load 将 PartialCachingControl.CachedControl 转换为 MyUserControl 并设置该控件的 BackColor 属性。

"

如果 ASCX 文件包括一个 OutputCache 指令并且控件的输出被缓存,则 LoadControl 返回一个对 PartialCachingControl(其 CachedControl 属性为空)的引用。注意,Page_Load 不再继续执行操作。无法设置控件的 BackColor 属性,因为该控件的输出来源于输出缓存。换句话说,根本没有要设置属性的 MyUserControl。

不管 .ascx 文件中是否具有 OutputCache 指令,图 2中的代码都将运行。虽然看起来复杂一点,但它会避免烦人的错误。简单并不总是代表易于维护。

会话和输出缓存

谈到输出缓存,ASP.NET 1.1 和 ASP.NET 2.0 都存在一个潜在的问题,该问题会影响在 Windows Server" 2003 和 IIS 6.0 上运行的服务器中的输出缓存页。我曾经亲眼看到该问题在 ASP.NET 生产服务器中出现过两次,这两次都是通过关闭输出缓冲来解决的。后来我了解到有一个比禁用输出缓存更好的解决方案。以下是我第一次遇到该问题时的情况。

当时的情况是这样的,某个网站(我们在此称为 Contoso.com,它在小型 ASP.NET Web 领域中运行公共电子商务应用程序)与我的团队联系,抱怨他们遇到了“跨线程”错误。使用 Contoso.com 网站的客户常常突然丢失已经输入的数据,但却看到另一用户的相关数据。稍做分析即发现,跨线程这个描述并不准确;“跨会话”错误更为贴切。看起来 Contoso.com 是在会话状态中存储数据的,由于某些原因,用户会偶尔随机地连接到其他用户的会话。

我的一个团队成员编写了一个诊断工具,用来将每个 HTTP 请求和响应的关键要素(包括 Cookie 标头)记录到日志中。然后,他将该工具安装在 Contoso.com 的 Web 服务器上,并让其运行了几天。结果非常明显。大概每 100000 个请求中会发生一次这样的情况:ASP.NET 正确地为全新会话分配一个会话 ID 并返回 Set-Cookie 标头中的会话 ID。然后,它会在下一个紧相邻的请求中返回相同的会话 ID(即,相同的 Set-Cookie 标头),即使该请求已经与一个有效的会话相关联并且正确提交了 Cookie 中的会话 ID。实际上,ASP.NET 是随机将用户从他们自己的会话中切换出去并将他们连接到其他会话。

我们很惊讶,于是开始寻找原因。我们首先检查了 Contoso.com 的源代码,让我们感到欣慰的是,问题不在那。接着,为了确保问题与应用程序宿主在 Web 领域无关,我们只保留一个服务器在运行,而关闭了所有其他服务器。问题仍然存在,这并不意外,因为我们的日志显示匹配的 Set-Cookie 标头绝不会来自两个不同的服务器。ASP.NET 意外地生成了重复的会话 ID,这令人难以置信,因为它使用 .NET Framework RNGCryptoServiceProvider 类生成这些 ID,并且会话 ID 的长度足以确保相同的 ID 决不会生成两次(至少在下一个万亿年内不会生成两次)。除此之外,即使 RNGCryptoServiceProvider 错误地生成了重复的随机数字,也无法解释 ASP.NET 为何不可思议地将有效的会话 ID 替换为新的 ID(不唯一)。

凭直觉,我们决定看一下输出缓存。当 OutputCacheModule 缓存 HTTP 响应时,它必须小心不要缓存了 Set-Cookie 标头;否则,包含新会话 ID 的缓存响应会将缓存响应的所有接收者(以及其请求生成了缓存响应的用户)连接到同一会话。我们检查了源代码;Contoso.com 在两个页面中启用了输出缓存。我们关闭了输出缓存。结果,应用程序运行数天而没有发生一个跨会话问题。此后,它运行了两年多都没有发生任何错误。在具有不同应用程序和一组不同 Web 服务器的另一家公司中,我们看到完全相同的问题也消失了。就像在 Contoso.com 一样,消除输出缓存就能解决问题。

Microsoft 后来确认此行为源于 OutputCacheModule 中的问题。(当您阅读本文时,可能已经发布了更新。)当 ASP.NET 与 IIS 6.0 一起使用并且启用内核模式缓存时,OutputCacheModule 有时无法从它传递给 Http.sys 的缓存响应中删除 Set-Cookie 标头。下面是导致出现错误的特定事件顺序:

"

最近没有访问网站(因此也没有对应的会话)的用户请求一个启用了输出缓存的页面,但是其输出当前在缓存中不可用。

"

该请求执行用于访问用户最新创建的会话的代码,从而导致会话 ID Cookie 在响应的 Set-Cookie 标头中返回。

"

OutputCacheModule 向 Http.sys 提供输出,但是无法从响应中删除 Set-Cookie 标头。

"

Http.sys 在后续的请求中返回缓存响应,误将其他用户连接到会话。

故事的寓意又是什么呢?会话状态和内核模式输出缓存不能混合使用。如果您在启用输出缓存的页中使用会话状态,并且应用程序在 IIS 6.0 上运行,则您需要关闭内核模式输出缓存。您仍将受益于输出缓存,但是因为内核模式输出缓存比普通输出缓存快得多,所以缓存不会同样有效。有关此问题的详细信息,请参见 support.microsoft.com/kb/917072

您可以通过在页面的 OutputCache 指令中包含 VaryByParam="*" 属性来关闭单个页面的内核模式输出缓存,虽然这样做可能导致内存需求骤增。另一种更安全的方法是通过在 web.config 中包含下列元素来关闭整个应用程序的内核模式缓存:

<httpRuntime enableKernelOutputCache="false" />

您还可以使用注册表设置来全局性地禁用内核模式输出缓存,即禁用全部服务器的内核模式输出缓存。有关详细信息,请参见 support.microsoft.com/kb/820129

每次我听到客户报告会话发生了费解的问题,我都会询问他们是否在任何页面中使用了输出缓存。如果确实使用了输出缓存,并且宿主操作系统是 Windows Server 2003,我会建议他们禁用内核模式输出缓存。问题通常就会迎刃而解。如果问题没有解决,则错误存在于代码中。警惕!

Forms 身份验证票证生存期

您能找出以下代码的问题吗?

FormsAuthentication.RedirectFromLoginPage(username, true);

此代码看似没有问题,但决不能在 ASP.NET 1.x 应用程序中使用,除非应用程序中其他位置的代码抵消了此语句的负面作用。如果您不能确定原因,请继续阅读。

FormsAuthentication.RedirectFromLoginPage 执行两个任务。首先,当 FormsAuthenticationModule 将用户重定向到登录页时,FormsAuthentication.RedirectFromLoginPage 将用户重定向到他们原来请求的页面。其次,它发布一个身份验证票证(通常携带在 Cookie 中,而且在 ASP.NET 1.x 中总是携带在 Cookie 中),这个票证允许用户在预定的一段时间内保持已经过身份验证状态。

问题就在于这个时间段。在 ASP.NET 1.x 中,向 RedirectFromLoginPage 传递另一个为 false 的参数会发出一个临时身份验证票证,该票证默认情况下在 30 分钟之后到期。(您可以使用 web.config 的 元素中的 Timeout 属性来更改超时期限。)然而,传递另一个为 true 的参数则会发出一个永久身份验证票证,其有效期为 50 年!这样就会发生问题,因为如果有人窃取了该身份验证票证,他们就可以在票证的有效期内使用受害者的身份访问网站。窃取身份验证票证有多种方法 — 在公共无线访问点探测未加密的通信、跨网站编写脚本、以物理方式访问受害者的计算机等等 — 因此,向 RedirectFromLoginPage 传递 true 比禁用您的网站的安全性好不了多少。幸运的是,此问题已经在 ASP.NET 2.0 中得到了解决。现在的 RedirectFromLoginPage 以相同的方式接受在 web.config 中为临时和永久身份验证票证指定的超时。

一种解决方案是决不在 ASP.NET 1.x 应用程序的 RedirectFromLoginPage 的第二个参数中传递 true。但是这不切实际,因为登录页的特点通常是包含一个“将我保持为登录状态”框,用户可以选中该框以收到永久而不是临时身份验证 Cookie。另一种解决方案是使用 Global.asax(如果您愿意的话,也可以使用 HTTP 模块)中的代码段,此代码段会在包含永久身份验证票证的 Cookie 返回浏览器之前对其进行修改。

图 3 包含一个这样的代码段。如果此代码段位于 Global.asax 中,它会修改传出永久 Forms 身份验证 Cookie 的 Expires 属性,以使 Cookie 在 24 小时后过期。通过修改注释为“新的过期日期”的行,您可以将超时设置为您喜欢的任何日期。

您可能会觉得奇怪,Application_EndRequest 方法调用本地 Helper 方法 (GetCookieFromResponse) 来检查身份验证 Cookie 的传出响应。Helper 方法是解决 ASP.NET 1.1 中另一个错误的方法,如果您使用 HttpCookieCollection 的字符串索引生成器来检查不存在的 Cookie,此错误会导致虚假 Cookie 添加到响应中。使用整数索引生成器作为 GetCookieFromResponse 可以解决该问题。

视图状态:无声的性能杀手

从某种意义上说,视图状态是有史以来最伟大的事情。毕竟,视图状态使得页面和控件能够在回发之间保持状态。因此,您不必像在传统的 ASP 中那样编写代码,以防止在单击按钮时文本框中的文本消失,或在回发后重新查询数据库和重新绑定 DataGrid。

但是视图状态也有缺点:当它增长得过大时,它便成为一个无声的性能杀手。某些控件(例如文本框)会根据视图状态作出相应判断。其他控件(特别是 DataGrid 和 GridView)则根据显示的信息量确定视图状态。如果 GridView 显示 200 或 300 行数据,我会望而生畏。即使 ASP.NET 2.0 视图状态大致是 ASP.NET 1 x 视图状态的一半大小,一个糟糕的 GridView 也可以容易地将浏览器和 Web 服务器之间的连接的有效带宽减少 50%或更多。

您可以通过将 EnableViewState 设置为 false 来关闭单个控件的视图状态,但某些控件(特别是 DataGrid)在不能使用视图状态时会失去某些功能。控制视图状态的更佳解决方案是将其保留在服务器上。在 ASP.NET 1.x 中,您可以重写页面的 LoadPageStateFromPersistenceMedium 和 SavePageStateToPersistenceMedium 方法并按您喜欢的方式处理视图状态。图 4 中的代码显示的重写可防止视图状态保留在隐藏字段中,而将其保留在会话状态中。当与默认会话状态进程模型一起使用时(即,会话状态存储在内存中的 ASP.NET 辅助进程中时),在会话状态中存储视图状态尤其有效。相反,如果会话状态存储在数据库中,则只有测试才能显示在会话状态中保留视图状态会提高还是降低性能。

在 ASP.NET 2.0 中使用相同的方法,但是 ASP.NET 2.0 能够提供更简单的方法将视图状态保留在会话状态中。首先,定义一个自定义页适配器,其 GetStatePersister 方法返回 .NET Framework SessionPageStatePersister 类的一个实例:

public class SessionPageStateAdapter :
System.Web.UI.Adapters.PageAdapter
{
public override PageStatePersister GetStatePersister ()
    {
return new SessionPageStatePersister(this.Page);
    }
}

然后,通过将 App.browsers 文件按以下方式放入应用程序的 App_Browsers 文件夹,将自定义页适配器注册为默认页适配器:

<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.Page"
adapterType="SessionPageStateAdapter" />
</controlAdapters>
</browser>
</browsers>

(您可以将文件命名为您喜欢的任何名称,只要它的扩展名为 .browsers 即可。)此后,ASP.NET 将加载页适配器并使用返回的 SessionPageStatePersister 以保留所有页面状态,包括视图状态。

使用自定义页适配器的一个缺点是它全局性地作用于应用程序中的每一页。如果您更愿意将其中一些页面的视图状态保留在会话状态中而不保留其他页面的视图状态,请使用图 4 中显示的方法。另外,如果用户在同一会话中创建多个浏览器窗口,您使用该方法可能会遇到问题。

<DIV>
<P>ASP.NET 成功的其中一个原因在于它降低了 Web 开发人员的门槛。即便您不是计算机科学博士也可以编写 ASP.NET 代码。我在工作中遇到的许多 ASP.NET 开发人员都是自学成材的,他们在编写 C# 或 Visual Basic 之前都在编写 Microsoft Excel 电子表格。现在,他们在编写 Web 应用程序,总的来说,他们所做的工作值得表扬。</P>
<P>但是与能力随之而来的还有责任,即使是经验丰富的 ASP.NET 开发人员也难免会出错。在多年的 ASP.NET 项目咨询工作中,我发现某些错误特别容易导致缺陷不断发生。其中某些错误会影响性能。其他错误会抑制可伸缩性。有些错误还会使开发团队耗费宝贵的时间来跟踪错误和意外的行为。</P>
<P>下面是会导致 ASP.NET 生产应用程序的发布过程中出现问题的 10 个缺陷以及可避免它们的方法。所有示例均来自我对真实的公司构建真实的 Web 应用程序的亲身体验,在某些情况下,我会通过介绍 ASP.NET 开发团队在开发过程中遇到的一些问题来提供相关的背景。</P><A></A>
<H2>LoadControl 和输出缓存</H2>
<P>极少有不使用用户控件的 ASP.NET 应用程序。在出现母版页之前,开发人员使用用户控件来提取公用内容,如页眉和页脚。即使在 ASP.NET 2.0 中,用户控件也提供了有效的方法来封装内容和行为以及将页面分为多个区域,这些区域的缓存能力可以独立于作为整体的页面进行控制(一种称为段缓存的特殊输出缓存形式)。</P>
<P>用户控件可以采用声明的方式加载,也可以强制加载。强制加载依赖于 Page.LoadControl,它实例化用户控件并返回控件引用。如果用户控件包含自定义类型的成员(例如,公共属性),则您可以转换该引用并从您的代码访问自定义成员。<A href=" http://msdn.microsoft.com/msdnmag/issues/06/07/WebAppFollies/default.aspx?fig=true#fig1"><U><FONT color=#0000ff>图 1</FONT></U></A> 中的用户控件实现名为 BackColor 的属性。以下代码加载用户控件并向 BackColor 分配一个值:</P><PRE>protected void Page_Load(object sender, EventArgs e)
{
// 加载用户控件并将其添加到页面中
Control control = LoadControl("~/MyUserControl.ascx");
PlaceHolder1.Controls.Add(control);
// 设置其背景色
((MyUserControl)control).BackColor = Color.Yellow;
}
</PRE>
<P>以上代码实际上很简单,但却是一个等待粗心的开发人员掉进去的陷阱。您能找出其中的破绽吗? </P>
<P>如果您猜到该问题与输出缓存有关,那么您是正确的。正如您所看到的一样,上述代码示例编译和运行都正常,但是如果尝试将以下语句(完全合法)添加到 MyUserControl.ascx 中:</P><PRE>&lt;%@ OutputCache Duration="5" VaryByParam="None" %&gt;
</PRE>
<P>则当您下一次运行该页面时,您将看到 InvalidCastException (oh joy!) 和以下错误消息:</P><PRE>“无法将类型为‘System.Web.UI.PartialCachingControl’的对象转换为类型‘MyUserControl’。”
</PRE>
<P>因此,此代码在没有 OutputCache 指令时运行正常,但如果添加了 OutputCache 指令就会出错。ASP.NET 不应该以这种方式运行。页面(和控件)对于输出缓存应该是不可知的。那么,这代表什么意思?</P>
<P>问题在于为用户控件启用输出缓存时,LoadControl 不再返回对控件实例的引用;相反,它返回对 PartialCachingControl 实例的引用,而 PartialCachingControl 可能会也可能不会包装控件实例,具体取决于控件的输出是否被缓存。因此,如果开发人员调用 LoadControl 以动态加载用户控件并且为了访问控件特定的方法和属性而转换控件引用,他们必须注意进行该操作的方式,以便不管是否具有 OutputCache 指令,代码都可以运行。</P>
<P><A href=" http://msdn.microsoft.com/msdnmag/issues/06/07/WebAppFollies/default.aspx?fig=true#fig2"><U><FONT color=#0000ff>图 2</FONT></U></A> 说明动态加载用户控件以及转换返回的控件引用的正确方法。以下是其工作原理概要: </P>
<TABLE cellSpacing=0 cellPadding=0 border=0>
<TBODY>
<TR>
<TD vAlign=top>"</TD>
<TD>
<P>如果 ASCX 文件缺少 OutputCache 指令,则 LoadControl 返回一个 MyUserControl 引用。Page_Load 将该引用转换为 MyUserControl 并设置控件的 BackColor 属性。 </P></TD></TR>
<TR>
<TD vAlign=top>"</TD>
<TD>
<P>如果 ASCX 文件包括一个 OutputCache 指令并且控件的输出没有被缓存,则 LoadControl 返回一个对 PartialCachingControl 的引用,此 PartialCachingControl 的 CachedControl 属性包含对基础 MyUserControl 的引用。Page_Load 将 PartialCachingControl.CachedControl 转换为 MyUserControl 并设置该控件的 BackColor 属性。 </P></TD></TR>
<TR>
<TD vAlign=top>"</TD>
<TD>
<P>如果 ASCX 文件包括一个 OutputCache 指令并且控件的输出被缓存,则 LoadControl 返回一个对 PartialCachingControl(其 CachedControl 属性为空)的引用。注意,Page_Load 不再继续执行操作。无法设置控件的 BackColor 属性,因为该控件的输出来源于输出缓存。换句话说,根本没有要设置属性的 MyUserControl。</P></TD></TR></TBODY></TABLE>
<P>不管 .ascx 文件中是否具有 OutputCache 指令,<A href=" http://msdn.microsoft.com/msdnmag/issues/06/07/WebAppFollies/default.aspx?fig=true#fig2"><U><FONT color=#0000ff>图 2</FONT></U></A>中的代码都将运行。虽然看起来复杂一点,但它会避免烦人的错误。简单并不总是代表易于维护。</P>
<DIV><A href=" http://zycsharp.spaces.msn.com/mmm2006-07-07_16.32/#top"><IMG height=9 alt=返回页首 src=" http://zycsharp.spaces.msn.com/library/gallery/templates/MNP2.Common/images/arrow_px_up.gif" width=7 border=0></A><A href=" http://zycsharp.spaces.msn.com/mmm2006-07-07_16.32/#top"><U><FONT color=#0000ff>返回页首</FONT></U></A></DIV><A></A>
<H2>会话和输出缓存</H2>
<P>谈到输出缓存,ASP.NET 1.1 和 ASP.NET 2.0 都存在一个潜在的问题,该问题会影响在 Windows Server" 2003 和 IIS 6.0 上运行的服务器中的输出缓存页。我曾经亲眼看到该问题在 ASP.NET 生产服务器中出现过两次,这两次都是通过关闭输出缓冲来解决的。后来我了解到有一个比禁用输出缓存更好的解决方案。以下是我第一次遇到该问题时的情况。</P>
<P>当时的情况是这样的,某个网站(我们在此称为 Contoso.com,它在小型 ASP.NET Web 领域中运行公共电子商务应用程序)与我的团队联系,抱怨他们遇到了“跨线程”错误。使用 Contoso.com 网站的客户常常突然丢失已经输入的数据,但却看到另一用户的相关数据。稍做分析即发现,跨线程这个描述并不准确;“跨会话”错误更为贴切。看起来 Contoso.com 是在会话状态中存储数据的,由于某些原因,用户会偶尔随机地连接到其他用户的会话。</P>
<P>我的一个团队成员编写了一个诊断工具,用来将每个 HTTP 请求和响应的关键要素(包括 Cookie 标头)记录到日志中。然后,他将该工具安装在 Contoso.com 的 Web 服务器上,并让其运行了几天。结果非常明显。大概每 100000 个请求中会发生一次这样的情况:ASP.NET 正确地为全新会话分配一个会话 ID 并返回 Set-Cookie 标头中的会话 ID。然后,它会在下一个紧相邻的请求中返回相同的会话 ID(即,相同的 Set-Cookie 标头),即使该请求已经与一个有效的会话相关联并且正确提交了 Cookie 中的会话 ID。实际上,ASP.NET 是随机将用户从他们自己的会话中切换出去并将他们连接到其他会话。</P>
<P>我们很惊讶,于是开始寻找原因。我们首先检查了 Contoso.com 的源代码,让我们感到欣慰的是,问题不在那。接着,为了确保问题与应用程序宿主在 Web 领域无关,我们只保留一个服务器在运行,而关闭了所有其他服务器。问题仍然存在,这并不意外,因为我们的日志显示匹配的 Set-Cookie 标头绝不会来自两个不同的服务器。ASP.NET 意外地生成了重复的会话 ID,这令人难以置信,因为它使用 .NET Framework RNGCryptoServiceProvider 类生成这些 ID,并且会话 ID 的长度足以确保相同的 ID 决不会生成两次(至少在下一个万亿年内不会生成两次)。除此之外,即使 RNGCryptoServiceProvider 错误地生成了重复的随机数字,也无法解释 ASP.NET 为何不可思议地将有效的会话 ID 替换为新的 ID(不唯一)。</P>
<P>凭直觉,我们决定看一下输出缓存。当 OutputCacheModule 缓存 HTTP 响应时,它必须小心不要缓存了 Set-Cookie 标头;否则,包含新会话 ID 的缓存响应会将缓存响应的所有接收者(以及其请求生成了缓存响应的用户)连接到同一会话。我们检查了源代码;Contoso.com 在两个页面中启用了输出缓存。我们关闭了输出缓存。结果,应用程序运行数天而没有发生一个跨会话问题。此后,它运行了两年多都没有发生任何错误。在具有不同应用程序和一组不同 Web 服务器的另一家公司中,我们看到完全相同的问题也消失了。就像在 Contoso.com 一样,消除输出缓存就能解决问题。</P>
<P>Microsoft 后来确认此行为源于 OutputCacheModule 中的问题。(当您阅读本文时,可能已经发布了更新。)当 ASP.NET 与 IIS 6.0 一起使用并且启用内核模式缓存时,OutputCacheModule 有时无法从它传递给 Http.sys 的缓存响应中删除 Set-Cookie 标头。下面是导致出现错误的特定事件顺序: </P>
<TABLE cellSpacing=0 cellPadding=0 border=0>
<TBODY>
<TR>
<TD vAlign=top>"</TD>
<TD>
<P>最近没有访问网站(因此也没有对应的会话)的用户请求一个启用了输出缓存的页面,但是其输出当前在缓存中不可用。 </P></TD></TR>
<TR>
<TD vAlign=top>"</TD>
<TD>
<P>该请求执行用于访问用户最新创建的会话的代码,从而导致会话 ID Cookie 在响应的 Set-Cookie 标头中返回。 </P></TD></TR>
<TR>
<TD vAlign=top>"</TD>
<TD>
<P>OutputCacheModule 向 Http.sys 提供输出,但是无法从响应中删除 Set-Cookie 标头。 </P></TD></TR>
<TR>
<TD vAlign=top>"</TD>
<TD>
<P>Http.sys 在后续的请求中返回缓存响应,误将其他用户连接到会话。</P></TD></TR></TBODY></TABLE>
<P>故事的寓意又是什么呢?会话状态和内核模式输出缓存不能混合使用。如果您在启用输出缓存的页中使用会话状态,并且应用程序在 IIS 6.0 上运行,则您需要关闭内核模式输出缓存。您仍将受益于输出缓存,但是因为内核模式输出缓存比普通输出缓存快得多,所以缓存不会同样有效。有关此问题的详细信息,请参见 <A href=" http://support.microsoft.com/kb/917072"><U><FONT color=#0000ff>support.microsoft.com/kb/917072</FONT></U></A>。</P>
<P>您可以通过在页面的 OutputCache 指令中包含 VaryByParam="*" 属性来关闭单个页面的内核模式输出缓存,虽然这样做可能导致内存需求骤增。另一种更安全的方法是通过在 web.config 中包含下列元素来关闭整个应用程序的内核模式缓存:</P><PRE>&lt;httpRuntime enableKernelOutputCache="false" /&gt;
</PRE>
<P>您还可以使用注册表设置来全局性地禁用内核模式输出缓存,即禁用全部服务器的内核模式输出缓存。有关详细信息,请参见 <A href=" http://support.microsoft.com/kb/820129"><U><FONT color=#0000ff>support.microsoft.com/kb/820129</FONT></U></A>。 </P>
<P>每次我听到客户报告会话发生了费解的问题,我都会询问他们是否在任何页面中使用了输出缓存。如果确实使用了输出缓存,并且宿主操作系统是 Windows Server 2003,我会建议他们禁用内核模式输出缓存。问题通常就会迎刃而解。如果问题没有解决,则错误存在于代码中。警惕!</P>
<DIV><A href=" http://zycsharp.spaces.msn.com/mmm2006-07-07_16.32/#top"><IMG height=9 alt=返回页首 src=" http://zycsharp.spaces.msn.com/library/gallery/templates/MNP2.Common/images/arrow_px_up.gif" width=7 border=0></A><A href=" http://zycsharp.spaces.msn.com/mmm2006-07-07_16.32/#top"><U><FONT color=#0000ff>返回页首</FONT></U></A></DIV><A></A>
<H2>Forms 身份验证票证生存期</H2>
<P>您能找出以下代码的问题吗?</P><PRE>FormsAuthentication.RedirectFromLoginPage(username, true);
</PRE>
<P>此代码看似没有问题,但决不能在 ASP.NET 1.x 应用程序中使用,除非应用程序中其他位置的代码抵消了此语句的负面作用。如果您不能确定原因,请继续阅读。 </P>
<P>FormsAuthentication.RedirectFromLoginPage 执行两个任务。首先,当 FormsAuthenticationModule 将用户重定向到登录页时,FormsAuthentication.RedirectFromLoginPage 将用户重定向到他们原来请求的页面。其次,它发布一个身份验证票证(通常携带在 Cookie 中,而且在 ASP.NET 1.x 中总是携带在 Cookie 中),这个票证允许用户在预定的一段时间内保持已经过身份验证状态。</P>
<P>问题就在于这个时间段。在 ASP.NET 1.x 中,向 RedirectFromLoginPage 传递另一个为 false 的参数会发出一个临时身份验证票证,该票证默认情况下在 30 分钟之后到期。(您可以使用 web.config 的 元素中的 Timeout 属性来更改超时期限。)然而,传递另一个为 true 的参数则会发出一个永久身份验证票证,其有效期为 50 年!这样就会发生问题,因为如果有人窃取了该身份验证票证,他们就可以在票证的有效期内使用受害者的身份访问网站。窃取身份验证票证有多种方法 — 在公共无线访问点探测未加密的通信、跨网站编写脚本、以物理方式访问受害者的计算机等等 — 因此,向 RedirectFromLoginPage 传递 true 比禁用您的网站的安全性好不了多少。幸运的是,此问题已经在 ASP.NET 2.0 中得到了解决。现在的 RedirectFromLoginPage 以相同的方式接受在 web.config 中为临时和永久身份验证票证指定的超时。</P>
<P>一种解决方案是决不在 ASP.NET 1.x 应用程序的 RedirectFromLoginPage 的第二个参数中传递 true。但是这不切实际,因为登录页的特点通常是包含一个“将我保持为登录状态”框,用户可以选中该框以收到永久而不是临时身份验证 Cookie。另一种解决方案是使用 Global.asax(如果您愿意的话,也可以使用 HTTP 模块)中的代码段,此代码段会在包含永久身份验证票证的 Cookie 返回浏览器之前对其进行修改。</P>
<P><A href=" http://msdn.microsoft.com/msdnmag/issues/06/07/WebAppFollies/default.aspx?fig=true#fig3"><U><FONT color=#0000ff>图 3</FONT></U></A> 包含一个这样的代码段。如果此代码段位于 Global.asax 中,它会修改传出永久 Forms 身份验证 Cookie 的 Expires 属性,以使 Cookie 在 24 小时后过期。通过修改注释为“新的过期日期”的行,您可以将超时设置为您喜欢的任何日期。</P>
<P>您可能会觉得奇怪,Application_EndRequest 方法调用本地 Helper 方法 (GetCookieFromResponse) 来检查身份验证 Cookie 的传出响应。Helper 方法是解决 ASP.NET 1.1 中另一个错误的方法,如果您使用 HttpCookieCollection 的字符串索引生成器来检查不存在的 Cookie,此错误会导致虚假 Cookie 添加到响应中。使用整数索引生成器作为 GetCookieFromResponse 可以解决该问题。</P>
<DIV><A href=" http://zycsharp.spaces.msn.com/mmm2006-07-07_16.32/#top"><IMG height=9 alt=返回页首 src=" http://zycsharp.spaces.msn.com/library/gallery/templates/MNP2.Common/images/arrow_px_up.gif" width=7 border=0></A><A href=" http://zycsharp.spaces.msn.com/mmm2006-07-07_16.32/#top"><U><FONT color=#0000ff>返回页首</FONT></U></A></DIV><A></A>
<H2>视图状态:无声的性能杀手</H2>
<P>从某种意义上说,视图状态是有史以来最伟大的事情。毕竟,视图状态使得页面和控件能够在回发之间保持状态。因此,您不必像在传统的 ASP 中那样编写代码,以防止在单击按钮时文本框中的文本消失,或在回发后重新查询数据库和重新绑定 DataGrid。</P>
<P>但是视图状态也有缺点:当它增长得过大时,它便成为一个无声的性能杀手。某些控件(例如文本框)会根据视图状态作出相应判断。其他控件(特别是 DataGrid 和 GridView)则根据显示的信息量确定视图状态。如果 GridView 显示 200 或 300 行数据,我会望而生畏。即使 ASP.NET 2.0 视图状态大致是 ASP.NET 1 x 视图状态的一半大小,一个糟糕的 GridView 也可以容易地将浏览器和 Web 服务器之间的连接的有效带宽减少 50%或更多。</P>
<P>您可以通过将 EnableViewState 设置为 false 来关闭单个控件的视图状态,但某些控件(特别是 DataGrid)在不能使用视图状态时会失去某些功能。控制视图状态的更佳解决方案是将其保留在服务器上。在 ASP.NET 1.x 中,您可以重写页面的 LoadPageStateFromPersistenceMedium 和 SavePageStateToPersistenceMedium 方法并按您喜欢的方式处理视图状态。<A href=" http://msdn.microsoft.com/msdnmag/issues/06/07/WebAppFollies/default.aspx?fig=true#fig4"><U><FONT color=#0000ff>图 4</FONT></U></A> 中的代码显示的重写可防止视图状态保留在隐藏字段中,而将其保留在会话状态中。当与默认会话状态进程模型一起使用时(即,会话状态存储在内存中的 ASP.NET 辅助进程中时),在会话状态中存储视图状态尤其有效。相反,如果会话状态存储在数据库中,则只有测试才能显示在会话状态中保留视图状态会提高还是降低性能。</P>
<P>在 ASP.NET 2.0 中使用相同的方法,但是 ASP.NET 2.0 能够提供更简单的方法将视图状态保留在会话状态中。首先,定义一个自定义页适配器,其 GetStatePersister 方法返回 .NET Framework SessionPageStatePersister 类的一个实例:</P><PRE>public class SessionPageStateAdapter :
System.Web.UI.Adapters.PageAdapter
{
public override PageStatePersister GetStatePersister ()
    {
return new SessionPageStatePersister(this.Page);
    }
}
</PRE>
<P>然后,通过将 App.browsers 文件按以下方式放入应用程序的 App_Browsers 文件夹,将自定义页适配器注册为默认页适配器: </P><PRE>&lt;browsers&gt;
&lt;browser refID="Default"&gt;
&lt;controlAdapters&gt;
&lt;adapter controlType="System.Web.UI.Page"
adapterType="SessionPageStateAdapter" /&gt;
&lt;/controlAdapters&gt;
&lt;/browser&gt;
&lt;/browsers&gt;
</PRE>
<P>(您可以将文件命名为您喜欢的任何名称,只要它的扩展名为 .browsers 即可。)此后,ASP.NET 将加载页适配器并使用返回的 SessionPageStatePersister 以保留所有页面状态,包括视图状态。 </P>
<P>使用自定义页适配器的一个缺点是它全局性地作用于应用程序中的每一页。如果您更愿意将其中一些页面的视图状态保留在会话状态中而不保留其他页面的视图状态,请使用<A href=" http://msdn.microsoft.com/msdnmag/issues/06/07/WebAppFollies/default.aspx?fig=true#fig4"><U><FONT color=#0000ff>图 4</FONT></U></A> 中显示的方法。另外,如果用户在同一会话中创建多个浏览器窗口,您使用该方法可能会遇到问题。</P><A></A></DIV>
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值