Overview
This chapter examines the ISAPI and CGI technologies. You can use CGI to
create applications that extend a Web server, and you can use ISAPI to create
DLLs that extend a Web server. In particular, ISAPI allows you to write scripts
and filters and to interact dynamically with a user of your browser.
ISAPI technology is specific to the Internet Information Server that ships
with Windows NT and to the Personal Web Server that ships with Microsoft
FrontPage. It is, however, merely a specification, and other servers could
conform to it if they wish. Several different vendors, including Borland, have
created technology that allows ISAPI-based technology to be used in conjunction
with NSAPI, which is a similar technology to ISAPI. (The NSAPI/ISAPI bridge is
available in Delphi 3.0, for example.)
ISAPI programming is very similar to CGI programming. The only major
difference is that you're creating a DLL instead of an executable. DLLs are
advantageous because they can be loaded into the address space of the Web
server. This capability gives them a leg up over CGI when you're considering
performance. CGI, on the other hand, is a very simple specification to use, and
it adapts itself easily to database applications.
ISAPI's reliance on the Windows platform might be a serious limitation in
some other context, but because C++Builder also relies on Windows, discussing
the topic at length in this book makes sense. Another feature to recommend ISAPI
is its extreme simplicity. Friendly, powerful, easy-to-use APIs are the bread
and butter of this book, and ISAPI fits the bill beautifully. Writing BCB
database applications with ISAPI is, however, a bit tricky.
The first half of this chapter deals with ISAPI, and the second half deals
with CGI. I will show how to retrieve data from both ISAPI DLLs and CGI
applications, though my treatment of the subject is more complete in the section
on CGI. If you are interested primarily in ISAPI, you should also take the time
to read about the CGI database applications in the second half of this chapter.
The C++ code in this chapter relies on the presence of several HTML files
that are quoted in full in this chapter and that are also available on the CD
that ships with this book. However, the C++ code will not function properly
unless the HTML files quoted in this chapter are in the correct location on your
hard drive. See the README.TXT file that comes with the CD for
additional information on setting up your system correctly to run this code.
Some of these files are located in the root of the Chap26 directory on
the CD. You will also need to use several databases' aliases described in the
readme file.
Hardware and Software Requirements
As I implied in the "Overview" section for this chapter, the code discussed
here requires one of the following:
- A copy of Microsoft Windows NT 3.51 Server or NT 4.0 or later server with
the Internet Information Server loaded
- Windows 95 and a copy of the Personal Web Server that ships with FrontPage
At the time of this writing, it is not clear whether the Personal Web Server
(PWS) will be available from other sources besides FrontPage. I downloaded the
beta copy I used while writing this book directly from Microsoft's Web site.
Checking to see if this copy is still available in that form is probably
worthwhile.
You should also check to see if the Netscape or WebSite servers are now
supporting the ISAPI API. An unfortunate and rather unseemly economic battle
between Microsoft and its various competitors may slow down or even halt the
spread of ISAPI as a standard, but checking to see if the battle has cooled
somewhat is still worthwhile.
At any rate, the PWS is a useful piece of software for home users to explore.
It will turn any Windows 95 machine into a Web server. I'm not sure how robust
it will be under the strain of more than a few contiguous users. However, it is
ideal if you want to set up a Web server in your home or in a small office.
WebSite, from O'Reilly, is another fine product to turn to, particularly if you
want to select a well-tested, robust server that can carry a heavy load.
You should also have a second computer equipped with a Web browser. This
second computer can be running any operating system and can use virtually any
software that supports Web browsing. I can think of no reason why you can't test
most of this code on a single machine running a server, of course, but you will
hardly get into the spirit of this enterprise if you're limited to that kind of
setup.
I assume that most readers working in a business setting will have an
intranet setup that will allow them to experiment with this technology. If
you're working at home, I cannot stress too often the incredible value of
setting up a network in your house. Network cards are very inexpensive these
days. One of the ones I use in my home cost about $30 new. Network cable is also
inexpensive, and both Windows 95 and Windows NT come equipped with all the
software you need to set up a network that supports both file browsing with
Windows Explorer and also TCP/IP.
When I first set up a network in my home, I thought I was pushing the extreme
edge of modern technology. Now I simply take it for granted and can't understand
how in the world I ever got along without it. Old computers don't have to die;
they can just become Web servers. Small hard drives are extended easily by
sharing storage space across multiple machines. After all, you don't need a
separate copy of every application or every file on each machine. You can just
share drives back and forth between machines, thereby saving a tremendous amount
of space.
Most importantly, you can study and experiment with your network at your
leisure and then apply that knowledge at work. A home network is an ideal place
to educate yourself regarding this valuable technology.
Getting More Information on ISAPI
The best place to go for information on ISAPI is the Microsoft MSDN or the
Microsoft Internet SDK. These two sources provide most of the information you
need that can't be found in this book.
Here's a place you can go on the Web if you want to find out more about the
ISAPI specification:
http://www.microsoft.com/win32dev/apiext/isalegal.htm
Of course, I can't guarantee that this Web page will still be in existence
when you read this book. However, two relatively stable sites on the Web that
should serve as links to this spot are
http://www.microsoft.com/intdev/
http://www.microsoft.com/win32dev/
Check in at both these sites on fairly regular intervals to get updates on
Win32 and Internet technology.
ISAPI
As stated in the Microsoft documentation, ISAPI allows you to "Write
server-side scripts and filters to extend the capabilities of Microsoft Internet
Information Server and other ISAPI Web servers."
ISAPI is a very easy-to-use yet extremely powerful technology that allows you
to extend the reach of the Internet Information Server or the Personal Web
Server. This tool allows you to make your Web site do pretty much whatever you
want it to do. For example, it provides a means for you to
- Set up interactive responses to user input
- Provide database browsing and updating
- Filter input to your browser to track who signs on and where he or she
goes
In the past, the best way to extend a Web server was to create CGI
applications. These powerful tools were limited by their executable format. When
you sent in a CGI-based request from a browser to a server, the CGI application
in question usually had to be loaded into memory, which took a long time. Also,
the CGI technology could be a bit awkward to use under some circumstances.
ISAPI is a method of writing DLLs that replace CGI applications. You can also
write filters with ISAPI, though this subject is not covered in this book. ISAPI
has the advantage of being easier to use than CGI, plus it can be much faster
and make much better use of system resources. In particular, the following
points help explain why ISAPI DLLs are better than CGI applications:
- ISAPI DLLs live in the same address space as the HTTP server. They can
therefore directly access the HTTP services available from the server. They
load into memory more quickly and have much less overhead when it comes to
making a call from the server. These capabilities can be particularly helpful
if you're working under a heavy load.
- You can control when the DLL is loaded or unloaded. For example, you can
preload DLLs for fast access on the first try or unload the ISAPI applications
DLLs that are not being used to free system resources. You can do the same
thing with the CGI executable, but the executable format was not really
designed for this kind of manipulation, although this is part of the native
capability of DLLs.
In this chapter, I will concentrate on writing DLLs that return datasets or
that simply communicate with the user who is running a browser. I will not
explore filters at all. For information on filters, you should go to the
Microsoft Web site or browse the MSDN.
ISAPI Basics
The file Httpext.h contains the key declarations used with ISAPI.
This file should ship with C++Builder and is available with versions of the
Microsoft SDK dated later than July 1996. It should also appear in the
\include\vcl directory as ISAPI.HPP. Because it is a Windows 95-
or Windows NT-based technology, you must be using a 32-bit compiler to access
this technology. You can't use it from a 16-bit compiler, nor is it available on
Windows 3.1.
Httpext.h contains the interface to the ISAPI technology created by
Microsoft. At the time of this writing, C++Builder has no custom interface for
ISAPI, and I will describe only how to use Microsoft's existing technology.
However, ISAPI is extremely easy to use, and the addition of a custom object is
not necessary for most users.
Three functions can serve as entry points to ISAPI DLLs. The first two listed
here are mandatory, whereas the third is optional:
- GetExtensionVersion: This function just does minimal version
checking.
- HttpExtensionProc: This function is the entry point of the DLL,
like the main begin..end block in a Delphi application.
- TerminateExtension: This optional routine can be used to clean up
threads of other memory allocations.
When you're creating an ISAPI DLL, you must export the first two of the three
preceding functions. Implementing these two functions is the key to all ISAPI
programming.
In C++Builder, DEF files are frowned upon. You should therefore make sure
that Httpext.h has been modified to export both of these functions with
__declspec(dllexport):
BOOL WINAPI __declspec(dllexport) GetExtensionVersion(HSE_VERSION_INFO *pVer);
DWORD
WINAPI
__declspec(dllexport) HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);
TerminateExtension is new in ISAPI 2.0. Its declaration looks like
this:
BOOL WINAPI TerminateExtension( DWORD dwFlags );
TerminateExtension is called just before a connection is broken. It
provides a place for you to deallocate memory allocated inside your DLL.
These three routines all contain the word Extension. This term is
used because ISAPI DLLs extend the Internet Information Server or the Personal
Web Server. (Remember, the Internet Information Server is Microsoft's Web
server. If you want to turn an NT Server into a Web server, you use this tool.
It ships with NT 4.0 and is installed automatically during the setup of that
operating system.)
Using GetExtensionVersion
The GetExtensionVersion function must be exported from your DLL;
otherwise, the server will not load your DLL. The only job of this function is
to report the version of ISAPI that you expect to support.
You can always just cut and paste the GetExtensionVersion code into
your DLLs. You need to change the function only slightly when you want to change
the description passed in the lpszExtensionDesc field of the
HSE_VERSION_INFO struct:
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL");
return (TRUE);
};
The parameter passed to this function is declared in Httpext.h as
follows:
typedef struct _HSE_VERSION_INFO {
DWORD dwExtensionVersion; // Version info
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; // Description
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
The two fields of the record are self-explanatory, with the first containing
the ISAPI version number and the second holding a user-defined string describing
the purpose of the DLL. The following are some constants declared in the DLL
that are used in the preceding code:
#define HSE_MAX_EXT_DLL_NAME_LEN 256
#define HSE_VERSION_MAJOR 1 // major version of this spec
#define HSE_VERSION_MINOR 0 // minor version of this spec
That's all you need to do to set up the first of the two mandatory functions
in an ISAPI DLL. The next step, using HttpExtensionProc, is a bit more
complex, so I will treat it in its own section.
Working with the HttpExtensionProc
The HttpExtensionProc routine is the entry point for the DLL. It
serves the same purpose that the main() routine does in a C program, or
that the main begin..end pair does in a Delphi program.
Here is a very simple example of an HttpExtensionProc routine:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
char ResultString[500];
DWORD resultLen;
char *IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL";
strcpy(pECB->lpszLogData, IsapiLogText);
pECB->dwHttpStatusCode = 200;
char *HtmlInfo =
"<HTML>"
"<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>"
"<H1>ISAPI1 Test Results</H1>"
"<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">"
"Hello from a C++ Builder ISAPI DLL!<BR></BODY>"
"</HTML>";
sprintf(ResultString,
"HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s", 500, HtmlInfo);
resultLen = lstrlen(ResultString);
fprintf(out, ResultString);
pECB->WriteClient(pECB->ConnID, ResultString, &resultLen, 0);
return (HSE_STATUS_SUCCESS);
}
If you queried a DLL containing this function from a browser, you would get a
page back with this message:
ISAPI1 Test Results
Hello from a C++ Builder ISAPI DLL!
In the next few paragraphs, I will describe the key points of the code shown
here. However, I'll help you develop a complete understanding of this routine
slowly in the next few sections of the chapter.
The HTML code for querying the DLL might look something like this:
<a href="/scripts/isapi1.dll">ISAPI1 Example</a>
Most of the body of the function is taken up with simple HTML code that
provides basic information to the user:
char *HtmlInfo =
"<HTML>"
"<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>"
"<H1>ISAPI1 Test Results</H1>"
"<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">"
"Hello from a C++ Builder ISAPI DLL!<BR></BODY>"
"</HTML>";
You also need to fill in a few fields of the EXTENSION_CONTROL_BLOCK:
char *IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL";
strcpy(pECB->lpszLogData, IsapiLogText);
pECB->dwHttpStatusCode = 200;
The lpszLogData field contains the string that will be written to
the log on your server. With the Personal Web Server, this log is kept by
default in the Windows directory, though you can change this in the
Administration section of the server applet found in the Control Panel.
The status code in this example is set to 200, which means "OK."
Other possible values include the following:
HTTP_STATUS_BAD_REQUEST
HTTP_STATUS_AUTH_REQUIRED
HTTP_STATUS_FORBIDDEN
HTTP_STATUS_NOT_FOUND
HTTP_STATUS_SERVER_ERROR
HTTP_STATUS_NOT_IMPLEMENTED
More information on the EXTENSION_CONTROL_BLOCK is provided in the
section called "Working with the EXTENSION_CONTROL_BLOCK."
Notice the function pointer called WriteClient in the struct. You
can call this function to send information back to the browser. When calling
this function, you use the value in the ConnID field of the
EXTENSION_CONTROL_BLOCK struct. ConnID is filled in for you
automatically when the HttpExtensionProc function is called.
Before you look at the EXTENSION_CONTROL_BLOCK struct, let me show
you a complete ISAPI DLL that uses the HttpExtensionProc function shown
in this section.
A Stripped-Down ISAPI Example
The source code in Listing 26.1 shows how to create the simplest possible
ISAPI DLL. The goal is to remove all the complications from the code, and just
include enough information to make sure everything is working correctly.
Listing 26.1. The ISAPI1 example.
///////////////////////////////////////
// FILE: ISAPI1.CPP
// PROJECT: ISAPI1.DLL
// copyright (3) 1996 by Charlie Calvert
//
// This example shows how to use ISAPI, which is similar to creating a
// CGI application. The code should return a simple string to an HTML
// browser such as the Internet Explorer.
//
// Here is the HTML you output in a browser to call this ISAPI DLL:
//
// <HTML>
// <HEAD>
// <TITLE>CharlieC Home Page</TITLE>
// </HEAD>
// <BODY>
// <H1>My Home Page </H1>
// <P>
// This is the home page for my home computer.
// <P>
// <A HREF="/scripts/isapi1.dll" >ISAPI One</A><BR>
// </BODY>
// </HTML>
#include <vcl\vcl.h>
#include <string.h>
#include <stdio.h>
#pragma hdrstop
#include "..\..\utils\Httpext.h"
USERES("Isapi1.res");
FILE *out;
// GetExtensionVersion callback definition
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
fputs("Version", out);
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL");
return (TRUE);
};
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
AnsiString ResultString;
DWORD resultLen;
AnsiString IsapiLogText = "ISAPI1 - Simple ISAPI Extension DLL";
strcpy(pECB->lpszLogData, IsapiLogText.c_str());
AnsiString HtmlInfo =
"<HTML>"
"<HEAD><TITLE>C++ Builder ISAPI DLL </TITLE></HEAD>"
"<H1>ISAPI1 Test Results</H1>"
"<BODY bgcolor=\"#0000FF\" text=\"#00FFFF\">"
"You are talking to a C++Builder ISAPI DLL."
"<BR></BODY>"
"</HTML>";
pECB->dwHttpStatusCode = 200;
ResultString = Format(
"HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s",
OPENARRAY(TVarRec, (HtmlInfo.Length(), HtmlInfo)));
resultLen = ResultString.Length();
fprintf(out, ResultString.c_str());
pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &resultLen, 0);
return (HSE_STATUS_SUCCESS);
}
#pragma argsused
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
switch (reason)
{
case DLL_PROCESS_ATTACH:
out = fopen("c:\\test.txt", "w+");
fprintf(out,"hello");
break;
case DLL_PROCESS_DETACH:
fprintf(out,"goodbye");
fclose(out);
break;
default:
break;
}
return (TRUE);
}
To use this DLL, you should copy it into a subdirectory of the scripts
directory beneath the root for your Web. On my NT 4.0 machine, the subdirectory
looks like this:
c:\winnt\system32\inetsrv\scripts\mystuff\isapi1.dll
In this case, I have created the directory called MYSTUFF, and it is
used solely for storing ISAPI DLLs I have created. Your mileage may, of course,
differ on your machine, depending on where you put the InetSrv
directory and various other factors.
To call this DLL, you should add the following hyperlink to one of your HTML
pages:
<A HREF="/scripts/mystuff/isapi1.dll" >ISAPI One</A><BR>
For example, here is a complete sample page:
<HTML>
<HEAD><TITLE>An ISAPI Page</TITLE></HEAD>
<BODY>
<H1>My ISAPI Page</H1>
<P>This is the home page for ISAPI on my computer.<P>
<A HREF="/scripts/mystuff/isapi1.dll" >ISAPI One</A><BR>
</BODY>
</HTML>
When the user clicks the hyperlink, the ISAPI1 DLL will be called and the
string "Hello from C++ Builder" will appear in the user's browser. If
you did not put the ISAPI1.DLL in the MYSTUFF directory, then
you should change the preceding HTML code to reflect that fact. Notice that the
path you assign is relative to the InetSrv directory and does not, and
should not, contain the entire path to your DLL.
Note that if you copy the ISAPI1.DLL into the MYSTUFF
directory multiple times, you will need to shut down the WWW portion of the
Internet server before each copy. The rule is that you can copy the DLL the
first time for free, but after you have used it, it belongs to the server, and
you need to shut down the WWW services on the server before you can copy an
updated version of the file over the first copy. You can use the Internet
Service Manager application to shut down the WWW services on the NT Server. This
application should be in the Microsoft Internet Server group created in Windows
Explorer or Program Manager (NT 3.51) at the time of the installation of the
Internet Information Server. You can use the PWS applet in the Control Panel if
you're using the Personal Web Server on Windows 95 or on the Windows NT
Workstation.
Working with the EXTENSION_CONTROL_BLOCK
By this point in the chapter, you should be able to create your first ISAPI
DLL and call it from a Web browser on a second machine. The rest of this chapter
explores ISAPI in more depth.
The following fairly complex record is passed as the sole parameter to
HttpExtensionProc:
typedef struct _EXTENSION_CONTROL_BLOCK {
DWORD cbSize; // size of this struct.
DWORD dwVersion; // version info of this spec
HCONN ConnID; // Context number not to be modified!
DWORD dwHttpStatusCode; // HTTP Status code
CHAR lpszLogData[HSE_LOG_BUFFER_LEN];// log info
LPSTR lpszMethod; // REQUEST_METHOD
LPSTR lpszQueryString; // QUERY_STRING
LPSTR lpszPathInfo; // PATH_INFO
LPSTR lpszPathTranslated; // PATH_TRANSLATED
DWORD cbTotalBytes; // Total bytes indicated from client
DWORD cbAvailable; // Available number of bytes
LPBYTE lpbData; // pointer to cbAvailable bytes
LPSTR lpszContentType; // Content type of client data
BOOL (WINAPI * GetServerVariable) ( HCONN hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSize );
BOOL (WINAPI * WriteClient) ( HCONN ConnID,
LPVOID Buffer,
LPDWORD lpdwBytes,
DWORD dwReserved );
BOOL (WINAPI * ReadClient) ( HCONN ConnID,
LPVOID lpvBuffer,
LPDWORD lpdwSize );
BOOL (WINAPI * ServerSupportFunction)( HCONN hConn,
DWORD dwHSERRequest,
LPVOID lpvBuffer,
LPDWORD lpdwSize,
LPDWORD lpdwDataType );
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
Notice that this record contains the ConnID field referenced
previously and passed as the first parameter to WriteClient.
The first parameter of this record is used for version control. It should be
set to the size of the EXTENSION_CONTROL_BLOCK. If Microsoft changes
this structure, then they can tell which version of the structure they are
dealing with by checking the size of the record as recorded in this field. You
should never change any of the first three fields of this record; these fields
are filled out ahead of time by ISAPI and can only be referenced, not changed,
by your program.
The most important field of this record is probably the lpszQueryString,
which contains information about the query passed in from the server. For
example, suppose you have created a DLL called ISAPI1.DLL. To call this
DLL, you would create an HREF that looks like this in one of your
browser pages:
<A HREF="/scripts/mystuff/test1.dll">Test One</A>
If you want to query the DLL, you would edit the preceding line so that it
looks like this:
<A HREF="/scripts/mystuff/test1.dll?MyQuery">Test One</A>
Given the second of the two HTML fragments listed here, your DLL would get
called with the string "MyQuery" in the lpszQueryString
parameter. Notice in particular the use of the question mark, followed by the
query string itself.
You could, of course, change the query string at will. For example, you could
write
<A HREF="/scripts/mystuff/test1.dll?ServerName">Test One</A>
To this query, the DLL might reply with the name of the server. You have no
limits on what you can pass in this parameter, but the string after the question
mark cannot have any spaces in it. If you need to use spaces, replace them with
a plus sign: Instead of "Server Name", write "Server+Name".
The string can be anything you want, and it is up to you to parse the
information from inside the DLL as you like.
When you return information from the server back to the browser, you use the
WriteClient function pointer that is part of this record.
Writers of CGI applications will notice that the syntax for sending query
strings is familiar. Indeed, ISAPI follows many of the conventions of CGI, and
most of the fields in the EXTENSION_CONTROL_BLOCK are simply borrowed
directly to initialize this pointer; it is passed to you gratis by the Internet
Information Server.
Another key field in the EXTENSION_CONTROL_BLOCK is the lpbData
field, which contains any additional information sent to you by the browser. In
particular, it is used to pass information associated with a Submit button. For
example, if you have an HTML form with a number of fields in it, the information
from these fields will be sent in the pointer called lpbData after the
Submit button is clicked. The section of this chapter called "Getting
Information from a Submit Button" focuses on how to handle this situation.
So far I have zeroed in on three key fields of the
EXTENSION_CONTROL_BLOCK:
- WriteClient: A pointer to a function that allows you to send
formatted HTML data back to the browser. This function uses the ConnID
field of EXTENSION_CONTROL_BLOCK.
- lpszQueryString: The query passed to you from the browser.
- lpbData: Any additional data being passed to you from the
browser. This data is usually the contents of any fields on an HTML form. I
discuss this field further in the section on the Submit button.
Mirroring the Fields of the EXTENSION_CONTROL_BLOCK
The best way to get a feeling for how the rest of the fields of
EXTENSION_CONTROL_BLOCK work is simply to mirror them back to yourself in a
browser. In other words, you can create an HTML page that allows the user to
call a custom ISAPI DLL. The purpose of this ISAPI DLL is simply to snag the
contents of each field of the EXTENSION_CONTROL_BLOCK, format them in
HTML, and send them back to the browser. This will turn your browser into a
rather jazzy debugger that shows each of the fields in the
EXTENSION_CONTROL_BLOCK. Listing 26.2 contains the source to a DLL called
IsapiVars that performs this task.
Listing 26.2. The IsapiVars code that mirrors back the
parameters sent in the EXTENSION_CONTROL_BLOCK.
///////////////////////////////////////
// IsapiVars.cpp
// Mirror back the information sent to an ISAPI DLL by the server
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "..\..\utils\Httpext.h"
USERES("IsapiVars.res");
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL");
return (TRUE);
};
#define SIZE 2048
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
char ResultString[SIZE * 2];
char HtmlInfo[SIZE];
char Buffer[SIZE];
DWORD StrSize;
DWORD resultLen;
char *IsapiLogText = "ISAPIVars from C++ Builder";
strcpy(pECB->lpszLogData, IsapiLogText);
pECB->dwHttpStatusCode = 200;
sprintf(HtmlInfo,
"<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>"
"<H1>Test server results</H1><BODY>"
"Size = %d<BR>"
"Version = %.8x<BR>"
"ConnID = %.8x<BR>"
"Method = %s<BR>"
"Query = %s<BR>"
"PathInfo = %s<BR>"
"PathTranslated = %s<BR>"
"TotalBytes = %d<BR>"
"AvailableBytes = %d<BR>"
"ContentType = %s<BR><BR>"
"<H1>Calls to GetServerVariable</H1>",
pECB->cbSize, pECB->dwVersion, pECB->ConnID,
pECB->lpszMethod, pECB->lpszQueryString,
pECB->lpszPathInfo, pECB->lpszPathTranslated,
pECB->cbTotalBytes, pECB->cbAvailable,
pECB->lpszContentType);
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize);
AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>");
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_HOST", &Buffer, &StrSize);
VarString += "REMOTE_HOST = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_USER", &Buffer, &StrSize);
VarString += "REMOTE_USER = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_NAME", &Buffer, &StrSize);
VarString += "SERVER_NAME = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_PORT", &Buffer, &StrSize);
VarString += "SERVER_PORT = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_PROTOCOL", &Buffer, &StrSize);
VarString += "SERVER_PROTOCOL = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_SOFTWARE", &Buffer, &StrSize);
VarString += "SERVER_SOFTWARE = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "HTTP_ACCEPT", &Buffer, &StrSize);
VarString += "HTTP_ACCEPT = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "URL", &Buffer, &StrSize);
VarString += "URL = " + AnsiString(Buffer) + "<BR><BR><BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "ALL_HTTP", &Buffer, &StrSize);
VarString += "ALL_HTTP = " + AnsiString(Buffer) + "<BR>";
strcat(HtmlInfo, VarString.c_str());
sprintf(ResultString,
"HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s </HTML>",
SIZE, HtmlInfo);
StrSize = strlen(ResultString);
pECB->WriteClient(pECB->ConnID, ResultString, &StrSize, 0);
return (HSE_STATUS_SUCCESS);
}
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
return 1;
}
To call this DLL, you should create an HTML script that contains the
following line:
<A HREF="/scripts/mystuff/isapivars.dll">Test One</A> <BR>
Of course, the actual path shown in your code may be somewhat different from
what I show here.
The HttpExtensionProc for this DLL is broken into two sections. The
first retrieves all the main fields from the EXTENSION_CONTROL_BLOCK,
and the second goes to town on one particular field, which is a function called
GetServerVariable.
The code that parses the main fields of the EXTENSION_CONTROL_BLOCK
is fairly straightforward:
sprintf(HtmlInfo,
"<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>"
"<H1>Test server results</H1><BODY>"
"Size = %d<BR>"
"Version = %.8x<BR>"
"ConnID = %.8x<BR>"
"Method = %s<BR>"
"Query = %s<BR>"
"PathInfo = %s<BR>"
"PathTranslated = %s<BR>"
"TotalBytes = %d<BR>"
"AvailableBytes = %d<BR>"
"ContentType = %s<BR><BR>"
"<H1>Calls to GetServerVariable</H1>",
pECB->cbSize, pECB->dwVersion, pECB->ConnID,
pECB->lpszMethod, pECB->lpszQueryString,
pECB->lpszPathInfo, pECB->lpszPathTranslated,
pECB->cbTotalBytes, pECB->cbAvailable,
pECB->lpszContentType);
This code is nothing more than a simple call to sprintf. The goal is
simply to take the fields of the EXTENSION_CONTROL_BLOCK and mirror
them back to the user's browser. To do so, all you need to do is set up a
legitimate HTML document and then add a human-readable version of the struct to
the body of the form.
The output from this part of the code looks like the screen shot shown in
Figure 26.1.
FIGURE 26.1.
The main fields of the EXTENSION_CONTROL_BLOCK shown in a browser.
Working with GetServerVariable is a bit more complicated. As a
result, I will give this description a whole section so that it can have plenty
of room in which to knock about.
The GetServerVariable and ReadClient Routines
You can use GetServerVariable to retrieve information from a server
just as you would request information inside a CGI application. Here is an
example of calling the routine:
#define SIZE 2048;
...
char Buffer[SIZE];
...
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize);
AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>");
This function takes a connection ID in the first parameter, a constant in the
second parameter, a buffer in the third parameter, and the length of the buffer
in the fourth parameter:
BOOL WINAPI GetServerVariable(
HCONN hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSizeofBuffer
);
As a rule, you will have to reset the fourth parameter before each call to
this function because the function itself returns the length of the string found
in the lpvBuffer parameter in the lpdwSizeOfBuffer parameter.
You definitely don't want to raise any exceptions or cause any errors to occur
in your DLL, so I suggest playing it safe when calling this function.
The preceding code first sets the length of the buffer that will hold the
information retrieved from the server. It then calls the server and asks for
information. In this case, it asks for the content length of the information
sent by the server.
You can pass the following strings in the second parameter of
GetServerVariable:
| AUTH_TYPE |
Type of authentication used. |
| CONTENT_LENGTH |
Number of bytes you can expect to receive from the client. |
| CONTENT_TYPE |
Type of information in the body of a POST request. |
| PATH_INFO |
Trailing part of the URL after the script name. |
| PATH_TRANSLATED |
PATH_INFO with any virtual pathname expanded. |
| QUERY_STRING |
Info following the "?" in the URL. |
| REMOTE_ADDR |
IP address of the client (could be a gateway or firewall). |
| REMOTE_HOST |
Hostname of the client or agent of the client. |
| REMOTE_USER |
Username supplied by the client and authenticated by the
server. |
| UNMAPPED_REMOTE_USER |
Username before ISAPI mapped to an NT user account. |
| REQUEST_METHOD |
The HTTP request method. |
| SCRIPT_NAME |
The name of the script program being executed. |
| SERVER_NAME |
The server name. |
| SERVER_PORT |
The TCP/IP port on which the request was received. |
| SERVER_PORT_SECURE |
If the request is on the secure port, then this will be
1; otherwise, it is 0. |
| SERVER_PROTOCOL |
Usually HTTP/1.0. |
| SERVER_SOFTWARE |
The name of the server software. |
| ALL_HTTP |
All headers not already parsed into one of the previous
variables. |
| HTTP_ACCEPT |
The special-case HTTP header. |
| URL |
New for version 2.0. The base portion of the URL. |
You can find additional information on these variable names in the Microsoft
online help.
You can see examples of the type of information returned by calling
GetServerVariable in Figure 26.2. Note that this screen shot simply shows
the lower half of the window shown in Figure 26.1.
FIGURE 26.2.
The results of making several repeated calls to GetServerVariable.
Note that many of the preceding pieces of information are automatically
passed in the EXTENSION_CONTROL_BLOCK record. Therefore, you usually do
not need to call GetServerVariable, but you can if you need to,
particularly if you want to retrieve information with ReadClient and
need to know how much information to read.
Most of the time, you don't need to call ReadClient. However, if the
amount of data being sent by the browser is larger than 48KB, you will need to
call ReadClient to get the rest of the data.
The DLLEntryPoint
Before completing the discussion of how this DLL works, I want to take a
moment to get some housekeeping chores out of the way. In particular, I want to
mention the DLLEntryPoint routine, which appears at the bottom of the
DLL.
All DLLs have an entry point that is called automatically. You don't have to
respond to this entry point, but doing so is usually a good idea. In this case,
I simply open a text file for debugging purposes:
#pragma argsused
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
switch (reason)
{
case DLL_PROCESS_ATTACH:
out = fopen("c:\\test.txt", "w+");
fprintf(out,"hello");
break;
case DLL_PROCESS_DETACH:
fprintf(out,"goodbye");
fclose(out);
break;
default:
break;
}
return (TRUE);
}
Nothing about the code shown here is mandatory. You don't have to open a text
file and write to it; the code serves no other purpose than to give you a simple
means of debugging your DLL. In particular, it creates a text file on the server
to leave a record of your DLL's behavior. This file is helpful, particularly if
you're learning ISAPI and have problems simply creating a valid DLL that exports
the key functions.
-
NOTE: You can debug an ISAPI DLL
by using several different means. One rather fancy method is to load the
entire server into the stand-alone debugger, load your ISAPI DLL, and then set
a break point inside it.
Although effective, the technique described in the preceding paragraph can be
overkill. My suggestion is not to be too proud about resorting to the
old-fashioned technique of creating a text file and writing to it. You can
produce very detailed reports in this fashion, and they can show you exactly
what is going on in your DLL. The only flaw in this system is that entering
all those fprintf statements takes a bit of time.
The example shown here has a more valuable purpose then merely showing one
rather simple-minded way to debug an ISAPI DLL. In particular, it reminds you of
the proper way to handle the entry point of a DLL. In addition to the
DLL_PROCESS_ATTACH and DLL_PROCESS_DETACH notifications are two
others called DLL_THREAD_ATTACH and DLL_THREAD_DETACH. Because
multiple clients could be using your DLL at the same time, DLL_THREAD_ATTACH
can be a very important entry point to use when you're debugging or constructing
your DLL.
Getting Information from a Submit Button
Often you get information sent to you from an HTML form that has a Submit
button on it. As long as this information is shorter than 49KB, you can assume
that it will be available in the lpbData field of
TExtensionControlBlock. Otherwise, you will need to call ReadClient.
Here is how you would typically read the information from the pointer passed in
this field:
AnsiString S;
if (pECB->lpbData != NULL)
{
S = (char *)pECB->lpbData;
S = Parse(S);
}
else
S = "Error occurred on get from lpbData field";
This code first checks to see if lpbData is non-NULL. This
type of conservative coding is a necessity in ISAPI, as you don't want errors to
be occurring way over on the server side, where it is hard to see what is going
on. The fragment shown here then typecasts lpbData so that you can
place its contents in a variable of type AnsiString. It then passes the
string to a user-defined function called Parse that handles the string
passed by the server. If something goes wrong, the string variable is set equal
to an error message and then returned to the user so he or she can view it in a
browser.
If you want to see exactly what information is available in the lpbData
field, you can use the following two functions to echo the data back to your Web
browser:
AnsiString SetupHeader(AnsiString &ResultString, AnsiString S, int &Len)
{
char *HeaderInfo = "HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s </HTML>";
ResultString.SetLength(S.Length() + strlen(HeaderInfo) + 1);
Len = ResultString.Length();
sprintf(ResultString.c_str(), HeaderInfo, Len, S);
return ResultString;
}
//HttpExtensionProc callback definition
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
AnsiString ResultString;
int ResultLen;
char *IsapiLogText = "Mirror lpbData";
strcpy(pECB->lpszLogData,IsapiLogText);
pECB->dwHttpStatusCode = 200;
AnsiString S;
if (pECB->lpbData != NULL)
{
S = (char *)pECB->lpbData;
}
else
S = "Error occurred get lpbData field";
SetupHeader(ResultString, S, ResultLen);
pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &(DWORD)ResultLen, 0);
return (HSE_STATUS_SUCCESS);
}
The first function, called SetupHeader, is just a utility routine
that automates the process of setting up a header. It forces you to pass in the
variable that is sent in the third parameter of WriteClient. I do this
simply to help remind myself that I have to initialize this variable before
passing it to the server.
The second routine simply mirrors the lpbData field back to the user
of the DLL. On the CD that accompanies this book, you will find a DLL called
MirrorData and an HTML form called MirrorDataTest.htm, which looks
like this:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=iso-8859-1">
<meta name="GENERATOR" content="Microsoft FrontPage 2.0">
<title>MirrorDataTest</title>
</head>
<body bgcolor="#0000FF" text="#00FFFF">
<h1>View lpbData Test</h1>
<form action="/scripts/Books/BUnleash/MirrorData.dll"
method="POST">
<p>Enter some information: </p>
<p><textarea name="SendData" rows="16" cols="74"></textarea></p>
<p>Enter More Information</p>
<p><input type="text" size="76" name="SendData"></p>
<p><input type="submit" name="SendData" value="Submit"></p>
</form>
</body>
</html>
You can use this code to test the HttpExtensionProc that mirrors
back the value of lpbData. Note that the HTML form contains two fields
for entering text, the first called SendData and the second called
MoreData. If you have only one field and you type Fast into it,
the DLL would mirror back the following to the user:
SendData=Fast&SendData=Submit
If you have two fields, as in the HTML code shown here, then the following
would be mirrored back if the first field contains Fast and the second
contains Loose:
SendData=Fast&MoreData=Loose&SendData=Submit
If you break down this text into three separate fields, they would look like
this:
SendData=Fast
&MoreData=Loose
&SendData=Submit
This text says, in effect, the SendData field has the word Fast
in it, the MoreData field has the word Loose in it, and the
SendData button is a Submit button.
Responding to a Submit Button
Assume that you have an HTML form with the code shown in Listing 26.3.
Listing 26.3. The HTML code for a Web page that uses
ISAPI to interact with the user.
<html>
<head>
<title>Talking ISAPI Test</title>
</head>
<body bgcolor="#0000FF" text="#00FFFF">
<h1>Talking ISAPI Test</h1>
<p>Press this button to see more of the simple ISAPI test:</p>
<form action="/scripts/Books/BUnleash/IsapiTalk.dll"
method="POST">
<p>Enter your name: <input type="text" size="20"
name="SendName"></p>
<p><input type="submit" name="SendName" value="Submit"></p>
</form>
</body>
</html>
This code will produce a form that contains a text area where the user can
enter his or her name and a button called Submit. Given this form, you can
expect the lpbData field to contain the following string, assuming the
user enters the word Sammy in the name field:
SendName=Sammy&SendName=Submit
To understand what is happening here, note the BODY of the HTML
statement composed on the server as reflected in the following excerpt from the
SetUpResString function shown previously:
`<BODY>lpbData = %s </BODY>' +
If you study the code in the HttpExtensionProc function, you will
see that it uses the Format routine to substitute the value of
ECB.lpbData for the %s variable in the preceding piece of code.
(If you don't understand how Format works, see the BCB documentation,
or my references to this method in Chapter 3, "C++Builder and the VCL.")
After you get the information from the form in the lpbData
parameter, you can parse it and return information to the user. For example, you
could extract the number 23 from the preceding example and then square it and
return it to the user. Doing so would in effect allow you to get information
from the user, namely a number, perform a mathematical action on the number, and
then return the result to the user. This means you're creating dynamic,
interactive Web pages on-the-fly, which is the current sine qua non of Internet
programming!
Listing 26.4 shows the complete code for a program that will reply to a user
who enters his or her name into a page of a Web browser and submits it to the
DLL. The HTML that accompanies this program is shown in Listing 26.5.
Listing 26.4. The code for the ISAPITalk DLL.
///////////////////////////////////////
// IsapiTalk.cpp
// Mirror back the information sent to an ISAPI DLL by the server
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <fstream.h>
#include <dir.h>
#pragma hdrstop
#include "..\..\utils\Httpext.h"
USERES("IsapiTalk.res");
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "C++ Builder ISAPI DLL");
return (TRUE);
};
// Remember: IOStream deals with OS paths, not relative path to server!
BOOL GetResultString(AnsiString &Result, AnsiString Path)
{
fstream InFile;
AnsiString FileName(Path + "\\BUnleash\\TalkTest\\TalkReply.htm");
char ch;
char S[500];
InFile.open(FileName.c_str(), ios::in, filebuf::openprot);
if (!InFile)
{
// If we couldn't get file, then what directory were we in?
Result = "<H>Error reading stream!</H>";
FileName = ExtractFilePath(FileName);
if (chdir(FileName.c_str()) == 0)
{
getcurdir(0, S);
Result += S;
}
else
Result += "Could not get file, nor find current directory!";
return FALSE;
}
while (InFile.get(ch))
Result += ch;
return TRUE;
}
AnsiString Parse(AnsiString &S)
{
int i = S.Pos("&");
S.SetLength(i - 1);
S = strrev(S.c_str());
i = S.Pos("=");
S.SetLength(i - 1);
S = strrev(S.c_str());
return S;
}
//HttpExtensionProc callback definition
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
AnsiString Header("HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length:%d\nContent:\n\n");
AnsiString ResultString;
int resultLen;
char *IsapiLogText = "ISAPI Talk";
strcpy(pECB->lpszLogData,IsapiLogText);
pECB->dwHttpStatusCode = 200;
// build the HTML result string
AnsiString S;
if (pECB->lpbData != NULL)
{
S = (char *)pECB->lpbData;
S = Parse(S);
}
else
S = "Error occurred get lpbData field";
AnsiString Date(DateToStr(Now()));
AnsiString Time(TimeToStr(Now()));
GetResultString(ResultString, pECB->lpszPathTranslated);
ResultString = Format(ResultString, OPENARRAY(TVarRec, (S, Date, Time)));
resultLen = ResultString.Length();
Header = Format(Header, OPENARRAY(TVarRec, (resultLen)));
pECB->WriteClient(pECB->ConnID, ResultString.c_str(), &(DWORD)resultLen, 0);
return (HSE_STATUS_SUCCESS);
}
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
return 1;
}
Listing 26.5. The HTML file, called TalkReply.htm,
used in the reply generated by the ISAPITalk DLL.
<html>
<head>
<title>This is a simple htm file</title>
</head>
<body bgcolor="#0000FF" text="#00FFFF">
<h1>Talking HTML Reply</h1>
<p> </p>
<p>Welcome, %s, to the talking HTML file!</p>
<p>It's remarkable that you should drop by today, %s, at %s (PST), as
I was just thinking about you. </p>
</body>
</html>
This program allows an HTML browser to work interactively with a Web server.
Screen shots of before and after a query is made are shown in Figure 26.3 and
Figure 26.4, respectively.
When you're working with this program, note that I assume you have placed the
TalkReply.htm program in the following location relative to the root of
your Web:
\\BUnleash\\TalkTest\\TalkReply.htm
The ISAPITalk DLL might receive the following string from the user
who clicks the Submit button, asking that a number be squared:
SendName=Sammy&SendName=Submit
FIGURE 26.3.
Preparing to query the ISAPITalk DLL.
FIGURE 26.4.
The results of querying the ISAPITalk DLL.
Given this input, the preceding code would return the following string to the
user across the Internet:
Welcome, Sammy, to the talking HTML file!
It's remarkable that you should drop by today, 2/15/97,
at 10:39:29 PM (PST), as I was just thinking about you.
In short, the user enters the string "Sammy", and the ISAPI DLL
mirrors this information back and even adds in the current date and time as it
is reported on the server side. This process sounds trivial, but the key issue
here is that this activity is taking place dynamically on the Internet.
The function that parses the data sent by the user looks like this:
AnsiString Parse(AnsiString &S)
{
int i = S.Pos("&");
S.SetLength(i - 1);
S = strrev(S.c_str());
i = S.Pos("=");
S.SetLength(i - 1);
S = strrev(S.c_str());
return S.
}
The code uses the strrev function from the Standard C Library to
expedite the task of parsing out all the unnecessary data sent by the server.
The ISAPITalk program does not imbed any HTML code inside the C++ code that
makes the DLL run. Instead, it reads in an HTML file and uses the VCL Format
function to fill in the blank fields of the form shown in Listing 26.5.
The code for reading in the HTML file goes to considerable lengths to
properly handle any errors that occur:
if (!InFile)
{
// If we couldn't get file, then what directory were we in?
Result = "<H>Error reading stream!</H>";
FileName = ExtractFilePath(FileName);
if (chdir(FileName.c_str()) == 0)
{
getcurdir(0, S);
Result += S;
}
else
Result += "Could not get file, nor find current directory!";
return FALSE;
}
The issue here is that the file you want to load might be missing or might be
in another directory than the one you suppose. The code shown here attempts to
find the directory in which the DLL expects the HTML file to reside and to
mirror that information back to the user on the off chance that an error occurs.
This information can help you fix any broken links in your program without too
much fussing around.
-
NOTE: The reason the ISAPITalk
program reads in the HTML for the reply form from a file is simply that I
don't like embedding HTML code inside C++ code. No technical reason prevents
me from embedding HTML inside C++, but doing so does tend to be confusing. In
particular, embedding the code makes it difficult for you to generate an
attractive form that has just the proper look you want to produce. If you
separate the HTML from your C++ code, you can use your favorite HTML editor to
produce just the look you want.
The discussion of the ISAPITalk program has one final leg that needs to be
completed before the race is done. This last portion of the journey involves
getting the proper path to the HTML file that you read in while generating the
reply form.
The Server Path Versus the OS Path
When you're writing ISAPI applications, you need to distinguish between the
relative path you use when talking to the Web server and the absolute OS path
you use when executing functions inside your DLL. Server paths should always be
relative to the root of your Web, but OS paths are concerned with the current
drive and directory.
If you embed the name of an HTML file in a hyperlink that is part of a Web
path, you're dealing with relative server paths. If you call the C library
chdir or getcurdir functions, then you're working with an OS path.
For example, I need to pass an OS-based path to the function in the ISAPITalk
program that reads an HTML file from disk.
Finding out the current OS path is relatively easy because ISAPI passes the
absolute path to the root of the Web in the lpszPathTranslated field of
the EXTENSION_CONTROL_BLOCK. For example, if the root of your Web is
C:\WEBSHARE\WWWROOT, that path is passed to you in the
lpszPathTranslated field.
The relative path you use in your Web pages is something that you usually
determine while laying out the Web itself. Sometimes you have to refer to this
path in your code. In particular, you will often reference this path if you're
creating HTML on-the-fly. Most of the time, you simply have to determine the
correct path by looking at the position of your files relative to the root of
your Web.
Here is an example of a relative path used in an HTML file:
\\BUnleash\\TalkTest\\TalkReply.htm
Here is the OS path to the same file:
C:\WEBSHARE\WWWROOT\\BUnleash\\TalkTest\\TalkReply.htm
Whatever you do, don't ever embed full OS pathnames in either your HTML or
your ISAPI DLLs. If you do, then you will have to edit your code and your HTML
whenever you change the location of your Web site. That is much too much work.
Furthermore, from a security point of view, telling the users of your Web any
more than they have to know about the actual layout of your hard drive is
probably not a good idea.
To find out more about this subject, you can examine the ISAPITalk
DLL on the CD that accompanies this book. This program provides an example of
finding out the exact path to a file on the server so that you can load it into
your DLL.
That's most of what I want to say about ISAPI in this chapter. This
information should be enough to get you up and running and having some fun with
this great technology.
Retrieving Data with an ISAPI DLL
In this section you will see a simple example for retrieving data from an
ISAPI DLL. The CGI examples featured later in this book go into more depth on
database techniques to use in this type of program. You can easily convert the
CGI code into code that can be used with ISAPI.
The sample DLL shown in Listing 26.6 retrieves all the data from the Country
table in the BCDEMOS database and shows it to the user in an HTML
table.
Listing 26.6. The IsapiData program shows how to
retrieve data from a database and display it in a browser.
///////////////////////////////////////
// IsapiData
// Return Database rows from an ISAPI DLL
// Copyright (c) 1997 by Charlie Calvert
// Thanks to David Intersimone and Roland Fernandez
//
#include <vcl\vcl.h>
#include "..\..\utils\Httpext.h"
#pragma hdrstop
AnsiString TableToHtml(TDataSet *Table)
{
int i;
AnsiString Result;
Result = "<TABLE BORDER>\n";
for (i = 0; i < Table->FieldCount; i++)
{
Result = Result + Format("<TH>%s</TH>",
OPENARRAY(TVarRec, (Table->Fields[i]->FieldName.c_str())));
}
while (!Table->Eof)
{
Result = Result + "<TR>";
for (i = 0; i < Table->FieldCount; i++)
{
Result = Result + Format("<TD>%s</TD>",
OPENARRAY(TVarRec, (Table->Fields[i]->AsString.c_str())));
}
Result = Result + "</TR>\n";
Table->Next();
}
return Result + "</TABLE>\n";
}
AnsiString _stdcall _export GetData()
{
TTable *Table;
AnsiString Result;
Table = new TTable(Application);
Table->DatabaseName = "DBDEMOS";
Table->TableName = "Country";
Table->Open();
Result = TableToHtml(Table);
Table->Close();
delete Table;
return Result;
}
BOOL WINAPI _export GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL");
return (TRUE);
};
void TextWrite(AnsiString S)
{
FILE *F;
S = S + "\n";
F = fopen("c:\\foo.txt", "w+");
fprintf(F, S.c_str());
fclose(F);
}
void TextAppend(AnsiString S)
{
FILE *F;
S = S + "\n";
F = fopen("c:\\foo.txt", "a+");
fprintf(F, S.c_str());
fclose(F);
}
DWORD WINAPI _export HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
AnsiString Header("HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length: "
" %d\nContent:\n\n");
AnsiString ResultString;
int resultLen;
char *IsapiLogText = "ISAPI1 - Simple BC++ ISAPI Extension DLL";
strcpy(pECB->lpszLogData,IsapiLogText);
pECB->dwHttpStatusCode = 200;
ResultString = GetData();
Header = Format(Header, OPENARRAY(TVarRec, (ResultString.Length())));
Header = Header + ResultString;
resultLen = Header.Length();
pECB->WriteClient(pECB->ConnID, Header.c_str(), &(DWORD)resultLen, 0);
return (HSE_STATUS_SUCCESS);
}
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
switch (reason)
{
case DLL_PROCESS_ATTACH:
// TextWrite("DllLoaded");
break;
case DLL_PROCESS_DETACH:
// TextAppend("Dll UnLoaded");
break;
}
return 1;
}
BCB makes creating database-centered DLLs very simple. To open a table, all
you have to do is write the standard code for creating and opening a database:
AnsiString _stdcall _export GetData()
{
TTable *Table;
AnsiString Result;
Table = new TTable(Application);
Table->DatabaseName = "DBDEMOS";
Table->TableName = "Country";
Table->Open();
Result = TableToHtml(Table);
Table->Close();
delete Table;
return Result;
}
In this example I create and open a table, being sure to set up the
DatabaseName and TableName properties. I then call a special
routine specifically designed to convert the data in a table into an HTML form.
Finally I close and delete the table.
If you wanted to create a simpler test program, you could simply access the
first field of data in the table:
Result = Table->Fields[0]->AsString.c_str()
As you will see in the CGI examples presented next, there is no reason why
you cannot add a DataModule to this kind of program. In fact, you can
extend ISAPI DLLs to the point where they handle relatively complex database
chores such as processing queries or editing records. I want to thank David
Intersimone and Roland Fernandez for showing me that these applications could be
considerably simpler than I thought at first.
Introduction to CGI Programming
CGI programming is similar to ISAPI programming, except that you create
executables rather than DLLs, and it works from inside any respectable server,
rather than only from Microsoft's servers. To get input from a browser, you read
the DOS environment variables.
Here is an example of reading a CGI variable:
char * S;
S = getenv("QUERY_STRING");
Here is an example of writing to a browser from a CGI application:
printf("This string will appear in a browser.");
As you can see, writing this code is not exactly rocket science. Because of
its simplicity, I will whip right through several simple examples and then get
on to some database applications. I don't mean to denigrate CGI because it is so
simple to use. I don't like anything more than simple APIs, and CGI is one of
the simplest. This technology is very powerful, and like ISAPI, it can quickly
upgrade a Web site from something pretty average to something pretty
spectacular.
A Simple CGI Example
CGI is so simple that I almost don't need to include the most basic of
possible examples. However, enough things can go wrong with Web-based
applications that you need one simple example that you know will work. The code
in Listing 26.7 provides that example.
Listing 26.7. The SimpleCGI application.
///////////////////////////////////////
// SimpleHelp.cpp
// Using an object to make CGI easier
// Copyright (3) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
char S[] =
"TO HIS COY MISTRESS<P>\n"
"Had we but world enough and time,<BR>\n"
"This coyness, lady, were no crime.<BR>\n"
"We would sit down, and think which way<BR>\n"
"To walk, and pass our long love's day.<BR>\n"
"Thou by the Indian Ganges' side<BR>\n"
"Shouldst rubies find: I by the tide<BR>\n"
"Of Humber would complain. I would<BR>\n"
"Love you ten years before the Flood:<BR>\n"
"And you should if you please refuse<BR>\n"
"Till the conversion of the Jews.<BR>\n"
"My vegetable love should grow<BR>\n"
"Vaster than empires, and more slow.<BR>\n"
"An hundred years should go to praise<BR>\n"
"Thine eyes, and on thy forehead gaze.<BR>\n"
"Two hundred to adore each breast:<BR>\n"
"But thirty thousand to the rest.<BR>\n"
"An age at least to every part,<BR>\n"
"And the last age should show your heart.<BR>\n"
"For lady, you deserve this state;<BR>\n"
"Nor would I love at lower rate.<BR>\n"
"But at my back I always hear<BR>\n"
"Time's winged chariot hurrying near:<BR>\n"
"And yonder all before us lie<BR>\n"
"Deserts of vast eternity.<BR>\n"
"Thy beauty shall no more be found,<BR>\n"
"Nor, in thy marble vault, shall sound<BR>\n"
"My echoing song; then worms shall try<BR>\n"
"That long preserved virginity:<BR>\n"
"And your quaint honour turn to dust;<BR>\n"
"And into ashes all my lust.<BR>\n"
"The grave's a fine and private place,<BR>\n"
"But none, I think , do there embrace.<BR>\n"
"Now therefore, while the youthful hue<BR>\n"
"Sits on thy skin like morning dew,<BR>\n"
"And while thy willing soul transpires<BR>\n"
"At every pore with instant fires,<BR>\n"
"Now let us sport us while we may;<BR>\n"
"And now, like am'rous birds of prey,<BR>\n"
"Rather at once our time devour,<BR>\n"
"Than languish in his slow chapped power<BR>\n"
"Let us roll all our strength, and all<BR>\n"
"Our sweetness, up into one ball:<BR>\n"
"And tear our pleasures with rough strife,<BR>\n"
"Through the iron gates of life.<BR>\n"
"Thus, though we cannot make our sun<BR>\n"
"Stand still, yet we will make him run.<BR>\n"
"-- Andrew Marvell";
void main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("\nContent-type: text/html\n\n");
printf("<html>\n");
printf("<body>\n");
printf("<body bgcolor=\"\#0000FF\" text=\"\#00FFFF\" link=\"\#FFFF00\""
"vlink=\"\#00FF00\" alink=\"\#FFFFFF\">");
printf(S);
printf("</body>\n");
printf("</html>\n");
}
This console application has no main form; its output appears only in a
browser, as shown in Figure 26.5.
Figure 26.5
The Simple CGI application.
To create the SimpleCGI application, start a new project and remove
the main form. Save the project file in its own directory, and then choose
Options | Project | Linker and set the Application Type to Console Application.
You can also use File | New and select Console App from the App Expert dialog.
Remove all the WinMain business, and replace it with a simple
DOS-style main block:
void main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("\nContent-type: text/html\n\n");
printf("<html>\n");
printf("<body>\n");
printf("...the sessions of sweet silent thought...");
printf("</body>\n");
printf("</html>\n");
}
This complete CGI application needs only be compiled and then copied to a
server.
You call a CGI application exactly as you would an ISAPI DLL:
<p><a href="/scripts/books/bunleash/SimpleCgi.exe">SimpleCgi.exe</a>
Two complete HTML files for use with this chapter are available in the root
of the Chap26 directory on the CD that accompanies this book. You
might, of course, have to edit the paths shown in these files.
Retrieving the Variables Passed to a CGI Application
The variables passed to a CGI application can be retrieved from the DOS
environment. You see these same types of variables when you type Set at
the DOS prompt. You can use the getenv or _environ function to
retrieve this data.
-
NOTE: Ghastly function names
like getenv and the deplorable _environ are the result of a
bygone time when compilers had a limited ability to work with long
identifiers. Furthermore, there was a need to try to save every byte in the
applications being produced.
Despite the ugly function names, those were romantic and exciting times. It
was a period when all PC-based computer technology was new, and the industry
was experiencing dynamic growth.
Despite the romance of that era, our much more complex contemporary
programming environments require more careful habits. In modern programming,
these functions would have easier-to-understand names like GetEnvironment
and Environment.
I sometimes catch myself wondering how many wasted hours have been lost, on a
global scale, as a result of the horrible naming conventions and ludicrous
capitalization standards that used to exist. On the other hand, I also can't
help but look back fondly, on the smaller, more manageable APIs that existed
in those times, and on the highly charged, and much more friendly, atmosphere
that prevailed.
The CGIVars application found on the CD that accompanies this book shows how
to iterate through all the environment variables passed to a CGI application.
The output from that program is shown in Figure 26.6, and the source for the
program is shown in Listing 26.8 through Listing 26.10.
FIGURE 26.6.
The output from the CGIVars application.
Listing 26.8. The CGIVars application shows how to
retrieve all the variables passed to a CGI application.
///////////////////////////////////////
// CGIVars.cpp
// Using an object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "CGIHelp1.h"
USEUNIT("..\..\Utils\CGIHelp1.cpp");
void main(void)
{
TCGIHelp CGI;
CGI.Header("World Enough and Time");
CGI.ShowEnv();
}
Listing 26.9. The TCGIHelp object provides support
for CGI application. This is the header for the module.
///////////////////////////////////////
// CGIHelp1.h
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef CGIHelp1H
#define CGIHelp1H
enum TColorType {ctLightBlue, ctRedYellow, ctBlueYellow, ctGreenBlack};
class TCGIHelp
{
public:
TCGIHelp();
~TCGIHelp();
void HTMLHeader();
void Header(AnsiString S);
AnsiString GetQuery();
void Colors(TColorType ColorType);
void ShowEnv();
};
#endif
Listing 26.10. The main source file for the TCGIHelp
utility.
///////////////////////////////////////
// CGIHelp1.cpp
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "CGIHelp1.h"
TCGIHelp::TCGIHelp()
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("\nContent-type: text/html\n\n");
printf("<html>\n");
printf("<body>\n");
}
TCGIHelp::~TCGIHelp()
{
printf("</body>\n");
printf("</html>\n");
}
void TCGIHelp::Header(AnsiString S)
{
AnsiString Temp = "<H1>" + S + AnsiString("</H1>");
printf(Temp.c_str());
}
AnsiString TCGIHelp::GetQuery()
{
char *Query;
Query = getenv("QUERY_STRING");
if (Query)
return Query;
else
return "";
}
void TCGIHelp::Colors(TColorType ColorType)
{
switch(ColorType)
{
case ctLightBlue:
printf("<body bgcolor=\"\#0000FF\""
"text=\"\#00FFFF\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
break;
case ctRedYellow:
printf("<body bgcolor=\"\#FF0000\""
"text=\"\#FFFF\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
break;
case ctBlueYellow:
printf("<body bgcolor=\"\#0000FF\""
"text=\"\#FFFF\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
break;
case ctGreenBlack:
printf("<body bgcolor=\"\#00FF00\""
"text=\"\#000000\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
break;
}
}
void TCGIHelp::ShowEnv()
{
int i =0;
while (_environ[i])
printf("%s<BR>", _environ[i++]);
}
The main body of this console application is simple:
void main(void)
{
TCGIHelp CGI;
CGI.Header("Current Environment");
CGI.ShowEnv();
}
The code is brief because I'm using a small helper object that gets many of
the worst chores out of the way for me:
class TCGIHelp
{
public:
TCGIHelp();
~TCGIHelp();
void HTMLHeader();
void Header(AnsiString S);
AnsiString GetQuery();
void Colors(TColorType ColorType);
void ShowEnv();
};
This object performs the basic chores needed to run a CGI application. It is
also the kind of simple object that can be improved and enlarged as you see fit.
In other words, you should feel free to add methods to this object as you find a
need for them.
The constructor for TCGIHelp takes care of sending off a MIME header
that browsers examine so they can know what to do with the HTTP code that is
being streamed toward them:
TCGIHelp::TCGIHelp()
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("\nContent-type: text/html\n\n");
printf("<html>\n");
printf("<body>\n");
}
The equally simple destructor outputs the lines that mark the end of an HTML
form:
TCGIHelp::~TCGIHelp()
{
printf("</body>\n");
printf("</html>\n");
}
Other functions perform mundane tasks such as automatically formatting a
header:
void TCGIHelp::Header(AnsiString S)
{
AnsiString Temp = "<H1>" + S + AnsiString("</H1>");
printf(Temp.c_str());
}
If you look at the source, you will see a method called colors that
sets the background and foreground colors for a form. A simple enumerated type
can be passed to the function so that you can set up the colors you need:
enum TColorType {ctLightBlue, ctRedYellow, ctBlueYellow, ctGreenBlack};
You can create more colors if you like. Each color statement sets the
background color, the text color, and the link colors for the times before,
during, and after they have been selected:
printf("<body bgcolor=\"\#0000FF\""
"text=\"\#00FFFF\""
"link=\"\#FFFF00\""
"vlink=\"\#00FF00\""
"alink=\"\#FFFFFF\">");
The numbers representing the colors work just like the standard Windows RGB
colors:
#0000FF: Blue
#00FF00: Green
#FF0000: Red
You can find more details in Chapter 7, "Graphics."
The method that actually iterates through the environment is extremely
simple:
void TCGIHelp::ShowEnv()
{
int i =0;
while (_environ[i])
printf("%s<BR>", _environ[i++]);
}
Not much I can say about that one. It's just simple C 101 code from your
first programming course.
If you want to retrieve an individual item from the environment, you can use
this method:
AnsiString TCGIHelp::GetQuery()
{
char *Query;
Query = getenv("QUERY_STRING");
if (Query)
return Query;
else
return "";
}
The main purpose of this code is to return a friendly AnsiString
rather than force the user to gamble on using a NULL-terminated string.
This place would, of course, be a particularly bad one to try to shave a few
clock cycles off an application by using NULL-terminated strings rather
than AnsiStrings. The reasons for this are twofold:
- 1. If you do make a mistake using a NULL-terminated
string, you will raise an exception, which will stop your application dead in
the water because it is running on another machine. Often the only way to
recover is to reboot the server. Later in the chapter, I will discuss how to
treat exceptions raised in CGI applications.
2. CGI applications are slow by their very nature because they run over
the network. As a result, nobody is going to notice if you shave a few
nanoseconds here and a few nanoseconds there. As a result, you should use safe
techniques for handling strings and think about other techniques for improving
performance.
Given these considerations, I chose to return AnsiStrings from this
method.
If no environment variable called QUERY_STRING exists, then the
GetQuery method returns an empty AnsiString; otherwise, it returns
the string retrieved from the system. getenv returns NULL if
it cannot find the environment string you request. The server is the one that
sets up the environment that will prevail inside the session where your
application is launched.
-
NOTE: This section is not the
place for me to get into the whole subject of creating CGI applications that
are as small and as efficient as possible. You should be aware, however, that
considerable thought has gone into sensible ways of creating applications that
will stay in memory so that they do not have to be launched each time they are
called. You can find books on this subject, or you can turn to third parties
such as HREF:
www.href.com.
Creating Simple CGI Database Applications
You can easily create a BCB-based CGI application that handles databases. All
you have to do is allocate memory for a table, open it, and wrap its contents
inside HTML statements. An example is shown Figure 26.7.
FIGURE 26.7.
The output for the CGIData application.
On the CD that accompanies this book, I include two applications that show
the basics of working with the TTable object inside a CGI application.
I also include one slightly more complex example that shows how to work with
TQuery, TDatamodule, and InterBase from inside a CGI
application.
-
NOTE: For the code shown in this
section, I assume that you have the BDE set up on your server and that you or
the installation program has created a BCDEMOS alias for the BDE.
Later, I will show an application running against InterBase tables. In that
case, you will need to have both InterBase and the BDE running on your server.
You could run the local InterBase server in this case because you can, if you
want, query InterBase from the same machine on which the server resides. In
other words, you can run both the local InterBase and your Web server on the
same machine.
If you're using InterBase, Oracle, or other SQL servers, you will find a limit
to the number of simultaneous connections you can support, depending on the
nature of the license for your database server. You can still have multiple
connections to your Web server, but you might be able to service only a
limited number of requests for data at any one time.
The source for the first of the CGI-based database applications is shown in
Listing 26.11. This program depends on the TCGIHelp object shown in
Listing 26.9.
Listing 26.11. The source for the simplest possible
case example of using a database code in a CGI application.
///////////////////////////////////////
// IsapiVars.cpp
// Mirror back the information sent to an ISAPI DLL by the server
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "..\..\utils\Httpext.h"
USERES("IsapiVars.res");
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
strcpy(pVer->lpszExtensionDesc, "ISAPI Variables DLL");
return (TRUE);
};
#define SIZE 2048
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
char ResultString[SIZE * 2];
char HtmlInfo[SIZE];
char Buffer[SIZE];
DWORD StrSize;
DWORD resultLen;
char *IsapiLogText = "ISAPIVars from C++ Builder";
strcpy(pECB->lpszLogData, IsapiLogText);
pECB->dwHttpStatusCode = 200;
sprintf(HtmlInfo,
"<HTML><TITLE>Fields of EXTENSION_CONTROL_BLOCK</TITLE>"
"<H1>Test server results</H1><BODY>"
"Size = %d<BR>"
"Version = %.8x<BR>"
"ConnID = %.8x<BR>"
"Method = %s<BR>"
"Query = %s<BR>"
"PathInfo = %s<BR>"
"PathTranslated = %s<BR>"
"TotalBytes = %d<BR>"
"AvailableBytes = %d<BR>"
"ContentType = %s<BR><BR>"
"<H1>Calls to GetServerVariable</H1>",
pECB->cbSize, pECB->dwVersion, pECB->ConnID,
pECB->lpszMethod, pECB->lpszQueryString,
pECB->lpszPathInfo, pECB->lpszPathTranslated,
pECB->cbTotalBytes, pECB->cbAvailable,
pECB->lpszContentType);
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &Buffer, &StrSize);
AnsiString VarString("REMOTE_ADDR = " + AnsiString(Buffer) + "<BR>");
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_HOST", &Buffer, &StrSize);
VarString += "REMOTE_HOST = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "REMOTE_USER", &Buffer, &StrSize);
VarString += "REMOTE_USER = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_NAME", &Buffer, &StrSize);
VarString += "SERVER_NAME = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_PORT", &Buffer, &StrSize);
VarString += "SERVER_PORT = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_PROTOCOL", &Buffer, &StrSize);
VarString += "SERVER_PROTOCOL = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "SERVER_SOFTWARE", &Buffer, &StrSize);
VarString += "SERVER_SOFTWARE = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "HTTP_ACCEPT", &Buffer, &StrSize);
VarString += "HTTP_ACCEPT = " + AnsiString(Buffer) + "<BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "URL", &Buffer, &StrSize);
VarString += "URL = " + AnsiString(Buffer) + "<BR><BR><BR>";
StrSize = sizeof(Buffer);
pECB->GetServerVariable(pECB->ConnID, "ALL_HTTP", &Buffer, &StrSize);
VarString += "ALL_HTTP = " + AnsiString(Buffer) + "<BR>";
strcat(HtmlInfo, VarString.c_str());
sprintf(ResultString,
"HTTP/1.0 200 OK\nContent-Type: text/html\n"
"Content-Length: %d\nContent:\n\n %s </HTML>",
SIZE, HtmlInfo);
StrSize = strlen(ResultString);
pECB->WriteClient(pECB->ConnID, ResultString, &StrSize, 0);
return (HSE_STATUS_SUCCESS);
}
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
{
return 1;
}
Because you're working in an application, as opposed to a DLL, the
TApplication object is available to you. This object helps set up the
TSessions object that is required by database applications. As a result,
the conservative and safe thing to do is to make sure that the
Application->Initialize method is called so that the database tools are set
up properly:
Application->Initialize();
-
NOTE: This step makes ISAPI
database applications a bit tricky. ISAPI runs in a DLL, and you have to be
careful setting up database code in a DLL. In particular, you have to be sure
the Sessions object is properly initialized and that each table is
assigned a Session.
After setting up the Application and Sessions objects, you
are free to open up a table. This application has no data module and no form, so
you must do all the work yourself:
Table = new TTable(Application);
Table->DatabaseName = "BCDEMOS";
Table->TableName = "Customer";
Table->Open();
This code allocates memory for the table, specifies the database and table
the application wants to use, and finally opens the table.
After the table is open, you can start retrieving data immediately. However,
you might want to put off this step just long enough to set up an HTML table:
printf("<TABLE BORDER>\n");
printf("<TH>%s</TH><TH>%s</TH>",
Table->Fields[0]->FieldName.c_str(),
Table->Fields[1]->FieldName.c_str());
This code calls the FieldName property of the TField object
to retrieve the name of each column in the database. This information is
displayed in the top row of the HTML table. As you can see, this example works
with only the first two columns. The CGIData2 program, shown in Listing 26.12,
provides a generic solution for converting any table to HTML.
The following code iterates through the database, displaying all the data
from the first two fields to the user:
while (!Table->Eof)
{
printf("<TR><TD>%s</TD><TD>%s</TD></TR>\n",
Table->Fields[0]->AsString.c_str(),
Table->Fields[1]->AsString.c_str());
Table->Next();
}
After the data is shown, I close the HTML table, close the TTable
object, and then delete the TTable object:
printf("</TABLE>\n");
Table->Close();
delete Table;
CGI database programming is obviously simple. You can, however, make the
process even easier. I will demonstrate these techniques in the next section.
Converting a Table to HTML
The CGIData2 application shows a technique for converting any arbitrary table
to HTML. The program also shows how to pass information to a CGI application so
that the user can choose the table he or she wants to display. The combination
of these two traits gives the user the ability to browse any table on your
system over the Internet.
The output from the CGIData2 program is shown Figure 26.8, and the source for
the program is shown in Listings 26.12 through 26.14. Note the URL passed to the
application in Figure 26.8. The information after the question mark specifies
which table the user wants to browse.
FIGURE 26.8.
The output from the CGIData2 application while looking at the Parts table from
BCDEMOS.
Listing 26.12. The source for the main module in the
CGIData2 application.
///////////////////////////////////////
// CGIData2.cpp
// Databases and CGI
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "cgiDBhelp1.h"
USEUNIT("..\..\Utils\CGIHelp1.cpp");
USEUNIT("..\..\Utils\CGIDBHelp1.cpp");
void main(void)
{
TCGIDBHelp CGI;
CGI.Colors(ctGreenBlack);
char *Query;
TTable *Table;
Query = getenv("QUERY_STRING");
if (Query)
{
Application->Initialize();
Table = new TTable(Application);
Table->DatabaseName = "BCDEMOS";
Table->TableName = Query;
try
{
Table->Open();
CGI.Header(Table->TableName);
CGI.PrintTable(Table);
Table->Close();
delete Table;
}
catch(Exception &E)
{
printf("Could not open table: %s", E.Message.c_str());
}
catch(...)
{
printf("Could not open table");
}
}
else
{
printf("<H1>No query string available</H1>\n");
}
Application->Run();
}
Listing 26.13. The header for the TCGIDBHelp file.
///////////////////////////////////////
// CGIDBHelp1.h
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef CGIDBHelp1H
#define CGIDBHelp1H
#include "CGIHelp1.h"
class TCGIDBHelp: public TCGIHelp
{
public:
void PrintTable(TDataSet *Table);
};
#endif
Listing 26.14. The TCGIDBHelp object provides
support for CGI database applications.
///////////////////////////////////////
// CGIDBHelp1.cpp
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "CGIDBhelp1.h"
void TCGIDBHelp::PrintTable(TDataSet *Table)
{
int i;
printf("<TABLE BORDER>\n");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str());
}
while (!Table->Eof)
{
printf("<TR>");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str());
}
printf("</TR>\n");
Table->Next();
}
printf("</TABLE>\n");
}
This application is designed to allow the user to interact with it
dynamically. In particular, this application retrieves the name of the table the
user wants to see from the environment variable called QUERY_STRING:
Query = getenv("QUERY_STRING");
if (Query)
{
... // Code omitted here
Table->TableName = Query;
}
else
{
printf("<H1>No query string available</H1>\n");
}
These strings are passed to the application if you append a question mark to
the URL that calls the CGI script and then append data after the question mark.
For example, the following HTML code would call this script without specifying
any table name:
<a href="/scripts/books/bunleash/CgiData2.exe">CGIData2.exe</a>
You might have to change the path specified in this code, but it shows the
general formula for the code you should write. If you click the reference listed
here, the program would report an error, as shown in Figure 26.9.
FIGURE 26.9.
The CGIData2 program returns an error like this if the user does not specify the
name of a table he or she wants to view.
If you want to ask about the Customer database, you can edit the string so
that it looks like this:
/scripts/books/bunleash/CgiData2.exe?Customer">CGIData2.exe
This request retrieves the Customer table from the database. You do
not, of course, have to hard-code the name of each table you want to query into
your form. In fact, in this particular example, you don't even have to use an
HTML form at all. If you want, you can merely edit the raw HTML string in the
Address field of your browser, appending different files after the
question mark to retrieve different sets of data. For example, you could ask for
the Customer, Orders, Items, Events,
Parts, or BioLife tables.
If CGIData2 finds a valid query string in the global environment, it tries to
open a table. If it can't find a string, then it prints a message to the user
explaining the problem:
printf("<H1>No query string available</H1>\n");
Figure 26.10 shows the message that the CGIData2 program returns to the
browser when the user enters an invalid table name.
FIGURE 26.10.
The output from the CGIData2 program when the user asks for a nonexistent table.
As you can see, the big danger here is that the user will pass in an invalid
string. If this happens, an exception will be raised, which can hang your
server, at least until you use a Task Manager to shut down the service.
-
NOTE: One way to ease the chore
of debugging CGI applications is to run against a server on your own system.
For example, if you are on Windows 95 and you use the Personal Web Server,
then exceptions raised by your application appear in message boxes that you
can read directly on your screen. You can then just click the OK button on the
message so that the application will terminate. This option is not available
if you're running against an NT server that resides on a separate machine. In
fact, exceptions never appear on the screen on most servers, and your
application will hang, with no way for you to click the OK button.
I'm sure most readers can guess that try..catch blocks are the way
to handle exceptions that occur on a server. In particular, the following code
catches exceptions that occur when you try to open a table or during the process
of iterating through the records of a table:
try
{
Table->Open();
CGI.Header(Table->TableName);
CGI.PrintTable(Table);
Table->Close();
delete Table;
}
catch(Exception &E)
{
printf("Could not open table: %s", E.Message.c_str());
}
catch(...)
{
printf("Could not open table");
}
}
In this case, I am assuming that all errors will be caused because the user
passed in an invalid string. However, the code will trap any VCL error and
return a string specifying what went wrong. On the off chance that the exception
is not raised by the VCL, I have a generic catch block that simply
states that something went wrong and that the table could not be opened. For
more information, see Chapter 5, "Exceptions."
Notice that I also call Application->Run(). Calling this method
appears to be necessary if you want to handle exceptions properly in a database
application.
The last chunk of code to look at from this application is the bit that
converts a table to HTML:
void TCGIDBHelp::PrintTable(TDataSet *Table)
{
int i;
printf("<TABLE BORDER>\n");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str());
}
while (!Table->Eof)
{
printf("<TR>");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str());
}
printf("</TR>\n");
Table->Next();
}
printf("</TABLE>\n");
}
This code is part of an object designed to work with database CGI
applications. I separate it from the TCGIHelp object because database
code adds so much to the size of a file. My theory is that you should include
this file in your project only if you're explicitly using database objects;
otherwise, stick with the smaller TCGIHelp file. TCGIDBHelp is
a direct descendant of TCGIHelp, so it inherits all of methods from
TCGIHelp.
The code shown here starts by iterating through all the fields in the
database and places their names in the header for the table:
printf("<TABLE BORDER>\n");
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TH>%s</TH>", Table->Fields[i]->FieldName.c_str());
}
With this task out of the way, the code then iterates through the entire
table, filling out each row in its entirety:
for (i = 0; i < Table->FieldCount; i++)
{
printf("<TD>%s</TD>", Table->Fields[i]->AsString.c_str());
}
That's all I'm going to say about the CGIData2 program. Once again, you can
see that publishing data over the Web is an easy operation. If you own BCB and
run on an NT Server, you can publish data all day long with very little effort.
If you have a smaller clientele, you can do the same thing with Windows 95 and
the Personal Web Server, or else you can turn to more powerful tools such as the
WebSite Server provided by O'Reilly.
O'Reilly is located at
www.ora.com. Try
http://www.ora.com/catalog/webpro/ for the professional version of
the product and
http://www.ora.com/catalog/web1.1/ for the inexpensive standard
edition.
Using Queries, Data Modules, and InterBase over the
Web
The real power of CGI applications is made clear when you start running SQL
queries against data. For example, I took the Music.gdb database from
Chapter 16, "Advanced InterBase Concepts," and let the user run some queries
against it, as shown in Figure 26.11 and Figure 26.12. In particular, this
application calls some stored procedures designed to quickly pump out data that
answers complex questions about a database. The stored procedures in question
were discussed in Chapter 16. Remember that this is an InterBase database, and
that you must set up the alias for it as described in the readme file on the CD
that accompanies this book. I have hardcoded the password "masterkey" into the
Params property of the TDatabase object in the program's data
module.
FIGURE 26.11.
Running a query against the Music database to see albums rated as
PEACEFUL.
FIGURE 26.12. Running a query against the
Music database to see albums with very high ratings.
The source for the CGIQuery program is shown in Listing 26.15. A special HTML
page is needed to query this application. A screen shot of the page is shown in
Figure 26.13, and the HTML source for the page is shown in Listing 26.16.
FIGURE 26.13.
The HTML page used to query the CGIQuery application.
Listing 26.15. The CGIQuery application lets the
user ask a number of questions of the database.
///////////////////////////////////////
// CGIQuery.cpp
// An object to make CGI easier
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "DMod1.h"
#include "CGIDBHelp1.h"
USERES("CGIQuery.res");
USEDATAMODULE("DMod1.cpp", DMod);
USEUNIT("\SrcC\PUnleash\Utils\CGIDBHelp1.cpp");
USEUNIT("\SrcC\PUnleash\Utils\CGIHelp1.cpp");
AnsiString ParseQuery(AnsiString S)
{
int i = S.Pos("&");
S.SetLength(i - 1);
S = strrev(S.c_str());
i = S.Pos("=");
S.SetLength(i - 1);
S = strrev(S.c_str());
return S.c_str();
}
AnsiString ParseQuery1(AnsiString S)
{
S = strrev(S.c_str());
int i = S.Pos("=");
S.SetLength(i - 1);
return S.c_str();
}
///////////////////////////////////////
// If user pressed a button on the HTML
// form, them must parse input.
// String will look like this:
// Query=6&Query1=7
///////////////////////////////////////
AnsiString GetButtonQuery(AnsiString S)
{
AnsiString Query = ParseQuery(S);
AnsiString Query1= ParseQuery1(S);
try
{
return Format("select * from RatingRange(%s, %s)",
OPENARRAY(TVarRec, (Query, Query1)));
}
catch(...)
{
return "You entered invalid data";
}
}
bool GetLetters(AnsiString(S))
{
int i;
FILE * out;
out = fopen("c:\\sam.txt", "w+");
for (i = 0; i < S.Length(); i++)
{
fprintf(out, "Letters: %c\n", S[i]);
}
fclose(out);
return True;
}
void RunQueries()
{
int i;
TCGIDBHelp CGI;
AnsiString S = CGI.GetQuery();
if (S.Length() == 0)
S = "0";
if (S[1] != `Q') // Did user press button? If so first letter is `Q'
{
if (S != "")
i = S.ToInt();
else
i = 0;
switch(i)
{
case 0: S = "select * from Album"; break;
case 1: S = "select * from NineOrBetter"; break;
case 2: S = "select * from GetLoudness(1)"; break;
case 3: S = "select * from GetLoudness(2)"; break;
case 4: S = "select * from GetLoudness(3)"; break;
default:
S = "select * from Artist";
}
}
else
S = GetButtonQuery(S);
DMod->Query1->SQL->Add(S);
try
{
DMod->Query1->Open();
CGI.Header(DMod->Query1->SQL->Strings[0]);
CGI.PrintTable(DMod->Query1);
DMod->Query1->Close();
}
catch(...)
{
printf("Invalid Query: %s", S.c_str());
}
}
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TDMod), &DMod);
RunQueries();
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
Listing 26.16. The HTML file used to query the
CGIQuery application.
<HTML>
<TITLE>CGI QueryPage</TITLE>
<HEAD>
<H1>CGI QueryPage</H1>
</HEAD>
<body bgcolor="#0000FF" text="#00FFFF" link="#FFFF00"
vlink="#00FF00" alink="#FFFFFF">
<BODY>
<UL>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe">CGIQuery.exe</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?1">
Albums rated nine or better</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?2">Peaceful Albums</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?3">Moderate Albums</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?4">Loud Albums</a>
</UL>
<HR>
<FORM METHOD="GET" ACTION="/scripts/books/bunleash/CGIQuery.exe">
Range Start<BR>
<INPUT TYPE = "text" NAME="Query" SIZE=10 defaultvalue="1"><BR><BR>
Range End<BR>
<INPUT TYPE = "text" NAME="Query1" SIZE=10 defaultvalue="2"><BR><BR>
<INPUT TYPE = "submit" VALUE="Submit">
<HR>
</BODY>
</HTML>
The most interesting thing about the CGIQuery application is that it
uses a TDataModule, as shown in Figure 26.14. The data module allows
you to access all the visual database tools that can make BCB programming so
simple. For example, you can drop down a TDataBase object, connect to a
database, set up your password in the Params property, and open up the
database. Performing these tasks visually is much easier than doing them in
code. You can also set up a one-to-many relationship or perform filters and
lookups.
FIGURE 26.14.
The data module for the CGIQuery application.
Using a data module in a CGI application allows you to leverage the power of the
BCB RAD environment.
When you're using a data module, giving up altogether on the idea of using a
Console application is probably simplest. Instead, keep the standard
WinMain block set up by BCB:
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
Application->CreateForm(__classid(TDMod), &DMod);
RunQueries();
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}
This code calls Application::Initialize, CreateForm, and
Run. As you can see, I do not include a standard TForm object
in the application, which means that it has no visual interface. It just runs
silently in the background, without ever showing its face to the user. Once
again, notice that I call Run, which appears to be necessary when you
include a data module and exception handling in your application.
I should perhaps add that during application development, I often include a
form so that I can test my queries at design time or sometimes even at runtime.
The form usually need not contain anything but a TDBGrid in which to
display the results of the query. I include a TDataSource on the data
module for use with a form during program testing. It will be deleted during the
final phases of project development. Furthermore, you can simply remove the
entire test form from your project when it's time to distribute your
application.
The key point to emphasize here is that you can leverage the visual tools
during CGI development, even if you end up stripping them out of the application
when you ship. This capability helps show why BCB is the best environment to use
even when you're not producing standard RAD applications.
-
NOTE: I once again want to
stress that you can design CGI applications in a number of ways. I mean for
the techniques I show here to be an introduction to the subject and to show
you how to get a lot of work done quickly. If you find that this subject
interests you, you should consider turning to third parties that have
developed custom VCL components to make this task simpler and more efficient.
Using a Browser to Ask Questions
To understand the CGI Query application, you should first look at the HTML
code that calls it:
<LI><a href="/scripts/books/bunleash/CgiQuery.exe">CGIQuery.exe</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?1">
Albums rated nine or better</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?2">Peaceful Albums</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?3">Moderate Albums</a>
<LI><a href="/scripts/books/bunleash/CgiQuery.exe?4">Loud Albums</a>
These options pass in various query strings, depending on the user's
interests. For example, if the query string "1" is passed in, the
following code gets called:
case 1: S = "select * from NineOrBetter"; break;
This line sets up a SQL statement that calls the stored procedure called
NineOrBetter. The code for the stored procedure looks like this:
begin
for
select Artist.Last, Album.Album, Album.Rating
from Album, Artist
where Album.GroupCode = Artist.Code and Album.Rating >= 9
Order By Album.Rating Desc
into :Last, :Album, :Rating
do
suspend;
end
As I said earlier, you can turn to Chapter 16 for an in-depth explanation of
this code. The key point is that it retrieves a list of albums rated 9
or better.
The code that runs the query looks like this:
DMod->Query1->SQL->Add(S);
try
{
DMod->Query1->Open();
CGI.Header(DMod->Query1->SQL->Strings[0]);
CGI.PrintTable(DMod->Query1);
DMod->Query1->Close();
}
catch(...)
{
printf("Invalid Query: %s", S.c_str());
}
The first line here sets up the query string. The TQuery object is
then run, and the data from it is processed by the TCGIDBHelp object,
as described in the examination of the CGIData2 application.
Besides calling the NineOrBetter stored procedure, the user's other
choices include selecting a query that retrieves data about the relative volume
of the albums in the database:
case 2: S = "select * from GetLoudness(1)"; break;
case 3: S = "select * from GetLoudness(2)"; break;
case 4: S = "select * from GetLoudness(3)"; break;
The GetLoudness stored procedure looks like this:
begin
for
select Artist.Last, Album.Album, Album.Rating, Loudness.Loudness
from Album, Artist, Loudness
where Album.GroupCode = Artist.Code and
Album.Loudness = :LoudnessValue and
Loudness.Code = :LoudnessValue
Order By Album.Album Desc
into :Last, :Album, :Rating, :LoudnessStr
do
suspend;
end
This SQL code depends on the values from the Loudness table. The
Loudness table contains three records, stating that Peaceful
records have a code of 1, Moderate records have a code of
2, and Loud records have a code of 3. The stored
procedure does a lookup into this table, retrieves the string specifying the
loudness of a particular album, and displays it to the user.
Now that you understand how the system works, you can see that calling
GetLoudness(1) retrieves a list of the Peaceful records,
GetLoudness(2) gets the records of Moderate volume, and so on. All
this information is then wrapped up in an HTML table and shown to the user, as
shown in Figure 26.11.
Submit Buttons and CGI Applications
The final point I want to make about this program is that it allows you to
run queries based on data typed in by the user, as shown Figure 26.14. In
particular, the user can type in a starting and ending range for the ratings of
albums. If he or she then clicks the Submit button, a query will be formulated
based on the user's input.
The standard HTML code that allows the user to submit the question looks like
this:
<HR>
<FORM METHOD="GET" ACTION="/scripts/books/bunleash/CGIQuery.exe">
Range Start<BR>
<INPUT TYPE = "text" NAME="Query" SIZE=10 defaultvalue="1"><BR><BR>
Range End<BR>
<INPUT TYPE = "text" NAME="Query1" SIZE=10 defaultvalue="2"><BR><BR>
<INPUT TYPE = "submit" VALUE="Submit">
<HR>
The FORM METHOD statement tells the browser what to do when the user
clicks the Submit button. In particular, it says that the CGIQuery
application should be called. The HTML then defines two edit controls and the
Submit button itself.
When the user clicks the button, a string like the following is automatically
generated and passed to the application after the question mark in the URL:
Query=6&Query1=7
This string specifies that the user wants to see records that have a rating
between 6 and 7.
I write some very ugly code that checks to see if the user is passing in a
string to query the ratings of a table:
AnsiString S = CGI.GetQuery();
if (S.Length() == 0)
S = "0";
if (S[1] != `Q') // Did user press button? If so first letter is `Q'
{
... // Handle queries against Loudness table, etc.
}
else
S = GetButtonQuery(S);
As you can see, I've actually commented my code in this case, because there
is no logical way for the reader to figure out why I am checking for the letter
Q. Once you understand the system, you can see that the check is made
because all but one type of query from the browser will consist of a simple
integer. That one exceptional query is the kind that asks for record albums and
CDs in a particular range, and I can identify that type of query because it
always begins with a Q. This is, as I readily confess, horrible code,
even by my standards. However, in this case I ask for your indulgence because of
the rather primitive nature of all CGI technology.
-
NOTE: When I say that CGI
technology is primitive, I am, of course, aware that network-based technology
in and of itself is quite remarkable. However, the interface between a browser
and an application is still very rudimentary, and will probably continue to be
so in the foreseeable future. Because of the shortcomings of this technology,
I take up DCOM in the next chapter. Of course, DCOM also has its limitations.
In fact, sometimes DCOM shines, and sometimes CGI and ISAPI shine. The key
point is knowing all the available technologies so that you can choose the
best ones for a particular situation.
That's all I'm going to say about the CGIQuery program. You might want to
examine parts of the application on your own. For example, you should take a
look at the code that handles exceptions and make sure you understand what I'm
doing when I parse the query string received from the browser.
I should perhaps add that various available technologies can relieve you of
the task of parsing query strings and of retrieving strings from the
environment. Much of the time, these tasks are so simple that you don't need
complex code to get the job done. However, if you need to create really big,
complex CGI applications, then you should get on the Web and see whether you can
examine your available options for offloading some of these chores.
Summary
In this chapter, you learned about ISAPI and CGI. These technologies allow
you to enhance a server with custom C++ code. In particular, they both allow you
to access databases over the World Wide Web.
You saw that ISAPI is a DLL-based technology that runs inside the address
space of the server. As a result, it is very highly optimized from a performance
standpoint. CGI technology, on the other hand, is based on executables rather
than DLLs. CGI applications, therefore, are usually slower than ISAPI
applications. They are, on the other hand, much simpler to write, or more
specifically, simpler to debug.
The key point to grasp about these technologies is that they can be used to
make Web sites interactive. Though its performance is a bit slow, the Web can be
a very powerful means of reaching a large audience that can extend across the
entire globe. Many of the most exciting Web sites are powered by CGI or ISAPI,
and BCB enables you to easily participate in this process.
|