This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA
Unfortunately, budget constraints have prevented SlowSoft from introducing any new products this year. We suggest you keep enjoying the old products.
You're looking at elementary HyperText Markup Language (HTML) code here, and the resulting Web page won't win any prizes. We won't go into details because dozens of HTML books are already available. From these books, you'll learn that HTML tags are contained in angle brackets and that there's often an "end" tag (with a / character) for every "start" tag. Some tags, such as (hypertext anchor), have attributes. In the example above, the line SlowSoft's Home Page
creates a link to another HTML file. The user clicks on "SlowSoft's Home Page," and the browser requests default.htm from the original server. Actually, newproducts.html references two server files, default.htm and /images/clouds.jpg. The clouds.jpg file is a JPEG file that contains a background picture for the page. The browser downloads each of these files as a separate transaction, establishing and closing a separate TCP connection each time. The server just dishes out files to any client that asks for them. In this case, the server doesn't know or care whether the same client requested newproducts.html and clouds.jpg. To the server, clients are simply IP addresses and port numbers. In fact, the port number is different for each request from a client. For example, if ten of your company's programmers are surfing the Web via your company's proxy server (more on proxy servers later), the server sees the same IP address for each client. Web pages use two graphics formats, GIF and JPEG. GIF files are compressed images that retain all the detail of the original uncompressed image but are usually limited to 256 colors. They support transparent regions and animation. JPEG files are smaller, but they don't carry all the detail of the original file. GIF files are often used for small images such as buttons, and JPEG files are often used \r\n" "\r\n\r\n"; try { if(!g_sListen.Accept(sConnect, saClient)) { // Handler in view class closed the listening socket return 0; } AfxBeginThread(ServerThreadProc, pParam); // read request from client sConnect.ReadHttpHeaderLine(request, 100, 10); TRACE("SERVER: %s", request); // Print the first header if(strnicmp(request, "GET", 3) == 0) { do { // Process the remaining request headers sConnect.ReadHttpHeaderLine(request, 100, 10); TRACE("SERVER: %s", request); // Print the other headers } while(strcmp(request, "\r\n")); sConnect.Write(headers, strlen(headers), 10); // response hdrs sConnect.Write(html, strlen(html), 10); // HTML code } else { TRACE("SERVER: not a GET\n"); // don't know what to do } sConnect.Close(); // Destructor doesn't close it } catch(CBlockingSocketException* e) { // Do something about the exception e->Delete(); } return 0; } The most important function call is the Accept call. The thread blocks until a client connects to the server's port 80, and then Accept returns with a new socket, sConnect. The current thread immediately starts another thread. In the meantime, the current thread must process the client's request that just came in on sConnect. It first reads all the request headers by calling ReadHttpHeaderLine until it detects a blank line. Then it calls Write to send the response headers and the HTML statements. Finally, the current thread calls Close to close the connection socket. End of story for this connection. The next thread is sitting, blocked at the Accept call, waiting for the next connection. 282.4.4 Cleaning Up To avoid a memory leak on exit, the program must ensure that all worker threads have been terminated. The simplest way to do this is to close the listening socket. This forces any thread's pending Accept to return FALSE, causing the thread to exit. try { g_sListen.Close(); Sleep(340); // Wait for thread to exit WSACleanup(); // Terminate Winsock } catch(CUserException* e) { e->Delete(); } When the user clicks on Idaho Weather Map, the browser sends the server a CGI GET request like this: GET scripts/maps.dll?State=Idaho HTTP/1.0 IIS then loads maps.dll from its scripts (virtual) directory, calls a default function (often named Default), and passes it the State parameter Idaho. The DLL then goes to work generating a JPG file containing the up-to-the-minute satellite weather map for Idaho and sends it to the client. If maps.dll had more than one function, the tag could specify the function name like this: Idaho Weather Map In this case, the function GetMap is called with two parameters, State and Res. You'll soon learn how to write an ISAPI server similar to maps.dll, but first you'll need to understand HTML forms, because you don't often see CGI GET requests by themselves. 291.3 HTML Forms—GET vs. POST In the HTML code for the simple CGI GET request above, the state name was hard-coded in the tag. Why not let the user select the state from a drop-down list? For that, you need a form, and here's a simple one that can do the job. Our courteous delivery person will arrive within 30 minutes. "; *pCtxt << " Thank you, " << pstrName << ", for using CyberPizza. "; // Now retrieve the order from disk by name, and then make the pizza. // Be prepared to delete the order after a while if the customer // doesn't confirm. m_cs.Lock(); // gotta be threadsafe long int nTotal = ++m_nTotalPizzaOrders; m_cs.Unlock(); *pCtxt << " Total pizza orders = " << nTotal; EndContent(pCtxt); } The customer's name comes back in the pstrName parameter, and that's what you use to retrieve the original order from disk. The function also keeps track of the total number of orders, using a critical section (m_cs) to ensure thread synchronization. 292.3 Building and Testing ex35a.dll If you have copied the code from the companion CD-ROM, your project is located in \vcpp32\ex35a. Building the project adds a DLL to the Debug subdirectory. You must copy this DLL to a directory that the server can find and copy PizzaForm.html also. You can use the scripts and wwwroot subdirec- tories already under \Winnt\System32\inetsrv, or you can set up new virtual directories. If you make changes to the EX35A DLL in the Visual C++ project, be sure to use Internet Service Manager (Figure 35-1) to turn off the WWW service (because the old DLL stays loaded), copy the new DLL to the scripts directory, and then turn the WWW service on again. The revised DLL will be loaded as soon as the first client requests it. If everything has been installed correctly, you should be able to load PizzaForm.html from the browser and then order some pizza. Enjoy! 292.4 Debugging the EX35A DLL The fact that IIS is a Windows NT service complicates debugging ISAPI DLLs. Services normally run as part of the operating system, controlled by the service manager database. They have their own window station, and they run on their own invisible desktop. This involves some of the murkier parts of Windows NT, and not much published information is available. However, you can use these steps to debug your EX35A DLL (or any ISAPI DLL): Use the Internet Service Manager to stop all IIS services. Choose Settings from the EX35A project Build menu, and in the Project Settings dialog, type in the data as shown. Open Sesame! Click anywhere to see the power of DHTML! This document is very short. Notice how easy it is to retrieve items with script. (The syntax calls for parentheses, similar to accessing an array in C++.) Also notice that each element in an HTML document has properties such as tagName that allow you to programmatically "search" for various elements. For example, if you wanted to write a script that filtered out all bold items, you would scan the all collection for an element with tagName equal to B. Now you have the basics of the DHTML object model down and you understand how to access them through scripts from the Webmaster's perspective. Let's look at how Visual C++ lets us work with DHTML from an application developer's perspective. 305. Visual C++ and DHTML Visual C++ 6.0 supports DHTML through both MFC and ATL. Both MFC and ATL give you complete access to the DHTML object model. Unfortunately, access to the object model from languages like C++ is done through OLE Automation (IDispatch) and in many cases isn't as cut-and-dried as some of the scripts we looked at earlier.
- 600 for photographic images for which detail is not critical. Visual C++ can read, write, and convert both GIF and JPEG files, but the Win32 API cannot handle these formats unless you supply a special compression/decompression module. The HTTP standard includes a PUT request type that enables a client program to upload a file to the server. Client programs and server programs seldom implement PUT. 280.8 FTP Basics The File Transfer Protocol handles the uploading and downloading of server files plus directory navigation and browsing. A Windows command-line program called ftp (it doesn't work through a Web proxy server) lets you connect to an FTP server using UNIX-like keyboard commands. Browser programs usually support the FTP protocol (for downloading files only) in a more user-friendly manner. You can protect an FTP server's directories with a username/password combination, but both strings are passed over the Internet as clear text. FTP is based on TCP. Two separate connections are established between the client and server, one for control and one for data. 280.9 Internet vs. Intranet Up to now, we've been assuming that client and server computers were connected to the worldwide Internet. The fact is you can run exactly the same client and server software on a local intranet. An intranet is often implemented on a company's LAN and is used for distributed applications. Users see the familiar browser interface at their client computers, and server computers supply simple Web-like pages or do complex data processing in response to user input. An intranet offers a lot of flexibility. If, for example, you know that all your computers are Intel-based, you can use ActiveX controls and ActiveX document servers that provide ActiveX document support. If necessary, your server and client computers can run custom TCP/IP software that allows communication beyond HTTP and FTP. To secure your company's data, you can separate your intranet completely from the Internet or you can connect it through a firewall, which is another name for a proxy server. 281. Build Your Own $99 Intranet Building a Microsoft Windows-based intranet is easy and cheap. Microsoft Windows 95, Microsoft Windows 98, and Microsoft Windows NT all contain the necessary networking capabilities. If you don't want to spend the $99, you can build a free intranet within a single computer. All the code in this chapter will run on this one-computer configuration. 281.1 NT File System vs. File Allocation Table With Windows 95 and Windows 98, you are restricted to one file system, File Allocation Table (FAT—actually VFAT for long filenames). With Windows NT, you choose between NT File System (NTFS) and FAT at setup time. Your intranet will be much more secure if you choose NTFS because NTFS allows you to set user permissions for individual directories and files. Users log on to a Windows server (or to an attached workstation) supplying a user name and password. Intranet and Internet clients participate in this operating-system security scheme because the server can log them on as though they were local users. Thus you can restrict access to any server directory or file to specific users who must supply passwords. If those user workstations are Windows network clients (as would be the case with a LANbased intranet), the user name and password are passed through from the user's logon. 281.2 Network Hardware You obviously need more than one computer to make a network. While your main development computer is probably a Pentium, a Pentium Pro, or a Pentium II, chances are you have at least one old computer hanging around. If it's at least a 486, it makes sense to connect it to your main computer for intranet testing and file backups. You will need a network board for each computer, but 10-megabit-per-second Ethernet boards now cost less than $50 each. Choose a brand that either comes with its own drivers for Windows 95, Windows 98, and Windows NT, or is already supported by those operating systems. To see a list of supported boards, click on the Network icon in the Control Panel and then click the Add button to add an Adapter. Most network boards have connectors for both thin coaxial (coax) and 10BaseT twisted pair. With 10BaseT, you must buy a hub, which costs several hundred dollars and needs a power supply. Thin coax requires only coaxial cable (available in precut lengths with connectors) plus terminator plugs. With coax, you daisy-chain your computers together and put terminators on each end of the chain. Follow the instructions that come with the network board. In most cases you'll have to run an MS-DOS program that writes to the electrically erasable/programmable read-only memory (EEPROM) on the board. Write down the values you select—you'll need them later. 281.3 Configuring Windows for Networking After clicking on the Network icon in the Control Panel, you select protocols, adapters (network boards), and services. The screens that appear depend on whether you're using Windows 95, Windows 98, or Windows NT. You must select TCP/IP as one of your protocols if you want to run an intranet. You must also install the Windows driver for your network board, ensuring that the IRQ and I/O address values match what you put into the board's EEPROM. You must also assign an IP address to each of your network boards. If you're not connected directly to the Internet, you can choose any unique address you want.
- 601 That's actually enough configuring for an intranet, but you'll probably want to use your network for sharing files and printers, too. For Windows NT, install Client And Server Services and bind them to TCP/IP. For Windows 95 and Windows 98, install Client For Microsoft Networks and File And Printer Sharing For Microsoft Networks. If you have an existing network with another protocol installed (Novell IPX/SPX or Microsoft NetBEUI, for example), you can continue to use that protocol on the network along with TCP/IP. In that case, Windows file and print sharing will use the existing protocol and your intranet will use TCP/IP. If you want one computer to share another computer's resources, you must enable sharing from Microsoft Windows Explorer (for disk directories) or from the Printers folder (for printers). 281.4 Host Names for an Intranet—The HOSTS File Both Internet and intranet users expect their browsers to use host names, not IP addresses. There are various methods of resolving names to addresses, including your own DNS server, which is an installable component of Windows NT Server. The easiest way of mapping Internet host names to IP addresses, however, is to use the HOSTS file. On Windows NT, this is a text file in the \Winnt\System32\DRIVERS\ETC directory. On Windows 95 and Windows 98, it's in the \WINDOWS directory, in a prototype HOSTS.SAM file that's already there. Just copy that file to HOSTS, and make the entries with Notepad. Make sure that you copy the edited HOSTS file to all computers in the network. 281.5 Testing Your Intranet—The Ping Program You can use the Windows Ping program to test your intranet. From the command line, type ping followed by the IP address (dotted-decimal format) or the host name of another computer on the network. If you get a positive response, you'll know that TCP/IP is configured correctly. If you get no response or an error message, proceed no further. Go back and troubleshoot your network connections and configuration. 281.6 An Intranet for One Computer—The TCP/IP Loopback Address The first line in the HOSTS file should be 127.0.0.1 localhost This is the standard loopback IP address. If you start a server program to listen on this address, client programs running on the same machine can connect to localhost to get a TCP/IP connection to the server program. This works whether or not you have network boards installed. 282. Winsock Winsock is the lowest level Windows API for TCP/IP programming. Part of the code is located in wsock32.dll (the exported functions that your program calls), and part is inside the Windows kernel. You can write both internet server programs and internet client programs using the Winsock API. This API is based on the original Berkely Sockets API for UNIX. A new and much more complex version, Winsock 2, is included for the first time with Windows NT 4.0, but we'll stick with the old version because it's the current standard for both Windows NT, Windows 95, and Windows 98. 282.1 Synchronous vs. Asynchronous Winsock Programming Winsock was introduced first for Win16, which did not support multithreading. Consequently, most developers used Winsock in the asynchronous mode. In that mode, all sorts of hidden windows and PeekMessage calls enabled single-threaded programs to make Winsock send and receive calls without blocking, thus keeping the user interface (UI) alive. Asynchronous Winsock programs were complex, often implementing "state machines" that processed callback functions, trying to figure out what to do next based on what had just happened. Well, we're not in 16-bit land anymore, so we can do modern multithreaded programming. If this scares you, go back and review Chapter 12. Once you get used to multithreaded programming, you'll love it. In this chapter, we will make the most of our Winsock calls from worker threads so that the program's main thread is able to carry on with the UI. The worker threads contain nice, sequential logic consisting of blocking Winsock calls. 282.2 The MFC Winsock Classes We try to use MFC classes where it makes sense to use them, but the MFC developers informed us that the CAsyncSocket and CSocket classes were not appropriate for 32-bit synchronous programming. The Visual C++ online help says you can use CSocket for synchronous programming, but if you look at the source code you'll see some ugly message-based code left over from Win16. 282.3 The Blocking Socket Classes Since we couldn't use MFC, we had to write our own Winsock classes. CBlockingSocket is a thin wrapping of the Winsock API, designed only for synchronous use in a worker thread. The only fancy features are exception-throwing on errors and time-outs for sending and receiving data. The exceptions help you write cleaner code because you don't need to have error tests after every Winsock call. The time-outs (implemented with the Winsock select function) prevent a communication fault from blocking a thread indefinitely. CHttpBlockingSocket is derived from CBlockingSocket and provides functions for reading HTTP data. CSockAddr and CBlockingSocketException are helper classes.
282.3.1
The CSockAddr Helper Class
- 602 Many Winsock functions take socket address parameters. As you might remember, a socket address consists of a 32-bit IP address plus a 16-bit port number. The actual Winsock type is a 16-byte sockaddr_in structure, which looks like this: struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; The IP address is stored as type in_addr, which looks like this: struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { u_short s_w1,s_w2; } S_un_w; u_long S_addr; } S_un; } These are ugly structures, so we'll derive a programmer-friendly C++ class from sockaddr_in. The file \vcpp32\ex34a\Blocksock.h on the CD-ROM contains the following code for doing this, with inline functions included: class CSockAddr : public sockaddr_in { public: // constructors CSockAddr() { sin_family = AF_INET; sin_port = 0; sin_addr.s_addr = 0; } // Default CSockAddr(const SOCKADDR& sa) { memcpy(this, &sa, sizeof(SOCKADDR)); } CSockAddr(const SOCKADDR_IN& sin) { memcpy(this, &sin, sizeof(SOCKADDR_IN)); } CSockAddr(const ULONG ulAddr, const USHORT ushPort = 0) // parms are host byte ordered { sin_family = AF_INET; sin_port = htons(ushPort); sin_addr.s_addr = htonl(ulAddr); } CSockAddr(const char* pchIP, const USHORT ushPort = 0) // dotted IP addr string { sin_family = AF_INET; sin_port = htons(ushPort); sin_addr.s_addr = inet_addr(pchIP); } // already network byte ordered // Return the address in dotted-decimal format CString DottedDecimal() { return inet_ntoa(sin_addr); } // constructs a new CString object // Get port and address (even though they're public) USHORT Port() const { return ntohs(sin_port); } ULONG IPAddr() const { return ntohl(sin_addr.s_addr); } // operators added for efficiency
- 603 const CSockAddr& operator=(const SOCKADDR& sa) { memcpy(this, &sa, sizeof(SOCKADDR)); return *this; } const CSockAddr& operator=(const SOCKADDR_IN& sin) { memcpy(this, &sin, sizeof(SOCKADDR_IN)); return *this; } operator SOCKADDR() { return *((LPSOCKADDR) this); } operator LPSOCKADDR() { return (LPSOCKADDR) this; } operator LPSOCKADDR_IN() { return (LPSOCKADDR_IN) this; } }; As you can see, this class has some useful constructors and conversion operators, which make the CSockAddr object interchangeable with the type sockaddr_in and the equivalent types SOCKADDR_IN, sockaddr, and SOCKADDR. There's a constructor and a member function for IP addresses in dotted-decimal format. The internal socket address is in network byte order, but the member functions all use host byte order parameters and return values. The Winsock functions htonl, htons, ntohs, and ntohl take care of the conversions between network and host byte order.
282.3.2
The CBlockingSocketException Class All the CBlockingSocket functions throw a CBlockingSocketException object when Winsock returns an error. This class is derived from the MFC CException class and thus overrides the GetErrorMessage function. This function gives the Winsock error number and a character string that CBlockingSocket provided when it threw the exception.
282.3.3
The CBlockingSocket Class Figure 34-9 shows an excerpt from the header file for the CBlockingSocket class. BLOCKSOCK.H class CBlockingSocket : public CObject { DECLARE_DYNAMIC(CBlockingSocket) public: SOCKET m_hSocket; CBlockingSocket(); { m_hSocket = NULL; } void Cleanup(); void Create(int nType = SOCK_STREAM); void Close(); void Bind(LPCSOCKADDR psa); void Listen(); void Connect(LPCSOCKADDR psa); BOOL Accept(CBlockingSocket& s, LPCSOCKADDR psa); int Send(const char* pch, const int nSize, const int nSecs); int Write(const char* pch, const int nSize, const int nSecs); int Receive(char* pch, const int nSize, const int nSecs); int SendDatagram(const char* pch, const int nSize, LPCSOCKADDR psa, const int nSecs); int ReceiveDatagram(char* pch, const int nSize, LPCSOCKADDR psa, const int nSecs); void GetPeerAddr(LPCSOCKADDR psa); void GetSockAddr(LPCSOCKADDR psa); static CSockAddr GetHostByName(const char* pchName, const USHORT ushPort = 0); static const char* GetHostByAddr(LPCSOCKADDR psa);
- 604 operator SOCKET(); { return m_hSocket; } }; Figure 34-9. Excerpt from the header file for the CBlockingSocketclass. Following is a list of the CBlockingSocket member functions, starting with the constructor: Constructor—The CBlockingSocket constructor makes an uninitialized object. You must call the Create member function to create a Windows socket and connect it to the C++ object. Create—This function calls the Winsock socket function and then sets the m_hSocket data member to the returned 32-bit SOCKET handle. Parameter
Description
nType
Type of socket; should be SOCK_STREAM (the default value) or SOCK_DGRAM
Close—This function closes an open socket by calling the Winsock closesocket function. The Create function must have been called previously. The destructor does not call this function because it would be impossible to catch an exception for a global object. Your server program can call Close anytime for a socket that is listening. Bind—This function calls the Winsock bind function to bind a previously created socket to a specified socket address. Prior to calling Listen, your server program calls Bind with a socket address containing the listening port number and server's IP address. If you supply INADDR_ANY as the IP address, Winsock deciphers your computer's IP address. Parameter
Description
psa
A CSockAddr object or a pointer to a variable of type sockaddr
Listen—This TCP function calls the Winsock listen function. Your server program calls Listen to begin listening on the port specified by the previous Bind call. The function returns immediately. Accept—This TCP function calls the Winsock accept function. Your server program calls Accept immediately after calling Listen. Accept returns when a client connects to the socket, sending back a new socket (in a CBlockingSocket object that you provide) that corresponds to the new connection. Parameter
Description
s
A reference to an existing CBlockingSocket object for which Create has not been called
psa
A CSockAddr object or a pointer to a variable of type sockaddr for the connecting socket's address
Return value
TRUE if successful
Connect—This TCP function calls the Winsock connect function. Your client program calls Connect after calling Create. Connect returns when the connection has been made. Parameter
Description
psa
A CSockAddr object or a pointer to a variable of type sockaddr
Send—This TCP function calls the Winsock send function after calling select to activate the time-out. The number of bytes actually transmitted by each Send call depends on how quickly the program at the other end of the connection can receive the bytes. Send throws an exception if the program at the other end closes the socket before it reads all the bytes. Parameter
Description
pch
A pointer to a buffer that contains the bytes to send
nSize
The size (in bytes) of the block to send
nSecs
Time-out value in seconds
Return value
The actual number of bytes sent
Write—This TCP function calls Send repeatedly until all the bytes are sent or until the receiver closes the socket. Parameter
Description
pch
A pointer to a buffer that contains the bytes to send
- 605 nSize
The size (in bytes) of the block to send
nSecs
Time-out value in seconds
Return value
The actual number of bytes sent
Receive—This TCP function calls the Winsock recv function after calling select to activate the time-out. This function returns only the bytes that have been received. For more information, see the description of the CHttpBlockingSocket class in the next section. Parameter
Description
pch
A pointer to an existing buffer that will receive the incoming bytes
nSize
The maximum number of bytes to receive
nSecs
Time-out value in seconds
Return value
The actual number of bytes received
SendDatagram—This UDP function calls the Winsock sendto function. The program on the other end needs to call ReceiveDatagram. There is no need to call Listen, Accept, or Connect for datagrams. You must have previously called Create with the parameter set to SOCK_DGRAM. Parameter
Description
pch
A pointer to a buffer that contains the bytes to send
nSize
The size (in bytes) of the block to send
psa
The datagram's destination address; a CSockAddr object or a pointer to a variable of type sockaddr
nSecs
Time-out value in seconds
Return value
The actual number of bytes sent
ReceiveDatagram—This UDP function calls the Winsock recvfrom function. The function returns when the program at the other end of the connection calls SendDatagram. You must have previously called Create with the parameter set to SOCK_DGRAM. Parameter
Description
pch
A pointer to an existing buffer that will receive the incoming bytes
nSize
The size (in bytes) of the block to send
psa
The datagram's destination address; a CSockAddr object or a pointer to a variable of type sockaddr
nSecs
Time-out value in seconds
Return value
The actual number of bytes received
GetPeerAddr—This function calls the Winsock getpeername function. It returns the port and IP address of the socket on the other end of the connection. If you are connected to the Internet through a Web proxy server, the IP address is the proxy server's IP address. Parameter
Description
psa
A CSockAddr object or a pointer to a variable of type sockaddr
GetSockAddr—This function calls the Winsock getsockname function. It returns the socket address that Winsock assigns to this end of the connection. If the other program is a server on a LAN, the IP address is the address assigned to this computer's network board. If the other program is a server on the Internet, your service provider assigns the IP address when you dial in. In both cases, Winsock assigns the port number, which is different for each connection. Parameter
Description
- 606 psa
A CSockAddr object or a pointer to a variable of type sockaddr
GetHostByName—This static function calls the Winsock function gethostbyname. It queries a name server and then returns the socket address corresponding to the host name. The function times out by itself. Parameter
Description
pchName
A pointer to a character array containing the host name to resolve
ushPort
The port number (default value 0) that will become part of the returned socket address
Return value
The socket address containing the IP address from the DNS plus the port number ushPort
GetHostByAddr—This static function calls the Winsock gethostbyaddr function. It queries a name server and then returns the host name corresponding to the socket address. The function times out by itself. Parameter
Description
psa
A CSockAddr object or a pointer to a variable of type sockaddr
Return value
A pointer to a character array containing the host name; the caller should not delete this memory
Cleanup—This function closes the socket if it is open. It doesn't throw an exception, so you can call it inside an exception catch block. operator SOCKET—This overloaded operator lets you use a CBlockingSocket object in place of a SOCKET parameter.
282.3.4
The CHttpBlockingSocket Class If you call CBlockingSocket::Receive, you'll have a difficult time knowing when to stop receiving bytes. Each call returns the bytes that are stacked up at your end of the connection at that instant. If there are no bytes, the call blocks, but if the sender closed the socket, the call returns zero bytes. In the HTTP section, you learned that the client sends a request terminated by a blank line. The server is supposed to send the response headers and data as soon as it detects the blank line, but the client needs to analyze the response headers before it reads the data. This means that as long as a TCP connection remains open, the receiving program must process the received data as it comes in. A simple but inefficient technique would be to call Receive for 1 byte at a time. A better way is to use a buffer. The CHttpBlockingSocket class adds buffering to CBlockingSocket, and it provides two new member functions. Here is part of the \vcpp32\ex34A\Blocksock.h file: class CHttpBlockingSocket : public CBlockingSocket { public: DECLARE_DYNAMIC(CHttpBlockingSocket) enum {nSizeRecv = 1000}; // max receive buffer size (> hdr line // length) CHttpBlockingSocket(); ~CHttpBlockingSocket(); int ReadHttpHeaderLine(char* pch, const int nSize, const int nSecs); int ReadHttpResponse(char* pch, const int nSize, const int nSecs); private: char* m_pReadBuf; // read buffer int m_nReadBuf; // number of bytes in the read buffer }; The constructor and destructor take care of allocating and freeing a 1000-character buffer. The two new member functions are as follows: ReadHttpHeaderLine—This function returns a single header line, terminated with a
Description
pch
A pointer to an existing buffer that will receive the incoming line (zero-terminated)
nSize
The size of the pch buffer
- 607 nSecs
Time-out value in seconds
Return value
The actual number of bytes received, excluding the terminating zero
ReadHttpResponse—This function returns the remainder of the server's response received when the socket is closed or when the buffer is full. Don't assume that the buffer contains a terminating zero. Parameter
Description
pch
A pointer to an existing buffer that will receive the incoming data
nSize
The maximum number of bytes to receive
nSecs
Time-out value in seconds
Return value
The actual number of bytes received
282.4 A Simplified HTTP Server Program Now it's time to use the blocking socket classes to write an HTTP server program. All the frills have been eliminated, but the code actually works with a browser. This server doesn't do much except return some hard-coded headers and HTML statements in response to any GET request. (See the EX34A program later in this chapter for a more complete HTTP server.) 282.4.1 Initializing Winsock Before making any Winsock calls, the program must initialize the Winsock library. The following statements in the application's InitInstance member function do the job: WSADATA wsd; WSAStartup(0x0101, &wsd); 282.4.2 Starting the Server The server starts in response to some user action, such as a menu choice. Here's the command handler: CBlockingSocket g_sListen; // one-and-only global socket for listening void CSocketView::OnInternetStartServer() { try { CSockAddr saServer(INADDR_ANY, 80); g_sListen.Create(); g_sListen.Bind(saServer); g_sListen.Listen(); AfxBeginThread(ServerThreadProc, GetSafeHwnd()); } catch(CBlockingSocketException* e) { g_sListen.Cleanup(); // Do something about the exception e->Delete(); } } Pretty simple, really. The handler creates a socket, starts listening on it, and then starts a worker thread that waits for some client to connect to port 80. If something goes wrong, an exception is thrown. The global g_sListen object lasts for the life of the program and is capable of accepting multiple simultaneous connections, each managed by a separate thread. 282.4.3 The Server Thread Now let's look at the ServerThreadProc function: UINT ServerThreadProc(LPVOID pParam) { CSockAddr saClient; CHttpBlockingSocket sConnect; char request[100]; char headers[] = "HTTP/1.0 200 OK\r\n" "Server: Inside Visual C++ SOCK01\r\n" "Date: Thu, 05 Sep 1996 17:33:12 GMT\r\n" "Content-Type: text/html\r\n"
- 608 "Accept-Ranges: bytes\r\n" "Content-Length: 187\r\n" "\r\n"; // the important blank line char html[] = "
- 609 A problem might arise if a thread were in the process of fulfilling a client request. In that case, the main thread should positively ensure that all threads have terminated before exiting. 282.5 A Simplified HTTP Client Program Now for the client side of the story—a simple working program that does a blind GET request. When a server receives a GET request with a slash, as shown below, it's supposed to deliver its default HTML file: GET / HTTP/1.0 If you typed http://www.slowsoft.com in a browser, the browser sends the blind GET request. This client program can use the same CHttpBlockingSocket class you've already seen, and it must initialize Winsock the same way the server did. A command handler simply starts a client thread with a call like this: AfxBeginThread(ClientSocketThreadProc, GetSafeHwnd()); Here's the client thread code: CString g_strServerName = "localhost"; // or some other host name UINT ClientSocketThreadProc(LPVOID pParam) { CHttpBlockingSocket sClient; char* buffer = new char[MAXBUF]; int nBytesReceived = 0; char request[] = "GET / HTTP/1.0\r\n"; char headers[] = // Request headers "User-Agent: Mozilla/1.22 (Windows; U; 32bit)\r\n" "Accept: */*\r\n" "Accept: image/gif\r\n" "Accept: image/x-xbitmap\r\n" "Accept: image/jpeg\r\n" "\r\n"; // need this CSockAddr saServer, saClient; try { sClient.Create(); saServer = CBlockingSocket::GetHostByName(g_strServerName, 80); sClient.Connect(saServer); sClient.Write(request, strlen(request), 10); sClient.Write(headers, strlen(headers), 10); do { // Read all the server's response headers nBytesReceived = sClient.ReadHttpHeaderLine(buffer, 100, 10); } while(strcmp(buffer, "\r\n")); // through the first blank line nBytesReceived = sClient.ReadHttpResponse(buffer, 100, 10); if(nBytesReceived == 0) { AfxMessageBox("No response received"); } else { buffer[nBytesReceived] = `\0'; AfxMessageBox(buffer); } } catch(CBlockingSocketException* e) { // Log the exception e->Delete(); } sClient.Close(); delete [] buffer; return 0; // The thread exits } This thread first calls CBlockingSocket::GetHostByName to get the server computer's IP address. Then it creates a socket and calls Connect on that socket. Now there's a two-way communication channel to the server. The thread sends its GET request followed by some request headers, reads the server's response headers, and then reads the response file itself, which it assumes is a text file. After the thread displays the text in a message box, it exits.
- 610 -
283.
Building a Web Server with CHttpBlockingSocket If you need a Web server, your best bet is to buy one or to use the Microsoft Internet Information Server (IIS) that comes bundled with Windows NT Server. Of course, you'll learn more if you build your own server and you'll also have a useful diagnostic tool. And what if you need features that IIS can't deliver? Suppose you want to add Web server capability to an existing Windows application, or suppose you have a custom ActiveX control that sets up its own non-HTTP TCP connection with the server. Take a good look at the server code in EX34A, which works under Windows NT, Windows 95, and Windows 98. It might work as a foundation for your next custom server application. 283.1 EX34A Server Limitations The server part of the EX34A program honors GET requests for files, and it has logic for processing POST requests. (POST requests are described in Chapter 35.) These are the two most common HTTP request types. EX34A will not, however, launch Common Gateway Interface (CGI) scripts or load Internet Server Application Programming Interface (ISAPI) DLLs. (You'll learn more about ISAPI in Chapter 35.) EX34A makes no provision for security, and it doesn't have FTP capabilities. Other than that, it's a great server! If you want the missing features, just write the code for them yourself. 283.2 EX34A Server Architecture You'll soon see that EX34A combines an HTTP server, a Winsock HTTP client, and two WinInet HTTP clients. All three clients can talk to the built-in server or to any other server on the Internet. Any client program, including the Telnet utility and standard browsers such as Microsoft Internet Explorer 4.0, can communicate with the EX34A server. You'll examine the client sections a little later in this chapter. EX34A is a standard MFC SDI document-view application with a view class derived from CEditView. The main menu includes Start Server and Stop Server menu choices as well as a Configuration command that brings up a tabbed dialog for setting the home directory, the default file for blind GETs, and the listening port number (usually 80). The Start Server command handler starts a global socket listening and then launches a thread, as in the simplified HTTP server described previously. Look at the ServerThreadProc function included in the file \vcpp32\ex34a\ServerThread.cpp of the EX34A project on the companion CD-ROM. Each time a server thread processes a request, it logs the request by sending a message to the CEditView window. It also sends messages for exceptions, such as bind errors. The primary job of the server is to deliver files. It first opens a file, storing a CFile pointer in pFile, and then it reads 5 KB (SERVERMAXBUF) blocks and writes them to the socket sConnect, as shown in the code below: char* buffer = new char[SERVERMAXBUF]; DWORD dwLength = pFile->GetLength(); nBytesSent = 0; DWORD dwBytesRead = 0; UINT uBytesToRead; while(dwBytesRead < dwLength) { uBytesToRead = min(SERVERMAXBUF, dwLength - dwBytesRead); VERIFY(pFile->Read(buffer, uBytesToRead) == uBytesToRead); nBytesSent += sConnect.Write(buffer, uBytesToRead, 10); dwBytesRead += uBytesToRead; } The server is programmed to respond to a GET request for a phony file named Custom. It generates some HTML code that displays the client's IP address, port number, and a sequential connection number. This is one possibility for server customization. The server normally listens on a socket bound to address INADDR_ANY. This is the server's default IP address determined by the Ethernet board or assigned during your connection to your ISP. If your server computer has several IP addresses, you can force the server to listen to one of them by filling in the Server IP Address in the Advanced Configuration page. You can also change the server's listening port number on the Server page. If you choose port 90, for example, browser users would connect to http://localhost:90. The leftmost status bar indicator pane displays "Listening" when the server is running. 283.3 Using the Win32 TransmitFile Function If you have Windows NT 4.0, you can make your server more efficient by using the Win32 TransmitFile function in place of the CFile::Read loop in the code excerpt shown. TransmitFile sends bytes from an open file directly to a socket and is highly optimized. The EX34A ServerThreadProc function contains the following line: if (::TransmitFile(sConnect, (HANDLE) pFile >m_hFile, dwLength, 0, NULL, NULL, TF_DISCONNECT)) If you have Windows NT, uncomment the line #define USE_TRANSMITFILE
- 611 at the top of ServerThread.cpp to activate the TransmitFile logic. 283.4 Building and Testing EX34A Open the \vcpp32\ex34a project in Visual C++, and then build the project. A directory under EX34A, called Website, contains some HTML files and is set up as the EX34A server's home directory, which appears to clients as the server's root directory. If you have another HTTP server running on your computer, stop it now. If you have installed IIS along with Windows NT Server, it is probably running now, so you must run the Internet Service Manager program from the Microsoft Internet Server menu. Select the WWW Service line, and then click the stop button (the one with the square). EX34A reports a bind error (10048) if another server is already listening on port 80. Run the program from the debugger, and then choose Start Server from the Internet menu. Now go to your Web browser and type localhost. You should see the Welcome To The Inside Visual C++ Home Page complete with all graphics. The EX34A window should look like this.
Look at the Visual C++ debug window for a listing of the client's request headers. If you click the browser's Refresh button, you might notice EX34A error messages like this: WINSOCK ERROR--SERVER: Send error #10054 -- 10/05/96 04:34:10 GMT This tells you that the browser read the file's modified date from the server's response header and figured out that it didn't need the data because it already had the file in its cache. The browser then closed the socket, and the server detected an error. If the EX34A server were smarter, it would have checked the client's If-Modified-Since request header before sending the file. Of course, you can test the server on your $99 intranet. Start the server on one computer, and then run the browser from another, typing in the server's host name as it appears in the HOSTS file. 283.5 Using Telnet The Telnet utility is included with Windows 95, Windows 98, and Windows NT. It's useful for testing server programs such as EX34A. With Telnet, you're sending one character at a time, which means that the server's CBlockingSocket::Receive function is receiving one character at a time. The Telnet window is shown here.
The first time you run Telnet, choose Preferences from the Terminal menu and turn on Local Echo. Each time thereafter, choose Remote System from the Connect menu and then type your server name and port number 80. You can type a GET request (followed by a double carriage return), but you'd better type fast because the EX34A server's Receive calls are set to time-out after 10 seconds.
284.
Building a Web Client with CHttpBlockingSocket If you had written your own Internet browser program a few years ago, you could have made a billion dollars by now. But these days, you can download browsers for free, so it doesn't make sense to write one. It does make sense, however, to add Internet access features to your Windows applications. Winsock is not the best tool if you need HTTP or FTP access only, but it's a good learning tool.
- 612 284.1 The EX34A Winsock Client The EX34A program implements a Winsock client in the file \vcpp32\ex34a\ClientSockThread.cpp on the CD-ROM. The code is similar to the code for the simplified HTTP client. The client thread uses global variables set by the Configuration property sheet, including server filename, server host name, server IP address and port, and client IP address. The client IP address is necessary only if your computer supports multiple IP addresses. When you run the client, it connects to the specified server and issues a GET request for the file that you specified. The Winsock client logs error messages in the EX34A main window. 284.2 EX34A Support for Proxy Servers If your computer is connected to a LAN at work, chances are it's not exposed directly to the Internet but rather connected through a proxy server, sometimes called a firewall. There are two kinds of proxy servers: Web and Winsock. Web proxy servers, sometimes called CERN proxies, support only the HTTP, FTP, and gopher protocols. (The gopher protocol, which predates HTTP, allows character-mode terminals to access Internet files.) A Winsock client program must be specially adapted to use a Web proxy server. A Winsock proxy server is more flexible and thus can support protocols such as RealAudio. Instead of modifying your client program source code, you link to a special Remote Winsock DLL that can communicate with a Winsock proxy server. The EX34A client code can communicate through a Web proxy if you check the Use Proxy check box in the Client Configuration page. In that case, you must know and enter the name of your proxy server. From that point on, the client code connects to the proxy server instead of to the real server. All GET and POST requests must then specify the full Uniform Resource Locator (URL) for the file. If you were connected directly to SlowSoft's server, for example, your GET request might look like this: GET /customers/newproducts.html HTTP/1.0 But if you were connected through a Web proxy server, the GET would look like this: GET http://slowsoft.com/customers/newproducts.html HTTP/1.0 284.3 Testing the EX34A Winsock Client The easiest way to test the Winsock client is by using the built-in Winsock server. Just start the server as before, and then choose Request (Winsock) from the Internet menu. You should see some HTML code in a message box. You can also test the client against IIS, the server running in another EX34A process on the same computer, the EX34A server running on another computer on the Net, and an Internet server. Ignore the "Address" URL on the dialog bar for the time being; it's for one of the WinInet clients. You must enter the server name and filename in the Client page of the Configuration dialog. 285. WinInet WinInet is a higher-level API than Winsock, but it works only for HTTP, FTP, and gopher client programs in both asynchronous and synchronous modes. You can't use it to build servers. The WININET DLL is independent of the WINSOCK32 DLL. Microsoft Internet Explorer 3.0 (IE3) uses WinInet, and so do ActiveX controls. 285.1 WinInet's Advantages over Winsock WinInet far surpasses Winsock in the support it gives to a professional-level client program. Following are just some of the WinInet benefits: Caching—Just like IE3, your WinInet client program caches HTML files and other Internet files. You don't have to do a thing. The second time your client requests a particular file, it's loaded from a local disk instead of from the Internet. Security—WinInet supports basic authentication, Windows NT challenge/response authentication, and the Secure Sockets Layer (SSL). Authentication is described in Chapter 35. Web proxy access—You enter proxy server information through the Control Panel (click on the Internet icon), and it's stored in the Registry. WinInet reads the Registry and uses the proxy server when required. Buffered I/O—WinInet's read function doesn't return until it can deliver the number of bytes you asked for. (It returns immediately, of course, if the server closes the socket.) Also, you can read individual text lines if you need to. Easy API—Status callback functions are available for UI update and cancellation. One function, CInternetSession::OpenURL, finds the server's IP address, opens a connection, and makes the file ready for reading, all in one call. Some functions even copy Internet files directly to and from disk. User friendly—WinInet parses and formats headers for you. If a server has moved a file to a new location, it sends back the new URL in an HTTP Location header. WinInet seamlessly accesses the new server for you. In addition, WinInet puts a file's modified date in the request header for you. 285.2 The MFC WinInet Classes WinInet is a modern API available only for Win32. The MFC wrapping is quite good, which means we didn't have to write our own WinInet class library. Yes, MFC WinInet supports blocking calls in multithreaded programs, and by now you know that makes us happy.
- 613 The MFC classes closely mirror the underlying WinInet architecture, and they add exception processing. These classes are summarized in the sections on the following pages. 285.2.1 CInternetSession You need only one CInternetSession object for each thread that accesses the Internet. After you have your CInternetSession object, you can establish HTTP, FTP, or gopher connections or you can open remote files directly by calling the OpenURL member function. You can use the CInternetSession class directly, or you can derive a class from it in order to support status callback functions. The CInternetSession constructor calls the WinInet InternetOpen function, which returns an HINTERNET session handle that is stored inside the CInternetSession object. This function initializes your application's use of the WinInet library, and the session handle is used internally as a parameter for other WinInet calls. 285.2.2 CHttpConnection An object of class CHttpConnection represents a "permanent" HTTP connection to a particular host. You know already that HTTP doesn't support permanent connections and that FTP doesn't either. (The connections last only for the duration of a file transfer.) WinInet gives the appearance of a permanent connection because it remembers the host name. After you have your CInternetSession object, you call the GetHttpConnection member function, which returns a pointer to a CHttpConnection object. (Don't forget to delete this object when you are finished with it.) The GetHttpConnection member function calls the WinInet InternetConnect function, which returns an HINTERNET connection handle that is stored inside the CHttpConnection object and used for subsequent WinInet calls.
285.2.3
CFtpConnection, CGopherConnection These classes are similar to CHttpConnection, but they use the FTP and gopher protocols. The CFtpConnection member functions GetFile and PutFile allow you to transfer files directly to and from your disk. 285.2.4 CInternetFile With HTTP, FTP, or gopher, your client program reads and writes byte streams. The MFC WinInet classes make these byte streams look like ordinary files. If you look at the class hierarchy, you'll see that CInternetFile is derived from CStdioFile, which is derived from CFile. Therefore, CInternetFile and its derived classes override familiar CFile functions such as Read and Write. For FTP files, you use CInternetFile objects directly, but for HTTP and gopher files, you use objects of the derived classes CHttpFile and CGopherFile. You don't construct a CInternetFile object directly, but you call CFtpConnection::OpenFile to get a CInternetFile pointer. If you have an ordinary CFile object, it has a 32-bit HANDLE data member that represents the underlying disk file. A CInternetFile object uses the same m_hFile data member, but that data member holds a 32-bit Internet file handle of type HINTERNET, which is not interchangeable with a HANDLE. The CInternetFile overridden member functions use this handle to call WinInet functions such as InternetReadFile and InternetWriteFile. 285.2.5 CHttpFile This Internet file class has member functions that are unique to HTTP files, such as AddRequestHeaders, SendRequest, and GetFileURL. You don't construct a CHttpFile object directly, but you call the CHttpConnection::OpenRequest function, which calls the WinInet function HttpOpenRequest and returns a CHttpFile pointer. You can specify a GET or POST request for this call. Once you have your CHttpFile pointer, you call the CHttpFile::SendRequest member function, which actually sends the request to the server. Then you call Read.
285.2.6
CFtpFileFind, CGopherFileFind These classes let your client program explore FTP and gopher directories. 285.2.7 CInternetException The MFC WinInet classes throw CInternetException objects that your program can process with try/catch logic. 285.3 Internet Session Status Callbacks WinInet and MFC provide callback notifications as a WinInet operation progresses, and these status callbacks are available in both synchronous (blocking) and asynchronous modes. In synchronous mode (which we're using exclusively here), your WinInet calls block even though you have status callbacks enabled. Callbacks are easy in C++. You simply derive a class and override selected virtual functions. The base class for WinInet is CInternetSession. Now let's derive a class named CCallbackInternetSession: class CCallbackInternetSession : public CInternetSession { public: CCallbackInternetSession( LPCTSTR pstrAgent = NULL, DWORD dwContext = 1, DWORD dwAccessType = PRE_CONFIG_INTERNET_ACCESS, LPCTSTR pstrProxyName = NULL, LPCTSTR pstrProxyBypass = NULL, DWORD dwFlags = 0 ) { EnableStatusCallback() }
- 614 protected: virtual void OnStatusCallback(DWORD dwContext, DWORD dwInternalStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength); }; The only coding that's necessary is a constructor and a single overridden function, OnStatusCallback. The constructor calls CInternetSession::EnableStatusCallback to enable the status callback feature. Your WinInet client program makes its various Internet blocking calls, and when the status changes, OnStatusCallback is called. Your overridden function quickly updates the UI and returns, and then the Internet operation continues. For HTTP, most of the callbacks originate in the CHttpFile::SendRequest function. What kind of events trigger callbacks? A list of the codes passed in the dwInternalStatus parameter is shown here. Code Passed
Action Taken
INTERNET_STATUS_RESOLVING_NAME
Looking up the IP address of the supplied name. The name is now in lpvStatusInformation.
INTERNET_STATUS_NAME_RESOLVED
Successfully found the IP address. The IP address is now in lpvStatusInformation.
INTERNET_STATUS_CONNECTING_TO_SERVER
Connecting to the socket.
INTERNET_STATUS_CONNECTED_TO_SERVER
Successfully connected to the socket.
INTERNET_STATUS_SENDING_REQUEST
Send the information request to the server.
INTERNET_STATUS_REQUEST_SENT
Successfully sent the information request to the server.
INTERNET_STATUS_RECEIVING_RESPONSE
Waiting for the server to respond to a request.
INTERNET_STATUS_RESPONSE_RECEIVED
Successfully received a response from the server.
INTERNET_STATUS_CLOSING_CONNECTION
Closing the connection to the server.
INTERNET_STATUS_CONNECTION_CLOSED
Successfully closed the connection to the server.
INTERNET_STATUS_HANDLE_CREATED
Program can now close the handle.
INTERNET_STATUS_HANDLE_CLOSING
Successfully terminated this handle value.
INTERNET_STATUS_REQUEST_COMPLETE
Successfully completed the asynchronous operation.
You can use your status callback function to interrupt a WinInet operation. You could, for example, test for an event set by the main thread when the user cancels the operation. 285.4 A Simplified WinInet Client Program And now for the WinInet equivalent of our Winsock client program that implements a blind GET request. Because you're using WinInet in blocking mode, you must put the code in a worker thread. That thread is started from a command handler in the main thread: AfxBeginThread(ClientWinInetThreadProc, GetSafeHwnd()); Here's the client thread code: CString g_strServerName = "localhost"; // or some other host name UINT ClientWinInetThreadProc(LPVOID pParam) { CInternetSession session; CHttpConnection* pConnection = NULL; CHttpFile* pFile1 = NULL; char* buffer = new char[MAXBUF]; UINT nBytesRead = 0; try { pConnection = session.GetHttpConnection(g_strServerName, 80); pFile1 = pConnection->OpenRequest(1, "/"); // blind GET pFile1->SendRequest(); nBytesRead = pFile1->Read(buffer, MAXBUF - 1);
- 615 buffer[nBytesRead] = `\0'; // necessary for message box char temp[10]; if(pFile1->Read(temp, 10) != 0) { // makes caching work if read complete AfxMessageBox("File overran buffer — not cached"); } AfxMessageBox(buffer); } catch(CInternetException* e) { // Log the exception e->Delete(); } if(pFile1) delete pFile1; if(pConnection) delete pConnection; delete [] buffer; return 0; } The second Read call needs some explanation. It has two purposes. If the first Read doesn't read the whole file, that means that it was longer than MAXBUF -1. The second Read will get some bytes, and that lets you detect the overflow problem. If the first Read reads the whole file, you still need the second Read to force WinInet to cache the file on your hard disk. Remember that WinInet tries to read all the bytes you ask it to—through the end of the file. Even so, you need to read 0 bytes after that. 286. Building a Web Client with the MFC WinInet Classes There are two ways to build a Web client with WinInet. The first method, using the CHttpConnection class, is similar to the simplified WinInet client on the preceding page. The second method, using CInternetSession::OpenURL, is even easier. We'll start with the CHttpConnection version. 286.1 The EX34A WinInet Client #1—Using CHttpConnection The EX34A program implements a WinInet client in the file \vcpp32\ex34a\ClientInetThread.cpp on the CD-ROM. Besides allowing the use of an IP address as well as a host name, the program uses a status callback function. That function, CCallbackInternetSession::OnStatusCallback in the file \vcpp32\ex34a\utility.cpp, puts a text string in a global variable g_pchStatus, using a critical section for synchronization. The function then posts a user-defined message to the application's main window. The message triggers an Update Command UI handler (called by CWinApp::OnIdle), which displays the text in the second status bar text pane. 286.2 Testing the WinInet Client #1 To test the WinInet client #1, you can follow the same procedure you used to test the Winsock client. Note the status bar messages as the connection is made. Note that the file appears more quickly the second time you request it. 286.3 The EX34A WinInet Client #2—Using OpenURL The EX34A program implements a different WinInet client in the file ClientUrlThread.cpp on the companion CDROM. This client uses the "Address" URL (that you type to access the Internet site). Here's the actual code: CString g_strURL = "http:// "; UINT ClientUrlThreadProc(LPVOID pParam) { char* buffer = new char[MAXBUF]; UINT nBytesRead = 0; CInternetSession session; // can't get status callbacks for OpenURL CStdioFile* pFile1 = NULL; // could call ReadString to get 1 line try { pFile1 = session.OpenURL(g_strURL, 0, INTERNET_FLAG_TRANSFER_BINARY |INTERNET_FLAG_KEEP_CONNECTION); // If OpenURL fails, we won't get past here nBytesRead = pFile1->Read(buffer, MAXBUF - 1); buffer[nBytesRead] = `\0'; // necessary for message box char temp[100]; if(pFile1->Read(temp, 100) != 0) {
- 616 // makes caching work if read complete AfxMessageBox("File overran buffer — not cached"); } ::MessageBox(::GetTopWindow(::GetDesktopWindow()), buffer, "URL CLIENT", MB_OK); } catch(CInternetException* e) { LogInternetException(pParam, e); e->Delete(); } if(pFile1) delete pFile1; delete [] buffer; return 0; } Note that OpenURL returns a pointer to a CStdioFile object. You can use that pointer to call Read as shown, or you can call ReadString to get a single line. The file class does all the buffering. As in the previous WinInet client, it's necessary to call Read a second time to cache the file. The OpenURL INTERNET_FLAG_KEEP_CONNECTION parameter is necessary for Windows NT challenge/response authentication, which is described in Chapter 35. If you added the flag INTERNET_FLAG_RELOAD, the program would bypass the cache just as the browser does when you click the Refresh button. 286.4 Testing the WinInet Client #2 You can test the WinInet client #2 against any HTTP server. You run this client by typing in the URL address, not by using the menu. You must include the protocol (http:// or ftp://) in the URL address. Type http://localhost. You should see the same HTML code in a message box. No status messages appear here because the status callback doesn't work with OpenURL. 287. Asynchronous Moniker Files Just when you thought you knew all the ways to download a file from the Internet, you're going to learn about another one. With asynchronous moniker files, you'll be doing all your programming in your application's main thread without blocking the user interface. Sounds like magic, doesn't it? The magic is inside the Windows URLMON DLL, which depends on WinInet and is used by Microsoft Internet Explorer. The MFC CAsyncMonikerFile class makes the programming easy, but you should know a little theory first. 287.1 Monikers A moniker is a "surrogate" COM object that holds the name (URL) of the "real" object, which could be an embedded component but more often is just an Internet file (HTML, JPEG, GIF, and so on). Monikers implement the IMoniker interface, which has two important member functions: BindToObject and BindToStorage. The BindToObject function puts an object into the running state, and the BindToStorage function provides an IStream or an IStorage pointer from which the object's data can be read. A moniker has an associated IBindStatusCallback interface with member functions such as OnStartBinding and OnDataAvailable, which are called during the process of reading data from a URL. The callback functions are called in the thread that created the moniker. This means that the URLMON DLL must set up an invisible window in the calling thread and send the calling thread messages from another thread, which uses WinInet functions to read the URL. The window's message handlers call the callback functions. 287.2 The MFC CAsyncMonikerFile Class Fortunately, MFC can shield you from the COM interfaces described above. The CAsyncMonikerFile class is derived from CFile, so it acts like a regular file. Instead of opening a disk file, the class's Open member function gets an IMoniker pointer and encapsulates the IStream interface returned from a call to BindToStorage. Furthermore, the class has virtual functions that are tied to the member functions of IBindStatusCallback. Using this class is a breeze; you construct an object or a derived class and call the Open member function, which returns immediately. Then you wait for calls to overridden virtual functions such as OnProgress and OnDataAvailable, named, not coincidentally, after their IBindStatusCallback equivalents. 287.3 Using the CAsyncMonikerFile Class in a Program Suppose your application downloads data from a dozen URLs but has only one class derived from CAsyncMonikerFile. The overridden callback functions must figure out where to put the data. That means you must associate each derived class object with some UI element in your program. The steps listed below illustrate one of many ways to do this. Suppose you want to list the text of an HTML file in an edit control that's part of a form view. This is what you can do: Use ClassWizard to derive a class from CAsyncMonikerFile.
- 617 Add a character pointer data member m_buffer. Invoke new for this pointer in the constructor; invoke delete in the destructor. Add a public data member m_edit of class CEdit. Override the OnDataAvailable function thus: void CMyMonikerFile::OnDataAvailable(DWORD dwSize, DWORD bscfFlag) { try { UINT nBytesRead = Read(m_buffer, MAXBUF - 1); TRACE("nBytesRead = %d\n", nBytesRead); m_buffer[nBytesRead] = `\0'; // necessary for edit control // The following two lines add text to the edit control m_edit.SendMessage(EM_SETSEL, (WPARAM) 999999, 1000000); m_edit.SendMessage(EM_REPLACESEL, (WPARAM) 0, (LPARAM) m_buffer); } catch(CFileException* pe) { TRACE("File exception %d\n, pe->m_cause"); pe->Delete(); } } Embed an object of your new moniker file class in your view class. In you view's OnInitialUpdate function, attach the CEdit member to the edit control like this: m_myEmbeddedMonikerFile.m_edit.SubClassDlgItem(ID_MYEDIT, this); In your view class, open the moniker file like this: m_myEmbeddedMonikerFile.Open("http://host/filename"); For a large file, OnDataAvailable will be called several times, each time adding text to the edit control. If you override OnProgress or OnStopBinding in your derived moniker file class, your program can be alerted when the transfer is finished. You can also check the value of bscfFlag in OnDataAvailable to determine whether the transfer is completed. Note that everything here is in your main thread and—most important—the moniker file object must exist for as long as the transfer is in progress. That's why it's a data member of the view class. 287.4 Asynchronous Moniker Files vs. WinInet Programming In the WinInet examples earlier in this chapter, you started a worker thread that made blocking calls and sent a message to the main thread when it was finished. With asynchronous moniker files, the same thing happens—the transfer takes place in another thread, which sends messages to the main thread. You just don't see the other thread. There is one very important difference, however, between asynchronous moniker files and WinInet programming: with blocking WinInet calls, you need a separate thread for each transfer; with asynchronous moniker files, only one extra thread handles all transfers together. For example, if you're writing a browser that must download 50 bitmaps simultaneously, using asynchronous moniker files saves 49 threads, which makes the program much more efficient. Of course, you have some extra control with WinInet, and it's easier to get information from the response headers, such as total file length. Your choice of programming tools, then, depends on your application. The more you know about your options, the better your choice will be. Chapter Thirty-Five 288. Programming the Microsoft Internet Information Server In Chapter 34, you used a "homemade" Web based on the Winsock APIs. In this chapter, you'll learn how to use and extend Microsoft Internet Information Server (IIS) 4.0, which is bundled with Microsoft Windows NT Server 4.0. IIS is actually three separate servers—one for HTTP (for the World Wide Web), one for FTP, and one for gopher. This chapter tells you how to write HTTP server extensions using the Microsoft IIS application programming interface (ISAPI) that is part of Microsoft ActiveX technology. You'll examine two kinds of extensions: an ISAPI server extension and an ISAPI filter, both of which are DLLs. An ISAPI server extension can perform Internet business transactions such as order entry. An ISAPI filter intercepts data traveling to and from the server and thus can perform specialized logging and other tasks. 289. IIS Alternatives The exercises in this chapter assume that you have Windows NT Server 4.0 and IIS. If you are running Windows NT Workstation, you can use Peer Web Services, which supports fewer connections and doesn't allow virtual servers. If you are running Microsoft Windows 95 or Windows 98, you can use Personal Web Server, which is packaged with
- 618 Microsoft FrontPage. Internet Information Server, Peer Web Services, and Personal Web Server can all use ISAPI extension DLLs. See your server's documentation for operating details. 290. Microsoft IIS Microsoft IIS is a high-performance Internet/intranet server that takes advantage of Windows NT features such as I/O completion ports, the Win32 function TransmitFile, file-handle caching, and CPU scaling for threads. 290.1 Installing and Controlling IIS When you install Windows NT Server 4.0, you are given the option of installing IIS. If you selected IIS at setup, the server will be running whenever Windows NT is running. IIS is a special kind of Win32 program called a service (actually three services—WWW, HTTP, and gopher—in one program called inetinfo.exe), which won't appear in the taskbar. You can control IIS from the Services icon in the Control Panel, but you'll probably want to use the Internet Service Manager program instead. 290.2 Running Internet Service Manager You can run Internet Service Manager from the Microsoft Internet Server menu that's accessible on the Start menu. You can also run an HTML-based version of Internet Service Manager remotely from a browser. That version allows you to change service parameters, but it won't let you turn services on and off. Figure 35-1 shows the Microsoft Internet Service Manager screen with the World Wide Web (WWW) running and FTP services stopped. You can select a service by clicking on its icon at the left. The triangle and square buttons on the toolbar of the screen allow you to turn the selected service on or off.
Figure 35-1. The Microsoft Internet Service Manager screen. 290.2.1 IIS Security After you double-click on the WWW service icon of the Microsoft Internet Service Manager screen, you'll see a property sheet. The Service page lets you configure IIS security. When a client browser requests a file, the server impersonates a local user for the duration of the request and that user name determines which files the client can access. Which local user does the server impersonate? Most often, it's the one you see in the Username field, shown in Figure 35-2.
- 619 -
Figure 35-2. The WWW Service Properties screen. Most Web page visitors don't supply a user name and password, so they are considered anonymous users. Those users have the same rights they would have if they had logged on to your server locally as IUSR_MYMACHINENAME. That means that IUSR_MYMACHINENAME must appear in the list of users that is displayed when you run User Manager or User Manager For Domains (from the Administrative Tools menu), and the passwords must match. The IIS Setup program normally defines this anonymous user for you. You can define your own WWW anonymous user name, but you must be sure that the entry on the Service page matches the entry in the computer's (or Windows NT domain's) user list. Note also the Password Authentication options. For the time being, stick to the Allow Anonymous option only, which means that all Web users are logged on as IUSR_MYMACHINENAME. Later in this chapter, we'll explain Windows NT Challenge/Response. 290.2.2 IIS Directories Remember SlowSoft's Web site from Chapter 34? If you requested the URL http://slowsoft.com/newproducts.html, the newproducts.html file would be displayed from the slowsoft.com home directory. Each server needs a home directory, even if that directory contains only subdirectories. The home directory does not need to be the server computer's root directory, however. As shown in Figure 35-3, the WWW home directory is really \WebHome, so clients read the disk file \WebHome\newproducts.html.
- 620 -
Figure 35-3. The \WebHome WWW home directory screen. Your server could get by with a home directory only, but the IIS virtual directory feature might be useful. Suppose SlowSoft wanted to allow Web access to the directory \BF on the D drive. The screen above shows a virtual /BugsFixed directory that maps to D:\BF. Clients would access files with a URL similar to this: http://slowsoft.com/BugsFixed/file1.html. If your computer was configured for multiple IP addresses (see the Control Panel Network icon), IIS would allow you to define virtual Web servers. Each virtual server would have its own home directory (and virtual directories) attached to a specified IP address, making it appear as though you had several server computers. Unfortunately, the IIS Web server listens on all the computer's IP addresses, so you can't run IIS simultaneously with the EX34A server with both listening on port 80. As described in Chapter 34, browsers can issue a blind request. As Figure 35-3 shows, Internet Service Manager lets you specify the file that a blind request selects, usually Default.htm. If you select the Directory Browsing Allowed option of the Directories page on the service property screen, browser clients can see a hypertext list of files in the server's directory instead. 290.2.3 IIS Logging IIS is capable of making log entries for all connections. You control logging from the Internet Service Manager's Logging property page. You can specify text log files, or you can specify logging to an SQL/ODBC database. Log entries consist of date, time, client IP address, file requested, query string, and so forth. 290.3 Testing IIS It's easy to test IIS with a browser or with any of the EX35A clients. Just make sure that IIS is running and that the EX35A server is not running. The default IIS home directory is \Winnt\System32\inetsrv\wwwroot, and some HTML files are installed there. If you're running a single machine, you can use the localhost host name. For a network, use a name from the Hosts file. If you can't access the server from a remote machine, run ping to make sure the network is configured correctly. Don't try to build and run ISAPI DLLs until you have successfully tested IIS on your computer. 291. ISAPI Server Extensions An ISAPI server extension is a program (implemented as a DLL loaded by IIS) that runs in response to a GET or POST request from a client program (browser). The browser can pass parameters to the program, which are often values that the browser user types into edit controls, selects from list boxes, and so forth. The ISAPI server extension typically sends back HTML code based on those parameter values. You'll better understand this process when you see an example. 291.1 Common Gateway Interface and ISAPI
- 621 Internet server programs were first developed for UNIX computers, so the standards were in place long before Microsoft introduced IIS. The Common Gateway Interface (CGI) standard, actually part of HTTP, evolved as a way for browser programs to interact with scripts or separate executable programs running on the server. Without altering the HTTP/CGI specifications, Microsoft designed IIS to allow any browser to load and run a server DLL. DLLs are part of the IIS process and thus are faster than scripts that might need to load separate executable programs. Because of your experience, you'll probably find it easier to write an ISAPI DLL in C++ than to write a script in PERL, the standard Web scripting language for servers. CGI shifts the programming burden to the server. Using CGI parameters, the browser sends small amounts of information to the server computer, and the server can do absolutely anything with this information, including access a database, generate images, and control peripheral devices. The server sends a file (HTML or otherwise) back to the browser. The file can be read from the server's disk, or it can be generated by the program. No ActiveX controls or Java applets are necessary, and the browser can be running on any type of computer. 291.2 A Simple ISAPI Server Extension GET Request Suppose an HTML file contains the following tag: Idaho Weather Map
If you looked at this HTML file with a browser, you would see the form shown in Figure 35-4.
- 622 -
Figure 35-4. The Weathermap HTML Form window. The select tag provides the state name from a list of four states, and the all-important "submit" input tag displays the pushbutton that sends the form data to the server in the form of a CGI GET request that looks like this: GET scripts/maps.dll?GetMap?State=Idaho HTTP/1.0 (various request headers) (blank line) Unfortunately, some early versions of the Netscape browser omit the function name in form-originated GET requests, giving you two choices: provide only a default function in your ISAPI DLL, and use the POST method inside a form instead of the GET method. If you want to use the POST option, change one HTML line in the form above to the following:
- 625 Figure 35-6 shows how the order form appears in the browser.
Figure 35-6. The CyberPizza order form. So far, no ISAPI DLL is involved. When the customer clicks the Submit Order Now button, the action begins. Here's what the server sees: POST scripts/ex35a.dll?ProcessPizzaForm HTTP/1.0 (request headers) (blank line) name=Walter+Sullivan&address=Redmond%2C+WA&quantity=2&size=12&top1=Pepperoni &top3=Mushrooms Looks like Walter Sullivan has ordered two 12-inch pepperoni and mushroom pizzas. The browser inserts a + sign in place of a space, the %2C is a comma, and the & is the parameter separator. Now let's look at the parse map entries in ex35a.cpp: ON_PARSE_COMMAND(ProcessPizzaForm, CEx35aExtension, ITS_PSTR ITS_PSTR ITS_I4 ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR) ON_PARSE_COMMAND_PARAMS("name address quantity size top1=~ top2=~ top3=~ top4=~") Optional Parameters When you write your parse map statements, you must understand the browser's rules for sending parameter values from a form. In the EX35A pizza form, the browser always sends parameters for text fields, even if the user enters no data. If the user left the Name field blank, for example, the browser would send name=&. For check box fields, however, it's a different story. The browser sends the check box parameter value only if the user checks the box. The parameters associated with check boxes are thus defined as optional parameters. If your parse macro for parameters looked like this ON_PARSE_COMMAND_PARAMS("name address quantity size top1 top2 top3 top4")
- 626 there would be trouble if the customer didn't check all the toppings. The HTTP request would simply fail, and the customer would have to search for another pizza site. The =~ symbols in the ex35a.cpp code designate the last four parameters as optional, with default values ~. If the Toppings option is checked, the form transmits the value; otherwise, it transmits a ~ character, which the DLL can test for. Optional parameters must be listed last. The DLL's ProcessPizzaForm function reads the parameter values and produces an HTML confirmation form, which it sends to the customer. Here is part of the function's code: *pCtxt << ""; // Store this order in a disk file or database, referenced by name } else { *pCtxt << "You forgot to enter name or address. Back up and try again. "; } EndContent(pCtxt); The resulting browser screen is shown in Figure 35-7.
Figure 35-7. The pizza confirmation browser screen. As you can see, we took a shortcut computing the price. To accept, the customer clicks the submit button named Confirm And Charge My Credit Card. 292.2 The Second Step—Processing the Confirmation When the user clicks the Confirm And Charge My Credit Card button, the browser sends a second POST request to the server, specifying that the CEx35aExtension::ConfirmOrder function be called. But now you have to solve a big problem. Each HTTP connection (request/response) is independent of all others. How are you going to link the confirmation request with the original order? Although there are different ways to do this, the most common approach is to send some text back with the confirmation in a hidden input tag. When the confirmation parameter values come back, the server uses the hidden text to match the confirmation to the original order, which it has stored somewhere on its hard disk. In the EX35A example, the customer's name is used in the hidden field, although it might be safer to use some combination of the name, date, and time. Here's the HTML code that CEx35aExtension::ProcessPizzaForm sends to the customer as part of the confirmation form: Here's the code for the CEx35aExtension::ConfirmOrder function: void CEx35aExtension::ConfirmOrder(CHttpServerContext* pCtxt,
- 627 LPCTSTR pstrName) { StartContent(pCtxt); WriteTitle(pCtxt); *pCtxt << "
- 628 Start User Manager or User Manager For Domains (Administrative Tools menu). Choose User Rights from the Policies menu, check Show Advanced User Rights, select the right Act As Part Of The Operating System, and add your user group as shown on the facing page. Repeat step 3 to set the right for Generate Security Audits. Log back on to Windows NT to activate the new permission. (Don't forget this step.) Make sure that the current EX35A DLL file has been copied into the scripts directory. Start debugging. You can set breakpoints, step through code, and see the output of TRACE messages.
293. ISAPI Database Access Your ISAPI server extension could use ODBC to access an SQL database. Before you write pages of ODBC code, however, check out the Internet Database Connector described in the IIS documentation. The Internet Database Connector is a ready-to-run DLL, Httpodbc.dll, that collects SQL query parameters and formats the output. You control the process by writing an IDC file that describes the data source and an HTX file that is a template for the resulting HTML file. No C++ programming is necessary. The Internet Database Connector is for queries only. If you want to update a database, you must write your own ISAPI server extension with ODBC calls. Make sure your ODBC driver is multithreaded, as is the latest SQL server driver. 294. Using HTTP Cookies to Link Transactions Now that you've wolfed down the pizza, it's time for some dessert. However, the cookies that we'll be digesting in this section are not made with chocolate chips. Cookies are used to store information on our customers' hard disks. In the EX35A example, the server stores the customer name in a hidden field of the confirmation form. That works fine for linking the confirmation to the order, but it doesn't help you track how many pizzas Walter ordered this year. If you notice that Walter consistently orders pepperoni pizzas, you might want to send him some e-mail when you have a surplus of pepperoni. 294.1 How Cookies Work With cookies, you assign Walter a customer ID number with his first order and make him keep track of that number on his computer. The server assigns the number by sending a response header such as this one: Set-Cookie: customer_id=12345; path=/; expires=Monday, 02-Sep-99 00:00:00 GMT The string customer_id is the arbitrary cookie name you have assigned, the / value for path means that the browser sends the cookie value for any request to your site (named CyberPizza.com), and the expiration date is necessary for the browser to store the cookie value. When the browser sees the Set-Cookie response header, it creates (or replaces) an entry in its cookies.txt file as follows: customer_id 12345 cyberpizza.com/ 0 2096697344 0 2093550622 35 *
- 629 Thereafter, when the browser requests anything from CyberPizza.com, the browser sends a request header like this: Cookie: customer_id=12345 294.2 How an ISAPI Server Extension Processes Cookies Your ISAPI server extension function makes a call like this one to store the cookie at the browser: AddHeader(pCtxt, "Set-Cookie: session_id=12345; path=/;" " expires=Monday, " 02-Sep-99 00:00:00 GMT\r\n"); To retrieve the cookie, another function uses code like this: char strCookies[200]; DWORD dwLength = 200; pCtxt->GetServerVariable("HTTP_COOKIE", strCookies, &dwLength); The strCookies variable should now contain the text customer_id=12345. 294.3 Problems with Cookies There was an uproar some time ago when Internet users first discovered that companies were storing data on the users' PCs. New browser versions now ask permission before storing a cookie from a Web site. Customers could thus refuse to accept your cookie, they could erase their cookies.txt file, or this file could become full. If you decide to use cookies at your Web site, you'll just have to deal with those possibilities. 295. WWW Authentication Up to now, your IIS has been set to allow anonymous logons, which means that anyone in the world can access your server without supplying a user name or password. All users are logged on as IUSR_MYMACHINENAME and can access any files for which that user name has permissions. As stated in Chapter 34, you should be using NTFS on your server for maximum security. 295.1 Basic Authentication The simplest way to limit server access is to enable basic authentication. Then, if a client makes an anonymous request, the server sends back the response HTTP/1.0 401 Unauthorized together with a response header like this: WWW-Authenticate: Basic realm="xxxx" The client prompts the user for a user name and password, and then it resends the request with a request header something like this: Authorization: Basic 2rc234ldfd8kdr The string that follows Basic is a pseudoencrypted version of the user name and password, which the server decodes and uses to impersonate the client. The trouble with basic authentication is that intruders can pick up the user name and password and use it to gain access to your server. IIS and most browsers support basic authentication, but it's not very effective. 295.2 Windows NT Challenge/Response Authentication Windows NT challenge/response authentication is often used for intranets running on Microsoft networks, but you can use it on the Internet as well. IIS supports it (see Figure 35-2), but not all browsers do. If the server has challenge/response activated, a client making an ordinary request gets this response header: WWW-Authenticate: NTLM Authorization: NTLM T1RMTVNTUAABAAAAA5IAA ... The string after NTLM is the well-encoded user name—the password is never sent over the network. The server issues a challenge, with a response header like this: WWW-Authenticate: NTLM RPTUFJTgAAAAAA ... The client, which knows the password, does some math on the challenge code and the password and then sends back a response in a request header like this: Authorization: NTLM AgACAAgAAAAAAAAAA ... The server, which has looked up the client's password from the user name, runs the same math on the password and challenge code. It then compares the client's response code against its own result. If the client's and the server's results match, the server honors the client's request by impersonating the client's user name and sending the requested data. When the client resends the request, the challenge/response dialog is performed over a single-socket connection with keep-alive capability as specified in the Connection request header. WinInet fully supports Windows NT challenge/response authentication. Thus, Internet Explorer 4.0 and the EX34A WinInet clients support it. If the client computer is logged on to a Windows NT domain, the user name and password are passed through. If the client is on the Internet, WinInet prompts for the user name and password. If you're writing
- 630 WinInet code, you must use the INTERNET_FLAG_KEEP_CONNECTION flag in all CHttpConnection::OpenRequest and CInternetSession::OpenURL calls, as EX34A illustrates. 295.3 The Secure Sockets Layer Windows NT challenge/response authentication controls only who logs on to a server. Anyone snooping on the Net can read the contents of the TCP/IP segments. The secure sockets layer (SSL) goes one step further and encodes the actual requests and responses (with a performance hit, of course). Both IIS and WinInet support SSL. (The secure sockets layer is described in the IIS documentation.) 296. ISAPI Filters An ISAPI server extension DLL is loaded the first time a client references it in a GET or POST request. An ISAPI filter DLL is loaded (based on a Registry entry) when the WWW service is started. The filter is then in the loop for all HTTP requests, so you can read and/or change any data that enters or leaves the server. 296.1 Writing an ISAPI Filter DLL The ISAPI Extension Wizard makes writing filters as easy as writing server extensions. Choose Generate A Filter Object, and Step 2 looks like this.
The list of options under Which Notifications Will Your Filter Process? refers to seven places where your filter can get control during the processing of an HTTP request. You check the boxes, and the wizard generates the code. 296.2 The MFC ISAPI Filter Classes There are two MFC classes for ISAPI filters, CHttpFilter and CHttpFilterContext. 296.2.1 CHttpFilter With the help of the ISAPI Extension Wizard, you derive a class from CHttpFilter for each ISAPI filter you create. There's just one object of this class. The class has virtual functions for each of seven notifications. The list of filters in the order in which IIS calls them is below. virtual DWORD OnReadRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData); virtual DWORD OnPreprocHeaders(CHttpFilterContext* pCtxt, PHTTP_FILTER_PREPROC_HEADERS pHeaderInfo); virtual DWORD OnUrlMap(CHttpFilterContext* pCtxt, PHTTP_FILTER_URL_MAP pMapInfo); virtual DWORD OnAuthentication(CHttpFilterContext* pCtxt, PHTTP_FILTER_AUTHENT pAuthent); virtual DWORD OnSendRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData); virtual DWORD OnLog(CHttpFilterContext* pfc, PHTTP_FILTER_LOG pLog); virtual DWORD OnEndOfNetSession(CHttpFilterContext* pCtxt); If you override a function, you get control. It would be inefficient, however, if IIS made virtual function calls for every notification for each transaction. Another virtual function, GetFilterVersion, is called once when the filter is loaded.
- 631 The ISAPI Extension Wizard always overrides this function for you, and it sets flags in the function's pVer parameter, depending on which notifications you want. Here's a simplified sample with all the flags set: BOOL CMyFilter::GetFilterVersion(PHTTP_FILTER_VERSION pVer) { CHttpFilter::GetFilterVersion(pVer); pVer->dwFlags |= SF_NOTIFY_ORDER_LOW | SF_NOTIFY_NONSECURE_PORT | SF_NOTIFY_LOG | SF_NOTIFY_AUTHENTICATION | SF_NOTIFY_PREPROC_HEADERS | SF_NOTIFY_READ_RAW_DATA | SF_NOTIFY_SEND_RAW_DATA | SF_NOTIFY_URL_MAP | SF_NOTIFY_END_OF_NET_SESSION; return TRUE; } If you had specified URL mapping requests only, the wizard would have set only the SF_NOTIFY_URL_MAP flag and it would have overridden only OnUrlMap. IIS would not call the other virtual functions, even if they were overridden in your derived class. 296.2.2 CHttpFilterContext An object of this second MFC class exists for each server transaction, and each of the notification functions gives you a pointer to that object. The CHttpFilterContext member functions you might call are GetServerVariable, AddResponseHeaders, and WriteClient. 297. A Sample ISAPI Filter—ex35b.dll, ex35c.exe It was hard to come up with a cute application for ISAPI filters. The one we thought up, ex35b.dll, is a useful visual logging utility. IIS, of course, logs all transactions to a file (or database), but you must stop the server before you can see the log file entries. With this example, you have a real-time transaction viewer that you can customize. 297.1 Choosing the Notification Start by looking at the list of CHttpFilter virtual member functions on page 1050. Observe the calling sequence and the parameters. For the EX35B logging application, we chose OnReadRawData because it allowed full access to the incoming request and header text (from pRawData) and to the source and destination IP addresses (from pCtxt>GetServerVariable). 297.2 Sending Transaction Data to the Display Program The ISAPI filter DLL can't display the transactions directly because it runs (as part of the IIS service process) on an invisible desktop. You need a separate program that displays text in a window, and you need a way to send data from the DLL to the display program. There are various ways to send the data across the process boundary. A conversation with Jeff Richter, the Windows guru who wrote Advanced Windows (Microsoft Press, 1997), led to the data being put in shared memory. Then a user-defined message, WM_SENDTEXT, is posted to the display program. These messages can queue up, so IIS can keep going independently of the display program. We declared two handle data members in CEx35bFilter::m_hProcessDest and CEx35bFilter::m_hWndDest. We added code at the end of the GetFilterVersion function to set these data members to the display program's process ID and main window handle. The code finds the display program's main window by its title, ex35b, and then it gets the display program's process ID. m_hProcessDest = NULL; if((m_hWndDest = ::FindWindow(NULL, "ex35b")) != NULL) { DWORD dwProcessId; GetWindowThreadProcessId(m_hWndDest, &dwProcessId); m_hProcessDest = OpenProcess(PROCESS_DUP_HANDLE, FALSE, dwProcessId); SendTextToWindow("EX35B filter started\r\n"); } Below is a helper function, SendTextToWindow, which sends the WM_SENDTEXT message to the display program. void CEx35bFilter::SendTextToWindow(char* pchData) { if(m_hProcessDest != NULL) { int nSize = strlen(pchData) + 1; HANDLE hMMFReceiver; HANDLE hMMF = ::CreateFileMapping((HANDLE) 0xFFFFFFFF, NULL, PAGE_READWRITE, 0, nSize, NULL); ASSERT(hMMF != NULL); LPVOID lpvFile = ::MapViewOfFile(hMMF, FILE_MAP_WRITE, 0, 0, nSize);
- 632 ASSERT(lpvFile != NULL); memcpy((char*) lpvFile, pchData, nSize); ::DuplicateHandle(::GetCurrentProcess(), hMMF, m_hProcessDest, &hMMFReceiver, 0, FALSE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE); ::PostMessage(m_hWndDest, WM_SENDTEXT, (WPARAM) 0, (LPARAM) hMMFReceiver); ::UnmapViewOfFile(lpvFile); } } The DuplicateHandle function makes a copy of EX35B's map handle, which it sends to the EX35C program in a message parameter. The EX35C process ID, determined in GetFilterVersion, is necessary for the DuplicateHandle call. Here is the filter's OnReadRawData function, which calls SendTextToWindow: DWORD CEx35bFilter::OnReadRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData) { TRACE ("CEx35bFilter::OnReadRawData\n"); // sends time/date, from IP, to IP, request data to a window char pchVar[50] = ""; char pchOut[2000]; DWORD dwSize = 50; BOOL bRet; CString strGmt = CTime::GetCurrentTime().FormatGmt("%m/%d/%y %H:%M:%SGMT"); strcpy(pchOut, strGmt); bRet = pCtxt->GetServerVariable("REMOTE_ADDR", pchVar, &dwSize); if(bRet && dwSize > 1) { strcat(pchOut, ", From "); strcat(pchOut, pchVar); } bRet = pCtxt->GetServerVariable("SERVER_NAME", pchVar, &dwSize); if(bRet && dwSize > 1) { strcat(pchOut, ", To "); strcat(pchOut, pchVar); } strcat(pchOut, "\r\n"); int nLength = strlen(pchOut); // Raw data is not zero-terminated strncat(pchOut, (const char*) pRawData->pvInData, pRawData->cbInData); nLength += pRawData->cbInData; pchOut[nLength] = `\0'; SendTextToWindow(pchOut); return SF_STATUS_REQ_NEXT_NOTIFICATION; } 297.3 The Display Program The display program, ex35c.exe, isn't very interesting. It's a standard AppWizard CRichEditView program with a WM_SENDTEXT handler in the main frame class: LONG CMainFrame::OnSendText(UINT wParam, LONG lParam) { TRACE("CMainFrame::OnSendText\n"); LPVOID lpvFile = ::MapViewOfFile((HANDLE) lParam, FILE_MAP_READ, 0, 0, 0); GetActiveView()->SendMessage(EM_SETSEL, (WPARAM) 999999, 1000000); GetActiveView()->SendMessage(EM_REPLACESEL, (WPARAM) 0, (LPARAM) lpvFile); ::UnmapViewOfFile(lpvFile); ::CloseHandle((HANDLE) lParam);
- 633 -
return 0; } This function just relays the text to the view. The EX35C CMainFrame class overrides OnUpdateFrameTitle to eliminate the document name from the main window's title. This ensures that the DLL can find the EX35C window by name. The view class maps the WM_RBUTTONDOWN message to implement a context menu for erasing the view text. Apparently rich edit view windows don't support the WM_CONTEXTMENU message. 297.4 Building and Testing the EX35B ISAPI Filter Build both the EX35B and EX35C projects, and then start the EX35C program. To specify loading of your new filter DLL, you must manually update the Registry. Run the Regedit application, and then double-click on Filter DLLs in \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\Parameters. Add the full pathname of the DLL separated from other DLL names with a comma. There's one more thing to do. You must change the IIS mode to allow the service to interact with the EX35C display program. To do this, click on the Services icon in the Control Panel, double-click on World Wide Web Publishing Service, and then check Allow Service To Interact With Desktop. Finally, use Internet Service Manager to stop and restart the WWW service to load the filter DLL. When you use the browser to retrieve pages from the server, you should see output like this.
You can use the same steps for debugging an ISAPI filter that you used for an ISAPI server extension. Chapter Thirty-Six 298. ActiveX Document Servers and the Internet An ActiveX document is a special file that you can download from a Web server. When the browser sees an ActiveX document file, it automatically loads the corresponding ActiveX document server program from your hard disk, and that program takes over the whole browser window to display the contents of the document. The Microsoft Internet Explorer browser is not the only ActiveX document container program. The Microsoft Office Binder program also runs ActiveX document server programs, storing the several ActiveX documents in a single disk file. In the COM world, an ActiveX document server program is called a server because it implements a COM component. The container program (Internet Explorer or Office Binder) creates and controls that COM component. In the Internet world, the same program looks like a client because it can request information from a remote host (Microsoft Internet Information Server).
- 634 In this chapter, you'll learn about ActiveX document servers and ActiveX documents and you'll build two ActiveX document servers that work over the Internet in conjunction with Internet Explorer. Pay attention to this technology now because you'll be seeing a lot more of it as Microsoft Windows evolves. 299. ActiveX Document Theory It's helpful to put ActiveX documents within the context of COM and OLE, which you already understand if you've read the other chapters in this book. You can, however, get started with ActiveX document servers without fully understanding all the COM concepts covered in Part VI. 299.1 ActiveX Document Servers vs. OLE Embedded Servers As you saw in Chapter 28, an OLE embedded server program runs in a child window of an OLE container application and occupies a rectangular area in a page of the container's document (see Figure 28-1). Unless an embedded server program is classified as a mini-server, it can run stand-alone also. In embedded mode, the server program's data is held in a storage inside the container application's file. The embedded server program takes over the container program's menu and toolbar when the user activates it by double-clicking on its rectangle. In contrast to an embedded server, an ActiveX document server takes over a whole frame window in its container application, and the document is always active. An ActiveX server application, running inside a container's frame window, runs pretty much the same way it would in stand-alone mode. You can see this for yourself if you have Microsoft Office 97. Office includes an ActiveX container program called Binder (accessible from the Office shortcut bar), and the Office applications (Microsoft Word, Microsoft Excel, and so on) have ActiveX server capability. Figure 36-1 shows a Word document and an Excel chart inside the same binder.
Figure 36-1. A Word document and an Excel chart inside a Microsoft Office Binder window. Like an embedded server, the ActiveX document server saves its data in a storage inside the ActiveX container's file. When the Office user saves the Binder program from the File menu, Binder writes a single OBD file to disk; the file contains one storage for the Word document and another for the Excel spreadsheet. You can see this file structure yourself with the DFVIEW utility, as shown in Figure 36-2.
- 635 -
Figure 36-2. A file structure displayed by the DocFile Viewer. 299.2 Running an ActiveX Document Server from Internet Explorer Running an ActiveX document server from Internet Explorer is more fun than running one from Microsoft Office Binder (Internet Explorer refers to Internet Explorer 3.0 or greater). Rather than load a storage only from an OBD file, the server program can load its storage from the other side of the world. You just type in a URL, such as http://www.DaliLama.in/SecretsOfTheUniverse.doc, and a Microsoft Word document opens inside your Browse window, taking over the browser's menu and toolbar. That's assuming, of course, that you have installed the Microsoft Word program. If not, a Word document viewer is available, but it must be on your hard disk before you download the file. An ActiveX document server won't let you save your changes back to the Internet host, but it will let you save them on your own hard disk. In other words, File Save is disabled but File Save As is enabled. If you have Microsoft Office, try running Word or Excel in Internet Explorer now. The EX34A server is quite capable of delivering documents or worksheets to your browser, assuming that they are accessible from its home directory. Note that Internet Explorer recognizes documents and worksheets not by their file extensions but by the CLSID inside the files. You can prove this for yourself by renaming a file prior to accessing it. 299.3 ActiveX Document Servers vs. ActiveX Controls Both ActiveX document servers and ActiveX controls can run with and without the Internet. Both are compiled programs that can run inside a browser. The following table lists some of the differences between the two. ActiveX Document Server
ActiveX Control
Module type
EXE
Most often a DLL
Can run stand-alone
Yes
No
Code automatically downloaded and registered by a WWW browser
No
Yes
Can be embedded in an HTML file
No
Yes
Occupies the entire browser window
Yes
Sometimes
Can be several pages
Yes
Not usually
Can read/write disk files
Yes
Not usually
299.4 OLE Interfaces for ActiveX Document Servers and Containers
- 636 ActiveX document servers implement the same interfaces as OLE embedded servers, including IOleObject, IOleInPlaceObject, and IOleInPlaceActiveObject. ActiveX document containers implement IOleClientSite, IOleInPlaceFrame, and IOleInPlaceSite. The menu negotiation works the same as it does for Visual Editing. Some additional interfaces are implemented, however. ActiveX document servers implement IOleDocument, IOleDocumentView, IOleCommandTarget, and IPrint. ActiveX document containers implement IOleDocumentSite. The architecture allows for multiple views of the same document—sort of like the MFC document-view architecture— but most ActiveX document servers implement only one view per document. The critical function in an OLE embedded server is IOleObject::DoVerb, which is called by the container when the user double-clicks on an embedded object or activates it through the menu. For an ActiveX document server, however, the critical function is IOleDocumentView::UIActivate. (Before calling this function, the container calls IOleDocument::CreateView, but generally the server just returns an interface pointer to the single document-view object.) UIActivate finds the container site and frame window, sets that window as the server's parent, sets the server's window to cover the container's frame window, and then activates the server's window. It's important to realize that the COM interaction takes place between the container program (Internet Explorer or Binder) and the ActiveX document server (your program), which are both running on the client computer. We know of no cases in which remote procedure calls (RPCs) are made over the Internet. That means that the remote host (the server computer) does not use COM interfaces to communicate with clients, but it can deliver data in the form of storages. 299.5 MFC Support for ActiveX Document Servers MFC allows you to create your own ActiveX document server programs. In addition, Visual C++ 6.0 now allows you to write ActiveX document containers. To get a server program, create a new MFC AppWizard EXE project and then check the Active Document Server check box, as shown in Figure 36-3. To create a container program, just make sure the Active Document Container check box is marked.
Figure 36-3. Step 3 of the MFC AppWizard. Here's a rundown of the classes involved in MFC's ActiveX Document Server Architecture. 299.5.1 COleServerDoc As it is for any COM component, your ActiveX document server's document class is derived from COleServerDoc, which implements IPersistStorage, IOleObject, IDataObject, IOleInPlaceObject, and IOleInPlaceActiveObject. The COM interfaces and MFC classes discussed here were named before Microsoft introduced ActiveX technology. An ActiveX document server was formerly known as a document object server or a doc object server, so those are the names you'll see in the source code and in some online documentation. 299.5.2 CDocObjectServerItem
- 637 This class is derived from the COleServerItem class used in embedded servers. Your ActiveX document server program has a class derived from CDocObjectServerItem, but that class isn't used when the program is running in ActiveX document mode. 299.5.3 CDocObjectServer This class implements the new ActiveX server interfaces. Your application creates an object of class CDocObjectServer and attaches it to the COleServerDoc object. If you look at COleServerDoc::GetDocObjectServer in your derived document class, you'll see the construction code. Thereafter, the document object and attached CDocObjectServer object work together to provide ActiveX document server functionality. This class implements both IOleDocument and IOleDocumentView, which means that you can have only one view per document in an MFC ActiveX document server. You generally don't derive classes from CDocObjectServer. 299.5.4 COleDocIPFrameWnd This class is derived from COleIPFrameWnd. Your application has a frame window class derived from COleDocIPFrameWnd. The framework constructs an object of that class when the application starts in embedded server mode or in ActiveX document server mode. In ActiveX document server mode, the server's window completely covers the container's frame window and has its own menu resource attached, with the identifier IDR_SRVR_INPLACE (for an SDI application). 300. ActiveX Document Server Example EX36A You could construct the EX36A example in two phases. The first phase is a plain ActiveX document server that loads a file from its container. The view base class is CRichEditView, which means the program loads, edits, and stores text plus embedded objects. In the second phase, the application is enhanced to download a separate text file from the Internet one line at a time, demonstrating that ActiveX document servers can make arbitrary WinInet calls. 300.1 EX36A Phase 1—A Simple Server The EX36A example on the book's CD-ROM is complete with the text download feature from Phase 2. You can exercise its Phase 1 capabilities by building it, or you can create a new application with AppWizard. If you do use AppWizard, you should refer to Figure 36-3 to see the AppWizard EXE project dialog and select the appropriate options. All other options are the default options, except those for selecting SDI (Step 1), setting the project's filename extension to 36a using the Advanced button in Step 4, and changing the view's base class (CRichEditView —on the wizard's last page). You don't have to write any C++ code at all. Be sure to run the program once in stand-alone mode to register it. While the program is running in stand-alone mode, type some text (and insert some OLE embedded objects) and then save the document as test.36a in your Internet server's home directory (\scripts or \wwwroot directory). Try loading test.36a from Internet Explorer and from Office Binder. Use Binder's Section menu for loading and storing EX36A documents to and from disk files. You should customize the document icons for your ActiveX document servers because those icons show up on the right side of an Office Binder window. 300.2 Debugging an ActiveX Document Server If you want to debug your program in ActiveX document server mode, click on the Debug tab in the Build Settings dialog. Set Program Arguments to /Embedding, and then start the program. Now start the container program and use it to "start" the server, which has in fact already started in the debugger and is waiting for the container. 300.3 EX36A Phase 2—Adding WinInet Calls The EX36A example on the CD-ROM includes two dialog bar objects, one for the main frame window and another for the in-place frame window. Both are attached to the same resource template, IDD_DIALOGBAR, which contains an edit control that accepts a text file URL plus start and stop buttons that display green and red bitmaps. If you click the green button (handled by the OnStart member function of the CEx36aView class), you'll start a thread that reads the text file one line at a time. The thread code from the file UrlThread.cpp is shown here: CString g_strURL = "http:// "; volatile BOOL g_bThreadStarted = FALSE; CEvent g_eKill; UINT UrlThreadProc(LPVOID pParam) { g_bThreadStarted = TRUE; CString strLine; CInternetSession session; CStdioFile* pFile1 = NULL; try { pFile1 = session.OpenURL(g_strURL, 0, INTERNET_FLAG_TRANSFER_BINARY | INTERNET_FLAG_KEEP_CONNECTION); // needed for Windows NT
- 638 // c/r authentication // Keep displaying text from the URL until the Kill event is // received while(::WaitForSingleObject(g_eKill.m_hObject, 0) != WAIT_OBJECT_0) { // one line at a time if(pFile1->ReadString(strLine) == FALSE) break; strLine += `\n'; ::SendMessage((HWND) pParam, EM_SETSEL, (WPARAM) 999999, 1000000); ::SendMessage((HWND) pParam, EM_REPLACESEL, (WPARAM) 0, (LPARAM) (const char*) strLine); Sleep(250); // Deliberately slow down the transfer } } catch(CInternetException* e) { LogInternetException(pParam, e); e->Delete(); } if(pFile1 != NULL) delete pFile1; // closes the file—prints a warning g_bThreadStarted = FALSE; // Post any message to update the toolbar buttons ::PostMessage((HWND) pParam, EM_SETSEL, (WPARAM) 999999, 1000000); TRACE("Post thread exiting normally\n"); return 0; } This code uses the CStdioFile pointer to pFile1 returned from OpenURL. The ReadString member function reads one line at a time, and each line is sent to the rich edit view window. When the main thread sets the "kill" event (the red button), the URL thread exits. Before you test EX36A, make sure that the server (EX34A or IIS) is running and that you have a text file in the server's home directory. Test the EX36A program first in stand-alone mode by entering the text file URL in the dialog bar. Next try running the program in server mode from Internet Explorer. Enter test.36a (the document you created when you ran EX36A in stand-alone mode) in Internet Explorer's Address field to load the server. We considered using the CAsyncMonikerFile class (see Asynchronous Moniker Files) instead of the MFC WinInet classes to read the text file. We stuck with WinInet, however, because the program could use the CStdioFile class ReadString member function to "pull" individual text lines from the server when it wanted them. The CAsyncMonikerFile class would have "pushed" arbitrary blocks of characters into the program (by calling the overridden OnDataAvailable function) as soon as the characters had been received. Displaying Bitmaps on Buttons Chapter 11 describes the CBitmapButton class for associating a group of bitmaps with a pushbutton. Microsoft Windows 95, Microsoft Windows 98, and Microsoft Windows NT 4.0 support an alternative technique that associates a single bitmap with a button. First you apply the Bitmap style (on the button's property sheet) to the button, and then you declare a variable of class CBitmap that will last at least as long as the button is enabled. Then you make sure that the CButton::SetBitmap function is called just after the button is created. Here is the code for associating a bitmap with a button, from the EX36A MainFrm.cpp and IpFrame.cpp files: m_bitmapGreen.LoadBitmap(IDB_GREEN); HBITMAP hBitmap = (HBITMAP) m_bitmapGreen.GetSafeHandle(); ((CButton*) m_wndDialogBar.GetDlgItem(IDC_START)) ->SetBitmap(hBitmap); If your button was in a dialog, you could put similar code in the OnInitDialog member function and declare a CBitmap member in the class derived from CDialog.
- 639 301. ActiveX Document Server Example EX36B Look at the pizza form example from Chapter 35 (EX35A). Note that the server (the ISAPI DLL) processes the order only when the customer clicks the Submit Order Now button. This is okay for ordering pizzas because you're probably happy to accept money from anyone, no matter what kind of browser is used. For a form-based intranet application, however, you can be more selective. You can dictate what browser your clients have, and you can distribute your own client software on the net. In that environment, you can make data entry more sophisticated, allowing, for example, the client computer to validate each entry as the user types it. That's exactly what's happening in EX36B, which is another ActiveX document server, of course. EX36B is a form-based employee time-sheet entry program that works inside Internet Explorer (as shown in Figure 36-4) or works as a stand-alone application. Looks like a regular HTML form, doesn't it? It's actually an MFC form view, but the average user probably won't know the difference. The Name field is a drop-down combo box, however—which is different from the select field you would see in an HTML form—because the user can type in a value if necessary. The Job Number field has a spin button control that helps the user select the value. These aren't necessarily the ideal controls for time-sheet entry, but the point here is that you can use any Windows controls you want, including tree controls, list controls, trackbars, and ActiveX controls, and you can make them interact any way you want.
Figure 36-4. Employee time-sheet entry form. Field Validation in an MFC Form View Problem: MFC's standard validation scheme validates data only when CWnd::UpdateData(TRUE) is called, usually when the user exits the dialog. Applications often need to validate data the moment the user leaves a field (edit control, list box, and so on). The problem is complex because Windows permits the user to freely jump between fields in any sequence by using the mouse. Ideally, standard MFC DDX- /DDV (data exchange/validation) code should be used for field validation, and the standard DoDataExchange function should be called when the user finishes the transaction. Solution: Derive your field validation form view classes from the class CValidForm, derived from CFormView, with this header: // valform.h #ifndef _VALIDFORM #define _VALIDFORM #define WM_VALIDATE WM_USER + 5 class CValidForm : public CFormView { DECLARE_DYNAMIC(CValidForm) private: BOOL m_bValidationOn;
- 640 public: CValidForm(UINT ID); // override in derived dlg to perform validation virtual void ValidateDlgItem(CDataExchange* pDX, UINT ID); //{{AFX_VIRTUAL(CValidForm) protected: virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam); //}}AFX_VIRTUAL //{{AFX_MSG(CValidForm) afx_msg LONG OnValidate(UINT wParam, LONG lParam); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; #endif // _VALIDFORM This class has one virtual function, ValidateDlgItem, which accepts the control ID as the second parameter. The derived form view class implements this function to call the DDX/DDV functions for the appropriate field. Here is a sample ValidateDlgItem implementation for a form view that has two numeric edit controls: void CMyForm::ValidateDlgItem(CDataExchange* pDX, UINT uID) { switch (uID) { case IDC_EDIT1: DDX_Text(pDX, IDC_EDIT1, m_nEdit1); DDV_MinMaxInt(pDX, m_nEdit1, 0, 10); break; case IDC_EDIT2: DDX_Text(pDX, IDC_EDIT2, m_nEdit2); DDV_MinMaxInt(pDX, m_nEdit2, 20, 30); break; default: break; } } Notice the similarity to the wizard-generated DoDataExchange function: void CAboutDlg::DoDataExchange(CDataExchange* pDX) { //{{AFX_DATA_MAP(CMyForm) DDX_Text(pDX, IDC_EDIT1, m_nEdit1); DDV_MinMaxInt(pDX, m_nEdit1, 0, 10); DDX_Text(pDX, IDC_EDIT2, m_nEdit2); DDV_MinMaxInt(pDX, m_nEdit2, 20, 30); //}}AFX_DATA_MAP } How does it work? The CValidForm class traps the user's attempt to move away from a control. When the user presses the Tab key or clicks on another control, the original control sends a killfocus command message (a control notification message) to the parent window, the exact format depending on the kind of control. An edit control, for example, sends an EN_KILLFOCUS command. When the form window receives this killfocus message, it invokes the DDX/DDV code that is necessary for that field, and if there's an error, the focus is set back to that field. There are some complications, however. First, we want to allow the user to freely switch the focus to another application—we're not interested in trapping the killfocus message in that case. Next, we must be careful how we set the focus back to the control that produced the error. We can't just call SetFocus in direct response to the killfocus message; instead we must allow the killfocus process to complete. We can achieve this by posting a user-defined WM_VALIDATE message back to the form window. The WM_VALIDATE handler calls our ValidateDlgItem virtual function after the focus has been moved to the next field. Also, we must ignore the killfocus message that results when we
- 641 switch back from the control that the user tried to select, and we must allow the IDCANCEL button to abort the transaction without validation. Most of the work here is done in the view's virtual OnCommand handler, which is called for all control notification messages. We could, of course, individually map each control's killfocus message in our derived form view class, but that would be too much work. Here is the OnCommand handler: BOOL CValidForm::OnCommand(WPARAM wParam, LPARAM lParam) { // specific for WIN32 — wParam/lParam processing different for // WIN16 TRACE("CValidForm::OnCommand, wParam = %x, lParam = %x\n", wParam, lParam); TRACE("m_bValidationOn = %d\n", m_bValidationOn); if(m_bValidationOn) { // might be a killfocus UINT notificationCode = (UINT) HIWORD( wParam ); if((notificationCode == EN_KILLFOCUS) || (notificationCode == LBN_KILLFOCUS) || (notificationCode == CBN_KILLFOCUS) ) { CWnd* pFocus = CWnd::GetFocus(); // static function call // if we're changing focus to another control in the // same form if( pFocus && (pFocus->GetParent() == this) ) { if(pFocus->GetDlgCtrlID() != IDCANCEL) { // and focus not in Cancel button // validate AFTER drawing finished BOOL rtn = PostMessage( WM_VALIDATE, wParam ); TRACE("posted message, rtn = %d\n", rtn); } } } } return CFormView::OnCommand(wParam, lParam); // pass it on } Note that m_bValidationOn is a Boolean data member in CValidForm. Finally, here is the OnValidate message handler, mapped to the user-defined WM_VALIDATE message: LONG CValidForm::OnValidate(UINT wParam, LONG lParam) { TRACE("Entering CValidForm::OnValidate\n"); CDataExchange dx(this, TRUE); m_bValidationOn = FALSE; // temporarily off UINT controlID = (UINT) LOWORD( wParam ); try { ValidateDlgItem(&dx, controlID); } catch(CUserException* pUE) { pUE->Delete(); TRACE("CValidForm caught the exception\n"); // fall through — user already alerted via message box } m_bValidationOn = TRUE; return 0; // goes no further } Instructions for use: Add valform.h and valform.cpp to your project. Insert the following statement in your view class header file: #include "valform.h"
- 642 Change your view class base class from CFormView to CValidForm. Override ValidateDlgItem for your form's controls as shown above. That's all. For dialogs, follow the same steps, but use valid.h and valid.cpp. Derive your dialog class from CValidDialog instead of from CDialog. 301.1 Generating POST Requests Under Program Control The heart of the EX36B program is a worker thread that generates a POST request and sends it to a remote server. The server doesn't care whether the POST request came from an HTML form or from your program. It could process the POST request with an ISAPI DLL, with a PERL script, or with a Common Gateway Interface (CGI) executable program. Here's what the server receives when the user clicks the EX36B Submit button: POST scripts/ex35a.dll?ProcessTimesheet HTTP/1.0 (request headers) (blank line) Period=12&Name=Dunkel&Hours=6.5&Job=5 And here's the thread code from PostThread.cpp: // PostThread.cpp (uses MFC WinInet calls) #include <stdafx.h> #include "PostThread.h" #define MAXBUF 50000 CString g_strFile = "/scripts/ex35a.dll"; CString g_strServerName = "localhost"; CString g_strParameters; UINT PostThreadProc(LPVOID pParam) { CInternetSession session; CHttpConnection* pConnection = NULL; CHttpFile* pFile1 = NULL; char* buffer = new char[MAXBUF]; UINT nBytesRead = 0; DWORD dwStatusCode; BOOL bOkStatus = FALSE; try { pConnection = session.GetHttpConnection(g_strServerName, (INTERNET_PORT) 80); pFile1 = pConnection->OpenRequest(0, g_strFile + "?ProcessTimesheet", // POST request NULL, 1, NULL, NULL, INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_RELOAD); // no cache pFile1->SendRequest(NULL, 0, (LPVOID) (const char*) g_strParameters, g_strParameters.GetLength()); pFile1->QueryInfoStatusCode(dwStatusCode); if(dwStatusCode == 200) { // OK status // doesn't matter what came back from server — we're looking // for OK status bOkStatus = TRUE; nBytesRead = pFile1->Read(buffer, MAXBUF - 1); buffer[nBytesRead] = `\0'; // necessary for TRACE TRACE(buffer); TRACE("\n"); }
- 643 } catch(CInternetException* pe) { char text[100]; pe->GetErrorMessage(text, 99); TRACE("WinInet exception %s\n", text); pe->Delete(); } if(pFile1) delete pFile1; // does the close — prints a warning if(pConnection) delete pConnection; // Why does it print a warning? delete [] buffer; ::PostMessage((HWND) pParam, WM_POSTCOMPLETE, (WPARAM) bOkStatus, 0); return 0; } The main thread assembles the g_strParameters string based on what the user typed, and the worker thread sends the POST request using the CHttpFile::SendRequest call. The tQueryInfoStatusCode to find out if the server sent back an OK response. Before exiting, the thread posts a message to the main thread, using the bOkStatus value in wParam to indicate success or failure. 301.2 The EX36B View Class The CEx36bView class is derived from CValidForm, as described in "Field Validation in an MFC Form View". CEx36bView collects user data and starts the post thread when the user clicks the Submit button after all fields have been successfully validated. Field validation is independent of the internet application. You could use CValidForm in any MFC form view application. Here is the code for the overridden or the overridden ValidateDlgItem member function, which is called whenever the user moves from one control to another: void CEx36bView::ValidateDlgItem(CDataExchange* pDX, UINT uID) { ASSERT(this); TRACE("CEx36bView::ValidateDlgItem\n"); switch (uID) { case IDC_EMPLOYEE: DDX_CBString(pDX, IDC_EMPLOYEE, m_strEmployee); // need custom DDV for empty string DDV_MaxChars(pDX, m_strEmployee, 10); if(m_strEmployee.IsEmpty()) { AfxMessageBox("Must supply an employee name"); pDX->Fail(); } break; case IDC_HOURS: DDX_Text(pDX, IDC_HOURS, m_dHours); DDV_MinMaxDouble(pDX, m_dHours, 0.1, 100.); break; case IDC_JOB: DDX_Text(pDX, IDC_JOB, m_nJob); DDV_MinMaxInt(pDX, m_nJob, 1, 20); break; default: break; } return; } The OnSubmit member function is called when the user clicks the Submit button. CWnd::UpdateData returns TRUE only when all the fields have been successfully validated. At that point, the function disables the Submit button, formats g_strParameters, and starts the post thread. void CEx36bView::OnSubmit() {
- 644 if(UpdateData(TRUE) == TRUE) { GetDlgItem(IDC_SUBMIT)->EnableWindow(FALSE); CString strHours, strJob, strPeriod; strPeriod.Format("%d", m_nPeriod); strHours.Format("%3.2f", m_dHours); strJob.Format("%d", m_nJob); g_strParameters = "Period=" + strPeriod + "&Employee=" + m_strEmployee + "&Hours=" +strHours + "&Job=" + strJob + "\r\n"; TRACE("parameter string = %s", g_strParameters); AfxBeginThread(PostThreadProc, GetSafeHwnd(), THREAD_PRIORITY_NORMAL); } } The OnCancel member function is called when the user clicks the Reset button. The CValidForm logic requires that the button's control ID be IDCANCEL. void CEx36bView::OnCancel() { CEx36bDoc* pDoc = GetDocument(); m_dHours = 0; m_strEmployee = "" ; m_nJob = 0; m_nPeriod = pDoc->m_nPeriod; UpdateData(FALSE); } The OnPostComplete handler is called in response to the user-defined WM_POSTCOMPLETE message sent by the post thread: LONG CEx36bView::OnPostComplete(UINT wParam, LONG lParam) { TRACE("CEx36bView::OnPostComplete - %d\n", wParam); if(wParam == 0) { AfxMessageBox("Server did not accept the transaction"); } else OnCancel(); GetDlgItem(IDC_SUBMIT)->EnableWindow(TRUE); return 0; } This function displays a message box if the server didn't send an OK response. It then enables the Submit button, allowing the user to post another time-sheet entry. 301.3 Building and Testing EX36B Build the /vcpp36/ex36b project, and then run it once in stand-alone mode to register it and to write a document file called test.36b in your WWW root directory. Make sure the EX35A DLL is available in the scripts directory (with execute permission) because that DLL contains an ISAPI function, ProcessTimesheet, which handles the server end of the POST request. Be sure that you have IIS or some other ISAPI-capable server running. Now run Internet Explorer and load test.36b from your server. The EX36B program should be running in the Browse window, and you should be able to enter time-sheet transactions. 301.4 ActiveX Document Servers vs. VB Script It's possible to insert VB Script (or JavaScript) code into an HTML file. We're not experts on VB Script, but we've seen some sample code. You could probably duplicate the EX36B time-sheet application with VB Script, but you would be limited to the standard HTML input elements. It would be interesting to see how a VB Script programmer would solve the problem. (In any case, you're a C++ programmer, not a Visual Basic programmer, so you might as well stick to what you know.) 302. Going Further with ActiveX Document Servers EX36A used a worker thread to read a text file from an Internet server. It used the MFC WinInet classes, and it assumed that a standard HTTP server was available. An ActiveX document server could just as easily make
- 645 Winsock calls using the CBlockingSocket class from Chapter 34. That would imply that you were going beyond the HTTP and FTP protocols. You could, for example, write a custom internet server program that listened on port 81. (That server could run concurrently with IIS if necessary.) Your ActiveX document server could use a custom TCP/IP protocol to get binary data from an open socket. The server could use this data to update its window in real-time, or it could send the data to another device, such as a sound board. Chapter Thirty-Seven 303. Introducing Dynamic HTML Dynamic HyperText Markup Language (DHTML) is a new and exciting technology—introduced as part of Microsoft Internet Explorer 4.0 (IE4)—that provides serious benefits to Webmasters and developers. DHTML could ultimately change the way we think about developing Windows applications. Why the buzz about DHTML? It began with the IE4 "HTML display engine"—sometimes called Trident in Microsoft literature. As part of the design of Internet Explorer 4, Microsoft made Trident a COM component that exposes many of its internal objects that are used for displaying HTML pages in Internet Explorer 4. This feature allows you to traverse the portions of an HTML page in script or code, as if the HTML page were a data structure. Gone are the days of having to parse HTML or write grotesque Common Gateway Interface (CGI) scripts to get to data in a form. The real power of DHTML, however, is not this ability to access the HTML objects but the ability to actually change and manipulate the HTML page on the fly—thus the name Dynamic HTML. Once you grasp the concept of DHTML, a million possible applications come to mind. For Webmasters, DHTML means that much of the logic that manipulates a Web page can live in scripts that are downloaded to the client. C++ developers can embed DHTML in their applications and use it as an embedded Web client or as a super-flexible, dynamic "form" that their application changes on the fly. Microsoft Visual J++ developers (who use Windows Foundation Classes [WFC]) can actually program DHTML on the server while an Internet Explorer client responds to the commands—an excellent alternative to CGI and potentially more powerful than Active Server Pages (ASP) server-side scripting. Unfortunately, DHTML is so powerful and extensive that it requires a separate book to fill you in on all of the copious details. For example, to really leverage DHTML you need to understand all of the possible elements of an HTML page: forms, lists, style sheets, and so on. Inside Dynamic HTML by Scott Isaacs (Microsoft Press, 1997) is a great resource for learning the details of DHTML. Instead of covering all aspects of DHTML, we will briefly introduce you to the DHTML object model, show you how to work with the model from the scripting angle (as a reference), and then show you how to work with the model from both the Microsoft Foundation Class Library version 4.21 (MFC) and the Active Template Library (ATL). These features are all made possible by the excellent DHTML support introduced in Visual C++ 6.0. 304. The DHTML Object Model If you've been heads down on a Visual C++ project and haven't yet had time to take a peek at HTML, the first thing you should know is that HTML is an ASCII markup language format. Here is the code for a very basic HTML page: This is some text with H1!
This is some text with H3!
This basic HTML "document" is composed of the following elements: A head (or header) In this example, the header contains a title: "This is an example very basic HTML page!" The body of the document The body in this example contains two text elements. The first has the heading 1 (h1) style and reads, "This is some text with H1!" The second text element has the heading 3 (h3) style and reads, "This is some text with H3!" The end result is an HTML page that—when displayed in Internet Explorer 4—looks like Figure 37-1. When Internet Explorer 4 loads this sample HTML page, it creates an internal representation that you can traverse, read, and manipulate through the DHTML object model. Figure 37-2 shows the basic hierarchy of the DHTML object model.
- 646 -
Figure 37-1. A very basic HTML page, as seen in Internet Explorer 4. At the root of the object model is the window object. This object can be used from a script to perform some action, such as popping up a dialog box. Here's an example of some JScript that accesses the window object: <SCRIPT LANGUAGE="JScript"> function about() { window.showModalDialog("about.htm","", "dialogWidth:25em;dialogHeight13em") } When the about script function is called, it will call the showModalDialog function in the window DHTML object to display a dialog. This example also illustrates how scripts access the object model—through globally accessible objects that map directly to the corresponding object in the DTHML object model. The window object has several "subobjects" that allow you to further manipulate portions of Internet Explorer 4. The document object is what we will spend most of our time on in this chapter because it gives us programmatic access to the various elements of the currently loaded HTML document. Below, some JScript shows how to create basic dynamic content that changes the document object.
- 647 -
Figure 37-2. Basic hierarchy of the DHTML object model.
- 648 <TITLE>Welcome! <SCRIPT LANGUAGE="JScript"> function changeMe() { document.all.MyHeading.outerHTML = "Dynamic HTML is magic!
"; document.all.MyHeading.style.color = "green"; document.all.MyText.innerText = "Presto Change-o! "; document.all.MyText.align = "center"; document.body.insertAdjacentHTML("BeforeEnd", " Dynamic HTML demo!
DHTML Rocks!
- 649 The DHTML object model is exposed to C++ developers through a set of COM objects with the prefix IHTML (IHTMLDocument, IHTMLWindow, IHTMLElement, IHTMLBodyElement, and so on). In C++, once you obtain the document interface, you can use any of the IHTMLDocument2 interface methods to obtain or to modify the document's properties. You can access the all collection by calling the IHTMLDocument2::get_all method. This method returns an IHTMLElementCollection collection interface that contains all the elements in the document. You can then iterate through the collection using the IHTMLElementCollection::item method (similar to the parentheses in the script above). The IHTMLElementCollection::item method supplies you with an IDispatch pointer that you can call QueryInterface on, requesting the IID_IHTMLElement interface. This call to QueryInterface will give you an IHTMLElement interface pointer that you can use to get or set information for the HTML element. Most elements also provide a specific interface for working with that particular element type. These element-specific interface names take the format of IHTMLXXXXElement, where XXXX is the name of the element (IHTMLBodyElement, for example). You must call QueryInterface on the IHTMLElement object to request the element-specific interface you need. This might sound confusing (because it can be!). But don't worry—the MFC and ATL sections in this chapter contain plenty of samples that demonstrate how it all ties together. You'll be writing DHTML code in no time. 305.1 MFC and DHTML MFC's support for DHTML starts with a new CView derivative, CHtmlView. CHtmlView allows you to embed an HTML view inside frame windows or splitter windows, where with some DHTML work it can act as a dynamic form. Example EX37A demonstrates how to use the new CHtmlView class in a vanilla MDI application. Follow these steps to create the EX37A example: Run AppWizard and create \vcpp32\ex37a\ex37a. Choose New from Visual C++'s File menu. Then click the Projects tab, and select MFC AppWizard (exe). Accept all defaults, except in Step 6 choose CHtmlView as the Base Class, as shown here.
Edit the URL to be loaded. In the CEx37aView::OnInitialUpdate function, you will see this line: Navigate2(_T("http://www.microsoft.com/visualc/"),NULL,NULL); You can edit this line to have the application load a local page or a URL other than the Visual C++ page. Compile and run. Figure 37-3 shows the application running with the default Web page.
- 650 -
Figure 37-3. The EX37A example. Now let's create a sample that really shows how to use DHTML with MFC. EX37B creates a CHtmlView object and a CListView object separated by a splitter. The example then uses DHTML to enumerate the HTML elements in the CHtmlView object and displays the results in the CListView object. The end result will be a DHTML explorer that you can use to view the DHTML object model of any HTML file. Here are the steps to create EX37B: Run AppWizard and create \vcpp32\ex37b\ex37b. Choose New from Visual C++'s File menu. Then click the Projects tab, and select MFC AppWizard (exe). Accept all the defaults but three: select Single Document, select Windows Explorer in Step 5, and select CHtmlView as the Base Class in Step 6. The options that you should see after finishing the wizard are shown in the graphic below.
Change the CLeftView to be a CListView derivative. By default, AppWizard makes the CLeftView of the splitter window a CTreeView derivative. Open the LeftView.h file, and do a global search for CTreeView and replace it with CListView. Open LeftView.cpp and do the same find and replace (Hint: Use Edit/Replace/Replace All.)
- 651 Edit the URL to be loaded. In the CEx37bView::OnInitialUpdate function, change the URL to res://ie4tour.dll/welcome.htm. Add a DoDHTMLExplore function to CMainFrame. First add the fol-lowing declaration to the MainFrm.h file: virtual void DoDHTMLExplore(void); Now add the implementation for DoHTMLExplore to MainFrm.cpp. void CMainFrame::DoDHTMLExplore(void) { CLeftView *pListView = (CLeftView *)m_wndSplitter.GetPane(0,0); CEx37bView * pDHTMLView = (CEx37bView *)m_wndSplitter.GetPane(0,1); //Clear the listview pListView->GetListCtrl().DeleteAllItems(); IDispatch* pDisp = pDHTMLView->GetHtmlDocument(); if (pDisp != NULL ) { IHTMLDocument2* pHTMLDocument2; HRESULT hr; hr = pDisp->QueryInterface( IID_IHTMLDocument2, (void**)&pHTMLDocument2 ); if (hr == S_OK) { IHTMLElementCollection* pColl = NULL; hr = pHTMLDocument2->get_all( &pColl ); if (hr == S_OK && pColl != NULL) { LONG celem; hr = pColl->get_length( &celem ); if ( hr == S_OK ) { for ( int i=0; i< celem; i++ ) { VARIANT varIndex; varIndex.vt = VT_UINT; varIndex.lVal = i; VARIANT var2; VariantInit( &var2 ); IDispatch* pDisp; hr = pColl->item( varIndex, var2, &pDisp ); if ( hr == S_OK ) { IHTMLElement* pElem; hr = pDisp->QueryInterface( IID_IHTMLElement, (void **)&pElem); if ( hr == S_OK )
- 652 { BSTR bstr; hr = pElem->get_tagName(&bstr); CString strTag = bstr; IHTMLImgElement* pImgElem; //Is it an image element? hr = pDisp->QueryInterface( IID_IHTMLImgElement, (void **)&pImgElem ); if ( hr == S_OK ) { pImgElem->get_href(&bstr); strTag += " - "; strTag += bstr; pImgElem->Release(); } else { IHTMLAnchorElement* pAnchElem; //Is it an anchor? hr = pDisp->QueryInterface( IID_IHTMLAnchorElement, (void **)&pAnchElem ); if ( hr == S_OK ) { pAnchElem->get_href(&bstr); strTag += " - "; strTag += bstr; pAnchElem->Release(); } }//end of else pListView->GetListCtrl().InsertItem( pListView->GetListCtrl() .GetItemCount(), strTag); pElem->Release(); } pDisp->Release(); } } } pColl->Release(); } pHTMLDocument2->Release(); } pDisp->Release(); } } Here are the steps that this function takes to "explore" the HTML document using DHTMLs: First DoHTMLExplore gets pointers to the CListView and CHtmlView views in the splitter window. Then it makes a call to GetHtmlDocument to get an IDispatch pointer to the DHTML document object. Next DoHTMLExplore gets the IHTMLDocument2 interface.
- 653 With the IHTMLDocument2 interface, DoHTMLExplore retrieves the all collection and iterates through it. In each iteration, DoHTMLExplore checks the element type.If the element is an image or an anchor, DoHTMLExplore retrieves additional information such as the link for the image. The all collection loop then places the textual description of the HTML element in the CListView object. Make sure that Mainfrm.cpp includes mshtml.h. Add the following line to the top of Mainfrm.cpp so that the DoHTMLExplore code will compile. #include <mshtml.h> Add a call to DoHTMLExplore. For this example, we will change the CEx37bApp::OnAppAbout function to call the DoDHTMLExplore function in the ex37b.cpp file. Replace the existing code with the following boldface code: void CEx37bApp::OnAppAbout() { CMainFrame * pFrame = (CMainFrame*)AfxGetMainWnd(); pFrame->DoDHTMLExplore(); } Customize the list view. In the CLeftView::PreCreateWindow function (LeftView.cpp), add this line: cs.style |= LVS_LIST; Compile and run. Compile and run the sample. Press the "?" toolbar item, or choose Help/About to invoke the explore function. Figure 37-4 shows the EX37B example in action.
Figure 37-4. The EX37B example in action. Now that you've seen how to use DHTML and MFC, let's look at how ATL implements DHMTL support. 305.2 ATL and DHTML ATL's support for DHTML comes in the form of an HTML object that can be embedded in any ATL ActiveX control. EX37C creates an ATL control that illustrates DHTML support. To create the example, follow these steps: Run the ATL COM AppWizard and create \vcpp32\ex37c\ex37c. Choose New from Visual C++'s File menu. Then click the Projects tab, and select ATL COM AppWizard. Choose Executable as the server type. Insert an HTML control. In ClassView, right-click on the ex37c classes item and select New ATL Object. Select Controls and HTML Control as shown in the graphic below.
- 654 -
Click Next and fill in the C++ Short Name as shown here.
If you look at the IDHTMLUI object, you will see this stock implementation of the OnClick handler: STDMETHOD(OnClick)(IDispatch* pdispBody, VARIANT varColor) { CComQIPtr
- 655 -
Figure 37-5. EX37C ActiveX control. 305.3 For More Information… We hope this introduction to DHTML has you thinking of some ways to use this exciting new technology in your Visual C++ applications. The possibilities are endless: completely dynamic applications, applications that update from the Internet, client/server ActiveX controls, and many more. If you would like to learn more about DHTML, we suggest the following resources: Inside Dynamic HTML by Scott Isaacs (Microsoft Press, 1997) Dynamic HTML in Action by William J. Pardi and Eric M. Schurman (Microsoft Press, 1998) The Internet SDK (an excellent resource on DHTML and other Microsoft technologies) www.microsoft.com (several areas discuss DHTML) Chapter Thirty-Eight 306. Visual C++ for Windows CE In early 1997, Microsoft released a new version of the Windows family of operating systems named Windows CE. Original equipment managers (OEMs) are the target audience for Windows CE. OEMs create small, portable devices —such as hand-held computers—and embedded systems. A myriad of different operating systems, a lack of strong development tools, and a maze of user interfaces have plagued both the portable-device and embedded system markets. In the past, these problems limited the use of these systems and restricted the availability of inexpensive software applications. At the time of this writing, Windows CE support for Visual C++ 6.0 was not available. All screen shots and samples programs in this chapter were created using Visual C++ for Windows CE 5.0. Microsoft hopes that Windows CE can do for the embedded and handheld markets what Windows did for the desktop PC industry. Based on the target audience, you can probably guess that Windows CE has different design goals than Windows 98 and Windows NT. One goal was modularity: if an OEM is using Windows CE in an embedded device for a refrigerator, the keyboard and graphics output modules are not required. The OEM does not pay a penalty for modules not used by the application of Windows CE. To date, there have been two major releases of Windows CE. The first release was primarily for Handheld PCs (H/PCs) and was limited to noncolor applications. Windows CE 1.0 lacked many advanced Win32 features such as COM and ActiveX, large chunks of GDI, and many Windows controls. Win- dows CE 2.0 was released in late 1997 and added support for a variety of new device types, color, COM and ActiveX technology, and also a Java virtual machine.
- 656 Before we look at the details of the Win32 support in Windows CE, let's examine some of the existing device types to get a feel for possible Windows CE applications. 306.1 Windows CE devices Currently the best known Windows CE devices are the H/PCs such as those available from HP, Sharp, Casio and a variety of vendors. Figure 38-1 shows a typical H/PC machine.
Figure 38-1. A typical Handheld PC. H/PCs currently have display resolutions anywhere from 480 by 240 pixels to as large as 640by 240 pixels. They typically have a keyboard, infrared port, serial port, and microphone. The standard software on these devices includes: Pocket Word, Pocket Excel, Internet Explorer, Outlook Express, and other scaled-down Microsoft productivity applications. A smaller device called a Palm-size PC (P/PC) shown in Figure 38-2 is completely pen-based and does not have a keyboard. Screen sizes are also smaller (240 by 320 pixels) and only gray-scale displays are currently available for P/PCs.
Figure 38-2. A Palm-size PC. Note: At the time of this book's publication, MFC is not supported on the Palm-size PC platform. The SDK for Palm-size PCs and embedded development is also not included with the Visual C++ for Windows CE product. These SDKs must be downloaded from the Microsoft website http://www.microsoft.com. Perhaps the most exciting Windows CE devices now reaching the market are embedded applications. For example, the CD player from Clarion shown in Figure 38-3 features a GUI, voice recognition, cellular phone support, and a variety of other features that are changing the way we think about electronic devices and appliances. Unlike Windows 95, which only supports Intel processors, and Windows NT, which only supports the Intel and Alpha processors, Windows CE supports a myriad of embeddable 32-bit processors such as the MIPS chip, Hitachi chips,
- 657 and a variety of other chip sets. This flexibility dramatically increases the potential reach of Windows CE in the embedded market.
Figure 38-3. An automotive CD player powered by Window CE. Because all of these devices are based on Windows CE, you can write applications for them using a subset of the familiar Win32 APIs that you have learned throughout this book. Before we investigate Visual C++ programming for Windows CE, let's take a look at the subset of Win32 implemented by Windows CE. 306.2 Windows CE vs. Windows 98 and Windows NT Each Windows CE platform (H/PC, Palm-size PC and embedded) supports various subsets of the Win32 API based on which Windows CE modules are loaded. The "core" functionality is fairly static among devices—GDI, windows, and controls and so on, but some user input functions are different. (On a Palm-size PC, for example, it doesn't make sense to have keyboard functions.) The Win32 support in Windows CE matches the primary design goal of Windows CE: keep everything as small as possible. Whenever a duplicate Win32 call exists, Windows CE provides only the most general API function. For example, instead of implementing both TextOut and ExtTextOut, Windows CE supports only the more flexible ExtTextOut, because in this single API you have the functionality of both. Another interesting aspect of the Win32 Windows CE implementation is that only Unicode functions and strings are supported. You need to be sure to wrap your Windows CE MFC strings with the _T macro. At the GDI layer, Windows CE supports a relatively small subset of the implementations found in Windows 95, Windows 98, and Windows NT. The key groups of GDI Win32 API functions not implemented in Windows CE are mapping modes, arcs, chords, metafiles, and Bézier curves. When you draw lines, you must use PolyLine because MoveTo and LineTo are not supported. Cursor and mouse handling in Windows CE can also be different from what you are accustomed to on larger systems. Version 2.0 of Windows CE adds many key features that allow for parity with its big brothers, such as color support, TrueType font support, printing, and memory DCs. Many other nuances of the various GDI implementations are well documented in the Windows CE SDK, which is shipped as part of the Visual C++ for Windows CE product. Windows CE also has some major differences in windowing. Perhaps the largest difference is the fact that only SDI applications are supported. Thus, porting existing MDI applications to Windows CE is relatively difficult. Another interesting windowing fact is that Windows CE windows are not resizable. Since there are a wide variety of screen resolutions, you should programmatically size windows based on the resolution of the display, instead of using static layouts. Most of the standard Windows 95, Windows 98, and Internet Explorer 4.0 common controls are available on Windows CE, except for the following: the rich edit control, the IP control, ComboBoxEx controls, and the hot key control. Windows CE actually introduces a new common control—the command bar. Command bars implement a hybrid menu bar and toolbar that occupies considerably less space than the standard menu bar and toolbar configuration found in most desktop applications. Figure 38-4 shows an example of a Windows CE command bar. Figure 38-4. A Windows CE command bar. ActiveX and COM are supported in Windows CE 2.0, but only for in-process COM objects such as ActiveX controls. Multithreading, memory management, exception handling, and most other areas of Win32 are fully supported —with a few caveats—by Windows CE. Now that you're familiar with the basics of Windows CE, let's take a look at the Visual C++ development environment for this new operating system.
- 658 307. Visual C++ for Windows CE Visual C++ for Windows CE is an add-on to Visual C++. When you install C++ for Windows CE, it extends the Visual C++ environment by adding several Windows CE-specific features: An Intel-based Windows CE emulation environment New targets for each of the Windows CE supported processors (MIPS/SH and the emulation environment) New AppWizards for Windows CE applications A Windows CE compatible port of MFC A Windows CE compatible port of ATL Tools for remote execution and debugging of Windows CE applications on actual devices One interesting aspect of Visual C++ for Windows CE is the fact that it also supports the older 1.0 and 1.01 versions of Windows CE. Figure 38-5 shows the Windows CE operating system and processor configuration bars that have been added to Visual C++. While the environment lets you remotely run and debug your applications on a connected Windows CE device, it also includes a very powerful Windows CE emulation environment. The Windows CE emulator (WCE) is an Intel-based software-only version of Windows CE that runs on your desktop and gives you the convenience of being able to run and test your applications on your development machine. Of course, to ensure that your applications work correctly, you still need to test on real devices, but the emulator takes much of the pain out of the early compile and debug stages of Windows CE development. Figure 38-6 shows the emulation environment in action. There are four Windows-CE-specific AppWizards that ship with Visual C++ for WCE: WCE ATL COM AppWizard—An ATL-based COM object project WCE MFC ActiveX ControlWizard—An MFC ActiveX control project WCE MFC AppWizard (dll)—An MFC DLL project WCE MFC AppWizard (exe)—An MFC executable project
Figure 38-5. Visual C++ for the Windows CE environment.
- 659 Figure 38-6. The Windows CE emulator. The WCE AppWizards are basically the same as their big brother Win32 counterparts, except that they have different features that take advantage of the Windows CE operating system, such as the Windows CE help environment. Figure 38-7 shows the first three steps of the Windows CE MFC executable AppWizard. Notice that there are only two project types: SDI and dialog-based. Notice also the variety of Windows-CE-specific options that you can choose from.
- 660 -
Figure 38-7. The Windows CE MFC AppWizard. 307.1 MFC for Windows CE Visual C++ for Windows CE ships with a smaller version of MFC named Mini MFC. To get a feel for which MFC classes are and are not supported, see Figure 38-8—the MFC for Windows CE hierarchy chart. The grayed out classes are not supported on Windows CE. Several classes have been added to Mini MFC, including classes for command bars, object store, and socket classes. Windows CE functions provide the command bars in the Mini MFC CFrameWnd class. Instead of implementing a file metaphor, Windows CE provides an object store. Several new MFC classes were added to give the Windows CE developer access to the object store: CCeDBDatabase—Encapsulates a database in the object store CCeDBEnum—Enumerates the databases in the object store CCeDBProp—Encapsulates a database property (Database properties are data items consisting of an applicationdefined property identifier, a data-type identifier, and the data value.) CCeDBRecord—Provides access to a record in the database In addition to these data store classes, a new class CCeSocket is provided. CCeSocket implements an asynchronous CSocket derivative.
- 661 -
Figure 38-8. MFC for Windows CE hierarchy chart. 307.2 Using Mini MFC
- 662 Let's look at a couple of examples to get a feel for the Mini version of MFC. In example EX38A, we will create a basic SDI application that draws some text in the view and displays a dialog when the user presses the left mouse button (or taps the screen on a Windows CE machine). EX38A is similar to the EX06A example, so you can compare the steps in creating a similar application for Windows 98, Windows NT, and Windows CE. For this example, you will create a simulated expense-tracking application for Windows CE—a perfect candidate for portable computing. For demonstration purposes, we will focus on creating a dialog that allows the user to enter expense information. Figure 38-9 shows the application running in the emulation environment.
Figure 38-9. The EX38A expense-tracking application. Here are the steps to create the EX38A example: Run the MFC Executable for Windows CE AppWizard to produce \vcpp32\ex38a\ex38a. Select the WCE MFC AppWizard project type and accept all the defaults. The options and the default class names are shown here. Select the WCE x86em Debug configuration. Choose the Set Active Configuration command from the Set menu and then select Ex38a-Win32 (WCE x86em) Debug from the list. Click OK. This configuration will allow you to work with the desktop emulator instead of working remotely with a connected Windows CE device. (If you have a connected Windows CE device you can select its configuration instead. For example, select Ex38a —Win32 [WCE SH] Debug if you have a connected HP 620LX.)
Use the dialog editor to create a dialog resource. Choose Resource from the Insert menu, select Dialog, and then click New. The dialog editor assigns the ID IDD_DIALOG1 to the new dialog. Change the dialog caption to
- 663 The Dialog that Ate Windows CE! You can resize the dialog to be wider but not much taller. (Windows CE displays are much wider than they are tall.) Remove the OK and CANCEL buttons and add an OK caption button. Since screen real estate is at a premium in Windows CE, we can save a good deal of space in our dialog by deleting the OK and CANCEL buttons. For Cancel functionality, users can use the close button that is part of the dialog caption. Windows CE also supports an OK caption button that you can create by setting the dialog's property bar. To set the property, open the properties editor for the dialog, click the Extended Styles tab, and then check the Caption Bar OK (WCE Only) option as shown here.
(You might also need to set the dialog's Visible property on the More Styles tab.) Add the dialog's controls. Add the following controls shown in Figure 38-9 and accept the default names: A static control and an edit control for a name A static control and an edit control for an amount A static control and a drop-down combo box with for an expense type A City group box with three radio buttons labeled Atlanta, Detroit, and Chicago A Payment Option group box with three check boxes labeled Check, Credit Card, and Cash. Add the CEx38aDialog class. After adding the controls, double-click on the dialog. ClassWizard detects that you have created a dialog resource and asks whether you want to create a class for the dialog. Click OK and accept the defaults to create the CEx06aDialog class. Program the controls. Use ClassWizard to create the following member variables in the dialog class: m_pComboBox—a member variable used to configure the expense type combo box m_pProgressCtrl—A member variable for the progress control If you need a refresher on how to program modal dialogs, please refer to Chapter 6. Next, add the following code to the CEx38aDialog::OnInitDialog handler to initialize the controls: BOOL CWindowsCEDlg::OnInitDialog() { CDialog::OnInitDialog(); m_pComboBox.AddString(_T("Travel")); m_pComboBox.AddString(_T("Meal")); m_pComboBox.AddString(_T("Cab Fare")); m_pComboBox.AddString(_T("Entertainment")); m_pComboBox.AddString(_T("Other")); m_pProgressCtrl.SetPos(50); return TRUE; } Notice that you must use the _T macro whenever you have inline strings. Connect the dialog to the View. In ClassWizard, select the CEx38aView class and use ClassWizard to add the OnLButtonDown member function. Write the code for the OnLButtonDown handler. Add the boldface code below: void CEx38aView::OnLButtonDown(UINT nFlags, CPoint point) { CWindowsCEDlg dlg; dlg.DoModal(); CView::OnLButtonDown(nFlags, point); }
- 664 Add code to the virtual OnDraw function in file ex38aView.cpp. The CDC::TextOut function used in previous examples is not supported on Windows CE, so we need to use the CDC::DrawText function as shown here: void CEx38aView::OnDraw(CDC* pDC) { CRect rect; GetClientRect(rect); pDC->SetTextColor(::GetSysColor(COLOR_WINDOWTEXT)); pDC->SetBkMode(TRANSPARENT); pDC->DrawText(_T("Press the left mouse button here."), rect, DT_SINGLELINE); } Add the ex38aDialog.h header file to ex38aView.cpp. Insert the include statement #include "ex38aDialog.h" at the top of the ex38aView.cpp source file, after the statement #include "ex38aView.h" Build and run the application. The EX38A application should appear as shown in Figure 38-10. If everything works satisfactorily, you can change the configuration for a real Windows CE device. Then you can run (or debug) the application remotely on the device to ensure it works in a real-world situation.
Figure 38-10. The EX 38A application running in the Windows CE emulator. As you can tell from the EX38A application, programming for Windows CE is very similar to programming for Windows 98 and Windows. Many Windows CE developers are interested in porting existing applications to the Windows CE environment. The next section shows you how to tackle this problem. 307.3 Porting an Existing MFC Application to Windows CE In example EX38B we will port an existing application (EX06B from Chapter 6) from Windows 98 and Windows NT to Windows CE. We chose the EX06B sample because it is an SDI application. If you are porting an MDI application to Windows CE, we recommend that you first convert it into an SDI application (or a series of SDI applications, if you have several views) and then port it to Windows CE. Here are the steps for converting EX06B: Run the MFC AppWizard to produce \vcpp32\ex38b\ex38b. Select the WCE MFC AppWizard project type and accept all defaults. (You might have thought that we could copy the EX06B workspace and then add a Windows CE configuration. However, it is actually easier to start with a WCE MFC AppWizard project instead because there are many complicated build settings that the wizard automatically sets up for you.) Using Windows Explorer, copy the EX06B files to the EX38B directory. Be sure to copy the following files: Ex06bDialog.h, Ex06bDialog.cpp, Ex06bDoc.h, Ex06bDoc.cpp, ex06bView.h, and ex06bView.cpp. Insert the new files into the project. Choose the Add To Project/Files command from the Project menu and insert the files from step 2 into the project. Copy the dialog and Icon resources from EX06B. Choose Open from the File menu and select \vcpp32\ex06b\ex06b\ex06b.rc. Drag and drop IDD_DIALOG1 from ex06b.rc into the EX38B project. Next, drag
- 665 and drop the color icon resources (IDI_BLACK, IDI_BLUE, and so on) from the ex06b.rc file into the EX38B project. Build the application and repair any compiler errors or warnings. Now that you have moved the key files that you need (the document, view, and dialog classes) from the EX06B application to the Windows CE EX38B application, try to build the project. You should see a number of errors that can be fixed with the following steps: Change all references to the file ex06b.h to ex38b.h Make sure that all inline strings use the _T macro. For example, in Ex06bDialog.cpp the line TRACE("updating trackbar data members\n"); should be changed to use the _T macro as follows: TRACE(_T("updating trackbar data members\n")); Convert any other non-Unicode strings to Unicode. (This is the most frequently encountered porting problem.) Ex06bView::OnDraw uses the unsupported CDC::TextOut member function. Change it to use DrawText as follows: CRect rect; GetClientRect(rect); pDC->SetTextColor(::GetSysColor(COLOR_WINDOWTEXT)); pDC->SetBkMode(TRANSPARENT); pDC->DrawText(_T("Press the left mouse button here."), rect, DT_SINGLELINE); Replace the wizard-generated view with the real view. Open the ex38b.cpp file and do a global search and replace: Change CEx38bDoc to CEx06bDoc Change CEx38bView to CEx06bView Also, make sure that ex38b.cpp #includes both the ex06bView.h and ex06bDoc.h header files. Using the dialog editor, adjust the dialog's layout for Windows CE. As it stands, the dialog is far too tall for Windows CE. You can rearrange it by following these steps: Remove the OK and CANCEL buttons and use the OK window button (see step 4 in the EX38A example). Move the progress bar controls, sliders, and spinners closer together at the top of the dialog. Move the list control and tree control closer together. To save vertical space, move the current selection static controls to the left of both the tree and list control. Size the dialog to fit on a smaller screen. Clean up the project. Now you can remove the document and view classes created by the MFC AppWizard by selecting the files in FileView and pressing the Delete key. Build and test. In eight easy steps, you have converted a small MFC application from Windows 98 and Windows NT to Windows CE. Figure 38-11 shows EX38B running in emulation mode for Window CE.
Figure 38-11. The EX38B application running in the Windows CE emulator. 307.4 ATL and Windows CE In addition to Mini MFC, Visual C++ for Windows CE also provides a Windows CE-friendly version of ATL. ATL is already a lightweight framework, so Microsoft didn't need to reduce the feature set for size constraints. However, there are some areas of COM not covered by Windows CE 2.0 that impact the feature set of ATL for Windows CE.
- 666 Windows CE doesn't support the apartment-threading model, so ATL for Windows CE doesn't implement the CComApartment, CComAutoThreadModule, CComClassFactoryAutoThread, or CComSimpleThreadAllocator classes. Windows CE also doesn't support asynchronous monikers, so ATL for Windows CE doesn't implement the IBindStatusCallbackImpl or CBindStatusCallback classes. A variety of other ATL class member functions that behave differently on Windows CE are documented in the Visual C++ for Windows CE documentation. In addition to ATL, a Windows CE version of the ATL Wizard is provided. Figure 38-12 shows the wizard that has only one option: to use MFC or not.
Figure 38-12. The WCE ATL COM AppWizard. When you write ActiveX controls for Windows CE, remember that they are binary objects and therefore processordependent. Depending on the devices you plan to support, you might have to provide several versions of the control (MIPS or SH, for example). 308. For More Information on Windows CE… Windows CE is an exciting new environment for Windows developers to explore. The ability to leverage your knowledge of Windows, Win32, and Visual C++ makes Windows CE an extremely appealing and easy-to-work-with environment. Programming Windows CE by Doug Boling (Microsoft Press, 1998) is an excellent resource for learning the details of Windows CE. Microsoft is developing Windows CE and related technologies at an incredible pace. To stay up to date, we recommend using the Web. Keep an eye on these sites: http://www.microsoft.com/windowsce. This site provides news and information about the Windows CE operating system, devices and development tools. You can sign up for a useful newsletter here. http://www.microsoft.com/msdn. The Microsoft Developer Network (MSDN) has tons of Windows CE articles. They are all online and searchable.
Your Information Source We're the independent publishing division of Microsoft Corporation—your source of inside information and unique perspectives about Microsoft products and related technologies. We've been around since 1984, and we offer a complete line of computer books—from self-paced tutorials for first-time computer users to advanced technical references for professional programmers. mspress.microsoft.com Appendix A 309. Message Map Functions in the Microsoft Foundation Class Library HANDLERS FOR WM_COMMAND MESSAGES Map Entry
Function Prototype
- 667 ON_COMMAND(
afx_msg void memberFxn();
ON_COMMAND_EX(
afx_msg BOOL memberFxn(UINT);
ON_COMMAND_EX_RANGE(
afx_msg BOOL memberFxn(UINT);
ON_COMMAND_RANGE(
afx_msg void memberFxn(UINT);
ON_UPDATE_COMMAND_UI(
afx_msg void memberFxn(CCmdUI*);
ON_UPDATE_COMMAND_UI_RANGE (
afx_msg void memberFxn(CCmdUI*);
ON_UPDATE_COMMAND_UI_REFLECT (<memberFxn>)
afx_msg void memberFxn(CCmdUI*);
HANDLERS FOR CHILD WINDOW NOTIFICATION MESSAGES Map Entry
Function Prototype
Generic Control Notification Codes ON_CONTROL(<wNotifyCode>,
afx_msg void memberFxn();
ON_CONTROL_RANGE(<wNotifyCode>,
afx_msg void memberFxn(UINT);
ON_CONTROL_REFLECT(<wNotifyCode>, <memberFxn>)
afx_msg void memberFxn();
ON_CONTROL_REFLECT_EX(<wNotifyCode>, <memberFxn>)
afx_msg BOOL memberFxn();
ON_NOTIFY(<wNotifyCode>,
afx_msg void memberFxn(NMHDR*, LRESULT*);
ON_NOTIFY_EX(<wNotifyCode>,
afx_msg BOOL memberFxn(UINT, NMHDR*, LRESULT*);
ON_NOTIFY_EX_RANGE(<wNotifyCode>,
afx_msg BOOL memberFxn(UINT, NMHDR*, LRESULT*);
ON_NOTIFY_RANGE(<wNotifyCode>,
afx_msg void memberFxn(UINT, NMHDR*, LRESULT*);
ON_NOTIFY_REFLECT(<wNotifyCode>, <memberFxn>)
afx_msg void memberFxn(NMHDR*, LRESULT*);
ON_NOTIFY_REFLECT_EX(<wNotifyCode>, <memberFxn>)
afx_msg BOOL memberFxn(NMHDR*, LRESULT*);
User Button Notification Codes ON_BN_CLICKED(
afx_msg void memberFxn();
ON_BN_DOUBLECLICKED(
afx_msg void memberFxn();
ON_BN_KILLFOCUS(
afx_msg void memberFxn();
ON_BN_SETFOCUS(
afx_msg void memberFxn();
Combo Box Notification Codes ON_CBN_CLOSEUP(
afx_msg void memberFxn();
ON_CBN_DBLCLK(
afx_msg void memberFxn();
- 668 ON_CBN_DROPDOWN(
afx_msg void memberFxn();
ON_CBN_EDITCHANGE(
afx_msg void memberFxn();
ON_CBN_EDITUPDATE(
afx_msg void memberFxn();
ON_CBN_ERRSPACE(
afx_msg void memberFxn();
ON_CBN_KILLFOCUS(
afx_msg void memberFxn();
ON_CBN_SELCHANGE(
afx_msg void memberFxn();
ON_CBN_SELENDCANCEL(
afx_msg void memberFxn();
ON_CBN_SELENDOK(
afx_msg void memberFxn();
ON_CBN_SETFOCUS(
afx_msg void memberFxn();
Check List Box Notification Codes ON_CLBN_CHKCHANGE(
afx_msg void memberFxn();
Edit Control Notification Codes ON_EN_CHANGE(
afx_msg void memberFxn();
ON_EN_ERRSPACE(
afx_msg void memberFxn();
ON_EN_HSCROLL(
afx_msg void memberFxn();
ON_EN_KILLFOCUS(
afx_msg void memberFxn();
ON_EN_MAXTEXT(
afx_msg void memberFxn();
ON_EN_SETFOCUS(
afx_msg void memberFxn();
ON_EN_UPDATE(
afx_msg void memberFxn();
ON_EN_VSCROLL(
afx_msg void memberFxn();
List Box Notification Codes ON_LBN_DBLCLK(
afx_msg void memberFxn();
ON_LBN_ERRSPACE(