提供一个更好的(ODP兼容的)ASP.NET Session对象






4.94/5 (28投票s)
2003年4月2日
25分钟阅读

156688

1035
本文讨论了.NET Session对象的ASP.NET会话状态维护和利用中存在的问题以及可用的解决方案。
引言
本文讨论了.NET Session对象的ASP.NET会话状态维护和利用中存在的问题以及可用的解决方案。我们将首先探讨使用现有会话的主要问题,介绍几种会话未能提供完全ODP兼容解决方案的场景。然后,我们将体验两种创建更好会话对象的方法,以及如何在任何所需应用程序中实现它们。除了本文的概念和架构方面,我还会介绍:如何从托管和非托管代码创建C++.NET DLL,MailSlots的工作方法,内存映射文件(MMF)的使用,同步/异步方法中的远程处理使用,以及使用C#代码创建新的Page
和Session
超类。本文是我在Sourceforge上启动的一个开源项目的概述[^])。如果您发现这些主题有趣,并且愿意贡献并迎接挑战,我们非常欢迎您的加入。
背景
微软发布ASP.NET时,附带了许多关于解决DNA架构中会话问题的声明。在使用ASP.NET并为大型组织构建主要基础设施项目后,我意识到“新技术”带来新问题的循环可能永远不会结束,并且当前的ASP.NET会话对象仍然需要进行重大改进。
让我们先介绍微软ASP.NET之前DNA架构中的三个主要问题
- 进程依赖性:如果Web进程因某种原因关闭,所有会话数据都将丢失。
- 服务器群集限制:在Web服务器群集中,会话不会在服务器之间进行转移。这意味着会话是特定于主机服务器的。如果会话数据集在一台机器上,则在其他机器上不可用。
- Cookie依赖性:会话数据基于Cookie,但并非所有浏览器都支持或允许Cookie,主要是出于安全原因。
微软确实解决了所有这些问题,会话状态可以通过在页面URL中嵌入一个唯一标识会话的字符串来供不支持Cookie的浏览器使用。进程依赖性和服务器群集限制问题通过与进程依赖性问题相同的解决方案得到了解决。此外,还引入了三个额外的选项来启用会话数据的托管。一个选项是将数据托管在进程数据内部(就像DNA中一样),第二个选项是将会话数据托管在进程外部,这样会话数据可以通过服务器群集中的每个服务器访问,而不仅仅是通过Web进程。第三个选项是将会话数据托管在SQL Server中。
让我们检查第二个选项,即提供进程外会话托管:状态机解决方案基于一台机器上的服务,该服务持有会话数据。该服务使用直接TCP调用来提供数据(不利用远程处理)。深入研究这个解决方案,我们发现了一个主要的缺点,如果服务器或服务崩溃或关闭会发生什么?似乎所有会话数据也会随之丢失。这肯定是一种不希望发生的情况。
一个更健壮的解决方案将是第三个选项,将会话数据托管在SQL Server上。这样数据将具有高可用性(SQL Server必须正常运行),但我们将付出“性能代价”(如表1.0所示)。使用第二和第三个选项会牺牲性能,主要是因为微软设计它们的方式是,每次您从Web应用程序调用会话数据时,该Web应用程序都会调用远程主机以获取会话数据;这个过程会浪费大量宝贵时间。
表 1.0 测试会话解决方案性能
RPS / 首次字节时间(毫秒) | 10(用户) | 50(用户) | 100(用户) |
进程内 | 493/8.78 | 522/25.90 | 521/40.73 |
进程外(状态机) | 319/12.65 | 341/44.67 | 333/100.10 |
进程外(SQL Server) | 130/73.35 | 117/342.00 | 97/903.00 |
(1GB RAM,2 * x86 Pentium III 800 Mhz)
如果你仔细阅读了上述内容,你可能会意识到肯定有改进的空间,以下是一些重要的概述:一个主要的改进是将会话数据保留在本地。这将消除远程调用,但这种增强需要一个新的机制来同步所有服务器的会话数据。否则,一台机器上的会话数据将与其他机器上的不相似。如前所述,保留会话数据的本地副本将主要防止由于机器外调用而导致的性能损失。在任何服务器上保存数据的本地副本并在Web服务器场中同步它们也将防止会话数据的单一副本。这将实现我们最渴望的目标:无单点故障(NSPOF)。总而言之,保留会话数据本地同步副本的主要优势是:
- 通过防止数据的远程调用而提高性能
- 通过会话数据同步实现的冗余解决方案。
正如我上面提到的,新的ASP.NET引入了新的问题(我宁愿称之为基础设施程序员的挑战)。有一个巨大的挑战来自于ASP.NET依赖于承载会话的*aspnet_wp*进程。这个事实阻止了ASP页面和其他进程共享ASP.NET应用程序的会话数据。主要障碍是当将ASP应用程序移植到ASP.NET应用程序,并且一些ASP页面留在新的ASP.NET应用程序中时。使用*aspnet_wp*进程,您将无法在ASP和ASP.NET会话之间共享会话数据。
在创建ASP应用程序时,您可以使用C++或Visual Basic DLL来获得更好的性能。在许多情况下,您希望将这些DLL托管在COM+中以启用COM+服务和功能。将您的.NET DLL托管在COM+中并启用对象池(仅C++ DLL)以获得更好的性能和对这些DLL的控制(例如,在不关闭整个Web应用程序的情况下关闭)可以通过将它们注册为服务器包来实现。将DLL注册为服务器包将导致一个专用进程`dllhost.exe`来运行这些DLL。这种方法的主要问题是这些DLL是应用程序的一部分,并且它们需要不时地访问会话数据。DNA架构通过允许将DLL注册为COM+应用程序或库包来解决此问题,从而可以访问所有ASP对象,包括会话数据。尽管这种构建Web应用程序的方式在ASP.NET中也是一个功能,但您无法从`aspnet_wp`以外的任何进程访问会话数据,即使它是一个DLLhost进程。可以通过将数据从网页传递到COM+ DLL来实现变通。当传输的参数数量变高时,此解决方案可能会产生新问题。
根据开放分布式处理标准[ODP-ISO/OSI 96]检查上述方法,发现会话与ODP标准不兼容。ODP关键定义规范了分布式透明性规定。当前的会话违反了两个透明性规定:第一个是故障透明性:所有使用会话的方式都只建立了单点故障,这对于Web应用程序是不透明的,并且肯定无法恢复。第二个是复制透明性:会话没有实现对象的复制以支持通用状态。探索ASP.NET会话架构揭示了针对所述问题的更好解决方案,并且更符合ODP。让我们假设会话数据将以一种每个进程都可以利用的方式保留,我们只需传递一个会话ID即可获取正确的会话数据。这样,我们将消除在进程之间复制会话数据的过程。
通过两种方法保留当前会话数据的本地副本并以每个进程都可以访问和获取会话数据的方式利用会话数据是可能的,这两种方法都基于相同的技术解决方案。我们需要创建一个机制,在主机上保存所有会话数据并与每个进程进行接口
- IPC (内存映射文件):MMF使我们能够将数据集中在计算机RAM中,并在进程之间共享这些数据。将会话数据与MMF结合使用将获得更好的性能。每个进程都可以通过专用的MMF访问、更改或设置数据,并且这些数据对所有其他进程都是透明的。此外,RAM中的数据可以由操作系统保留到物理文件,这些文件将在系统关闭时保留会话数据。使用MMF看起来是一个优雅的解决方案,但需要考虑一些缺点和限制。众所周知,会话数据是分层数据(即应用程序->会话ID->值键),需要以其插入时的相同分层结构进行保留。只有这样我们才能获得准确和及时的检索。STL映射(以及任何基于指针的结构)是保存会话数据的适当方法。如果我们能够将会话变量指针设置为一个映射,使其从映射文件在进程堆地址中的基地址开始,它可能会解决问题,但其困难在于无法在MMF视图的内存地址中保留复杂数据(包括指针)。克服映射问题的唯一方法是使用序列化将会话数据保存到MMF视图,然后将MMF视图的内容反序列化为映射对象。这种方法会降低性能,因为每次向映射添加数据时,我们都需要将其序列化到MMF视图中,以便其他进程注意到这些更改。还有一个未解决的问题,即处理来自除aspnet_wp之外的其他进程的会话数据的原因。如果会话数据应由其他进程更新,那么当我们从映射中检索数据时,我们需要从MMF视图中反序列化映射数据。
- 远程处理:远程处理是另一种可用于保存会话数据的方法。远程处理解决方案基于一个具有3个主要元素的Windows服务
- 哈希表中的数据
- 处理数据的类
- 维护单例对象以进行调用的监听器。
这样,每次远程调用服务类都会得到同一个对象,该对象在哈希表中保存数据,并且可以设置或获取这些数据。每个进程都可以通过远程处理访问该服务。通过将监听器从单例状态更改为单次调用并将会话数据维护在STL映射对象中,可以实现更好的性能。需要注意的是,
__gc
(托管)对象不能在C++.NET中声明为共享或全局。这种工作模式将RPS(每秒请求数)从100提高到140。远程处理选项允许我们消除每次获取/设置数据时的序列化,我们只需要在进程关闭或暂时不可用时使用序列化。这样,当服务器或进程重新启动时,我们将能够反序列化数据。序列化过程应根据会话数据大小因素仔细考虑。
使用代码
概述
* 正在进行的代码提供了替换会话数据机制的概念性建议。
所提出的解决方案将基于以下组件
- 一个包含三个页面的Web应用程序(用C#构建)
- MMF.aspx 用于使用内存映射文件选项。
- Remote.aspx 用于远程处理过程和
- RegSession.aspx 用于访问默认会话对象。
- 一个新的页面和会话超类(C#)嵌入在一个DLL中。这个DLL将负责替换当前的会话对象。
RemotingPage
和RemotingSession
将使用远程处理选项替换会话对象,而MMFPage
和MMFSession
将使用内存映射文件替换会话。 SeddionC
一个DLL(C++.NET 托管和非托管),它将会话数据保存在内存映射文件中,并维护到内存映射文件数据的访问通道。这个DLL将由以下部分组成- 一个
KDSession
类,该类将作为托管代码部分的入口点。 - 一个
MMF
类,它将负责从内存映射文件存储和检索数据。seddionC
DLL还将包含其他类,这些类将基于STL模板模拟会话数据的数据结构。由于在MMF中存储复杂数据的限制,在这个项目中,我们将使用字符串变量在内存映射文件中存储数据。
- 一个
- 一个Windows服务
tmpSessionSync
(C++.NET托管和非托管),用于保存会话数据,并通过监听器类使用远程处理授予对会话数据的访问权限。该服务还实现了Web服务器场中服务器之间的简单同步机制,以维护整个Web服务器场中会话数据的时效性。这种同步机制可以通过使用WIN32 MailSlots来完成。MailSlotReader
和MailSlotWriter
将帮助我们封装MailSlots的工作。UnManagedMSHandler
充当tmpSessionSync
服务中托管代码和非托管代码之间的网关。CRemotingSession
负责从托管世界获取请求。CUpdateClass
负责会话数据同步。CservicModule
是服务类,CListener
包含启动远程请求监听器的代码。
以下描述了所建议解决方案的两个主要场景
- 场景1 - 远程处理:我们将创建一个新的页面超类(
RemotingPage
)作为替换会话对象的入口。为此,我们还将使用一个新的会话类(RemotingSession
),它封装了对新的远程会话数据处理器(CservicModule
)的调用。页面超类(RemotingPage
)将主要覆盖现有的会话属性,并由此返回一个新的会话对象。会话数据处理器(CservicModule
)是一个Windows服务,旨在公开我们的远程处理类(CRemotingSession
)。CRemotingSession
类实现了一个接口(IRemotingSessionHandler
),该接口允许调用者获取/设置托管会话数据的值。这个接口也在新的会话类中用于调用远程处理器类。Windows服务还将包含一个监听器类(CListener
)。这个类将使我们的远程处理类可用。重要的是要记住,每个设置会话数据的请求都将引发对远程处理器类的异步调用,该调用将数据插入哈希表。结果是,每个会话数据请求都将以对远程处理器类的同步调用结束,该调用将从哈希表中获取数据。 - 场景2 - 内存映射文件(MMF):此场景从设计一个新的页面超类(
MMFPage
)开始,与第一个场景一样,它将替换会话对象。为了替换会话对象,我们将创建一个新的会话类(MMFSession
)。该类将封装对MMF处理器的调用。页面超类将覆盖现有的会话属性,并返回新创建的会话对象。MMF处理器是一个DLL(SeddionC
),用于将我们的文件视图映射到DLL主机进程及其内存地址空间。新的DLL在新的会话类之间交换请求,并在进程内存空间中获取/设置数据。这样,每个使用此DLL的进程都将映射到同一个MMF对象,并向调用进程提供相同的数据。
跨服务器同步:每个从我们新的会话类接收到的设置数据请求都将导致对Windows服务同步类(CUpdateClass
)的异步调用。该类的职责是获取Web服务器场中所有现有服务器的列表,并专门调用它们中的每一个以在本地设置会话数据。服务器列表由一个每分钟触发一次的计时器构建和触发。该计时器使用MailSlot(MailSlotWriter
)来发布服务器名称。相应地,Windows服务监听MailSlot(MailSlotReader
),这样,发送到MailSlot的每个数据都会被服务跟踪并添加到Web服务器场中所有“活动”服务器的列表中。
开始工作吧……
创建新的页面基类
我们将从Web应用程序开始。Web应用程序将基于三个网页,每个网页都使用会话对象来设置然后从会话数据中获取字符串值。这三个网页之间唯一的区别是每个网页都从不同的基本页面类继承。
public class MMFForm : sessionpage.MMFPage
{
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
try
{
this.Session["nat"] = System.DateTime.Now.ToString();
Response.Write (this.Session["nat"]);
}
catch(Exception Err)
{
Trace.Warn ("---ERROR---",Err.Message ,Err);
throw Err;
}
}
我们的下一步是替换默认会话数据。为此,我们将使用重载。我们将首先创建一个新的会话类,它继承自默认会话类。接下来,我们将重载负责获取/设置会话数据的索引器。记住会话类是一个sealed
类很重要。要从会话类继承,我们需要绕过 sealed 限制。我创建了一个新的会话类,它实现了与会话对象相同的接口。在每个已实现(无需更改)的成员中,我只是调用System.Web.HttpContext.Current.Session
来使用当前会话。每个新的基页类都使用会话属性上的new
修饰符来隐藏基类的会话属性。在新的会话属性中,我在应用程序对象中搜索新的会话类。如果找到,我使用该对象。如果找不到,我从新的会话对象实例化一个新对象,并将此对象存储在应用程序中。
public class RemotingPage : System.Web.UI.Page
{
public RemotingPage()
{
}
public new RemotingSession Session
{
get
{
if (System.Web.HttpContext.Current.Application
["RemotingSession"] == null )
System.Web.HttpContext.Current.Application
["RemotingSession"] = new RemotingSession();
return (RemotingSession)
System.Web.HttpContext.Current.Application
["RemotingSession"];
}
}
}
新的会话对象将处理所有当前应用程序会话。在应用程序中存储会话数据可以减少创建新会话数据的次数,并显著提高性能。该示例演示了RemotePage
代码,但MMFPage
也使用相同的代码。
新会话对象的实现几乎相同,但由于每个会话实现不同的数据存储方式而存在差异。在浏览会话代码时,我将重点关注存储实现的差异。
创建MMFSession类
MMFSession
类的内部成员持有对KDSession
类的引用。此引用在内存映射文件中处理会话数据。声明了委托和TCP通道,以便在会话数据更改时异步通知远程处理类CUpdateClass
。
public class MMFSession : System.Collections.ICollection ,
System.Collections.IEnumerable
{
SeddionC.KDSession oKdSession = new SeddionC.KDSession();
public delegate void RemoteAsyncDelegate(string sessionID,
string name, object data);
TcpChannel channel = new TcpChannel();
构造函数注册TCP通道。
public MMFSession()
{
if (ChannelServices.RegisteredChannels.Length == 0)
ChannelServices.RegisterChannel(channel);
}
如前所述,大多数函数和属性只是调用当前会话以保持当前会话行为。我只使用新功能覆盖处理会话数据的函数。为了证明这个概念,我只覆盖了使用键而不是序数位置处理数据的函数和属性。Add
函数和set
索引器看起来相同,它们都通过发送会话ID和索引器字符串以及值来调用内存映射文件类来设置数据。在内存中设置数据后,它们异步调用存储在Windows服务中的远程处理类,以使用更改更新其他服务器。这将在同步部分更详细地介绍。
public void Add(string name,object val)
{
//call the MMF object
oKdSession.SetData(SessionID,name,val);
//call the service to sync.
SessionInterface.IRemotingSessionHandler oObj =
(SessionInterface.IRemotingSessionHandler)Activator.GetObject
(typeof(SessionInterface.IRemotingSessionHandler),
"tcp://:1967/TcpSession");
AsyncCallback RemoteCallback = new AsyncCallback
(this.OurRemoteAsyncCallBack);
RemoteAsyncDelegate RemoteDel = new RemoteAsyncDelegate
(oObj.ReflectChanges);
IAsyncResult RemAr = RemoteDel.BeginInvoke(SessionID,name,val,
RemoteCallback, null);
}
public virtual object this[string index]
{
get
{
try
{
//call the MMF object
return oKdSession.GetData(SessionID,index);
}
catch(Exception Err)
{
string s = Err.Message;
throw Err;
}
}
set
{
//call the MMF object
oKdSession.SetData(SessionID,index,value);
//call the service to sync.
SessionInterface.IRemotingSessionHandler oObj =
(SessionInterface.IRemotingSessionHandler)
Activator.GetObject
(typeof(SessionInterface.IRemotingSessionHandler),
"tcp://:1967/TcpSession");
AsyncCallback RemoteCallback = new AsyncCallback
(this.OurRemoteAsyncCallBack);
RemoteAsyncDelegate RemoteDel = new RemoteAsyncDelegate
(oObj.ReflectChanges);
IAsyncResult RemAr = RemoteDel.BeginInvoke(SessionID,index,
value, RemoteCallback, null);
return;
}
}
public void Remove(string name)
{
oKdSession.Remove(SessionID,name);
}
public void RemoveAll()
{
oKdSession.RemoveAll(SessionID );
}
获取或删除会话数据只需对处理请求的内存映射文件进行一次调用。让我们检查KDSession
类,看看内存中如何处理会话数据。
本节涵盖了使托管代码能够将数据存储在内存映射文件中的两个类。该DLL同时包含托管代码和非托管代码。KDSession
类是托管代码,作为调用托管代码的网关,而MMF类是非托管代码,与WIN32内存映射文件API协同工作。
我将从构建一个映射结构来保存数据开始。为此,我将使用SessionData
和Session
类。不幸的是,我发现在一个进程中将复杂数据放入内存映射文件并用另一个进程使用这些数据是不可行的。因此,我决定将会话数据以XML格式的字符串形式保存;这样就可以使用XML获取和设置数据。我下一个复杂的任务是找到序列化映射数据的最佳方法,然后在收到数据请求时使用反序列化和序列化。在此示例中,我还将会话数据限制为字符串。展望未来,下一版本将通过使用序列化并将结果保存为字符串来支持对象。
KDSession
类的任务是:与托管世界进行接口,将数据传输到等效的非托管世界,调用非托管代码,接收结果并将其转换为托管类型,最后将结果发送回调用者。GetData
函数是一个很好的例子,因为它获取需要转换为非托管字符串的字符串类型,然后返回需要再次从非托管代码转换为托管字符串的字符串值。要将字符串发送到非托管代码,我们需要将其转换为wchat_t*
。为了进行此转换,我们将使用Marshal
类的静态函数StringToHGlobalUni
。这将在非托管块中为字符串分配空间。此过程必须与IntPtr
结构的静态函数ToPointer
结合使用,该函数返回指向已分配字符串的指针。在转换新的非托管字符串后,可以将其发送到非托管代码。切记不要忘记释放内存空间,Marshal
类的FreeHGlobal
静态函数将完成此工作。
Object* GetData(String* SessionID, String* Key)
{
Object* RV;
// cast the managed string to wchar_t
wchar_t* szSessionID = static_cast<WCHAR_T*>
(Marshal::StringToHGlobalUni (SessionID).ToPointer());
wchar_t* szKey = static_cast<WCHAR_T*>
(Marshal::StringToHGlobalUni(Key).ToPointer());
// Construct new managed string from wchat_t
String* szobj = new String(oMMF.GetValue
(szSessionID,szKey).c_str());
RV = szobj;
//free memory
Marshal::FreeHGlobal ((int)szSessionID);
Marshal::FreeHGlobal ((int)szKey);
return RV;
}
将非托管字符串重新转换为托管字符串很简单,因为String
类型有一个以wchat_t*
作为参数的构造函数。这只剩下我们一个担忧,即从非托管函数返回wchat_t*
类型。
MMF类是基于STL的纯非托管代码。当前的实现将字符串(wchat_t*
)保存在内存映射文件中。获取字符串后,我将其转换为wstring
以简化字符串操作。每次更改字符串内容时,我都会将wstring
数据反映在内存映射文件字符串中。
Init
函数只是简单地创建或打开一个文件,根据文件句柄创建文件映射,并将内存映射文件对象的内容返回到类的wstring
成员(Data
)中。如果内存映射文件已经存在,该函数将使用此文件而不是创建新文件。
void MMF::Init ()
{
HANDLE hFile;
m_sessions = NULL;
HANDLE hMMF = OpenFileMapping (FILE_MAP_ALL_ACCESS,true,MMfName);
// if named mmf exist use it, else create new one
if (hMMF == NULL)
{
//create file
hFile = ::CreateFile("c:\\DevSessionMMF.nat",
GENERIC_READ | GENERIC_WRITE ,
FILE_SHARE_READ | FILE_SHARE_WRITE ,
NULL, OPEN_ALWAYS ,0,NULL);
if(hFile != INVALID_HANDLE_VALUE)
{
//create file mapping
hMMF = ::CreateFileMapping (hFile,
NULL,PAGE_READWRITE,0, 100*1024,MMfName);
if (hMMF != NULL)
{
//point wchar_t* to the string in the map view
Data = (wchar_t *)::MapViewOfFile(hMMF,
FILE_MAP_ALL_ACCESS, 0,0,0);
//set the wchar_t to wstring
wsData = Data;
}
else
throw "error";
}
}
else
{
Data =(wchar_t *)::MapViewOfFile(hMMF,
FILE_MAP_ALL_ACCESS,0,0,0);
wsData = Data;
}
}
序列化只是将wstring
成员的内容复制到Data
成员(它是内存映射文件视图的指针)中。
void MMF::DeSerialize()
{
wmemcpy (Data,(wchar_t*)wsData.c_str (),wsData.size ());
}
所有其他函数都使用basic_string
模板函数来操作Data
字符串并反映会话数据的添加、更改和删除。析构函数调用UnmapViewOfFile
来释放视图所持有的内存区域,并将数据刷新到RAM。这些类还包含GetValueList
函数;此函数返回当前存储的会话数据的会话和键值列表。此函数在同步过程的某个部分中得到了充分涵盖。
创建RemotingSession类
RemotingSession
类的内部成员是委托和一个TCP通道,声明它们是为了用于同步和异步调用远程处理类。如前所述,CRemotingSession
类处理并存储数据。
public class RemotingSession : MMFSession
{
public new delegate void RemoteAsyncDelegate(string sessionID,
string name,object data);
public delegate void RemoteAsyncRemove(string sessionID,string name);
public delegate void RemoteAsyncRemoveAll(string sessionID);
TcpChannel channel = new TcpChannel();
public RemotingSession()
{
if (ChannelServices.RegisteredChannels.Length == 0)
ChannelServices.RegisterChannel(channel);
}
Add
、Remove
函数、RemoveAll
函数和索引器的set
部分异步调用远程类来处理数据。在使用索引器获取数据时,会发生对会话存储的同步调用,这是获取数据的调用。与内存映射文件会话类相同,RemotingSession
类仅覆盖处理带键数据的函数和属性,而不覆盖处理带序数位置数据的函数和属性。对远程类的每次调用都通过使用接口间接完成。我使用Activator
类的Invoke
函数为远程对象创建代理。
Add
函数类似于异步调用。
public new void Add(string name,object val)
{
//create proxy
SessionInterface.IRemotingSessionHandler oObj =
(SessionInterface.IRemotingSessionHandler)Activator.GetObject
(typeof(SessionInterface.IRemotingSessionHandler),
"tcp://:1967/TcpSession");
//async call SetData
AsyncCallback RemoteCallback = new
AsyncCallback(this.OurRemoteAsyncCallBack);
RemoteAsyncDelegate RemoteDel = new
RemoteAsyncDelegate(oObj.SetData);
IAsyncResult RemAr = RemoteDel.BeginInvoke(SessionID,
name,val,RemoteCallback, null);
}
索引器的get
部分进行同步调用。
public override object this[string index]
{
get
{
try
{
//create proxy
SessionInterface.IRemotingSessionHandler oObj =
(SessionInterface.IRemotingSessionHandler)
Activator.GetObject(typeof
(SessionInterface.IRemotingSessionHandler),
"tcp://:1967/TcpSession");
//sync call
return oObj.GetData (SessionID ,index);
}
catch(Exception Err)
{
string s = Err.Message;
throw Err;
}
}
set …
RemotingSession
类(如MMFClass
)的目标是将处理会话数据的会话调用转发到后端新的存储数据类(内存映射文件或远程处理进程中托管的哈希表)。下一节将演示我如何在远程会话类中保留数据。
创建CRemotingSession类
在远程对象(CRemotingSession
)中实现会话存储是一个简单的案例。该类使用HashTable
作为存储目标,实现了IRemotingSessionHandler
接口中声明的函数。
__gc class CRemotingSession : public MarshalByRefObject,
public IRemotingSessionHandler
{
private:
System::Collections::Hashtable *oSessionTable;
public:
CRemotingSession()
{
oSessionTable = new System::Collections::Hashtable();
}
void SetData(String* sessionID,String* name,Object* data)
{
oSessionTable->Add (sessionID->Concat(name),data);
}
Object* GetData(String* sessionID,String* name)
{
return oSessionTable->get_Item (sessionID->Concat (name)) ;
}
…
}
为了实现Windows服务中类的远程激活,服务包含另一个类CListener
,它使远程处理成为可能。服务在启动时会调用这个监听器类(CListener
)。在以下代码中,您可以看到类设计代码。我提请您注意我更改端口名称以实现多端口监听的方式。这是解决CLR已知限制的一个简单方案,CLR被设计为在AppDomain中以相同名称监听单个端口。
__gc class CListener
{
public:
static void StartListener()
{
try
{
//Create dictionary to hold the port data
System::Collections::IDictionary * props =
new System::Collections::Hashtable();
// boxing of int, the dictionary receive objects.
Object *oVal = __box(1967);
props->Add (S"name",S"tcp_session");
props->Add (S"port", oVal);
// use the properties in the class constructor
TcpChannel *channel = new TcpChannel(
props,
NULL,
new BinaryServerFormatterSinkProvider());
ChannelServices::RegisterChannel (channel);
WellKnownServiceTypeEntry *WSTE = new
WellKnownServiceTypeEntry(System::Type::GetType("CTryClass"),
"TcpSession", WellKnownObjectMode::Singleton);
RemotingConfiguration::RegisterWellKnownServiceType(WSTE);
TcpChannel *channelS = new TcpChannel (1966);
ChannelServices::RegisterChannel (channelS);
WellKnownServiceTypeEntry *WSTES = new
WellKnownServiceTypeEntry(System::Type::GetType("CUpdateClass"),
"TcpUpdater", WellKnownObjectMode::SingleCall);
RemotingConfiguration::RegisterWellKnownServiceType(WSTES);
}
catch (Exception *Err)
{
String* err = Err->Message ;
}
}
};
服务器之间会话数据的同步
这个小框架包含一个Web服务器场中服务器之间会话数据的简单同步工具。目前,这个工具仅支持内存映射文件解决方案中会话数据的插入或更改。我没有实现会话数据Remove
和RemoveAll
的同步。
同步功能基于一个Windows服务,该服务执行以下操作:
- 监听并处理每个同步请求
- 使用一个计时器,维护Web服务器场中所有服务器的列表。
Windows服务通过MailSlots维护列表。每分钟,Windows服务将其名称写入MailSlot。服务也在监听MailSlot,每分钟它会读取MailSlot中所有等待的消息。然后它将它们(如果不存在)添加到服务器列表中。
该过程从服务类的PreMessageLoop
开始。在该函数中,我们首先调用非托管代码,它打开MailSlot并启动一个计时器。
HRESULT CservicModule::PreMessageLoop (int nShowCmd)
{
HRESULT hr = __super::PreMessageLoop(nShowCmd);
#if _ATL_VER == 0x0700
if (SUCCEEDED (hr) && !m_bDelayShutdown)
hr = CoResumeClassObjects();
#endif
if (SUCCEEDED(hr) )
{
// open MailSlot and start timer
m_MMS.StartWork ();
hTimer = SetTimer (NULL,NULL,60000,TimerProc);
}
return hr;
}
UnManagedMSHandler
函数的StartWork
函数实例化MailSlotReader
并开始监听MailSlot。MailSlotReader
类封装了所有与从MailSlots读取相关的工作。
void UnManagedMSHandler::StartWork()
{
MailSlotReader oSR;
m_hMSFile = oSR.CreateMailSlot ();
}
CreateMailSlot
使用WIN32 API创建一个命名的MailSlot对象。
HANDLE MailSlotReader::CreateMailSlot()
{
try
{
HANDLE hFile =
::CreateMailslot("\\\\.\\MailSlot\\MmfFiles\\MMFSync",
0,MAILSLOT_WAIT_FOREVER,NULL);
return hFile;
}
catch(...)
{
return NULL;
}
}
让我们回到服务类,回到处理Timer
事件的TimerProc
。这个函数负责维护服务器列表任务,这是同步过程的一部分。调用SyncServers
最终会读取MailSlot中的所有消息,并将当前服务器名称发送给其他服务器中的所有打开的MailSlots。isFirstTime
检查可用性,用“活动”服务器的会话数据初始化会话数据。为了做到这一点,我建立了一个2分钟的迭代,它调用SyncServers
函数来获取“活动”服务器。当退出迭代时,我检查ServerList
是否包含任何数据,如果是(!=0
),则调用UpdateCurServer
以将会话数据与其他服务器同步。
VOID CALLBACK CservicModule::TimerProc(
HWND hwnd, // handle to window
UINT uMsg, // WM_TIMER message
UINT_PTR idEvent, // timer identifier
DWORD dwTime // current system time
)
{
//stop the timer
::KillTimer (NULL,hTimer);
//read waiting servers notifications from the slot
//and write to all open slots the current computer name
m_MMS.SyncServers();
//if first time check if other servers alive for 2 minute.
//If found get their data.
if (!isFirstTime)
{
String* ServerList = new String(m_MMS.GetServerList().c_str ());
int StartTime = System::Environment::TickCount;
int CurTime = System::Environment::TickCount;
while ((ServerList->CompareTo(L"") == 0)&&
((CurTime - StartTime)< 120000))
{
m_MMS.SyncServers();
ServerList = new String(m_MMS.GetServerList().c_str ());
CurTime = System::Environment::TickCount;
}
// start the remoting listener.
CListener::StartListener ();
if(ServerList->CompareTo(L"") != 0)
// update the server with data from others.
UpdateCurServer (ServerList);
isFirstTime = TRUE;
}
hTimer = SetTimer (NULL,NULL,6000,TimerProc);
}
SyncServers
函数读取其他服务器的通知并发送当前服务器的通知。
void UnManagedMSHandler::SyncServers()
{
MailSlotWriter oSW;
MailSlotReader oSR;
oSR.Read (m_hMSFile);
TCHAR lpszBuffer[256];
DWORD cbBuffSize=sizeof(lpszBuffer)/sizeof(TCHAR);
::GetComputerName (lpszBuffer,&cbBuffSize);
oSW.Write (lpszBuffer);
}
尽管这里使用了处理从MailSlots读取和写入的两个类,但我将重点关注读取过程,因为它更复杂且更重要。
void MailSlotReader::Read(HANDLE hFile)
{
DWORD cbMessage=0,cMessage=0,cAllMessages=0,cbRead=0;
//Get the number of messages in the queue
BOOL bRV = ::GetMailslotInfo (hFile,(LPDWORD) NULL,
&cbMessage,&cMessage,(LPDWORD) NULL);
if (! bRV)
throw "Cant get mail slot info";
cAllMessages = cMessage;
// loop until the queue is empty
while (cMessage != 0)
{
LPSTR lpszBuffer;
lpszBuffer = (LPSTR) GlobalAlloc(GPTR, cbMessage);
bRV = ReadFile(hFile,
lpszBuffer,
cbMessage,
&cbRead,
NULL);
if (!bRV)
{
GlobalFree((HGLOBAL) lpszBuffer);
throw "cant read from mail slot";
}
//check if the given server name not of this machine
//LPSTR lpszCompBuffer;
LPSTR lpszCompBuffer = (LPSTR) GlobalAlloc(GPTR, 256);
DWORD cbBuffSize=256;//=sizeof(lpszCompBuffer)/sizeof(TCHAR);
BOOL b = ::GetComputerName(lpszCompBuffer,&cbBuffSize);
wchar_t* inMessage = new
wchar_t[sizeof( wchar_t) * cbRead * 2] ;
wchar_t* inComName = new
wchar_t[sizeof( wchar_t) *(cbBuffSize+1)*2];
mbstowcs(inComName,lpszCompBuffer,cbBuffSize+1);
mbstowcs(inMessage,lpszBuffer,cbMessage);
if( wcscoll(inMessage,inComName) !=0 )
//check if the server name already exists
if (oVec.find (inMessage) == -1)
{
oVec += inMessage ;
oVec += L",";
}
delete [] inMessage;
delete [] inComName;
GlobalFree((HGLOBAL) lpszBuffer);
GlobalFree((HGLOBAL) lpszCompBuffer);
// get waiting messages in queue after processing one.
bRV = GetMailslotInfo(hFile, // mailslot handle
(LPDWORD) NULL, // no maximum message size
&cbMessage, // size of next message
&cMessage, // number of messages
(LPDWORD) NULL); // no read time-out
if (!bRV)
{
throw "cant get mail slot info for the sec. time";
}
}
}
该函数使用GetMailslotInfo
函数获取队列中的消息数量并处理所有这些消息。在添加从消息中提取的服务器名称之前,我检查提取的名称是否不是现有机器的名称,以及提取的名称是否已存在于列表中。这种简单的机制维护了所有服务器的列表,以便我们可以使用它们来反映此机器中发生的更改。UpdateCurServer
函数的任务是从其他活动服务器同步当前服务器的会话数据。该函数通过其远程处理类CUpdateClass
调用第一个服务器。首先,调用远程处理类以获取可用会话数据键的列表。这会调用远程机器上的KDSession
类以获取机器数据键。然后,设置调用CupdateClass
类中的GetObject
函数以从远程服务器获取每个数据并在当前机器中设置数据。
void CservicModule::UpdateCurServer (String* ServerList)
{
try
{
System::String *strServers = ServerList;
SeddionC::KDSession* oCache = new SeddionC::KDSession ();
__wchar_t comma __gc[] = {L','};
System::String* arr[] = strServers->Split(comma);
String* strConnect = L"tcp://";
strConnect = String::Concat(strConnect,arr[0]);
strConnect = String::Concat(strConnect,L":1966/TcpUpdater");
// create proxy to the remote class
CUpdateClass* oObj = __try_cast<CUPDATECLASS*>
(Activator::GetObject(__typeof(CUpdateClass),strConnect));
// get the remote session data list
String* Objects = oObj->GetObjectsList();
if(Objects->CompareTo(L"") == 0)
return;
arr = Objects->Split(comma);
// for each data update current machine
for(int i=0;i< arr->Count ; i++)
{
String* SessionID = arr[i]->Substring(0,
arr[i]->IndexOf((wchar_t)3));
String* Key = arr[i]->Substring(arr[i]->IndexOf((wchar_t)3)+1);
Object* oNewObj = oObj->GetObject(SessionID,Key);
oCache->SetData(SessionID,Key,oNewObj);
}
}
catch(Exception* Err)
{
String* s = Err->Message;
}
}
CUpdateClass
类执行实际的同步工作。该类由3个函数组成,这些函数获取或设置给定会话数据的数据,并提供内存映射文件中所有会话数据的列表。
__gc class CUpdateClass : public MarshalByRefObject
{
private:
public:
CUpdateClass()
{
}
void SetData(String* SessionID, String* name,Object* data)
{
SeddionC::KDSession * oCache = new SeddionC::KDSession ();
oCache->SetData(SessionID, name,data);
}
Object* GetObject(String* SessionID,String* name)
{
SeddionC::KDSession * oCache = new SeddionC::KDSession ();
return oCache->GetData (SessionID,name);
}
String* GetObjectsList()
{
SeddionC::KDSession * oCache = new SeddionC::KDSession ();
return oCache->GetValueList ();
}
};
CRemotingSession
类与CRemotingSession
类的ReflectChanges
函数以及服务器列表一起用于将会话数据的更改同步到可用服务器。每次触发数据更改时,都会从新的会话类调用ReflectChanges
。然后,该函数循环扫描服务器列表中的所有可用服务器,并调用CUpdateClass
类的SetData
函数来设置远程服务器上的数据。如果远程调用失败,该函数将当前服务器从服务器列表中删除,假设该服务器已关闭。
void ReflectChanges(String* sessionID,String* name,Object* data)
{
UnManagedMSHandler m_MMS;
//Get Server list
System::String *strServers=new
String(m_MMS.GetServerList().c_str());
__wchar_t comma __gc[] = {L','};
System::String* arr[] = strServers->Split(comma);
//for each server create new proxy of the CUpdateClass class
for(int i=0;i< arr->Count ; i++)
{
// create dynamic connect string
String* strConnect = L"tcp://";
strConnect = String::Concat(strConnect,arr[i]);
strConnect = String::Concat(strConnect,L":1966/TcpUpdater");
// create proxy
CUpdateClass* oObj = __try_cast<CUPDATECLASS*>
(Activator::GetObject(__typeof(CUpdateClass),strConnect));
if(oObj == NULL)
{
// if faild to create proxy, remove from list
wchar_t* sServerName = static_cast<WCHAR_T*>
(Marshal::StringToHGlobalAnsi (arr[i]).ToPointer());
m_MMS.RemoveServer(sServerName);
Marshal::FreeHGlobal ((int)sServerName);
}
else
// set the remote server with the data
oObj->SetData(sessionID,name,data);
}
}
我将本文视为基础设施再造的可行性基础,旨在利用默认提供的会话并将其替换为新会话。
我将不胜感激对本文的任何贡献,并欢迎所有意见和观察,这将最终产生更好的会话状态解决方案。