Programmer to ProgrammerTM | |||||
|
|
|
|
|
|
|
|
|
|
|
| |||||||||||||||||||
The ASPToday
Article February 20, 2001 |
Previous
article - February 19, 2001 |
||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||
ABSTRACT |
| ||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||
Article Discussion | Rate this article | Related Links | Index Entries | ||||||||
ARTICLE |
One of the issues in web development is how to create a coherent application with several stand-alone HTML pages. Since HTTP is a stateless protocol, each request to the server is taken as an independent request.
ASP's intrinsic session objects provide an easy way to maintain the state of the application for each user, which requires no special programming. The state is maintained by using a sessionid cookie in the client (browser) and its related state information in web server memory.
However, we have been warned about the inability of session objects to work in web farms. Though, using a local director with a "sticky bit" set on maintains session in web farm, it is does not provide true load balancing. There are several out-of-box solutions to overcome this issue, such as software artisan's SA-Session Pro. These solutions are developed in COM and use database or NT files to store the session variables. All the servers in the farm use the same database or the file to retrieve the state information.
One of the most nagging questions in all these solutions is, "is it scalable ?" ASP Intrinsic session objects work well in single server co-hosting, but fail in web farms. Database-driven COM session objects work in web farms, but have severe restrictions in co-hosting. The third party co-hosting provider will not allow the COM DLLs to be registered in their machine. Co-hosting can even be forced on big-sites when they move in for mirror-site in other regions.
The scalability can be achieved by splitting the application into n-tiers, and using an independent language to develop each tier:
User-Interface Visual Control Objects are plain ASP files, which use the Common Business Object for processing. Common Business Objects and Data Access layers can either be ASP files or COM DLL.
The next obvious question will be, "Can we write a business logic object in pure script as the co-host will not allow COM". Amazingly, YES is the answer - we can do it in VBScript Version 5 and above. You can define class with properties and methods in ASP/VBScript like this:
<% Class myClass Private myVar1 Property Get var1 var1 = myvar1 End Property Property Let var1(value) mstrSessionID = Trimvalue End Property Public Function myFunc() 'blah blah. End Function End Class %>
If say, later, there is a machine that can run COM objects, we can 'port' the CBO code to visual basic with some minor modification and compile scripts. Compiled COM objects provides more security as even a read access (like the recent IIS bugs) to the web server will not expose the business logic.
To implement the scalable session objects, three flavors are discussed here. Flavor 1 uses ASP/VBScript classes and database for storing the session variables. This can be employed in web farms. Flavor 2 is also written in ASP/VBScript but it uses memory for storing the session variables. This implementation is apt for single-server. Flavor 3 is developed using VB/COM and database for persisting the session variables. This flavor will provide good performance in Web farms.
Common Business Object (clsSession.asp) defines a class cSession with properties
Class_ Initialize: This function will be called when the object is created. The function tries to retrieve the sessionid from the client. If the sessionid is not found or the corresponding session variables are not found in the server, it creates a new session.
Private Sub Class_Initialize() InitializeVars if RetriveSessionID Then if Not Load(mstrSessionID) Then CreateNewSession End if Else CreateNewSession End if End Sub Private Sub InitializeVars() If IsEmpty(mstrSessionID) Then mstrSessionID = "" End If If IsEmpty(mstrLogIP) Then mstrLogIP = "" End If mstrStatus = "" flgDirty = false Set mcolSessionVars = Server.CreateObject("Scripting.Dictionary") End Sub Private Function RetriveSessionID() RetriveSessionID = false mstrSessionID = Getstring("SessionID", 38, false) if Not Len(mstrSessionID) = 0 Then RetriveSessionID = True End if End Function
RetriveSessionID uses Getstring function to read the sessionid from client. Getstring is a locally defined function, which searches the given key in querystring, form post and in cookie. This function is defined in i_util.asp
Private Function CreateNewSession() Dim flgStatus flgStatus = CreateSessionID flgStatus = flgStatus and StoreSessionID if flgStatus Then mstrStatus = con_Active mdtLogDate =Date & " " & Time mstrLogIP = Request.ServerVariables("REMOTE_ADDR") End if Save CreateNewSession = flgStatus End Function Private Function CreateSessionID() CreateSessionID = False mstrSessionID = GetUniqueId mstrStatus = con_Active flgDirty = True CreateSessionID = True End Function
CreateNewSession will create a new sessionid, save the sessionid in client and then to the database. UniqueID function is defined in daSession.asp. The Save function in turn calls daSave, which is defined in daSession.asp
Con_Active is a constant defined in the same file, while FlgDirty is used to track the changes and to prevent unnecessary database transactions.
Public Function Abandon() mstrStatus = con_InActive mdtLogOutDate = Date & " " & Time flgDirty = True Save End Function
Abandon function will be called to delete the session - it'll first update the status flag and LogOutDate, and then save the information:
Public Function Load(strSessionID) Load = daLoad(strSessionID) End Function Public Function Save() if flgDirty and mcolSessionVars.Count > 0 Then Save = daSave Else Save = False End if End Function
The Load and Save functions help in persisting the data across the pages. Save will check for flgDirty to prevent useless repeated updates. As shown in the architecture diagram Common Business Objects does not know about the information storage and retrieval process. This functionality is handled in the separate layer (file) called Data Access Layer. The daLoad and daSave are functions defined in another file, daSession.asp. The advantage is, if we need to use memory instead of the database for persistence, or if there is a change in database design, then we need to change the file daSession.asp only.
In typical n-tier architecture, Data Access Layer and Business Layer communicate through a well-defined structure like ADODB.Recordset, or as a flattened string, or as XML. Here, for performance reasons we'll allow the Data Access layer to directly access the private variables of the Business layer. The file is included as shown:
<% Class cSession %> <!--#include file="daSession.asp"--> <% End Class %>
CAUTION: daSession.asp should be included before the End Class statement.
This layer is responsible for data storage and retrieval. It has two major functions - daLoad and daSave, which will be called from Common Business Object
The database has two tables, SessionHdr and SessionDet, with the following structure:
Table name: SessionHdr
Column Name | Data Type | Comments |
---|---|---|
SessionID | Char(38) | To store the session ID. Primary key |
LogIP | Char(16) | The IP address of the client. For tracking purposes. Non-Null |
LogDate | DateTime | Session Creation Date time |
LogOutDate | DateTime | Log out DateTime. Typically the time when user clicks Logout in the web page |
Status | Char(1) | Status of the Session A - Active I - Inactive This can be used to time-out the session. In a typical timeout, Logout Date will be set to NULL and status set to 'I' |
Table name: SessionDet
Column Name | Data Type | Comments |
---|---|---|
SessionID | Char(38) | Foreign Key. Links SessionHdr table and SessionDet table. |
Name | Char(50) | Name of the session variable to be stored. Ex, 'Username' |
Value | Char(100) | Value of the session variable. Ex, 'Joe' |
Private Function daLoad(strSessionID) daLoad = false Dim rs 'as recordeset Set rs = server.CreateObject("ADODB.Recordset") rs.Open Replace(Application("qry_Session_Active_PK"), _ ":1", strSessionID), _ conDB, adOpenForwardOnly, adLockReadOnly if not rs.EOF then mstrSessionID = trim(rs.Fields("SessionID")) & "" mstrLogIP = trim(rs.Fields("LogIP")) & "" mdtLogDate = trim(rs.Fields("LogDate")) & "" mstrStatus = trim(rs.Fields("Status")) & "" Set mcolSessionVars = _ Server.CreateObject("Scripting.Dictionary") Do While Not rs.eof mcolSessionVars.Add trim(rs.Fields("name")), _ trim(rs.Fields("value")) rs.movenext Loop flgDirty = False daLoad = true end if rs.Close Set rs = nothing End Function Private Function daSave() Dim tstrKey daSave = false Dim rs 'as recordeset Set rs = server.CreateObject("ADODB.Recordset") rs.Open Replace(Application("qry_SessionHdr_PK"), _ ":1", mstrSessionID), conDB, _ adOpenDynamic, adLockOptimistic if rs.EOF then rs.addnew rs.Fields("SessionID") = mstrSessionID End if rs.Fields("LogIP") = mstrLogIP rs.Fields("LogDate") = mdtLogDate rs.Fields("LogOutDate") = mdtLogOutDate rs.Fields("Status") = mstrStatus rs.update rs.Close 'Save Detail... For each tstrKey in mcolSessionVars.Keys rs.open Replace(Replace(Application("qry_SessionDet_PK"), _ ":1", mstrSessionId), ":2", tstrKey), _ conDB, adOpenDynamic, adLockOptimistic if rs.eof then rs.addnew rs.fields("SessionID") = mstrSessionID End if rs.fields("name") = tstrKey rs.fields("value") = mcolSessionVars.item(tstrKey) rs.update rs.close Next Set rs = nothing daSave = true End Function
If the same sessionid key is found in the header table, then the record is updated, else a new record is added. Again similar action is performed on the detail table. The query is stored in ASP Intrinsic Application Object. For scalability, Use stored procedures.
With this groundwork, coding front pages will be easy.
Visual control object (ASP file), an Interface Utility, will create the session and initialize and provide a routine for accessing the client information. It also controls the database connection (through a separate include file)
The location of Common Business Object and its implementation should be transparent to this object. If we are using ASP/VBScript for Common Business object then we should include the files.
<!-- #include File = "i_db_conn.asp" --> <!--#include file = "clsSession.asp" -->
The i_db_conn.asp file will establish the connection to the database and provide the service through the variable conDB
Dim objSession CreateSession() Private Function CreateSession() Set objSession = New cSession End Function
CreateSession will create the new cSession object and provide the service through objSession.
Our ASP front end to try out this code will look like this:
Once we have entered some session data, we can retrieve it:
If any of the files need to use a session, they have to include i_util.asp and start using objSession as they use for the ASP Intrinsic session object. ObjSession need not be declared in the User Interface page.
For example, to store a data in the session:
ObjSession("myVar") = "myval"
To retrieve:
Response.write ObjSession("myVar")
To abandon:
ObjSession.Abandon
The object-oriented concept has concealed the implementation details and provides an easy programming model.
It is sane to ask, "If I'm running my site on co-host, why should I use a database for state maintenance" The architecture also allows you to scale down the program to maintain the state without the database, yet the front end programs continue to use objSession without any change. This is the key advantage of splitting the application into n-tier.
The Data Access Layer can be changed to store and retrieve data using ASP intrinsic session object. Common Business Object will have to amend include file name if there is a change in data access layer file name otherwise there is no change.
The new data access layer will look like this:
Private Function daLoad(strSessionID) daLoad = false mstrSessionID = Session.SessionID mstrLogIP = Request.ServerVariables("REMOTE_ADDR") mdtLogDate = Date mstrStatus = "A" 'Always active Set mcolSessionVars = Server.CreateObject("Scripting.Dictionary") For iCount = 1 to Session.Contents.Count mcolSessionVars.Add Session.Contents.Key(iCount), _ Session.Contents.Item(iCount) Next flgDirty = False daLoad = true End Function Private Function daSave() Dim tstrKey daSave = false For each tstrKey in mcolSessionVars.Keys Session(tstrKey) = mcolSessionVars.item(tstrKey) Next daSave = true End Function
All other files remain unchanged.
This is the interesting part. Since ASP/VBScript and Visual Basic share the almost same syntax, it'll be easy to convert the ASP/VBScript code to Visual Basic.
ASP Intrinsic objects can be directly accessed from VB using OnStartPage function. However, it is often tough to debug the program. Therefore, we can avoid using ASP intrinsic objects in the VB/COM object and do some tinkering in our Visual Control Objects (i_util.asp) during instantiation.
Steps to create the COM component
Some common module files are also added (code is copied from ASP files). Next, compile the program.
In i_util.asp, change the Instantiating routine to the following:
Private Function CreateSession() Set objSession = Server.CreateObject("Session.clsSession") objSession.SessionID = GetString("SessionID", 38, False) objSession.LogIP = Request.ServerVariables("REMOTE_ADDR") objSession.Init Response.Cookies("SessionID") = objSession.SessionID Response.Cookies("SessionID").Expires = DateAdd("d", 1, Date) End Function
As the COM object cannot read the cookie, we provide the session id value retrieved from the cookie and then initialize the object. Init function will check the validity of sessionid and creates a new id if necessary. The sessionid is stored in the cookie after calling the init function. In the previous implementations, the clsSession directly accessed the cookie value and stored the session id.
We have created a session object that is scalable from co-hostable without database to using in web-farm. The scalability is achieved by using the object-oriented design concept's property of implementation concealment and near common syntax of ASP/VBScript with visual basic. In all the flavors, the user-interface files are not changed. They have to include i_util.asp and use the objSession object for state maintenance.
However, when using VBScript for class definitions and object implementation, there will be a performance hit. The solution can be effectively used for sites, which starts in co-hosting environment and has immediate plans for dedicated/multiple hosting.
The security part of session is not handled in the program. Sessionid is directly stored on to cookie, which will expose the key of the database table. With some tweaking, it'll be possible to read other client's session values. It is strongly advised to encrypt the SessionID before storing on to the cookie. This will involve additional coding in Data Access Layer functions.
Using ASP Intrinsic session (flavor 2) will face a performance hit, as data is stored in two places, in ASP intrinsic session and clsSession. The idea is to use this a stepping-stone for an easy expansion at a later stage.
The full source code is provided. Please look at readme.txt file for implementation details. You'll need ServerObject's Guidmaker for unique id creation. The component is a free download and is available in http://www.serverobjects.com/products.htm#free?WROXEMPTOKEN=711612ZW37kEJTrwI1rvrPIrYD. You can also use database provided unique id.
|
| |||||||
| |||||||||||||||
|
ASPToday is brought to you by
Wrox Press (http://www.wrox.com/). Please see our terms
and conditions and privacy
policy. ASPToday is optimised for Microsoft Internet Explorer 5 browsers. Please report any website problems to [email protected]. Copyright © 2001 Wrox Press. All Rights Reserved. |