C#开发技术点说明-四种简单的排序算法,AJAX,Http Module,Http 请求处理流

我觉得如果想成为一名优秀的开发者,不仅要积极学习时下流行的新技术,比如WCF、Asp.Net MVC、AJAX等,熟练应用一些已经比较成熟的技术,比如Asp.Net、WinForm。还应该有着牢固的计算机基础知识,比如数据结构、操作系统、 编译原理、网络与数据通信等。有的朋友可能觉得这方面的东西过于艰深和理论化,望而却步,但我觉得假日里花上一个下午的时间,研究一种算法或者一种数据结 构,然后写写心得,难道不是一件乐事么?所以,我打算将一些常见的数据结构和算法总结一下,不一定要集中一段时间花费很大精力,只是在比较空闲的时间用一 种很放松的心态去完成。我最不愿意的,就是将写博客或者是学习技术变为一项工作或者负担,应该将它们视为生活中的一种消遣。人们总是说坚持不易,实际上当 你提到“坚持”两个字之时,说明你已经将这件事视为了一种痛苦,你的内心深处并不愿意做这件事,所以才需要坚持。你从不曾听人说“我坚持玩了十年的电子游 戏”,或者“坚持看了十年动漫、电影”、“坚持和心爱的女友相处了十年”吧?我从来不曾坚持,因为我将其视为一个爱好和消遣,就像许多人玩网络游戏一样。

好了,闲话就说这么多吧,我们回到正题。因为这方面的著作很多,所以这里只给出简单的描述和实现,供我本人及感兴趣的朋友参考。我会尽量用C#和C++两种语言实现,对于一些不好用C#表达的结构,仅用C++实现。

本文将描述四种最简单的排序方法,插入排序、泡沫排序、选择排序、希尔排序,我在这里将其称为“简单排序”,是因为它们相对于快速排序、归并排序、堆排序、分配排序、基数排序从理解和算法上要简单一些。对于后面这几种排序,我将其称为“高级排序”。

简单排序

开始之前先声明一个约定,对于数组中保存的数据,统一称为记录,以避免和“元素”,“对象”等名称相混淆。对于一个记录,用于排序的码,称为关键码。 很显然,关键码的选择与数组中记录的类型密切相关,如果记录为int值,则关键码就是本身;如果记录是自定义对象,它很可能包含了多个字段,那么选定这些 字段之一为关键码。凡是有关排序和查找的算法,就会关系到两个记录比较大小,而如何决定两个对象的大小,应该由算法程序的客户端(客户对象)决定。对 于.NET来说,我们可以创建一个实现了IComparer的类(对于C++也是类似)。关于IComparer的 更多信息,可以参考这篇文章《基于业务对象的排序》。最后,为了使程序简单,对于数组为空的情况我并没有做处理。

1.插入排序

算法思想

插入排序使用了两层嵌套循环,逐个处理待排序的记录。每个记录与前面已经排好序的记录序列进行比较,并将其插入到合适的位置。假设数组长度为n,外 层循环控制变量i由1至n-1依次递进,用于选择当前处理哪条记录;里层循环控制变量j,初始值为i,并由i至1递减,与上一记录进行对比,决定将该元素 插入到哪一个位置。这里的关键思想是,当处理第i条记录时,前面i-1条记录已经是有序的了。需要注意的是,因为是将当前记录与相邻的上一记录相比较,所以循环控制变量的起始值为1(数组下标),如果为0的话,上一记录为-1,则数组越界。

现在我们考察一下第i条记录的处理情况:假设外层循环递进到第i条记录,设其关键码的值为X,那么此时有可能有两种情况:

  1. 如果上一记录比X大,那么就交换它们,直到上一记录的关键码比X小或者相等为止。
  2. 如果上一记录比X小或者相等,那么之前的所有记录一定是有序的,且都比X小,此时退出里层循环。外层循环向前递进,处理下一条记录。

算法实现(C#)

public class SortAlgorithm {
    // 插入排序
    public static void InsertSort(T[] array, C comparer)
        where C:IComparer
    {          
        for (int i = 1; i <= array.Length - 1; i++) {
            //Console.Write("{0}: ", i);
            int j = i;
            while (j>=1 && comparer.Compare(array[j], array[j - 1]) < 0) {
                swap(ref array[j], ref array[j-1]);
                j--;
            }
            //Console.WriteLine();
            //AlgorithmHelper.PrintArray(array);
        }
    }

    // 交换数组array中第i个元素和第j个元素
    private static void swap(ref T x,ref T y) {
        // Console.Write("{0}<-->{1} ", x, y);
        T temp = x;
        x = y;
        y = temp;
    }
}

上面Console.WriteLine()方法和AlgorithmHelper.PrintArray()方法仅仅是出于测试方 便,PrintArray()方法依次打印了数组的内容。swap()方法则用于交换数组中的两条记录,也对交换数进行了打印(这里我 注释掉了,但在测试时可以取消对它们的注释)。外层for循环控制变量i表示当前处理第i条记录。

public class AlgorithmHelper {
    // 打印数组内容
    public static void PrintArray(T[] array) {
        Console.Write("   Array:");
        foreach (T item in array) {
            Console.Write(" {0}", item);
        }
        Console.WriteLine();
    }
}

// 获得Comparer,进行比较
public class ComparerFactory {
    public static IComparer<int> GetIntComparer() {
        return new IntComparer();
    }

    public class IntComparer : IComparer<int> {
        public int Compare(int x, int y) {
            return x.CompareTo(y);
        }
    }
}

上面这段代码我们创建了一个ComparerFactory类,它用于获得一个IntComparer对象,这个对象实现了 IComparer接口,规定了两个int类型的关键码之间比较大小的规则。如果你有自定义的类型,比如叫MyType,只需要在 ComparerFactory中再添加一个类,比如叫MyTypeComparer,然后让这个类也实现IComparer接口,最 后再添加一个方法返回MyTypeComparer就可以了。

输出演示(C#)

接下来我们看一下客户端代码和输出:

static void Main(string[] args) {
    int[] array = {42,20,17,13,28,14,23,15};
    //int[] array = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
    AlgorithmHelper.PrintArray(array);

    SortAlgorithm.InsertSort
        (array, ComparerFactory.GetIntComparer());
}

算法实现(C++)

// 对int类型进行排序
class IntComparer{
    public:
        static bool Smaller(int x, int y){
            return x         }
        static bool Equal(int x, int y){
            return x==y;
        }
        static bool Larger(int x, int y){
            return x>y;
        }
};

// 插入排序
template
void InsertSort(T a[], int length){
    for(int i=1;i<=length-1;i++){
        int j = i;
        while(j>=1 && C::Smaller(a[j], a[j-1])){
            swap(a[j], a[j-1]);
            j--;
        }
    }
}

2.冒泡排序

算法思想

如果你从没有学习过有关算法方面的知识,而需要设计一个数组排序的算法,那么很有可能设计出的就是泡沫排序算法了。因为它很好理解,实现起来也很简 单。它也含有两层循环,假设数组长度为n,外层循环控制变量i由0到n-2递增,这个外层循环并不是处理某个记录,只是控制比较的趟数,由0到n-2,一 共比较n-1趟。为什么n个记录只需要比较n-1趟?我们可以先看下最简单的两个数排序:比如4和3,我们只要比较一趟,就可以得出3、4。对于更多的记 录可以类推。

数组记录的交换由里层循环来完成,控制变量j初始值为n-1(数组下标),一直递减到1。数组记录从数组的末尾开始与相邻的上一个记录相比,如果上 一记录比当前记录的关键码大,则进行交换,直到当前记录的下标为1为止(此时上一记录的下标为0)。整个过程就好像一个气泡从底部向上升,于是这个排序算 法也就被命名为了冒泡排序。

我们来对它进行一个考察,按照这种排序方式,在进行完第一趟循环之后,最小的一定位于数组最顶部(下标为0);第二趟循环之后,次小的记录位于数组 第二(下标为1)的位置;依次类推,第n-1趟循环之后,第n-1小的记录位于数组第n-1(下标为n-2)的位置。此时无需再进行第n趟循环,因为最后 一个已经位于数组末尾(下标为n-1)位置了。

算法实现(C#)

// 泡沫排序
public static void BubbleSort(T[] array, C comparer)
    where C : IComparer
{
    int length = array.Length;

    for (int i = 0; i <= length - 2; i++) {
        //Console.Write("{0}: ", i + 1);
        for (int j = length - 1; j >= 1; j--) {
            if (comparer.Compare(array[j], array[j - 1]) < 0) {
                swap(ref array[j], ref array[j - 1]);
            }
        }
        //Console.WriteLine();
        //AlgorithmHelper.PrintArray(array);
    }
}

输出演示(C#)

static void Main(string[] args) {
    int[] array = {42,20,17,13,28,14,23,15};
    AlgorithmHelper.PrintArray(array);

    SortAlgorithm.BubbleSort
        (array, ComparerFactory.GetIntComparer());
}

算法实现(C++)

// 冒泡排序
template
void BubbleSort(T a[], int length){
    for(int i=0;i<=length-2;i++){
        for(int j=length-1; j>=1; j--){
            if(C::Smaller(a[j], a[j-1]))
                swap(a[j], a[j-1]);
        }
    }
}

3.选择排序

算法思想

选择排序是对冒泡排序的一个改进,从上面冒泡排序的输出可以看出,在第一趟时,为了将最小的值13由数组末尾冒泡的数组下标为0的第一个位置,进行了多次交换。对于后续的每一趟,都会进行类似的交换。

选择排序的思路是:对于第一趟,搜索整个数组,寻找出最小的,然后放置在数组的0号位置;对于第二趟,搜索数组的n-1个记录,寻找出最小的(对于 整个数组来说则是次小的),然后放置到数组的第1号位置。在第i趟时,搜索数组的n-i+1个记录,寻找最小的记录(对于整个数组来说则是第i小的),然 后放在数组i-1的位置(注意数组以0起始)。可以看出,选择排序显著的减少了交换的次数。

需要注意的地方是:在第i趟时,内层循环并不需要递减到1的位置,只要循环到与i相同就可以了,因为之前的位置一定都比它小(也就是第i小)。另外里层循环是j>i,而不是j>=i,这是因为i在进入循环之后就被立即保存到了lowestIndex中。

算法实现(C#)

public static void SelectionSort(T[] array, C comparer)
    where C : IComparer
{
    int length = array.Length;
    for (int i = 0; i <= length - 2; i++) {
        Console.Write("{0}: ", i+1);
        int lowestIndex = i;        // 最小记录的数组索引
        for (int j = length - 1; j > i; j--) {
            if (comparer.Compare(array[j], array[lowestIndex]) < 0)
                lowestIndex = j;
        }
        swap(ref array[i], ref array[lowestIndex]);
        AlgorithmHelper.PrintArray(array);
    }
}

输出演示(C#)

static void Main(string[] args) {
    int[] array = {42,20,17,13,28,14,23,15};
    AlgorithmHelper.PrintArray(array);

    SortAlgorithm.SelectionSort
        (array, ComparerFactory.GetIntComparer());
}

算法实现(C++)

// 选择排序
template
void SelectionSort(T a[], int length) {
    for(int i = 0; i <= length-2; i++){
        int lowestIndex = i;
        for(int j = length-1; j>i; j--){
            if(C::Smaller(a[j], a[lowestIndex]))
                lowestIndex = j;
        }
        swap(a[i], a[lowestIndex]);
    }
}

4.希尔排序

希尔排序利用了插入排序的一个特点来优化排序算法,插入排序的这个特点就是:当数组基本有序的时候,插入排序的效率比较高。比如对于下面这样一个数组:

int[] array = { 1, 0, 2, 3, 5, 4, 8, 6, 7, 9 };

插入排序的输出如下:

可以看到,尽管比较的趟数没有减少,但是交换的次数却明显很少。希尔排序的总体想法就是先让数组基本有序,最后再应用插入排序。具体过程如下:假设有数组int a[] = {42,20,17,13,28,14,23,15},不失一般性,我们设其长度为length。

第一趟时,步长step = length/2 = 4,将数组分为4组,每组2个记录,则下标分别为(0,4)(1,5)(2,6)(3,7);转换为数值,则为{42,28}, {20,14}, {17,23}, {13,15}。然后对每个分组进行插入排序,之后分组数值为{28,42}, {14,20}, {17,23}, {13,15},而实际的原数组的值就变成了{28,14,17,13,42,20,23,15}。这里要注意的是分组中记录在原数组中的位置,以第2个 分组{14,20}来说,它的下标是(1,5),所以这两个记录在原数组的下标分别为a[1]=14;a[5]=20。

第二趟时,步长 step = step/2 = 2,将数组分为2组,每组4个记录,则下标分别为(0,2,4,6)(1,3,5,7);转换为数值,则为{28,17,42,23}, {14,13,20,15},然后对每个分组进行插入排序,得到{17,23,28,42}{13,14,15,20}。此时数组就成了 {17,13,23,14,28,15,42,20},已经基本有序。

第三趟时,步长 step=step/2 = 1,此时相当进行一次完整的插入排序,得到最终结果{13,14,15,17,20,23,28,42}。

算法实现(C#)

// 希尔排序
public static void ShellSort(T[] array, C comparer)
    where C : IComparer
{
    for (int i = array.Length / 2; i >= 1; i = i / 2) {
        Console.Write("{0}: ", i);
        for (int j = 0; j < i; j++) {
            InsertSort(array, j, i, comparer);
        }
        Console.WriteLine();
        AlgorithmHelper.PrintArray(array);
    }
}

// 用于希尔排序的插入排序
private static void InsertSort
    (T[] array, int startIndex, int step, C comparer)
    where C : IComparer
{
    for (int i = startIndex + step; i <= array.Length - 1; i += step) {
        int j = i;
        while(j>= step && comparer.Compare(array[j], array[j - step]) <0 ){
            swap(ref array[j], ref array[j - step]);
            j -= step;
        }
    }
}

注意这里插入排序InsertSort()方法的参数,startIndex是分组的起始索引,step是步长,可以看出,前面的插入排序只是此处step=1,startindex=0的一个特例

输出演示(C#)

static void Main(string[] args) {
    int[] array = {42,20,17,13,28,14,23,15};
    AlgorithmHelper.PrintArray(array);

    SortAlgorithm.ShellSort
        (array, ComparerFactory.GetIntComparer());
}

算法实现(C++)

// 希尔排序
template
void ShellSort(T a[], int length){
    for(int i = length/2; i >= 1; i = i/2 ){
        for(int j = 0; j             InsertSort(&a[j], length-1, i);
        }
    }
}

// 用于希尔排序的插入排序
template
void InsertSort(T a[], int length, int step){
    for(int i = step; i         int j = i;
        while(j>=step && C::Smaller(a[j], a[j-step])){
            swap(a[j], a[j-step]);
            j-=step;
        }
    }
}

对于上面三种算法的代价,插入排序、冒泡排序、选择排序,都是Θ(n2),而希尔排序略好一些,是Θ(n1.5),关于算法分析,大家感兴趣可以参考相关书籍。这里推荐《数据结构与算法分析(C++版)第二版》和《算法I~IV(C++实现)——基础、数据结构、排序和搜索》,都很不错,我主要也是参考这两本书。

感谢阅读,希望这篇文章可以给你带来帮助
 
Asp.Net Ajax的两种基本开发模式

引言

最近花了一些时间,将微软Asp.Net官方的Ajax视频全部看了一遍,地址是http://www.asp.net/learn/ajax-videos/,视频大多都很短,8至15分钟的居多,有讲述AjaxControlToolkit中控件用法的,也有讲述Asp.Net Ajax常见的应用场景和技巧的。

本文介绍了使用Asp.Net Ajax做开发时两种最常见的与服务端进行交互(客户端请求服务端执行逻辑,服务端返回结果)的开发模式。第一种我姑且称为UpdatePanel模式,第二种称为Web Service(WCF Service)模式。

开始前的一些准备

对于这些文章,我假设大家都已经安装好了Asp.Net Ajax Extension 和 Asp.Net Ajax Control ToolKit 这两个组件。其中Asp.Net Ajax Extension已经包含在了.Net Framework 3.5中,而Ajax Control Toolkit可以去这个位置下载:http://www.codeplex.com/AjaxControlToolkit/Release/ProjectReleases.aspx?ReleaseId=16488 。因为我使用的是VS2008,所以Ajax Extension无需安装,而Ajax Control Toolkit我安装到了GAC(Global Assembly Cache,全局程序集缓存)中,因此文章所附代码的Bin目录不会包含任何的dll组件。如果你想运行代码,可以像我一样将Ajax Control Toolkit安装到GAC中,或者针对自己的情况(VS2005或者VS2008,私有程序集部署还是GAC部署)对代码进行一些简单的修改和配置。

如果你想安装到GAC中,假设你将AjaxControlToolkit.dll拷贝到了“C:\”下,那么可以打开“VS2008命令提示符”,然后输入下面的命令,按回车:

gacutil -i C:\AjaxControlToolkit.dll

除此以外,还有两点需想要说明。如果你想要在页面的CodeBehind中使用AjaxControlToolkit中定义的类型,那么需要在Web.config中进行一下配置,假设你和我一样采用的是GAC部署,那么Web.Config的设置为:


   
       
           
           
   

在VS2008(VS2005)中,你可以将AjaxControlToolkit安装到工具箱(Toolbox)中,但是在安装好以后,当你向页面拖放一个控件时,控件默认的前缀是cc1,并且会在页面顶部自动生成一行控件的声明,类似于这样:

// 自动在页面顶部产生的声明
<%@ Register Assembly="AjaxControlToolkit, Version=3.0.20820.37372, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" Namespace="AjaxControlToolkit" TagPrefix="cc1" %>
// 页面中控件的样式
...

这样让人感觉页面很不清爽,除此以外,cc1也没有任何的含义。为了解决这个问题,我们也可以在Web.Config进行一下设置:


   
       
           
           
       

   

如果你和我一样经过上面三个步骤的设置的话,那么在Web站点Bin目录中不会有任何的程序集,另外页面顶部也不会再有控件的声明,同时,拖放控件到页面中时,它的代码将是这样子的:

...

本文以及所有Asp.Net Ajax相关的文章,都假设你采用了和我相同的配置。

Asp.Net Ajax - UpdatePanel模式

现在考虑一个最简单的范例,页面上放置一个Label控件、一个Button控件,当我们点击Button控件的时候,将Label控件的文本更新 为当前时间,这里的关键是更新时间的代码位于服务端,而非使用Javascript在客户端来完成。尽管这里服务端的代码仅仅是更新一下时间,但在实际中 却可以执行任何的服务端操作。

UpdatePanel是是大家熟悉的一种方式了,即是在页面拖放一个UpdatePanel,将需要用Ajax方式进行更新的控件放在 UpdatePanel之内,在本例中是Label控件。可以将Button控件也放置在UpdatePanel之内,也可以不放置。如果 UpdatePanel内不放置Button控件,则需要设置UpdatePanel的Triggers节点,其中包括一个ControlID属性和 EventName属性,用于指定哪个控件的哪个事件可以触发了一个PostBack。本例中ControlID自然是Button的ID,而 EventName则为Click。也就是说当Button的Click事件触发时,进行PostBack操作。下面是aspx页面的主要代码:


 


 当前时间:
 
     
         
     

   
       
     

 

 

 

而在后置代码中,我们只需要像平常的Asp.Net开发一样,编写Button控件的Click事件处理程序就可以了:

protected void Button1_Click(object sender, EventArgs e) {
     Label1.Text = DateTime.Now.ToLongTimeString();
 }

OK,现在一切都已经就绪了,如果你运行这个页面,并且点击Button,会看到Label的值变为了最新的时间,而且没有因为PostBack所产生的页面闪动,即是人们常说的无刷新更新页面。 这可能是实现一个Asp.Net Ajax的最简单范例了。但是它的问题是什么呢?当我们点击Button的时候,在服务端执行了一个完整的Asp.Net 页面生命周期,和你不使用UpdatePanel更新页面没有任何的区别。可以做一个测试,在页面在拖放一个Label,ID为Label2,然后在 Page_Load中写入下面代码:

protected void Page_Load(object sender, EventArgs e) {
    if (!IsPostBack) {
        Label2.Text = DateTime.Now.ToLongTimeString();
    } else {
        Label2.Text = DateTime.Now.ToLongTimeString();
    }
}

然后在Page_Load一行设置断点,接下来运行调试,会发现每次你点击Button按钮的时候,都会运行else{...}中的语句,说明每次页面都会执行Page_Load方法。这说明使用这种方式时,服务器端的开销是比较大的。这 里还可以发现一个有趣的现象,尽管服务器执行了为Label2.Text赋值的语句,但是页面上Label2却并没有更新。如果想要更新它,那么需要将它 也放置到UpdatePanel中,这里我们可以在页面上重新拖放一个UpdatePanel,然后把Label2放置进去。然后我们再点击 Button,会发现Label1和Label2都进行了更新。这里又引出了一个有趣的问题:回想一下前面,我们只为第一个UpdatePanel设置了Triggers节点,而并没有为后来新添的UpdatePanel设置Triggers节点,但是对一个UpdatePanel的更新也会影响到另一个。 有的时候这种情况是我们所需要的,但更多时候不是,我们可能希望对于Label2的更新由其他控件的其他事件触发。此时,可以将第二个 UpdatePanel的UpdateMode属性设为“Conditional”,就避免了受到其他UpdatePanel提交的影响,这个值默认为 “Always”。

下面是此时Aspx页面的代码:





UpdateMode="Conditional">
 
     
 

以上这些就是一种最常见的Asp.Net Ajax开发模式了,我们看到它如何实现,也看到了它的缺陷:每次客户端的操作,都会在服务端执行一次完整的页面生命周期,加重了服务器的负担,同时客户端和服务端的通信过程中也会传递完整的http协议内容,增大了网络流量。我们也应该看到它的优点:实现起来非常的简单,操作上基本等同于通常的Asp.Net开发,所使用的控件也为Asp.Net服务器控件(Server Control,这里相对于HTML控件而言)

Asp.Net Ajax - Web Service模式

还有一种方式就是Web Service模式了,客户端不再提交页面,而只是发送Web Service请求,并对收到应答进行处理。由于这里采用了异步方式,所以客户端在发送WebService请求之后无需等待。采用这种方式服务端不会执 行生命周期,往返的数据量也减到了最小。但缺点就是需要手动编写一些代码。我们来一步步看下如何完成,因为WCF是下一代Windows平台通信的基础, 集成了Web Service和Remoting这两大技术,所以我们采用WCF来创建服务。

首先选择“添加新项”,然后选择“启用了AJAX的WCF服务”,输入名称SimpleService,这样会在站点中添加一个 SimpleService.svc文件,在App_Code中创建一个SimpleService.cs,还会在Web.Config中添加相关的配 置。我们只需要改动一下App_Code中的SimpleService.cs下的代码:

public class SimpleService
{
    [OperationContract]
    public string GetCurrentDate(string clientValue) {
        string rtn = "Server Time :" + DateTime.Now.ToLongTimeString() + "
"
;
        rtn += "Client Value(round trip): " + clientValue;
        return rtn;
    }
}

SimpleService还用一些特性修饰了,我将它取消掉了以节省空间。方法接受一个字符串clientValue,然后获取服务器时间,最后 返回clientValue。这段代码看上去没有什么特别之处,但是注意到我在Client Value旁加了一个括号,写着“round trip”,对于Ajax程序来说,这个值由客户端发送,最后再返还给客户端,进行了一趟由客户端到服务端,再到客户端的周游。

为了要让javascript可以调用这个Web服务,我们需要在aspx页面中对它进行注册,拖放也一个ScriptManager到页面上,然后向下面这样进行设置:


   
       
   

接下来我们新建一个Pattern2.aspx页面,在上面拖放三个HTML标记。一个span,一个input(Button),一个 input(Text)。注意,是客户端HTML标记,不含有runat="server"的,从这里已经可以看到一个很大的不同,我们使用的是客户端 HTML控件。接着在input(Button)上双击,会自动生成Javascript脚本,此时aspx页面的主要代码如下:


         
[未设置]



最关键是接下来要编写的javascript代码,我先将它贴出来,然后再进行解释:

首先看这个Button1_onclick()方法,我们先声明了一个context,这个content很类似于在C#的委托变量上调用 BeginInvoke()方法时的最后一个参数,这个值用于传递给回调函数,以方便进行一些处理。接着我们获取了input(Text)中输入的值,保 存在了clientValue中。然后调用了Web服务,其中第一个参数就是上面定义的GetCurrentDate()时的参数,我们传入了 clientValue,第二个参数是OnComplete是成功时的回调函数,第三个是OnFailed是失败时的回调函数,最后一个参数我们传递了 context,可以将它交由回调函数处理。因为是异步操作,所以没有在这里获取GetCurrentDate()方法返回值,而是通过回调函数 OnComplete的参数对返回值进行了传递。

接下来看OnComplete()函数,其实我们最需要搞清楚的就是它的两个参数:第一个args即为Web服务方法 GetCurrentDate()的返回值;而第二个参数,即为调用GetCurrentDate()时传递的最后一个参数。在方法内部,我们使用 alert()显示了context的值,随后将Web服务的返回值显示在了span中。OnFailed()仅仅是提示用户更新失败。

接下来在页面上点击Button,应该可以看到下面的效果:

我们再次在Page_Load的位置设置断点,然后启动调试,会发现当我们使用这种方式时,点击Button服务端并没有再次执行页面生命周期,而 参与客户端/服务端往返的数据也是最少量的(仅往返必需数据),因此,虽然采用这种方式我们需要编写一定量的javascript代码,但是却能够显著地 提高站点的性能。

总结

这篇文章简单的讲述了使用Asp.Net Ajax进行开发时常见的两种方式,使用UpdatePanel + 服务器控件;或者是使用 Web Service + HTML标记 + Javascript,并且这两种方式的实现方式和效果做了简要的说明。

感谢阅读,希望这篇文章能给你带来帮助!

 

 

引言

Http 请求处理流程 和 Http Handler 介绍 这两篇文章里,我们首先了解了Http请求在服务器端的处理流程,随后我们知道Http请求最终会由实现了IHttpHandler接口的类进行处理(应该记得Page类实现了IHttpHandler)。从 Http 请求处理流程 一文的最后的一幅图中可以看到,在Http请求由IHttpHandler处理之前,它需要通过一系列的Http Module;在请求处理之后,它需要再次通过一系列的Http Module,那么这些Http Module是如何组成的?用来做什么呢?本文将对Http Module作以介绍。

Http Module概述

暂时先不考虑我们自己实现Http Module的情况。在.Net中,Http Module 是实现了IHttpModule接口的程序集。IHttpModule 接口本身并没有什么好大写特写的,由它的名字可以看出,它不过是一个普普通通的接口而已。实际上,我们关心的是实现了这些接口的类,如果我们也编写代码实 现了这个接口,那么有什么用途。一般来说,我们可以将Asp.Net中的事件分成三个级别,最顶层是 应用程序级事件、其次是页面级事件、最下面是控件级事件,事件的触发分别与 应用程序周期、页面周期、控件周期紧密相关。而 Http Module 的作用是与应用程序事件 密切相关的。

我们通过Http Module在Http请求管道(Pipeline)中注册期望对应用程序事件做出反应的方法,在相应的事件触发的时候(比如说BeginRequest 事件,它在应用程序收到一个Http请求并即将对其进行处理时触发),便会调用Http Module注册了的方法,实际的工作在这些方法中执行。.Net 本身已经有很多的Http Module,其中包括 表单验证Module(FormsAuthenticationModule), Session 状态Module(SessionStateModule),输出缓存Module (OutputCacheModule)等。

注册 Http Module

在注册我们自己编写的 Http Module 之前,先来看看Asp.Net中已经有的HttpModule。与 Http Handler类似,我们需要打开机器上C:\WINDOWS\Microsoft.NET\Framework\ v2.0.50727\CONFIG 目录下的 web.config 文件。找到 结点,应该可以看到下面的内容:


   
   
   
   
   
   
   
... 略

我们先从结点上看,type属性与上一节所说的http handler结点的type属性类似,都代表了相应的程序集。但是,与http handler 不同,module只提供了一个name属性,没有诸如 path这样指定某一特定(或者用通配符 * 代表某一种类)文件的处理程序。这是与Module的特点相关的,我们知道 module 是响应应用程序周期中触发的事件,对于所有提交到aspnet_isapi.dll的请求都一样,即便请求只是像类似 http://www.tracefact.net/images/logo.gif 这样获取一张图片而已(对ISAPI进行过设置以后,默认aspnet_isapi.dll不接手图片文件)。

与Http handler类似,在这册我们自己的http module 时,假设类名为ModuleDemo,位于myNameSpace命名空间下,程序集名称为myDll,我们只需将myDll.dll拷贝到Bin目录 下,并在站点的 web.config 文件 system.web 结点下创建 httpModules 结点:


   
      
   

type属性由分号“,”分为两部分,前面是命名空间及类名,也就是类型名;后面是程序集名。如果我们将代码创建在App_Code目录中,则不需要再指定程序集名。

name属性由我们自己命名,不一定与类名相同,此处我将它命名为“CustomModuleName”。我们可以通过应用程序 (HttpApplication)的Modules属性获取HttpModuleCollection集合,然后通过name属性,进一步获取 HttpModule对象。

通过name属性,我们还可以在global.asax中文件中编写自定义HttpModule暴露出的事件的处理程序,它采用的格式是:void ModuleName_EventName(object sender, EventArgs e)。我们将在后面做更详细介绍。

Asp.Net 内置的 Http Modules

下面这张表格列出了C:\WINDOWS\Microsoft.NET\Framework\ v2.0.50727\CONFIG下的Web.Config中的 Asp.Net 内置的Http Modules 及其主要作用。

名称 类型 功能
OutputCache System.Web.Caching.OutputCacheModule 页面级输出缓存
Session System.Web.SessionState.SessionStateModule Session状态管理
WindowsAuthentication System.Web.Security.WindowsAuthenticationModule 用集成Windows身份验证进行客户端验证
FormsAuthentication System.Web.Security.FormsAuthenticationModule 用基于Cookie的窗体身份验证进行客户端身份验证
PassportAuthentication System.Web.Security.PassportAuthenticationModule 用MS护照进行客户身份验证
RoleManager System.Web.Security.RoleManagerModule 管理当前用户角色
UrlAuthorization System.Web.Security.UrlAuthorizationModule 判断用户是否被授权访问某一URL
FileAuthorization System.Web.Security.FileAuthorizationModule 判断用户是否被授权访问某一资源
AnonymousIdentification System.Web.Security.AnonymousIdentificationModule 管理Asp.Net应用程序中的匿名访问
Profile System.Web.Profile.ProfileModule 管理用户档案文件的创立 及相关事件
ErrorHandlerModule System.Web.Mobile.ErrorHandlerModule 捕捉异常,格式化错误提示字符,传递给客户端程序

我们将在后面用编程的方式来查看它。

IHttpModule接口

看了这么多理论知识,本节将开始动手写点程序,实现自己的Http Module。我们首先需要看下IHttpModule 接口,它包括下面两个方法:

public void Init(HttpApplication context);
public void Dispose();

Init():这个方法接受一个HttpApplication对象,HttpApplication代表了当前的应用程序,我们需 要在这个方法内注册 HttpApplication对象暴露给客户端的事件。可见,这个方法仅仅是用来对事件进行注册,而实际的事件处理程序,需要我们另外写方法。

整个过程很好理解:

  1. 当站点第一个资源被访问的时候,Asp.Net会创建HttpApplication类的实例,它代表着站点应用程序,同时会创建所有在Web.Config中注册过的Module实例。
  2. 在创建Module实例的时候会调用Module的Init()方法。
  3. 在Init()方法内,对想要作出响应的HttpApplication暴露出的事件进行注册。(仅仅进行方法的简单注册,实际的方法需要另写)。
  4. HttpApplication在其应用程序周期中触发各类事件。
  5. 触发事件的时候调用Module在其Init()方法中注册过的方法。

NOTE:如果你不了解事件注册等相关内容,请参阅 C#中的委托与事件 一文。

Dispose():它可以在进行垃圾回收之前进行一些清理工作。

综上所述:实现一个 IHttpModule 的模板一般是这样的:

public class ModuleDemo:IHttpModule
{
    public void Init(HttpApplication context) {
       // 注册HttpApplication应用程序 BeginRequest 事件
       // 也可以是其他任何HttpApplication暴露出的事件
       context.BeginRequest += new EventHandler(context_BeginRequest);
    }

    void context_BeginRequest(object sender, EventArgs e) {
       HttpApplication application = (HttpApplication)sender;
       HttpContext context = application.Context;
       // 做些实际的工作,HttpContext对象都获得了,剩下的基本可以自由发挥了
    }

    public void Dispose() {
    }
}

通过Http Module向Http请求输出流中写入文字

本例中,我们仅用BeginRequest事件和 EndRequest 事件对 Http Module 的使用作以说明。我们通过这个范例,了解 Http Module 基本的使用方法。

首先,请创建一个新的站点,在App_Code目录中添加类文件: ModuleDemo.cs:

public class ModuleDemo:IHttpModule
{
    // Init方法仅用于给期望的事件注册方法
    public void Init(HttpApplication context) {
       context.BeginRequest += new EventHandler(context_BeginRequest);
       context.EndRequest += new EventHandler(context_EndRequest);
    }

    // 处理BeginRequest 事件的实际代码
    void context_BeginRequest(object sender, EventArgs e) {
       HttpApplication application = (HttpApplication)sender;
       HttpContext context = application.Context;
       context.Response.Write("

来自HttpModule 的处理,请求到达


");
    }

    // 处理EndRequest 事件的实际代码
    void context_EndRequest(object sender, EventArgs e) {
       HttpApplication application = (HttpApplication)sender;
       HttpContext context = application.Context;
       context.Response.Write("

来自HttpModule的处理,请求结束

"
);
    }
      
    public void Dispose() {
    }
}

上面的代码很简单,它注册了 HttpApplication实例的 BeginRequest 事件 和 EndRequest事件,事件处理方法的作用仅仅是在http请求开始和结束的时候,给http请求的输入流中分别写入不同的内容。

接下来在 Web.config 的 System.web 结点中写入以下内容:


   
      
   

然后,打开建立站点时自动创建的 Default.aspx文件,在里面打几个字,为了做区分,我输入的是:位于.aspx页面上的文字。然后,我们在浏览器中打开它,应该会看到像这样:

然后我们再新建一个 Default2.aspx,在浏览器中浏览,可以看到,两个页面的效果相同。这说明对于不同的两个文件,http Module都起了作用,可见它确实是位于应用程序级,而非页面级。

现在,我们再打开站点中的一张图片文件,发现显示出的是一个红叉叉,为什呢?因为Http Module 针对是http 请求,而不是某个或某一类文件,所以当请求一张图片的时候,我们编写的http Module依然会起作用,将文字插入到二进制图片中,破坏了文件格式,自然只能显示红叉叉了。

NOTE:如果你发现你的图片显示正常,请不要惊讶,事情是这样的:回想一下第一节我们讨论到的,对于图片文件,由IIS直接处理,并不会交由aspnet_isapi.dll,所以,Module无法捕获对于图片类型文件的请求。解决方法就是在IIS中进行设置一下。
    这里需要提请注意的是:如果你使用Vs2005自带的Local Server,那么你无需对IIS进行设置,所有的不论图片还是任何文件类型,都会交由aspnet_isapi.dll处理。

遍历Http Module集合

现在,我们通过遍历 HttpModuleCollection 集合来查看注册给应用程序的所有 Http Module 的名称。

新建一个文件 RegisteredModules.aspx,在代码后置文件中添加如下方法:

private string ShowModules() {
    HttpApplication app = Context.ApplicationInstance; //获取当前上下文的HttpApplication环境
    HttpModuleCollection moduleCollection = app.Modules; //获取所有Module集合

    // 获取所有的 Module 名称
    string[] moduleNames = moduleCollection.AllKeys;

    System.Text.StringBuilder results = new System.Text.StringBuilder();    //遍历结果集

    foreach (string name in moduleNames) {
       // 获得Module名称
       results.Append("名称:" + name + "
"
);
        // 获得Module类型
       results.Append("类型:" + moduleCollection[name].ToString() + "
"
);
    }

    return results.ToString();
}

然后在Page_Load方法中输出一下:

protected void Page_Load(object sender, EventArgs e)
{
    Response.Write(ShowModules());
}

我们应该可以看到下面这样的画面:

与之前列出的那张表格比较一下,可以看出是几乎完全一致的(多了一个DefaultAuthentication)。另外注意上图的倒数第四行,那不是我们自己定义的Module么?name为MyModule,类型为ModuleDemo。

Global.asax文件与 Http Module

早在asp时代,大家就知道这个文件了。它主要用于放置对于 应用程序事件或者 Session事件的响应程序。大家熟悉的有Application_Start、Application_End、Session_Start、Session_End 等。

在asp.net中,Glabal不仅可以注册应用程序和Session事件,还可以注册Http Module暴露出的事件;不仅可以注册系统Module的事件,也可以注册我们自己义的Module暴露出的事件。在具体介绍之前,这里需要首先注意两点:

  1. 在每处理一个Http请求时,应用程序事件都会触发一遍,但是Application_Start和 Application_End 例外,它仅在第一个资源文件被访问时被触发。
  2. Http Module无法注册和响应Session事件,对于Session_Start 和 Session_End,只能通过Glabal.asax来处理。

好了,我们现在修改之前 ModuleDemo 范例程序,给它像下面这样给它添加一个事件(为了使程序简洁一些,我做了简化):

public class ModuleDemo : IHttpModule {

    // 声明一个事件
    public event EventHandler ExposedEvent;

    // Init方法仅用于给期望的事件注册方法
    public void Init(HttpApplication context) {
       context.BeginRequest += new EventHandler(context_BeginRequest);
    }

    // 处理BeginRequest 事件的实际代码
    void context_BeginRequest(object sender, EventArgs e) {
       HttpApplication application = (HttpApplication)sender;
       HttpContext context = application.Context;
       context.Response.Write("

来自HttpModule的处理,请求到达


");
      
       OnExposedEvent(new EventArgs()); // 调用方法
    }

    protected override void OnExposedEvent(EventArgs e) {
       if (ExposedEvent != null) // 如果Global中有注册
           ExposedEvent(this, e);   // 调用注册了的方法
    }
   
    public void Dispose() {
    }
}

接下来,我们在站点中创建一个 Global.asax 文件,在里面添加如下代码,注意到格式是:void 模块名_事件名(object sender, EventArgs e)。

void MyModule_ExposedEvent(object sender, EventArgs e)
{
     Response.Write("

来自 Global.asax 的文字

");
}

现在,我们打开之前的页面,应该可以见到这样,可见,我们成功的将 Glabal.asax文件与我们自己定义的Http Module所暴露出的事件 ExposedEvent 联系到了一起:

总结

本文简单地介绍了什么是Http Module。我们首先了解了Http Module的作用,然后查看了Asp.Net 内置的Module,接着我们介绍了IHttpModule接口,并通过了一个简单的范例实现了此接口,最后我们讨论了 Http Module与 Global.asax 文件的联系。

本文仅仅是对IHttpModule作以简单介绍,对其更多的实际应用,会在后续文章中补充。

希望这篇文章能给你带来帮助!

posted @ 2009-08-03 11:12 高天乐 阅读(20) 评论(0) 编辑


 

Http Handler 介绍
 
引言

在 Part.1 Http请求处理流程 一文中,我们了解了Http请求的处理过程以及其它一些运作原理。我们知道Http管道中有两个可用接口,一个是IHttpHandler,一个是 IHttpModule,但在Part.1中,我并没有详细讲述如何对它们进行编程,只是轻描淡写地一笔带过。所谓学以致用,前面已经介绍了不少概念和原 理。在本文中,我们通过几个范例来了解 IHttpHandler,看看掌握这些原理的实际用途。

IHttpHandler 概述

可能和我一样,很多Asp.Net开发人员都有过Asp的背景,以至于我们在开发程序的时候,通常都是在“页面级”上思考,也就是说我们现在正在做 的这个页面应该有什么样的功能,是进行一个问卷调查还是一个数据库查询等等。而很少在“请求级”思考,考虑有没有办法来通过编码的方式来操控一个Http 请求。

实际上,Framework提供了一系列的接口和类,允许你对于Http请求进行编程,而实现这一操作的一个主要的接口,就是 IHttpHandler(另一个是IHttpModule)。

应该还记得第一节中我们提到过 ISAPI,它根据文件名后缀把不同的请求转交给不同的处理程序。但是仔细看看就会发现:几乎一大半的文件都交给 aspnet_isapi.dll 去处理了。很明显,aspnet_isapi.dll 不可能对每种文件采用同一种方式处理,那么 aspnet_isapi.dll 是如何更进一步处理不同的文件,交由谁去处理呢?为了搞清楚这个问题,我们需要打开机器上C:\WINDOWS\Microsoft.NET \Framework\v2.0.50727\CONFIG\ 目录下的web.config 文件。

NOTE:我查阅了很多资料,都说是在 machine.config 中,但实际上 v2.0.50727 下的machine.config中httpHandlers结点是这样的:,并没有给出详细的处理程序,在Web.config中才能看到。而v1.1.4322 下的machine.config中却有。

找到httpHandlers结点,应该可以看到如下这样的代码(做了省略):


... ... //略

  
  

  
  
  
   ... ... //略

可以看到,在结点中将不同的文件类型映射给不同的Handler去处理,对于.aspx来说,是由 System.Web.UI.PageHandlerFactory来处理。而对于.cs来说,是由 System.Web.HttpForbiddenHandler 处理,从ForbiddenHandler名字中出现的Forbidden (翻译过来是“禁止”)可以看出,这个Handler可以避免我们的源码被看到。

NOTE:System.Web.UI.PageHandlerFactory 是一个IHttpHandlerFactory,而不是一个单一的HttpHandler,IHttpHandlerFactory用来做什么后面会说明。

上面列出的是.Net Framework在处理Http请求时的所采用的默认Handler。而如果我们要用编程的方式来操控一个Http请求,我们就需要实现IHttpHandler接口,来定制我们自己的需求。

IHttpHandler的定义是这样的:

public interface IHttpHandler{
    void ProcessRequest(HttpContext context);
    bool IsReusable { get; }
}

由上面可以看出IHttpHandler要求实现一个方法和一个属性。其中 ProcessRequest,从名字(处理请求)看就知道这里应该放置我们处理请求的主要代码。

IsReusable属性,MSDN上是这样解释的:获取一个值,该值指示其他请求是否可以使用 IHttpHandler 实例。也就是说后继的Http请求是不是可以继续使用实现了该接口的类的实例,一般来说,我把它设置成true。

那么实现此接口的类形式应该是这样的:

public class CustomHandler : IHttpHandler{
    public void ProcessRequest(HttpContext context)  {
       // 处理请求的代码
    }
    public bool IsReusable {
       get { return true; }
    }
}

而为了能使用这个自定义的HttpHandler,我们需要在应用程序目录下的Web.config中注册它。

 
   
     
   

 

应该发现这与之前在C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\目录下 web.config中看到的几乎完全一样。这里,path指的是请求的文件名称,可以使用通配符扩大范围,也可以明确指定这个handler仅用于处理 某个特定的文件(比如说:filename.aspx)的请求。verb指的是请求此文件的方式,可以是post或get,用*代表所有访问方式。 type属性由“,”分隔成两部分,第一部分是实现了接口的类名,第二部分是位于Bin目录下的编译过的程序集名称。

NOTE:如果你新建一个项目,并且在项目下创建HandlerTest.cs,然后让站点引用该项目,那么在生成解决方案的时候会自动将编译好的.dll文件添到Bin目录中。
    NOTE:MyDll只写程序集名,不要加后面的.dll。

使用HttpHandler实现图片防盗链

有了之前这么多的准备知识,实现现在的目标就容易得多了:

NOTE:这个例子,以及下面的一个例子均来自于《Maximizing ASP.NET Real World, Object-Oriented Development》一书:

Step.1:创建文件 CustomHandler.cs,代码如下:

using System;
using System.Web;

namespace CustomHandler{
    public class JpgHandler : IHttpHandler{
       public void ProcessRequest(HttpContext context){
           // 获取文件服务器端物理路径
           string FileName = context.Server.MapPath(context.Request.FilePath);
           // 如果UrlReferrer为空,则显示一张默认的禁止盗链的图片
           if (context.Request.UrlReferrer.Host == null){
              context.Response.ContentType = "image/JPEG";
              context.Response.WriteFile("/error.jpg");
           }else{
              // 如果 UrlReferrer中不包含自己站点主机域名,则显示一张默认的禁止盗链的图片
             if (context.Request.UrlReferrer.Host.IndexOf("yourdomain.com") > 0){
                  context.Response.ContentType = "image/JPEG";
                  context.Response.WriteFile(FileName);
              }else{
                  context.Response.ContentType = "image/JPEG";
                  context.Response.WriteFile("/error.jpg");
              }
           }
       }

       public bool IsReusable{
           get{ return true; }
       }
    }
}

Step.2 编译这个文件

csc /t:library /r:System.Web.dll CustomHandler.cs

Step.3 将编译好的 CustomHandler.dll 拷贝到站点的 Bin 目录下。

Step.4 在Web.Config 中注册这个Handler。


   
     
   

 

OK,诸位可以按步骤自行测试一下,这里就不赘述了。

通过IhttpHandler实现图片验证码

也可以在一个.ashx文件中实现IHttpHandler,而不是采用这种提前编译的方式。

Step.1 打开Vs2005,“添加新项”,“一般处理程序”。新建文件后,VS会自动在文件中添加如下的代码:

<%@ WebHandler Language="C#" Class="Handler" %>

using System;
using System.Web;

public class Handler : IHttpHandler {
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";
        context.Response.Write("Hello World");
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }
}

Step.2 将代码改写成如下所示:

<%@ WebHandler Language="C#" Class="Handler" %>

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Text;
using System.Web;
using System.Web.SessionState;

public class Handler : IHttpHandler, IRequiresSessionState {

    public void ProcessRequest(HttpContext context) {
       context.Response.ContentType = "image/gif";
       //建立Bitmap对象,绘图
       Bitmap basemap = new Bitmap(200, 60);
       Graphics graph = Graphics.FromImage(basemap);
       graph.FillRectangle(new SolidBrush(Color.White), 0, 0, 200, 60);
       Font font = new Font(FontFamily.GenericSerif, 48, FontStyle.Bold, GraphicsUnit.Pixel);
       Random r = new Random();
       string letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ";
       string letter;
       StringBuilder s = new StringBuilder();
      
       //添加随机的五个字母
       for (int x = 0; x < 5; x++) {
           letter = letters.Substring(r.Next(0, letters.Length - 1), 1);
           s.Append(letter);
           graph.DrawString(letter, font, new SolidBrush(Color.Black), x * 38, r.Next(0, 15));
       }
      
       //混淆背景
       Pen linePen = new Pen(new SolidBrush(Color.Black), 2);
       for (int x = 0; x < 6; x++)
           graph.DrawLine(linePen, new Point(r.Next(0, 199), r.Next(0, 59)), new Point(r.Next(0, 199), r.Next(0, 59)));
             
       //将图片保存到输出流中      
       basemap.Save(context.Response.OutputStream, ImageFormat.Gif);
       context.Session["CheckCode"] = s.ToString();   //如果没有实现IRequiresSessionState,则这里会出错,也无法生成图片
       context.Response.End();     
    }

    public bool IsReusable {
       get { return true; }
    }
}

需要特别注意的是,Handler类不仅需要实现 IHttpHandler接口(这个显然),为了在这个Handler类中使用SessionState,还需要实现 IRequiresSessionState接口,对于这个接口,MSDN的解释是这样的:Specifies that the target HTTP handler requires read and write access to session-state values. This is a marker interface and has no methods.(翻译过来是:指定当前Http Handler需要对SessionState值的读写访问权。这是一个标记接口,没有任何方法)。

而实际上,IRequiresSessionState的接口定义是这样的:

public interface IRequiresSessionState{}

可见,这个接口没有任何需要实现的方法或属性,大家只要记得:如果想在HttpHandler中使用SessionState,必须实现这个接口,实际上也就是在类的标头将这个接口加进去。

Step.3 新建一个ImageCode.aspx页面,在HTML代码中写下:

图片验证码

OK,在浏览器中打开ImageCode.aspx,应该可以看到如下所示:

利用HttpHandler创建自定义后缀Rss源

RSS如今已经可以说是随处可见,而RSS的实现方式,通常是在一个.aspx的CodeBehind文件中写一个XML文件,然后加载到 Response的OutputStream中, Rss源通常是Rss.aspx这种形式的。通过第一章学到的ISAPI的知识,再结合本章学到的关于HttpHandler的知识,很容易想到:我们可 以自定一个以 .rss 作为后缀名的文件来实现 Rss 源,比如说Article.rss。现在我们就一步步来实现它:

NOTE:关于RSS的更多内容,可以参阅我编译的 在Web站点中创建和使用RSS源。本文不再解释Rss是什么,如何创建Rss源,为了文章的独立性,仅给出创建过程。

Step.1 创建范例数据库

Create Table RssSample
(
    SampleId      Int Identity(1,1)    Not Null,
    Title         Varchar(100)          Not Null Constraint uq_Title Unique,
    Author        Varchar(50)              Not Null,
    PubDate       DateTime              Not Null Default GetDate(),
    [Description] Varchar(500)          Not Null,
    Link          Varchar(150)          Not Null

    Constraint pk_RssSample Primary Key(SampleId)
)
-- 插入范例数据
Insert Into RssSample(Title, Author, [Description], Link)
Values('标题1', '作者1', '文章摘要1', 'http://127.0.0.1/#' )

-- 省略 ....

Step.2 建立站点,在App_Code目录下建立RssFeedsLib.cs文件。

using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Web;
using System.Xml;
using System.Text;

namespace RssFeadsLib {
    public class RssGenerator {
       public static string GetRSS() {
           MemoryStream ms = new MemoryStream();
           XmlTextWriter writer = new XmlTextWriter(ms, null);
           SqlConnection conn = new SqlConnection("Data Source=.;Initial Catalog=Sample;User ID=sa;Password=sa");       //修改这里成你的数据库连接
           SqlCommand cmd = new SqlCommand("select * from RssSample order by pubdate desc", conn);

           conn.Open();
           SqlDataReader reader = cmd.ExecuteReader();
           writer.WriteStartElement("rss");
           writer.WriteAttributeString("version", "2.0");
           writer.WriteStartElement("channel");
           // Channel 下的结点静态写入
           writer.WriteElementString("title", "TraceFact.Net 技术文章");
           writer.WriteElementString("link", "http://www.tracefact.net");
           writer.WriteElementString("description", "Dedicated to asp.net...");
           writer.WriteElementString("copyright", "Copyright (C) 2007");
           writer.WriteElementString("generator", "My RSS Generator");
           // Item 结点从数据库读取
           while (reader.Read()) {
              writer.WriteStartElement("item");
              writer.WriteElementString("author", reader.GetString(reader.GetOrdinal("Author")));
              writer.WriteElementString("title",             reader.GetString(reader.GetOrdinal("title")));
              writer.WriteElementString("link", reader.GetString(reader.GetOrdinal("Link")));
              writer.WriteElementString("description", reader.GetString(reader.GetOrdinal("Description")));
              writer.WriteElementString("pubDate", reader.GetDateTime(reader.GetOrdinal("PubDate")).ToString(@"ddd, dd MMM yyyy 12:00:00 tt "));
              writer.WriteEndElement();
           }

           writer.WriteEndElement();
           writer.WriteEndElement();
           reader.Close();
           conn.Close();

           writer.BaseStream.Flush();
           writer.Flush();
           ms.Flush();

           // 将流转换成String并返回
           byte[] data = new byte[ms.Length];
           ms.Seek(0, SeekOrigin.Begin);
           ms.Read(data, 0, data.Length);
           ms.Close();
           return UTF8Encoding.UTF8.GetString(data);
       }
    }
}

Step.3 创建可以处理 .rss 后缀名的 RssHandler

我们在这个 RssFeedsLib命名空间下,再添加一个类,这个类用于处理对 .rss 后缀名文件的Http请求。

public class RSSHandler:IHttpHandler{
    public bool IsReusable
    {
       get {return false;}
    }

    public void ProcessRequest(HttpContext context){
       context.Response.ContentType = "text/xml";
       string str = RssGenerator.GetRSS();
       context.Response.Write(str);
    }
}

Step.4 在Web.config中进行配置


   

 

NOTE:因为这个类和命名空间位于App_Code中,这里就不需要再手动编译 RssFeadsLib.cs然后将编译好的.dll应用程序集放到Bin目录中了。至于为什么可以这样,将会在 《Asp.Net 构架与安全机制 Part.5 – 页面生存周期与编译模型》中解释。

Step.5 在IIS 对ISAPI进行设置。

应该还记得在Part.1中如何在IIS中设置ISAPI来进行文件与处理程序映射:

  1. 打开IIS,选择本范例所用的站点,右键,选择“属性”。
  2. 选择“主目录”选项卡,点击“配置...”按钮。
  3. 点击“添加”,设置“可执行文件”为“C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll”,设置“扩展名”为“.rss”,点“确定”。
  4. 注意,不要勾选“检查文件是否存在”复选框,这样不用创建文件,只要在地址栏输入任意以.rss后缀结尾的文件名,均会交由上面创建的Handler去处理,而不管这个文件是否存在,也不管请求的是Article.rss还是Sample.rss。

进行了这些设置以后,现在IIS就知道如何去处理对.rss后缀名文件的请求了。

Step.6 测试范例

这个时候,随便打开一个页面,比如空白的Default.aspx,然后我们在地址栏将文件改为:Article.rss(改成abc.rss也是一样),敲回车,应该可以看到如下的画面。

IHttpHandlerFactory 概述

现在假设我们有这样的需求,我们不仅想要处理 .rss 后缀名,还想要能够处理 .atom后缀名,假设处理atom的类命名为AtomHandler,那么我们的Web.config该如何设置呢?我想应该是这样的:




如果我们有很多个HttpHandler分别映射不同后缀名的请求,这样我们的Web.config会变得很冗长,或者,我们只有在程序运行时才能确切地知道使用哪个Handler,这个时候,可以考虑实现 IHttpHandlerFactory来完成这一过程。

IHttpHandlerFactory的定义是这样的:

public interface IHttpHandlerFactory{
    IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated);
    void ReleaseHandler(IHttpHandler handler);
}

可见,需要实现两个方法,分别是 GetHandler() 和 ReleaseHandler()。

  • GetHandler(),返回实现了IHttpHandler接口的类的实例。
  • ReleaseHandler(),使得Factory可以重复使用一个已经存在的Handler实例。

对于上面 .atom 和 .rss 的问题,我们可以这样来实现 IHttpHandlerFactory接口:

class HandlerFactory:IHttpHandlerFactory{
    public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated){
       string path = context.Request.PhysicalPath;
       if (Path.GetExtension(path) == ".rss"){
           return new RSSHandler();
       }

       if (Path.GetExtension(path) == ".atom"){
           return new ATOMHandler();
       }
       return null;
    }

    public void ReleaseHandler(IHttpHandler handler){
    }
}

这时,在Web.Config 中节点下进行如下设置即可:



但是,这不能简化IIS中ISAPI的设置,还是需要手动去对.rss和.atom分别设置。

总结

在本文中,我们首先讨论了aspnet_isapi.dll 如何将对不同后缀名文件的请求分发给相应的处理程序,如何查看Framework默认的处理程序Handler。

然后,我们通过三个实例,图片防盗链、图片验证码、处理自定义后缀名请求,详细讲解了IHttpHandler的实现方法和使用过程。

最后,我向大家概要地介绍了IHttpHandlerFactory接口。

 

Http 请求处理流程

引言

我查阅过不少Asp.Net的书籍,发现大多数作者都是站在一个比较高的层次上讲解Asp.Net。他们耐心、细致地告诉你如何一步步拖放控件、设置控件属性、编写CodeBehind代码,以实现某个特定的功能。

这种做法,实际上是回答了“如何去做”的问题,却没有回答“为什么可以这样做”的问题。

尽管我很推崇 悉江华 先生的《圣殿祭祀的Asp.Net开发详解》一书,但当我翻看了一下其对角色(Role) 和 用户(Member)的讲解时,我决定跳过去直接读后面的章节。因为我发现他也随了大流,对这部分的讲解停留在“如何去做”的层面上。我相信像悉先生 这样的牛人是不可能不了解底层运作原理的,仅仅是因为那本书原本就已经很厚了吧。

当你按“如何去做”所讲解的内容去开发程序的时候,对于你的用户,你仍是一名程序员;但对于实现了MembershipProvider 和 RoleProvider 抽象类的微软开发人员来说,你已经成了他们的一个用户。

NOTE:我既不反对一些作者只讲解“如何去做”,也不反对你只学“如何去做”,这样也有它的好处,就是可以快速开发。我只是建议多掌握一点底层知识,对一些问题会有更好的理解。

希望通过这一系列文章的讲解,可以让你更好的理解Asp.Net的运作原理和做以了解。

Http请求处理流程概述

思考“为什么在地址栏输入www.tracefact.net就可以看到张子阳的个人空间?”,类似于思考“为什么苹果是往地上掉不是往天上 飘?”。对于普通访问者来说,这就像每天太阳东边升起西边落下一样是理所当然的;对于很多程序员来说,认为这个与己无关,不过是系统管理员或者网管员的责 任。毕竟,IIS是 Windows 的一个组件,又不是 Asp.Net 的一个组成部分。而实际上,从你轻拍回车到页面呈现在你眼前的十分之一秒内,IIS和.Net Framework已经做了大量的幕后工作。

你可能觉得了解这些幕后工作是如何运作的无关紧要,作为程序员的你只要保证开发出的程序可以高效地运行就可以了。然而,在开发过程中,你却发现常常 需要使用诸如 HttpContext 这样的类。这个时候,你可曾思考过这些类的构成和类的实体是如何创建的?你可能简单地回答:HttpContext代表当前请求的一个上下文环境。可你又 知道IIS 、Framework、Asp.Net 是如何协同工作处理每个Http请求、如何区分不同的请求、IIS、Framework、Asp.Net三者之间的数据如何流动么?

回答上面这些问题,首先需要了解IIS是如何处理页面请求的,这也是理解 Form验证模式和Windows 验证模式 的基础。

Http请求刚刚到达服务器的时候

当服务器接收到一个 Http请求的时候,IIS 首先需要决定如何去处理这个请求(NOTE:服务器处理一个.htm页面和一个.aspx页面肯定是不一样的么)。那IIS依据什么去处理呢?―― 根据文件的后缀名。

服务器获取所请求的页面(NOTE:也可以是文件,比如 jimmy.jpg)的后缀名以后,接下来会在服务器端寻找可以处理这类后缀名的应用程序,如果IIS找不到可以处理此类文件的应用程序,并且这个文件也没有受到服务器端的保护(NOTE:一个受保护的例子就是 App_Code中的文件,一个不受保护的例子就是你的js脚本),那么IIS将直接把这个文件返还给客户端。

能够处理各种后缀名的应用程序,通常被称为 ISAPI 应用程序(NOTE:Internet Server Application Programe Interface,互联网服务器应用程序接口)。虽然这 ISAPI 听上去还挺气派,也算是“应用程序”呢,但仔细看看它的全称就明白了:它实际上只是一个接口,起到一个代理的作用,它的主要工作是映射所请求的页面(文件)  和与此后缀名相对应的实际的处理程序。

让我们更进一步地看一下 ISAPI ,看看它到底是什么样子,请按下面的步骤进行:

  1. 打开IIS。
  2. 选择随意一个站点,鼠标右键,“属性”。
  3. 选择“主目录”选项卡。
  4. 选择“配置”。

你应该会看到如下的画面:

图1. 应用程序配置

很清楚地就可以看到,所有IIS所能处理,或者叫 ISAPI 所提供代理服务的 文件类型 及其相对应的实际的后台处理程序都在这里清楚地列出来了。

我们找到 .aspx 的应用处理程序,然后点“编辑”,会出现下面的画面:

图2. 编辑.aspx文件的处理程序

 

一路看到这里,可以看出,所有的.aspx文件实际上都是由 aspnet_isapi.dll 这个程序来处理的,当IIS把对于.aspx页面的请求提交给了aspnet_isapi.dll以后,它就不再关心这个请求随后是如何处理的了。现在我们应该知道:Asp.Net 只是服务器(IIS)的一个组成部分而已,它是一个 ISAPI扩展。

这里需要注意两点:

  • 当你修改“限制为”后,可以限制页面(文件)只能以某种特定方式访问
  • “确认文件是否存在”是实现 URL 地址映射的关键选项,我以后会专门讲述。

理解宿主环境(Hosting)

从本质上讲,Asp.Net 主要是由一系列的类组成,这些类的主要目的就是将Http请求转变为对客户端的响应。HttpRuntime类是Asp.Net的一个主要入口,它有一个 称作 ProcessRequest 的方法,这个方法以一个 HttpWorkerRequest 类作为参数。HttpRuntime 类几乎包含着关于单个 Http请求的所有信息:所请求的文件、服务器端变量、QueryString、Http 头信息 等等。Asp.Net 使用这些信息来加载、运行正确的文件,并且将这个请求转换到输出流中,一般来说,也就是HTML页面。

NOTE:二般来说,也可以是张图片。

当 Web.config文件的内容发生改变 或者 .aspx文件发生变动的时候,为了能够卸载运行在同一个进程中的应用程序(NOTE:卸载也是为了重新加载),Http请求被分放在相互隔离的应用程序域中。

NOTE:可能你以前就听过应用程序域,但是不了解怎么回事,应用程序域就是 AppDomain。

对于IIS来说,它依赖一个叫做 HTTP.SYS 的内置驱动程序来监听来自外部的 HTTP请求。在操作系统启动的时候,IIS首先在HTTP.SYS中注册自己的虚拟路径。

NOTE:实际上相当于告诉HTTP.SYS哪些URL是可以访问的,哪些是不可以访问的。举个简单的例子:为什么你访问不存在的文件会出现 404 错误呢?就是在这一步确定的。

如果请求的是一个可访问的URL,HTTP.SYS会将这个请求交给 IIS 工作者进程。

NOTE:IIS6.0中叫做 w3wp.exe,IIS5.0中叫做 aspnet_wp.exe。

每个工作者进程都有一个身份标识 以及 一系列的可选性能参数。

NOTE:可选性能参数,是指诸如 回收机制的设置、超时时间设置 等等。

接下来进行的事情就是上一章节讲述的 ISAPI 了。

NOTE:这部分的内容相关性比较强,为了让大家好理解,我最后还是决定把 ISAPI 放到前面了,可能全系列完成的时候会再调整吧。

除了映射文件与其对应的处理程序以外,ISAPI 还需要做一些其他的工作:

  1. 从HTTP.SYS中获取当前的Httq请求信息,并且将这些信息保存到 HttpWorkerRequest 类中。
  2. 在相互隔离的应用程序域AppDomain中加载HttpRuntime。
  3. 调用 HttpRuntime的ProcessRequest方法。

接下来才是程序员通常编写的代码所完成的工作了,然后,IIS 接收返回的数据流,并重新返还给 HTTP.SYS,最后,HTTP.SYS 再将这些数据返回给客户端浏览器。

OK,现在你看到张子阳的空间主页了。

图3.Asp.Net 的宿主环境

理解管道(Pipeline)

在前面两章中,我们在一个相对比较低的层次上讨论了从发出Http请求到看到浏览器输出这转瞬即逝的十分之一秒内IIS和 Framework 所做的事情。但是我们忽略了一个细节:程序员编写的代码是如何在这一过程中衔接的,本章我们就来看看这个问题。

当Http请求进入 Asp.Net Runtime以后,它的管道由托管模块(NOTE:Managed Modules)和处理程序(NOTE:Handlers)组成,并且由管道来处理这个 Http请求。

图4. 理解 Http 管道

我们按编号来看一下这幅图中的数据是如何流动的。

1. HttpRuntime将Http请求转交给 HttpApplication,HttpApplication代表着程序员创建的Web应用程序。HttpApplication创建针对此Http 请求的 HttpContext对象,这些对象包含了关于此请求的诸多其他对象,主要是HttpRequest、HttpResponse、 HttpSessionState等。这些对象在程序中可以通过Page类或者Context类进行访问。、

2. 接下来Http请求通过一系列Module,这些Module对Http请求具有完全的控制权。这些Module可以做一些执行某个实际工作前的事情

3. Http请求经过所有的Module之后,它会被HttpHandler处理。在这一步,执行实际的一些操作,通常也就是.aspx页面所完成的业务逻 辑。可能你会觉得在创建.aspx页面并没有体会到这一过程,但是,你一定知道,.aspx 页面继承自Page类,我们看一下Page类的签名:

public class Page : TemplateControl, IHttpHandler{
    // 代码省略
}

可以看到,Page类实现了IHttpHandler接口,HttpHandler也是Http请求处理的最底层。

4.HttpHandler处理完以后,Http请求再一次回到Module,此时Module可以做一些某个工作已经完成了之后的事情。

NOTE:注意我用红色标识的字,然后回想一下:Asp.Net 中是不是有众多的 Inserting 、Inserted 之类成对的事件?其实,这里讲述的就是为什么Asp.Net可以将一个Insert操作分成前后两部分,然后再分别进行事件拦截的幕后原理。

如果我们将注意力只集中在Http请求、HttpHandler和HttpModule上,不去考虑HttpContext和HttpApplication,那么图4.可以简化成下面这样:

图5.Http请求在HttpHandler 和 HttpModule 中的流动方向

总结

本文中,我首先概要介绍了这系列文章将要为大家讲述的主题。然后,我提出了部分程序员存在的一个问题:在一个比较高的层次上学习和使用Asp.Net。

随后,我以一个访问我个人空间首页的例子,引出了本文主要讲述的三个内容:

  1. Http请求刚刚到达时IIS时,IIS 所做的工作。
  2. Http请求的宿主环境。
  3. Http管道。

希望这篇文章能给你带来帮助。

 

 

你可能感兴趣的:(C#开发技术点说明-四种简单的排序算法,AJAX,Http Module,Http 请求处理流)