June 1998
How to Design Reusable HTTP Components by Exploiting WinInet and COM
Download Jun98WinInetCode.exe (257KB)
Aaron Skonnard works as a Project Technical Lead for Axiom Technologies, specializing in MFC-based business applications, COM and ActiveX technologies, and Internet application development. He can be reached at aarons@axtech.com. Portions of this article will appear in an upcoming book to be published by Addison-Wesley.
|
Every time
I use the Windows® Internet API (WinInet) in my applications, it seems like I'm writing the same lines of code over and over again. And, every time I write a new WinInet-based function, it looks just like the last one I wrote with a few minor modifications. After a thorough investigation, I've decided that this is mostly due to the general nature of Internet application development. Typically, all transactions that use a given Internet protocol are going to follow the same high-level logic. Of course, certain aspects of an Internet transaction will always need to be customized for a given task.
For example, suppose you want to create an application that uses HTTP to communicate via TCP/IP with different servers across the Internet. The natural approach would be to write WinInet-based Win32® functions to handle data transmission. Most overhead-conscious developers would look for ways to make the functions reusable and generic. Object-oriented developers might even develop their own C++ classes to encapsulate the desired transmission functionality. Regardless of the transmission implementation, every task is going to require different HTTP methods, headers, input data formats, host names, port numbers, and parsing routines to handle the various HTTP response formats. To add another layer of complexity, some requests may require Secure Sockets Layer (SSL) and some form of authentication, while other requests may simply transmit clear text. While you can find ways to solve this design problem using standard Win32 functions or C++ classes, isn't there a better approach that will allow more reusable and extensible WinInet code? If you haven't already guessed, the solution I'm hinting at is the COM architecture. Whether you're in the business of selling shrinkwrapped products, client-customized applications, or software components, finding better ways to write reusable code will make everyone around you (including yourself) happier. Most developers find that simple, concise examples help put concepts into practice. The COM-based approach to developing WinInet code presented here will help you get one step closer to using COM in your everyday work. As you'll see, COM is the perfect solution for writing reusable WinInet components that are easily extensible and reusable. Breaking the functionality described in the HTTP scenario into multiple COM objects allows you to reuse the more generic components and rewrite the task-specific components on a case-per-case basis. This approach allows you to leverage well-written and tested components while minimizing the amount of code required for a new WinInet-related task. Furthermore, the COM approach offers dynamic application extensibility by simply adding new components to the user's system at runtime. In this article, I'll present a case study that illustrates how to write reusable WinInet HTTP COM components. First, I'll give you a brief WinInet primer and describe how you can begin using WinInet in your applications. Then, I'll cover the typical client/server application model and dive into HTTP details. With that foundation in place, I'll show you how to implement two ATL-based COM components: IHttpRequest and IQuoteProvider. Finally, I'll show you how to tie it all together in a sample application called Stock Watcher. Stock Watcher downloads stock quotes from different Internet stock quote providers and displays the M to the user. If you get nothing else from this article, you'll at least end up with a nifty application for monitoring your portfolio.
WinInet Primer
|
Figure 1 TCP/IP Protocol Layers |
|
Figure 2 HTTP Client/Server Model |
Getting Started With WinInet
There are two main sources of WinInet documentation. You can find complete WinInet API documentation integrated with the Visual C++ help files (version 4.2 or greater). The most complete and up-to-date sample code and reference material is provided in the Internet Client SDK online documentation. WinInet was originally developed for programmers using Win32, so it closely resembles the standard Win32 API. All Internet APIs use a new Internet handle type called HINTERNET. You can find a complete HINTERNET handle hierarchy in the Internet Client SDK documentation. Microsoft has also provided a set of MFC class wrappers that encapsulate the WinInet API functionality (see Figure 3). The WinInet MFC classes provide default parameters, exception handling, and buffered I/O. Furthermore, if you're an MFC developer, they'll fit into your overall application design. If you plan on using the MFC classes, you'll need to include afxinet.h instead of WinInet.h.
The HTTP Protocol
Most HTTP servers listen for requests on the well-known HTTP port 80. While you'll normally use port 80 to communicate via HTTP, other ports can be specified to establish the connection. Unlike the binary TCP and IP protocols, the HTTP request and response protocols are ASCII-based and much easier to work with. For example, an HTTP request consists of a method, a URI (Uniform Resource Identifier), the protocol version, a list of headers, a blank line, and an entity body (meaning data): |
|
The CRLF (a carriage-return line-feed sequence) is very important in the request format. The CRLF tells the HTTP server where the entity body begins. Leaving out the CRLF character will cause big headaches. Here is an example of a typical HTTP request: |
|
The HTTP response format is much like the request format. Everything but the first line of the response header is exactly the same. Here is the format of an HTTP response: |
|
The first line in the response format is referred to as the response status line. This line contains important information about the status of the request. The entity body contains the data returned by the server. Here is an HTTP response returned by the previous request: |
|
I've told you more than you possibly need to know about HTTP. WinInet lets you forget about most of the HTTP implementation details so you can focus on the Overall design and functionality of your app. Plus, as I walk through the development of some the WinInet-based COM components in the sample application, you'll see how easy it is to reuse and leverage well-written WinInet code.
The Stock Watcher App
|
Figure 4 Stock Watcher Application |
The Stock Watcher code and executable are available from the link at the top of this article. I wrote Stock Watcher using Microsoft Visual C++ 5.0 and some of the new common control features provided by the Internet Explorer 3.x version of the Windows common control library. If you compile the workspace, all the COM components should be registered for you properly. If you want to run the executable without compiling the project, run the SW.BAT file first to register the components.
If you open Stock Watcher.dsw in Developer Studio®, you should see the following three projects: Stock Watcher, HttpObjectServer, and QuoteProviders. Stock Watcher is an MFC AppWizard-generated application that implements the user interface. The HttpObjectServer and QuoteProviders projects are both ATL COM AppWizard-generated projects that service the IHttpRequest and IQuoteProvider interfaces. Figure 5 provides a high-level view of the Stock Watcher design model. When the user initiates an HTTP request, the MFC application creates instances of the CHttpRequest and CQuoteProvider components. The application establishes an event sink between the CQuoteProvider component and its own CQuoteProviderEventSink component. At this point, the application passes the IQuoteProvider interface pointer to the CHttpRequest component, which subsequently interfaces with it directly. As the CHttpRequest component needs quote-provider specific information for the request, it simply queries the IQuoteProvider interface. Finally, when CQuote Provider is finished parsing the HTTP result, it fires an event notifying the application. |
Figure 5 Stock Watcher Design Model |
As far as memory management is concerned, the MFC application only needs to track the IHttpRequest interface pointers. After the application passes the IQuoteProvider interface pointer to the CHttpRequest component, the CHttpRequest component calls IUnknown::AddRef, which is followed by the
MFC application calling
IUnknown::Release. Thus, the CQuoteProvider component will remain in memory until the CHttpRequest destructor releases the IQuoteProvider interface.
Now let's look at each of the interfaces and components in more detail.
The Quote Provider Component
To MFC or Not to MFC
|
|
After the AFX_MANAGE_STATE macro goes out of scope, the previous module state will be reset. While the ATL wizards take care of most of the details, you must manage this manually for any entry points not generated by ATL. This is especially true for connection points and their corresponding event sink handlers. If you think this gyration is more than you care to deal with, you can simply avoid using MFC in your COM server.
The CProviderYahoo Component
Adding Interface Methods
|
Figure 9 Add Method Dialog |
Before wildly adding methods to the IProviderYahoo interface, let's take a step back and look once again at the purpose of this module. The CHttpRequest component is going to use the quote provider components to get information about a given quote server and to parse the data returned from the same quote server. Obviously, every quote server is going to have different host names and potentially different HTTP port numbers. Furthermore, different quote servers will have different server-side script names and will require different input parameters. Additionally, since I don't control the server-side scripts, I must take special care in parsing the results from the different stock quote providers.
I want to make the IProviderYahoo component intelligent enough to tell the IHttpRequest component everything it needs to know about connecting to the Yahoo! server and parsing the data it returns. Figure 10 describes the methods that are part of the IProviderYahoo interface. Once again, refer to ProviderYahoo.cpp for more details on how the Se methods are implemented.
Didn't I Mean IQuoteProvider?
Connection Points
|
|
Additionally, the wizard adds the COM_INTERFACE_ ENTRY_IMPL for IConnectionPointContainer to the CProviderYahoo COM map. |
|
And finally, the wizard adds a connection point map without any initial entries. |
|
This wizard-generated code fully implements the IConnectionPointContainer interface. Now, I simply need to implement my own connection point interface.
IQuoteProviderEvent
|
|
At this point, you must recompile the QuoteProviders project to generate a new type library containing the IQuoteProviderEvent interface information. Once you've generated a new type library, you're ready to use the ATL Proxy Generator component.
You can access the ATL Proxy Generator through the Components and Controls Gallery (Project | Add To Project | Components and Controls). After double clicking on the Developer Studio Components directory, select the ATL Proxy Generator component and press Insert. After confirming, the ATL Proxy Generator dialog appears. Next, enter the path to the QuoteProvider type library (QuoteProvider.tlb); both the IQuoteProvider and IQuoteProviderEvent interfaces should appear on the left. Finally, select the IQuoteProviderEvent interface and make sure the Proxy Type is set to Connection Point before pressing Insert. The ATL Proxy Generator creates a new file named CPQuoteProviders.h (see Figure 8). This contains the declaration of CProxyIQuoteProviderEvent, which implements the IConnectionPoint interface and is capable of calling UpdateSymbol on all clients of this connection point through its Fire_UpdateSymbol method. To add this functionality to the CProviderYahoo component, CProviderYahoo also needs to inherit from the CProxyIQuoteProviderEvent template, so I added the following line of code to the list of CProviderYahoo base classes: |
|
Finally, I added a connection point entry for the IQuoteProviderEvent interface to the CProviderYahoo connection point map: |
|
Figure 8 illustrates what the CProviderYahoo class definition looks like with connection point support and the methods I added above. This approach to adding connection point functionality to your ATL COM components greatly reduces the amount of code that you must write.
The Quote Provider Component Category
|
|
Then, in QuoteProviders.cpp, I added some component category helper functions to make it easier to create the component category and register/unregister components with the category (see Figure 8). To register the QuoteProvider component category and the CProviderYahoo component, I added the Se lines of code to DllRegisterServer: |
|
To unregister the CProviderYahoo component, I added the following lines to DllUnregisterServer: |
|
The Stock Watcher application will use the Component Category Manager to query the CATID_QuoteProviders component category for all available Quote Provider components. After Stock Watcher has been installed, additional quote provider components may be added to the user's system by simply registering the M with the CATID_QuoteProviders component category.
The HTTP Component
|
|
After storing the IQuoteProvider pointer in m_pQuoteProvider, ProcessRequest must call IUnknown::AddRef on the interface pointer. ProcessRequest launches a worker thread, HttpWorkerThread, that takes care of the WinInet business from here. Since MFC objects are not thread safe, calls that manipulate the main application's MFC objects must be synchronized through window messages.
HttpWorkerThread
|
|
The HttpWorkerThread gathers all of the information it needs to make the Se WinInet calls by calling the appropriate IQuoteProvider methods. For example, CInternetSession::GetHttpConnection requires a host name and port number. Thus, before calling this method, the HttpWorkerThread calls IQuoteProvider::GetHost and IQuoteProvider::
GetPort to retrieve the provider-specific information. Furthermore, after HttpWorkerThread finishes receiving the HTTP response, it allows each Quote Provider to handle its responses differently by calling IQuoteProvider::ParseResult.
The CHttpRequest object is generic enough to support all types of Internet stock quote providers. As additional IQuoteProvider components are implemented and released, the CHttpRequest component will be able to transmit data to and from the new providers without modifying a single line of code.
Stock Watcher Implementation Details
|
Figure 12 Quote Providers |
After the user selects a stock provider and presses OK, CQuoteProviderDlg saves the CLSID of the component to an application-specific registry key named ProviderCLSID. From that point on, the application will use the selected quote provider component.
For Stock Watcher to receive events fired by the IQuoteProvider components, it must implement a sink object and establish a connection with the IQuoteProvider connection point. I called the sink object used by the application CQuoteProviderEventSink. The following is the CQuoteProviderEventSink class declaration: |
|
CStockWatcherDoc creates an instance of this object in its constructor (see Figure 13). Then, each time it creates an IHttpRequest component, it calls AtlAdvise to establish the connection point with IQuoteProvider. When CProviderYahoo::ParseResult calls Fire_UpdateSymbol, the CQuoteProviderEventSink::UpdateSymbol method will be called.
The Stock Watcher application allows the user to decide when to update the stock quote information by choosing Refresh All on the Symbol menu. The CStockWatcherDoc:: OnRefreshAllSymbols handler creates an instance of the currently selected quote provider component along with an IHttpRequest component. Then it passes the stock symbol information to the quote provider component using the IQuoteProvider::InitializeData method. Finally, after establishing the connection point with IQuoteProvider, it calls IHttpRequest::ProcessRequest (see Figure 13), which encapsulates the entire HTTP transaction functionality.
Conclusion
From the June 1998 issue of Microsoft Systems Journal.
|