Client/Server


Creating a Guestbook with ISAPI

Don Gaspar

Collecting data from visitors to your Web site is always a boon to marketing. You just have to steer a course through the alphabet soup of HTML, CGI, ISAPI, and STL.


When you've got potential customers browsing your web site, you don't want them to quickly jump to another URL or hit Back on their browser. You'll likely lose them. With all the other web sites competing for their attention, no matter how much contact information you supply on your page — phone number, email address, even your Pony Express address with cool graphics — they may never make it back. You may have lost a potential customer.

Having customers fill out a form before they go can alleviate this problem. You can maintain a guestbook of information gathered from your customers, including email addresses, products they might be interested in, etc. Forms can do anything from querying users for their address, name, and phone number to asking them "how many" items they would like to add to that web shopping cart.

As simple as forms are, you still don't find many dynamic pages using them. This article touches on how to create a form in HTML, but the main focus will be on the server's handling of the collected data. Two well-known methods of interfacing with a server are Microsoft's ISAPI (Internet Server API) and the universally available CGI (Common Gateway Interface). There are similarities in the uses of CGI and ISAPI, so I can treat them almost in parallel. For the purposes of this article, I assume you're using the Internet Information Server (IIS) server, and I show how to implement the guestbook with ISAPI. I include comments on how it would be done in CGI at relevant points. The source code for the ISAPI implementation is available on the CUJ ftp site (see p. 3 for downloading information).

Creating a Form in HTML

Handling data on the server requires an understanding of how HTML handles forms, so I concentrate on that first. In particular, I focus on how variables get passed to the server module.

Throughout this article, I use the example of an order form for products that are available for beta testing (see Figure 1) . The HTML for this form must specify links in the order form's fields that correspond to the fields to be used in the guestbook database. (It may seem like a long jump, but we'll get there before this article is through.) What the user types into these fields will be stored in HTML variables. Note that while this example uses edit-text fields for user input, you could use anything on your pages: checkboxes, comboboxes, radiobuttons, etc.

The HTML variables will contain the following information from the data form:

first_name
last_name
company
address1
address2
city
state
zip
phone_number
email_address
comments

This list of variables is just screaming out "structure," so when I get to the ISAPI portion of this project I create a struct whose members directly correspond to these variables:

struct GuestInfoRef
{
 LPTSTR first_name;
 LPTSTR last_name;
 LPTSTR company;
 LPTSTR address1;
 LPTSTR address2;
 LPTSTR city;
 LPTSTR state;
 LPTSTR zip;
 LPTSTR phone_number;
 LPTSTR email_address;
 LPTSTR comments;
};

In the HTML page, each variable looks something like this:

INPUT TYPE="text" NAME="first_name" VALUE="" SIZE=37 MAXLENGTH=40

Note that the NAME= field specifies the name of the variable that will be sent to the server. (I've also imposed some size limitations, mostly to stop kids from pushing the database to extreme limits while having some fun with sending tons of text as a gag.) To submit the data, I've added a few controls to the page: Send and Reset. Here's the HTML for these controls:

<INPUT TYPE="submit" NAME="" VALUE="Send" ></TD>
<INPUT TYPE="reset" NAME="" VALUE="Reset" >

The types "submit" and "reset" are HTML-defined actions; the variable names aren't used here. "reset" will cause the user's browser to clear all the fields on the form. "submit" is linked to a function on the server; the following line of HTML will link any "submit" to a member function called SignGuestBook:

<INPUT TYPE=HIDDEN NAME="MfcISAPICommand" VALUE = "SignGuestBook">

VALUE= specifies the names that will appear on the controls; it offers no functionality of concern here since we're not using it. I also could have linked a variable to an HTML button by adding NAME="some_var". "some_var" would have been sent to the ISAPI or CGI module as well.

The following line specifies which CGI or ISAPI module to call:

<FORM ACTION="/scripts/Guestbook.dll" METHOD=POST>

I could also have done something like this:

<FORM ACTION="/scripts/Guestbook.dll?SignGuestBook" METHOD=POST>

For CGI applications, just replace the .dll extension with .exe, or whatever is appropriate, in the first of the two HTML snippets above.

POST is an adequate action for this HTML form — in the case of the second snippet, it tells the server that data is being posted from the form to the function SignGuestBook in the module /scripts/guestbook.dll. The customer's browser will generate a URL something like the following and pass it to the server:

http://www.gigantor.com/scripts/guestbook.dll?SignGuestBook ?first_name=Don&
    last_name=gaspar&
    address1='650 Castro Street, Suite #120-228&
    next_variable = , ...

The variables previously discussed are easy to find in this URL: the list of variables starts after the last ?; from that point each variable starts with a & and its value appears after the = sign. All the server needs to do is receive these items, copy them into a structure of type GuestInfoRef, and save the structure to the disk.

The ISAPI Implementation

I've implemented both the ISAPI and CGI versions of the guestbook. The ISAPI version uses some code from MFC to parse the variables; in the CGI version, I had to explicitly code the parser and pass the variables on as appropriate.

As I showed in the previous section, I've defined a structure of type GuestInfoRef to contain the form's data. This structure is the same in both the ISAPI and CGI versions. (If you're not using VC++ you may want to change the LPTSTR type to a char *.)

Visual C++ 4.2 and later provide a class CHttpServer that is used by all MFC ISAPI modules. I've derived a class CGuestBook from this class to extend its functionality. The first thing I need is a member function to receive the information entered by the user:

void CGuestBook::SignGuestBook
  ( CHttpServerContext *pCtxt,
    LPTSTR inFirstName,
    LPTSTR inLastName,
    LPTSTR inCompanyName,
    LPTSTR inAddress1,
    LPTSTR inAddress2,
    LPTSTR inCity,
    LPTSTR inState,
    LPTSTR inZip,
    LPTSTR inPhone,
    LPTSTR inEmail,
    LPTSTR inComments)

Each argument to this function matches a member of the GuestInfoRef structure, which also matches the sequence of variables on the HTML form. The pointer pCtxt is furnished by the CHttpServer class. It contains (among lots of things) an HTTP stream where output can be directed. It's worth noting that the CHttpServer class breaks out the arguments for us and passes them into the correct member function.

MFC PARSE Maps

MFC uses a PARSE map so that the CHttpServer class will know how to parse variables that arrive and send them to the Guestbook DLL. This PARSE map must be written so that the proper member function is called with the correct arguments. The following point is essential (and hard to find any documentation on): the arguments must match the form exactly, or the PARSE map will not resolve the proper member function to call. If a variable name does not match, or the number of variables is wrong, then the PARSE map calls the Default member function. This is a good place to put a debugging message, error message, or something equivalent.

Somewhere in the source the PARSE map will contain:

BEGIN_PARSE_MAP(CGuestBook, CHttpServer)
 ON_PARSE_COMMAND(Default, CGuestBook, ITS_EMPTY)
 DEFAULT_PARSE_COMMAND(Default, CGuestBook)
END_PARSE_MAP(CGuestBook)

For those who have experience with MFC, this should remind you of the MFC MESSAGE maps. In effect, these macros set the default member function to Default in the class CGuestBook. Default has no arguments, so ITS_EMPTY is passed. If the function were passed some arguments, the ON_PARSE_COMMAND macro call would appear similar to the following:

ON_PARSE_COMMAND(Default, CGuestBook, ITS_PSTR ITS_PSTR)

The above statement tells the CHttpServer that the Default member function has two string pointer arguments. (Note that there are no commas between the argument types; I lost an entire evening not knowing that, so I hope you've saved an evening reading about it here.)

Other possible argument types are as follows:

ITS_PSTR  a string pointer
ITS_I2    two-byte integer
ITS_I4    four-byte integer (long)
ITS_R4    float
ITS_R8    double
ITS_EMPTY no arguments being used

Thus, to prepare CHttpServer for a SignGuestBook member function, I use the following:

ON_PARSE_COMMAND(SignGuestBook, \
  CGuestBook, ITS_PSTR ITS_PSTR \
  ITS_PSTR ITS_PSTR ITS_PSTR    \
  ITS_PSTR ITS_PSTR ITS_PSTR    \
  ITS_PSTR ITS_PSTR ITS_PSTR)   \

This text must be placed inside the BEGIN_PARSE_MAP and END_PARSE_MAP macros! It is also necessary to identify the form's variables that correspond to the arguments of this member function. So right after ON_PARSE_COMMAND, I place the following:

ON_PARSE_COMMAND_PARAMS("first_name \
  last_name company address1 address2\
  city state zip phone_number \
  email_address comments")

The variables match the form's names identically; CHttpServer will parse these variables and call the appropriate function (SignGuestBook in this case). In the CGI implementation, I provide some functions that parse these variables out — slightly more work, but not rocket science either.

Saving the Data

Now I show how to save the items to a disk file; you may want to save yours to an SQL server, or link into FileMaker or FoxPro with ODBC commands. Just remember that a DLL can be used by lots of people simultaneously, so you must protect certain resources from being clobbered — like a file being written at the same time. Imagine two customers filling out information and hitting the "submit" button at the same time: you could lose one, or even corrupt a file or two. I've added some blocking in critical sections for the ISAPI module:

CSingleLock lock(&m_criticalParts, TRUE);
...save the file, etc.
lock.Unlock()

The m_criticalParts is a CCriticalSection class, which I have made a private member of the CGuestBook class. This effectively stops other threads from executing the code writing to the file while the current thread is writing to it. In the CGI implementation, I have created a loop that tries to open the file several times until it gets write access.

Since I'm working in C++, I should be able to stream the output to the file, appending to the end of the file. I considered using something like an ostream_iterator on an ostream, but it seemed like some overkill since all I was going to do was append a single guest at a time. I use the following code to write the data to the file:

ofstream customerFile("MyCustomers",ios::ate | ios::out); // append "At The End"
customerFile << cust;      // put it in our file
customerFile.close();      // save our changes

This code should be self-explanatory. I've also written a few lines to implement the direction operators:

ostream&
operator<< ( ostream &inOStream, const GuestInfo &inGuest )
{
 inOStream << inGuest.first_name << "|";
 inOStream << inGuest.last_name << "|";
 inOStream << inGuest.company << "|";
 inOStream << inGuest.address1 << "|";
 inOStream << inGuest.address2 << "|";
 inOStream << inGuest.city << "|";
 inOStream << inGuest.state << "|";
 inOStream << inGuest.zip << "|";
 inOStream << inGuest.phone_number << "|";
 inOStream << inGuest.email_address << "|";
 inOStream << inGuest.comments << "|";
 return inOStream;
}
istream&
operator>> ( istream &inIStream, GuestInfo &outGuest )
{
inIStream.getline(outGuest.first_name,32,'|');
 inIStream.getline(outGuest.last_name,32,'|');
 inIStream.getline(outGuest.company,32,'|');
 inIStream.getline(outGuest.address1,32,'|');
 inIStream.getline(outGuest.address2,32,'|');
 inIStream.getline(outGuest.city,32,'|');
 inIStream.getline(outGuest.state,32,'|');
 inIStream.getline(outGuest.zip,16,'|');
 inIStream.getline(outGuest.phone_number,16,'|');
 inIStream.getline(outGuest.email_address,64,'|');
 inIStream.getline(outGuest.comments,256,'|');
 return inIStream;
}

The istream's getline gets the data up to the delimeter ('|' in this case), or the size of the buffer you're copying to — whichever it hits first. Note that when using getline, as opposed to get, that the istream is incremented one past the delimeter, so the '|' character in this case is never seen.

In the CGI version, each copy runs in its own process space, but still uses the same data file. To prevent two or more CGI instances from accessing the file at the same time, the code loops through until it gets exclusive priveleges for that file. I've set the loop for 100 iterations. (This is really a kludge, and I wish there were a better way around it.)

All the information passed in through a GuestInfoRef structure is thus directed to a file. The code to do all this amounts to about 50 lines, counting the PARSE map. Not bad for adding the flexibility of a dynamic guestbook to an HTML page.

Extensions and Refinements

I've also provided another page using an istream_iterator to get information out of the guestbook database. I've used this iterator to build a dynamic page showing all the existing guests who have signed onto the guest book. Note: you do not want to do this with your customers. I only did it here as an example. Customers generally want privacy and do not want their information displayed for the spammers to use and sell to yet other spammers. You may want to password protect an intranet that contains customer information and view it internally for your business, however.

The iterator provides an elegant way to load the GuestInfoRef structures into memory, and then display the names of guests who have signed the guest book. Here's a brief look at the code:

// restrict access
CSingleLock lock(&m_criticalParts, TRUE);
try
{
 vector<GuestInfo> theGuests;
 ifstream inputFile("OurGuestBook"); // the file to use
 //iterator
 istream_iterator<GuestInfo,ptrdiff_t> iter(inputFile), end;
 //copy to vector
 copy( iter, end, inserter(theGuests,theGuests.begin()) );
 ...
}

In brief, this code does the following:

This function starts at the beginning of the guestbook file, and iterates through the file; at each iteration it uses the istream_iterator to get a record's worth of data and then stuffs it into an element of an STL vector of GuestInfoRef structures. This technique provides an excellent way to implement persistence, and a convenient method for reading files. The STL copy function has linear time complexity since first-last copies are performed. Space complexity remains constant.

This guestbook display also calls for a way to sort the guestbook efficiently, and a way to handle duplicate names (when they do not represent different people). Fortunately, STL provides a lot of help in this area, with its sort and unique functions.

The first thing to do is open an output file, so that once things are reordered and sorted the changes can be saved:

ofstream outputFile("OurGuestBook");
ostream_iterator<GuestInfo> oiter(outputFile);

The next thing to do is call sort, passing in iterators to the beginning and end of the storage container, and a comparator function to be used in the sort. The comparator checks the last names, and if they're the same it then checks the first names using a simple string compare:

// alphabetically
sort( theGuests.begin(), theGuests.end(),
    guest_bigger_than );

sort's time complexity is listed as O(NlogN), with constant space complexity. That's not bad considering it comes practically for free (almost free — it was necessary to include the header file and write a few lines of code). Interested readers can study the comparator function in the source code. It demonstrates the general approach for using comparators/predicates with STL functions.

To ensure that all the items are unique, call the STL unique function:

// remove duplicates
vector<GuestInfo>::iterator last =
  unique( theGuests.begin(),
    theGuests.end(),check_uniqueness ); 

unique returns an iterator set to the position one past the last element in the container. unique has linear time complexity since the comparisons take place from first element to last; space complexity remains constant.

One problem with unique is that it leaves the items past "last" still hanging around, so they must be erased. After calling the vector's erase function, I write the changes to the output file using the ostream_iterator mentioned above:

theGuests.erase( last, theGuests.end() );
// write the output file!
copy( theGuests.begin(), last, oiter );
outputFile.close();

I've used that handy copy function again; this time it goes in reverse order from the istream_iterator. theGuests.begin() and last are iterators to the container in memory, and oiter puts everything into the output file.

That's it. This completes the design of a dynamic guestbook that displays guests who have signed in, sorts the names alphabetically, and removes duplicate names. Since I've covered so much ground here, I encourage you to go through the sources and try things out on your server. One nightmare I neglected to mention is that debugging these things can be difficult; it would be best to test the code on a separate version of your server that's running on your development machine.

Thanks for Stopping By

Following the approach demonstrated here to build a guestbook, it should be easy to generate your own pages that are dynamic and also interactive with the user. The guestbook database can contain customer data, which can help you expand your sales, or provide additional leads if you're a consultant. It's sort of like having a receptionist who works 24x7 and costs almost nothing. The only thing missing now is selling goods from your pages; wouldn't it be great to sell your goods with a store that's open 24x7? That's another story. A long one. o

Don Gaspar is president of Gigantor Software, Inc., of Mountain View, California. He specializes in server architectures and is currently completing the book Web Commerce in C++, to be published by Coriolis Group Books in September 1997. He can be reached at don@gigantor.com.