Features


Better Pipes for Windows

Bill Heyman

Named pipes are a useful way to connect clients and servers, but it helps to know the nitty gritty details of using them. This article shows how to get some nice added features, such as non-blocking I/O and graceful termination.


Introduction

Windows 95 and Windows NT provide a common way of communicating between processes on the same or different machines: named pipes. You can use named pipes to allow either half- or full-duplex communication between two different applications. The application that is waiting for an incoming connection is typically called the server. The application making the connection to the server is called the client. Client and server applications can send byte streams back and forth using named pipes.

Using the sample code in the Win32 SDK, you can quickly and easily develop a named pipe client and server application. However, this article will go beyond Microsoft's simplistic sample code to describe how to take advantage of some of the more advanced features of named pipes: waiting and retrying a connection to a named pipe server, handling read and write time-outs, and properly shutting down a server application. In addition, this article will provide a family of C++ classes that you can reuse in your applications to take advantage of these features. The sample application provided with the article uses the Win32 SDK, but does not present the familiar Windows GUI code that you see with most Windows apps. The client and server apps presented here can be invoked from the command line. It is also a fairly simple matter to add "front end" GUI code if required.

The Simple Named Pipe Model

You must have both a client and a server application to use named pipes. Although, technically, the same application could be both client and server, for the sake of illustration this article assumes that there are two separate applications that each take one of the roles. The example presented in this article provides two separate applications, called client.exe and server.exe, each taking their respective role. These applications both rely upon a named-pipe base class, nmpipe_cNamedPipe. client.exe uses a connector class derived from this base class, nmpipe_cConnector; server.exe uses a listener class derived from the base class, nmpipe_cListener. These classes are shown in Listing 1. Figure 1 represents these applications in their respective roles.

You must first run the server application so it will wait for an incoming client connection. Because the server is waiting for a connection, it is said to be passive or listening. So, when a named pipe server is run, it first creates an instance of a named pipe (with a specific name) and then waits on that instance of the named pipe for a connection.

The specific name the server gives this pipe is what uniquely identifies this pipe from any other pipe created by any other server. Using this unique name, client applications can find the correct instance of a server application. In some cases, client and server applications are coded with a hard-coded pipe name. In other cases, the application architectures provide a means of resolving a logical name to find the server's real pipe name. Since server pipe name resolution is beyond the scope of this article, I present a client and a server application with a hard-coded pipe name.

Pipe names must take the form \\computer_name\PIPE\pipe_name, where computer_name can either be the name of a specific Windows network computer name or "." (which means the current computer) and pipe_name is a unique identifier for the pipe, that follows the standard long file naming conventions. For example, "\\.\PIPE\MyPipe" identifies a pipe on the local machine, whereas \\Snoopy\PIPE\MyPipe identifies a pipe on the machine named Snoopy (which may be the local machine).

Client Access to a Server

A named pipe client application can communicate with a server as shown in Figure 2. The sample client application is shown in Listing 2. Client applications must first open a server-named pipe instance. To open the pipe instance the client executes a loop, which waits for a server-named pipe instance to become available and then attempts to open the instance. The client repeats this process until it successfully opens a pipe. The sample client application waits for a pipe and attempts to open the pipe within the constructor (Listing 3) for the object Connector, called at the top of the client application's while loop. Once open, the client can read from and write to the pipe according to the protocol expected by the server application. When it is finished using the pipe, the client flushes its output buffers and closes its handle to the server pipe instance. Although it's not obvious from Listing 2, the client application does this within Connector's destructor (Listing 3) , which is called when the client application shuts down.

For the most part, a client application opens and interacts with a named pipe as though it was using a file of the same name, which makes developing client applications relatively easy. Using the Win32 SDK, the functions to use are CreateFile, ReadFile, WriteFile, and CloseHandle to open, read from, write to, and close a pipe. In fact, using the ANSI C standard I/O library, you can use fopen, fread, fwrite, fclose to perform the same respective functions. The sample client application calls CreateFile within the Connector constructor, and calls ReadFile and WriteFile within Connector's member functions Read and Write respectively. (The implementation of these functions are not shown here. Full source code for the client and server applications is available on the code disk and online sources. See page 3 for details.)

One difference between named pipes and standard files, however, is that a specified server-named pipe may not be available at the time it is opened. This is a relatively common occurrence, particularly if many client applications are accessing simultaneously all of the instances of a server's pipes. Because this problem is likely, Win32 provides the WaitNamedPipe function that allows a client application to wait for a server pipe instance to become available. The client can either specify an amount of time to wait (in milliseconds) or to use the wait time established by the server when it created the first instance of a specific named pipe (NMPWAIT_USE_DEFAULT_WAIT). The sample client application uses the latter method, as seen in the call to WaitNamedPipe within the Connector constructor (Listing 3) .

When WaitNamedPipe returns successfully, the code should attempt to open an instance of the pipe using CreateFile. However, there is no guarantee that this thread will be able to successfully open the pipe. If the file open fails, the client should wait again for a pipe instance to become available. The open may fail because a second thread is also attempting to access an instance of the named pipe, or has actually connected to it before the first has a chance to open the pipe.

At this point, the client has successfully opened a connection to the server. The next step is to communicate with the server by reading from and writing to the opened pipe handle. Both the amount of data to be transferred and the order of transfer (sending and receiving of data) is application-defined. The only stipulation is that both client and server agree on when to send and receive data and how much data to transfer. In other words, both the client and server must understand the protocol to be used on a particular named pipe.

Once the client has completed its part of the protocol, it must flush its write buffers and close its connection. Flushing the write buffers ensures that the server receives all the data the client has written to the pipe. The client application should call FlushFileBuffers to do this. Finally, it should close the open pipe instance handle to terminate its use of the connection to the server, using CloseHandle. The named-pipe client sample code in nmpipe.h declares a named pipe connector class (nmpipe_cConnector) which you can use for client-side communications to a named pipe server. The code for this class is defined in nmpipe.cpp. Usage of this code is demonstrated in client.cpp. The code compiles into an executable called client.exe. (These files are available on the code disk.)

Standard Named Pipe Server Model

Named pipe servers are typically designed to use the multithreaded features of the operating system on which they are run. Figure 3 shows a commonly used model. In this model, the server creates a specific number of worker threads, each of which creates and waits on incoming connections from clients. The sample server application manages these threads via a class srv_cThread (Listing 4) . The application creates a single object of class srv_cPipeServer (not shown here); the constructor for this object executes a for loop (Listing 5) which attempts to create the srv_cThread objects and begin execution of each thread with a call to function _beginthreadex. This Visual C++ Library function is passed the address of member function srv_cPipeServe::ServerThread (Listing 6) , which in turn calls the currently indexed thread's member function Handler (Listing 7) . The main thread of the application simply waits for each of the worker threads to finish before terminating itself and the process.

Each thread must create an instance of a server-side named pipe using CreateNamedPipe, which returns a handle. Each thread in the sample server application does this by a somewhat roundabout route, as follows: each srv_cThread object contains a nested Listener object (more on this later); when the srv_cThread object is being constructed, the nested Listener object's constructor is called; it is this constructor which makes the call to CreateNamedPipe. When creating this pipe, you must specify dot (.) (for local machine) as the computer name part of the pipe name. Thus, if you attempt to create a server pipe instance using the name "\\MyComputer\PIPE\MyPipe", the CreateNamedPipe function will fail, even if MyComputer is the name of the local computer. So, you need to use the name "\\.\PIPE\MyPipe" instead. (Client applications, however may choose either form if accessing a server on the local computer.)

Another important note:When creating a named pipe instance, make sure that you pass the desired security attributes when calling CreateNamedPipe. If, for example, you pass a NULL, instead of an address for an initialized security attributes structure (SECURITY_ATTRIBUTES), don't be surprised when your client programs can't access your named pipe server. To specify the security attributes of your server, you first must declare a SECURITY_DESCRIPTOR structure and initialize it using InitializeSecurityDescriptor. Once the structure is initialized, passing NULL as the access control list parameter to SetSecurityDescriptorDacl will establish global access to the named pipe server, if that is what is desired. Finally, call CreateNamedPipe with a SECURITY_ATTRIBUTES structure that references this SECURITY_DESCRIPTOR.

Named pipe handler threads usually have an infinite loop within which they handle incoming requests. The sample server application uses such an infinite loop in the srv_cThread object's member function Handler (Listing 7) . The handler thread must first call ConnectNamedPipe, which waits for an incoming connection. Member function Handler calls ConnectNamedPipe within the Listener object's member function Connect (Listing 8) . Once a connection has been established, the thread processing the connection calls ReadFile and WriteFile to implement the application-defined protocol for the server and to communicate to the connected client application. Finally, when the server is finished communicating with the client it calls FlushFileBuffers to clear its output data buffer and DisconnectNamedPipe to terminate the connection.

Regarding named pipe servers, note that Microsoft supports the named pipe server calls only on Windows NT. Any attempt to create a server-side named pipe on Windows 95 will fail with a Win32 last error equal to ERROR_CALL_NOT_IMPLEMENTED. So, if you need to create a named pipe server, you must use Windows NT as your server operating system. Windows 95 clients can access your Windows NT server, assuming that the server's security configuration will permit access. The named pipe client sample code in nmpipe.h declares a named pipe listener class (nmpipe_cListener) which you can use for server-side communications to a named pipe server. The code for this class is defined in nmpipe.cpp. Usage of this code is demonstrated in server.cpp. The code compiles into an executable called server.exe.

Handling Time-Outs

Client and server applications must exist in real world conditions, in which networks can suffer propagation delays, broken connections, or connection attempts with incorrect protocols. When developing client/server applications, it is important to have a way to handle these conditions. If you don't, you may end up with a deadlock condition where the client and server are waiting on each other — thus preventing other applications from using the server. Also, if you want to develop a mission-critical client/server application, you must deal with situations in which the communication is just taking too long.

Unfortunately, the Win32 API named pipe functions do not directly support time-outs. So, if a connection, read, or write request takes too long, your thread is stuck until the request completes or an error occurs. As a simple example of a deadlock condition, consider the case where a client writes only four bytes of data and attempts to read some data from the server, but your server is waiting for eight bytes to arrive before writing data back to the client. Without time-outs, both your client and server threads will be blocked, each waiting for the other to make a move, and you've used up one of your server's named pipe instances.

There's a way out, though. You can support time-outs on named pipes through the use of overlapped I/O. The basic idea of overlapped I/O is to allow one thread to handle the I/O through several open file and pipe handles simultaneously. In general, with overlapped I/O, a single thread can issue one or more nonblocking read or write requests on multiple file or pipe handles and then continue until one of the I/O requests completes. For UNIX programmers, if this sounds very similar to using nonblocking file descriptors and using select to wait for I/O availability, it is.

To support time-outs on named pipes, you can use overlapped file I/O to monitor the I/O availability on only one file handle, rather than a group of handles. Doing this requires simply an open named pipe handle in overlapped mode and an event semaphore. Overlapped mode allows you to issue a named pipe operation (such as read or write) in a nonblocking manner and check for the results later. You check the results of your pipe operation by checking the event semaphore associated with the overlapped event. Since it's possible to wait on an event semaphore for a specific amount of time, a thread can wait to be notified of either an I/O event or wait for expiration of the time-out period.

You can create a server named pipe or open a client pipe by passing the flag FILE_FLAG_OVERLAPPED to the CreateNamedPipe and CreateFile functions. When you set this flag for a file handle, you must create an event semaphore for I/O event signaling and place it in the overlapped data structure (named OVERLAPPED). The functions ConnectNamedPipe, ReadFile, and WriteFile receive this structure as a parameter and immediately return FALSE, having set the last Win32 error to be ERROR_IO_PENDING. You must use GetLastError to get this error value from the system.

When your I/O request is complete or an error has occurred, Windows will signal the event semaphore that you passed in the OVERLAPPED structure. You can wait on this event semaphore using WaitForSingleObject, passing it your time-out value. If the time period expires before an I/O event occurs, this function returns the value WAIT_TIMEOUT, and you have a time-out condition. Otherwise, as long as the return code does not indicate a failure condition (WAIT_FAILED), your event semaphore must be in a signaled state, meaning that an I/O event has occurred. You should handle this I/O event by calling GetOverlappedResult on the named pipe handle to finish the processing of your named pipe request. The server application handles these events in function srv_cNamedPipe::BlockForIO (Listing 9) . If the return code is neither WAIT_TIMEOUT or WAIT_FAILED, BlockForIO calls the named-pipe member function GetAvailableData, which in turn calls GetOverlappedResult.

Controlled Server Shutdown

Shutting down a named pipe server with grace is an irritating problem for named pipe server developers. Certainly, assuming that each server pipe instance was handled on a per-thread basis, a program could simply kill each thread using TerminateThread. However, this could cause communication failures for clients currently using the server, and will likely cause some amount of resource leakage, particularly if a thread has allocated some resources during its lifetime that it expects to free at termination.

Additionally, if a server thread is blocking on a named pipe I/O operation, there is no straightforward way to force an error return code to occur on that pipe handle to unblock that operation. Therefore, an attempt to close the pipe instance handle from another thread, for example, will not work. Fortunately, you can use the infrastructure developed thus far for proper handling of time-outs to add controlled shutdown support quickly and cleanly. Using one additional event semaphore, it is possible to signal all the threads within the server of a shutdown in progress. The server threads can check for shutdown when waiting for an incoming connection and be notified when it occurs.

The server must create the shutdown event semaphore before creating any of the its pipe instance handling threads. This event semaphore must be a manual event semaphore — this ensures that every server thread can sense that it's in a signaled state. Next, as the server creates threads to handle pipe communications, it passes the semaphore's handle as part of the thread's data. This allows each thread to know which semaphore to check on for shutdown.

Since the sample application already supports waiting on an event semaphore for I/O events, adding an additional semaphore for shutdown is easy. Basically, all you need to do is to change the call to WaitForSingleObject to a WaitForMultipleObjects, passing it an array of event semaphore handles on which to wait. With this in place, you can be notified of an I/O event, a shutdown event, and a time-out event, and handle each appropriately in your code. Specifically, when each thread gets notified of the shutdown event, it can release any resources it currently owns and gracefully exit. The client and server applications call WaitForMultipleObjects within named-pipe member function BlockForIO (Listing 9) .

If you need to allow your server application to finish all current connections and not allow any additional connections, you will only need to make the above change following the ConnectNamedPipe call. Otherwise, if you need your server threads to be notified whenever they attempt any communication through pipes, then you will need to make this change after the ReadFile and WriteFile calls as well.

Conclusion

Named pipes provide a means for applications to communicate between other applications running on the same machine or across the network. They provide a reliable and efficient way to send data between programs and implement a client/server and distributed processing model. Using the techniques describe in this article, you should be able to simply and easily create named pipe clients and servers that can handle real-world situations.

Bill Heyman is a freelance software consultant for Heyman Software, Inc. who specializes in client/server technical architecture design and development. He can be reached via e-mail at 73737.2373@compuserve.com, on the web at http://ourworld.compuserve.com/homepages/Heyman.