Web 应用程序状态






4.87/5 (15投票s)
本文讲解如何维护 Web 应用程序的状态。
引言
Web 应用程序状态
每次将 Web 页面发布到服务器时,都会重新创建该页面。在传统的 Web 编程中,这意味着页面内的所有信息以及控件中的信息都会在每次往返时丢失。为了克服传统 Web 编程的这一限制,ASP.NET 页面框架提供了各种选项来帮助我们保留更改。一种选项涉及将信息保留在客户端,直接保留在页面或 Cookie 中,另一种选项涉及在往返之间将信息存储在服务器上。以下对象用于保存应用程序状态:HttpSessionState
、HttpApplicationState
、ViewState
、HttpCookie
、HiddenFiled
,此外查询字符串和数据库也可以充当状态保存器。因此,我们可以说管理 Web 应用程序状态主要有两种方式:客户端和服务器端。
客户端状态管理
Cookie
Cookie 是存储在客户端文件系统中的文本文件或客户端浏览器会话中的内存中的少量数据。Cookie 主要用于跟踪数据设置。举个例子:假设我们想自定义一个欢迎网页;当用户请求默认网页时,应用程序首先检测用户是否之前登录过;在这种情况下,我们可以从 Cookie 中检索用户信息。
隐藏字段
保存数据状态的默认方式是使用 HTML 隐藏字段。隐藏字段使用 HTML <input type="hidden">
存储文本数据。隐藏字段在浏览器中不可见,但我们可以像设置其他标准控件的属性一样设置它的属性。当页面发布到服务器时,隐藏字段的内容会与其它控件的值一起在 HTTP Form
集合中发送。为了在页面处理期间可用隐藏字段值,我们必须提交页面。
视图状态(ViewState)
Control.ViewState
属性提供了一种在同一页面的多次请求之间保留值的方法。这是页面在往返之间用来保留页面和控件属性值的方法。当页面被处理时,页面的当前状态和控件的状态被哈希成一个字符串并作为隐藏字段保存在页面中。当页面发布回服务器时,页面在页面初始化时解析视图状态字符串并恢复页面中的属性信息。
查询字符串
查询字符串提供了一种简单但有限的方式来维护一些状态信息。你可以轻松地将信息从一个页面传递到另一个页面,但大多数浏览器和客户端设备对 URL 的长度都有限制,通常为 255 个字符。此外,查询值会通过 URL 暴露给 Internet,因此在某些情况下,安全性可能是一个问题。带有查询字符串的 URL 可能如下所示:http://myapplication.com/listing.aspx?group=1&item=1。
服务器端状态管理
Application 对象
Application
对象提供了一种存储在 Web 应用程序内所有代码都可以访问的数据的机制。最适合插入应用程序状态变量的数据是多个会话共享且不经常更改的数据。而且,由于它对整个应用程序可见,因此需要使用 Lock
和 UnLock
方法来避免冲突值。
Session 对象
Session
对象可用于存储需要在服务器往返和页面请求之间维护的特定于会话的信息。Session
对象是按客户端划分的,这意味着不同的客户端会生成不同的 Session
对象。最适合存储在会话状态变量中的数据是短暂的、敏感的、特定于单个会话的数据。
使用数据库
当存储用户特定的信息且存储量很大时,使用数据库维护状态是一种非常常见的方法。数据库存储对于维护长期状态或即使服务器必须重新启动也必须保留的状态特别有用。数据库方法通常与 Cookie 一起使用。例如,当用户访问应用程序时,他可能需要输入用户名/密码进行登录。你可以在数据库中查找用户,然后向用户传递一个 Cookie。Cookie 可能只包含你数据库中用户的 ID。然后,你可以在后续的请求中使用该 Cookie 来根据需要查找数据库中的用户信息。
问题
如上所述,Web 应用程序是无状态的,因此你需要实现数据持有者来为 Web 应用程序提供状态。例如,以下代码演示了 HttpSessionState
的基本用法。
MyDtoObject dtoObject = new MyDtoObject();
dtoObject.FirstName = Session["FirstName"] as string;
dtoObject.LastName = Session["LastName"] as string;
dtoObject.Login = Session["Login"] as string;
//and so on
但是,如果需求强制我暂时将 MyDtoObject
存储在数据库中呢?然后,它迫使我将 MyDtoObject
存储在 HttpApplicationState
中,以便在用户会话之间共享。如果我必须一直修改大量文件,这将非常困难。如果我需要一种定义如何存储 MyDtoObject
的能力,而无需重新编码呢?
解决方案
我认为直接使用 HttpSessionState
、HttpCookie
和 HttpApplicationState
不方便,因此我按照面向对象(OOP)范例设计了一些包装器。例如,我需要一个 ApplicationSettings
对象来将状态保存在 Cookie 中,并且它本身应该是一个单例对象。我只需要让 ApplicationSettings
类从 SingletonStateObject<ApplicationSettings, CookieStorage<ApplicationSettings>>
派生。
public class ApplicationSettings : SingletonStateObject<ApplicationSettings,
CookieStorage<ApplicationSettings>>
{
private string _hostName;
private int _counter;
public int Counter
{
get { return _counter; }
set { _counter = value; }
}
public string HostName
{
get { return _hostName; }
set { _hostName = value; }
}
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
ApplicationSettings settings = ApplicationSettings.Get();
Response.Write("ApplicationSettings:");
Response.Write("<br/>");
Response.Write("HostName: " + settings.HostName);
Response.Write("<br/>");
Response.Write("Counter: " + settings.Counter);
}
protected void uxSave_Click(object sender, EventArgs e)
{
ApplicationSettings settings = ApplicationSettings.Get();
settings.HostName = uxHostName.Text;
settings.Counter++;
settings.Save();
Response.Redirect("~/SingletonDefault.aspx");
}
SingletonStateObject
和 StateObject
是任何状态对象的基础类。这两个类都处理 IStorage<T>
的实现者。
IStorage
接口定义了从存储中保存、检索和删除数据的约定。
IStorage 实现者
StateObject
、SingletonStateObject
和 IStorage
的实现者共同演示了桥接模式(Bridge pattern),该模式允许以灵活的方式定义自己的状态对象。根据你的需求,你可以使用具体的存储实现者或实现自己的存储。
- 如果你需要在客户端存储少量信息,并且安全性不是问题,请使用
CookieStorage
。 - 如果你需要存储少量不经常更改的信息,并且这些信息是在用户之间共享的,请使用
ApplicationStorage
。 - 如果你需要存储特定于单个会话的短暂信息,并且安全性是一个问题,请使用
SessionStorage
。不要在会话状态对象中存储大量信息。请注意,会话状态对象将在应用程序中每个会话的生命周期内创建和维护。在托管大量用户的应用程序中,这可能会占用大量服务器资源并影响可伸缩性。 - 如果你需要存储大量信息并管理事务,或者信息必须在应用程序和会话重新启动后仍然存在,并且安全性是一个问题,请使用
DoaStorage
。
DaoStorage 实现
让我们回到 ApplicationSettings
对象。如果需求迫使我更改存储,并且 ApplicationSettings
不再是单例呢?需求迫使存储在应用程序和会话重新启动后仍然存在,因此我们需要使用数据库来保存状态。MS Access 数据库(附加到示例项目)中有一个具有 Key
和 Value
列的堆(Heap)表。我需要做的就是序列化对象并使用提供的键将其保存。
public class DaoStorage<T> : IStorage<T> where T : class
{
public void Save(object key, T obj)
{
string value = Serializer<T>.Serialize(obj);
using (OleDbConnection connection =
new OleDbConnection(Config.ConnectionString))
{
connection.Open();
if (IsExist(connection, key))
Update(connection, key, value);
else
Insert(connection, key, value);
}
}
public T Get(object key)
{
using (OleDbConnection connection =
new OleDbConnection(Config.ConnectionString))
{
connection.Open();
string obj = Get(connection, key);
return Serializer<T>.Deserialize(obj);
}
}
public void Delete(object key)
{
using (OleDbConnection connection =
new OleDbConnection(Config.ConnectionString))
{
connection.Open();
Delete(connection, key);
}
}
public List<T> GetAll(Type type)
{
using (OleDbConnection connection =
new OleDbConnection(Config.ConnectionString))
{
connection.Open();
return GetAll(connection);
}
}
public void Clear()
{
using (OleDbConnection connection =
new OleDbConnection(Config.ConnectionString))
{
connection.Open();
Clear(connection);
}
}
....
在这个类中,我使用 XmlSerialazer
将对象放入数据库,因此我们需要记住 ApplicationSettigs
必须是 XmlSerializable
的。让我们修改 ApplicationSettings
类
public class ApplicationSettings : StateObject<ApplicationSettings,
DaoStorage<ApplicationSettings>>
{
private string _hostName;
private int _counter;
public int Counter
{
get { return _counter; }
set { _counter = value; }
}
public string HostName
{
get { return _hostName; }
set { _hostName = value; }
}
}
这样就可以了,现在 ApplicationSettings
类不再是单例,也不再依赖于应用程序和会话的重新启动。它不再使用 HttpSessionState
或 HttpApplicationState
,而是将其状态序列化到数据库中。
可扩展性
使用 HttpSessionState
存在性能问题,使用 Cookie 存在安全问题。有许多替代存储可以使用,但如果我需要为一组对象提供可配置的存储策略该怎么办?让我们设计 ConfigurableStorage
,它允许我们从配置中定义存储策略。
public class ConfigurableStorage<T> : IStorage<T> where T : class
{
private readonly IStorage<T> _storage = ResolveStorage();
private static IStorage<T> ResolveStorage()
{
switch (Config.StorageType)
{
case StorageType.AppicationStorage:
return new ApplicationStorage<T>();
case StorageType.SessionStorage:
return new SessionStorage<T>();
case StorageType.FileStorage:
return new FileStorage<T>();
case StorageType.DaoStorage:
return new DaoStorage<T>();
case StorageType.CookieStorage:
return new CookieStorage<T>();
default:
throw new ApplicationException("StorageType");
}
}
public void Save(object key, T obj)
{
_storage.Save(key, obj);
}
public T Get(object key)
{
return _storage.Get(key);
}
....
如你所见,ConfigurableStorage
是一个简单的类,它创建另一种类型的存储并使用它来保存对象状态。它还演示了代理模式(Proxy pattern)。让我们修改 ApplicationSettings
类
public class ApplicationSettings : StateObject<ApplicationSettings,
ConfigurableStorage<ApplicationSettings>>
{
private string _hostName;
private int _counter;
public int Counter
{
get { return _counter; }
set { _counter = value; }
}
public string HostName
{
get { return _hostName; }
set { _hostName = value; }
}
}
现在,ApplicationSettings
继承自 ConfigurableStorage
,以及其他打算可配置的类。这是改变应用程序中一组对象存储策略的最无痛的方法。