previous page
next page

Session Management

The scalability of the Web comes directly from its stateless nature. As far as the web server is concerned, every HTTP request is independent. The stateless architecture means that server farms and load balancing are easy, caching can be added at many different places, and it's easy to add hardware to an existing system.

It also makes writing a shopping cart a real pain in the neck.

Nearly every web application needs to deal with state management. State can be on a per-session basis, a per-application basis, or a per-page basis. When thinking about state management, some standard questions need to be answered:

  • What's the scope? From where is the state data available, and what's its lifetime?

  • Where's the data stored? In memory? In a database? In a disk file? In a hidden form field?

  • How do we find the state when processing a particular request?

One of the more difficult state-management pieces to build by hand is session state: per-user data that persists across HTTP request. Luckily for us, ATL Server, like all other serious web frameworks, provides a session-state service so we don't have to roll our own.

Using Session State

Before diving into the internals, let's take a quick look at how to use session state. In our ongoing forum example, I want to add a hit counter to each forum's page, so I can see how often I've gone to the page. It looks something like Figure 14.4.

Figure 14.4. Forum page with a hit counter


The .srf file for this page is pretty simple:

<html>
{{handler SimpleForums.dll/ShowPosts}}
<head>
<title>{{ForumName}}</title>
</head>
<body>
<h1>{{ForumName}}</h1>
<p>You have visited this forum {{HitCount}} times in the
current session.</p>
<div>
<! ... Post List content removed for clarity >
</div>
<a href="newpost.srf?forumid={{ForumId}}">New Post</a>
<a href="forumlist.srf">Return to forum list</a>
{{endif}}
</body>
</html>

The trick is, how do we implement the HitCount replacement? We want the hit counter to stick around between page views; as the user moves from forum to forum on the site, we want each page's hit count to be independent and persistence.

Unlike classic ASP and ASP.NET, ATL Server does not automatically create a session for you. In the C++ tradition of "don't pay for what you don't use," you must explicitly create a session object when you need it.

Getting the Session Service

The first thing you need to do is get hold of an ISessionStateService interface pointer. This interface provides the capability to create and retrieve sessions. The object is available in your request handler via the m_spServiceProvider member that is inherited from CRequestHandlerT< >. In your ValidateAndExchange function, do something like this:

ShowPostsHandler.h:

class ShowPostsHandler :
    public CRequestHandlerT< ShowPostsHandler > {
...
private:
...
    CComPtr< ISessionStateService > m_spSessionStateSvc;
    CComPtr< ISession > m_spSession;
};

ShowPostsHandler.cpp:

HTTP_CODE ShowPostsHandler::ValidateAndExchange( ) {
  if( FAILED( m_spServiceProvicer->QueryService(
    __uuidof(ISessionStateService), &m_spSessionStateSvc ) ) ) {
    return HTTP_FAIL;
  }

  // Do rest of validation
  ...

  // Retrieve session data
  if( FAILED( RetrieveOrCreateSession( ) ) ) {
    return HTTP_FAIL;
  }

  if( FAILED( UpdateHitCount( ) ) ) {
    return HTTP_FAIL;
  }

  m_HttpResponse.SetContentType( "text/html" );
  return HTTP_SUCCESS;
}

The line in bold is the magic call that gets us the ISessionStateService interface pointer we need.

An Aside: The IServiceProvider Interface

The IServiceProvider interface is actually a standard interface that was introduced back in the IE4 days. It hasn't gotten a whole lot of attention, but implementing it can give you a surprisingly powerful system. The definition is actually quite simple:

interface IServiceProvider : IUnknown {            
    HRESULT QueryService(                          
        [in] REFGUID guidService,                  
        [in] REFIID riid,                          
        [out, iid_is(riid)] IUnknown ** ppvObject);
};                                                 

The parameters of QueryService are essentially identical to those of QueryInterface, and QueryService acts a lot like QueryInterface: You ask for a particular IID, and you get back an interface pointer. There's a major difference, though: QueryInterface is required to return an interface pointer on the same object and obey all the rules of COM identity. QueryService, on the other hand, can (and usually does) return an interface pointer on a different COM object.

This explains the guidService parameter to the QueryService call: It's specifying which particular object we want to get the interface pointer to. This GUID doesn't need to be a CLSID, or an IID, or a CATID, or anything else. It's simply a predefined GUID that the developer chooses to represent that particular service.

The IServiceProvider interface is how ATL Server provides, well, services to the request handlers. When you create your project via the ATL Server Project Wizard and you choose session support, these lines get added to your ISAPI extension class:

// session state support
typedef CSessionStateService<WorkerThreadClass,
  CMemSessionServiceImpl> sessionSvcType;
CComObjectGlobal<sessionSvcType> m_SessionStateSvc;

public:

BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer) {
  // ...
  if (S_OK != m_SessionStateSvc.Initialize(&m_WorkerThread,
    static_cast<IServiceProvider*>(this))) {
    TerminateExtension(0);
    return SetCriticalIsapiError(
      IDS_ATLSRV_CRITICAL_SESSIONSTATEFAILED);
  }
  return TRUE;
}

BOOL TerminateExtension(DWORD dwFlags) {
  m_SessionStateSvc.Shutdown();
  BOOL bRet = baseISAPI::TerminateExtension(dwFlags);
  return bRet;
}

HRESULT STDMETHODCALLTYPE QueryService(
  REFGUID guidService, REFIID riid, void** ppvObject) {
  if (InlineIsEqualGUID(guidService,
    __uuidof(ISessionStateService)))
    return m_SessionStateSvc.QueryInterface(riid, ppvObject);
  return baseISAPI::QueryService(guidService, riid,
    ppvObject);
}

The ISAPI extension creates a session-state service object as a "global" object; you might remember CComObjectGlobal from Chapter 4, "Objects in ATL." This object lives as long as the ISAPI extension object does and basically ignores AddRef and Release counts. The QueryService implementation checks to see if the guidService parameter is equal to the ISessionStateService method; if so, it simply calls QueryInterface on the member session state service object.

ATL Server uses this technique to provide several kinds of services to the request headers. If you have your own services that you want to provide across the application, this is a good way to do it.

Creating and Retrieving Sessions

So, we now have an ISessionService pointer. The next step is to use that pointer to look up our session, and to create one if it doesn't exist.

The first question is, how do we know which session to grab? ATL Server has built-in support for the standard approach (a session cookie) and the flexibility to let you do your own session identification, if you need to.

Here's how you retrieve a session using a session cookie:

HRESULT ShowPostsHandler::RetrieveOrCreateSession( ) {
  HRESULT hr;
  CStringA sessionId;
  m_HttpRequest.GetSessionCookie( ).GetValue( sessionId );
  if( sessionId.GetLength( ) == 0 ) {
    // No session yet, create one
    const size_t nCharacters = 64;
    CHAR szID[nCharacters + 1];
    szID[0] = 0;
    DWORD dwCharacters = nCharacters;
    hr = m_spSessionStateSvc->CreateNewSession(szID,
      &dwCharacters, &m_spSession) );
    if( FAILED( hr ) ) return hr;

    CSessionCookie theSessionCookie( szID );
    m_HttpResponse.AppendCookie( &theSessionCookie );
  }
  else {
    // Retrieve existing session
    hr = m_spSessionStateSvc->GetSession(sessionId,
      &m_spSession ) );
    if( FAILED( hr ) ) return hr;
  }
  return S_OK;
}

First, we grab the value of the cookie. This gives us our session ID. If there isn't a value, we create the session via the ISessionService::CreateNewSession method. This both creates the session and returns the ID for the session created. We then create a new session cookie and add it to the response. This step is important, and you can easily forget it if you're used to other web frameworks that create sessions for you automatically.

If there is a cookie value, we use the ISessionService::GetSession method to get an ISession interface pointer and connect back up to the session.

Storing and Retrieving Session Data

When we have our ISession pointer, we can store and retrieve values. ISession maps names (as ANSI strings) to VARIANTS. Usage is pretty much what you'd expect:

HRESULT ShowPostsHandler::UpdateHitCount() {
  CStringA sessionVarName = "mySessionVariable";
  CComVariant hits;
  if( FAILED(
    m_spSession->GetVariable( sessionVarName, &hits ) ) ) {
    // If no such session variable, GetVariable return E_FAIL.
    // Gotta love nice specific HRESULTS
    hits = CComVariant( 0, VT_I4 );
  }
  m_hits = ++hits.lVal;
  return m_spSession->SetVariable( sessionVarName, hits ) );
}

The ISession interface provides the GetVariable and SetVariable methods to get and save a single variable. There are also methods to enumerate the session variables and control session timeouts.

Session State Implementations

One question about session management hasn't been answered yet: Where is session data stored? The answer, as usual for ATL, depends on which template arguments you use.

Let's look back at that type declaration in the ISAPI extension:

typedef CSessionStateService<WorkerThreadClass,
  CMemSessionServiceImpl> sessionSvcType;

The CSessionStateService template takes two parameters: The first is the worker thread class for the ISAPI extension. The second is the class that implements the ISessionService interface. In this case, we use CMemSessionServiceImpl, which provides in-memory session storage. In-memory session-state storage has the advantage of being very fast, but because it is only in memory on the server, it doesn't work in a server farm.

ATL Server provides the CDBSessionServiceImpl as an alternative. This stores session state in a database instead. The access to a session is slower, but it can be shared across multiple machines in a farm. Choose the appropriate service implementation based on your requirements.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)