Porting Server Applications from UNIX to Windows NT
Thomas Becker
UNIX is nice, but NT is growing in popularity. Here's some useful lore if you have to make the switch.
Introduction
Because of Windows NT's recent success in the operating system market, porting existing software from UNIX to NT has become a hot trend in software development. What makes these porting projects difficult and interesting is the fact that the Windows NT API is radically different from the world of UNIX system calls. It is clearly impossible to give a comprehensive guide to porting from UNIX to NT in an article such as this, if such a thing is possible at all. Instead, I focus on problems that arise typically, but not exclusively, when porting server applications. These server applications are of the kind that wait for clients to connect over a network and then communicate with these clients. A typical example is a WWW or ftp server. As an illustration, I provide a reusable wrapper class for communication via named pipes.
Of Daemons and Services
In a UNIX environment, an application that runs in the background and serves clients is called a daemon. Under Windows NT, a program that can run when no interactive user is logged on is called a service. Services must be written following a certain pattern, and they must be registered with the system before they can run. A registered service appears in the Services dialog box that can be opened from the control panel. There are samples available that show how to write a service. One such sample is SIMPLE SERVICE, which comes with Microsoft Visual C++. Another is "Creating a Simple Windows NT Service in C++," in the Microsoft Developer Library. The latter actually wraps the service skeleton in a C++ class.
The obvious way to go about writing a service is to first write and test the program as a console application. Turning it into a service is then a relatively easy task.
Multiple Threads vs. Multiple Processes
In a traditional UNIX environment, a program that needs to service several clients at a time uses multiple processes, one for each client. The same thing is possible under Windows NT using the CreateProcess API. This option should be considered when porting an existing program from UNIX to NT. Unfortunately, however, CreateProcess has a quite different effect than UNIX's fork system call. Whereas fork creates a copy of the current process with its entire run-time state, CreateProcess can only start a brand new process. If, for a example, a file handle from the parent process is to be used in the child process, the handle must first be declared inheritable upon creation. Then the CreateProcess call must be explicitly told to allow inheriting of all inheritable handles. Finally, the actual value of the handle must be communicated to the child process through the command line or some IPC mechanism. This usually makes one-to-one porting of existing UNIX programs difficult.
Windows NT offers multithreading as an alternative to multiple processes. A new thread is essentially just another program counter with an associated stack within an existing process. Threads thus account for much less overhead than separate processes. Also, different threads within the same process can easily communicate with each other via global variables, thus eliminating the need for IPC mechanisms. For the same reason, threads have nothing to protect them from interfering with each other in memory. Let us not forget that the very reason processes and virtual memory were invented was to avoid interference; so in a sense, multithreading is a step back in history! (For other multithreading problems, see the sidebar. )
For a typical server application that serves several clients simultaneously, a multithreaded architecture makes a good choice. This is because the overall structure of the program is not too complex and the danger of threads causing trouble among each other is low.
Sockets vs. Named Pipes
A UNIX daemon that is meant to serve clients over a network will use sockets to communicate with its clients, and the underlying protocol will be TCP/IP. It is possible to use the same mechanism under Windows NT using the WinSock library functions. Socket programming under NT is very much the same as under UNIX. Moreover, WinSock offers the use of alternate network protocols such as NetBEUI.
In addition, Windows NT offers named pipes for network communication. Named pipes use the NetBIOS interface, which in turn can be bound to a variety of protocols. These bindings can be configured through the Network applet in NT's control panel. An important difference between sockets and named pipes concerns client authentification.
When a client attempts to connect to a socket, the system performs no authentification whatsoever. When a client tries to connect to a named pipe over the network, however, Windows NT cranks up the same mechanism that it uses to authenticate a user connecting to a shared directory. To allow a connection, NT requires that the client perform a net logon to the server machine. Here's what NT does to establish a net logon:
1. Check if the client machine has already successfully performed a net logon to the server machine. If so, use that net logon.
2. Else, take the current username and password from the client machine to the server machine and see if the same username with the same password exists on the user machine. If so, log onto the server machine under that account.
3. Else, see if the guest account is enabled on the server machine. If so, try to log on as guest with an empty password.
If none of the above succeeds during an attempt to connect to a shared directory, the user is prompted to enter a username and password for the connection. You can use the WNetAddConnection2 API to achieve such an explicit connection from your own program. Unfortunately, WNetAddConnection2 does not work for named pipes, so there is no way to connect to a named pipe using a specific account.
All this leaves us with the following considerations when choosing between sockets and named pipes:
- If you're in a heterogeneous network environment with more than one operating system involved, sockets are probably the best choice because of their universal availability.
- If your clients are all running Windows NT (or other Microsoft products), then you may still opt for sockets because of the net logon problem. In a low-security environment, you can simply skip authentification altogether. Or, if you need some sort of authentication and want to use sockets, you can ask the client to send a name and password and use the LogonUser API to check if that account exists on your server machine.
- There is one situation where named pipes could be just what you're looking for. When your clients are all members of the same Windows NT domain as the server, or of trusted domains, then Step 2 in the net logon procedure will always succeed, because accounts are verified in the same security database for all machines involved. Access to your server will thus be granted precisely to the members of your domain and trusted domains.
The successful net logon is only the first step in obtaining client access to a named pipe. In addition, the named pipe itself must grant access to the account under which the client has logged on. By default, only the account that started the process that created the pipe has access. The CreateAndConnectPipes member function of the CNamedPipeArray class that accompanies this article shows how to grant everyone access to a named pipe.
If you find it hard to decide between sockets and named pipes, you may want to consider using Remote Procedure Calls (RPC). RPC is really meant for distributed computing. The advantage of Microsoft's implementation of RPC, however, is that it lets you choose the underlying Interprocess Communication mechanism and protocol at run time.
Multithreaded Server Architectures
Now I examine the problem of writing a multithreaded server. The server will have a listening thread that sits in an infinite loop, blocking on a function such as CNamedPipeArray::Listen. Each time a client connects, the server passes the pipe index on to a worker thread, which takes care of the client and then disconnects the communication device to make it available for a new client. The question is how to handle the creation of worker threads.
The most straightforward solution is to let the listening thread create a new worker thread for each client. In practice, this solution is often unsatisfactory. For example, consider the common situation in which a server pulls its data from a database for which the customer holds a limited number of licenses. Clearly no one wants to pay the overhead of starting a thread for each client. Instead, the server would, at program start, create as many worker threads as there are database licenses.
Often the customer will want to have a certain number of clients queued when all the worker threads are busy. Only when this queue is full may clients be rejected on the level of the network protocol. This sort of strategy calls for placing clients in a thread-safe FIFO queue. The class CPassiveQueue, discussed below, provides such a queue.
A passive queue has the disadvantage that once a client is in there, it will stay there until a worker thread picks it up. That is good enough in many cases, because the client can always decide to give up and close its end of the communication device. Eventually, a worker thread will pick up the dead connection from the queue and throw it away. On the other hand, if you wish to give the server control over the timeouts, you need a queue that removes clients after a timeout interval. The class CActiveQueueWithTimeout, also discussed below, provides such a queue.
A Named Pipe Wrapper Class
The file NamedPipeArray.h provides a ready-to-use class template that wraps the server side of an array of named pipe instances. The wrapper makes using the server end of a named pipe quite similar to using a socket. A Listen member function waits for clients to connect. It returns an index which can be used like a socket descriptor to communicate with the client via the Read and Write member functions. Typically, the Listen function sits in an infinite loop in a main thread that does nothing but pass the clients on to worker threads that take care of them.
The user of the class must pass the handle to a Win32 event object to the constructor. Signaling this event will cause all member functions of the class to return immediately. This technique is an instance of a Win32 programming pattern in which you use an event to achieve what you would have done with a signal under UNIX. I say more about this shortly.
The template argument to the class is the number of pipe instances. You can easily change the source code so that this number can be set at run time: make the template argument a class member that is set by the constructor, find the places where it occurs as the length parameter in array declarations, and replace these array declarations with heap allocations using the new operator. As is, the class does not contain any calls to new.
You don't need to know anything about named-pipe programming in order to use CNamedPipeArray. However, a few remarks are in order.
Win32 named pipes are different from UNIX named pipes in that they are strictly connection oriented. That is, a named pipe has a client and a server end, and an exchange of information takes places only when both ends of the named pipe are connected. If either client or server breaks the connection, all buffered input is discarded.
The server end creates the named pipe with the CreateNamedPipe API and starts listening for clients with the ConnectNamedPipe API. Listing 1, which is an excerpt from the NamedPipeArray.h sample file, shows an example (full source is provided at the CUJ ftp site. See p. 3 for details.). This listing shows that you can actually create several instances of a named pipe with a single name. These instances are, technically, completely independent pipes. However, they can be addressed by clients under one and the same name.
Listing 2 shows the core part of the Listen member function. The interesting part is how the wait operation can be forced to complete from the outside. Since Win32 does not have an equivalent to UNIX-style signals, the way to do it is to extend the array of objects that the function is waiting on (in this case, the events that get signaled when a client has connected to a pipe instance), by an additional exit event. The function checks if it was this event that caused the wait operation to complete.
The Read member function of CNamedPipeArray uses the same technique. This function works synchronously: when there is no client input to read, the function simply blocks, possibly with a timeout value. If it weren't for the necessity of an exit signal, Read could internally use the ReadFile API in its synchronous version. However, to break the blocking when the exit event gets signaled, the ReadFile API must be used asynchronously. It is then followed by a call to the WaitForMultipleObjects API that waits for one of two events to get signaled: the exit event and the event that signals completion of the ReadFile operation. If you need to apply this technique in your own code, you may want to look at the source code of Read in NamedPipeArray.h. The proper way of dealing with return and error codes is somewhat intricate, to say the least.
The code on the client side of the named pipe is not affected by the wrapping provided by CNamedPipeArray. See Listing 3, which is taken from the Client.cpp sample file, for an example. Information can be transferred back and forth using the ReadFile and WriteFile APIs as long as the connection persists. After that, all unread pipe input is discarded.
Thread-Safe Queues
A FIFO queue that can be accessed from multiple concurrent threads is a fairly easy but instructive example for the use of Win32 synchronization objects. Listings 4 and 5 show the bare bones version of the Insert and Retrieve functions for a FIFO queue that stores characters in a global array achTheQueue of length LEN_QUEUE. These functions have a critical section cs that guarantees that only one thread at a time does anything with the character array. More interesting, these functions use a "free-semaphore" and an "occupied-semaphore." The free-semaphore counts the free slots in the queue. This semaphore is created with a maximum and initial count of LEN_QUEUE. The occupied-semaphore counts the occupied slots in the queue. It is created with a maximum count of LEN_QUEUE and an initial count of 0.
The class CPassiveQueue that comes with this article provides a ready-to-use thread-safe FIFO queue. Its Insert and Retrieve member functions differ from the simple ones that we just discussed in that they add error handling, timeouts, and wait termination through an exit event.
A Variable Timeout Queue
As stated previously, a simple FIFO queue such as CPassiveQueue has a disadvantage: once an element is in the queue, it cannot be timed out. The class CActiveQueueWithTimeout provides a FIFO queue that allows every element placed in the queue to have an individual timeout value. When this time interval has elapsed, the element is removed from the queue. A callback function whose address is passed to the constructor of the class is then called on the timed-out element. Typically, queue elements are pipe indices or socket descriptors that represent clients. The callback function would then close the communications device, perhaps after sending a message to the client.
CActiveQueueWithTimeout objects are active in the sense that they internally start a thread which administers the queue. The thread function starts out with a multiple wait, where it waits for one of four possible things to happen:
1) The exit event is set, requesting
termination.
2) Some thread wants to insert into the queue via the
Insert member function.
3) Some thread wants to retrieve from the queue by
means of the Retrieve member function.
4) A timer for an element in the queue has gone off.
Implementing the timeouts for the individual queue elements used to be tedious. You had to use a separate thread that would sleep for the prescribed amount of time and then set an event, thus imitating a UNIX-style SIGALARM. Beginning with Windows NT 4.0 and Visual C++ 5.0, Win32 provides a new synchronization object called a waitable timer. A waitable timer is like an event that can be set to become signaled at a prescribed point in time, or periodically. For waitable timers and all the other nifty additions to Win32 to be enabled, you must set _WIN32_WINNT to the value 0x0400 with a preprocessor definition.
Summary
Porting a server from UNIX to Windows NT is typically not a straightforward process. Windows NT offers only rough equivalents to the functionality relied upon by servers running on UNIX:
- The Windows NT equivalent to the UNIX daemon is the NT service. The easiest way to create a service is to first write and test the program as a console application.
- NT provides multiple processes, as does UNIX, but NT process creation is less flexible, making one-to-one ports difficult. Multithreading is frequently a desirable alternative to multiple processes on NT. However, multithreading typically requires attention to synchronization and proper freeing of unused resources.
- WinSock on NT is very similar to UNIX sockets, and is frequently the communication method of choice. However, sockets do not perform authentication prior to connection. Named pipes can be used in Windows NT to perform authentication.
A wrapper class was presented for creating named pipes under Windows NT. This article also presented classes to create two kinds of thread-safe queues, a simple FIFO queue and a queue allowing timeouts of individual queue elements. o
Thomas Becker lives in Munich, Germany and works as a software developer and trainer. The UNIX people despise him because he knows too much about NT, Win32, and MFC. The NT folks don't trust him ever since they found a GNU Emacs on his machine. He can be reached at [email protected].