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.
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.
|