4. ASP.NET 开发基础
4.1. ASP.NET Web 应用程序的概念
4.1.1. 简介
Web 应用程序是一种将各种文档(HTML、.NET 代码、图像、声音等)整合在一起的应用程序。这些文档必须位于一个单一的根目录下,该目录被称为 Web 应用程序根目录。Web 服务器上的一个虚拟路径与该根目录相关联。 我们在讨论 Cassini Web 服务器时曾接触过虚拟目录的概念。IIS Web 服务器也存在这一概念。这两款服务器之间有一个重要区别:在任何给定时刻,IIS 可以拥有任意数量的虚拟目录,而 Cassini Web 服务器只有一个——即启动时指定的那个。这意味着 IIS 服务器可以同时提供多个 Web 应用程序,而 Cassini 服务器每次只能提供一个。 在之前的示例中,Cassini 服务器始终使用参数 (<webroot>,/aspnet) 启动,将虚拟文件夹 /aspnet 与物理文件夹 <webroot> 关联起来。因此,Web 服务器始终提供同一个 Web 应用程序。但这并不妨碍我们在该单一 Web 应用程序内编写和测试不同的、独立的页面。每个 Web 应用程序都有自己的资源,位于其物理根目录 <webroot> 下:
- 一个 [bin] 文件夹,用于存放预编译类
- 一个 [global.asax] 文件,用于初始化整个 Web 应用程序及其每个用户的运行时环境
- 一个 [web.config] 文件,用于配置应用程序的行为
- 一个 [default.aspx] 文件,作为应用程序的入口点
- ...
一旦应用程序使用了这三类资源中的任何一种,就需要其专属的物理路径和虚拟路径。事实上,两个不同的 Web 应用程序没有理由采用相同的配置。我们之前的示例之所以都能放置在同一个应用程序中(<webroot>,/aspnet),正是因为它们未使用上述任何资源。
让我们重新审视本章开头推荐的 Web 应用程序开发 MVC 架构:

Web 应用程序由类文件(控制器、业务类、数据访问类)和呈现文件(HTML 文档、图像、声音、样式表等)组成。 所有这些文件都将放置在一个根目录下,我们有时将其称为 <application-path>。该根目录将与虚拟路径 <application-vpath> 相关联。该虚拟路径与物理路径之间的映射是通过 Web 服务器配置的。我们看到,对于 Cassini 服务器,这种映射发生在服务器启动时。例如,在命令提示符窗口中,我们会使用以下命令启动 Cassini:
在 <application-path> 文件夹中,根据我们的需求,我们会发现:
- 用于存放预编译类(DLL)的 [bin] 文件夹
- [global.asax] 文件,用于在应用程序启动时或用户会话期间执行初始化
- [web.config] 文件,用于配置应用程序
- [default.aspx] 文件,用于在应用程序中设置默认页面
为了遵循这一 Web 应用程序概念,接下来的示例都将放置在应用程序专用的 <application-path> 文件夹中,该文件夹将与虚拟的 <application-vpath> 文件夹相关联,因为 Cassini 服务器在启动时会将这两个参数关联起来。
4.1.2. 配置 Web 应用程序
如果 <application-path> 是 ASP.NET 应用程序的根目录,您可以使用 <application-path>\web.config 文件进行配置。该文件采用 XML 格式。以下是一个示例:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
请注意,XML 标签区分大小写。所有配置信息必须包含在 <configuration> 和 </configuration> 标签之间。可用的配置部分有很多。这里我们只介绍其中一个:<appSettings> 部分,它允许您使用 <add> 标签初始化数据。该标签的语法如下:
当 Web 服务器启动应用程序时,会检查 <application-path> 路径下是否存在名为 web.config 的文件。如果存在,则读取该文件并将信息存储在 [ConfigurationSettings] 对象中,只要应用程序处于活动状态,该对象的信息便可供应用程序的所有页面使用。[ConfigurationSettings] 类提供了一个静态方法 [AppSettings]:

要从配置文件中检索键 C 的值,请编写 ConfigurationSettings.AppSettings("C")。这将返回一个字符串。要使用上述配置文件,让我们创建一个名为 [default.aspx] 的页面。[default.aspx.vb] 文件中的 VB 代码如下:
Imports System.Configuration
Public Class _default
Inherits System.Web.UI.Page
Protected nom As String
Protected age As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
'retrieve configuration information
nom = ConfigurationSettings.AppSettings("nom")
age = ConfigurationSettings.AppSettings("age")
End Sub
End Class
我们可以看到,当页面加载时,会检索到配置参数 [name] 和 [age] 的值。这些值将由 [default.aspx] 中的呈现代码显示:
<%@ Page src="default.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="_default" %>
<html>
<head>
<title>Configuration</title>
</head>
<body>
Nom :
<% =nom %><br/>
Age :
<% =age %><br/>
</body>
</html>
为了测试,请将 [web.config]、[default.aspx] 和 [default.aspx.vb] 文件放在同一个文件夹中:
D:\data\devel\aspnet\poly\chap2\config1>dir
30/03/2004 15:06 418 default.aspx.vb
30/03/2004 14:57 236 default.aspx
30/03/2004 14:53 186 web.config
设 <application-path> 为包含这三个应用程序文件的文件夹。Cassini 服务器通过参数 (<application-path>,/aspnet/config1) 启动。我们请求 URL [http://localhost/aspnet/config1]。由于 [config1] 是一个文件夹,Web 服务器将在此文件夹内查找名为 [default.aspx] 的文件,并显示该文件(若找到)。在此情况下,它将找到该文件:

4.1.3. 应用程序、会话、上下文
4.1.3.1. global.asax 文件
[global.asax] 文件中的代码总是在当前请求的页面加载之前执行。它必须位于应用程序的 <application-path> 根目录下。如果存在,Web 服务器会在以下各种情况下使用 [global.asax] 文件:
- Web 应用程序启动或结束时
- 用户会话开始或结束时
- 用户请求开始时
与 .aspx 页面类似,[global.asax] 文件可以采用不同的编写方式,特别是将 VB 代码拆分为控制器类和呈现代码。这是 Visual Studio 的默认选择,我们在此也将采用这种方式。通常无需处理呈现逻辑,因为该职责已分配给 .aspx 页面。因此,[global.asax] 文件的内容仅需包含一条引用控制器代码文件的指令:
<%@ Application src="Global.asax.vb" Inherits="Global" %>
请注意,该指令不再是 [Page],而是 [Application]。由 Visual Studio 生成的相关控制器代码 [global.asax.vb] 如下所示:
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when application is started
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when the session is started
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' Triggered at the start of each request
End Sub
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when user authentication is attempted
End Sub
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Triggers when an error occurs
End Sub
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when session ends
End Sub
Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when application ends
End Sub
End Class
请注意,控制器类继承自 [HttpApplication] 类。在应用程序的整个生命周期中,会发生几个重要的事件。这些事件由上述示例中所示的程序处理。
- [Application_Start]:请记住,Web 应用程序“包含”在某个虚拟路径中。当客户端请求位于该虚拟路径下的页面时,应用程序即刻启动,随后执行 [Application_Start] 过程。这将是唯一一次执行。在此过程中,我们将执行应用程序所需的任何初始化操作,例如创建生命周期与应用程序一致的对象。
- [Application-End]:在应用程序终止时执行。每个应用程序都关联有一个空闲超时时间,该时间可在 [web.config] 中配置,超过该时间后,应用程序将被视为已终止。因此,是 Web 服务器根据应用程序的设置做出这一决定。应用程序的空闲超时时间定义为在此期间内没有客户端请求应用程序资源的时间段。
- [Session-Start]/[Session_End]:除非应用程序配置为不使用会话,否则每个客户端都会关联一个会话。 客户端并非指坐在屏幕前的用户。如果用户打开了两个浏览器来与应用程序交互,则代表两个客户端。客户端通过会话令牌进行标识,且必须在每次请求中包含该令牌。该会话令牌是由 Web 服务器随机生成的唯一字符串。两个客户端不可能拥有相同的会话令牌。该令牌随客户端的流程如下:
- 首次发送请求的客户端不会发送会话令牌。Web 服务器会识别这一点并为其分配一个。这标志着会话的开始,并将执行 [Session_Start] 过程。此过程仅发生一次。
- 客户端在后续请求中会发送用于标识自身的令牌。这使 Web 服务器能够检索与该令牌关联的信息,从而实现对客户端不同请求之间的跟踪。
- 应用程序可向客户端提供会话结束表单。在此情况下,由客户端自行发起会话终止。此时将执行 [Session_End] 过程。此操作仅发生一次。
- 客户端可能永远不会主动请求结束会话。在这种情况下,经过一段会话闲置时间(该时间也可通过 [web.config] 进行配置)后,Web 服务器将终止会话。随后将执行 [Session_End] 过程。
- [Application_BeginRequest]:该过程在收到新请求时立即执行。因此,它会对来自任何客户端的每个请求都执行。这是在将请求转发至目标页面之前检查请求内容的理想位置。您甚至可以决定将其重定向到另一个页面。
- [Application_Error]:当发生未被 [global.asax.vb] 控制器中的代码显式处理的错误时,该过程会被执行。在此处,您可以将客户端的请求重定向到一个解释错误原因的页面。
如果无需处理这些事件,则可以忽略 [global.asax] 文件。本章的前几个示例中就是这样做的。
4.1.3.2. 示例 1
让我们开发一个应用程序,以便更好地理解三个关键时刻:应用程序启动、会话启动以及客户端请求。[global.asax] 文件将如下所示:
相关的 [global.asax.vb] 文件如下:
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when application is started
' we note the time
Dim startApplication As String = Date.Now.ToString("T")
' we place it in the context of the application
Application.Item("startApplication") = startApplication
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when the session is started
' we note the time
Dim startSession As String = Date.Now.ToString("T")
' put it in the session
Session.Item("startSession") = startSession
End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' we note the time
Dim startRequest As String = Date.Now.ToString("T")
' put it in the session
Context.Items("startRequest") = startRequest
End Sub
End Class
该代码的关键点如下:
- Web 服务器向 [global.asax.vb] 中的 [HttpApplication] 类提供了若干对象:
- 类型为 [HttpApplicationState] 的 Application——代表 Web 应用程序——提供对 [Application.Item] 对象字典的访问权限,该字典可供应用程序的所有客户端访问——支持不同客户端之间的信息共享——多个客户端对同一数据的并发读写访问需要客户端同步。
- 类型为 [HttpSessionState] 的 Session——代表特定客户端——提供对 [Session.Item] 对象字典的访问权限,该字典可供该客户端的所有请求访问——允许存储有关客户端的信息,这些信息可在该客户端的所有请求中被检索。
- [HttpRequest] 类型的请求——表示客户端当前的 HTTP 请求
- [HttpResponse] 类型的响应——表示服务器当前正在为客户构建的 HTTP 响应
- 类型为 [HttpServerUtility] 的服务器——提供实用方法,特别是用于将请求重定向到与最初目标不同的页面。
- 类型为 [HttpContext] 的 Context —— 该对象在每次新请求时都会被重新创建,但由参与处理该请求的所有页面共享 —— 允许在请求处理过程中通过其 Items 字典在页面之间传递信息。
- [Application_Start] 过程将应用程序的开始记录在一个变量中,该变量存储在应用程序级别可访问的字典中
- [Session_Start] 过程将会话的开始记录在一个变量中,该变量存储在一个可在会话级别访问的字典中
- [Application_BeginRequest] 过程将请求的开始记录在一个变量中,该变量存储在请求级别可访问的字典中(即在整个处理过程中可用,但在处理结束时丢失)
目标页面将是以下 [main.aspx] 页面:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
<head>
<title>global.asax</title>
</head>
<body>
jeton de session :
<% =jeton %><br/>
début Application :
<% =startApplication %><br/>
début Session :
<% =startSession %><br/>
début Requête :
<% =startRequest %><br/>
</body>
</html>
此展示页面显示由其控制器 [main.aspx.vb] 计算出的值:
Public Class main
Inherits System.Web.UI.Page
Protected startApplication As String
Protected startSession As String
Protected startRequest As String
Protected jeton as String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve application and session info
jeton=Session.SessionId
startApplication = Application.Item("startApplication").ToString
startSession = Session.Item("startSession").ToString
startRequest = Context.Items("startRequest").ToString
End Sub
End Class
该控制器仅通过 [global.asax.vb] 检索存储在应用程序、会话和上下文中的三项信息。
我们按以下方式测试应用程序:
- 将文件集中到 <application-path> 文件夹中

- 使用参数 (<application-path>,/aspnet/globalasax1) 启动 Cassini 服务器
- 第一个客户端请求 URL [http://localhost/aspnet/globalasax1/main.aspx] 并收到以下结果:

- 同一客户端发起新请求(使用浏览器的“重新加载”选项):

我们可以看到只有请求时间发生了变化。这表明了两点:
- 第二次请求期间,[global.asax] 中的 [Application_Start] 和 [Session_Start] 过程并未被执行。
- 存储了应用程序和会话开始时间的 [Application] 和 [Session] 对象,在第二次请求中仍然可用。
- 我们启动第二个浏览器以创建第二个客户端,并再次请求相同的 URL:

这次,我们看到会话时间发生了变化。尽管第二个浏览器位于同一台机器上,但它被视为第二个客户端,并为其创建了一个新的会话。我们可以看到,这两个客户端的会话令牌并不相同。应用程序开始时间没有变化,这意味着:
- [global.asax.vb] 中的 [Application_Start] 过程并未被执行
- 存储应用程序启动时间的 [Application] 对象对第二个客户端是可访问的。因此,该对象应用于存储需要在应用程序各个客户端之间共享的信息,而 [Session] 对象则用于存储需要在同一客户端的不同请求之间共享的信息。
4.1.3.3. 概述
基于目前所学内容,我们可以绘制一个初步图表,说明 Web 服务器及其所服务的 Web 应用程序的工作原理:

上图展示了一台服务器,它托管着两个分别标记为 A 和 B 的应用程序,每个应用程序各有两个客户端。Web 服务器能够同时托管多个 Web 应用程序。这些应用程序彼此完全独立。我们将重点关注应用程序 A。客户端 1A 向应用程序 A 发送请求的处理过程如下:
- 客户端 1A 向 Web 服务器请求属于应用程序 A 域的资源。这意味着它请求的 URL 格式为 [http://machine:port/VA/ressource],其中 VA 是应用程序 A 的虚拟路径。
- 如果 Web 服务器检测到这是对应用程序 A 资源的首次请求,它将触发应用程序 A 的 [global.asax] 文件中的 [Application_Start] 事件。此时将创建一个类型为 [HttpApplicationState] 的 [ApplicationA] 对象。应用程序的各个部分将在此对象中存储 [Application] 作用域的数据,即与所有用户相关的数据。 [ApplicationA] 对象将一直存在,直到 Web 服务器卸载应用程序 A。
- 如果 Web 服务器还检测到正在处理应用程序 A 的新客户端,它将触发应用程序 A 的 [global.asax] 文件中的 [Session_Start] 事件。将创建一个类型为 [HttpSessionState] 的 [Session-1A] 对象。该对象将允许应用程序 A 存储 [Session] 作用域的对象,即属于特定客户端的对象。 只要客户端 1A 发出请求,[Session-1A] 对象就会一直存在。它将支持对该客户端的跟踪。Web 服务器在以下两种情况下会检测到正在处理一个新客户端:
- 客户端在其请求的 HTTP 头中未发送会话令牌
- 客户端发送的会话令牌不存在(客户端故障或黑客攻击)或已过期。会话令牌在客户端闲置一段时间后(IIS 默认 20 分钟)将过期。此超时时间可配置。
- 在所有情况下,Web 服务器都会触发 [global.asax] 文件中的 [Application_BeginRequest] 事件。该事件启动了对客户端请求的处理。通常不处理此事件,而是将控制权传递给客户端请求的页面,由该页面处理请求。我们也可以利用此事件来分析请求、处理请求,并决定应返回哪个页面作为响应。 我们将利用此技术实现一个遵循我们所讨论的 MVC 架构的应用程序。
- 一旦通过了 [global.asax] 中的过滤器,客户端的请求就会被传递给一个 .aspx 页面,该页面将处理该请求。我们稍后将看到,可以将请求通过由多个页面组成的过滤器进行传递。最后一个页面将负责向客户端发送响应。 页面可以将计算出的信息添加到客户端的初始请求中。它们可以将这些信息存储在 Context.Items 集合中。实际上,所有参与处理客户端请求的页面都可以访问这个数据池。
- 各页面的代码可访问由 [ApplicationA]、[Session-1A] 等对象表示的数据池。需注意的是,Web 服务器会同时处理应用程序 A 的多个客户端。所有这些客户端均可访问 [Application A] 对象。若需修改该对象中的数据,则必须进行客户端同步。 每个客户端 XA 还可以访问 [Session-XA] 数据池。由于该数据池专为其保留,因此此处无需进行同步。
- Web 服务器同时为多个 Web 应用程序提供服务。这些不同应用程序的客户端之间不会产生干扰。
根据上述说明,我们可以总结以下要点:
- 在任何给定时刻,Web 服务器都会同时为多个客户端提供服务。这意味着它不会等待一个请求完成后才处理另一个请求。因此,在时间点 T,会有属于不同客户端、针对不同应用程序的多个请求正在被处理。在 Web 服务器内部同时运行的处理代码有时被称为执行线程。
- 来自不同 Web 应用程序的客户端的执行线程互不干扰。它们之间存在隔离。
- 来自同一应用程序的客户端的执行线程可能需要共享数据:
- 来自两个不同客户端(非同一会话令牌)的请求的执行线程可通过 [Application] 对象共享数据。
- 来自同一客户端的连续请求的执行线程可通过 [Session] 对象共享数据。
- 处理来自特定客户端同一请求的连续页面的执行线程可通过 [Context] 对象共享数据。
4.1.3.4. 示例 2
让我们开发一个新示例来说明刚才的内容。我们将以下文件放在同一个文件夹中:
[global.asax]
[global.asax.vb]
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when application is started
' init customer counter
Application.Item("nbRequêtes") = 0
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when the session is started
' init query counter
Session.Item("nbRequêtes") = 0
End Sub
End Class
该应用程序的目的是统计发送到应用程序的请求总数以及每个客户端的请求数。当应用程序启动时 [Application_Start],发送到应用程序的请求计数器被设置为 0。该计数器位于 [Application] 作用域中,因为它必须由所有客户端进行递增。 当客户端首次连接时 [Session_Start],我们将该客户端的请求计数器设置为 0。该计数器位于 [Session] 作用域中,因为它仅适用于特定的客户端。
执行完 [global.asax] 后,将执行以下 [main.aspx] 文件:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
<head>
<title>application-session</title>
</head>
<body>
jeton de session :
<% =jeton %>
<br />
requêtes Application :
<% =nbRequêtesApplication %>
<br />
requêtes Client :
<% =nbRequêtesClient %>
<br />
</body>
</html>
它显示由其控制器计算出的三项信息:
- 通过会话令牌识别客户端身份:[token]
- 向应用程序发出的请求总数:[nbRequêtesApplication]
- 标识为 1 的客户端发出的请求总数:[nbClientRequests]
这三项信息在 [main.aspx.vb] 中计算得出:
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As String
Protected nbRequêtesClient As String
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' one more request for the
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
' one more request in the session
Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
' init presentation variables
nbRequêtesApplication = Application.Item("nbRequêtes").ToString
jeton = Session.SessionID
nbRequêtesClient = Session.Item("nbRequêtes").ToString
End Sub
End Class
当执行 [main.aspx.vb] 时,我们正在处理来自某个客户端的请求。我们使用 [Application] 对象来递增应用程序的请求次数,并使用 [Session] 对象来递增当前正在处理其请求的客户端的请求次数。请记住,虽然同一应用程序的所有客户端共享同一个 [Application] 对象,但每个客户端都有自己的 [Session] 对象。
我们将前四个文件放入一个名为 <application-path> 的文件夹中,并使用参数 (<application-path>,/aspnet/webapplia) 启动 Cassini 服务器,以此测试该应用程序。我们打开浏览器,访问 URL [http://localhost/aspnet/webapplia/main.aspx]:

我们使用 [Reload] 按钮发起第二次请求:

我们打开第二个浏览器请求同一 URL。对于 Web 服务器而言,这是一个新的客户端:

我们可以看到会话令牌已发生变化,这表明这是一个新客户端。这一点在客户端请求的数量上得到了体现。现在让我们回到第一个浏览器,再次请求相同的 URL:

应用程序接收的请求次数被正确统计。
4.1.3.5. 同步应用程序客户端的必要性
在前一个应用程序中,针对应用程序的请求计数器是在 [main.aspx] 页面的 [Form_Load] 过程 中按如下方式递增的:
' une requête de plus pour l'application
Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
虽然这条指令很简单,但执行它需要几条处理器指令。假设需要三条:
- 读取计数器
- 递增计数器
- 写入计数器
Web 服务器运行在多任务机器上,这意味着每个任务在失去处理器之前会获得几毫秒的处理时间,然后在所有其他任务也都获得过自己的时间份额后,再重新获得处理器。假设两个客户端 A 和 B 同时向 Web 服务器发出请求。 假设客户端 A 先执行,进入 [main.aspx.vb] 中的 [Form_Load] 过程,读取计数器(=100),随后因其时间片已到期而被中断。现在假设轮到客户端 B,而客户端 B 遭遇了同样的命运:它到达 方法,读取计数器值(=100),但没有时间将其递增。 客户端 A 和 B 的计数器值均为 100。假设轮到客户端 A 再次操作:它将计数器递增至 101,然后终止。现在轮到客户端 B 操作,但它持有的是旧的计数器值,而非新的值。因此,它也将计数器值设为 101 并终止。此时,应用程序的请求计数器值已不正确。
为说明此问题,我们将重新审视之前的应用程序并按以下方式进行修改:
- 文件 [global.asax]、[global.asax.vb] 和 [main.aspx] 保持不变
- [main.aspx.vb] 文件修改为如下内容:
Imports System.Threading
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As Integer
Protected nbRequêtesClient As Integer
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' one more request for the application and session
' meter reading
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' wait 5 s
Thread.Sleep(5000)
' meter incrementation
nbRequêtesApplication += 1
nbRequêtesClient += 1
' meter registration
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' init presentation variables
jeton = Session.SessionID
End Sub
End Class
计数器的递增过程分为四个阶段:
- 读取计数器
- 暂停执行线程
- 递增计数器
- 重写计数器
让我们再次考虑两个客户端 A 和 B。在请求计数器的读取阶段与递增阶段之间,我们强制执行线程暂停 5 秒。这会立即导致该线程失去处理器,处理器随后将被分配给另一个任务。 假设客户端 A 先执行。它将读取计数器值 N,随后被中断 5 秒。如果在此期间客户端 B 获得了 CPU 访问权限,它也应读取相同的计数器值 N。最终,两个客户端应显示相同的计数器值,这将是不正常的。
我们将前四个文件放置在一个名为 <application-path> 的文件夹中,并使用参数 (<application-path>,/aspnet/webapplib) 启动 Cassini 服务器,以此测试该应用程序。 我们分别在两个不同的浏览器中输入 URL [http://localhost/aspnet/webapplib/main.aspx]。首先启动第一个浏览器请求该 URL,然后不等待 5 秒后到达的响应,立即启动第二个浏览器。5 秒多一点后,我们得到以下结果:

我们看到:
- 存在两个不同的客户端(会话令牌不相同)
- 每个客户端都已发起请求
- 因此,其中一个浏览器的应用程序请求计数器应显示为 2。但实际情况并非如此。
现在,让我们尝试另一个实验。使用同一浏览器,向 URL [http://localhost/aspnet/webapplib/main.aspx] 发送五个请求。同样,我们连续发送这些请求,不等待结果。当所有请求执行完毕后,我们得到最后一个请求的以下结果:

我们可以观察到:
- 由于客户端请求计数器显示为5,这表明这5个请求被视为来自同一个客户端。虽然上文未展示,但我们可以看到这5个请求的会话令牌确实是相同的。
- 应用程序的请求计数器是准确的。
我们能得出什么结论?尚无定论。或许当客户端已有请求正在处理时,Web 服务器不会开始执行该客户端的新请求?因此,来自同一客户端的请求永远不会同时执行,而是依次执行。这一点需要进一步验证。这可能确实取决于所使用的客户端类型。
4.1.3.6. 客户端同步
前一个应用程序中突显的问题是资源独占访问的经典(但难以解决)问题。在我们的具体案例中,必须确保两个客户端 A 和 B 不能同时处于以下代码序列中:
- 读取计数器
- 递增计数器
- 写入计数器
这样的代码序列被称为临界区。它要求同时执行该代码的线程进行同步。 .NET 平台提供了多种工具来确保这一点。在此,我们将使用 [Mutex] 类。

在此,我们将仅使用以下构造函数和方法:
创建一个同步对象 M | |
执行 M.WaitOne() 操作的线程 T1 请求对同步对象 M 的所有权。如果互斥锁 M 尚未被任何线程持有(初始情况),则将其“授予”请求它的线程 T1。 如果稍后线程 T2 执行相同的操作,它将被阻塞。这是因为互斥锁一次只能属于一个线程。当线程 T1 释放其持有的互斥锁 M 时,该互斥锁将被释放。因此,多个线程在等待互斥锁 M 时可能会被阻塞。 | |
执行 M.ReleaseMutex() 操作的线程 T1 会放弃对互斥锁 M 的持有权。当线程 T1 失去处理器控制权时,系统可将其分配给正在等待互斥锁 M 的某个线程。只有一个线程会依次获得它;其余等待 M 的线程仍处于阻塞状态 |
互斥锁 M 管理对共享资源 R 的访问。线程通过 M.WaitOne() 请求资源 R,并通过 M.ReleaseMutex() 释放它。必须由单个线程在特定时间点执行的代码关键区即为共享资源。关键区执行的同步可通过以下方式实现:
其中 M 是一个互斥锁对象。当然,绝不能忘记释放不再需要的互斥锁,以便其他线程能够依次进入临界区;否则,等待着永远不会被释放的互斥锁的线程将永远无法获得处理器访问权限。此外,必须避免两个线程相互等待的死锁情况。考虑以下按顺序发生的操作:
- 线程 T1 获取互斥锁 M1 的控制权以访问共享资源 R1
- 线程 T2 获取互斥锁 M2 以访问共享资源 R2
- 线程 T1 请求互斥锁 M2,被阻塞
- 线程 T2 请求互斥锁 M1,被阻塞。
在此,线程 T1 和 T2 处于相互等待的状态。当线程需要两个共享资源时,即由互斥锁 M1 控制的资源 R1 和由互斥锁 M2 控制的资源 R2,就会发生这种情况。一种可能的解决方案是使用单个互斥锁 M 同时请求这两个资源。但这并非总是可行,特别是如果这会导致对昂贵资源的长期锁定。 另一种解决方案是:持有 M1 但无法获得 M2 的线程应释放 M1,以避免死锁。
若将上述知识付诸实践,我们的应用程序将如下所示:
- [global.asax] 和 [main.aspx] 文件保持不变
- [global.asax.vb] 文件变为如下内容:
Imports System
Imports System.Web
Imports System.Web.SessionState
Imports System.Threading
Public Class global
Inherits System.Web.HttpApplication
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when application is started
' init customer counter
Application.Item("nbRequêtes") = 0
' create a synchronization lock
Application.Item("verrou") = New Mutex
End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
' Triggered when the session is started
' init query counter
Session.Item("nbRequêtes") = 0
End Sub
End Class
唯一的新功能是创建了一个 [Mutex],客户端将使用它进行同步。由于所有客户端都必须能够访问它,因此将其放置在 [Application] 对象中。
- [main.aspx.vb] 文件内容如下:
Imports System.Threading
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As Integer
Protected nbRequêtesClient As Integer
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' one more request for the application and session
' enter a critical section - retrieve the synchronization lock
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' we ask you to enter the following critical section on your own
verrou.WaitOne()
' meter reading
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' wait 5 s
Thread.Sleep(5000)
' meter incrementation
nbRequêtesApplication += 1
nbRequêtesClient += 1
' meter registration
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' allows access to the critical section
verrou.ReleaseMutex()
' init presentation variables
jeton = Session.SessionID
End Sub
End Class
我们可以看到,客户端:
- 请求单独进入临界区。为此,它请求独占互斥锁(mutex)的所有权
- 在临界区结束时释放互斥锁 [lock],以便另一个客户端可以依次进入临界区。
我们将前四个文件放置在一个名为 <application-path> 的文件夹中,并使用参数 (<application-path>,/aspnet/webapplic) 启动 Cassini 服务器,以此测试该应用程序。 我们打开两个不同的浏览器,访问 URL [http://localhost/aspnet/webapplic/main.aspx]。首先启动第一个浏览器请求该 URL,然后不等待 5 秒后到达的响应,立即启动第二个浏览器。5 秒多一点后,我们得到以下结果:

这次,应用程序的请求计数器显示正确。
从这一冗长的演示中得出的关键结论是:如果同一 Web 应用程序的客户端需要更新所有客户端共享的元素,则必须进行同步。
4.1.3.7. 会话令牌管理
我们已多次讨论过客户端与 Web 服务器之间交换的会话令牌。让我们回顾一下其工作原理:
- 客户端向服务器发起初始请求。此时不发送会话令牌。
- 由于请求中缺少会话令牌,服务器会将其识别为新客户端并分配一个令牌。该令牌关联着一个 [Session] 对象,用于存储该客户端的特定信息。此后,该令牌将随该客户端的所有请求一同发送,并包含在对客户端首次请求的响应 HTTP 头中。
- 客户端现在已知晓其会话令牌。在后续向 Web 服务器发出的每次请求中,它都会将该令牌包含在 HTTP 头中。借助该令牌,服务器将能够检索与该客户端关联的 [Session] 对象。
为演示此机制,我们将重温之前的应用程序,仅修改 [main.aspx.vb] 文件:
Imports System.Threading
Public Class main
Inherits System.Web.UI.Page
Protected nbRequêtesApplication As Integer
Protected nbRequêtesClient As Integer
Protected jeton As String
Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' one more request for the application and session
' enter a critical section - retrieve the synchronization lock
Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
' you ask to enter the next section on your own
verrou.WaitOne()
' meter reading
nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
' wait 5 s
Thread.Sleep(5000)
' counter incrementation
nbRequêtesApplication += 1
nbRequêtesClient += 1
' meter registration
Application.Item("nbRequêtes") = nbRequêtesApplication
Session.Item("nbRequêtes") = nbRequêtesClient
' allows access to the critical section
verrou.ReleaseMutex()
' init presentation variables
jeton = Session.SessionID
End Sub
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' the client request is stored in request.txt of the application folder
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
End Class
当 [Page_Init] 事件发生时,我们将客户端的请求保存到应用程序目录中。让我们回顾几个要点:
- [TemplateSourceDirectory] 代表当前运行页面的虚拟路径,
- MapPath(TemplateSourceDirectory) 表示相应的物理路径。这使我们能够构建待创建文件的物理路径,
- [Request] 是一个表示当前正在处理的请求的对象。该对象是通过处理客户端发送的原始请求生成的,即以下形式的文本行序列:

- Request.Save([FileName]) 将整个客户端请求(包括 HTTP 头部以及后续的文档内容)保存到路径作为参数传递的文件中。
因此,我们将能够确切地知道客户端的请求内容。我们通过将前四个文件放置在一个名为 <application-path> 的文件夹中来测试应用程序,并使用参数 (<application-path>,/aspnet/session1) 启动 Cassini 服务器。然后,使用浏览器请求 URL
[http://localhost/aspnet/session1/main.aspx]。结果如下:

我们使用 [main.aspx.vb] 保存的 [request.txt] 文件来查看浏览器的请求:
GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316
我们可以看到,浏览器对 URL [/aspnet/session1/main.aspx] 发出了请求,并发送了上一章中讨论过的其他信息。这里没有显示会话令牌。响应中收到的页面显示服务器已创建了一个会话令牌。我们尚不清楚浏览器是否已收到该令牌。现在让我们使用同一浏览器(刷新)发出第二次请求。我们收到以下新的响应:

会话跟踪确实有效,因为会话请求计数已正确递增。现在让我们查看 [request.txt] 文件的内容:
GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Cookie: ASP.NET_SessionId=y153tk45sise0lrhdzrf22m3
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316
我们可以看到,对于第二个请求,浏览器向服务器发送了一个新的 HTTP 头 [Cookie:],定义了一段名为 [ASP.NET_SessionId] 的信息,其值等于第一个请求的响应中出现的会话令牌。 利用此令牌,Web 服务器将把此新请求与由令牌 [y153tk45sise0lrhdzrf22m3] 标识的 [Session] 对象关联起来,并检索相关的请求计数器。
由于无法访问服务器的 HTTP 响应,我们仍然不清楚服务器向客户端发送令牌的具体机制。回顾一下,该响应与客户端的请求具有相同的结构,即一组格式如下所示的文本行:

我们之前曾使用过一个能访问 Web 服务器 HTTP 响应的 Web 客户端:curl 客户端。我们将再次在命令行窗口中使用它,查询与之前浏览器相同的 URL:
E:\curl>curl --include http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:31:42 GMT
X-AspNet-Version: 1.1.4322
Set-Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
qxnxmqmvhde3al55kzsmx445
<br>
requêtes Application :
3
<br>
requêtes Client :
1
<br>
</body>
</HTML>
我们已经找到了问题的答案。Web 服务器会以 HTTP 标头 [Set-Cookie:] 的形式发送会话令牌:
现在让我们不发送会话令牌来发送相同的请求。我们得到以下响应:
E:\curl>curl --include http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:36:06 GMT
X-AspNet-Version: 1.1.4322
Set-Cookie: ASP.NET_SessionId=cs2p12mehdiz5v55ihev1kaz; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
cs2p12mehdiz5v55ihev1kaz
<br>
requêtes Application :
4
<br>
requêtes Client :
1
<br>
</body>
</HTML>
由于我们未将会话令牌发回,服务器无法识别我们,因此生成了一个新令牌。要延续现有会话,客户端必须将会话令牌发回给服务器。我们将在此使用 curl 的 [--cookie key=value] 选项来实现,该选项会生成 [Cookie: key=value] HTTP 头。我们看到浏览器在其第二次请求中发送了此 HTTP 头。
E:\curl>curl --include --cookie ASP.NET_SessionId=cs2p12mehdiz5v55ihev1kaz http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:40:20 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
cs2p12mehdiz5v55ihev1kaz
<br>
requêtes Application :
5
<br>
requêtes Client :
2
<br>
</body>
</HTML>
有几点值得注意:
- 客户端请求计数器确实已递增,表明服务器已成功识别我们的令牌。
- 页面显示的会话令牌确实是我们发送的那个
- 会话令牌不再包含在 Web 服务器发送的 HTTP 头中。实际上,服务器仅在生成令牌时(即新会话开始时)发送一次。一旦客户端获取了令牌,是否使用该令牌以供识别便由客户端自行决定。
没有任何机制能阻止客户端使用多个会话令牌,如下面的 [curl] 示例所示,其中我们使用了首次请求(请求 #1)中获取的令牌:
E:\curl>curl --include --cookie ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445 http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:48:47 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
qxnxmqmvhde3al55kzsmx445
<br>
requêtes Application :
6
<br>
requêtes Client :
2
<br>
</body>
</HTML>
这个示例意味着什么?我们发送了一个之前获取的令牌。当 Web 服务器生成一个令牌时,只要与该令牌关联的客户端继续向其发送请求,它就会保留该令牌。经过一段时间的闲置(IIS 的默认值为 20 分钟)后,该令牌会被删除。前面的示例表明,我们使用了一个仍然有效的令牌。
您可能好奇 [curl] 客户端在所有这些操作过程中发送了哪些 HTTP 请求。我们知道这些请求已记录在 [request.txt] 文件中。以下是最后一条:
GET /aspnet/session1/main.aspx HTTP/1.1
Pragma: no-cache
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4
发送会话令牌的 HTTP 头确实存在。
服务器通过 [Set-Cookie:] HTTP 头传输的信息被称为 Cookie。 服务器可以利用此机制传输除会话令牌以外的信息。当服务器 S 将 Cookie 传输给客户端时,它还会指定 Cookie 的有效期 D 和相关的 URL U。这意味着,当客户端在 向服务器 S 请求形式为 /U/path 的 URL 时,如果客户端超过 D 时间未收到该 Cookie,服务器可能会返回该 Cookie。没有任何东西能阻止客户端无视这一行为准则。 然而,浏览器确实遵守这一规范。部分浏览器允许用户查看收到的 Cookie 内容,Mozilla 浏览器便是如此。例如,以下是前文示例中服务器发送的 Cookie 相关信息:

其中包括:
- Cookie名称 [ASP.NET_SessionId]
- 其值 [y153...m3]
- 关联的机器 [localhost]
- 关联的 URL [/]
- 其有效期 [会话结束时]
因此,浏览器每次请求形式为 [http://localhost/...] 的 URL 时,即每次向 [localhost] 机器上的 Web 服务器请求 URL 时,都会发送该会话令牌。该 Cookie 的有效期即为会话的有效期。对浏览器而言,这意味着该 Cookie 永不过期。它每次向 [localhost] 机器请求 URL 时都会发送该 Cookie。 因此,如果浏览器在第 D 天收到会话令牌,随后关闭浏览器,并在次日重新打开,它将重新发送该会话令牌(该令牌已被存储在文件中)。服务器将收到此令牌,但此时服务器上已不存在该令牌,因为会话令牌在服务器上的生命周期是有限的(在 IIS 上为 20 分钟)。因此,服务器将启动一个新的会话。
用户可以在浏览器中禁用 Cookie。在这种情况下,客户端虽然会接收会话令牌,但不会将其发回,从而阻止了会话跟踪。为了演示这一点,我们将在浏览器(此处为 Mozilla)中禁用 Cookie:

此外,我们删除所有现有的 Cookie:

完成上述操作后,我们重启 Cassini 服务器以从头开始,并通过浏览器再次请求 URL [http://localhost/aspnet/session1/main.aspx]:

让我们看看浏览器是否存储了 Cookie:

我们可以看到,浏览器并未存储服务器发送的会话令牌 Cookie。因此,我们可以预期不会进行会话跟踪。我们再次请求同一 URL(刷新页面):

结果完全符合预期。尽管浏览器曾接收过会话令牌但未将其存储,因此并未将其发回。 因此,服务器使用新的令牌启动了一个新会话。这个示例告诉我们,如果用户在浏览器中禁用了 Cookie,我们的会话跟踪策略就会失效。不过,除了 Cookie 之外,还有另一种方法可以在服务器和客户端之间交换会话令牌。实际上,我们可以通知 Web 服务器应用程序正在无 Cookie 环境下运行。这可以通过 [web.config] 配置文件来实现:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<system.web>
<sessionState cookieless="true" timeout="10" />
</system.web>
</configuration>
上述配置文件表明,应用程序将不使用 Cookie 运行(cookieless="true"),且会话令牌的最大闲置时长为 10 分钟(timeout="10")。超过此时间后,与该令牌关联的会话将被终止。服务器与客户端之间交换会话令牌的过程如下:
- 客户端请求 URL [http://machine:port/V/chemin],其中 V 是 Web 服务器上的一个虚拟目录
- 服务器生成令牌 J,并指示客户端重定向至 URL [http://machine:port/V/(J)/path]。因此,服务器将令牌放置在待请求的 URL 中,紧跟在虚拟目录 V 之后
- 客户端遵循此重定向并请求新 URL [http://machine:port/V/(J)/path]。
- 服务器响应此请求并发送响应页面。
让我们通过实例说明这些要点。我们将之前的整个应用程序放入一个新文件夹 <application-path> 中。我们将之前的 [web.config] 文件也放入该文件夹。此外,我们修改呈现代码 [main.aspx] 以包含一个链接:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<HEAD>
<title>application-session</title>
</HEAD>
<body>
jeton de session :
<% =jeton %>
<br>
requêtes Application :
<% =nbRequêtesApplication %>
<br>
requêtes Client :
<% =nbRequêtesClient %>
<br>
<a href="main.aspx">Recharger l'application</a>
</body>
</HTML>
此链接指向 [main.aspx] 页面,因此相当于浏览器的“重新加载”按钮。Cassini 服务器将使用参数 (<application-path>,/session2) 启动。 我们偏离了通常指定虚拟目录 [/aspnet/XX] 的做法。这是因为,由于 URL 中插入了会话令牌,虚拟目录必须仅包含 /XX 部分。我们首先使用 [curl] 客户端请求 URL [http://localhost/session2/main.aspx]:
E:\curl>curl --include http://localhost/session2/main.aspx
HTTP/1.1 302 Found
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 13:52:36 GMT
X-AspNet-Version: 1.1.4322
Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 163
Connection: Close
<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href='/session2/(hinadjag3bt0u155g5hqe245)/main.aspx'>here
</body></html>
我们可以看到,服务器返回的 HTTP 头为 [HTTP/1.1 302 Found],而非 [HTTP/1.1 200 OK]。该头指示客户端重定向至 HTTP Location 头指定的 URL [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]。 我们可以看到会话令牌已被插入到重定向 URL 中。收到此响应的浏览器会向用户透明地请求新 URL,用户不会看到这个新请求。如果浏览器无法自行处理重定向,则会随上述 HTTP 状态码一起发送一个 HTML 文档。该文档包含指向重定向 URL 的链接,用户可以点击该链接。
现在,让我们在禁用了 Cookie 的浏览器中进行同样的操作。我们再次请求 URL [http://localhost/session2/main.aspx]。我们从服务器收到以下响应:

首先,请注意浏览器显示的 URL 并非我们请求的那个。这表明发生了重定向。实际上,浏览器总是显示最后接收到的文档的 URL。因此,如果它没有显示 URL [http://localhost/session2/main.aspx],就意味着它被指示重定向到另一个 URL。可能存在多次重定向。浏览器显示的 URL 是最后一次重定向的 URL。 我们可以看到,会话令牌出现在浏览器显示的 URL 中。之所以能看到这一点,是因为我们的程序也在页面上显示了该令牌。
让我们回顾一下页面上放置的链接代码:
<a href="main.aspx">Recharger l'application</a>
这是一个相对链接,因为它不以 / 字符开头,否则就会成为绝对链接。相对于什么?要理解这一点,我们需要查看当前显示文档的 URL:[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]。 该文档中出现的任何相对链接,其基准路径均为 [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)]。因此,上述链接等同于以下链接:
<a href=" http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx">Recharger l'application</a>
当我们将鼠标悬停在链接上时,浏览器会显示如下内容:

如果点击 [重新加载应用程序] 链接,将调用 URL
[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]。因此,服务器将接收会话令牌,并能检索与其关联的信息。以下是服务器的响应内容:

需要注意的是,如果我们在 Web 应用程序中需要跟踪会话,且不确定该应用程序的客户端浏览器是否允许使用 Cookie,那么
- 必须将应用程序配置为在不使用 Cookie 的情况下运行
- 应用程序的页面必须使用相对链接而非绝对链接
4.2. 从客户端请求中获取信息
4.2.1. 客户端-服务器 Web 请求-响应循环
让我们回顾一下 Web 应用程序的客户端-服务器环境:

客户端对 Web 应用程序的请求处理流程如下:
- 客户端向托管 Web 应用程序的机器 M 上的 Web 服务端口 P 建立 TCP/IP 连接
- 它根据HTTP协议通过此连接发送一系列文本行。这组文本行构成了所谓的客户端请求,其格式如下:

请求发送后,客户端等待响应。
- HTTP 头部的第一行指定了向 Web 服务器请求的操作。它可以有以下几种形式:
- GET HTTP URL/<version>,其中<version>当前为1.0或1.1。在此情况下,请求不包含[Document]部分
- POST HTTP URL/<version>。在此情况下,请求包含一个 [Document] 部分,通常是一组供 Web 应用程序使用的信息列表
- PUT HTTP URL/<version>。客户端在 [Document] 部分发送一个文档,并希望将其存储在该 URL 对应的服务器上
当客户端希望向已连接的 Web 应用程序传输信息时,主要有两种方法:
- (待续)
- 其请求为 [GET enriched_url HTTP/<version>],其中 enriched_url 的形式为 [url?param1=val1¶m2=val2&...]。除 URL 外,客户端还会以 [key=value] 的形式传输一系列信息。
- 其请求格式为 [POST enriched-url HTTP/<version>]。在 [Document] 部分,它们以与之前相同的格式发送信息:[param1=val1¶m2=val2&...]。
- 在服务器端,整个客户端请求处理链可通过名为 Request 的全局对象访问该请求。Web 服务器已将整个客户端请求以稍后将探讨的格式放入此对象中。被请求的应用程序将处理此对象并为客户构建响应。该响应可在名为 Response 的全局对象中获取。 Web 应用程序的作用是根据接收到的 [Request] 对象构建 [Response] 对象。处理链中还包含 [Application] 和 [Session] 这两个全局对象,我们之前已对此进行过讨论,它们将允许应用程序在不同客户端之间(Application)或在同一客户端的连续请求之间(Session)共享数据。
- 应用程序将使用 [Response] 对象将其响应发送至服务器。一旦进入网络,该响应将采用以下 HTTP 格式:

响应发送后,服务器将关闭传入的网络连接(除非客户端指示其不要这样做)。
- 客户端将接收该响应,并随之关闭连接(出站方面)。对该响应的具体处理取决于客户端的类型。如果客户端是浏览器且接收到的文档是 HTML 文档,则会将其显示出来。如果客户端是程序,则会对响应进行解析和处理。
- 由于在请求-响应循环结束后,连接客户端与服务器的连接会被关闭,因此HTTP是一种无状态协议。在下一次请求时,客户端将与同一台服务器建立新的网络连接。由于这已不再是同一条网络连接,服务器在TCP/IP和HTTP层面上无法将此新连接与之前的连接建立关联。正是会话令牌系统实现了这种关联。
4.2.2. 检索客户端发送的信息
接下来我们将探讨 [Request] 对象的某些属性和方法,这些功能允许应用程序代码访问客户端的请求,从而获取其传输的信息。[Request] 对象的类型为 [HttpRequest]:

该类拥有众多属性和方法。我们需要关注 HttpMethod、QueryString、Form 和 Params 这些属性,它们将使我们能够访问信息字符串 [param1=val1¶m2=val2&...] 中的各项元素。
客户端请求方法:GET、POST、HEAD 等 | |
查询字符串 param1=val1¶m2=val2&... 中元素的集合,该字符串取自第一行 HTTP [method]?param1=val1¶m2=val2&...,其中 [method] 可以是 GET、POST、HEAD。 | |
来自查询字符串 param1=val1¶m2=val2&.. 的元素集合,位于请求的 [Document] 部分(POST 方法)。 | |
将多个集合(QueryString、Form、ServerVariables、Cookies)合并为一个集合。 |
4.2.3. 示例 1
让我们在第一个示例中实现这些元素。该应用程序仅包含一个元素 [main.aspx]。呈现代码 [main.aspx] 如下所示:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
<% = méthode %>
<br />
nom :
<% = nom %>
<br />
âge :
<% = age %>
<br />
</body>
</html>
该页面显示了由其控制器 [main.aspx.vb] 计算出的三项信息 [method, name, age]:
Public Class main
Inherits System.Web.UI.Page
Protected nom As String = "xx"
Protected age As String = "yy"
Protected méthode As String
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' the client request is stored in request.txt of the application folder
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve query parameters
méthode = Request.HttpMethod.ToLower
If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
If Not Request.QueryString("age") Is Nothing Then age = Request.QueryString("age").ToString
If Not Request.Form("nom") Is Nothing Then nom = Request.Form("nom").ToString
If Not Request.Form("age") Is Nothing Then age = Request.Form("age").ToString
End Sub
End Class
页面加载时(Form_Load),会从客户端请求中获取 [name, age] 信息。 我们在 [QueryString] 和 [Form] 这两个集合中查找这些信息。此外,在 [Page_Init] 中,我们会存储客户端请求,以便验证发送的内容。我们将这两个文件放在 <application-path> 文件夹中,并使用参数 (<application-path>,/request1) 启动 Cassini 服务器,然后使用浏览器请求 URL
[http://localhost/request1/main.aspx?nom=tintin&age=27]。我们收到如下响应:

客户端发送的信息已正确检索。存储在 [request.txt] 文件中的浏览器请求如下:
GET /request1/main.aspx?nom=tintin&age=27 HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316
我们可以看到浏览器发出了一个 GET 请求。要发出 POST 请求,我们将使用 [curl] 客户端。在 DOS 窗口中,我们输入以下命令:
用于显示响应的 HTTP 头部 | |
通过 POST 请求发送 param=value 信息 |
服务器的响应如下:
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Fri, 02 Apr 2004 09:27:25 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 178
Connection: Close
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
post
<br />
nom :
tintin
<br />
âge :
27
<br />
</body>
</html>
服务器再次成功检索到了本次通过POST请求发送的参数。要确认这一点,您可以查看[request.txt]文件的内容:
POST /request1/main.aspx HTTP/1.1
Pragma: no-cache
Content-Length: 17
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4
nom=tintin&age=27
[curl] 客户端成功发送了 POST 请求。现在,让我们结合这两种传递信息的方法。我们将 [age] 放入请求的 URL 中,并将 [name] 放入 POST 数据中:
[curl] 发送的请求如下(request.txt):
POST /request1/main.aspx?age=27 HTTP/1.1
Pragma: no-cache
Content-Length: 10
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4
nom=tintin
我们可以看到,年龄参数是通过请求的 URL 传递的。我们将从 [QueryString] 集合中获取该值。姓名参数是通过发送到该 URL 的文档传递的。我们将从 [Form] 集合中获取该值。客户端 [curl] 收到的响应:
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
post
<br />
nom :
tintin
<br />
âge :
27
<br />
</body>
</html>
最后,让我们不要向服务器发送任何信息:
E:\curl>curl --include http://localhost/request1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Fri, 02 Apr 2004 12:43:14 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 173
Connection: Close
<html>
<head>
<title>Requête client</title>
</head>
<body>
Requête :
get
<br />
nom :
xx
<br />
âge :
yy
<br />
</body>
</html>
建议读者查阅控制器代码 [main.aspx.vb] 以理解此响应。
4.2.4. 示例 2
客户端可能为同一个键发送多个值。那么,如果在前一个示例中,我们请求 URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou](其中键 [name] 出现两次),会发生什么情况呢?让我们在浏览器中试一试:

我们的应用程序已成功检索到与键 [name] 关联的两个值。显示结果可能有些误导性。它是通过以下语句获取的
If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
[ToString] 方法生成了字符串 [tintin,milou],并将其显示出来。这掩盖了一个事实:实际上,对象 [Request.QueryString("name")] 是一个字符串数组 {"tintin","milou"}。以下示例说明了这一点。[main.aspx] 呈现页将如下所示:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<HEAD>
<title>Requête client</title>
</HEAD>
<body>
<P>Informations passées par le client :</P>
<form runat="server">
<P>QueryString :</P>
<P><asp:listbox id="lstQueryString" runat="server" EnableViewState="False" Rows="6"></asp:listbox></P>
<P>Form :</P>
<P><asp:listbox id="lstForm" runat="server" EnableViewState="False" Rows="2"></asp:listbox></P>
</form>
</body>
</HTML>
本页面上的一些新功能使用了所谓的服务器控件。这些控件通过 [runat="server"] 属性进行标识。现在介绍服务器控件的概念还为时过早。目前只需知道:
- 该页面包含两个列表(<asp:listbox> 标签)
- 这些列表是 [ListBox] 类型的对象(lstQueryString、lstForm),将由页面控制器创建
- 这些对象仅存在于 Web 服务器中。当响应被发送时,它们会被转换为客户端可识别的标准 HTML 标签。因此,一个 [ListBox] 对象会被转换(或“渲染”)为 <select> 和 <option> HTML 标签。
- 这些对象的主要目的是将所有 VB 代码从表示层移除,使其仅保留在控制器中。
负责构建 [lstQueryString] 和 [lstForm] 这两个对象的控制器 [main.aspx.vb] 如下所示:
Imports System.Collections
Imports System
Imports System.Collections.Specialized
Public Class main
Inherits System.Web.UI.Page
Protected infosQueryString As ArrayList
Protected WithEvents lstQueryString As System.Web.UI.WebControls.ListBox
Protected WithEvents lstForm As System.Web.UI.WebControls.ListBox
Protected infosForm As ArrayList
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
' the client request is stored in request.txt of the application folder
Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
Me.Request.SaveAs(requestFileName, True)
End Sub
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' we retrieve the entire collection of information from QueryString
infosQueryString = getValeurs(Request.QueryString)
lstQueryString.DataSource = infosQueryString
lstQueryString.DataBind()
infosForm = getValeurs(Request.Form)
lstForm.DataSource = infosForm
lstForm.DataBind()
End Sub
Private Function getValeurs(ByRef data As NameValueCollection) As ArrayList
' starting with an empty info list
Dim infos As New ArrayList
' we retrieve the keys of the
Dim clés() As String = data.AllKeys
' browse the key table
Dim valeurs() As String
For Each clé As String In clés
' values associated with the key
valeurs = data.GetValues(clé)
' a single value?
If valeurs.Length = 1 Then
infos.Add(clé + "=" + valeurs(0))
Else
' several values
For ivalue As Integer = 0 To valeurs.Length - 1
infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
Next
End If
Next
' we return the result
Return infos
End Function
End Class
此代码的要点如下:
- 在 [Form_Load] 中,页面检索 [QueryString] 和 [Form] 这两个集合。它使用 [getValues] 函数将这两个集合的内容放入两个 [ArrayList] 对象中,如果集合键与单个值相关联,则这些对象将包含 [key=value] 形式的字符串;如果键与多个值相关联,则包含 [key(i)=value] 形式的字符串。
- 随后,通过以下两条语句,将每个 [ArrayList] 对象绑定到呈现页面上的一个 [ListBox] 对象:
- [ListBox.DataSource=ArrayList] 和 [ListBox.DataBind]。后者将 [DataSource] 中的元素传输到 [ListBox] 对象的 [Items] 集合中
请注意,这两个 [ListBox] 对象均未通过 [New] 操作显式创建。我们可以推断,当存在 <asp:listbox id="xx">...<asp:listbox/> 标签时,Web 服务器会自行创建该标签的 [id] 属性所引用的 [ListBox] 对象。
- [getValeurs] 函数使用作为参数传递给它的 [NameValueCollection] 对象,返回类型为 [ArrayList] 的结果。
我们将前两个文件放置在名为 <application-path> 的文件夹中,并使用参数 (<application-path>,/request2) 启动 Cassini 服务器,然后请求 URL
[http://localhost/request2/main.aspx?nom=tintin&age=27]。我们得到以下响应:

现在我们请求一个包含两次 [nom] 键的 URL:

我们可以看到,对象 [Request.QueryString("nom")] 确实是一个数组。在此,请求是使用 GET 方法发出的。我们使用 [curl] 客户端发出一个 POST 请求:
E:\curl>curl --data nom=milou --data nom=tintin --data age=14 --data age=27 http://localhost/request2/main.aspx
<HTML>
<HEAD>
<title>Requête client</title>
</HEAD>
<body>
<P>Informations passées par le client :</P>
<form name="_ctl0" method="post" action="main.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwtMTI3MjA1MzUzMTs7PtCDC7NG4riDYIB4YjyGFpVAAviD" />
<P>QueryString :</P>
<P><select name="lstQueryString" size="6" id="lstQueryString">
</select></P>
<P>Form :</P>
<P><select name="lstForm" size="2" id="lstForm">
<option value="nom(0)=milou">nom(0)=milou</option>
<option value="nom(1)=tintin">nom(1)=tintin</option>
<option value="age(0)=14">age(0)=14</option>
<option value="age(1)=27">age(1)=27</option>
</select></P>
</form>
</body>
</HTML>
我们可以看到,客户端接收到了页面上两个列表的标准 HTML 代码。其中出现了我们自己并未包含的信息,例如隐藏字段 [_VIEWSTATE]。这些信息是由 <asp:xx runat="server"> 标签生成的。我们需要学习如何有效地使用这些标签。
4.3. 实现 MVC 架构
4.3.1. 概念
让我们通过实现一个基于 MVC(模型-视图-控制器)模式构建的应用程序,来结束这一长篇章。按照此模式设计的 Web 应用程序结构如下:

- 客户端将其请求发送给应用程序中名为“控制器”的特定组件
- 控制器分析客户端的请求并执行它。为此,它依赖于包含应用程序业务逻辑的类以及数据访问类。
- 根据请求执行的结果,控制器会选择向客户端返回特定的页面
在我们的模型中,所有请求都通过一个单一的控制器,该控制器充当整个 Web 应用程序的协调者。该模型的优势在于,每次请求之前需要执行的所有操作都可以整合在控制器中。例如,假设应用程序需要身份验证,这只需执行一次。 认证成功后,应用程序会将刚完成认证的用户相关信息存储在会话中。由于客户端可以在未认证的情况下直接调用应用程序中的页面,因此每个页面都需要检查会话以确认认证是否确实已完成。如果所有请求都通过单个控制器,那么由控制器来执行此任务。最终接收该请求的页面则无需执行此操作。
4.3.2. 在不使用会话的情况下控制 MVC 应用程序
根据目前所见,人们可能会认为 [global.asax] 文件可以充当控制器。确实,我们知道所有请求都会经过它,因此它非常适合控制一切。 以下应用程序正是以此为目的。其虚拟路径为 [http://localhost/mvc1/main.aspx]。客户端通过在 URL 后附加 action=value 参数来指定请求内容。根据 [action] 参数的值,[global.asax] 控制器将把请求定向到特定页面:
- 如果 action 参数未定义,或者 action=main,则跳转至 [main.aspx]
- 若 action=action1,则跳转至 [action1.aspx]
- 如果 action 不属于情况 1 和 2,则为 [unknown.aspx]
页面 [main.aspx、action1.aspx、unknown.aspx] 仅显示触发其显示的 [action] 参数的值。下面我们列出该应用程序中的八个文件,并在必要处提供注释:
[global.asax]
[global.asax.vb]
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class Global
Inherits System.Web.HttpApplication
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' retrieve the action to be performed
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' put the action in the context of the request
Context.Items("action") = action
' execute the action
Select Case action
Case "main"
Server.Transfer("main.aspx", True)
Case "action1"
Server.Transfer("action1.aspx", True)
Case Else
Server.Transfer("inconnu.aspx", True)
End Select
End Sub
End Class
注意事项:
- 我们在 [Application_BeginRequest] 过程 中拦截所有客户端请求,该过程会在应用程序收到每个新请求时自动执行。
- 在此过程 中,我们可以访问 [Request] 对象,该对象代表客户端的 HTTP 请求。由于我们预期 URL 格式为 [http://localhost/mvc1/main.aspx?action=xx],因此会在 [Request.QueryString] 集合中查找名为 [action] 的键。如果不存在,则将 [action] 的默认值设置为“main”。
- [action] 参数的值会被存入 [Context] 对象中。与 [Application、Session、Request、Response、Server] 对象一样,该对象是全局的,任何代码均可访问。如果请求由多个页面处理(如本例所示),该对象会在页面间传递。一旦响应发送给客户端,该对象即被删除。因此,其生命周期仅限于请求处理期间。
- 根据 [action] 参数的值,请求会被转发至相应的页面。为此,我们使用全局 [Server] 对象,该对象通过其方法允许我们将当前请求转发至另一个页面。其第一个参数是目标页面的名称,第二个参数是一个布尔值,用于指示是否将 [QueryString] 和 [Form] 集合转发至目标页面。 在此,答案是“是”。
[main.aspx] 和 [main.aspx.vb] 文件:
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<head>
<title>main</title></head>
<body>
<h3>Page [main]</h3>
Action : <% =action %>
</body>
</HTML>
Public Class main
Inherits System.Web.UI.Page
Protected action As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve the current action
action = Me.Context.Items("action").ToString
End Sub
End Class
控制器 [main.aspx.vb] 仅从上下文中检索 [action] 键的值;该值由呈现代码显示。此处的目的是演示在处理同一客户端请求的不同页面之间传递 [Context] 对象。页面 [action1.aspx] 和 [inconnu.aspx] 的功能类似:
[action1.aspx]
<%@ Page src="action1.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="action1" %>
<HTML>
<head>
<title>action1</title></head>
<body>
<h3>Page [action1]</h3>
Action : <% =action %>
</body>
</HTML>
[action1.aspx.vb]
Public Class action1
Inherits System.Web.UI.Page
Protected action As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve the current action
action = Me.Context.Items("action").ToString
End Sub
End Class
[unknown.aspx]
<%@ Page src="inconnu.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="inconnu" %>
<HTML>
<head>
<title>inconnu</title></head>
<body>
<h3>Page [inconnu]</h3>
Action : <% =action %>
</body>
</HTML>
[unknown.aspx.vb]
Public Class inconnu
Inherits System.Web.UI.Page
Protected action As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve the current action
action = Me.Context.Items("action").ToString
End Sub
End Class
为了进行测试,将上述文件放置在 <application-path> 文件夹中,并使用参数 (<application-path>,/mvc1) 启动 Cassini。我们请求 URL [http://localhost/mvc1/main.aspx]:

该请求未发送任何 [action] 参数。应用程序控制器代码 [global.asax.vb] 渲染了页面 [main.aspx]。现在我们请求 URL [http://localhost/mvc1/main.aspx?action=action1]:

应用程序控制器代码 [global.asax.vb] 返回了页面 [action1.aspx]。现在我们请求 URL [http://localhost/mvc1/main.aspx?action=xx]:

该操作未被识别,控制器 [global.asax.vb] 渲染了页面 [unknown.aspx]。
4.3.3. 使用会话控制 MVC 应用程序
大多数情况下,客户端向应用程序发出的各种请求需要共享信息。我们已经看到解决此问题的可能方案:将待共享的信息存储在请求的 [Session] 对象中。该对象确实由所有请求共享,并能够以 (键, 值) 的形式存储信息,其中键的类型为 [String],值是任何从 [Object] 派生的类型。
在上一个示例中,与不同操作关联的各个页面是在 [global.asax.vb] 文件的 [Application_BeginRequest] 过程内调用的:
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
' retrieve the action to be performed
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' put the action in the context of the request
Context.Items("action") = action
' execute the action
Select Case action
Case "main"
Server.Transfer("main.aspx", True)
Case "action1"
Server.Transfer("action1.aspx", True)
Case Else
Server.Transfer("inconnu.aspx", True)
End Select
End Sub
事实证明,在 [Application_BeginRequest] 过程 中,无法访问 [Session] 对象。在执行转移到的页面上也是如此。因此,此模型不能用于具有会话的应用程序。 我们可以将控制器角色分配给任意页面,例如 [default.aspx]。随后,文件 [global.asax, global.asax.vb] 会被移除,并由文件 [default.aspx, default.aspx.vb] 取代:
[default.aspx]
[default.aspx.vb]
Imports System
Imports System.Web
Imports System.Web.SessionState
Public Class controleur
Inherits System.Web.UI.Page
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve the action to be performed
Dim action As String
If Request.QueryString("action") Is Nothing Then
action = "main"
Else
action = Request.QueryString("action").ToString.ToLower
End If
' put the action in the context of the request
Context.Items("action") = action
' retrieve the previous action if it exists
Context.Items("actionPrec") = Session.Item("actionPrec")
If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
' the current action is saved in the session
Session.Item("actionPrec") = action
' execute the action
Select Case action
Case "main"
Server.Transfer("main.aspx", True)
Case "action1"
Server.Transfer("action1.aspx", True)
Case Else
Server.Transfer("inconnu.aspx", True)
End Select
End Sub
End Class
为了突出会话机制,各个页面不仅会显示当前操作,还会显示前一个操作。对于操作序列 A1、A2、...、An,当操作 Ai 发生时,上面的控制器:
- 将当前操作 Ai 放入上下文中
- 从会话中检索前一个操作 Ai-1。如果不存在(如操作 A1 的情况),则将前一个操作设置为空字符串。
- 将当前操作 Ai 放入会话中以替换 Ai-1
- 将执行权移交给相应的页面
该应用程序的三个页面如下:
[main.aspx]
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<HEAD>
<title>main</title>
</HEAD>
<body>
<h3>Page [main]</h3>
Action courante :
<% =action %>
<br>
Action précédente :
<% =actionPrec %>
</body>
</HTML>
[action1.aspx]
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<head>
<title>action1</title></head>
<body>
<h3>Page [action1]</h3>
Action courante :
<% =action %>
<br>
Action précédente :
<% =actionPrec %>
</body>
</HTML>
[unknown.aspx]
<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
<head>
<title>inconnu</title>
</head>
<body>
<h3>Page [inconnu]</h3>
Action courante :
<% =action %>
<br>
Action précédente :
<% =actionPrec %>
</body>
</HTML>
由于这三个页面显示的信息相同 [action, actionPrec],因此它们可以共用同一个页面控制器。因此,我们在 [main.aspx.vb] 文件中让它们都继承自 [main] 类:
Public Class main
Inherits System.Web.UI.Page
Protected action As String
Protected actionPrec As String
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' retrieve the current action
action = Me.Context.Items("action").ToString
' and the previous action
actionPrec = Me.Context.Items("actionPrec").ToString
End Sub
End Class
上述代码仅用于检索应用程序控制器 [default.aspx.vb] 放置在上下文中的信息。
所有这些文件都放置在 <application-path> 中,Cassini 通过参数 (<application-path>,/mvc2) 启动。我们首先请求 URL [http://localhost/mvc2]:

URL [http://localhost/mvc2] 指向一个文件夹。我们知道,在此情况下,如果该文件夹中存在 [default.aspx] 文档,服务器将返回该文档。此处未指定具体操作,因此执行了 [main] 操作。接下来我们尝试 [action1] 操作:

当前操作和上一个操作均被正确识别。接下来我们来看操作 [xx]:

4.4. 结论
现在我们已经掌握了构建每个 ASP.NET 应用程序所需的基本要素。不过,还有一个重要的概念尚未介绍:表单。这将是下一章的主题。