lcomplete's Blog

一个带着极客精神正努力成为黑客的程序员

大型 ASP.NET 应用中常用的开源项目

| Comments

最近几年,涌现了一大批优秀的开源项目,开源软件越来越受到人们的重视,现如今,可以说几乎没有一个大型软件是完全独立于开源项目的,越来越多的应用构建于开源项目之上,ASP.NET 应用也不例外,以往不太 “Open” 的微软也开始大力发展开源项目,将其 MVC 和 EntityFramework 等框架纷纷开源。

使用开源软件往往意味着减少软件开发成本、提升软件质量,开源是一股强大的力量,它帮助我们减少重复性的工作,提升我们这些程序员的生活品质,今天我们就来列举一些在构建大型 ASP.NET 应用中经常使用到的开源项目。

  1. log4net

    log4net 是 log4j 的一个移植版本,它是一个强大的日志记录组件,可以使用不同的日志级别记录日志,也可以将日志写入到文件、控制台、事件日志和数据库中,通过在XML配置中修改日志级别,可以很灵活地控制哪些日志需要记录,根据需要在开发环境和生产环境中定制配置,可以灵活地对应用程序进行跟踪调试。

  2. Json.NET

    AJAX 的广泛使用在一定程序上促进了 json 数据格式的流行,作为轻量级的数据交换格式,json 在很多场合下比 xml 更加合适,那么对于这个频繁使用的数据格式,一个高性能的序列化、反序列化工具自然是必不可少,json.net 便满足这一需求,使用它你可以灵活高效地在 json 数据和 .net 对象之间进行转换、使用 linq 语法对 json 数据进行查询、生成缩进好的 json 、在 json 和 xml 之间进行转换。

  3. StructureMap

    ASP.NET MVC 框架很好地贯彻了面向接口编程这一设计原则,这使得我们可以很容易替换 MVC 框架中的一些默认实现,比如通过替换默认的依赖解析器,我们可以得到 Controller 中获取依赖对象的控制权,因此便可以决定 Controller 中使用的 Service 接口或 Repository 接口的具体实现。如何决定接口的具体实现是什么?如果初始化对象需要提供构造参数,那么该如何处理依赖?这里面涉及许多枯燥的配对工作和复杂的依赖处理工作,自己实现一套 IoC 容器的代价是巨大的,还好我们有开源的 StructureMap。有很多文章介绍 StructureMap,说它是一个轻量级的 IoC 框架,其实不然,StructureMap 其实是一个代码量大且比较复杂的框架,也许是因为它没有其他第三方依赖,且基础功能使用起来比较简单的关系,使人们产生了这种错觉。

    StructureMap 除了一些大多数 IoC 框架都有的一些功能外,还有一些非常强大的特色功能,比如扫描程序集自动匹配子类型,这能够减少许多不必要的配置,此外它还提供了插件系统,通过编写插件可以使应用程序具有高可伸缩性,配合面向切面编程组件可谓是如虎添翼。

  4. Quartz.NET

    随着 Web 应用的复杂性不断加大,单纯的响应处理机制将无法满足一些需求,比如大量的数据统计、报表输出,如果等到请求出现再处理,那往往是不可接受的,这个时候我们需要将一些任务放在后台定时运行,对于简单的任务,也许使用 .net 自带的 Timer 就足以搞定,但应用是在不断进化中的,为了得到良好的可扩展性,我们应该使用一种可以满足更多触发条件的机制,可以说 Quartz.NET 是最佳选择。

    Quartz.NET 是一个功能齐全的任务调度系统,任务配置可以使用 XML、数据库或编程方式,使用它我们可以在任务调度上获得很高的可扩展性。有时候高可扩展性也意味着高复杂性,就像一个复杂的设计模式会带来理解上的困难一样,庆幸的是引入 Quartz.NET 并不会带来什么复杂性,在配置中可以使用 Cron 表达式描述任务的触发时间,只要熟悉这个概念便能完美控制任务运行时间。

    最后值得一提的是,Quartz.NET 提供了远程代理的方式控制在远端服务器上的 Quartz 任务,这大大方便了任务的监控、管理和测试。

  5. Topshelf

    刚才说到对于大型应用,后台的任务调度是少不了的,如果把任务放在网站中运行,那将是不可靠的,因为很多情况会导致 iis 应用程序池重启,所以一般情况下将使用 Windows 服务,使用 Topshelf 就是创建 Windows 服务的一种方法,它拥有跨平台特性,相比 .NET 中提供的创建 Windows 服务的 API,它的优势是使用简单且可以从控制台运行,这给开发和调试服务提供了非常大的便利。

.NET 中已有的优秀开源项目非常之多,并且不断地有新项目涌现,针对特定需求的组件大家可以上 github 上找找,本文就先列举这些吧。

论工作场所播放音乐的经济成本

| Comments

在工作场所播放音乐是否能提高工作效率,这个问题国外有大学专门做过研究,得出的结论是在某些工作场景下可以提高效率,另外一些场景则会令人感到分心、效率下降。这个结论跟我们的认知应该不会有任何冲突,是很自然的一个结论,因此若以此为前提,一家企业若考虑在工作场所播放音乐,则应该考虑自己公司的工作场合、工作性质等因素,判断这种行为是否合适。

一个较为普遍的观点是,若工作为机械式的重复劳动,在这种情况下播放音乐可能会带来效率上的提升,若工作需要复杂的脑力劳动,则播放音乐会严重影响工作效率和工作质量。如果认同这个观点的话,那么是否播放音乐就成了一个非常简单的选择,你会选择在制衣工厂或电子工厂里面播放音乐来提高那些从事机械工作的工人们的生产效率,甚至是让员工感到愉悦;相应地,你绝不会选择在一家高科技公司或互联网公司里面强制性地播放音乐。

即使选择了播放音乐,那也应该意识到这背后还存在着一些需要考虑的因素,比如播放何种音乐,如果你在思考具体播放哪些歌曲,那完全是一条错路的途径,播放什么歌曲,这完全不应该由任何个体去决定,因为每个人的品味、喜好都不一样,当然也不应该由团体去决定,若由整个集体做出选择,那虽然照顾到了大多数人,但是每个个体的感受都是同等重要的,不能顾此失彼,显然这个问题没有好的答案,再继续思考只会徒劳无功,这其中的原因在于这个问题的出发点就有问题,在公司里面播放音乐需要照顾到每一个人,很难找到有一首歌是所有人都喜欢的,需要思考的不是播放什么音乐,而是播放音乐的形式。想象一下在一家大型工厂里面,每个工人都在做着机械式的重复工作,而喇叭里面放着音乐,尽管随意想象一种音乐,不管是什么音乐,这种场景都很容易让人联想到独裁国家使用宣传工具向民众洗脑,同一种声音向每个人传播,这种做法是在减少个体之间的差异,使人们同化,在我看来这是不人道的,这种说法也许比较极端,但不可否认这并不是尊重个体的行为。这种想象中的工厂应该是并不多见的,据我所知,在一些工厂里面,一些小集体,比如某一车间或几个协作比较紧密的人,这几个人之间可能会用手机播放一些大家都喜欢的音乐,大家一边听着音乐,一边工作,一边聊着天,这种氛围倒还不错,由于是比较紧密的几个人,彼此之间也更容易获得尊重。在互联网企业中,需要安静的工作环境,但人们有时也有听歌的需求,这时人们往往会自己带上耳机听音乐,我认为播放音乐的最佳形式就是充分地尊重每一个个体。

企业的领导者往往不需要思考这些,因为大多数的公司根本都不考虑播放音乐这档子事,可是无奈还是有少数公司会选择播放音乐,选择了那就需要仔细思考这个问题,未经仔细思考就做出决定那可能会错的离谱。拿我现在待的公司举例,每天早晨和中午刚开始上班时都要播放 20 至 30 分钟的音乐,这真是播放音乐型公司中错的离谱的典型代表,我可以接受每天上班前放一首比较优美的歌曲使大家进入工作状态这种行为,但要是说每天得忍受 40 到 60 分钟的时间,我相信大多数人都无法接受,尤其是意识到这其中还存在尊重问题的人。40 到 60 分钟的音乐时间,这本身就是个大的错误,如果播放的音乐还是难听的、令人分心的,甚至引起公愤的,那这就成了公司在管理层面上一个大的疏漏。

不断持续地播放音乐,员工的心理也在潜移默化地受到影响。在公司刚开始决定播放音乐的时候,由于惯性使然,员工会一边被动听到音乐,一边工作,然而久而久之,当播放音乐已经成为一件令人反感的事情之后,心理上便会排斥这件事情,事情发展到这个地步,若管理层仍未察觉,这时大多数人便会开始认为管理者并不期望他们这段时间能有效地进行工作,这是“音乐时间”,此时至少有 90% 以上的人在这段时间里都不会进行任何工作,假设未播放音乐前是 40% (实际情况因公司而异),以此为条件,不妨粗略估算一下,这给公司造成的经济成本,员工数按 40 人算,每天的音乐时间按 50 分钟计算,那么每天将浪费 1000 分钟,将时间换算成金钱,这绝对是笔不小的开销,再将这每日开销乘以一周、一个月、一年,这在无形之中导致了公司整体工作效率的严重下降,就这样的一件小事情,却对公司带来如此负面的影响,作为企业的管理者必需谨小慎微,可千万不要拍拍脑袋就做决定。

一家公司最重要的组成部分就是员工,只有充分体现对员工的尊重,一家公司才能获得长久的发展,否则只会导致人才流失,员工怨声载道,真正人性化的公司会处处为员工着想,即使是播放音乐这类小事。

七周七语言之Scala

| Comments

“七周七语言”这个系列原本打算每一篇都写一些有用的小程序来阐述所讲的语言,但是由于 Scala 不像 Io 和 Prolog 那样特殊,想不出可以用它来写什么有明显差异化的程序,把一段已知的程序用 Scala 写出来也并不能说明什么,所以我就不重复造轮子啦,不过在介绍到 Scala 的一些特点时,我会引用其他人写好的代码。下面开始介绍这门编程语言。

Scala 是一门结合了面向对象编程和函数式编程特性的语言,它有两个版本,一个运行在 JVM 上,一个运行在 CLR 上,当然由于 Scala 并非微软出品,可想而知其在 CLR 上的实现也不如 JVM 上成熟,因此大多是使用运行在 JVM 上的 Scala,如果想使用一门在 CLR 上运行的函数式编程语言,那么推荐使用微软自家的 F#。

相对 Io、Prolog 来说,Scala 已经应用在一些比较知名的系统中,比如 Twitter,这也使人们敢于大胆地在生产环境上使用 Scala,为什么 Scala 能得到高科技公司的青睐呢,下面我们来具体看一看 Scala 都有哪些优点。

  1. Scala 支持面向对象特性,值得一提的是类扩展有两种途径,一种是继承机制,另一种是使用 trait 的方式,这与 ruby 中的 mixin 类似,可以很好地解决多继承产生的问题。
  2. Scala 出彩的地方在于它支持两种编程范式,它的强大更多来自于对函数式编程的支持,函数式编程和命令式编程的区别在于,前者关注做什么(what),后者关注如何做(how),前者所产生的代码更清晰、意图更加明显,使用函数式编程操作集合也更加方便。
  3. 语法简洁,Scala 定义方法、类、匿名函数的语法有点类于 ruby,代码越少,需要读的也就越少,可读性也更高。
  4. 对 XML 有着非常好的支持,在 Scala 中 XML 可以说是一等公民,可以像表示字符串一样表示 XML,此外还可以使用内置的类 XPATH 进行查询。
  5. 高效,一些性能测试表明 Scala 和 Java 的性能在同一个水平。
  6. 可扩展性,Scala 语言的名字就来自于 Scalable 一词,这意味着这门语言本身是可以进行扩展的,也就是说可以很方便地编写领域特定语言。看一个简单的例子,为整形增加一个计算阶乘的方法“!”
  7. 并发编程模型,Scala 使用了 Actor 简化并发程序的开发,具有可复用线程的能力,且能更好地利用多核 CPU。
  8. 和 java 的互操作性,在 Scala 中可以直接使用 java 库,这点很重要,这样一来在 Scala 中可以使用大量在 java 社区中已有的资源,比如在 Scala 中使用 LuceneScala mp3 播放器(在这个代码片段里还演示了 Actor 的使用)。

Scala 提供了一个更简洁的 java,并且可以直接使用 java 库,这使得 Scala 有一个非常好的起点,现在 Scala 社区也在不断地发展壮大,出现了很多框架,比如类似 rails 的开源 web 框架 Lift,也就是说 Scala 不仅可以利用现有的 java 库编写桌面程序,也可以用来编写 Web 程序了,由于构建在 JVM 上,因此也是跨平台的,这么多的优点,相信会有越来越多的人使用 Scala 和越来越多的成功案例出现。

这次虽然没有使用 Scala 编写什么程序,但学习它后让我多了一个选择,以后在需要编写运行在 JVM 上的程序时,首选 Scala。:)

浅谈中国互联网上的垃圾信息

| Comments

这是一个信息时代,信息传播方式的变革给人们的生活带来了翻天覆地的变化,现在获取信息变的更简单了,只需要轻点鼠标或移动手指就能够在互联网上浏览海量信息,这无疑给人们带来了很多便利,但与此同时也带来了一些挑战,面对如此多的信息和信息来源,面对质量参差不齐的博客、新闻和软文等等信息,我们如何找到自己想要的信息?当我们在互联网上浏览时,期望能够遨游在高质量信息的海洋里,但很多时候其实是深处在垃圾信息的沼泽中,这实在是件令人沮丧的事情。怎样才能避免垃圾信息进入自己的视线呢,这就要求我们能够鉴别垃圾信息,当你对垃圾信息了解的越多,也就越容易远离它们。

垃圾信息有一个共同特点,利益性强,如果某篇文章在大力鼓吹某种东西,并且对发布者(个体或团队)来说能带来很大的利益,那这个时候读者多半需要提防一下,这篇文章是否是想诱导你相信某些事物,很多软文都属于这类信息。然而软文只是某种形式的广告,其被经济利益所驱动的,这类信息还是较容易识别的,而当信息涉及到政治利益时,往往很难对信息里的观点进行判断,因为大多数时候我们都无法获取足够多的事实,我们都知道中国互联网其实是一个巨大的局域网,虽然有一部分用户会翻墙,但这实在是相当小众的一部分人群,大多数普通用户获取信息的途径都被阉割了,中国互联网的信息审查机制也是十分严格的,很多事情是说不得的,从这两个角度来说,中国互联网上的信息发布和传播都是不自由的,虽然信息不自由并不会增加垃圾信息,但是它却会影响信息的竞争,使信息无法进行自然的优胜劣汰,假如一些敏感话题只能由一些“权威机构”来描述、发表议论和观点的时候,我们如何形成自己的立场呢,也许这里面灌输的是一些错误观点,而我们却不自知,由此可见,信息不自由会让垃圾信息有更多的生存空间。当然并不是说带有功利性的文章都是不好的,这里仅指那些通过歪曲事实使发布者直接或间接获利的信息。

从某种角度来说,垃圾信息大致可以分为两类,一类是误导类信息,这一类是人写的,你需要有一定的批判性思维才能避免被误导,而第二类则很好区分,因为它根本就不是人写的,对,不要笑,这可是垃圾信息的一个大源头。不是人写的,也就是说这些信息是由机器产生的,这类信息又分为几类:采集、伪原创、自动翻译等等,这类信息根本就毫无价值,如果说第一类信息还需要稍微动动脑筋才能鉴别出好坏,那这一类简直是不用动脑也能看出来是垃圾信息,那你说这些信息为什么还会存在呢?这不得不说到几个新兴的行业,网络营销和搜索引擎优化,SEO优化有白帽和黑帽之分,所谓黑帽SEO,其实就是钻搜索引擎空子,毕竟搜索引擎也是机器,它对信息质量判断的准确度取决于它的算法,有大量的搞seo优化的人,整天都在想方设法让搜索引擎认为一篇垃圾信息是原创的、高质量的,在他们看来被搜索引擎收录,有排名有流量就有money,所以这背后也是有利益链条存在的。这类垃圾信息的特点就是量大,高质量信息的产生速度怎么可能比的上这些机器自动生成的信息呢,虽然我们可以一眼就看出这类信息不值得阅读,但是当你在搜索时,这些垃圾信息很有可能出现在你的搜索结果里面,它们妨碍了你找到真正有价值的内容,要避免这种情况,我们只能挑选更好的搜索引擎,根据经验使用恰当的关键字。说到搜索信息,那无疑是推荐使用更好的 Google 了,而在国内,因为墙的问题 Google 又经常打不开,所以为了更好的获取信息、不让垃圾信息肆意滋生,我们应该翻墙,信息应该自由的传播。

使用 C# 编写简易 ASP.NET Web 服务器

| Comments

你是否有过这样的需求——想运行 ASP.NET 程序,又不想安装 IIS 或者 Visual Studio?我想如果你经常编写 ASP.NET 程序的话,应该或多或少都会碰到这种情况。除了使用 IIS 和 VS,我们还有哪些方式可以运行 ASP.NET 程序呢,自己写一个支持 ASP.NET 的 Web 服务器怎么样?NO NO NO,如果你只是想找个这样的工具的话,那完全没必要,我们知道使用 VS 可以运行 ASP.NET 程序,那么我们就可以找出 VS 所调用的程序,将其拷贝到没有 VS 和 IIS 的环境中运行,就能运行 ASP.NET 程序了,安装了 VS 的朋友可以到 C:\Program Files\Common Files\Microsoft Shared\DevServer\ 这个目录里面找找看,这个程序的使用方式如下。

1
WebDev.WebServer.EXE /port:80 /path:"c:\mysite" /vpath:"/"

怎么样?不错吧,轻而易举地就解决了文章开头所说的问题了。当然这并不是本篇文章的重点,如果你不满足于只知道这个用法,那可以继续往下阅读,接下来,我们将使用 C# 编写一个支持 ASP.NET 的 Web 服务器,看看这一切究竟是如何运作的。

C# 中有着许多丰富的类库,使用不同的类库,我们可以站在不同的抽象层级去编写一个 Web 服务器,比如在 System.Net 命名空间下提供了一个 HttpListener 类,使用这个类,我们可以很容易地创建一个简单的 Web 服务器,但是这个类隐藏了很多实现的细节,为了避免知其然不知其所以然,我们将使用网络框架最底层的 Socket 类来编写这个程序。

预备知识

正式编写这个程序之前,让我们先来了解一些基础知识。编写一个 Web Server,必需要了解 HTTP 协议,它是万维网的基础,位于 TCP/IP 协议栈的应用层。

  1. HTTP 协议

    HTTP 协议是一个基于请求与响应模式、无状态的应用层协议,HTTP 请求主要包括三部分:请求行、请求报头、请求正文,下面是一个请求示例。

    GET /lcomplete/AspNetServer HTTP/1.1 
    Host: github.com
    Connection: keep-alive
    Cache-Control: max-age=0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
    
    postdata #可选的消息体
    

    第一行是请求行,该行又分为3个部分,分别是动作、URI 和 HTTP 协议版本,后面的 {key}: {value} 格式的行为报头,如果请求为 post 动作的话,则报头后面的post数据为请求正文,需要注意报头和请求正文之间必需以(回车+换行)分割。

    Web 服务器接收到一个请求后,就会将请求解析成上面3个部分,并开始处理应答,响应也由3个部分组成:状态行、响应报头、响应正文,响应报头和正文同样使用进行分割,状态行为HTTP协议版本、状态码、状态描述组成,响应报头与请求报头格式相同,只不过请求报头由服务器解释并处理,响应报头由浏览器解释并处理,最后的响应正文便是我们所熟悉的 HTML。

    了解了 HTTP 协议的基础知识后,我们可以很容易地构建出一个支持静态文件的 HTTP 服务器,但是如何处理 ASP.NET 动态内容呢,这就要求我们熟悉 ASP.NET 的 HTTP 架构、管道机制、应用程序生命周期和宿主环境。

  2. ASP.NET 运行时机制

    ASP.NET 被特意设计成避免依赖 IIS,它的底层架构采用了管道机制,管道由一系列处理 HTTP 消息的对象组成,每个 HTTP 请求都要经过这些对象,每个对象都执行一些自己职责之内的任务。

    HttpRuntime 类是管道的入口,它负责开始处理请求,管理首先执行 HttpRuntime 类上的静态方法 ProcessRequest ,这个方法接收一个 HttpWorkerRequest 对象参数,该对象包含了当前请求的相关信息,HttpRuntime 类使用这个请求信息构建 HttpContext 对象,其中包含了 HttpRequest 和 HttpResponse 属性,然后根据上下文获取 HttpApplication 对象,之后请求交给 HttpApplication 对象进行处理。

    处理请求时,HttpApplication 会执行一系列任务,其中包括为请求调用合适的 IHttpHandler 类的 ProcessRequest 方法,例如,如果请求针对某页,则使用该页的实例处理该请求,另外 HttpApplication 中还维护了 IHttpModule 对象列表,它可以在页面实例处理请求前后进行一些额外的工作。

    管道机制是完全自主的,不需要依附于 IIS 上,不过管道并没有接收 HTTP 请求的能力,我们需要自己编写这部分代码,当收到请求时,创建 HttpWorkerRequest 对象并提供给 HttpRuntime.ProcessRequest 方法调用以启动管道。

    要处理 ASP.NET 请求,还需要创建一个应用程序域以托管 HTTP 管道,我们可以使用 ApplicationHost.CreateApplicationHost 方法创建应用程序域,该方法接收3个参数:宿主类型、虚拟路径和物理路径,宿主类型需要跨域应用程序边界,所以需要继承自 MarshalByRefObject 类,并提供与其交互的方法,例如至少要提供一个方法使得可以提交 ASP.NET 请求以进行处理。

    了解了 ASP.NET 的运行机制后,再来看看编写 ASP.NET 服务器需要使用到哪些类,首先我们需要使用 ApplicationHost 创建应用程序域以获得处理 ASP.NET 请求的能力,接收到请求后构造 HttpWorkerRequest (该类是抽象类,需要定义它的子类)对象,交由 HttpRuntime 类进行处理,接下来的事情就由 HTTP 管道处理了。

    好了,预备知识已经讲解完毕,下面让我们进入编码实战。

编码实战

还记得文章开头的命令吗?运行一个网站需要提供3个必要的东西,端口、网站物理路径、网站虚拟路径,在程序开始运行时需要得到这3个参数。

Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void Main(string[] args)
{
    int port;
    string dir = Directory.GetCurrentDirectory();
    if(args.Length==0 || !int.TryParse(args[0],out port))
    {
        port = 45758; //端口
    }

    InitHostFile(dir);
    SimpleHost host= (SimpleHost) ApplicationHost.CreateApplicationHost(typeof (SimpleHost), "/", dir);
    host.Config("/", dir); //配置虚拟路径和物理路径

    WebServer server = new WebServer(host, port);
    server.Start();
}

//需要拷贝执行文件 才能创建ASP.NET应用程序域
private static void InitHostFile(string dir)
{
    string path = Path.Combine(dir, "bin");
    if (!Directory.Exists(path))
        Directory.CreateDirectory(path);
    string source = Assembly.GetExecutingAssembly().Location;
    string target = path + "/" + Assembly.GetExecutingAssembly().GetName().Name + ".exe";
    if(File.Exists(target))
        File.Delete(target);
    File.Copy(source, target);
}

为了便于测试,我将这3个参数都写死了,端口默认使用45758,物理路径使用当前程序所在目录,虚拟路径使用根目录,这两个路径信息保存在 host 对象中。由于 Application.CreateApplicationHost 方法期望在 GAC 或指定的物理路径中的 bin 目录中找到宿主类型所在的程序集,所以在创建应用程序域之前先将当前程序拷贝到了物理路径的 bin 目录中,创建完应用程序域后初始化 WebServer 对象,调用该对象的 Start 方法以启动服务器。在 WebServer 中保留了 host 的引用,当处理 ASP.NET 请求时会使用到,我们先看一下启动服务器的方法。

WebServer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public void Start()
{
    _serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    _serverSocket.ExclusiveAddressUse = true;
    _serverSocket.Bind(new IPEndPoint(IPAddress.Any, Port));
    _serverSocket.Listen(1000);
    IsRuning = true;

    Console.WriteLine("Serving HTTP on 0.0.0.0 port " + Port + " ...");

    new Thread(OnStart).Start();
}

private void OnStart(object state)
{
    while (IsRuning)
    {
        try
        {
            Socket socket = _serverSocket.Accept();
            ThreadPool.QueueUserWorkItem(AcceptSocket, socket);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            Thread.Sleep(100);
        }
    }
}

private void AcceptSocket(object state)
{
    if (IsRuning)
    {
        Socket socket = state as Socket;
        HttpProcessor processor = new HttpProcessor(_host, socket);
        processor.ProcessRequest();
    }
}

在 Start 方法中,创建了一个全局的 socket 对象,使其监听指定端口,并新开了一个线程用于处理客户端请求,当接收到客户端请求后,将其交给 HttpProcessor 对象处理。

HttpProcessor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void ProcessRequest()
{
    try
    {
        RequestInfo requestInfo = ParseRequest();
        if (requestInfo != null)
        {
            string staticContentType = GetStaticContentType(requestInfo);
            if (!string.IsNullOrEmpty(staticContentType))
            {
                WriteFileResponse(requestInfo.FilePath, staticContentType);
            }
            else if (requestInfo.FilePath.EndsWith("/"))
            {
                WriteDirResponse(requestInfo.FilePath);
            }
            else
            {
                _host.ProcessRequest(this, requestInfo);
            }
        }
        else
        {
            SendErrorResponse(400);
        }
    }
    finally
    {
        Close();//确保连接关闭
    }
}

处理的步骤如下:

  1. 解析请求数据,从建立的 socket 连接处获取请求数据,将其解析为RequestInfo对象。
  2. 判断请求是否有效,无效则响应 400 错误,有效则进行下一步处理。
  3. 判断请求的是否为静态内容,是则输出文件响应。
  4. 判断请求是否为目录,是则输出目录下的子文件夹和文件的链接,与 IIS 目录服务类似。
  5. 不为静态内容和目录时,则交给 host 对象处理(使用ASP.NET HTTP 运行时进行处理)。
  6. 处理完后确保连接关闭。

其中输出响应是构造状态行、响应报头和响应正文,接着通过 socket 发送给客户端的过程。相信看到这里,大家已经对整个交互过程有了一个了解,剩下的最后一个问题就是如何处理动态内容。

为了与 ASP.NET 的应用程序域交互,我们需要将请求信息提交给宿主对象 host 进行处理,下面是我们实现的宿主类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SimpleHost : MarshalByRefObject
{
    public string PhysicalDir { get; private set; }

    public string VituralDir { get; private set; }

    public void Config(string vitrualDir, string physicalDir)
    {
        VituralDir = vitrualDir;
        PhysicalDir = physicalDir;
    }

    public void ProcessRequest(HttpProcessor processor, RequestInfo requestInfo)
    {
        WorkerRequest workerRequest = new WorkerRequest(this, processor, requestInfo);
        HttpRuntime.ProcessRequest(workerRequest);
    }
}

在 ProcessRequest 方法中,创建了 HttpWorkerRequest 的子类 WorkerRequest 对象,并提交给 HttpRuntime 进行处理。WorkerRequest 类中实现了 HttpWorkerRequest 中的抽象方法,其中包括 GetRawUrl 、GetHttpVerbName 等等这一类获取请求相关信息的方法,HTTP 管道调用这些方法以获取请求数据,同时它还包含类似 FlushResponse 这类输出响应的方法,HTTP 管道最终会调用这类方法向客户端发送数据,下面是 FlushResponse 方法的实现,在该方法中我们使用 HttpProcessor 对象向 socket 客户端发送响应数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public override void FlushResponse(bool finalFlush)
{
    if (!_isHeaderSent)
    {
        _processor.SendHeaders(_statusCode, _responseHeaders, -1, finalFlush);
        _isHeaderSent = true;
    }

    for (int i = 0; i < _responseBodyBytes.Count; i++)
    {
        byte[] data = _responseBodyBytes[i];
        _processor.SendResponse(data);
    }

    _responseBodyBytes = new List<byte[]>();
    if (finalFlush)
        _processor.Close();
}

到这一步,我们已经可以运行 ASP.NET 程序了,但是只实现抽象方法还不能提供足够的信息给 HTTP 管道,例如 HTTP 管道无法得知 POST 数据和 Cookie 数据,要提供这些信息我们还需要重写一些虚拟方法,如 GetKnownRequestHeader 、GetPreloadedEntityBody 等等,实现一些必要的方法之后,ASP.NET 程序就能够良好地运行了。

总结

编写支持 ASP.NET 的 Web 服务器,并不是一件难事,这得益于 ASP.NET 优雅的设计,只要向运行时提供必要的信息,HTTP 管道就能够正确地进行处理。

文中只贴了一小部分代码,你可以通过 https://github.com/lcomplete/AspNetServer 该地址查看所有代码。