Previous Page TOC Next Page



Chapter 14


ISAPI Server Applications


by Wayne Berry

HTTP Server Extensions and HTTP Server Filters are Internet Server Applications (ISA). HTTP Server Extensions are CGI scripts on steroids. They execute faster, are easier to debug, and are easier to interface then CGI scripts. One of the reasons for their faster speed is that they remain cached in memory. This means that they do not have to load the Server Extension for every request, as do CGI scripts. Another reason for their speed is that all requests run within the same process; whereas every request of a CGI creates a separate process. Server Extensions are easier to debug because they are DLLs (Dynamic Link Libraries), and Microsoft Developer Studio handles DLL debugging better than the debugging of executables. CGI parameters passed to a Server Extension are packed into a structure, making them easier to access.

How HTTP Server Extensions Work


When a request is made to the server to execute an HTTP Server Extension, the request is passed on to the Server Extension. Before the server passes the request to the extension, it creates a thread, and each request has a separate thread. All HTTP Server Extensions have two common procedures that the HTTP Server calls: One is the version of the HTTP Server Extension; the other is the entrance for the thread. The thread enters the Server Extension by way of the HttpExtensionProc() procedure. The Server retrieves the version number of the HTTP Server Extension from GetExtensionVersion(). Dynamic link libraries built with these two APIs are ISAPI compliant.

Warning

If your server is expected to have a high volume of activity, creating a thread for every request will bog down your machine. In later versions of the IIS, you will be able to make a registry setting to limit the number of threads created. Until that time, it's recommended that you create a thread pool within the Server Extension to keep your machine from being thread bound.

Because there is a thread for each process, making threads execute quickly and return quickly is a must. Users do not want to wait on other users for their threads to get processed. When writing the Server Extension code, make sure that the threads are not waiting for other threads.

You also need to make sure that the code you write is thread safe. Static data that is shared between threads needs to be protected. Also, make sure that DLLs you're calling are thread safe. Currently, ODBC (Open Database Connectivity) is thread safe, but DAO (Data Access Object) is not.

Creating an HTTP Server Extension with Microsoft Developer Studio


Microsoft Developer Studio allows you to create HTTP Server Extensions by using a wizard. Follow these steps:

  1. 1. From the Menu bar, click File|New.
  2. 2. Select Project Workspace from the New List and select OK (see Figure 14.1).

Figure 14.1. Creating a ISAPI server extension using the New Project wizard.

  1. 4. From the Type list, go to the bottom and select ISAPI Extension Wizard.
  2. 5. In the Location edit box, change the location to the root of your scripts directory. The default location of the IIS is c:\inetsrv\scripts. This allows you to debug the Server Extension in the same directory that you compile it.
  3. 6. Type in the name of the first Listing, lst14_01, in the Name edit box. The wizard creates the entire listing for you.
  4. 7. Click Create, and you will see the dialog box in Figure 14.2.

Figure 14.2. The ISAPI Extension Wizard Dialog Box

  1. 8. Leave the defaults and click Finish.
Tip


Leaving MFC as a shared DLL will increase the loading speed of your DLL. It will also help shared resources if more then one HTTP Server Extension is being run at the same time. But you will need to remember to copy mfc40.dll and msvcrt40.dll to your production server when you copy your release build of HTTP Server Extensions.

  1. 9. Click OK to create the files.

Listing 14.1 shows what the Server Extension wizard created for the source file (lst14_1.cpp).

Listing 14.1. lst14_1.cpp.


// LST14_01.CPP - Implementation file for your Internet Server

//    lst14_01 Extension

#include <afx.h>

#include <afxwin.h>

#include <afxisapi.h>

#include "resource.h"

#include "lst14_01.h"

///////////////////////////////////////////////////////////////////////

// command-parsing map

BEGIN_PARSE_MAP(CLst14_01Extension, CHttpServer)

// TODO: insert your ON_PARSE_COMMAND() and 

// ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

// For example:

ON_PARSE_COMMAND(Default, CLst14_01Extension, ITS_EMPTY)

ON_PARSE_COMMAND(Hello, CLst14_01Extension, ITS_EMPTY)

ON_PARSE_COMMAND(GetName, CLst14_01Extension, ITS_PSTR)

ON_PARSE_COMMAND_PARAMS("Name")

DEFAULT_PARSE_COMMAND(Default, CLst14_01Extension)

END_PARSE_MAP(CLst14_01Extension)

///////////////////////////////////////////////////////////////////////

// The one and only CLst14_01Extension object

CLst14_01Extension theExtension;

///////////////////////////////////////////////////////////////////////

// CLst14_01Extension implementation

CLst14_01Extension::CLst14_01Extension()

{

TRACE("Constructor\n");

m_pnCounter = new UINT;

(*m_pnCounter) = 0;

}

CLst14_01Extension::~CLst14_01Extension()

{

TRACE("Destructor\n");

delete m_pnCounter;

}

BOOL CLst14_01Extension::GetExtensionVersion(HSE_VERSION_INFO* pVer)

{

// Call default implementation for initialization

CHttpServer::GetExtensionVersion(pVer);

// Load description string

TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];

ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),

IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));

_tcscpy(pVer->lpszExtensionDesc, sz);

return TRUE;

}

///////////////////////////////////////////////////////////////////////

// CLst14_01Extension command handlers

void CLst14_01Extension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("This default message was produced by the Internet");

*pCtxt << _T(" Server DLL Wizard. Edit your CLst14_01Extension::Default()");

*pCtxt << _T(" implementation to change it.\r\n");

EndContent(pCtxt);

}

void CLst14_01Extension::Hello(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("Hello World\r\n");

EndContent(pCtxt);

}

void CLst14_01Extension::GetName(CHttpServerContext* pCtxt, LPCTSTR pName)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("Name: ");

*pCtxt << pName;

*pCtxt << _T("<BR>\r\n");

EndContent(pCtxt);

}

///////////////////////////////////////////////////////////////////////

// If your extension will not use MFC, you'll need this code to make

// sure the extension objects can find the resource handle for the

// module.  If you convert your extension to not be dependent on MFC,

// remove the comments arounn the following AfxGetResourceHandle()

// and DllMain() functions, as well as the g_hInstance global.

/****

static HINSTANCE g_hInstance;

HINSTANCE AFXISAPI AfxGetResourceHandle()

{

return g_hInstance;

}

BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ulReason,

LPVOID lpReserved)

{

if (ulReason == DLL_PROCESS_ATTACH)

{

g_hInstance = hInst;

}

return TRUE;

}

****/

Here is the header file (lst14_01.h) that got created:


// LST14_01.CPP - Implementation file for your Internet Server

//    lst14_01 Extension

class CLst14_01Extension : public CHttpServer

{

public:

UINT *m_pnCounter;

CLst14_01Extension();

~CLst14_01Extension();

BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);

// TODO: Add handlers for your commands here.

// For example:

void Default(CHttpServerContext* pCtxt);

void Hello(CHttpServerContext* pCtxt);

void GetName(CHttpServerContext* pCtxt, LPCTSTR pName);

DECLARE_PARSE_MAP()

};

Now compile the debug version of the newly created HTTP Server Extension.

Debugging the HTTP Server Extension


The best way to debug the Server Extension is to run the server through Microsoft Developer Studio. To do this, you need to be on the same machine that the IIS is currently running on. Make sure that this server is being used only by you; no other Web pages should be served from it. It is advisable to run another copy of the Microsoft Developer Studio to make changes to and recompile your Server Extension. Use your browser on the same machine to call the Server Extension.

Correct Permissions


It's important to remember that the operating system believes that there are two users in the debugging scenarios. The first user, you, is debugging the IIS with the Microsoft Developer Studio. The second user, you, is coming in from the network using a browser and calling the ISAPI Server Extension. The first user has your login name and has to be an administrator. The second user is the IUSR_MACHINENAME where MACHINENAME is the name of your machine. The second user gets set up by the IIS when it installs. This name can be changed in the Internet service manager; IUSR_MACHINENAME is the default.

In the debugging scenario, the second user permissions are adequate. However, special permission is needed for the first user to run the IIS under Microsoft Developer Studio. If you are not in the Administrator group on this machine, find an administrator and have that administrator make you one. After you are an administrator, you need to give yourself a few more permissions. Follow these steps:

  1. 1. Make sure that you are logged on with administrator permissions.
  2. 2. Choose Administrative Tools|User Manager for Domains.
  3. 3. Make sure that User Manager is administrating the local machine and not the domain.
  4. 4. Select Policies|User Rights.
  5. 5. Click the Show Advanced User Rights checkbox in the bottom-right corner.
  6. 6. From the Grant Select Box on the right hand side, choose "Act as part of the operating system."
  7. 7. Click Add and add yourself.
  8. 8. Click OK.
  9. 9. From the Grant Select Box choose "Generate security audits."
  10. 10. Click Add and add yourself.
  11. 13. Click OK to exit Add Users and Groups Dialog.
  12. 14. Click OK to exit the User Rights Policy Dialog.
  13. 15. Exit User Manager.
  14. 16. Log off and log back on.

Disabling the IIS


In order to run the IIS on Microsoft Developer Studio, you will need to stop running the default IIS. You can do this by going into the Internet Service Manager and stopping the server. You might also want to disable the server from restarting if you reboot the machine. To do this, you will need to do the following:

  1. 1. Go into Control Panel.
  2. 2. Choose Services.
  3. 3. Choose World Wide Web Publishing Service from the Services List.
  4. 4. Click Startup.
  5. 5. In the Startup dialog box, choose Manual and click OK.
  6. 6. Click Close and exit the Control Panel.

Disabling Caching


As mentioned before, the IIS Server will cache the Server Extension upon the first request and keep it loaded in memory. It's important to disable caching when debugging the Server Extension. With the Server Extension loading and unloading for each request, Microsoft Developer Studio is able to detect some types of memory leaks.

Here is the way to modify the registry so that the IIS will not cache the Server Extensions.

  1. 1. Open the Registry editor, by executing regedit.exe.
  2. 2. Choose HKEY_LOCAL_MACHINE of the registry tree and expand the branch.
  3. 3. Then expand the branches leading to this registry key at the location, SYSTEM\CurrentControlSet\Services\W3SVC\Parameters.
  4. 4. There is a value within Parameters called CacheExtensions. Set this to 0.

Running the IIS in Microsoft Developer Studio


Now that you have adequate permissions and have disabled the default IIS and extension caching, you can run the IIS in Microsoft Developer Studio by following these steps:

  1. 1. Start another copy of Microsoft Developer Studio besides the one open for compiling the Server Extension.
  2. 2. From the menu bar, choose File|Open Workspace.
  3. 3. From the Open Workspace dialog's File of Type list, select Executable files (*.exe).
  4. 4. Open the IIS executable c:\inetsrv\server\inetinfo.exe.
  5. 5. From the menu bar of Microsoft Developer Studio, click Build|Settings.
  6. 6. Click the Debug tab.
  7. 7. Select General from the Category drop-down box.
  8. 8. In the Program argument's edit box, type -e W3Svc.
  9. 9. Select "Additional DLLs" from the Category drop-down box.
  10. 10. In the Modules box, choose the debug HTTP Server lst14_01.dll. It should be at c:\inetsrv\lst14_01\debug\lst14_01.dll.
  11. 11. Click OK and exit the Settings dialog.
  12. 12. From the Menu bar, choose File|Save All.
  13. 13. When asked Do you want to save project workspace information for C:\inetsrv\server\inetinfo.mdp, choose Yes. This way, whenever you want to run the IIS Server Extension in the Microsoft Developer Studio, you can just open C:\inetsrv\server\inetinfo.mdp instead of performing all the previous steps.
  14. 14. From the Menu bar, choose Build.
  15. 15. Click on Execute inetinfo.exe.
  16. 16. Every time you execute inetinfo.exe, you will be asked 'C:\inetsrv\server\inetinfo.exe' does not contain debugging information. Do you want to continue? Choose Yes.

Now IIS is running in the debugger of IIS. You can debug the server extension like any other DLL.

Running the Example Created by Extension Wizard


With the IIS running in the debugger, open your browser and type in the address of the Server Extension DLL. The example you just created should be at the URL


http://MYMACHINE/scripts/lst14_01/debug/lst14_01.dll

where MYMACHINE is the name of your machine. The page returned from the Server Extension should look like Figure 14.3.

Figure 14.3. Listing 14.1 Extension DLL displayed in the browser.

The Default Memory Leak


In the default set up by the Extension wizard, there is a bug that I want to expose right away.

After executing the Server Extension, look at the output window of the debugger running the IIS. Notice that there is a line that reads


Detected Memory leaks!

The reason for the memory leak is that the destructor for CHttpServer class is not being called.

This is a bug in the Microsoft Foundation Classes shipping with Microsoft Visual C++ 4.1 that will be fixed in Microsoft Visual C++ 4.2. To fix the bug in Microsoft Foundation Classes that ship with Microsoft Visual C++ 4.1, you must add this line in lst14_01.cpp:


CWinApp BugFix;

It needs to go right after the static declaration of the extension class


CLst14_01Extension theExtension;

This creates a CWinApp object, which you never use. The problem is fixed because the compiler links with a different library when a CWinApp object is present. The different library doesn't contain the bug.

Recompile the Server Extension. Every time you recompile the server extension, you will need to stop debugging the IIS. This is because the Server Extension symbols file is loaded into the Microsoft Developer Studio while the IIS is debugging. This is the reason that I recommend two open Microsoft Developer Studios. With two Microsoft Developer Studios, it's easier to switch between compiling and debugging.

Execute the IIS again under Microsoft Developer Studio. Notice that the memory leak is gone.

Two Ways of Calling Server Extensions


You can call the Server Extension in two ways. One way is to call the server extension by using the full path name as described in the preceding section. The only problem with this is that it isn't very clean. The user knows that you are running an IIS using an ISA. The second way involves assigning an extension to the ISA in the registry. The request to the server would use this extension to signify that it wants to run the Server Extension. Microsoft uses this technique with its Internet Database Connector. Microsoft in IIS assigns the extension .idc to a DLL named httpodbc.dll. Whenever a request is made with an extension of .idc, the Internet Database Connector (httpodbc.dll) is called. The Internet Database Connector is a good example of when you would want to assign extensions to ISA. The request to the Internet Database Connector looks like a request to a file; for example:


http://MYMACHINE/scripts/query.idc

The server doesn't load query.idc; instead, it sees the .idc extension and passes the request on to the Internet Database Connector. The Internet Database Connector in turn loads the file called query.idc and processes it in a special way. Here is how you assign an unique extension to your ISAPI Server Extension:

  1. 1. Open the Registry editor.
  2. 2. Choose HKEY_LOCAL_MACHINE.
  3. 3. Then choose the registry key Script Map at this location SYSTEM\CurrentControlSet\Services\W3SVC\Parameters\Script Map.
  4. 4. Create a new string value named after your extension and with a value that points to the path of your Server Extension DLL. Use the Internet Database Connector as an example.
  5. 5. Reboot the Machine.
Note


After you create a mapping to the extension, that extension will be called whether or not there is a file by that name. The DLL can handle each request for a file, with the unique extension, like a separate file, without actually loading the file from the disk. This creates the impression that the user is skipping from one file to another when there really aren't any files at all. For example if you have a server extension and you map it the file extension .shp, then the server extension would be called no matter if you requested page1.shp or page2.shp. The file page1.shp does not need to exist on your hard drive for the server extension to be called.


Using the Parse Map


The parse map is a feature of Microsoft's CHttpServer class. It is used to bind specific procedures with requests to the Server Extension. Each request to the Server Extension must have a name that is the name of the procedure in the Server Extension it's calling. For example, if you have an ISAPI Server Extension that is using parse maps and that server extension has two procedures called Page1 and Page2, you will have to reference in the URL to call them. The references might look like this:


http://MYMACHINE/scripts/MYDLL.dll?Page1

http://MYMACHINE/scripts/MYDLL.dll?Page2

MYMACHINE is the name of your machine and MYDLL is the ISAPI Server Extension.

Besides calling the name of the procedure in the request, the client must also supply a specific matching parameter set in the right order. Because of these requirements, the parse map approach is difficult to use.

The Default Procedure


The Extension wizard writes a default procedure into the parse map. This procedure is called Default and takes no parameters. When the Extension DLL binds all requests that do not specify a procedure to the Default procedure. The entry in the parse map looks like this:


ON_PARSE_COMMAND(Default, CLst14_01Extension, ITS_EMPTY)

Notice the second command of the ON_PARSE_COMMAND is ITS_EMPTY. This is used to signify that the Default command takes no parameters.

The Default procedure looks like this:


void CLst14_01Extension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("This default message was produced by the Internet");

*pCtxt << _T(" Server DLL Wizard. Edit your CLst14_01Extension::Default()");

*pCtxt << _T(" implementation to change it.\r\n");

EndContent(pCtxt);

}

Notice that a CHttpServerContext class is passed into the Default procedure. All procedures that are called from the parse map must accept the CHttpServerContext class. By using the << operator of the CHttpServerContext class, you can pipe information to the client. Notice that before any information is written to the CHttpServerContext class, the CHttpServer StartContent() method is called. All this method does is write the <HTML> and <BODY> tags to the client. The EndContent() method writes the end <BODY> and end <HTML> tags to the client. These methods are frivolous because they can be replaced with


*pCtxt << _T("<HTML><BODY>");

and


*pCtxt << _T("</HTML></BODY>");

To request the Default procedure, type the server path name of the DLL and the method to call into the browser like this:


http://MYMACHINE/scripts/lst14_01/debug/lst14_01.dll?Default

In this line, MYMACHINE is the name of your machine.

A second way to call the Default procedure is by using the following:


http://MYMACHINE/scripts/lst14_01/debug/lst14_01.dll

This works because the Default procedure has also been defined in the parse map as the procedure to call if no procedure is named. The parse map entry to do this is


DEFAULT_PARSE_COMMAND(Default, CLst14_01Extension)

Creating a Second Method


Let's create another method called Hello. First make the entry in the parse map like this:


ON_PARSE_COMMAND(Hello, CLst14_01Extension, ITS_EMPTY)

Then create a method called Hello that returns nothing and takes a pointer to CHttpServerContext:


void CLst14_01Extension::Hello(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("Hello World!\r\n");

EndContent(pCtxt);

}

Make sure that you add the method to the CLst14_01Extension class definition, also.

To call the new method, type the server path, the name of the DLL, and the method that you wish to call, into the browser address space, like this:


http://MYMACHINE/scripts/lst14_01/debug/lst14_01.dll?Hello

where MYMACHINE is the name of your machine.

Your screen should look like Figure 14.4.

Figure 14.4. Browser view of the Hello World Method

Getting the CGI Parameters


Create another method called GetName(). Besides a pointer to CHttpServerContext, GetName() also takes a character string. GetName() should look like this:


void CLst14_01Extension::GetName(CHttpServerContext* pCtxt, LPCTSTR pName)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("Name: ");

*pCtxt << pName;

*pCtxt << _T("<BR>\r\n");

EndContent(pCtxt);

}

The entry in the parse map should look like this:


ON_PARSE_COMMAND(GetName, CLst14_01Extension, ITS_PSTR)

Make sure that you add the method to the CLst14_01Extension class definition also.

To call the new method, type the server path, the name of the DLL, the method to call, and the string you want to enter into the browser address space, like this:


http://MYMACHINE/scripts/lst14_01/debug/lst14_01.dll?GetName?Joe

Here, MYMACHINE is the name of your machine.

The output from the browser should look like Figure 14.5.

Figure 14.5. The output from the browser.

Using Forms with Parse Maps


As discussed in the preceding chapter, in writing CGI scripts, you need to include code to handle both the GET and POST methods for HTML forms. By using a Server Extension, underlying MFC code handles the differences between these methods, and the programmer doesn't need to do anything. To demonstrate this, let's create an HTML page that calls the Server Extension with the GET method.

Listing 14.2. HTML to Request Listing 14.1.


<HTML>

<BODY>

<FORM ACTION="http://dartbldn/scripts/lst14_01/debug/lst14_01.dll?GetName"

[ic:ccc]METHOD=POST>

<INPUT TYPE=TEXT NAME="Name" VALUE="">

<INPUT TYPE=SUBMIT>

</FORM>

</BODY>

</HTML>
Tip


Parse maps work best with forms that send POST methods. Some browsers truncate the ?GetName with a GET method, causing the Server Extension to fail.


Default Parameters


Try calling the GetName() method without a name, like this:


http://MYMACHINE/scripts/lst14_01/debug/lst14_01.dll?GetName

This should cause an error because you don't tell the parse map what to use for default parameters.

Try adding the following line right after the ON_PARSE_COMMAND that binds the GetName() method:


ON_PARSE_COMMAND_PARAMS("string=N/A")
Note


It's important to note here that the ON_PARSE_COMMAND_PARAMS line does not contain a reference to a method. The compiler knows which ON_PARSE_COMMAND_PARAMS line is associated to which method by looking at the ON_PARSE_COMMAND line proceeding the ON_PARSE_COMMAND_PARAMS line in the parse map. The ON_PARSE_COMMAND line then contains the needed reference to the method.

Recompile and try the preceding URL again. This time the browser should look like Figure 14.6.

Figure 14.6. The request to GetName through the Browser

The Problems with Parse Maps


Parse maps are good for simple things, but they are not suited for complex Server Extensions. One problem is that from a programmer's perspective, there are many entrances to the DLL. If the programmer wants to add specific code that needs to be called whenever a thread enters a DLL, it must be added to each method that is bound to the parse map. This makes things such as a thread pool harder to program. A second problem is that the parameters must be sent in a particular order. This works well for the disciplined programmer but is not the way CGI applications are accustomed to receiving their CGI parameters. Besides sending parameters in a particular order (that matches the parse map), you must also send the right number of parameters. This limitation makes it more difficult to implement certain types of Server Extensions.

Beyond Parse Maps


You can program Server Extensions like you program CGI scripts, with one entrance for every thread. This technique still leaves you with the performance of Server Extensions without the headaches of parse maps. You need to remove the parse maps and write a little code to grab the CHttpServerContext for yourself.

One Entrance Server Extension


Create a Server Extension with the Extension Wizard call it lst14_03.dll.

The created code for the header file (lst14_03.h) will look like this :

Listing 14.2. lst14_03.cpp.


// LST14_03.CPP - Implementation file for your Internet Server

//    lst14_03 Extension

class CLst14_03Extension : public CHttpServer

{

public:

UINT *m_pnCounter;

CLst14_03Extension();

~CLst14_03Extension();

BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);

// TODO: Add handlers for your commands here.

// For example:

void Default(CHttpServerContext* pCtxt);

DECLARE_PARSE_MAP()

};

The created code for the header file (lst14_03.h) will look like this :


// LST14_03.CPP - Implementation file for your Internet Server

//    lst14_03 Extension

#include <afx.h>

#include <afxwin.h>

#include <afxisapi.h>

#include "resource.h"

#include "lst14_03.h"

///////////////////////////////////////////////////////////////////////

// command-parsing map

BEGIN_PARSE_MAP(CLst14_03Extension, CHttpServer)

// TODO: insert your ON_PARSE_COMMAND() and 

// ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

// For example:

ON_PARSE_COMMAND(Default, CLst14_03Extension, ITS_EMPTY)

DEFAULT_PARSE_COMMAND(Default, CLst14_03Extension)

END_PARSE_MAP(CLst14_03Extension)

///////////////////////////////////////////////////////////////////////

// The one and only CLst14_03Extension object

CLst14_03Extension theExtension;

///////////////////////////////////////////////////////////////////////

// CLst14_03Extension implementation

CLst14_03Extension::CLst14_03Extension()

{

}

CLst14_03Extension::~CLst14_03Extension()

{

}

BOOL CLst14_03Extension::GetExtensionVersion(HSE_VERSION_INFO* pVer)

{

// Call default implementation for initialization

CHttpServer::GetExtensionVersion(pVer);

// Load description string

TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];

ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),

IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));

_tcscpy(pVer->lpszExtensionDesc, sz);

return TRUE;

}

///////////////////////////////////////////////////////////////////////

// CLst14_03Extension command handlers

void CLst14_03Extension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("This default message was produced by the Internet");

*pCtxt << _T(" Server DLL Wizard. Edit your CLst14_03Extension::Default()");

*pCtxt << _T(" implementation to change it.\r\n");

EndContent(pCtxt);

}

///////////////////////////////////////////////////////////////////////

// If your extension will not use MFC, you'll need this code to make

// sure the extension objects can find the resource handle for the

// module.  If you convert your extension to not be dependent on MFC,

// remove the comments arounn the following AfxGetResourceHandle()

// and DllMain() functions, as well as the g_hInstance global.

/****

static HINSTANCE g_hInstance;

HINSTANCE AFXISAPI AfxGetResourceHandle()

{

return g_hInstance;

}

BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ulReason,

LPVOID lpReserved)

{

if (ulReason == DLL_PROCESS_ATTACH)

{

g_hInstance = hInst;

}

return TRUE;

}

****/

Now remove the parse map code by removing the following lines from lst14_03.cpp:


///////////////////////////////////////////////////////////////////////

// command-parsing map

BEGIN_PARSE_MAP(CLst14_03Extension, CHttpServer)

// TODO: insert your ON_PARSE_COMMAND() and

// ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

// For example:

ON_PARSE_COMMAND(Default, CLst14_03Extension, ITS_EMPTY)

DEFAULT_PARSE_COMMAND(Default, CLst14_03Extension)

END_PARSE_MAP(CLst14_03Extension)

Remove the declaration of the parse map from lst14_03.h:


DECLARE_PARSE_MAP()

Then remove the Default method from lst14_03.cpp:


///////////////////////////////////////////////////////////////////////

// CLst14_03Extension command handlers

void CLst14_03Extension::Default(CHttpServerContext* pCtxt)

{

StartContent(pCtxt);

WriteTitle(pCtxt);

*pCtxt << _T("This default message was produced by the Internet");

*pCtxt << _T(" Server DLL Wizard. Edit your CLst14_03Extension::Default()");

*pCtxt << _T(" implementation to change it.\r\n");

EndContent(pCtxt);

}

Remove the definition of the default from lst14_03.h:


// TODO: Add handlers for your commands here.

// For example:

void Default(CHttpServerContext* pCtxt);

Now that you've removed the parse maps, you need to add the code that fixes the default memory leak. Add this line to lst14_03.cpp:


CWinApp BugFix;

It needs to go right after the static declaration of the extension class


CLst14_01Extension theExtension;

To create the one entrance into the Server Extension, you need to overload the method CallFunction() in your CHttpServer class. Add this code to lst14_03.cpp:


int CLst14_03Extension::CallFunction(CHttpServerContext*

[ic:ccc]pCtxt,LPTSTR pszQuery, LPTSTR pszCommand)

{

int nRet=callOK;

ISAPIASSERT(pCtxt->m_pStream == NULL);

pCtxt->m_pStream = ConstructStream();

if (pCtxt->m_pStream == NULL)

nRet = callNoStream;

else

{

pCtxt->m_pStream->InitStream();

Entrance(pCtxt,pszQuery);

}

return(nRet);

}

You also need to declare the method in the definition of the class. Add this code to lst14_03.h:


int CallFunction(CHttpServerContext* pCtxt,LPTSTR pszQuery, LPTSTR pszCommand);

Notice that a new method called Entrance is called in CallFunction(). You need to add the Entrance method, also. For now, let's make it return Hello World!:


void CLst14_03Extension::Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery)

{

(*pCtxt) << _T("<HTML><BODY>Hello World!</BODY</HTML>");

};

You also need to declare the method in the definition of the class. Add this code to lst14_03.h:


void Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery);

This is the minimum amount of code needed to make a single entrance Server Extension. All other examples in this chapter will be based on this starting code.

Here is what you should have after making all the changes :

Listing 14.3. lst14_#1.h.


Header (lst14_01.h):

// LST14_03.CPP - Implementation file for your Internet Server

//    lst14_03 Extension

class CLst14_03Extension : public CHttpServer

{

public:

UINT *m_pnCounter;

CLst14_03Extension();

~CLst14_03Extension();

BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);

int CallFunction(CHttpServerContext* pCtxt,LPTSTR pszQuery, LPTSTR pszCommand);

void Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery);

};

Source:


// LST14_03.CPP - Implementation file for your Internet Server

//    lst14_03 Extension

#include <afx.h>

#include <afxwin.h>

#include <afxisapi.h>

#include "resource.h"

#include "lst14_03.h"

///////////////////////////////////////////////////////////////////////

// The one and only CLst14_03Extension object

CLst14_03Extension theExtension;

CWinApp BugFix;

///////////////////////////////////////////////////////////////////////

// CLst14_03Extension implementation

CLst14_03Extension::CLst14_03Extension()

{

}

CLst14_03Extension::~CLst14_03Extension()

{

}

BOOL CLst14_03Extension::GetExtensionVersion(HSE_VERSION_INFO* pVer)

{

// Call default implementation for initialization

CHttpServer::GetExtensionVersion(pVer);

// Load description string

TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];

ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),

IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));

_tcscpy(pVer->lpszExtensionDesc, sz);

return TRUE;

}

int CLst14_03Extension::CallFunction(CHttpServerContext *pCtxt,LPTSTR pszQuery, LPTSTR pszCommand)

{

int nRet=callOK;

ISAPIASSERT(pCtxt->m_pStream == NULL);

pCtxt->m_pStream = ConstructStream();

if (pCtxt->m_pStream == NULL)

nRet = callNoStream;

else

{

pCtxt->m_pStream->InitStream();

Entrance(pCtxt,pszQuery);

}

return(nRet);

}

void CLst14_03Extension::Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery)

{

(*pCtxt) << _T("<HTML><BODY>Hello World!</BODY</HTML>");

};

Compile lst14_03.dll. Before you run the IIS in the debugger, you will need to add this new DLL to the IIS project. You can do this by including it in Settings|Additional DLLs, just as you did for lst14_01.dll. Now, run the IIS in the Debugger and execute the Server Extension through your browser.

Why Not Overload HttpExtensionProc?


HttpExtensionProc is the procedure that IIS calls when entering the DLL. It seems logical that if you want to get down to the nitty gritty of Server Extension programming, you would want to overload HttpExtensionProc. This, however, is not the goal; the goal is to have a single entry point into the DLL. HttpExtensionProc calls CallFunction(), but between the two functions, is code to initialize streams and handle some types of errors. Because CallFunction() has both the CHttpServerContext and the CGI parameters, it's easier to overload and use the HttpExtensionProc. CallFunction() also meets the goal because it handles all requests. The parent method of CallFunction() figures out which method to call using the parse map. Overloading CallFunction() removes the parse maps from the Server Extension.

Creating a Thread Pool


Single-entrance server extensions allow the programmer to easily create thread pools. Because a thread is created for every request to the DLL, a Web site that is busy might create enough requests to bog down the machine with threads. This could cause thread lock, making for slow request, or it could cause the operating system to crash. To prevent this type of problem, it's best to use a thread pool within the Server Extension. The type of thread pool that I recommend is one that makes threads wait until other threads are processed, instead of telling the user that the site is too busy to handle the requests.

To create a thread pool, you need to create some protected member variables that will keep track of the number of threads in the DLL.

Create a protected member variable in CLst14_03Extension called m_pnCounter with this line of code:


UINT *m_pnCounter;

You also need a Critical Section to prevent two threads from accessing the counter variable at the same time. A Critical Section is a section of code in Windows that is restricted to one process. Let's create another protected member variable to handle this:


LPCRITICAL_SECTION m_lpCriticalExtension;

Initialize the counter in the server extension constructor and delete it in the destructor. Also initialize and delete the Critical Section:


CLst14_03Extension::CLst14_03Extension()

{

m_pnCounter = new UINT;

(*m_pnCounter) = 0;

m_lpCriticalExtension = new CRITICAL_SECTION;

InitializeCriticalSection(m_lpCriticalExtension);

}

CLst14_03Extension::~CLst14_03Extension()

{

delete m_pnCounter;

DeleteCriticalSection(m_lpCriticalExtension);

delete m_lpCriticalExtension;

}

Create two new methods, as shown in the following code segment. One method will check to see if the maximum number of threads has been reached; call this method ThreadPool(). Another method will decrement the counter when the thread exits the DLL; call this method LeavePool().


void CLst14_03Extension::ThreadPool()

{

BOOL bGoodToGo=FALSE;

while (!bGoodToGo)

{

EnterCriticalSection(m_lpCriticalExtension);

bGoodToGo=((*m_pnCounter)<15);

if(bGoodToGo)

(*m_pnCounter)++;

LeaveCriticalSection(m_lpCriticalExtension);

}

}

void CLst14_03Extension::LeavePool()

{

EnterCriticalSection(m_lpCriticalExtension);

(*m_pnCounter)--;

LeaveCriticalSection(m_lpCriticalExtension);

};

Make sure to define the methods in the declaration of the class. The methods also need to be added to Entrance(): one method at the beginning of Entrance, and one at the end, as in the following:


void  CLst14_03Extension::Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery)

{

ThreadPool();

(*pCtxt) << _T("<HTML><BODY>Hello World!</BODY</HTML>");

LeavePool();

};

Considerations When Dealing with Thread Pools


You might have some trouble with this code because it holds the number of threads down to 15. If a lot of requests come simultaneously to your Web space, some of them might time out before being allowed to enter the DLL and get information. This really isn't a problem with the thread pooling; the thread pooling just tries to prevent the operating system from crashing. If users can't get into the DLL in a timely manner, you need to consider either making the DLL return the pages faster by doing performance enhancements, or adding hardware to meet the demand. The number 15 is an arbitrary number. This number should be increased or decreased based on the hardware you are running. Machines with more processors may want to have more threads running at any given time. You also need to take into consideration other Server Extensions' threads.

Notice that the threads are not processed on a first come, first served basis. This is because the code assumes that all processes will be handled in a timely manner and the count is above fifteen, just momentarily. Think of your DLL as a store that is not used to having long lines—if you do have long lines, you need to hire another cashier (add more hardware).

You might want to implement a feature that returns a page saying "Please return later our server is busy". This message would only come up if threads come in while there are more threads than you can handle in the DLL. This makes it look as though you are in control of the situation instead of your server timing out. This works well if you're sure of the number of threads that your server can handle. If you set the maximum thread count too low, you could be turning away people you don't have to.

CGI Parameters and Single Entrance Server Extension


The CGI parameters passed to the server from the browser with both the POST and GET methods are available in the Entrance() method. The variable pszQuery, which is passed into the Entrance() method, contains the CGI parameters. Unlike the parse map, the CGI parameters in pszQuery are still encoded. To read the CGI parameters, you need to add a new method to the class. The two parameters of FindNameValue() are the pszQuery string, plus the name of the 'name=value' pair. FindNameValue() returns the value of the 'name=value'. The memory allocated for the value needs to be deleted by the caller. Here is the method:


// The Caller must delete the memory

LPTSTR CLst14_03Extension::FindNameValue(LPCTSTR lpszString,

[ic:ccc]LPCTSTR lpszName,TCHAR cSeparator,TCHAR cDelimiter)

{

LPTSTR lpszIndex;

LPTSTR lpszEnd;

DWORD dwValueSize=0;

LPTSTR lpszValue;

LPTSTR lpszStringCopy;

LPTSTR lpszNameCopy;

DWORD dwNameLength;

lpszStringCopy = new TCHAR[_tcslen(lpszString)+1];

_tcscpy(lpszStringCopy,lpszString);

lpszNameCopy = new TCHAR[_tcslen(lpszName)+2];

_tcscpy(lpszNameCopy,lpszName);

dwNameLength=_tcslen(lpszNameCopy);

lpszNameCopy[dwNameLength]=cSeparator;

lpszNameCopy[dwNameLength+1]=(TCHAR)_T('\0');

// Find The Name in the Query String

lpszIndex=_tcsstr(lpszStringCopy,lpszNameCopy);

delete lpszNameCopy;

// Error: The Name part of the Name value pair doesn't exist

if (!lpszIndex)

{

delete lpszStringCopy;

return (NULL);

}

// Increase the pointer to pass the Name and to Get the Value

lpszIndex+=_tcslen(lpszName)+1;

// Find the End of the Value by looking for the Delimiter

lpszEnd=_tcschr(lpszIndex,_T(cDelimiter));

// if we find a Demiliter set it as the end

if (lpszEnd)

(*lpszEnd)='\0';

// Remove the CGI Syntax

PreprocessString(lpszIndex);

// Calculate the Value Size

dwValueSize=_tcslen(lpszIndex);

lpszValue = new TCHAR[dwValueSize+1];

_tcscpy(lpszValue,lpszIndex);

delete lpszStringCopy;

return lpszValue;

}

Add the following FindNameValue() method to lst14_03.dll. Remember to define the method in the class definition. To test how this works, change Entrance to look for a name being passed in:


void  CLst14_03Extension::Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery)

{

ThreadPool();

(*pCtxt) << _T("<HTML><BODY>Name: ");

LPTSTR lpszValue;

if (lpszValue=FindNameValue(pszQuery,_T("Name"),_T('='),_T('&')))

{

(*pCtxt) << lpszValue;

delete lpszValue;

}

(*pCtxt) << _T("</BODY</HTML>");

LeavePool();

};

Notice that if FindNameValue() returns NULL, no memory has been allocated; otherwise, the caller, in this case Entrance(), needs to delete the memory allocated by FindNameValue().

Recompile and then execute lst14_03.dll through the browser, like this:


http://MYMACHINE/scripts/lst14_03/debug/lst14_03.dll?Name=Joe

where MYMACHINE is the name of your machine.

The browser should look like Figure 14.7.

Figure 14.7. Listing 14.3 running on a browser with the name of Joe.

Connecting to ODBC


One of the greatest advantages of using a Server Extension is that once it's loaded, it remains loaded in the cache. You can make a connection to an ODBC data source when the Server Extension is loaded and break the connection when the DLL is unloaded. This type of connection can be thought of as a constantly open pipe from the Server Extension to the data source. This makes for a huge performance advantage in querying data from a database because the most costly thing in making a small query to an ODBC data source is to open the connection. If the connection is already open when the request for information is made, the data comes back much more quickly.

The Internet Database Connector that comes with the IIS doesn't have this type of advantage. Every time a request is made to the IDC, the IDC opens a new connection to the database and closes it when it is done servicing the request. Server extensions that use the caching method described in the preceding paragraph can be much faster then the IDC.

Before discussing the code for this example, you need to create a database in SQL Server and a table called Surfers. After you've created a database in SQL Server, run this code in ISQL_w, and the table will be created for you:


CREATE TABLE dbo.Surfers (

Id int IDENTITY (1, 1) NOT NULL PRIMARY KEY,

Date datetime NULL DEFAULT GETDATE()

)

ISQL_w is an application that comes with SQL Server. After you create the table, create a data source entry in ODBC to point to the database and make the data source name "Surfers" so that it matches the example. If you don't know how to create an ODBC data source, refer to the section entitled "Creating a Data Source" in Chapter 16, "ISPI Internet Database Connector."

Create another Server Extension called lst14_04.dll by using the code in Listing 14.4. After you've created the Server Extension, you will have to add odbc32.lib to the Object|Library Modules Edit box under Build|Settings|Link so that the ODBC calls can link.

Listing 14.4 is the example of using ODBC to create a unique identifier.

Listing 14.4. lst14_04.cpp.


// LST14_04.CPP - Implementation file for your Internet Server

//    lst14_04 Extension

#include <afx.h>

#include <afxwin.h>

#include <afxisapi.h>

#include "resource.h"

#include "lst14_04.h"

///////////////////////////////////////////////////////////////////////

// The one and only CLst14_04Extension object

CLst14_04Extension theExtension;

CWinApp BugFix;

///////////////////////////////////////////////////////////////////////

// CLst14_04Extension implementation

CLst14_04Extension::CLst14_04Extension()

{

// Hard Coded Datasource, login, and password

m_csDSN="Surfers";

m_csUser="sa";

m_csPassword="";

InitODBC();

Connect();

}

CLst14_04Extension::~CLst14_04Extension()

{

Disconnect();

CleanUpODBC();

}

BOOL CLst14_04Extension::GetExtensionVersion(HSE_VERSION_INFO* pVer)

{

// Call default implementation for initialization

CHttpServer::GetExtensionVersion(pVer);

// Load description string

TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];

ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),

IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));

_tcscpy(pVer->lpszExtensionDesc, sz);

return TRUE;

}

int CLst14_04Extension::CallFunction(CHttpServerContext* pCtxt

[ic:ccc],LPTSTR pszQuery, LPTSTR pszCommand)

{

int nRet=callOK;

ISAPIASSERT(pCtxt->m_pStream == NULL);

pCtxt->m_pStream = ConstructStream();

if (pCtxt->m_pStream == NULL)

nRet = callNoStream;

else

{

pCtxt->m_pStream->InitStream();

Entrance(pCtxt,pszQuery);

}

return(nRet);

}

void  CLst14_04Extension::Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery)

{

DWORD dwId;

dwId=UniqueId();

CString csOutput;

csOutput.Format(_T("Your Unique Id is: %d"),dwId);

(*pCtxt) << csOutput;

};

void CLst14_04Extension::InitODBC()

{

m_lpcsFlag=new CRITICAL_SECTION;

InitializeCriticalSection(m_lpcsFlag);

};

void CLst14_04Extension::CleanUpODBC()

{

DeleteCriticalSection(m_lpcsFlag);

delete m_lpcsFlag;

};

void CLst14_04Extension::Connect()

{

RETCODE rc;

rc=SQLAllocEnv(&m_henv);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

ODBCError(SQL_NULL_HSTMT);

rc=SQLAllocConnect(m_henv, &m_hdbc);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

ODBCError(SQL_NULL_HSTMT);

rc=SQLConnect(m_hdbc, (UCHAR FAR *)(LPCTSTR) m_csDSN,

SQL_NTS, (UCHAR FAR *)(LPCTSTR) m_csUser ,

[ic:ccc]m_csUser.GetLength(), (UCHAR FAR *)(LPCTSTR) m_csPassword, m_csPassword.GetLength());

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

ODBCError(SQL_NULL_HSTMT);

};

void CLst14_04Extension::Disconnect()

{

SQLDisconnect(m_hdbc);

SQLFreeConnect(m_hdbc);

SQLFreeEnv(m_henv);

}

void CLst14_04Extension::ODBCError (HSTMT hstmt)

{

UCHAR FAR szSqlState[5];

UCHAR FAR szErrorMsg[80];

SWORD cbErrorMsgMax=80;

SWORD FAR *pcbErrorMsg= new SWORD;

SDWORD FAR *pfNativeError= new SDWORD;

RETCODE rc;

rc=SQLError(m_henv, m_hdbc, hstmt,

[ic:ccc] szSqlState, pfNativeError, szErrorMsg, cbErrorMsgMax, pcbErrorMsg);

if (!_tcscmp((char*)szSqlState,"08S01"))

{

// Bad Connection Let's try to recover!

ISAPITRACE("Bad Connection\n");

EnterCriticalSection(m_lpcsFlag);

Disconnect();

Connect();

LeaveCriticalSection(m_lpcsFlag);

}

ISAPITRACE1("SQL Error: %s\n",szErrorMsg);

delete pfNativeError;

delete pcbErrorMsg;

};

HSTMT CLst14_04Extension::GetStatement()

{

// The only reason that we are entering a critical section here

// is because the connection could be bad and we are trying to reconnect

// otherwise this isn't needed since ODBC is thread proof.

EnterCriticalSection(m_lpcsFlag);

HSTMT hstmt;

RETCODE rc;

rc=SQLAllocStmt(m_hdbc, &hstmt);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

// Bad Connection Let's try to recover!

// The CriticalSection Prevents other threads

// From trying to get a statement with a bad connection

ISAPITRACE("Bad Connection\n");

EnterCriticalSection(m_lpcsFlag);

Disconnect();

Connect();

LeaveCriticalSection(m_lpcsFlag);

//Try Again

rc=SQLAllocStmt(m_hdbc, &hstmt);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

// Only try to reconnect once

// Otherwise it is beyond our control

return(NULL);

}

}

LeaveCriticalSection(m_lpcsFlag);

return(hstmt);

}

void CLst14_04Extension::FreeStatement(HSTMT hstmt)

{

SQLFreeStmt(hstmt, SQL_DROP);

}

DWORD CLst14_04Extension::UniqueId()

{

DWORD Id=0;

HSTMT hstmt;

RETCODE rc;

SDWORD cbData;

if((hstmt=GetStatement())==NULL)

{

return(0);

}

CString SQLScript=

[ic:ccc]"INSERT Surfers DEFAULT VALUES SELECT Id=@@IDENTITY FROM Surfers WHERE Id=@@IDENTITY ";

rc=SQLExecDirect(hstmt, (UCHAR FAR *) (LPCTSTR) SQLScript, SQL_NTS);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

ODBCError(hstmt);

return(0);

}

rc = SQLMoreResults(hstmt);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

ODBCError(hstmt);

return(0);

}

rc = SQLFetch(hstmt);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

ODBCError(hstmt);

return(0);

}

rc=SQLGetData(hstmt, 1, SQL_C_SLONG, &Id, 0,&cbData);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

ODBCError(hstmt);

return(0);

}

FreeStatement(hstmt);

return(Id);

};

The heading file, lst14_04.h:


// LST14_04.CPP - Implementation file for your Internet Server

//    lst14_04 Extension

#include <sql.h>

#include <sqlext.h>

class CLst14_04Extension : public CHttpServer

{

protected:

HENV m_henv;

HDBC m_hdbc;

LPCRITICAL_SECTION m_lpcsFlag;

Cstring m_csDSN;

Cstring m_csUser;

Cstring m_csPassword;

void InitODBC();

void CleanUpODBC();

void Connect();

void Disconnect();

void ODBCError(HSTMT hstmt);

public:

CLst14_04Extension();

~CLst14_04Extension();

BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);

int CallFunction(CHttpServerContext* pCtxt,LPTSTR pszQuery, LPTSTR pszCommand);

void  Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery);

HSTMT GetStatement();

void FreeStatement(HSTMT hstmt);

DWORD UniqueId();

};

After you've compiled the example, run it by typing


MYMACHINE\scripts\lst14_04\debug\lst14_04.dll

where MYMACHINE is the name of your machine. The browser output should look something like Figure 14.8.

Figure 14.8. Request to Listing 14.4.

Notice that every time you refresh the browser, a unique ID is created in the database and displayed in the Web page. The ODBC connection is in the constructor of Server Extension, which gets called when the DLL loads. The ODBC connection is closed in the destructor. With the caching turned off for debugging, you do not benefit from the performance enhancement of a constantly open connection because the connection is being terminated with every load and unload of the DLL. Notice that the data source name, the user name, and the password and being statically defined in the constructor; this is to keep the example simple. Also, to simplify the example, thread pooling has been removed.

Losing the ODBC Connection


In a perfect world, a constantly open ODBC connection would not be a problem; however, this is not a perfect world. Because of failures in either hardware or software, the connection to ODBC can be lost. That's why it's important to write recovery software. Some recovery has been added to Listing 14.4; if the SQL Server connection is broken, the software tries to create a new connection. This keeps the Server Extension running and keeps your Web site up when the Server Extension encounters a minor problem. Notice that when the server extension is recovering, it enters a Critical Section, which makes sure that other threads entering the server extension do not disturb the recovery process.

Give Me a Cookie


Cookies are name/value pairs that are passed between the Web server and the Web browser. This is how a typical cookie implementation scheme works: If a cookie has been issued in the past, the browser passes that cookie to the server. The server extension then checks to see if a cookie has been passed to it. If the browser doesn't have that cookie, the server extension gives the browser a named cookie with a unique value. The next time the browser returns to the server, the browser tells the server what cookie it has gotten.

Cookies are great for keeping track of users who are traversing your Web space. They are also a good way to tell if a user is returning to your Web space.

With a little modification, Listing 14.4 can be used to issue cookies. You need to add a couple of methods, starting with GetServerVariable(), which retrieves variables passed from the server to the Server Extension. The server variable for the cookie example is HTTP_COOKIE, which is passed from the client to the server and then to the server extension. If the server extension has been visited before and a cookie has been assigned, this call will return a 'name=value' pair with the unique cookie identifier. Other server variables are listed in Table 13.2 in Chapter 13, "Windows CGI Scripting."

Here is the code for GetServerVariable(), method:


// The Caller must delete the memory, unless error in which case returns NULL

LPTSTR CLst14_04Extension::GetServerVariable(CHttpServerContext* pCtxt,

[ic:ccc] LPCTSTR pszVariableName)

{

LPVOID lpvBuffer=NULL;

DWORD dwSize=0;

pCtxt->GetServerVariable((LPTSTR)pszVariableName,NULL,(LPDWORD)&dwSize);

// Check to see if variable exists

if(dwSize==0)

return(NULL);

lpvBuffer=(LPVOID)new TCHAR[dwSize+1];

if (!(pCtxt->GetServerVariable((LPTSTR)pszVariableName,

[ic:ccc]lpvBuffer,(LPDWORD)&dwSize)))

{

delete lpvBuffer;

return(NULL);

}

if(dwSize==0)

{

delete lpvBuffer;

return(NULL);

}

return((LPTSTR)lpvBuffer);

};

You also have to add another method called FindNameValue(). FindNameValue() is used to separate the 'name=value' cookie pair if there is a cookie. Here is the code for FindNameValue():


// The Caller must delete the memory

LPTSTR CLst14_04Extension::FindNameValue(LPCTSTR lpszString,

[ic:ccc]LPCTSTR lpszName,TCHAR cSeparator,TCHAR cDelimiter)

{

LPTSTR lpszIndex;

LPTSTR lpszEnd;

DWORD dwValueSize=0;

LPTSTR lpszValue;

LPTSTR lpszStringCopy;

LPTSTR lpszNameCopy;

DWORD dwNameLength;

lpszStringCopy = new TCHAR[_tcslen(lpszString)+1];

_tcscpy(lpszStringCopy,lpszString);

lpszNameCopy = new TCHAR[_tcslen(lpszName)+2];

_tcscpy(lpszNameCopy,lpszName);

dwNameLength=_tcslen(lpszNameCopy);

lpszNameCopy[dwNameLength]=cSeparator;

lpszNameCopy[dwNameLength+1]=(TCHAR)_T('\0');

// Find The Name in the Query String

lpszIndex=_tcsstr(lpszStringCopy,lpszNameCopy);

delete lpszNameCopy;

// Error: The Name part of the Name value pair doesn't exist

if (!lpszIndex)

{

delete lpszStringCopy;

return (NULL);

}

// Increase the pointer passed the Name and Get to the Value

lpszIndex+=_tcslen(lpszName)+1;

// Find the End of the Value by looking for the Demiliter

lpszEnd=_tcschr(lpszIndex,_T(cDelimiter));

// if we find a Demiliter set it as the end

if (lpszEnd)

(*lpszEnd)='\0';

// Remove the CGI Syntax

PreprocessString(lpszIndex);

// Calculate the Value Size

dwValueSize=_tcslen(lpszIndex);

lpszValue = new TCHAR[dwValueSize+1];

_tcscpy(lpszValue,lpszIndex);

delete lpszStringCopy;

return lpszValue;

}

If a cookie is present, the cookie is checked to see if it is a contains a valid ID. The way the Server Extension checks this is too query the SQL Server and see if this id exists. To do this, use a method called ValidateId(). Here is the code for ValidateId().


BOOL CLst14_04Extension::ValidateId(DWORD dwId)

{

HSTMT hstmt;

RETCODE rc;

SDWORD cbData;

DWORD dwReturnId=0;

if((hstmt=GetStatement())==NULL)

{

return(FALSE);

}

CString SQLScript;

SQLScript.Format("SELECT Id FROM Surfers WHERE Id=%d",dwId);

rc=SQLExecDirect(hstmt, (UCHAR FAR *) (LPCTSTR) SQLScript, SQL_NTS);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

ODBCError(hstmt);

return(FALSE);

}

rc = SQLFetch(hstmt);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

ODBCError(hstmt);

return(FALSE);

}

rc=SQLGetData(hstmt, 1, SQL_C_SLONG, &dwReturnId, 0,&cbData);

if ((rc != SQL_SUCCESS) && (rc != SQL_SUCCESS_WITH_INFO))

{

ODBCError(hstmt);

return(FALSE);

}

FreeStatement(hstmt);

return(dwReturnId==dwId);

};

If a cookie doesn't exist or the cookie isn't valid, use the method UniqueId() to create a unique ID to use as a cookie. The server tells client what the cookie is by adding a line in the header of the page getting returned. When the client reads the page, it will assign the cookie. The next time the client returns to the Server Extension, that cookie will appear in the HTTP_COOKIE variable. The line inserted into the header not only contains the 'name=value' pair of the cookie, but also an expiration date for the cookie and a server path. After the expiration date, the client does not pass the cookie to the server anymore. For this example, the expiration date is set three years into the future (imagine what the Internet will be like in three years). Be careful in changing this because the day of the week is also included in the expiration date. The server path is set to the root of the server. This means that other dynamic Web pages in other directories on the same server can use the cookie you issued. You might consider changing the path to be local to your directory if others are using your server.

Note


Other Web servers cannot use the cookies you issue because the client returns a cookie only when it returns to the site that issued the cookie.

The following code creates the cookie header and inserts it into the Web page header:


void CLst14_04Extension::CreateCookie (CHttpServerContext* pCtxt, DWORD dwId)

{

CString csCookie;

csCookie.Format(_T

("Set-Cookie: SurferId=%d; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT \r\n"),

[ic:ccc]dwId);

EXTENSION_CONTROL_BLOCK *pECB =pCtxt->m_pECB;

DWORD dwHeaderSize = csCookie.GetLength();

pECB->ServerSupportFunction(pECB->ConnID,

 HSE_REQ_SEND_RESPONSE_HEADER,NULL, &dwHeaderSize,

[ic:ccc] (LPDWORD)(LPCSTR)csCookie);

}

Finally, the Entrance() method ties all the methods together in the right order:


void  CLst14_04Extension::Entrance(CHttpServerContext* pCtxt,LPTSTR pszQuery)

{

DWORD dwId=0;

LPTSTR lpszCookie;

LPTSTR lpszValue;

lpszCookie=GetServerVariable(pCtxt,"HTTP_COOKIE");

if (lpszCookie!=NULL)

{

if (lpszValue=FindNameValue(lpszCookie,"SurferId",'=',';'))

{

dwId=atoi(lpszValue);

delete lpszValue;

if (!ValidateId(dwId))

dwId=0;

}

delete lpszCookie;

}

if (!dwId)

{

dwId=UniqueId();

if (dwId)

{

CreateCookie(pCtxt,dwId);

}

}

CString csOutput;

csOutput.Format(_T("Your Unique Id is: %d"),dwId);

(*pCtxt) << csOutput;

};

Run Listing 14.4 in your browser. Notice that it assigns you a unique ID, and you continue to display that ID no matter how many times you refresh the browser. This example could be expanded to display the first time that the user viewed the page because the date the row was created is kept in the SQL Server.

Note


Some browsers don't support cookies. This means that when the server returns the Web page with a cookie in the header, the browser is not enabled to read the cookie or return the cookie. You need to write your code in such a way that both cookie-enabled browsers and cookie-disabled browsers can use your Web site. Make sure that you test your server extension with a cookie-disabled browser to insure that you Server Extensions work correctly.


Debugging Cookies


To test to make sure that your Server Extension is issuing a cookie correctly, you can look in the cookie file the browser creates. You can also erase the cookie it has given the browser from the cookie file. Erasing the cookie allows the Server Extension to issue you another one. To erase the cookie, you need to find the text file that the browser uses to store the cookies. Search your hard drive for files that begin with cookie. You will need to close the browser, open the file in a text editor such as Notepad, then erase the entry in the cookie file.

Summary


There are two main reasons for using Server Extensions instead of CGI scripts. One is that Server Extensions are faster because they share process space with the server, they are multithreaded, and they are cached in memory. Second, Server Extensions are easier to debug. Microsoft Developer Studio handles the debugging of DLLs nicely.

Server Extensions have advantages in accessing SQL Server data. Server Extensions are cached in memory, a constantly open connection can be made from the Server Extension to SQL Server. The open connection makes for faster data retrieval.

Previous Page Page Top TOC Next Page