美祺's profileEden of HexagonPhotosBlogListsMore Tools Help

Blog


    January 30

    关于自定义profileprovider中遇到的问题——若干解决方法

    在自定义的profileprovider中,几乎所有的相关资料都是提到在web.config中profile段中进行配置,如下:
     
    <authentication mode="Forms">
          <forms loginUrl="LoginTest.aspx" name="LIUBIANXINGAUTH" timeout="300" slidingExpiration="true"/>
        </authentication>
        <authorization>
          <deny users="?"/>
        </authorization>
        <anonymousIdentification enabled="true"/>
        <profile defaultProvider="SeaSkyNetLibProvider">
          <providers>
            <clear/>
            <add name="SeaSkyNetLibProvider" type="SeaSkyNetLib.Profile.SeaSkyNetLibProfileProvider" connectionStringName="SQLConnString1" applicationName="SeaSkyNetLib 1.0"/>
          </providers>
          <properties>
            <add name="ProfileDetail" type="SeaSkyNetLib.Model.ProfileDetailInfo" allowAnonymous="true" provider="SeaSkyNetLibProvider"/>
          </properties>
        </profile>
        <membership defaultProvider="SQLMembershipProvider">
          <providers>
            <add name="SQLMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="SQLMembershipConnString" applicationName="SeaSkyNetLib 1.0" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" passwordFormat="Hashed"/>
          </providers>
        </membership>
     
    可是往往在具体运行到根据反射提取SeaSkyNetLibProvider(我的自定义provider)的时候总是报错:
    provider配置节错误!
     
    很是让我头疼,不过想了一下,这个其实也是通过反射提取的,所以干脆自己在test.aspx中做了一个反射,看看到底系统能不能找到自定义的profileprovider.
     
    于是,在页面的后台历做了如下实验:
    ProfileDetailInfo profiledetail = new ProfileDetailInfo();
            profiledetail.AliaName = "jewel2";
           ProfileProvider tem = (ProfileProvider)Assembly.Load("SeaSkyNetLib.Profile").CreateInstance("SeaSkyNetLib.Profile.SeaSkyNetLibProfileProvider");//自定义的profileprovider
            ProfileDetailInfo tem2 = (ProfileDetailInfo)Assembly.Load("SeaSkyNetLib.Model").CreateInstance("SeaSkyNetLib.Model.ProfileDetailInfo");//自定义的profileproperty类
            Profile.ProfileDetail = profiledetail;

            Profile.Save();
     
    然后,让我至今无法理解的事情发生了,后台的系统又自己找到了这个类,依次推断,也许这个问题出在了aspnet编译系统的缓存里,一旦这里的页面生成通过,之后的web.config中配置的类也可以找到。
     
    大家以后碰到这种情况下,可以试着把WINNT\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files虾的文件晴空,如果再不行,那可以试试看以上的方法,让系统“记住”具体的程序集的引用路径。
     
     
    January 19

    The first hacking in my career

        The first book i read on IT is called "Kevin Mitnick", which is about the romaunt of Kevin Mitnick. Who's Kevin Mitnick? I also asked like this, but after I've read through and found he's the only one named after "The Greatest Hacker on Earth" by millions of hackers in the world of IT. Someday later when I've join this field- IT, it's true that he's such a talented and skilled man and worthy of the fame. Kevin Mitnick, maybe he'll become another originator of the software field if he hadn't chose the hacking.
     
        Why I metioned about this man? The reason is that I did experienced a process of so-called "hacking" work last Wed. Maybe it's of little value but important for me, since I've been written codes for years and familiar with those principles of application to some extent.
     
        Last Wed, My colleages and I booked a raft of books from a store in Peking on net, and after we submitted our list, they offered a kind of lottery to chose one from 6 presents on different price. Because the lottery program's a kind of flash, it's easy to press the right button of mouse and select the replay item in order that we can cirle this action till we get the present needed( most expensive one, mainly). It's a common way to replay most of flash files when u surfing the net with some webs  including flashes. However, after we've replayed it so many times and found there's no chance to get a better present-most are cheapest and second cheapest ones. All of my coleages and I raged, u know, a group of man waiting there, staring at the screen, drawing lottery time after time, showing no sign of leaving when it's about 30 mins after the duty-off time. So, these disappointing guys can't bear and decided to counter-attack.
     
        We first tried to surveillance the infomation in and out this page, and found a key value to represent the id of the kind of present we chose. But it's also hard for us because the id had been encrypted with MD5, one of the best encryption techs. But it's a good sign for us that if we can intercept and capture the encrypted id of the present we need we can go back home and wait the stuff posted later. Therefore, one of our colleage has a tool to dencrypt a flash file, so we saved that flash and succeeded to get the source code of this flash. God damn it! With the logic of this program, there's no chance to get the best one cause there's no conditional expression to trigger posting the id of the best lottery! We raged again, like flaming plastics( it's easy and fast to burn with plastics, u know). Thanks for the god, that negligent coder left the key encrypted id of the lottery we want in the source code and the lefting things become so easy. U know, we created a false post infomation with that id and sent it to the server, heihei. Finally, we'r delighted to see the invaluable pearl in the list and recieved it this Tuesday.
     
        Since it's not a favoring way to accomplish that goal, with a little troubles, we concluded from this evidence that is never put those important business functions into the client applications, especially like the one coded with script languages, flash mostly.
     
        Ok, time for bed again, gotta playing basketball tomorrow, good night:)
    January 17

    Just want to say sth, but it's time for bed, so...

        Couple of days, yeah, really not calm down and have the motive to write sth, and what i can do is to paste those copies of tech-articles i read into this space in order to keep as memorandum. Maybe it's time for me to say sth, hum?
     
    To be continued...
    January 11

    转载:ASP.NET 2.0 中的异步页功能应用 (二)

    续上篇 
     
    为验证这一机制的实现效果,我们可以在各个方法的入口处设置断点。因为 VS2005 的 IDE 屏蔽了底层 CLR 实现信息,我们需要在 Debug\Windows\Immediate Window 窗口中,输入 .load sos 命令加载 CLR 调试支持。具体的 sos 命令可以输入 !help 查询帮助,或参考我以前《用WinDbg探索CLR世界》的系列文章,这里不再罗嗦。

          在 AsyncPage 类型的 Page_Load、BeginAsyncOperation 和 EndAsyncOperation 方法中,分别输入 !ClrStack 命令可以获取当前线程的调用堆栈:

    !clrstack
    OS Thread Id: 0xb00 (2816)
    ESP       EIP     
    0361d9d4 05a9c817 AsyncPage.Page_Load(System.Object, System.EventArgs)
    ...
    0361df34 050dd403 System.Web.HttpRuntime.ProcessRequest(System.Web.HttpWorkerRequest)
    ...

    !clrstack
    OS Thread Id: 0xb00 (2816)
    ESP       EIP     
    0361dc04 05a9d819 AsyncPage.BeginAsyncOperation(System.Object, System.EventArgs, System.AsyncCallback, System.Object)
    ...
    0361df34 050dd403 System.Web.HttpRuntime.ProcessRequest(System.Web.HttpWorkerRequest)
    ...

    !clrstack
    OS Thread Id: 0xd30 (3376)
    ESP       EIP     
    04c5eee0 05fef30f AsyncPage.EndAsyncOperation(System.IAsyncResult)
    ...
    04c5f60c 05fe976c System.Net.Connection.ReadComplete(Int32, System.Net.WebExceptionStatus)
    ...


          可以看到 Page_Load 和 BeginAsyncOperation 方法都是在 ID 为 2816 的线程中被调用,其调用源也都是处理 HTTP 请求的 HttpRuntime.ProcessRequest 方法;而 EndAsyncOperation 则是在另外一个 ID 为 3376 的线程中调用,调用源也是完成网络读操作的 Connection.ReadComplete 方法。

          而从实现角度来看,AddOnPreRenderCompleteAsync 方法将异步页面处理的启动和停止方法,放到一个 Page.PageAsyncInfo 对象中。此对象维护了与页面相关的各种上下文信息,以及开始、停止和状态的数组。而 RegisterAsyncTask 方法也是类似,将 PageAsyncTask 实例放到 PageAsyncTaskManager 类型的管理器中。

     
     1class Page
     2{
     3    private Page.PageAsyncInfo _asyncInfo;
     4    private PageAsyncTaskManager _asyncTaskManager;
     5
     6  public void AddOnPreRenderCompleteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state)
     7  {
     8      // 处理参数和状态异常情况
     9      
    10      // 延迟构造异步页面信息
    11      if (_asyncInfo == null)      
    12        _asyncInfo = new Page.PageAsyncInfo(this);  
    13      
    14      _asyncInfo.AddHandler(beginHandler, endHandler, state);
    15    }

    16    
    17    public void RegisterAsyncTask(PageAsyncTask task)
    18    {
    19      // 处理参数和状态异常情况
    20      
    21      // 延迟构造异步任务管理器
    22    if (this._asyncTaskManager == null)
    23      _asyncTaskManager = new PageAsyncTaskManager(this);
    24     
    25    _asyncTaskManager.AddTask(task);
    26    }

    27}


          HttpApplication 在处理页面请求时,通过其 pipeline 的 CallHandlerExecutionStep 步骤,调用页面的 BeginProcessRequest 方法,其伪代码如下:
     1void HttpApplication.IExecutionStep.Execute()
     2{
     3  // 从上下文中获取获取当前页面的处理器
     4  HttpContext context = _application.Context;
     5  IHttpHandler handler = context.Handler;
     6    
     7  if (handler == null)
     8  {
     9    _sync = true;
    10  }

    11  else if (handler is IHttpAsyncHandler)
    12  {
    13    // 如果是异步处理器,则调用异步处理开始方法
    14        IHttpAsyncHandler asyncHandler = (IHttpAsyncHandler) handler1;
    15    
    16    _sync = false;
    17    _handler = asyncHandler;
    18    
    19    IAsyncResult result = asyncHandler.BeginProcessRequest(context, _completionCallback, null);
    20            
    21        // 如果的确是异步操作,就直接返回            
    22        if (!result.CompletedSynchronously)
    23            return;
    24      
    25    // 否则恢复同步的页面处理流程
    26    _sync = true;
    27    _handler = null;
    28    asyncHandler.EndProcessRequest(result);
    29  }

    30  else
    31  {
    32      // 采用同步模式处理页面
    33    _sync = true;
    34    
    35    _application.SyncContext.SetSyncCaller();
    36    try
    37    {
    38      handler.ProcessRequest(context);
    39    }

    40    finally
    41    {
    42      _application.SyncContext.ResetSyncCaller();
    43    }

    44  }

    45}


          而 ASP.NET 页面一旦通过 Page 标记定义为异步模式,其编译生成的 Page 子类就会实现 IHttpAsyncHandler 接口。
          例如对上述例子,我们可以通过 !ClrStack 命令看到页面被编译为名称为 ASP.asyncpage_aspx 的类型。

    !clrstack
    OS Thread Id: 0xb00 (2816)
    ESP       EIP     
    0361d9d4 05a9c817 AsyncPage.Page_Load(System.Object, System.EventArgs)
    ...
    0361dd5c 0545b90e System.Web.UI.Page.AsyncPageBeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object)
    0361dd98 0545b813 ASP.asyncpage_aspx.BeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object)
    0361ddec 0545b6b6 System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
    0361de28 05400c18 System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
    ...


          进一步使用 !DumpDomain 命令可以找到其页面编译的临时文件,如

    !DumpDomain 

    ...

    Assembly: 0023ade0 [D:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\asyncpage\ba0a3865\3cd7d92\App_Web_n97gem4v.dll]
    ClassLoader: 0023abc0
    SecurityDescriptor: 00229ac0
      Module Name
    05a502cc D:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\asyncpage\ba0a3865\3cd7d92\App_Web_n97gem4v.dll

    ...


          使用 IL 反汇编根据打开此文件,可以看到 AsyncPage.aspx 被编译为 asyncpage_aspx 类型,如下所示:
     1public class asyncpage_aspx : AsyncPage, IHttpAsyncHandler, IHttpHandler
     2{
     3    public virtual IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object data)
     4    {
     5    return base.AsyncPageBeginProcessRequest(context, cb, data);
     6    }

     7 
     8    public virtual void EndProcessRequest(IAsyncResult ar)
     9    {    
    10    base.AsyncPageEndProcessRequest(ar);
    11    }

    12}

    13
    14public interface IHttpAsyncHandler : IHttpHandler
    15{
    16  // Methods
    17  IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
    18  void EndProcessRequest(IAsyncResult result);
    19}


          其中 AsyncPage 类型是后台实现代码编译生成的类型,asyncpage_aspx 则是 .aspx 页面编译生成。

          而在 Page.AsyncPageBeginProcessRequest 方法中,将首先处理上下文环境初始化,初始化异步执行信息,以及相应回调函数的执行;然后会调用 PageAsyncTaskManager.RegisterHandlersForPagePreRenderCompleteAsync 将异步任务管理器中所有的异步任务,封装后注册到 Page.PageAsyncInfo 对象中维护的异步调用信息中;最后调用      其 CallHandlers 方法完成对异步处理开始方法的调用。完整的伪代码如下:
     1class Page
     2{
     3  protected IAsyncResult AsyncPageBeginProcessRequest(HttpContext context, AsyncCallback callback, object extraData)
     4    {
     5        // 处理上下文环境初始化
     6        
     7        // 初始化异步执行信息
     8        _asyncInfo.AsyncResult = new HttpAsyncResult(callback, extraData);
     9    _asyncInfo.CallerIsBlocking = callback == null;
    10
    11        // 执行相应回调函数
    12        
    13        // 注册异步任务
    14        if ((_asyncTaskManager != null&& !_asyncInfo.CallerIsBlocking)    
    15      _asyncTaskManager.RegisterHandlersForPagePreRenderCompleteAsync();
    16    
    17    // 调用所有的异步处理开始方法
    18        _asyncInfo.CallHandlers(true);
    19    
    20        return _asyncInfo.AsyncResult;    
    21    }

    22}


          而在 PageAsyncTaskManager 中被管理的异步任务,会作为一个异步执行信息注册到 PageAsyncInfo 中去。并在其被调用时,实际调用 PageAsyncTaskManager 类型的 ExecuteTasks 方法,实现较为复杂的异步调用逻辑。
     1internal class PageAsyncTaskManager
     2{
     3    internal void RegisterHandlersForPagePreRenderCompleteAsync()
     4    {
     5    _page.AddOnPreRenderCompleteAsync(new BeginEventHandler(this.BeginExecuteAsyncTasks), new EndEventHandler(this.EndExecuteAsyncTasks));
     6    }

     7    
     8    private IAsyncResult BeginExecuteAsyncTasks(object sender, EventArgs e, AsyncCallback cb, object extraData)
     9    {
    10    return ExecuteTasks(cb, extraData);
    11    }

    12    
    13    private void EndExecuteAsyncTasks(IAsyncResult ar)
    14    {
    15    _asyncResult.End();
    16    }

    17}



          以上我们对异步页面的目的、范围、使用方式和实现原理等,有了一个大致的了解。并针对异步任务的管理做了简要的分析,基本上已经能弄清异步页面的静态运行机制如何。下一节我们将从动态执行的角度,对两级异步任务,以及相应的调度和线程使用做进一步探索。

    转载:ASP.NET 2.0 中的异步页功能应用(一)

    与 ASP.NET 1.0 相比,ASP.NET 2.0 的各方面改进可以说是非常巨大的。但就其实现层面来说,最大的增强莫过于提供了对异步页面的支持。通过此机制,编写良好的页面可以将数据库、WebService 调用等慢速操作,对网站吞吐能力的影响降到最低,并极大的改善网站的平均页面响应速度。本文将从使用和实现两个层面,简单的剖析这一强大机制的原理,以便读者能够更好的应用这一机制。
          对一个网页请求的生命周期来说,首先是 Web 服务器收到客户端 HTTP 请求,将请求转交给 ASP.NET 引擎;引擎将以流水线方式调用合适的 Web 应用程序和最终的页面进行处理;页面会根据请求内容,执行某些后台操作,如访问数据库、调用远程 WebService 等等;最终将结果以某种可视化形式,展示到最终用户的浏览器中。
          而为了提高响应速度和吞吐量,现代的 Web 服务器往往会将 Web 应用程序和页面,放在一个缓冲池中备用,避免每次处理请求时重建环境。而请求到来时,Web 服务器会从一个系统线程池中获取临时线程,调用从 Web 应用程序和页面缓冲池中获取的处理实例,完成对请求的处理,并最终返回处理的结果。
          咋一看这种机制非常完美,能够最大限度的重用系统资源,但实际上其中存在着很大的优化余地。

          我们可以将一个页面请求的处理过程进一步细化为下面步骤:

          1.Web 服务器接受请求并由引擎转发请求
          2.页面处理请求,访问数据库、调用远程 WebService 等
          3.页面将处理结果,以某种形式展现,如 HTML 表格等
          4.Web 服务器将结果返回给最终客户的浏览器

          其中第一步涉及到核心态网络驱动,需要频繁切换回用户态,以将请求转交给处理引擎。这里涉及到大量的核心态和用户态切换。为减少这个负担,IIS 6 开始提供了 http.sys 在核心态直接对大多数请求进行处理。这是 MS 在中间件一级就已经替我们做好的优化,我们无需也无法关心。

          而另外一个潜在的优化点就是异步页面的目标:增强页面处理请求的并发性。
          Web 服务器在从线程池获取临时线程后,在线程中调用页面相关代码处理请求。而这里的请求处理过程往往涉及到较为缓慢的操作,例如访问数据库、调用远程 WebService 等。如果数据库是在本机的话还好,系统 CPU 时间只是从处理线程移交到后台数据库线程;而一旦处理运算逻辑在远程,例如访问外部独立数据库,或调用 WebService 完成某种操作,此时此线程就只能无谓的等待操作结束。而作为 Web 服务器处理客户端请求的线程池,其最大容纳线程的数量肯定是有限的。(虽然大多数情况下这个上限值可以修改,例如 ASP.NET 中可以通过修改 machine.config 的 processModel 标签调整最大数量,缺省25)。一旦超过此数量的请求正在并行执行,或者说正在等待后台慢速的操作,此时新来的请求就会因为处理请求线程池中无可用线程,出现虽然 CPU 负荷非常低,但仍出现 “503 服务器不可用” 类似的错误,从而事实上造成对应用的 DoS(拒绝服务)攻击。即使此上限设置很大,也会因为大量等待操作,降低其它本可以快速的页面的响应速度。

          要处理这种情况,虽然可以通过继续增大请求处理线程池最大容量缓解,但总是治标不治本。更好的方法就是将请求处理和页面处理分离,避免慢速页面处理占用快速请求处理的时间。页面在接受到引擎的处理页面请求后,通过调用异步方法来试图完成实际页面处理,处理结果从单独线程池获取线程进行监控,而发送页面请求的请求处理线程将被直接释放,以便继续处理其它的页面请求。这也就是 ASP.NET 异步页面的基本思路。实际上这个思路在 ASP.NET 1.1 时就已经提出,Fritz Onion 曾于 2003 年在 MSDN 杂志发表过一篇文章详细讨论这个问题,并给出了一个简单的解决方案。

          Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code

          文中提供的实现,很好的对此问题进行原理上的验证。但从实现角度较为繁琐,需要自行处理 IAsyncResult 接口以及自定义线程池,而且缺少对 HTTP 上下文以及超时等的处理。

          好在 ASP.NET 2.0 对此问题提供了内建的支持,Jeff Prosise 在 MSDN 杂志的文章中详细的讨论了其实现思路和使用方法。

          Asynchronous Pages in ASP.NET 2.0

          从使用角度来说,异步页面的支持非常透明。使用者只需要在页面定义的 Page 标签中指定异步模式,例如:
    <%@ Page Async="true" ... %>


          然后就可以在 Page 的实现代码中,通过 Page 类型的 AddOnPreRenderCompleteAsync 或 PageAsyncTask 方法,提交异步的页面处理代码。ASP.NET 引擎会根据页面的异步模式设定,调用合适的页面处理开始和结束方法。

          对大多数简单的异步处理情况,可以直接调用 AddOnPreRenderCompleteAsync 方法,提交页面请求开始和结束时的处理代码,例如上述文章中给出的一个内部处理 HTTP 页面请求的例子:
     1// AsyncPage.aspx.cs
     2using System;
     3using System.Web;
     4using System.Web.UI;
     5using System.Web.UI.WebControls;
     6using System.Net;
     7using System.IO;
     8using System.Text;
     9using System.Text.RegularExpressions;
    10
    11public partial class AsyncPage : System.Web.UI.Page
    12{
    13    private WebRequest _request;
    14
    15    void Page_Load (object sender, EventArgs e)
    16    {
    17        AddOnPreRenderCompleteAsync (
    18            new BeginEventHandler(BeginAsyncOperation),
    19            new EndEventHandler (EndAsyncOperation)
    20        );
    21    }

    22
    23    IAsyncResult BeginAsyncOperation (object sender, EventArgs e, 
    24        AsyncCallback cb, object state)
    25    {
    26        _request = WebRequest.Create("http://msdn.microsoft.com");
    27        return _request.BeginGetResponse (cb, state);
    28    }

    29    void EndAsyncOperation (IAsyncResult ar)
    30    {
    31        string text;
    32        using (WebResponse response = _request.EndGetResponse(ar))
    33        {
    34            using (StreamReader reader = 
    35                new StreamReader(response.GetResponseStream()))
    36            {
    37                text = reader.ReadToEnd();
    38            }

    39        }

    40
    41        Regex regex = new Regex ("href\\s*=\\s*\"([^\"]*)\""
    42            RegexOptions.IgnoreCase);
    43        MatchCollection matches = regex.Matches(text);
    44
    45        StringBuilder builder = new StringBuilder(1024);
    46        foreach (Match match in matches)
    47        {
    48            builder.Append (match.Groups[1]);
    49            builder.Append("<br/>");
    50        }

    51
    52        Output.Text = builder.ToString ();
    53    }

    54}


          AsyncPage 页面的 OnLoad 事件中提交异步处理方法;ASP.NET 引擎会在页面加载完成后,调用 BeginAsyncOperation 方法启动异步方法。这里的异步请求是打开一个远程 Web 页面,而大多数诸如数据库、WebService 调用等等,都提供了类似的异步调用版本。页面处理开始方法会返回异步调用请求的 IAsyncResult 封装,通过此接口检测处理的完成情况。而在 BeginAsyncOperation 方法返回之后,处理连接请求的线程将回到线程池,用来处理后续的连接请求。直到实际的异步处理操作完成,例如 Web 页面被取回,引擎才会从独立线程池中获取临时线程,调用 EndAsyncOperation 方法完成后续的操作。
          其实际的处理流程如下图所示:

     

          而 PageAsyncTask 的方式则是增强版本,除了异步页面处理开始和结束方法自身外,还可以提供在超时情况下的处理方法,以及处理时的状态对象。上述文章中给出的对应例子如下:
     1// AsyncPageTask.aspx.cs
     2using System;
     3using System.Web;
     4using System.Web.UI;
     5using System.Web.UI.WebControls;
     6using System.Net;
     7using System.IO;
     8using System.Text;
     9using System.Text.RegularExpressions;
    10
    11public partial class AsyncPageTask : System.Web.UI.Page
    12{
    13    private WebRequest _request;
    14
    15    protected void Page_Load(object sender, EventArgs e)
    16    {
    17        PageAsyncTask task = new PageAsyncTask(
    18            new BeginEventHandler(BeginAsyncOperation),
    19            new EndEventHandler(EndAsyncOperation),
    20            new EndEventHandler(TimeoutAsyncOperation),
    21            null
    22        );
    23        RegisterAsyncTask(task);
    24    }

    25
    26    IAsyncResult BeginAsyncOperation(object sender, EventArgs e, 
    27        AsyncCallback cb, object state)
    28    {
    29        _request = WebRequest.Create("http://msdn.microsoft.com");
    30        return _request.BeginGetResponse(cb, state);
    31    }

    32
    33    void EndAsyncOperation(IAsyncResult ar)
    34    {
    35        string text;
    36        using (WebResponse response = _request.EndGetResponse(ar))
    37        {
    38            using (StreamReader reader = 
    39                new StreamReader(response.GetResponseStream()))
    40            {
    41                text = reader.ReadToEnd();
    42            }

    43        }

    44
    45        Regex regex = new Regex("href\\s*=\\s*\"([^\"]*)\""
    46            RegexOptions.IgnoreCase);
    47        MatchCollection matches = regex.Matches(text);
    48
    49        StringBuilder builder = new StringBuilder(1024);
    50        foreach (Match match in matches)
    51        {
    52            builder.Append(match.Groups[1]);
    53            builder.Append("<br/>");
    54        }

    55
    56        Output.Text = builder.ToString();
    57    }

    58
    59    void TimeoutAsyncOperation(IAsyncResult ar)
    60    {
    61        Output.Text = "Data temporarily unavailable";
    62    }

    63}



         

    January 10

    转载:通过避免下列 10 个常见 ASP.NET 缺陷使网站平稳运行

    这两天着实在这个问题上好好的反思了一下,觉得个中的建议挺有深度的,也许再应该做一些实验比较下优劣,或许作者的用意就能更深的表达出来了:)

     

    通过避免下列 10 个常见 ASP.NET 缺陷使网站平稳运行

    发布日期: 2006-07-11 | 更新日期: 2006-07-11

    Jeff Prosise

    本文将讨论:

    缓存和 Forms 身份验证

    视图状态和会话状态

    配置文件属性序列化

    线程池饱和

    模拟和设置配置文件

    本文使用了下列技术:

    .NET Framework、ASP.NET、Windows Server 2003

    *
    本页内容
    LoadControl 和输出缓存 LoadControl 和输出缓存
    会话和输出缓存 会话和输出缓存
    Forms 身份验证票证生存期 Forms 身份验证票证生存期
    视图状态:无声的性能杀手 视图状态:无声的性能杀手
    SQL Server 会话状态:另一个性能杀手 SQL Server 会话状态:另一个性能杀手
    未缓存的角色 未缓存的角色
    配置文件属性序列化 配置文件属性序列化
    线程池饱和 线程池饱和
    模拟和 ACL 授权 模拟和 ACL 授权
    不要完全信赖它 — 请设置数据库的配置文件! 不要完全信赖它 — 请设置数据库的配置文件!
    结论 结论

    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 中显示的方法。另外,如果用户在同一会话中创建多个浏览器窗口,您使用该方法可能会遇到问题。

    SQL Server 会话状态:另一个性能杀手

    ASP.NET 使得在数据库中存储会话状态变得简单:只需切换 web.config 中的开关,会话状态就会轻松地移动到后端数据库。对于在 Web 领域中运行的应用程序来说,这是一项重要功能,因为它允许该领域中的每个服务器共享会话状态的一个公共库。添加的数据库活动降低了单个请求的性能,但是可伸缩性的提高弥补了性能的损失。

    这看起来都还不错,但是您略微考虑一下下列几点,情况就会有所不同:

    即使在使用会话状态的应用程序中,大多数页也不使用会话状态。

    默认情况下,ASP.NET 会话状态管理器对每个请求中的会话数据存储执行两个访问(一个读取访问和一个写入访问),而不管请求的页是否使用会话状态。

    换句话说,当您使用 SQL Server™ 会话状态选项时,您在每个请求中都要付出代价(两个数据库访问)— 甚至在与会话状态无关的页面的请求中。这会直接对整个网站的吞吐量造成负面影响。

    a

    图 5 消除不必要的会话状态数据库访问

    那么您应该怎么办呢?很简单:禁用不使用会话状态的页中的会话状态。这样做总是一个好办法,但是当会话状态存储在数据库中时,该方法尤其重要。图 5 显示如何禁用会话状态。如果页面根本不使用会话状态,请在其 Page 指令中包含 EnableSessionState="false",如下所示:

    <%@ Page EnableSessionState="false" ... %>
    

    该指令阻止会话状态管理器在每个请求中读取和写入会话状态数据库。如果页面从会话状态中读取数据,但却不写入数据(即,不修改用户会话的内容),则将 EnableSessionState 设置为 ReadOnly,如下所示:

    <%@ Page EnableSessionState="ReadOnly" ... %>
    

    最后,如果页面需要对会话状态进行读/写访问,则省略 EnableSessionState 属性或将其设置为 true:

    <%@ Page EnableSessionState="true" ... %>
    

    通过以这种方式控制会话状态,可以确保 ASP.NET 只在真正需要时才访问会话状态数据库。消除不必要的数据库访问是构建高性能应用程序的第一步。

    顺便说一下,EnableSessionState 属性是公开的。该属性自 ASP.NET 1.0 以来就已经进行了说明,但是我至今仍很少见到开发人员利用该属性。也许是因为它对于内存中的默认会话状态模型并不十分重要。但是它对于 SQL Server 模型却很重要。

    未缓存的角色

    以下语句经常出现于 ASP.NET 2.0 应用程序的 web.config 文件以及介绍 ASP.NET 2.0 角色管理器的示例中:

    <roleManager enabled="true" />
    

    但正如以上所示,该语句确实会对性能产生明显的负面影响。您知道为什么吗?

    默认情况下,ASP.NET 2.0 角色管理器不会缓存角色数据。相反,它会在每次需要确定用户属于哪个角色(如果有)时参考角色数据存储。这意味着一旦用户经过了身份验证,任何利用角色数据的页(例如,使用启用了安全裁减设置的网站图的页,以及使用 web.config 中基于角色的 URL 指令进行访问受到限制的页)将导致角色管理器查询角色数据存储。如果角色存储在数据库中,那么对于每个请求需要访问多个数据库的情况,您可以轻松地免除访问多个数据库。解决方案是配置角色管理器以在 Cookie 中缓存角色数据:

    <roleManager enabled="true" cacheRolesInCookie="true" />
    

    您可以使用其他<roleManager> 属性控制角色 Cookie 的特征 — 例如,Cookie 应保持有效的期限(以及角色管理器因此返回角色数据库的频率)。角色 Cookie 默认情况下是经过签名和加密的,因此安全风险虽然不为零,但也有所缓解。

    配置文件属性序列化

    ASP.NET 2.0 配置文件服务为保持每个用户的状态(例如个性化首选项和语言首选项)的问题提供了一个现成的解决方案。要使用配置文件服务,您可以定义一个 XML 配置文件,其中包含要保留的代表单个用户的属性。然后,ASP.NET 编译一个包含相同属性的类,并通过添加到页的配置文件属性提供对类实例的强类型访问。

    配置文件灵活性很强,它甚至允许将自定义数据类型用作配置文件属性。但是,其中却存在一个问题,我亲眼看到该问题导致开发人员出差错。图 6 包含一个名为 Posts 的简单类,以及将 Posts 用作配置文件属性的配置文件定义。但是,该类和该配置文件在运行时会产生意外的行为。您能找出其中的原因吗?

    问题在于 Posts 包含一个名为 _count 的私有字段,该字段必须进行序列化和反序列化,才能完全冻结和重新冻结类实例。但是 _count 却没有经过序列化和反序列化,因为它是私有的,而且默认情况下 ASP.NET 配置文件管理器使用 XML 序列化对自定义类型进行序列化和反序列化。XML 序列化程序将忽略非公共成员。因此,会对 Posts 的实例进行序列化和反序列化,但是每次反序列化类实例时,_count 都会重设为 0。

    一种解决方案是使 _count 成为公共字段而非私有字段。另一种解决方案是使用公共读/写属性封装 _count。最佳解决方案是将 Posts 标记为可序列化(使用 SerializableAttribute),并将配置文件管理器配置为使用 .NET Framework 二进制序列化程序对类实例进行序列化和反序列化。该解决方案能够保持类本身的设计。与 XML 序列化程序不同的是,二进制序列化程序序列化字段,而不管是否可以访问。图 7 显示 Posts 类的修复版本并突出显示了更改的附带配置文件定义。

    您应该牢记的一点是,如果您使用自定义数据类型作为配置文件属性,并且该数据类型具有必须序列化才能完全序列化类型实例的非公共数据成员,则在属性声明中使用 serializeAs="Binary" 属性并确保类型本身是可序列化的。否则,将无法进行完整的序列化,并且您还将浪费时间来尝试确定配置文件无法工作的原因。

    线程池饱和

    在执行数据库查询并等待 15 秒或更长时间来获得返回的查询结果时,我经常对看到的实际的 ASP.NET 页数感到非常惊讶。(我也等待了 15 分钟才看到查询结果!)有时,延迟是由于返回的数据量很大而导致的不可避免的无奈结果;而有时,延迟则是由于数据库的设计不佳导致的。但不管是什么原因,长时间的数据库查询或任何类型的长时间 I/O 操作在 ASP.NET 应用程序中都会导致吞吐量的下降。

    关于这个问题我以前已经详细地描述过,所以在此就不再作过多的说明了。我只说一点就够了,ASP.NET 依赖于有限的线程池处理请求,如果所有线程都被占用来等待数据库查询、Web 服务调用或其他 I/O 操作完成,则在某个操作完成并且释放出一个线程之前,其他请求都必须排队等待。当请求排队时,性能会急剧下降。如果队列已满,则 ASP.NET 会使随后的请求失败并出现 HTTP 503 错误。这种情况不是我们希望在 Web 生产服务器的生产应用程序上所乐见的。

    解决方案非异步页面莫属,这是 ASP.NET 2.0 中最佳却鲜为人知的功能之一。对异步页面的请求从一个线程上开始,但是当它开始一个 I/O 操作时,它将返回该线程以及 ASP.NET 的 IAsyncResult 接口。操作完成后,请求通过 IAsyncResult 通知 ASP.NET,ASP.NET 从池中提取另一个线程并完成对请求的处理。值得注意的是,当 I/O 操作发生时,没有占用线程池线程。这样可以通过阻止其他页面(不执行较长的 I/O 操作的页面)的请求在队列中等待,从而显著地提高吞吐量。

    您可以在 MSDN®Magazine 的 2005 年 10 月刊中阅读有关异步页面的所有信息。I/O 绑定而不是计算机绑定且需要很长时间执行的任何页面很有可能成为异步页面。

    当我将关于异步页面的信息告知开发人员时,他们经常回答“那真是太棒了,但是我的应用程序中并不需要它们。”对此我回答说:“你们的任何页面需要查询数据库吗?它们调用 Web 服务吗?您是否已经检查 ASP.NET 性能计数器中关于排队请求和平均等待时间的统计信息?即使您的应用程序至今运行正常,但是随着您的客户规模的增长,应用程序的负载可能会增加。”

    实际上,绝大多数实际的 ASP.NET 应用程序都需要异步页面。请切记这一点!

    模拟和 ACL 授权

    以下是一个简单的配置指令,但是每当在 web.config 中看到它时都让我眼前一亮:

    <identity impersonate="true" />
    

    此指令在 ASP.NET 应用程序中启用客户端模拟。它将代表客户端的访问令牌附加到处理请求的线程,以便操作系统执行的安全性检查针对的是客户端身份而不是辅助进程身份。ASP.NET 应用程序很少需要模拟;我的经验告诉我,开发人员通常都是由于错误的原因而启用模拟的。以下是原因所在。

    开发人员经常在 ASP.NET 应用程序中启用模拟,以便可以使用文件系统权限来限制对页面的访问。如果 Bob 没有查看 Salaries.aspx 的权限,则开发人员将会启用模拟,以便可以通过将访问控制列表 (ACL) 设置为拒绝 Bob 的读取权限,阻止 Bob 查看 Salaries.aspx。但是存在以下隐患:对于 ACL 授权来说,模拟是不必要的。在 ASP.NET 应用程序中启用 Windows 身份验证时,ASP.NET 会自动为请求的每个 .aspx 页面检查 ACL 并拒绝没有读取文件权限的调用者的请求。即使禁用了模拟,它仍会这样操作。

    有的时候需要证明模拟的合理性。但是您通常可以用良好的设计来避免它。例如,假定 Salaries.aspx 在数据库中查询只有管理人员才能知道的工资信息。通过模拟,您可以使用数据库权限拒绝非管理人员查询工资数据的能力。或者您可以不考虑模拟,并且通过为 Salaries.aspx 设置 ACL 以使非管理人员不具有读取权限,从而限制对工资数据的访问。后一种方法提供的性能更佳,因为它完全避免了模拟。它也消除了不必要的数据库访问。为什么查询数据库仅由于安全原因被拒绝?

    顺便说一下,我曾经帮助对一个传统的 ASP 应用程序进行故障排除,该应用程序由于内存占用不受限制而定期重新启动。一个没有经验的开发人员将目标 SELECT 语句转换成了 SELECT *,而没有考虑要查询的表包含图像,这些图像很大而且数目很多。问题由于未检测到内存泄漏而恶化。(我的托管代码领域!)多年来运行正常的应用程序开始突然停止工作,因为以前返回一两千字节数据的 SELECT 语句现在却返回了几兆字节。如果再加上不充分的版本控制,开发团队的生活将不得不“亢奋起来”— 这里所谓的“亢奋”,就如同当您在晚上要睡觉时,还不得不看着您的孩子玩令人厌烦的足球游戏一样。

    理论上,传统的内存泄漏不会发生在完全由托管代码组成的 ASP.NET 应用程序中。但是内存使用量不足会通过强制垃圾收集更频繁地发生而影响性能。即使是在 ASP.NET 应用程序中,也要警惕 SELECT *!

    不要完全信赖它 — 请设置数据库的配置文件!

    作为一名顾问,我经常被询问为何应用程序没有按预期执行。最近,有人询问我的团队为何 ASP.NET 应用程序只完成请求文档所需吞吐量(每秒的请求数)的大约 1/100。我们以前所发现的问题是我们在不能正常运行的 Web 应用程序中发现的问题特有的 — 和我们所有人应该认真对待的教训。

    我们运行 SQL Server Profiler 并监视此应用程序和后端的数据库之间的交互情况。在一个更极端的案例中,仅仅只是一个按钮单击,就导致数据库发生了 1,500 多个错误。您不能那样构建高性能的应用程序。良好的体系结构总是从良好的数据库设计开始。不管您的代码的效率有多高,如果它被编写不佳的数据库所拖累,就会不起作用。

    糟糕的数据访问体系结构通常源于下面的一个或多个方面:

    拙劣的数据库设计(通常由开发人员设计,而不是数据库管理员)。

    DataSets 和 DataAdapters 的使用 — 尤其是 DataAdapter.Update,它适用于 Windows 窗体应用程序和其他胖客户端,但是对于 Web 应用程序来说通常不理想。

    具有拙劣编制计算程序、以及执行相对简单的操作需消耗很多 CPU 周期的设计糟糕的数据访问层 (DAL)。

    必须先确定问题才能对其进行处理。确定数据访问问题的方式是运行 SQL Server Profiler 或等效的工具以查看后台正在执行的操作。检查应用程序和数据库之间的通信之后,性能调整才完成。尝试一下 — 您可能会对您的发现大吃一惊。

    结论

    现在您已经了解在生成 ASP.NET 生产应用程序过程中可能遇到的一些问题及其解决方案了。下一步是仔细查看您自己的代码并尝试避免我在此概述的一些问题。ASP.NET 可能降低了 Web 开发人员的门槛,但是您的应用程序完全有理由灵活、稳定和高效。请认真考虑,避免出现新手易犯的错误。

    图 8 提供了一个简短检查列表,您可以使用它来避免本文中描述的缺陷。您可以创建一个类似的安全缺陷检查列表。例如:

    您是否已经对包含敏感数据的配置节进行加密?

    您是否正在检查并验证在数据库操作中使用的输入,是否使用了 HTML编码输入作为输出?

    您的虚拟目录中是否包含具有不受保护的扩展名的文件?

    如果您重视网站、承载网站的服务器以及它们所依赖的后端资源的完整性,则这些问题非常重要。

    Jeff Prosise 是对 MSDN Magazine 贡献很大的编辑以及多本书籍的作者,这些书籍中包括 Programming Microsoft .NET (Microsoft Press, 2002)。他也是软件咨询和教育公司 Wintellect 的共同创始人。

    January 09

    转载+自己的感觉:使用SqlServer模式的会话状态管理

        SqlServer模式的会话状态管理其实是把Session里的值,今天也体验了一下,不过而而,为什么?这种极度消耗带宽,数据库资源的方式应该是不怎么推荐的,不过却也有它存在的必要。如果重要的用户Session一旦丢失,这个损失让谁来负责呢?呵呵,过去觉得瞬间即逝的东西没有多少保存的价值,但是也并非所有吧:)Ok,下面开始介绍下SqlServer的会话模式。
     
        在Web应用程序中,都会有一个Web.config文件来配置当前Web项目。其中包括关于会话状态Session的配置。下面来详细说明:
    <sessionState
    timeout="timeout in minutes"
    cookieless="[true|false]"
    mode="Off|InProc|StateServer|SQLServer"
    stateConnectionString="tcpip=server:port"
    stateNetworkTimeout="for network operations with State Server,in seconds"
    sqlConnectionString="valid SqlConnection string,minus Initial Catalog"
    />
    timeout:指定了活动结束后会话的生存期(以分钟计算)。如果用户在一段指定的时间内没有被激活,就会有一个新的会话被创建,而先前的状态将全部丢失。
    cookieless:在默认状态下,所生成的会话ID被存储在一个cookie中,稍后,这个cookie会在其他请求中被ASP.NET读取,以便对会话状态进行判断,从而连接到当前用户。
    如果有些用户禁用了浏览器中的cookie,我们就可以通过cookieless为这些用户起用会话状态。当设置为true时,ASP.NET会自动把会话ID追加到URL,以及存在于被请求页面中的任何相关的URL。
    没有启用,设置为false,启用,设置为true
    这个机制会增加一个处理步骤,因为页面中所有的链接都必须进行重写才能包含这个会话ID,以后所请求的URL必须经过解析才能提取它并获取实际的资源URL(没有会话ID)。
    mode:状态模式。
    InProc——这是一个默认设置。所有的状态都保存在运行应用程序的同一个进程的内存中。这样能够使性能达到最优,但是如果应用程序被重启,或者进程由于某种原因而被挂起,那么相关用户的所有会话数据都将丢失。
    StateServer——可以利用这个设置从运行应用程序的进程中分离出状态存储器。它可以联合下面两个属性:stateConnectionString="tcpip=server:port"  stateNetworkTimeout="for network operations with State Server,in seconds"
    可以通过指定机器的地址和端口,把状态信息保存到它自己的进程和内存中。这样可以把状态从应用程序中隔离出来,防止它出现故障。在状态服务器及其中,必须启动ASP.NET状态服务,这项服务的启动既可以通过Serivces控制台完成,也可以通过下面的命令提示完成:
    >net start aspnet_state
    还可以把这项服务设置为自动启动。通过设置好状态服务器的IP地址,就可以把相应的机器指定为保存应用程序的状态信息。这样可以防止应用程序服务器重启,但是却不能防止机器重新启动。还需要注意的是,把状态存储器放在应用进程外面会产生性能冲突,特别是当应用程序位于网络中的另一台机器上时。一定要弄清楚保留会话信息所引起的冲突是否是正常的。
    SQLServer——如果决定不惜任何代价保留会话状态,就可以利用这个设置。这个模式可以把所有会话状态保存在SQL Server数据库中,因此它可以经受应用程序、服务器、甚至数据库服务器(假设数据库本身无故障)的任何失败操作。对这个模式进行设置其实就是对sessionState元素的以下属性进行配置:
    sqlConnectionString="valid SqlConnection string,minus Initial Catalog"
    还必须运行一个脚本,准备存储状态所需的数据库。脚本在D:\WINDOWS\Microsoft.NET\Framework\v1.1.4322路径下的installsqlstate.sql文件

    运行这个脚本不需要使用SQL Server 2000 Query Analyzer(查询分析器)。MSDE为我们提供了一个命令行实用程序:osql。
    >osql –S [servername] –U [login] –P [pwd] < InstallSqlState.sql
    为了获取最大的可靠性,我们甚至可以对SQL Server进行分组。这种模式是保护会话状态的最有力的方法,不过从性能上而言,这种模式也是最为昂贵的。每个请求都需要在数据库之间进行往返,这会严重影响应用程序进行响应。同时,利用网络进行处理也会由于高负荷而产生瓶颈问题。
    使用InProc模式的会话状态管理时,会话数据存储在ASP.NET工作进程(Aspnet_wp.exe)的内存中。使用这种模式可以快速的存取数据,但是一旦ASP.NET工作进程重启后,会话状态数据就会丢失。
    SQL Server模式将会话状态数据存储在SQL Server数据库中,可以解决会话状态数据丢失的问题。
    下面的步骤描述了通过运行InstallSqlState.sql 和 UninstallSqlState.sql两个脚本文件配置Sql Server管理会话状态的方法:
    1.在查询分析器中运行InstallSqlState.sql脚本。这个脚本可以在下面其中一个目录中找到:
          system drive\WINNT\Microsoft.NET\Framework\version\
          system drive\Windows\Microsoft.NET\Framework\version\
    如果先前已配置过SqlServer模式的会话状态管理,必须首先运行脚本UninstallSqlState.sql移除原先的配置。注意移除之前需要停止w3svc进程,移除之后再将其启动。
    停止w3svc进程:在命令提示符窗口输入net stop w3svc
    启动w3svc进程:在命令提示符窗口输入net start w3svc
     如果启动了VS2005中的ASP.NET Development Server,也需要停止。
     2. 修改应用程序的web.config文件:
    </system.web>
    <sessionState
    mode="SQLServer"
    sqlConnectionString="data source=127.0.0.1;user id=sa;password=sa"
    cookieless="false"
    timeout="20"/>
    </system.web>
     如果在运行UninstallSqlState.sql之前没有停上w3svc进程,将会收到下面的错误信息:
             Cannot drop the database 'ASPState' because it is currently in use
     配置好SqlServer模式的会话状态管理后,ASP.NET将在tempdb数据库中创建数据表ASPStateTempSessions 和ASPStateTempApplications,以保存会话状态数据,因此重启Sql Server将会丢失存储在ASPStateTempSessions 和ASPStateTempApplications中的会话数据。
     这两个脚本文件的永久化版本是InstallPersistSqlState.sql 和 UninstallPersistSqlState.sql,通过在ASPState数据库中创建这两个表要解决这个问题。
     
     
    January 08

    转载:将更智能的 ASP.NET 文件下载体验内置到您的 Web 应用程序中

    将更智能的 ASP.NET 文件下载体验内置到您的 Web 应用程序中

    发布日期: 2006-10-30 | 更新日期: 2006-10-30

    Joe Stagner

    本文将介绍以下内容:

    从 ASP.NET 站点进行动态下载

    生成即时链接

    可恢复下载和自定义处理程序

    自定义下载机制所涉及的安全性问题

    本文涉及以下技术:

    ASP.NET

    代码下载位置:

    Downloading2006_09.exe (174KB)

    *
    本页内容
    基本下载链接 基本下载链接
    适用于所有文件类型的强制下载 适用于所有文件类型的强制下载
    将大文件分为小块下载 将大文件分为小块下载
    更有效的解决方案 更有效的解决方案
    恢复失败的下载 恢复失败的下载

    提要栏

    无意的文件访问

    您的用户极有可能需要从贵组织的网站下载文件。既然提供下载和提供链接一样容易,您当然不需要去阅读有关此过程的文章,对吧?但随着 Web 领域的巨大进步,我们有很多理由可以相信,这个过程不一定像我们想像的那么容易。也许您希望将文件作为一个文件下载,而不是作为内容在浏览器中显示。也许您还不知道这些文件的路径(或者它们根本就不在磁盘上),因此那些简单的 HTML 链接不可能实现下载。也许您会担心用户在下载大文件期间会失去连接。

    在本文中,我将介绍一些解决这些问题的方法,这样您的用户就可以拥有快速、无错的下载体验了。在整篇文章中,我将讨论动态生成的链接,说明如何绕过默认文件行为,并借用 HTTP 1.1 功能来例示可恢复的由 ASP.NET 驱动的下载。

    基本下载链接

    让我们首先来解决缺失链接的问题。如果您不知道某文件的路径将是什么,您只需稍后从数据库中拉出链接列表即可。您甚至可以通过在运行时于给定的目录中枚举文件来动态建立链接列表。这里我将探讨第二种方法。

    假设我在 Visual Basic® 2005 中建立一个 DataGrid,并在其中填入指向下载目录中所有文件的链接,如图1 所示。要完成此操作,可先在页面内使用 Server.MapPath 来检索下载目录的完整路径(此例中为 ./downloadfiles/),再使用 DirectoryInfo.GetFiles 检索该目录中所有文件的列表,然后从 FileInfo 对象的最终所得数组建立一个 DataTable(其中含有代表每个相关属性的列)。可将 DataTable 绑定到页面上的 DataGrid,通过该 DataTable 可生成带有以下 HyperLinkColumn 定义的链接:

    <asp:HyperLinkColumn DataNavigateUrlField="Name"  
        DataNavigateUrlFormatString="downloadfiles/{0}" 
        DataTextField="Name" 
        HeaderText="File Name:" 
        SortExpression="Name" />
    

    如果您单击这些链接,就会发现浏览器对每个文件类型的处理方式都不同,具体取决于注册了哪些助手应用程序来打开每个文件类型。默认情况下,如果您单击 .asp 页面、.html 页面、.jpg、.gif 或 .txt,它会在浏览器其本身中打开,并且不出现“另存为”对话框。这是因为这些文件的扩展名都属于已知的 MIME 类型。因此,要么浏览器本身知道如何呈现文件,要么操作系统具有一个将被浏览器使用的助手应用程序。Webcasts(.wmv、.avi 等等)、PodCasts(.mp3 或 .wma)、PowerPoint® 文件以及所有的 Microsoft® Office 文档都属于已知的 MIME 类型,如果您不想在默认情况下联机打开这些文件,就产生了一个难题。

    .

    图 1 DataGrid 中简单的 HTML 链接

    此外,如果您允许以此方式下载,则只有一个非常普通的访问控制机制可供您使用。您可以逐个目录地控制下载访问,但是逐一控制对各个文件或文件类型的访问需要详尽复杂的访问控制,这对于 Web 主管和系统管理员而言是一个非常麻烦的过程。幸运的是,ASP.NET 和 .NET Framework 提供了大量的解决方案。其中包括:

    使用 Response.WriteFile 方法

    使用 Response.BinaryWrite 方法流式传送文件

    使用 ASP.NET 2.0 中的 Response.TransferFile 方法

    使用 ISAPI 筛选器

    写入到自定义浏览器控件

    适用于所有文件类型的强制下载

    在刚才所列的解决方案中最简单易用的就是 Response.WriteFile 方法。其基本语法非常简单;这个完整的 ASPX 页面将查找被指定为查询字符串参数的文件路径,并将该文件一直伺服到客户端:

    <%@ Page language="VB" AutoEventWireup="false" %>
    <html>
       <body>
            <%
                If Request.QueryString("FileName") Then
                    Response.Clear()
                    Response.WriteFile(Request.QueryString("FileName"))
                    Response.End()
                End If
            %>
       </body>
    </html>
    

    当在 IIS 辅助进程中运行的代码(IIS 5.0 上的 aspnet_wp.exe 或 IIS 6.0 上的 w3wp.exe)调用 Response.Write 时,ASP.NET 辅助进程开始向 IIS 进程(inetinfo.exe 或 dllhost.exe)发送数据。在数据从辅助进程发送到 IIS 进程的过程中,要在内存中进行缓冲处理。这在许多情况下不会产生什么问题。但对于非常大的文件,这却算不上一个很好的解决方案。

    从有利方面看,由于发送文件的 HTTP 响应是在 ASP.NET 代码中创建的,因此您对所有的 ASP.NET 身份验证和授权机制都拥有完全访问权限,从而就可以根据身份验证状态、运行时存在的 Identity 和 Principal 对象或者其他任何您认为适合的机制来做出决策。

    这样,您就可以集成现有的安全机制(例如内置的 ASP.NET 用户和组机制)、Microsoft 服务器加载项(例如授权管理器和定义的角色组)、Active Directory® 应用程序模式 (ADAM) 乃至 Active Directory,以提供对下载权限的精确控制。

    从应用程序代码内部启动下载还可以让您替换对已知 MIME 类型的默认行为。要完成此操作,您需要更改所显示的链接。以下代码构造了一个将回发到 ASPX 页面的超链接:

    <!-- in the DataGrid definition in FileFetch.aspx -- >
    <asp:HyperLinkColumn DataNavigateUrlField="Name"          
        DataNavigateUrlFormatString="FileFetch.aspx?FileName={0}" 
        DataTextField="Name" 
        HeaderText="File Name:" 
        SortExpression="Name" />
    

    接下来,当页面受到请求时,您需要检查查询字符串以确定该请求是否是一个包含要发送到客户端浏览器的文件名参数的回发(参见图2)。现在,由于有了 Content-Disposition 响应标头,当您单击网格中的某个链接时,无论文件是否为 MIME 类型,都会出现“保存”对话框(参见图3)。同时还应注意,我已根据调用 IsSafeFileName 方法的结果限定了可对哪些文件进行下载。有关这样操作的原因以及此方法可实现什么结果的详细信息,请参阅“无意的文件访问”提要栏。

    .

    图 3 强制显示文件下载对话框

    在使用此方法时要考虑的一个重要度量标准就是文件下载的大小。您必须限制文件的大小,否则就会将您的站点暴露给“拒绝服务”攻击。如果试图下载大小超出资源允许范围的文件,将会产生表明该页无法显示的运行时错误,或显示如下所示的错误消息:

    无法访问服务器应用程序
    
    您目前无法访问此 Web 服务器中的 Web 应用程序。请在 Web 浏览器中点击“刷新”按钮以重新提交请求。
    
    管理员通知:可在 Web 服务器的系统事件日志中找到造成此特定请求失败的详细信息。请查看此日志条目以找到造成此错误的原因。
    

    可下载的文件大小上限是服务器硬件配置和运行时状态的一个要素。要应对此问题,请参阅知识库文章“FIX:下载大文件导致大内存丢失并导致 Aspnet_wp.exe 进程以循环”,网址为 support.microsoft.com/kb/823409

    在下载视频之类的大文件时,此方法可能会出现一些症状,尤其是在运行 Windows 2000 和 IIS 5.0 的 Web 服务器(或以兼容模式运行 IIS 6.0 的 Windows Server™ 2003)上更是如此。在配置了最低内存的 Web 服务器上,此问题会更加严重,因为必须先将文件加载到服务器内存中才能将其下载到客户端。

    我曾对一个运行 IIS 5.0 并且 RAM 为 2GB 的服务器进行过测试,实践证明,当文件大小接近 200MB 时,下载就会失败。在生产环境中,同时运行的用户下载越多,就有越多的服务器内存限制导致用户下载失败。对于此问题的解决方案需要使用几行更简明直接的代码。

    将大文件分为小块下载

    先前代码示例所存在的文件大小问题源于对 Response.WriteFile 的单一调用,该调用将在内存中缓冲整个源文件。处理大文件的更有效方法就是将文件分成小的、易管理的文件块来读取并发送到客户端,如图4 中的示例所示。此版本的 Page_Load 事件处理程序每次使用 while 循环读取文件中的 10,000 个字节,然后将这些文件块发送给浏览器。因此,在运行时文件不会有任何重要部分保留在内存中。文件块大小目前被设为一个常量,但可通过编程方式对其修改,甚至也可以将其移动到配置文件中,以便根据服务器限制和性能要求对其进行更改。我使用一个大小高达 1.6GB 的文件测试了此代码,结果是下载速度非常块,并且不会耗用大量的服务器内存。

    IIS 本身并不支持文件大小超出 2GB 的文件下载。如果您要下载较大的文件,则需要使用 FTP、第三方控件、Microsoft 后台智能传送服务 (BITS) 或一个自定义解决方案(例如,通过套接字将数据流式传送到托管浏览器的自定义控件)。

    更有效的解决方案

    文件下载要求的共同性以及通常文件大小都在不断增加的这个事实促使 ASP.NET 开发团队在 ASP.NET 中添加了一个特定方法,以便在下载文件时,不必在内存中对文件进行缓冲处理就可以将其发送到浏览器。该方法就是 Response.TransmitFile,在 ASP.NET 2.0 中提供。

    TransmitFile 的用法与 WriteFile 非常相似,但 TransmitFile 通常会产生更好的性能特征。TransmitFile 还可以与其他功能性相媲美。看一下图5 中的代码,此代码使用新增的 TransmitFile 的一些附加功能来避免上述的内存使用问题。

    我只需额外添加几行代码就可以增加一些安全性和容错性。首先,我使用被请求文件的文件扩展名添加了一些安全性和逻辑限制来确定 MIME 类型,并通过设置 Response 对象的“ContentType”属性来指定 HTTP 标头中被请求的 MIME 类型:

    Response.ContentType = "application/x-zip-compressed"
    

    这使我可以将下载目标仅限制为某些内容类型,并可将不同的文件扩展名映射到一种单一内容类型。还应注意一下添加 Content-Disposition 标头的语句。此语句使我可以指定要下载的文件名,此文件名不同于服务器硬盘上的原始文件名。

    在此代码中,我通过在原始文件名中附加一个前缀来创建一个新文件名。尽管此处的前缀是静态不变的,但我可以动态创建一个前缀,以便下载的文件名绝对不会与用户硬盘上已有的文件名相冲突。

    但是,如果在获取大文件的中途出现下载失败怎么办?尽管该代码迄今为止已从简单的下载链接跨出了一大步,但我仍然无法妥善处理失败的下载并在中断后继续下载已将部分内容从服务器移至客户端的文件。我至今所检验过的所有解决方案都需要用户在下载失败时从头开始重新下载。

    恢复失败的下载

    要解决恢复失败下载这个问题,让我们回顾一下将文件手动拆分成块进行传送的方法。尽管不像使用 TransmitFile 方法的代码那样简单,但手动编写分块读取和发送文件的代码具备一个优点。在任何给定时刻,运行时状态都包含了已发送到客户端的字节数,通过从整个文件大小中减去该字节数,就会得到为使此文件完整还需要传送的剩余字节数。

    如果回顾一下该代码,您就会发现读取/发送循环会在某循环构成 Response.IsClientConnected 结果的条件时进行检验。该测试将确保在与客户端断开连接时将传送过程暂停。在测试结果为“假”(启动文件下载的 Web 浏览器已断开连接)的第一次循环迭代中,服务器将停止发送数据,并且可记录要完成文件所需发送的剩余字节数。此外,如果用户试图完成失败的下载,可将客户端收到的部分文件进行保存。

    可恢复下载解决方案的剩余部分是通过 HTTP 1.1 协议中的一些鲜为人知的功能实现的。通常,HTTP 的无状态性质是 Web 开发人员的克星,但在本例中,HTTP 规范却提供了很大帮助。具体来说,有两个 HTTP 1.1 标头元素与我们要完成的这项任务相关。Accept-Ranges 和 Etag。

    Accept-Ranges 标头元素可以非常简单地向客户端(本例中指 Web 浏览器)指明,此进程支持可恢复下载。实体标记或 Etag 元素将为该会话指定一个唯一标识符。因此,可由 ASP.NET 应用程序发送到浏览器以开始一个可恢复下载的 HTTP 标头可能如下所示:

    HTTP/1.1 200 OK
    Connection: close
    Date: Mon, 22 May 2006 11:09:13 GMT
    Accept-Ranges: bytes
    Last-Modified: Mon, 22 May 2006 08:09:13 GMT
    ETag: "58afcc3dae87d52:3173"
    Cache-Control: private
    Content-Type: application/x-zip-compressed
    Content-Length: 39551221
    

    由于使用了 ETag 和 Accept-Headers,浏览器知道了 Web 服务器将支持可恢复下载。

    如果下载失败,则当该文件再一次被请求时,Internet Explorer 将发送 ETag、文件名和指明在中断前已成功下载的文件字节数的值范围,以便 Web 服务器 (IIS) 可以尝试恢复下载。第二次请求可能如下所示。

    GET http://192.168.0.1/download.zip HTTP/1.0
    Range: bytes=933714-
    Unless-Modified-Since: Sun, 26 Sep 2004 15:52:45 GMT
    If-Range: "58afcc3dae87d52:3173"
    

    请注意,If-Range 元素包含服务器可用于标识要重新发送的文件的原始 ETag 值。您还会看到 Unless-Modified-Since 元素包含了最初下载的开始日期和时间。服务器将利用此信息来确定自最初下载开始后该文件是否已被修改过。如果已被修改,则服务器将从头开始重新下载。

    Range 元素也包含在标头中,它会向服务器指明还需要传送多少字节才能完成文件,服务器可以利用此信息来确定应从已部分下载文件的何处开始继续下载。

    不同浏览器使用这些标头的方式略有不同。客户端可能发送的用于唯一标识该文件的其他 HTTP 标头包括:If-Match、If-Unmodified-Since 和 Unless-Modified-Since。请注意,HTTP 1.1 在某个客户端应该需要支持哪些标头方面并没有特定要求。因此,就有可能出现这样的情况,某些 Web 浏览器不支持这些 HTTP 标头中的任一个,而其他浏览器可能使用不同于 Internet Explorer® 要求的标头的另一个标头。

    默认情况下,IIS 将包含一个如下所示的标头集:

    HTTP/1.1 206 Partial Content
    Content-Range: bytes 933714-39551221/39551222
    Accept-Ranges: bytes
    Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT
    ETag: "58afcc3dae87d52:3173"
    Cache-Control: private
    Content-Type: application/x-zip-compressed
    Content-Length: 2021408
    

    此标头集包含的响应代码不同于原始请求的响应代码。原始响应包含的代码为 200,而该请求使用的响应代码为 206(即“恢复下载”),用于向客户端指明,后面的数据不是一个完整文件,而只是继续先前启动的下载,该下载的文件名由 ETag 标识。

    尽管某些 Web 浏览器依赖的是文件名其本身,但 Internet Explorer 非常明确地要求 ETag 标头。如果 ETag 标头在最初下载响应或下载恢复中不存在,则 Internet Explorer 不会尝试恢复下载,而只是开始一个新下载。

    为使 ASP.NET 下载应用程序实现可恢复下载功能,您需要能够拦截浏览器发出的请求(进行下载恢复),并使用请求中的 HTTP 标头在 ASP.NET 代码中明确表达相应的响应。要完成此操作,您应在正常处理序列中早一些捕获该请求。

    令人欣慰的是,.NET Framework 可以助我们一臂之力。这是 .NET 基本设计前提的一个极好例子,为开发人员每天都需要执行的大部分标准探测工作提供了一个被良好分解的功能对象库。

    在这种情况下,您可以利用 .NET Framework 中 System.Web 命名空间所提供的 IHttpHandler 接口来构建您自己的自定义 HTTP 处理程序。通过创建您自己的实现 IHttpHandler 的类,您将能够拦截对特定文件类型的 Web 请求并用自己的代码响应这些请求,而不是仅让 IIS 以其默认行为做出响应。

    本文中的下载代码包含了支持可恢复下载的 HTTP 处理程序的工作实现。尽管对于此功能存在多个代码,并且其实现需要对 HTTP 机制有一定了解,但 .NET Framework 使此实现变得相对简单。此解决方案提供了下载大文件的能力,并且在下载启动后可以继续进行浏览。然而,还有某些基础结构注意事项不在您的控制范围之内。

    例如,许多公司和 Internet 服务提供商会维护他们自己的高速缓存机制。出现故障或配置错误的 Web 高速缓存服务器会因文件损坏或会话过早终止而导致大文件下载失败,尤其在您的文件大小超过 255MB 时更是如此。

    如果您需要下载超过 255MB 的文件或使用其他自定义功能,您可能会考虑使用自定义的或第三方下载管理器。例如,您可能会构建一个自定义浏览器控件或浏览器助手功能来管理下载,将它们提交给 BITS,或甚至用自定义代码将文件请求提交给 FTP 客户端。摆在眼前的选择数不胜数,应根据您的特定需要来量身定制。

    从通过两行代码实现的大文件下载到具有自定义安全性的可分段的可恢复下载,.NET Framework 和 ASP.NET 为网站的最终用户提供了多种选择来打造最适合的下载体验。

    January 07

    别样的程序员版《大腕》

    别样的程序员版《大腕》:            
     
     
                一定得是N层结构,
         层数越多越好,层少了用户会误会我们不重视,
         什么数据访问层呀,实体控制层啊,能给他加上的全加上。
        
         程序员一定都得是老鸟,30以下基本不考虑,
         还得清一色的外企空降兵,
         都有10年以上编程经验的那种,用过的语言越多越好,
         编程都不带查MSDN的,牛吧!
         程序员个个都配IBM笔记本,
         CPU要3G以上的,内存硬盘和屏幕都要最大的,
         扩展槽别剩下,能插上的全插上。
         能安的操作系统全安上,开机一屏幕上就一堆系统等你选,
         倍有面子!
        
         系统平台就得是J2EE,人家竞争对手除了C++就是JAVA,
         你要用一.NET都不好意思和别人打招呼。
         你说这样的系统设计出来得卖多少钱?
         我觉得怎么也得100万吧?
         100万,那是单机版!
         1000万起!
         你还别嫌太贵,还不免实施服务费。
         你得研究有钱用户得心理,
         愿意花1000万买这套软件用的,
         根本不在乎再多花上几百万!
        
         什么叫软件泡沫你知道么?
         软件泡沫就是做都做最复杂的,
         用就得用最贵的,还得特难用,显得用户水平高。
         所以我们搞软件的口号就是:
         不求好用,但求费劲!
    January 04

    First dairy in my 2007

        Ok, finally got a mood to write sth down for a new beginning of a year:)
     
        The past year's full of puzzles and lieves, which for me,  a good chance to pocess in the rest of my life. People don't like things that brought them unfortune, sadness and dispair, but as for me, maybe i'm such a strange person that always got anything odd or contradictory, they'r normal. After whole everything, it's just matters, like fisherman's drying fishing nets, cleanman's sweeping floors, milkman's riding with raft of bottles, or any other robotech-like movements everyday in our daily life. What i believe, it makes u perfect, not only with ventages, but those frustrations and distresses as well.
     
         Well, one day, a friend asked me about the new plan in the 2007, and her own view on this point is that plan's too changeable and unreliable to keep, so she or he ofen updates her mind in the face of new enviorment. What's more, I also gave out my answer, i've no blueprint with this 2007, if i have----i think i have----, it's just a watershed, and all the feeling on the boundary between 2006 and 2007 is that i'm one year older and more mature.
     
         2006 is really an amazing year for me, during which, i finally found lots of my friends while losing a certain mounts of others, reconnected with my dear teacher in Peking and said goodbye to those teacher-like figures in college ( some are good, really ), got the luck to work in my favourite field and a good company with it's kind adminstration stuff while got to know the crucial circumstance in society, car, house, money, whatever.
     
         On the same year, i took a lot from Julia( in Vancouver ), she's so nice and far-sight( compared to me ) female, with fine style of conversation and allowant attitude with my poor english in our e-talk, I just wanna thank u a lot here:) I remember the story about fengfeng and zhangzhang, it's so explosive and fantastic.
     
         Also in the 2006, after the graduation, my bro- laden, whose nick name founded by me, had been to the Australia, Melbourme, wish this bad guy good luck there, and succeed to finish his courses and come back or never come back.
     
         As well, the others of my friends, who'r keeping touch with me or not having time to call me, my bro, my roomates, my sisters( have to call those girls sisters-elder mainly, coz they'r a strong colony), wish they can have sth better in 2007 and get a certain success.
     
         Fine, Kobe's a sunshine boy, and there's no problem with him to stick to those depressed things. He still got a lot time to keep on his living theatre on the version of 2007, please check it out:)
     
    建国57年1月4日