[C#]Asp.Net页面执行流程分析

mikel阅读(1005)

在我的上一篇文章中说到了HttpModule、HttpHandle的简单使用,我们可以利用它们在页面请求的过程中加入自己的事件处理程序。那么在一个aspx页面请求时后台到底做了什么?当然ASP.NET做了很多事情,过程也比较复杂,本文主要分析一下大体的流程。总体流程如下:
请求一个页面时首先被WWW服务截获(inetinfo.exe进程),这个进程首先判断页面的后缀,然后根据IIS中的配置来决定调用哪个扩展程序,比 如aspx的页面就会调用c:\windows\microsoft.net\framework\v2.0.50727 \aspnet_isapi.dll,aspnet_isapi.dll将请求发送给w3wp.exe进程(我们在调试IIS中网站时就是把VS2005 附加到这个进程上的)。
接下来w3wp.exe进程就会调用.net类库进行具体处理:
ISAPIRuntime–>HttpRuntime–>HttpApplicationFactory–>HttpApplication–>HttpModule–HttpHandlerFactory–>HttpHandler 这也是本文主要分析的地方。
下面只是列出主要流程,如果喜欢钻研的同学可以用Reflector去查看
一:ISAPIRuntime

       bool uSEOOP = iWRType == 1;
        wr 
= ISAPIWorkerRequest.CreateWorkerRequest(ecb, uSEOOP);
        wr.Initialize();
        
string appPathTranslated = wr.GetAppPathTranslated();
        
string appDomainAppPathInternal = HttpRuntime.AppDomainAppPathInternal;
        
if ((appDomainAppPathInternal == null|| StringUtil.EqualsIgnoreCase(appPathTranslated, appDomainAppPathInternal))
        
{
            HttpRuntime.ProcessRequestNoDemand(wr);
            
return 0;
        }

        HttpRuntime.ShutdownAppDomain(ApplicationShutdownReason.PhysicalApplicationPathChanged, SR.GetString(
"Hosting_Phys_Path_Changed"new object[] { appDomainAppPathInternal, appPathTranslated }));
        
return 1;

它的主要作用是调用一些非托管代码生成HttpWorkerRequest对象,该对象包含当前请求的所有信息,然后传递给HttpRuntime,这里生成的HttpWorkerRequest对象可以直接在我们的页面中调用的,通过它取得原始的请求信息:

            IServiceProvider provider = (IServiceProvider)HttpContext.Current;
            HttpWorkerRequest wr 
= (HttpWorkerRequest)provider.GetService(typeof(HttpWorkerRequest));

二:HttpRuntime
最主要的就是private void ProcessRequestInternal(HttpWorkerRequest wr)方法:

        context = new HttpContext(wr, false);
        
        IHttpHandler applicationInstance 
= HttpApplicationFactory.GetApplicationInstance(context);
        
  IHttpAsyncHandler handler2 
= (IHttpAsyncHandler) applicationInstance;
  context.AsyncAppHandler 
= handler2;
  handler2.BeginProcessRequest(context, 
this._handlerCompletionCallback, context);
        

1、根据HttpWorkerRequest对象生成HttpContext,HttpContext应该大家都很熟悉的,它包含request、response等属性,在页面中经常会用到的;
2、调用HttpApplicationFactory来生成IHttpHandler(这里生成的是一个默认的HttpApplication对象,HttpApplication也是IHttpHandler接口的一个实现)
3、调用HttpApplication对象执行请求

三:HttpApplicationFactory
正如2.2中所提到的,这里主要是生成一个HttpApplication对象:

 internal static string GetApplicationFile()
 
{
     
return Path.Combine(HttpRuntime.AppDomainAppPathInternal, "global.asax");
 }

首先会查看是否存在global.asax文件,如果有的话就用它来生成HttpApplication对象,从这里我们可以看到global.asax的文件名是在ASP.NET的框架中写死的,不能修改的。如果这个文件不存在就使用默认的对象。
创建好HttpApplication之后对它进行初始化:

    application = (HttpApplication) HttpRuntime.CreateNonPublicInstance(this._theApplicationType);
 
using (ApplicationImpersonationContext context2 = new ApplicationImpersonationContext())
 
{
     application.InitInternal(context, 
this._state, this._eventHandlerMethods);
 }


 四、HttpApplication
这个是比较复杂也比较重要的一个对象
首先是执行初始化操作,比较重要的一步就是进行HttpModule的初始化:

        private void InitModules()
        
{
            
this._moduleCollection = RuntimeConfig.GetAppConfig().HttpModules.CreateModules();
            
this.InitModulesCommon();
        }

它会读取web.config中所有HttpModule的配置
在HookupEventHandlersForApplicationAndModules方法中绑定Module的事件处理程序
接着进行事件实际绑定:

 if (HttpRuntime.UseIntegratedPipeline)
 
{
     
this._stepManager = new PipelineStepManager(this);
 }

 
else
 
{
     
this._stepManager = new ApplicationStepManager(this);
 }

 
this._stepManager.BuildSteps(this._resumeStepsWaitCallback);

在ApplicationStepManager的BuildSteps方法中可以看到事件的绑定执行顺序:

app.CreateEventExecutionSteps(HttpApplication.EventBeginRequest, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventAuthenticateRequest, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventDefaultAuthentication, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventPostAuthenticateRequest, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventAuthorizeRequest, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventPostAuthorizeRequest, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventResolveRequestCache, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventPostResolveRequestCache, steps);
 steps.Add(new HttpApplication.MapHandlerExecutionStep(app));
 app.CreateEventExecutionSteps(HttpApplication.EventPostMapRequestHandler, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventAcquireRequestState, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventPostAcquireRequestState, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventPreRequestHandlerExecute, steps);
 steps.Add(new HttpApplication.CallHandlerExecutionStep(app));

 app.CreateEventExecutionSteps(HttpApplication.EventPostRequestHandlerExecute, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventReleaseRequestState, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventPostReleaseRequestState, steps);
 steps.Add(
new HttpApplication.CallFilterExecutionStep(app));
 app.CreateEventExecutionSteps(HttpApplication.EventUpdateRequestCache, steps);
 app.CreateEventExecutionSteps(HttpApplication.EventPostUpdateRequestCache, steps);
 
this._endRequestStepIndex = steps.Count;
 app.CreateEventExecutionSteps(HttpApplication.EventEndRequest, steps);
 steps.Add(
new HttpApplication.NoopExecutionStep());

注意上面红色标注的 MapHandlerExecutionStep(读取所有的HttpHandler配置)、CallHandlerExecutionStep就是对 Handle程序进行处理的,也就是说在web.config中配置的HttpHandler都是在这里进行处理的,执行顺序如上所示
 
然后就是调用2.3中的方法执行请求:

Code

在ResumeSteps中就是执行事件处理程序。 

五、HttpModule

在系统web.config中默认的配置有:

Code

基本使用方法可以参见我的上一篇文章

六、HttpHandlerFactory、HttpHandler

这两个对象在web.config中的配置方法是相同的,默认配置有:

Code

要注意的是相同的后缀名配置多次的话,后面的配置会把前面的覆盖。
这里我们重点看一下aspx的配置:System.Web.UI.PageHandlerFactory
这是一个HttpHandlerFactory对象,根据不同的Page生成不同的HttpHandler对象(我们自己的Page页面都是一个IHttpHandler):

    Page page = BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page), context, truetrueas Page;
    
if (page == null)
    
{
        
return null;
    }

    page.TemplateControlVirtualPath 
= virtualPath;
    
return page;

这里会调用web.config中的buildProviders配置编译页面:

Code

这样就可以进入我们的Page执行了,大的执行顺序可以参见第四部分的描述,它也就是一个HttpHandler.

[C#]ASP.NET底层机制

mikel阅读(1072)

1.ASP时代的HTTP请求处理过程

在IIS的应用程序映射中,IIS会将对asp和asa文件的请求转交给asp.dll这个ISAPI来处理

Inetinfo.exe进程,即www服务进程
解释型处理,每次请求都会重新解释一次,不适用于大流量请求
2.ASP.NET的HTTP请求处理过程

3.在HttpRuntime中请求处理过程
HttpRequest请求:
进入HttpRumtime——通过HttpApplicationFactory,建立HttpApplication实例——进入HttpModule——通过HttpHandlerFactory,建立HttpHandler实例
    *这个HttpApplication实例在HttpModule的Init方法中会用到
4.HttpModule工作原理
负责监听HttpRequest,同时对HttpRequest增添或者过滤掉一部分内容。
HttpModule实现了接口IHttpModule,我们可以自定义实现该接口的类,从而取代HttpModule。
ASP.NET默认的HttpModule如下:

        System.Web.SessionState.SessionStateModule;
        System.Web.Security.WindowsAuthenticationModule;
        System.Web.Security.FormsAuthenticationModule;
        System.Web.Security.PassportAuthenticationModule;
        System.Web.Security.UrlAuthorizationModule;
        System.Web.Security.FileAuthorizationModule;

IHttpModule接口分析:

public interface IHttpModule
{
    
// 销毁不再被HttpModule使用的资源
    void Dispose();

    
//初始化一个Module,为捕获HttpRequest做准备
    void Init(HttpApplication context);
}

编写自己的HttpModule:

//注意要在这个类库中添加System.Web引用
using System;
using System.Web;

namespace ClassLibraryModule
{
    
public class MyHttpModule : IHttpModule
    
{
        
public void Init(HttpApplication context)
        
{
            context.BeginRequest 
+= new EventHandler(this.Application_BeginRequest);
            context.EndRequest 
+= new EventHandler(this.Application_EndRequest);
        }


        
public void Dispose() { }


        
//自己要处理私事的两个方法
        public void Application_BeginRequest(Object sender, EventArgs e)
        
{
            HttpApplication application 
= (HttpApplication)sender;

            HttpContext context 
= application.Context;
            HttpResponse response 
= application.Response;
            HttpRequest request 
= application.Request;

            response.Write(
"来自Application_BeginRequest");
        }


        
public void Application_EndRequest(Object sender, EventArgs e)
        
{
            HttpApplication application 
= (HttpApplication)sender;

            HttpContext context 
= application.Context;
            HttpResponse response 
= application.Response;
            HttpRequest request 
= application.Request;

            response.Write(
"来自Application_EndRequest");
        }

    }

}

在Web项目中添加这个类库的引用,同时在Web.config的system.web标签中添加:

        <httpModules>
            
<add name="Test" type="ClassLibraryModule.MyHttpModule,ClassLibraryModule"></add>
        
</httpModules>

name可以随意指定,没有影响。
type有两个参数,第一个表示具体哪个类,第二个表示是哪个dll

不需要在Web项目添加对类库的引用,只是复制一份到bin目录下即可

于是该站点下的每个页面都会Response.Write两句话——这适合做广告,只要替换成JavaScript即可
5.HttpModule内部事件机制
HttpApplication实例有很多事件,BenginRequest和EndRequest分别是HttpModule容器最开始的和最后的事件

注意,EndRequest之后还会触发PreSendRequestHeaders事件和PreSendRequestContent事件,这不是在HttpModule外的两个事件,表示HttpModule结束,即将开始向Client发送数据。
HttpModule容器与HttpHandler容器的交互:
    HttpModule容器会将HttpRequest传递到HttpHandler容器,这个时间点是ResolveRequestCache事件。
    HttpModule容器会建立HttpHandler实例作为入口——Session从此生效
    触发AcquireRequestState事件以及PreRequestHandlerExecute事件,
    HttpModule容器便将对HttpRequest的控制权转让给HttpHandler容器。
    HttpHandler容器处理HttpRequest——使用自身的ProcessRequest方法,将对其控制权又还给HttpModule容器——之后Session失效

可以同时加载两个HttpModule,

        <httpModules>
            
<add name="Test1" type="ClassLibraryModule.MyHttpModule1,ClassLibraryModule1"></add>
            
<add name="Test2" type="ClassLibraryModule.MyHttpModule2,ClassLibraryModule2"></add>
        
</httpModules>

这时,根据add标签的先后,依次执行:
    Test1.BeginRequest
    Test2.BeginRequest
    …..
    Test1.EndRequest
    Test2.EndRequest
利用HttpModule实现当满足一定条件时终止此次HttpRequest:
在BeginRequest事件中,使用HttpApplication.CompleteRequest()方法

        public void Application_BeginRequest(Object sender, EventArgs e)
        
{
            HttpApplication application 
= (HttpApplication)sender;
            HttpContext context 
= application.Context;

            application.CompleteRequest();

            context.Response.StatusCode 
= 500;
            context.Response.StatusDescription 
= "Internal Server Error";
        }

在BeginRquest中终止,但是仍然会调用EndRequest事件,以及PreSendRequestHeaders事件和PreSendRequestContent事件——应该说直接跳转到EndRequest事件,而不会调用这期间的事件
如果有两个HttpModule,在第一个Module的BeginRequest中终止,仅仅不会调用第二个Module的BeginRequest, 但仍然会调用两个EndRequest事件,以及PreSendRequestHeaders事件和PreSendRequestContent事件。
以上两句话,可以用下图来表示:

1.IHttpHandler接口
    定义了实现一个HttpRequest的处理所必须实现的一些系统约定方法。

    public interface IHttpHandler
    
{
        
//其他Request是否可以使用IHttpHandler
        bool IsReusable get; }

        
//处理HttpRequest
        void ProcessRequest(HttpContext context);
    }

NET为ASP.NET提供了很多系统默认HttpHandler类,用来适应不同类型的HttpRequest
    比如aspx,在machine.config中是这样定义的:    
        <add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory"/>
            说明遇到aspx的Request,ASP.Net会将其交给System.Web.UI.PageHandlerFactory的HttpHandler类来处理
如果自己定义了新的HttpHandler,而且在Web.config中指定,则系统只会使用这个新的HttpHandler,而不再使用原先指定的
2.HttpHandler实现了IHttpHandler接口
    一个aspx页面在HttpHandler容器中的ProcessRequest方法才被系统真正的处理解析——即交给PageHandlerFactory处理,该工厂负责提供一个HttpHandler容器,由其处理HttpRequest
3.如果要在HttpHandler容器中使用Session,必须要实现IRequiresSessionState接口——这只是一个空接口,一个标记

using System;
using System.Web;
using System.Web.SessionState;

namespace MyNamespace
{
    
public class MyHandler:IHttpHandler,IRequiresSessionState
    
{
        
public MyHandler() {}

        
public bool IsReusable
        
{
            
get
            
{
                
return true;
            }

        }


        
public void ProcessRequest(HttpContext context)
        
{
            HttpResponse response 
= context.Response;
            HttpRequest request 
= context.Request;

            HttpSessionState Session 
= context.Session;
            Session[
"test"= "hi";

            response.Write(
"<b>Hello world!</b>");
            response.Write(Session[
"test"]);
        }

    }

}

同时,还要在Web.config中加上声明:

   <httpHandlers>
        
<add verb="*" path="*" type="MyNamespace.MyHandler,MyNamespace"></add>
   
</httpHandlers>

4.IHttpHandlerFactory
    待续。。。

[C#]C#多线程学习(一) 多线程的相关概念

mikel阅读(1001)

什么是进程?

当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的。

什么是线程?

线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。

什么是多线程?

多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

多线程的好处:

可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。

多线程的不利方面:

线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要CPU时间跟踪线程; 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题; 线程太多会导致控制太复杂,最终可能造成很多Bug;

接下来将对C#编程中的多线程机制进行探讨。为了省去创建GUI那些繁琐的步骤,更清晰地逼近线程的本质,接下来的所有程序都是控制台程序,程序最后的Console.ReadLine()是为了使程序中途停下来,以便看清楚执行过程中的输出。

任何程序在执行时,至少有一个主线程。

一个直观印象的线程示例:

using System;
using System.Threading;
namespace ThreadTest
{
  class RunIt
  {
    [STAThread]
    static void Main(string[] args)
    {
      Thread.CurrentThread.Name="System Thread";//给当前线程起名为"System Thread"
            Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Thread.CurrentThread.ThreadState);
      Console.ReadLine();
    }
  }
}

输出如下:

System Thread's Status:Running

在这里,我们通过Thread类的静态属性CurrentThread获取了当前执行的线程,对其Name属性赋值“System Thread”,最后还输出了它的当前状态(ThreadState)。

所谓静态属性,就是这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。很容易理解CurrentThread为什么是静态的——虽然有多个线程同时存在,但是在某一个时刻,CPU只能执行其中一个。

在程序的头部,我们使用了如下命名空间:

using System;

using System.Threading;

在.net framework class library中,所有与多线程机制应用相关的类都是放在System.Threading命名空间中的。如果你想在你的应用程序中使用多线程,就必须包含这个类。

我们通过其中提供的Thread类来创建和控制线程,ThreadPool类用于管理线程池等。 (此外还提供解决了线程执行安排,死锁,线程间通讯等实际问题的机制。)

Thread类有几个至关重要的方法,描述如下:

Start():启动线程;

Sleep(int):静态方法,暂停当前线程指定的毫秒数;

Abort():通常使用该方法来终止一个线程;

Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复;

Resume():恢复被Suspend()方法挂起的线程的执行。

[C#]What else is burried down in the depth’s of Go

mikel阅读(1102)

What else is burried down in the depth’s of Google’s amazing JavaScript?

So the new GTalk interface in GMail is pretty rad. Congrats to Dan and the rest of the team that made it “go”.

The talk feature is cool not just from a UI perspective as the code is also chock full of little gems. I’m kind of a dork about low-latency data transport to the browser. HTTP wasn’t meant to be used this way…so of course I’m interested! Ever since Joyce got me involved in the rewrite of mod_pubsub I’ve had my eye on the various ways that servers can push data to browsers and the kinds of technology that will prevent a server that’s doing this from melting down (hellooooooooo Twisted). Using just what’s available to the browser, it’s possible to have the server push data encapsulated in <script> blocks and rely on a progressive rendering behavior that every modern browser implements to dispatch events in near real-time (compared to full page refresh or polling delay). There are a mountain of browser quirks that of course play into this process. The least desirable of these to the user are the “phantom click” and the “throbber of doom” that afflict IE users.

When a page (or an iframe it hosts) is loading content, your browser usually shows some sort of “I’m working” indicator. In the bottom “taskbar” there is usually some sort of progress meter. In the upper right (on IE) the “throbber” will continue to animate until the work is done. Of course in the scenario I’m describing the sent page is never done. The whole point is that the server keeps the connection open. Combine this with the IE behavior of producing a “click” like sound when an iframe is navigated to a different URL, and you’ve got a pretty poor user experience.

But couldn’t you do something with XMLHTTP? Short answer: yes, but not as portably and it won’t get you around IE’s 2-connection limit either so there’s not much of a win. For the long answer, see my talk at ETech or wait for me to post the slides. At the end of the day, the hidden <iframe> hack scales best and is the most portable. Especially if you can lick the UX problems.

Which Google has.

How? By cleverly abusing another safe-for-scripting ActiveX control in IE. Here’s the basic structure of the hack:

  // we were served from child.example.com but
// have already set document.domain to example.com
var currentDomain = "http://exmaple.com/";
var dataStreamUrl = currentDomain+"path/to/server.cgi";
var transferDoc = new ActiveXObject("htmlfile"); // !?!
// make sure it's really scriptable
transferDoc.open();
transferDoc.write("<html>");
transferDoc.write("<script>document.domain='"+currentDomain+"';</script>");
transferDoc.write("</html>");
transferDoc.close();
// set the iframe up to call the server for data
var ifrDiv = transferDoc.createElement("div");
transferDoc.appendChild(ifrDiv);
// start communicating
ifrDiv.innerHTML = "<iframe src='"+dataStreamUrl+"'></iframe>";

This is the kind of fundamental technique that is critical to making the next generation of interactive experiences a reality. Server tools like mod_pubsub and LivePage (and perhaps even JMS buses) are starting to come into their own and the benefits of event-driven IO are starting to become well understood by server-side devs. It’s only a matter of time before server-push data hits an inflection point in the same way that background single-request/single-response data transfer did with Ajax. Dojo will, of course, have infrastructure to support this kind of thing when the borader developer community is ready (most if it is already in place).

From long and painful experience and amazingly deep respect, I take my hat off and bow to whoever it was on the GMail/GTalk team that figured this out. It’s a hell of a hack. It’s no wonder that Google has been able to attract and develop the best DHTML hackers in the world.

Update: so just to be *very* clear, I worked on the rewrite of the mod_pubsub *client*. The server rewrite was handled by some folks who are much smarter than I am.

[C#]什么是.ashx文件

mikel阅读(892)

.ashx 文件用于写web handler的。其实就是带HTML和C#的混合文件。当然你完全可以用.aspx 的文件后缀。使用.ashx 可以让你专注于编程而不用管相关的WEB技术。.ashx必须包含IsReusable. 如下例所示
<% @ webhandler language="C#" class="AverageHandler" %>
using System;
using System.Web;
public class AverageHandler : IHttpHandler
{
public bool IsReusable
{ get { return true; } }
public void ProcessRequest(HttpContext ctx)
{
ctx.Response.Write("hello");
}
}
.ashx比.aspx的好处在与不用多一个html   

注意了VS2005中Web应用程序项目模板里的Generic Handler 项,发现它是一个.ashx文件,实际上它是一个HttpHandler。后来查了一下.Net SDK文档,发现ASP.NET1.1也支持.ashx,但是没有给出详细内容。
我们都知道,HttpHandler是一个彻底自定义Http请求的方法,它通过web.config来定义ASP.NET运行时来过滤出要自定义的Http请求,发送到定义在web.config的指定类中。
利用.ashx文件是一个更好的方法,这个文件类似于.aspx文件,可以通过它来调用HttpHandler类,从而免去了普通.aspx页面的控件解析以及页面处理的过程。这个文件特别适合于生成动态图片,生成动态文本等内容。
建立方法如下:
首先打开一个Web项目,然后在任意目录下使用VS2003解决方案资源管理器的“添加”–>“添加新项”,在对话框中选择“文本文件”,然后在文件名处输入“TextBuilder.ashx”。
然后在同目录下,使用解决方案资源管理器,使用“添加”–>“添加类”,在类文件名处输入“TextBuilder.ashx.cs”。可以看出,它的文件命名规律与.aspx文件相同。
然后在.cs文件处输入以下代码(名称空间略):

using System.Web
public sealed class TextBuilder : IHttpHandler
{
    
public void ProcessRequest(HttpContext context)
    
{
        context.Response.ClearContent();
        context.Response.ContentType 
= "text/plain";
        context.Response.Write(
"Hello World");
        context.Response.End();
    }


    
public bool IsReusable
    
{
        
get return true; }
    }

}

然后在“TextBuilder.ashx”文件的第一行处输入上面这个类的调用代码:

<%@ WebHandler language="C#" Class="MyNamespace.TextBuilder" codebehind="TextBuilder.ashx.cs" %>

上面的代码需要注意的是:必须在Class项中输入类的完整名称,即包括名称空间及类名称。
最后保存并编译项目。
使用IE测试,输入这个.ashx的地址即可。
大家可以看出Response类有个OutputStream方法,可以向客户端输出二进制数据流,所以在我的项目中,使用这个方法,在一个.ashx中 使用DundasChart控件就可以生成非常好的统计图,用它发送二进制数据,方便快捷,而且不需在web.config内输入任何配置代码。
.ashx文件有个缺点,他处理控件的回发事件非常麻烦,比如说如果用它来生成DataGrid的列表也不是不行,但是处理数据的回发,需要一些.aspx页的功能,只有自己手动处理这些功能。所以,一般使用.ashx,用来输出一些不需要回发处理的项目即可。

[C#]实现ORM的四种方案

mikel阅读(890)

   正如ORM名称所指示的,实现ORM的关键点在于解决“对象–关系”之间的映射,例如,如何将一个DataRow转换为一个Entity Object,又如何将一个对某Entity Object的操作映射到一个IDbCommand,等等。我们以DataRabbit为例,在DataRabbit中,使用IORMapping接口来抽象这些映射:

    public interface IORMapping<TEntity>
    {
        TEntity GetEntityFrom(DataRow row ,
bool withBlob);
        
        
/// <summary>
        
/// FillParameterValue  使用entity的内容填充command中的各个IDbDataParameter的参数值。
        
/// </summary>      
        void FillParameterValue(IDbCommand command, TEntity entity);
     }

     关于如何实现IORMapping接口,至少有四种方案。我们以实现GetEntityFrom方法为例为例来讲述这四种方案,这个方法是将一个DataRow转换为一个Entity Object。

1.代码生成器

     现在有很多代码生成器可以直接生成实现了ORM功能的DAL层,其背后的原理是,根据数据表的大纲(如有哪些列、每个列的类型等信息)来生成对应的实现了IORMapping接口的类。代码生成器是在编译之前就完成了这些工作的。

2.反射

     将一个DataRow转换为一个Entity Object,如果粒度更细一点,我们实际要解决的是如何将DataRow的一列的值赋值给Entity Object对应的属性,我们可以使用反射在运行时给Entity Object的属性赋值,如:

entityType.InvokeMember(columnName, BindingFlags.Public | BindingFlags.IgnoreCase |
                        BindingFlags.Instance 
| BindingFlags.SetProperty, null, entity, row[columnName]);

     这行代码的含义是将row中【columnName】列的值赋值给entity对象的【columnName】属性。

3.Emit

     我们可以在运行时根据每个Entity类型动态发射对应的实现了IORMapping接口的类型,这些动态类型发射成功后,便可以被实例化然后被使用。比如我们要发射实现GetEntityFrom方法的代码:

private void EmitGetEntityFromMethod(TypeBuilder typeBuilder, MethodInfo baseMethod, Type entityType, DataSchema dataSchema)
        {
            MethodBuilder methodBuilder 
= typeBuilder.DefineMethod("GetEntityFrom", baseMethod.Attributes & ~MethodAttributes.Abstract, baseMethod.CallingConvention, baseMethod.ReturnType, EmitHelper.GetParametersType(baseMethod));
            ILGenerator ilGenerator 
= methodBuilder.GetILGenerator();
            Label retLabel 
= ilGenerator.DefineLabel();
            ilGenerator.DeclareLocal(entityType); 
//Member member = null ;
            ilGenerator.Emit(OpCodes.Nop);
            ilGenerator.Emit(OpCodes.Newobj, entityType.GetConstructor(
new Type[] { }));
            ilGenerator.Emit(OpCodes.Stloc_0); 
//member = new Member() ;

            IList
<PropertyInfo> blobList = new List<PropertyInfo>();
            
#region 为非blob属性赋值
            
foreach (PropertyInfo property in entityType.GetProperties())
            {
                ColumnSchema columnSchema 
= dataSchema.GetColumnSchema(property.Name);
                
if (columnSchema == null)
                {
                    
continue;
                }
                
if ((property.PropertyType == typeof(byte[])) && (!columnSchema.IsTimestamp))
                {
                    blobList.Add(property);
                    
continue;
                }
                EmitSetProperty(entityType, ilGenerator, property);
            }
            
#endregion
            
if (blobList.Count > 0)
            {
                ilGenerator.Emit(OpCodes.Ldarg_2);
                ilGenerator.Emit(OpCodes.Brfalse, retLabel);
                
#region 为blob属性赋值
                
foreach (PropertyInfo property in blobList)
                {
                    EmitSetProperty(entityType, ilGenerator, property);
                }
                
#endregion
            }
            ilGenerator.MarkLabel(retLabel);
            ilGenerator.Emit(OpCodes.Nop);
            ilGenerator.Emit(OpCodes.Ldloc_0);
            ilGenerator.Emit(OpCodes.Ret);
            typeBuilder.DefineMethodOverride(methodBuilder, baseMethod);    
// 定义方法重载
        }

 

4.使用Lamda表达式

     如果是在.NET3.5上,我们可以使用动态生成的Lamda表达式来完成Entity Object属性的赋值操作,关键点是要如何生成用于赋值的动态委托。比如:

 

 private Action<TEntity, object> CreateFunctionOfSetProperty<TEntity>(MethodInfo setPropertyMethod, Type columnType)
        {
            ParameterExpression paramEntityObj 
= Expression.Parameter(typeof(TEntity), "entity");
            ParameterExpression paramProVal 
= Expression.Parameter(typeof(object), "propertyVal");
            UnaryExpression paramProVal2 
= Expression.Convert(paramProVal, columnType);
            MethodCallExpression body 
= Expression.Call(paramEntityObj, setPropertyMethod, paramProVal2);
            Expression
<Action<TEntity, object>> setPropertyExpression = Expression.Lambda<Action<TEntity, object>>(body, paramEntityObj, paramProVal);
            Action
<TEntity, object> setPropertyAction = setPropertyExpression.Compile();
            
return (entity, propertyVal) => { setPropertyAction(entity, propertyVal); };
        } 

     这个方法返回一个委托,返回的委托接收两个参数–Entity Object 和要赋的属性值,调用这个委托便可以为Entity Object的某个属性进行赋值。

 

     好了,四种方案已经简单介绍完毕,下面我们来比较一下。

(1)除了第一种方案是在编译期完成外,后面三种方案都是在运行期完成的。

(2)第一种方案的效率是最高的,但是所需的手工操作也是最多的(比如每次修改了表结构都需要重新生成DAL层)。第二种方案的效率是最低的,反射使效率的折损非常之大。后两种方案的效率都不错(几乎接近第一种方案)。

(3)Emit方案的实现难度应当是最大的,其次是Lamda表达式方案。

(4)Lamda表达式方案在.NET3.5及以上平台才可使用。

     DataRabbit 3.x及之前版本采用的是Emit方案,后续的4.0及以上版本则对Emit方案和Lamda表达式方案都支持(默认为Emit方案,可以修改配置选项使之采用Lamda表达式方案)。

 

[C#]ASP.NET and Comet: Bringing Sockets Back

mikel阅读(1038)

Sample Image

Introduction

Web pages and ASP.NET are wonderful tools, but as the old proverb goes, "if all you have is a hammer, everything seems like a nail". ASP.NET is a web technology, all of which exists to serve up resources to Web Browsers which request them. That's great, and it works for the vast majority of things currently on the Internet. In fact, when people think of the Internet, they mostly associate it with the ubiquitous World Wide Web model or its HTTP protocol. But, what if we want more…what if instead of Web Browsers just requesting resources, we want to push information actively out to them, without being asked first? Examples of this include: live wiki's, polls, chat, stock tickers, real-time auctions, and games.

This is where the existing paradigm fails, and we have to fall back to older technologies, the roots on which the Internet is founded….yes, I'm talking about actual two-way communication, interactivity, *gasp* sockets, TCP/IP!

Taking a step back, web requests can roughly be compared to a student asking a teacher questions. However, only the student is allowed to ask questions, the teacher cannot actively prompt the student with facts. This article sets out one experimental approach to breaking that model. Most of the current solutions, collectively called "Comet", involve the Web Browser keeping an active HTTP connection to the Web Server, like a student always keeping the teacher's attention. Presented here is another aproach, utilizing a "side-channel", where the "teacher" (that's the Web Server) doesn't spend as much attention or resources responding to pesky questions (web requests), and we can push information out instead, or engage in a more interactive way.

Background

This article is inspired and partially based on the work and code by Alex MacCaw, the developer who created the Juggernaut extension to Ruby on Rails. But, why leave all the fun to those dynamic language people? Can't we have that in ASP.NET too? The answer is "yes we can".

How do we communicate with the Web Browser without it making a request first? One solution is to have the Web Browser keep open a socket connection directly to the server using a small light-weight Flash component.

What are the benefits of this aproach?

  • It consumes much less Web Server resources. We are not dedicating any Web Server threads to responding to a "long-polling" request.
  • It's simple and easy to implement.
  • Good cross-platform support (any modern Web Browser that supports Flash 8, e.g., IE, FireFox, Safari, Opera).
  • Lower latency, no re-establishing connections, or other HTTP protocol overheads.

And yes, it does have drawbacks:

  • We need to install something on the Web Server or the network domain to accept these socket connections.
  • The embedded Flash component making the connection to the Comet server will not work behind firewalls or proxies that block non-HTTP ports.
  • It needs Flash and JavaScript enabled on the Web Browser.
  • It's a hack (though the same thing could be said about many, now ubiquitous, Web Browser technologies).

Using the Code

Comet applications can be divided into two categories:

  • "Server-push" applications where the Web Server pushes out information on updates and changes based on user events, e.g., live wiki's, polls, chat etc.
  • "Interactive" applications where an external process pushes information to the web browser in response to an event, e.g., stock tickers, real-time auctions, games etc.

Server-push

The work flow for using Comet in a server-push scenario is:

  1. The Web Browser requests a page from the Web Server, which includes the Flash Comet component, Comet JavaScript, and a unique authentication string.
  2. The Web Server registers the Comet connection with the "Comet Server", specifying its channel, its authentication string, and any initial state.
  3. The Web Browser opens a socket connection to the Comet Server, and sends its authentication string.
  4. The Web Browser sends data to the Web Server using postbacks.
  5. The Web Server responds to any postbacks or AJAX calls and publishes any updates to the Comet Server.
  6. The Web Browser responds to any updates from its Comet connection.

In code, with a simple "GuestBook" example:

  1. Placing a "CometControl" on the ASP.NET web page:
  2. Collapse
    <CometControl
    id="cometControl"
    runat="server"
    channel="GuestBook"></CometControl>
  3. Registering the Comet connection:
  4. Collapse
    protected void Page_Load(object sender, EventArgs e)
    {
    if (!IsPostBack)
    cometControl.Register();
    }
  5. Opening a socket is done automatically on the Web Browser if the "AutoConnect" property is set to true (the default).
  6. AJAX postbacks can be done with a simple UpdatePanel using ASP.NET AJAX extensions.
  7. The Web Server responds to AJAX events by publishing JavaScript code that is automatically evaluated on the Web Browser.
  8. Collapse
    protected void submitButton_Click(object sender, EventArgs e)
    {
    string name = nameTextBox.Text.Trim();
    string text = commentTextBox.Text.Trim();
    if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(text))
    {
    cometControl.Data = "guestbook_addComment ("
    + Wxv.Comet.Utility.SerializeJSON(
    HttpContext.Current.Server.HtmlEncode(name))
    + "," + Wxv.Comet.Utility.SerializeJSON(
    HttpContext.Current.Server.HtmlEncode(text)) + ");";
    cometControl.Publish();
    }
    commentTextBox.Text = "";
    }
  9. Finally, the Web Browser receives the data published on the Web Server and executes it:
  10. Collapse
    guestbook_addComment = function(name, comment)
    {
    var commentsDiv = document.getElementById ("comments");
    var e = document.createElement("blockquote");
    e.innerHTML = comment + " - " + name + "";
    commentsDiv.insertBefore (e, commentsDiv.firstChild);
    }

Interactive

The interactive scenario is very similar, but instead of the Web Browser doing postbacks or AJAX calls to the Web Server, the Web Browser uses its Comet connection to send data directly to an Application Client and vice versa.

The work flow for using Comet in an interactive scenario is:

  1. The Web Browser requests a page of the Web Server, which includes the Flash Comet component, Comet JavaScript, and a unique authentication string.
  2. The Web Server registers the Comet connection with the "Comet Server", specifying its channel, its authentication string, and any initial state.
  3. The Web Browser opens a socket connection to the Comet Server, and sends its authentication string, and the Comet Server tells any Application Clients whether a connection has been made on a channel they have subscribed to.
  4. The Application Client sends data to the Comet Server, which passes it onto any Web Browser which has connected on that channel.
  5. The Web Browser sends data to the Comet Server over its Comet connection, which passes it onto any Application Client which has subscribed to that channel.

The Web Browser code is still very similar, but now, instead of doing AJAX calls or postbacks, we can just send data to our actively listening Application Client over the Comet connection.

E.g.: in response to a button click in a "sound player" application:

Collapse
function sounds_click(soundId)
{
comet.send (soundId);
}

The Comet Server and Clients

The Comet Server is a simple event handling and messaging component that accepts two types of connections:

  • Comet connections from Web Browsers, these are always pre-registered and associated with a single channel.
  • Client connections from the Web Server, Comet Controls, or Client Applications.

It responds to the following Client events:

  • Register – A Client wants to allow a Web Browser to make a Comet connection.
  • Subscribe – A Client wants to listen to events on a specific channel.
  • Publish/Push – A Client wants to send data to an entire channel (Publish) or a specific Comet connection (Push).

It sends the following events to any Client that has subscribed to the related channel.

  • Connect – A Web Browser has successfully connected and authenticated with the Comet Server.
  • Disconnect – An authenticated Web Browser has disconnected from the Comet Server.
  • Send – A Web Browser has sent data to the Comet Server.

Both the Comet Server and the Client component, which Application Clients can be written with, are simple components. The Comet Server component is self-contained, the only interaction needed is to get it to start and stop, and it can be easily hosted in a Windows application, a Service, or even in the Web Server itself. Similarly, the Client is a simple component (which the CometControl already encapsulates) which exposes some simple methods and events (on a single thread) which can easily be extended to write your own Application Clients which can also be hosted in a variety of ways.

Summary

Web browsers opening socket connections to a server isn't actually that new. In their isolated little sandboxes, Java applets have been doing it since they were invented more then a decade ago, and Flash has had the capability for a while too. Microsoft's Silverlight will eventually be getting it in version 2.0. All that the Comet approaches are trying to do is get a similar capability directly within the web browser itself.

I've had fun with this project, exploring a Comet solution for ASP.NET. I don't pretend that it's anything more than it is, a toy project; so consume and eat it with a grain of salt (and credit me if it tastes nice). For this author however, it gave an interesting insight to what's possible.

History

  • 7-Apr-2008 – First version!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Wilfred Verkley

Im a Software Developer working in Auckland, New Zealand. When i was a lot shorter, i started programming in Atari Basic, though these days its mostly C#, and a bit of java (mostly the caffinated kind).

[C#]Scalable COMET Combined with ASP.NET

mikel阅读(1096)

Disclaimer

This code is not production ready; it is designed to demonstrate a theoretical solution to using COMET in ASP.NET. This article covers the server-side implementation of COMET and how to combat the scalability problems. To demonstrate the client code, I will release a smaller article soon which demonstrates a small tic-tac-toe game using the COMET thread pooling mechanism I mention below, which should give you some idea about using it in a real world application.

Introduction

Over the past six months, I have been putting together an online chess application where players can register, login, and play chess in real-time. One of the obstacles I had to overcome was how to implement a real-time communication between the client and the server. For this to be successful, a number of factors need to be addressed:

  • Scalability – I wanted it to function in a load-balanced environment, and not consume huge amounts of server resources.
  • Compatibility – I wanted it to function with many different flavours of browsers, hopefully without the need for a plug-in.
  • Performance – I needed responses from players to be available to the opponents as soon as possible so I could manage time controls and provide a better experience.
  • Simplicity – I wanted to implement the communication layer without installing a third party server application. Mainly, so it would work on hosted environments such as www.discountASP.NET.

So, I evaluated up all of my options. My first prototype used a standard AJAX mechanism which polled the server; this created too much latency and too much traffic, so I quickly moved on from this. I researched other mechanisms of transport, such as using socket communication via a hidden Flash applet; this required a browser plug-in, and so I moved on. I then found the COMET idea, and thought, bingo, this is what I want to use, so I did some more research and built a prototype.

The idea behind COMET

COMET uses a persistent connection between the client (Web Browser, using XmlHttpRequest) and the server. This persistent connection is held open on the server for a predefined period of time (let's say, 5 seconds) and will only respond back to the client with either a timeout message, or when some part of the server's application logic wants to send a message. Once the client receives the message, it is processed using whatever application logic is implemented on the client. The persistent connection is then reopened, and the process starts again.

This mechanism solves the Performance requirement; it means that whenever a message is needed to be sent to the client, and if a persistent connection is open, the client should receive it with very little latency, almost instantly.

A second connection is used to send messages to the server; this connection is not persistent, and typically returns immediately after it is processed. From the point of view of a chess game, the persistent connection would be waiting for my opponent’s move, while the non-persistent connection would send my move.

sequence1.GIF

sequence2.GIF

Real world use implementation of COMET

So far, everything looks great on paper; we have a mechanism that can deliver messages to a browser in real-time without a plug-in, but in practice, this is much more difficult. Many articles that describe the features of using a persistent connection comment about how much of a "hack" this is. I tend to disagree with these statements.

It is true that COMET can run into issues when running on some browsers (mainly because HTTP imposes a two connection per browser, per host limitation). This limitation of the HTTP protocol was implemented to provide better performance for normal browsing over low bandwidth connections (e.g., dialup modems), and can cause performance issues when running COMET (there are ways around this!). This issue is only really noticeable in Internet Explorer (I think IE is strict with this standard up until IE8); Firefox 2 allows more connections, and manages them better, and Firefox 3 allows even more, meaning the future for COMET style applications is bright.

The second issue comes from the scalability of the technology, and this is mainly what this article is trying to remedy. This issue is present because the platforms lack the support for a COMET style protocol, and presently do not scale well when using persistent connections. I would say this is not a failure of the overall idea of COMET, rather a failure of the implementation of a specific COMET server.

Other developers have put together servers that sit in front of the platforms we develop on, these allow us to separate the COMET request mechanism from the web server, and allow the solution to scale by managing their own persistent connections. What I will demonstrate in this article is how you should not use COMET in ASP.NET, and the possible solution.

Load testing COMET

The major drawback with persistent connections to ASP.NET is that each connection consumes an ASP.NET worker thread for the five seconds that the connection is open. Therefore, each client that connects will hold a thread from the ASP.NET thread pool and, eventually, under load, the server will stop responding.

To demonstrate this, I put together a very simple application to simulate persistent connections to an ASP.NET application, with a handler that holds the requests open for 5 seconds before returning to the client:

Collapse
public class CometSyncHandler : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
int workerAvailable = 0;
int completionPortAvailable = 0;
ThreadPool.GetAvailableThreads(out workerAvailable,
out completionPortAvailable);
Debug.WriteLine("CometSyncHandler.ProcessRequest Start");
Debug.WriteLine(string.Format("Worker Threads Available: {0}",
workerAvailable));
Debug.WriteLine(string.Format("Completion Port Threads Available: {0}",
completionPortAvailable));
DateTime now = DateTime.Now;
while (true)
{
Thread.Sleep(50);
if (DateTime.Now.Subtract(now).TotalSeconds >= 5)
break;
}
Debug.WriteLine("CometSyncHandler.ProcessRequest End");
}
#endregion
}

This handler is very simple, it holds the execution of the request up to for 5 seconds, then returns. This simulates a COMET request that would eventually timeout and return the client.

I also wrote a console application that ramps up WebRequest calls to the CometSyncHandler handler. The results were pretty much as expected; each client used an ASP.NET worker thread, and eventually after 40 or so connections, the website started to underperform, and page requests started to respond very slowly.

The screenshot below shows this happening:

sync.PNG

I ramped up approximately 50 connections with two instances of the CometClientSimulator application, and as the website performance dropped, it started to drop connections from the simulator. To repeat this test, you can open and Debug the CometAsync website, which would open default.aspx and set everything running, then open an instance of the CometClientSimulator console application and type addsyncclients; this will then ramp up 25 clients, adding one every two seconds into the ASP.NET application.

Clearly, this is no good for any real world application, so I did some digging and devised a solution.

IHttpAsyncHandler

This is the first part of the solution. This little piece of magic allows us to run code asynchronously on the web server when we service a request to a handler. If you are not familiar with IAsyncHttpHandler, then read my brief explanation of how it works below:

IHttpAsyncHandler exposes two main methods that require implementation; these are BeginProcessRequest and EndProcessRequest. The general idea behind this is we service the start of our request in BeginProcessRequest; we then hand execution off to some sort of asynchronous method, e.g., Database Query Execution, or indeed any asynchronous .NET method. The asynchronous method completes, and then the response to the client is processed in EndProcessRequest.

The sequence diagram below shows how this would work:

sequence3.gif

CometThreadPool

The sequence diagram above introduces a custom thread pool for servicing the COMET requests that sits around on the server. This is required because we don't want ASP.NET to use one of its threads while waiting around for a COMET request to timeout.

The code for this pooling mechanism is located in the website in the CometAsync folder. It contains the following files:

  • CometAsyncHandler – This is the IHttpAsyncHandler implementation.
  • CometAsyncResult – This is a custom IAsyncResult implementation that contains the state of a COMET asynchronous operation.
  • CometThreadPool – This is a static class that manages the COMET thread pool.
  • CometWaitRequest – This is an object that represents the request from the client. These are queued to be processed in the custom thread pool.
  • CometWaitThread – This is a thread that processes CometWaitRequest objects from the queue.

This implementation works by first creating a set of background CometWaitThread objects that each contain a single thread that processes the CometWaitRequest queue items. In our web application, we would initialize the thread pool in Application_Start in the global.asax file.

Collapse
protected void Application_Start(object sender, EventArgs e)
{
//
//  queue 5 threads to run
//  the comet requests
CometThreadPool.CreateThreads(5);
}

This fires up five threads that idle along in the background, waiting for CometWaitRequest instances to service.

Our CometAsyncHandler than waits for requests from the client; its responsibility is to queue the requests in the thread pool.

Collapse
public IAsyncResult BeginProcessRequest(HttpContext context,
AsyncCallback cb, object extraData)
{
int workerAvailable = 0;
int completionPortAvailable = 0;
ThreadPool.GetAvailableThreads(out workerAvailable,
out completionPortAvailable);
Debug.WriteLine(
string.Format(
"BeginProcessRequest: {0} {1} out of {2}/{3} ({4} Requests Active)",
Thread.CurrentThread.IsThreadPoolThread,
Thread.CurrentThread.ManagedThreadId,
workerAvailable,
completionPortAvailable,
CometWaitRequest.RequestCount));
//  get the result here
CometAsyncResult result =
new CometAsyncResult(context, cb, extraData);
result.BeginWaitRequest();
//  ok, return it
return result;
}

The BeginProcessRequest outputs some Debug information so we can keep track exactly of what threads are available, and then creates an instance of a CometAsyncResult class that tracks the HttpContext and is used to return to ASP.NET to indicate it has started an asynchronous process. Before returning, it calls BeginWaitRequest, which adds the request into the thread pool.

Collapse
public void BeginWaitRequest()
{
CometThreadPool.QueueCometWaitRequest(new CometWaitRequest(this));
}

This code creates a new instance of the CometWaitRequest object and queues it in the thread pool.

Collapse
internal static void QueueCometWaitRequest(CometWaitRequest request)
{
CometWaitThread waitThread;
lock (state)
{
//  else, get the next wait thread
waitThread = waitThreads[nextWaitThread];
//  cycle the thread that we want
nextWaitThread++;
if (nextWaitThread == maxWaitThreads)
nextWaitThread = 0;
CometWaitRequest.RequestCount++;
}
//  queue the wait request
waitThread.QueueCometWaitRequest(request);
}

This logic picks a CometWaitThread to assign the CometWaitRequest to, based on a round robin approach (e.g., if thread 1 received the previous request, thread 2 will receive the second).

The CometWaitThread class

Collapse
internal void QueueCometWaitRequest(CometWaitRequest request)
{
lock (this.state)
{
waitRequests.Add(request);
}
}

The request is added into an internal list of CometWaitRequest objects in the chosen thread.

At this point, the CometAsyncHandler has returned the ASP.NET thread to the pool, and is waiting for the CometWaitThread to complete the asynchronous process so it can complete the client's request. Our CometWaitThread code looks like this:

Collapse
private void QueueCometWaitRequest_WaitCallback()
{
//  here we are...
//  in a loop
while (true)
{
CometWaitRequest[] processRequest;
lock (this.state)
{
processRequest = waitRequests.ToArray();
}
Thread.Sleep(100);
for (int i = 0; i < processRequest.Length; i++)
{
//  timed out so remove from the queue
if (DateTime.Now.Subtract
(processRequest[i].DateTimeAdded).TotalSeconds >= 5)
{
//
//  queue anotehr wait callback, so
//  we tell close handler down
//  the endRequest will exist on a 
//  different thread to this
//  one and not tear down this thread
processRequest[i].Result.ResponseObject =
this.CheckForServerPushEvent(processRequest[i], true);
this.QueueCometWaitRequest_Finished(processRequest[i]);
}
else
{
object serverPushEvent =
this.CheckForServerPushEvent(processRequest[i], false);
if (serverPushEvent != null)
{
//  we have our event, which is good
//  it means we can serialize it back to the client
processRequest[i].Result.ResponseObject =
serverPushEvent;
//  queue the response on another 
//  ASP.NET Worker thread
this.QueueCometWaitRequest_Finished
(processRequest[i]);
//  dequeue the request
DequeueCometWaitRequest(processRequest[i]);
}
}
Thread.Sleep(100);
}
}
}

The QueueCometWaitRequest_WaitCallback is the entry point for this thread, and was started way back in Application_Start. It has been cycling around in a loop, waiting for a CometWaitRequest item to appear in its queue; once a client requests the CometAsyncHandler handler, one will be present.

It processes each item in the queue in a sequenced order in each loop iteration, e.g., if there were three requests pending, it would check request 1, 2, then 3, then continue the loop and process request 1, 2, and 3 again. This ensures that each request is processed as soon as possible without having to wait for a previous request to finish its 5 second timeout.

The loop checks to see if the CometWaitRequest has been in the queue for longer than the predefined timeout (in this case, 5 seconds); otherwise, it checks to see if there is an event waiting to be sent back to the client. If either case is true, it completes the CometWaitRequest, returning the desired response object, and then removes it from the queue.

Collapse
private void QueueCometWaitRequest_WaitCallback()
{
.
.
//  queue the response on another ASP.NET Worker thread
this.QueueCometWaitRequest_Finished(processRequest[i]);
//  dequeue the request
DequeueCometWaitRequest(processRequest[i]);
.
.
}
.
.
private void QueueCometWaitRequest_Finished(object target)
{
CometWaitRequest request = target as CometWaitRequest;
request.Result.SetCompleted();
}
.
.

The QueueCometWaitRequest_Finished method completes the asynchronous operation by calling SetCompleted on the CometAsyncResult object, which then calls the callback delegate in the CometAsyncResult which is pointing to EndProcessRequest on the CometAsyncHandler. The following code is then executed:

Collapse
public void EndProcessRequest(IAsyncResult result)
{
int workerAvailable = 0;
int completionPortAvailable = 0;
ThreadPool.GetAvailableThreads(
out workerAvailable, out completionPortAvailable);
Debug.WriteLine(string.Format("EndProcessRequest: {0} {1}" +
" out of {2}/{3} ({4} Requests Active)",
Thread.CurrentThread.IsThreadPoolThread,
Thread.CurrentThread.ManagedThreadId,
workerAvailable,
completionPortAvailable,
CometWaitRequest.RequestCount));
CometAsyncResult cometAsyncResult = result as CometAsyncResult;
if (cometAsyncResult != null &&
cometAsyncResult.ResponseObject != null)
{
DataContractJsonSerializer serializer =
new DataContractJsonSerializer(
cometAsyncResult.ResponseObject.GetType());
serializer.WriteObject(
cometAsyncResult.HttpContext.Response.OutputStream,
cometAsyncResult.ResponseObject);
}
cometAsyncResult.HttpContext.Response.End();
}

This method responds to the client by serializing whatever response object we set in the completion into the response stream of the request's HttpContext.

One thing to mention here is what threads are actually doing the processing of the request. When it arrives in BeginProcessRequest, it is an ASP.NET worker process that is executing, and when the CometWaitThread has completed, either with a timeout or a message, the EndProcessRequest method is executed on one of the CometThreadPool threads, meaning that ASP.NET has only used one of its thread pool thread for the initialization of the COMET request, the remaining 5 seconds were processed without an ASP.NET thread.

We can see that in action in the following screenshot:

async.png

At this point, it is also worth mentioning that the response from the website is very good, considering there are 200 persisted connections, and the CPU/memory usage for the box is good (it is also running the clients).

To also double check everything is working smoothly, I log a counter for each request and response pair to ensure that each request is met with a response. The screenshot below shows this output from running the test with 200 clients for five minutes:

cpu.png

summary.png

This shows that all the requests were complete successfully, (apart from 1, but I put that down to a small race condition in the shutdown code, you can see it completes after!).

Conclusion

By implementing a custom thread pool, we can leverage a COMET mechanism in our ASP.NET server code without implementing a custom server, or even implementing any complicated messaging routines, just a simple thread pool to manage multiple requests (e.g., we had five threads managing all 200 COMET requests).

Using the code

The website project CometAsync will execute and provide a default.aspx page that lists the amount of available threads available to ASP.NET when that request executes.

The CometClientSimulator application will simulate the client connections; it should be executed as follows:

Collapse
CometClientSimulator [websitehost]
websitehost = the host name of the website CometSync app is running e.g. http://localhost:2251

I am currently using a similar engine on my chess website, but it is undergoing vigorous testing right now. By the way, if anyone’s interested in playing chess on there and wants to help out with the testing, drop me an email at imebgo@gmail.com. Thanks!

History

  • 19th June 2008 – Created.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

James Simpson

[C#]Scalable COMET Combined with ASP.NET - Part 2

mikel阅读(1314)

Introduction

If you have read my previous article Scalable COMET Combined with ASP.NET, then you should understand what I was trying to achieve. I explained COMET and how to get the best scalable performance from ASP.NET; however, I think the previous article was a little too close to the wire. It demonstrated the technique well enough, but did not really contain any useful code. So, I thought I would write an API that wrapped up the functionality of the previous article into a neat set of classes that can be included in a typical web project, giving you the opportunity to leverage (and test) the idea.

I'm not going to go into much detail about the threading model, because it is pretty much covered in the previous article; I'm just going to cover the API and how to use it in your web applications.

I decided I would write a lightweight messaging API that is similar to the Bayeux protocol in the way it exchanges messages; however, it is not an implementation of this protocol as I believe it was overkill for what was required to get this API to work, and it is also only a draft.

My original article stated I would put together a Tic-Tac-Toe game; unfortunately, I figured the idea would be easier demonstrated with a simple chat application. The application uses a COMET channel to receive messages, and a WCF service to send messages.

The basic chat application

chat.PNG

Glossary of Terms

Below is a list of terms that I use in this document, and what they are meant to describe:

  • Channel – This is an end point that a COMET client can connect to. Any messages sent to the client must be delivered through a channel.
  • Timeout – This is when a client has been connected to a channel for a predefined amount of time and no messages have been received. A client can reconnect when they "timeout".
  • Idle Client – This is the time frame that a client has not been connected to the server, an idle client will be disconnected after a predefined time.
  • Message – A JSON message that is sent through a channel to a client.
  • Subscribed – A client that is subscribed to a channel. They are connected and ready to receive messages.

The Core Project

The core project contains all the classes required to enable COMET in your ASP.NET application. The code is very close in design to the code in the original article, but I have extended the functionality to enable the transmission of generic messages between the clients and the server.

The main class that controls the COMET mechanism is CometStateManager. This class manages a single channel within your application. This class aggregates an ICometStateProvider instance that manages the state in a particular way for your application. In the API, there is a built-in InProcCometStateProvider implementation that stores the state within the server's memory. Obviously, this is no good for load balanced environments, but one could implement a custom provider that uses a DB, or a custom state server.

To expose your channel to the outside world, it needs to be wrapped up in an IHttpAsyncHandler implementation. I actually attempted to use the asynchronous model within WCF, but found that it did not release the ASP.NET worker threads the same way as the asynchronous handler, which is a bit of a shame, and totally unexpected.

The code below demonstrates how you would setup an IHttpAsyncHandler to provide an end point for your COMET channel:

public class DefaultChannelHandler : IHttpAsyncHandler
{
//    this is our state manager that 
//    will manage our connected clients
private static CometStateManager stateManager;
static DefaultChannelHandler()
{
//    initialize the state manager
stateManager = new CometStateManager(
new InProcCometStateProvider());
}
#region IHttpAsyncHandler Members
public IAsyncResult BeginProcessRequest
(HttpContext context, AsyncCallback cb, object extraData)
{
return stateManager.BeginSubscribe(context, cb, extraData);
}
public void EndProcessRequest(IAsyncResult result)
{
stateManager.EndSubscribe(result);
}
#endregion
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
throw new NotImplementedException();
}
public static CometStateManager StateManager
{
get { return stateManager; }
}
#endregion
}

The above code is pretty simple. We have a static instance of our CometStateManager that is constructed with an implementation of ICometStateProvider. In this example, we use the built-in InProcCometStateProvider implementation.

The rest of the implementation of the class simply maps the BeginProcessRequest and EndProcessRequest methods to the BeginSubscribe and EndSubscribe methods of our CometStateManager instance.

We also need the entry in the web.config file that enables the handler.

<add verb="POST"
path="DefaultChannel.ashx"
type="Server.Channels.DefaultChannelHandler, Server" />

That's it, the channel is now ready to be subscribed to by a client.

The CometClient Class

The channel needs to keep track of clients, each client is represented in some sort of cache by an instance of the CometClient class. We don't want any old client connecting to the server or subscribing to channels without some sort of authentication, so we would implement an authentication mechanism, maybe a standard ASP.NET login form, or possibly a WCF call to a service that can validate some credentials and then initialize a client in our channel.

The code below shows the login action of the default.aspx file in the included chat application:

protected void Login_Click(object sender, EventArgs e)
{
try
{
DefaultChannelHandler.StateManager.InitializeClient(
this.username.Text, this.username.Text, this.username.Text, 5, 5);
Response.Redirect("chat.aspx?username="
+ this.username.Text);
}
catch (CometException ce)
{
if (ce.MessageId == CometException.CometClientAlreadyExists)
{
//  ok the comet client already exists, so we should really show
//  an error message to the user
this.errorMessage.Text =
"User is already logged into the chat application.";
}
}
}

We are not validating a password or anything, we are simply taking the username directly from the page and using that to identify our client. A COMET client has two tokens that are supplied by the consumer of the API:

  • PrivateToken – This is the token which is private to the client, the token that is used to subscribe to messages for that client.
  • PublicToken – This is the token which is used to identify the client to other clients. This is typically used when sending messages to a specific client.

The reason why we use a public and private token is because the private token can be used to subscribe to a channel and receive messages for that user. We don't want any other client to be able to do that apart from the original client (e.g., we don't want the messages spoofed!). Because of this reason, we use the public token if we wanted to send messages between clients.

I have also included a DisplayName property on the client that can be used to store a username; this has been added just for simplicities sake.

To setup a client in the channel, you need to call InitializeClient. This is shown above. This method takes the following parameters:

  • publicToken – The public token of the client
  • privateToken – The private token of the client
  • displayName – The display name of the client
  • connectionTimeoutSeconds – The amount of seconds a connected client will wait for a message until it responds with a timeout message
  • connectionIdleSeconds – The amount of seconds the server will wait for a client to reconnect before it kills the idle client

In the above example, InitializeClient is called, specifying the username from the form as the publicToken, privateToken, and displayName. Although this is not very secure, it is good enough for the example. To make this more secure, I could have generated a GUID for the privateToken, and kept the public token as the username.

The InitializeClient call will then call through to the ICometStateProvider.InitializeClient with a newly initialized CometClient class, and expect it to store it in a cache.

With the CometClient now available in the channel, clients may subscribe to the channel using their privateToken.

Client-Side JavaScript

To enable the client-side functionality, there is a WebResource located in the core project, Scripts/AspNetComet.js that contains all the JavaScript needed to subscribe to the channel (and a public domain JSON parser from here). To make things easier, I have included a static method on CometStateManager called RegisterAspNetCometScripts, which accepts a Page as a parameter and registers the script on that page.

protected void Page_Load(object sender, EventArgs e)
{
CometStateManager.RegisterAspNetCometScripts(this);
}

With this call in place, we are free to use the very basic client-side API that is available to us. The example below is taken from chat.aspx in the web project, and shows how you can subscribe to a particular channel once a client has been initialized.

var defaultChannel = null;
function Connect()
{
if(defaultChannel == null)
{
defaultChannel =
new AspNetComet("/DefaultChannel.ashx",
"<%=this.Request["username"] %>",
"defaultChannel");
defaultChannel.addTimeoutHandler(TimeoutHandler);
defaultChannel.addFailureHandler(FailureHandler);
defaultChannel.addSuccessHandler(SuccessHandler);
defaultChannel.subscribe();
}
}

All the functionality for the client-side API is wrapped up in a JavaScript class called AspNetComet. An instance of this class is used to track the state of a connected client. All that is required to subscribe is the URL of the COMET end point handler, the privateToken of the CometClient, and an alias that is used to identify the channel on the client. Once we have constructed an instance of AspNetComet, we setup a bunch of handlers that are called at specific times during the COMET life cycle.

  • addTimeoutHandler – Adds a handler that is called when a client has waited for a predefined amount of time and no messages have been received.
  • addFailureHandler – Adds a handler that is called when a COMET call fails; examples of failures would be the COMET client is not recognised.
  • addSuccessHandler – Adds a handler that is called for every message that is sent to the client.

The following code shows the signatures of each handler method:

    function SuccessHandler(privateToken, channelAlias, message)
{
// message.n - This is the message name
// message.c - This is the message contents
}
function FailureHandler(privateToken, channelAlias, errorMessage)
{
}
function TimeoutHandler(privateToken, channelAlias)
{
}

The message parameter of the SuccessHandler is an instance of the CometMessage class. The code below shows the class and its JSON contract:

[DataContract(Name="cm")]
public class CometMessage
{
[DataMember(Name="mid")]
private long messageId;
[DataMember(Name="n")]
private string name;
[DataMember(Name="c")]
private object contents;
/// <summary>
/// Gets or Sets the MessageId, used to track 
/// which message the Client last received
/// </summary>
public long MessageId
{
get { return this.messageId; }
set { this.messageId = value; }
}
/// <summary>
/// Gets or Sets the Content of the Message
/// </summary>
public object Contents
{
get { return this.contents; }
set { this.contents = value; }
}
/// <summary>
/// Gets or Sets the error message if this is a failure
/// </summary>
public string Name
{
get { return this.name; }
set { this.name = value; }
}
}

Sending a Message

In the chat web application, I have included an AJAX-enabled WCF web service that acts as the end point for the "Send Message" functionality of the chat application. The code below shows the client-side event handler for the click of the Send Message button:

function SendMessage()
{
var service = new ChatService();
service.SendMessage(
"<%=this.Request["username"] %>",
document.getElementById("message").value,
function()
{
document.getElementById("message").value = '';
},
function()
{
alert("Send failed");
});
}

The code constructs an instance of the ChatService client-side object that is created by the ASP.NET Web Service framework, then just calls the SendMessage method, passing over the privateToken of the client and their message.

The server code for SendMessage then takes the parameters, and writes a message to all the clients; the code below demonstrates this:

[OperationContract]
public void SendMessage(string clientPrivateToken, string message)
{
ChatMessage chatMessage = new ChatMessage();
//
//  get who the message is from
CometClient cometClient =
DefaultChannelHandler.StateManager.GetCometClient(clientPrivateToken);
//  get the display name
chatMessage.From = cometClient.DisplayName;
chatMessage.Message = message;
DefaultChannelHandler.StateManager.SendMessage(
"ChatMessage", chatMessage);
// Add your operation implementation here
return;
}

This method looks up the CometClient from the private token, and then creates a ChatMessage object that is used as the content of the message that is sent to each connected client using the SendMessage method on the CometStateManager instance. This will trigger any connected client to the callback on the SuccessHandler method contained in chat.aspx, which writes the message to the chat area on the page.

function SuccessHandler(privateToken, alias, message)
{
document.getElementById("messages").innerHTML +=
message.c.f + ": " + message.c.m + "<br/>";
}

Using the Code

The website included in the solution will execute without any configuration changes, then just connect a few clients to the application login using a desired username, and chat. Messages should be received in real-time, and appear instantly to each user.

Using the API will enable you to use a COMET style approach in your AJAX enabled applications. Using WCF can be handy for sending messages to the server, this is all neatly wrapped up for you automatically, then just callback to the connected clients on a COMET channel.

History

  • 11th July 2008 – Created.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

James Simpson

[C#]多线程TcpListener自定义事件

mikel阅读(1161)

目的:
1:Server:可以接受多个连接
创建一个等待连接线程,处理新的客户TCP连接请求。
为每一个TCP连接建立一个服务线程。
在接受到请求后,触发外部的一个方法。所以具有onRequest事件
发布一个serverPort的属性,供外部连接。
2:Client:可以连接多个Server
需要知道远程Server的remoteHostName和remotePort属性
具有ProcessRequest方法,供外部调用
————–server code———————
///

/// 为程序安装一个Server,侦听所有连接请求
///
public class Server
{
  public event RequestEventHandler OnRequest;
private IPEndPoint localEndPoint;
public Server(int serverPort)
{
string localName=Dns.GetHostName();
IPHostEntry ipEntry=Dns.GetHostByName(localName);
IPAddress ip=ipEntry.AddressList[0];
localEndPoint=new IPEndPoint (ip,serverPort);
}
public void Start()
{
//创建新的TCP连接需要一个线程,他一直在运行。
//这里只需要把这个线程启动起来
            Thread createServer=new Thread(new ThreadStart(CreateServer));
            createServer.Name="createServer";
createServer.Start();
}
private void CreateServer()
{
//为每一个新的客户端连接请求建立TCP连接
TcpListener listener;
    listener = new TcpListener(localEndPoint);
listener.Start();
//Log.Write("Start to Listen at :"+_localEndPoint.ToString());
while (true)
{
Socket connection;
connection = listener.AcceptSocket();//接受到请求
Connection conHandler=new Connection();
conHandler.connection=connection;
conHandler.OnRequest+=new RequestEventHandler(connectionHandler_OnRequest);//一个连接的每个请求都会执行一个Server的方法
    Thread clientThread =new Thread(new ThreadStart(conHandler.HandleConnection));
clientThread.Start();
}
}
public void Stop()
{
 
}
private string connectionHandler_OnRequest(string request)
{
//这个方法用于触发使用该server的一个事件。
string response;
if(this.OnRequest!=null)
response=OnRequest(request);
else
response="SERVER ERROR : NOT DISPATCH THE SERVER METHODE";
return response;
}
}
public delegate string RequestEventHandler(string request);
///

/// 处理一个连接的每个请求
/// 通过事件OnRequest向外发布事件
///
internal class Connection
{
private NetworkStream socketStream;
private BinaryWriter writer;
private BinaryReader reader;
public event RequestEventHandler OnRequest;
private Socket _connection;
public Socket connection
{
set{this._connection=value;}
}
public void HandleConnection()
{
//处理一个TCP连接的每个请求
while (true)
{
socketStream = new NetworkStream(_connection);
writer = new BinaryWriter(socketStream);
reader = new BinaryReader(socketStream);
string request = "";
do
{
try
{
request = reader.ReadString();
string response=OnRequest(request);//引发处理事件
writer.Write(response);
}
catch (Exception)
{
break;
}
} while (request != "CLIENT>>>EXIT" && _connection.Connected);
writer.Close();
reader.Close();
socketStream.Close();
_connection.Close();
}
}
}
——————–end server code—————————-
——————–client code——————————–
///

/// 客户端,可以发布请求
///
public class Client
{
  private NetworkStream output;
private BinaryWriter writer;
private BinaryReader reader;
        TcpClient client ;
public Client(string remoteHostName,int remoteHostPort)
{
IPHostEntry ipEntry=Dns.GetHostByName(remoteHostName);
IPAddress remoteIP=ipEntry.AddressList[0];
  client = new TcpClient();
client.Connect(remoteIP,remoteHostPort);
output = client.GetStream();
writer = new BinaryWriter(output);
reader = new BinaryReader(output);
            //writer.Write("this is the client to connect");
}
public string ProcessRequest(string request)
{
    writer.Write(request);
//这里是同步消息,需要加入一个异步处理
string response=reader.ReadString();
return response;
}
public void Close()
{
writer.Close();
reader.Close();
output.Close();
client.Close();
}
}
————————end client code——————————