Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Offering a better (ODP Compliant) ASP.NET Session object

0.00/5 (No votes)
1 Apr 2003 1  
This article discusses the problems and the available solutions for maintenance and utilization of ASP.NET session state of the .NET session object.

Introduction

This article discusses the problems and the available solutions for maintenance and utilization of ASP.NET session state of the .NET session object. We will start with exploring major issues working with the existing session, by introducing several scenarios in which the session fails to supply a total ODP compliant solution. Then we will experience two ways to create a better session object and the ways to implement them in any desired application. Aside to the conceptual and architecture pans of this article I will also cover: the creation of a C++.NET DLL from a managed and an unmanaged code, the working method with MailSlots, the use of memory map files (MMF), the use of remoting in synchronous/ asynchronous methods and the creation of a new Page and Session super class using C# code. This article is an overview of an open source project that I started in Sourceforge [^]). If you find these subjects interesting and if you are willing to contribute and to face the challenge, we will be happy having you on board.

Background

Microsoft shipped ASP.NET with many statements about solving the session problems in the DNA architecture. After using ASP.NET and building major infrastructure projects for large organizations, I realized that the cycle of "new technologies" - deliver new problems, probably will never end and that the current ASP.NET session object still requires major refinements.

Let's start with introducing the three major problems that were in the DNA architecture prior to Microsoft ASP.NET:

  1. Process dependency: if the web process is down for some reason all the session data was gone.
  2. Server farm limitations: the session did not follow turnovers from server to server in a web server's farm. This means that the session was specific for the host server. If a session data set was on one machine, it was not available on other machines.
  3. Cookies dependency: The session data was based on cookies but not all the browsers supported or allowed cookies, mainly for security reasons.

Microsoft did address all of these problems, session state can be used by browsers that not support cookies by embedding an unique string that identify the session in the page's URL. The process dependency and the server farm limitations problems, were solved by the same solution introduced for the process dependency problem. And, three additional options were introduced to enable hosting of the session data. One option is to host the data inside the process data (just as in DNA), the second option is to host the session data outside the process, in this way the session data can be accessed via every server in the servers farm and not only by the web process. The third option is to host the session data in a SQL server.

Let's examine the second option offering an out of the process session hosting: The state machine solution is based on service on one machine that holds the session data. This service is using direct call to TCP to supply the data (not remoting utilization). Getting deeper into this solution we discover a major draw back, what will happen if the server or service will collapse or shut down? It seems like all of the session data is down as well. This is surely an undesired situation.

A more robust solution will be the third option, to host the session data on a SQL server. This way the data will be highly available (the SQL server must be up), but we will pay the "performance toll" (as described in table 1.0). Using the second and third options cost in performance, mainly because Microsoft designed them in a way that every time you call to session data from a web application, this web application calls the remote host to get the session data; a procedure that plunders a lot of precious time.

Table 1.0 Testing session solutions performance:

RPS / time to first byte (msecs): 10(users) 50(users) 100(users)
In process: 493/8.78 522/25.90 521/40.73
Out of process (State machine): 319/12.65 341/44.67 333/100.10
Out of process (SQL server): 130/73.35 117/342.00 97/903.00

(1GB RAM, 2 * x86 Pentium III 800 Mhz)

If you read the above lines carefully you probably realize that there is surely a room for refinements, here are some important outlines before getting there: a major improvement will be to retain the session data locally. This will eliminate remote calls, but this enhancement will have to have a new mechanism which synchronizes all the servers with the sessions data. Otherwise, the session data on one machine won't be similar to other machine. As said, retention of a local copy of the session data will mainly prevent the performance penalty for out of the machine calls. Saving a local copy of the data on any server and synchronizing them in the web servers farm will also prevent single copy of the session data. This will accomplish our heart wish, No Single Point of Failure (NSPOF). To sum up, the major advantages achieved from keeping a local synchronized copy of a session data are:

  1. Improvement of performance, which is gained by preventing remoting call for the data
  2. A redundant solution resulted by the synchronization of the session data.

As I mentioned above, the new ASP.NET introduces new problems, (I rather refer them as challenges for the infrastructure programmers). There is one big challenge arising from the fact that ASP.NET relay on the aspnet_wp process, the process that hosts the session. This fact prevents ASP pages and other processes from sharing the session data of an ASP.NET application. The major obstacle is when porting an ASP application to an ASP.NET application and some ASP pages left in the new ASP.NET application. With aspnet_wp process you will be unable to share session data between ASP and ASP.NET sessions.

While creating an ASP application you can use C++ or Visual Basic DLLs to gain better performance. There are many cases in which you will desire to host those DLLs in COM+ to enable COM+ services and features. Hosting your .NET DLLs in COM+ and enabling object pooling (just c++ DLLs) to gain better performance and control on these DLLs (e.g. shutting down without all the web application) can be achieved by registering them as server package. Registering DLLs as server package will result with a dedicated process, dllhost.exe, which will run the DLLs. The main problem with this method is that these DLLs are part of the application and from time to time they need to access the session data. The DNA architecture solves this issue by enabling to register DLLs as a COM+ application or library package, which resolves with access to all of the ASP objects including session data. Although this way of building web application is also a feature in ASP.NET, you can't reach the session data from any other process than aspnet_wp, even if it is a DLLhost process. A workaround can be implemented by passing the data from the web pages to the COM+ DLLs. This solution might rise new problems when the number of transferred parameters is getting higher.

Examining the methods above with the Open Distribute Processing standard [ODP-ISO/OSI 96], reveals incompatibility between the session and the ODP standard. The ODP key definitions regulate the distribution transparency regulations. There are two transparency regulations that the current session violate: the first is the failure transparency: all the way to use the session establishes only single point of failure that is not transparent to the web application and certainly unrecoverable. The second is the replication transparency: the session does not implement replication of objects to support common state. Exploring the ASP.NET session architecture reveals a better solution for the described problems and more ODP compatible. Lets assume that the session data will be retain in a way that every process can take advantage of it we will only have to pass a session ID in order to get the right session data. This way we will eliminate the procedure of replicating session data between processes.

Retaining a local copy of the current session data and utilizing the session data in a way that every process can access and obtain it is possible by two approaches, both based over the same technological solution. We need to create a mechanism which holds all the session data on the host and interfaces, with every process:

  1. IPC (Memory Map File): The MMF enables us to concentrate data in the computer RAM and to share this data between processes. Using MMF with session data will gain better performance. Every process can access, change or set the data over the dedicated MMF and this data will be transparent to all other processes. Furthermore, the data on the RAM can be preserved by the operating system to physical files, these files will enable the retention of the session data in case of system shutdown. Using MMF does look like an elegant solution but there are drawbacks and limitations need to be concerned. As understood, the session data is hierarchal data (i.e. Application->sessionID->ValueKey) that need to be retained at the same hierarchal structure it was inserted. Only this way we will get accurate and prompt retrievals. The STL map (and any structure based on pointers) is an appropriate means for holding the session data. If we could set the map variable pointer to a map in a way it will start from the base address of the map file in the process heap address, it would probably answer the problem, the difficulty with based on the inability to retain complex data (which include pointers) at the memory address of the MMF view. The unique way to overcome the mapping problem is by using serialization to save the map data of the MMF view and de-serialize the content of the MMF view into a map object. This approach reduces performance due to the fact that, every time data is added to the map we need to serialize it to the MMF view in order that another process will notice those changes. There is also an unresolved inquiry concerning to the reasons to handle the session data from other processes then aspnet_wp. If the session data should be updated by other processes, we need to de-serialize the map data from the MMF view when we retrieve data from the map.
  2. Remoting: Remoting is another method available for holding the session data. The remoting solution is based on a window service with 3 major elements:
    1. The data in hash table
    2. Class to handle the data
    3. Listener that maintains singleton object for calls.

    This way every remote call to the service class results with the same object that holds the data in the hash table and can set or get this data. Every process can reach the service via remoting. A better performance can be achieved by changing the listener from singleton state to single-call and by maintaining the data in the STL map object. It should be mentioned that __gc (managed) objects can not be declared as shared or global within C++.NET. This mode of working improves the RPS (Request Per Second) from 100 to 140. The remoting option allows us eliminate serialization of data in each and every call for getting/setting the data, we will only need to use serialization when the process is down or temporarily unavailable. This way we will be able to de-serialize the data when the server or process restarts. The serialization procedure should be carefully considered as against the size of session data factor.

Using the code

General overview

* The ongoing code offers the conceptual suggestions replacing the session data mechanism.

The presented solutions will be based on the followings components:

  1. A web application (built with C#) with three pages:
    1. MMF.aspx for using the memory map file option.
    2. Remote.aspx for the use of remoting procedure and
    3. RegSession.aspx for accessing the default session object.
  2. A new page and session super classes (C#) embedded in one DLL. This DLL will be responsible for replacing the current session object. RemotingPage and RemotingSession will replace the session object with remoting option And MMFPage and MMFSession will replace the session with the memory map file use.
  3. SeddionC a DLL (C++.NET Managed and Unmanaged) which holds the session data in a memory map file and maintaining an access channel to the memory map file data. This DLL will be built from
    1. A KDSession class, this class will help us as an entry point to the managed-code section.
    2. A MMF class which will be responsible for storing and retrieving data from memory map file. The seddionC DLL will also hold other classes which will resemble data structure for session data based on STL templates. Due to the limitation of storing complex data in a MMF, in this project we will use string variables to store data in memory map file.
  4. A window service tmpSessionSync (C++.NET Managed and Unmanaged) to hold the session data and to grant access to the session data using remoting by a listener class. The service also implements simple synchronization mechanism between servers in the web farm to maintain session data currency in the entire web farm. This synchronization mechanism can be done by using WIN32 MailSlots. MailSlotReader and MailSlotWriter will help us to encapsulate the work with MailSlots. UnManagedMSHandler serve as the gateway between the manage code and the unmanaged code in the tmpSessionSync service. CRemotingSession is responsible for getting requests from the managed world. CUpdateClass is engaged with the session data synchronization. CservicModule is the service class and CListener holds the code to start listener for remoting requests.

The following describes the two major scenarios for the suggested solutions:

  • Scenario 1 � Remoting: We will create a new page super class (RemotingPage) as our entry to replace the session object. For this we will also use a new session class (RemotingSession) that encapsulate the calls to a new remote session data handler (CservicModule). The page super class (RemotingPage) will mainly override the existing session property and by that will return a new session object. The session data handler (CservicModule) is a window service designed to expose our remoting class (CRemotingSession). The CRemotingSession class implements an interface (IRemotingSessionHandler) that let the callers to get/set values of the hosted session data. This interface is also used in the new session class to call to the remote handler class. The window service will also contain a listener class (CListener). This class will make our remoting class available. It is important to remember that every request to set session data will raise an a-synchronic call to the remote handler class that insert the data into a hash table. The result will be that each and every request for session data will end up with synchronic call to the remote handler class which will obtain the data from the hash table.
  • Scenario 2 � Memory Map File (MMF): This scenario starts with the design of a new page super class (MMFPage) that, as in the first scenario, replaces the session object. In order to replace the session object we will create a new session class (MMFSession). This class will encapsulate the calls to the MMF handler. The page super class will override the existing session property and will return the new created session object. The MMF handler is a DLL (SeddionC) used to map our file view into the DLL host process and its memory address space. The new DLL interchange requests from the new session class and get/set the data in the process memory space. This way every process that uses this DLL will be mapped to the same MMF object and will provide the same data to the calling processes.

Cross server synchronization: every request to set data received from our new session classes will cause an a-synchronic call to the window service synchronization class (CUpdateClass). The duty of this class will be to get a list of all the existing servers in the web farm and to specifically call each of them to set session data on a local basis. The list of servers is built and triggered by a timer that fires every one minute. This timer uses the MailSlot (MailSlotWriter) in order to publish the server name. Respectfully to the timer, the window service listens to the MailSlot (MailSlotReader), in this way every sent data to the MailSlot is tracked by the service and added to a list of all the �Live� servers in the web-farm.

Let's get to work �

Creating new Page base classes

We will start with the web application. The web application will be based on three web pages each and every one of them uses the session object to set and then get a string value from the session data. The only difference between the three pages is that every one of them inherits its class from different base page classes.

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;
        }
    }

Our next step will be to replace the default session data. For this we will use overloading. We will start by creating a new session class that inherits from the default session class. Next we will overload the indexer that is responsible for getting/setting the session data. It is important to remember that the session class is a sealed class. To inherit from the session class we will need to workaround the sealed limitation. I created a new session classe that implement the same interface that the session object does. In every member that is implemented (where there are no changes to make), I simply call the System.Web.HttpContext.Current.Session to use the current session. Every one of the new base page classes is using the new modifier on the session property in order to hide the base class session property. Within the new session property, I search for the new session class in the application object. If I find, I use that object. If not I instantiate a new one from the new session object and store this object in the application.

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"];
    }
  }
}

The new session object will handle all of the current application sessions. Storing the session's data in the application lessens the number of times we'll need to create the new session data and dramatically improves performance. The example demonstrates the RemotePage code but the same code is used with MMFPage.

The implementation of the new sessions object is almost the same, but there are differences because of the fact that every session implements a different way to store the data. While going through the session's code, I will focus on the differences of the storing implementation.

Create the MMFSession class

The MMFSession class' internal members hold reference to the KDSession class. This reference handles the session data in a memory map file. Delegate and TCP channel are declared in order to signal a-synchronous to the remoting class CUpdateClass, when data changes in the session.

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(); 

The constructor registers the TCP channel.

public MMFSession()
{
    if (ChannelServices.RegisteredChannels.Length == 0)
                ChannelServices.RegisterChannel(channel);
}

As I mentioned before, most of the functions and attributes just call the current session to maintain the current session behavior. I override with new functionality just the functions that handle the session data. Proving this concept I only override the functions and properties that handle the data with key, not with ordinal position. The Add function and the set indexer look the same, they are both calling the memory map file class to set the data by sending the session id and the indexer string followed by the value. After setting the data in memory they call the remoting class, that store in the window service, a-synchronously to update other servers with the changes. This is covered in more detail within the Synchronization section.

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://localhost: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://localhost: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 ); 
}

Getting the session data or removing it is followed by single call to the memory map file that handles the request. Let's examine the KDSession class to see how session data is handled in the memory.

Create the KDSession/MMF classes

This part covers the two classes that enable managed code to store data in a memory map file. The DLL engage both managed and unmanaged code. The KDSession class is a managed code that serves as a gateway to calling managed code, while the MMF class is an unmanaged code that works with the WIN32 memory map file API.

I will begin with building a structure of maps to hold the data. For this I will use the SessionData and Session classes. Unfortunately, I discovered that putting complex data in a memory map file with one process and using this data with another process is unavailable. Because of this I decided to keep the session data in string using an XML format; this result with the ability to get and set the data using the XML. My next intricate task will be to find the optimal way to serialize the map data, then to use de-serialization and serialization when a request for data is received. In this sample, I also limit the session data to strings. Looking ahead, objects will be supported in the next versions by using serialization and saving the results as string.

The KDSession class tasks are: interfacing with the managed world, transfer the data into equivalent unmanaged world, call the unmanaged code, receive the results and convert them into managed types and finally to send the results back to the caller. The GetData function is a good example since it gets string type that need to be converted to an unmanaged string and then return a string value that is needed, again, to be converted to a managed string from an unmanaged code. To send a string to an unmanaged code we need to cast it to wchat_t*. In order to make this casting we will use the static function StringToHGlobalUni of the Marshal class. This will allocate space for the string in the unmanaged block. This process must be used in conjunction with the static function ToPointer of the IntPtr structure, that returns a pointer to the allocated string. After casting the new unmanaged string, it can be send to the unmanaged code. One must not forget to free up the memory space, the FreeHGlobal static function of the Marshal class will do the work.

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;
}

The re-conversion of the unmanaged string to a managed string is simple because of the fact that String type has got a constructor that receives wchat_t* as a parameter. This leave us with single concern which is to return from the unmanaged function wchat_t* a type.

The MMF class is a pure unmanaged code based on STL. The current implementation saves a string (wchat_t*) in the memory map file. After getting the string back, I convert it to wstring to make the string manipulation easier. Every time I change the string content, I reflect the wstring data in the memory map file string.

The Init function simply creates or opens a file, create file mapping based on the file handle and return the content of the memory map file object into a wstring member (Data) of the class. If the memory map file already exists, the function uses this file instead of creating a new one.

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;
    }
}

The serialization simply copies the content of the wstring member into the Data member (that is a pointer to the view of the memory map file).

void MMF::DeSerialize()
{
    wmemcpy (Data,(wchar_t*)wsData.c_str (),wsData.size ());
}

All other functions use the basic_string template functions to manipulate the Data string and to reflect add, change and delete session data. The destructor calls the UnmapViewOfFile to free the memory area that the view hold, and flush the data to the RAM. The classes also contain the GetValueList function; this function returns a list of session and key values of the currently stored session data. This function is fully covered in part of the synchronization process.

Create the RemotingSession class

The RemotingSession class' internal members are delegates and a TCP channel declared in order to be used to call a-synchronous and synchronous to the remoting class. As has been said, the CRemotingSession class handles and stores the data.

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);
    }

The Add, Remove function, RemoveAll function and the set section (of the indexer) call the remote class a-synchronically to handle the data. While getting data using the indexer, a synchronous call to the session storage occurred, this is the call that gets the data. The same way as in the memory map file session class, the RemotingSession class overrides only the functions and the properties that handle the data with key, and not those with ordinal position. Every call to the remote class is done indirectly by using an interface. I use the Invoke function of the Activator class to create a proxy for the remote object.

The Add function resembles an a-synchronic call.

public new void Add(string name,object val)
{
    //create proxy

    SessionInterface.IRemotingSessionHandler oObj = 
            (SessionInterface.IRemotingSessionHandler)Activator.GetObject
            (typeof(SessionInterface.IRemotingSessionHandler), 
            "tcp://localhost: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);
                
}

The get section of the indexer makes synchronic call.

public override object this[string index]
{
    get
    {
        try
        {    
            //create proxy                

            SessionInterface.IRemotingSessionHandler oObj = 
                   (SessionInterface.IRemotingSessionHandler)
                   Activator.GetObject(typeof
                   (SessionInterface.IRemotingSessionHandler), 
                   "tcp://localhost:1967/TcpSession");
            //sync call

            return oObj.GetData (SessionID ,index);                    
        }
        catch(Exception Err)
        {
            string s = Err.Message;
            throw Err;
        }
                
 
    }
    set

The objective of the RemotingSession class (as the MMFClass) is to forward the session call that handles the session data to the back end new storage data class (memory map file or hash table hosted in remoting process). Next section demonstrates how I preserve the data in the remoting session class.

Create the CRemotingSession class

Implementing the session storage in the remoting object (CRemotingSession) is a simple case. The class implements the functions that are declared in the IRemotingSessionHandler interface by using HashTable as the storage destination.

__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)) ;
    }
    �
}

To enable remote activation of the classes in the window service, the service contains another class CListener that enable remoting. The service calls this listener class (CListener) as it starts working. In the following code you can see the class design code. I draw your attention to the way I change the port name to enable multi-port listening. This is a simple solution to the known limitation of the CLR which is designed to listen to single port with the same name in 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 ;
        }
    }
};

Synchronization of session data between servers

This small framework contains a simple synchronization utility of the session data between servers in the web farm. Currently this utility support only inserts or changes of the session data in the memory map file solution. I didn't implement synchronization on Remove and RemoveAll of session data.

The synchronization is built on a window service that does the following:

  1. Listen and process every request to synchronize and
  2. Using a timer that maintain a list of all the servers in the web-farm.

The window service maintains the list by using MailSlots. Every minute the window service writes into MailSlot its name. The service is also listening to the MailSlot and every minute it reads all the waiting messages in the MailSlot. It then adds them (if they does not exist) to the server list.

The process starts in the PreMessageLoop of the service class. In this function we first call the unmanaged code that opens MailSlot and start a timer.

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;
}

The StartWork function of UnManagedMSHandler function instancing the MailSlotReader and start listening to the MailSlot. The MailSlotReader class encapsulates all the work regarding reading from MailSlots.

void UnManagedMSHandler::StartWork()
{
    MailSlotReader   oSR;
    m_hMSFile = oSR.CreateMailSlot ();    
}

CreateMailSlot uses the WIN32 API to create a named MailSlot object.

HANDLE MailSlotReader::CreateMailSlot()
{
    try
    {
        HANDLE hFile = 
          ::CreateMailslot("\\\\.\\MailSlot\\MmfFiles\\MMFSync",
          0,MAILSLOT_WAIT_FOREVER,NULL);
        return hFile;
    }
    catch(...)
    {
        return NULL;
    }
}

Let's go back to the service class, and to the TimerProc that handle the Timer event. This function is responsible for maintaining the server list task which is a part of the synchronization process. The call to SyncServers ends up with reading all the messages in the MailSlot and sending the current server name to all the open MailSlots in other servers. The isFirstTime check availability initializes the session data with session data from a �live� server. In order to do this I established a 2 minutes iteration, which calls the SyncServers function to get "live" servers. When going out from the iteration, I check if the ServerList contain any data, if yes (!=0), the UpdateCurServer is called to synchronize the server session data with other server.

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);
        
    }

The SyncServers function reads other servers' notification and sends notification for the current server.

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);
}

Although the two classes that handle reading and writing from MailSlots are being used here, I will focus on the read process since it's more complicated and significant.

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";
        }
    }
}

The function uses the GetMailslotInfo function to get the number of messages on queue and process all of them. Before adding the server name that is extracted from the message, I check that the extracted name isn't the name of the existing machine and that the extracted name isn't already existing in the list. This simple mechanism maintains the list of all the servers so we can use them to reflect changes made in this machine. The UpdateCurServer function's task is to synchronize the current server's session data from other live servers. The function calls the first server via its remoting class CUpdateClass. First there is a call to the remoting class to get the list of available session data keys. This calls the KDSession class on the remote machine in order to get the machine data keys. Then a call to the GetObject function from the CupdateClass class is set to get every data from the remote server and set the data in the current machine.

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 class does the actual work of synchronization. This class is built from 3 functions that get or set the data of given session data and supplies a list of all the session's data in the memory map file.

__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 ();
    }
};

The CRemotingSession class with the ReflectChanges function of the CRemotingSession class and the server list are all used to synchronize the available servers with changes made to the session data. The ReflectChanges is called from the new session classes every time that a data change is triggered. Then the function circularly scans through all of the available servers in the server list and call the SetData function of the CUpdateClass class to set the data on the remote server. If the remote call fails the function removes the current server from the server list assuming that the server is down.

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);
 
        }
    }

I refer to this article as a feasibility foundation for infrastructure reengineering in order to utilize the default shipped session and replaces it with a new session.

I would appreciate any contribution to this article and welcome all remarks and observations which will finally result in a better session state solution.

Links

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here