Writing Efficient Client-Server Applications Using LAN Manager

Danny Glasser

Portions of this article are based on material originally written by Ken Reneris.

Created: March 20, 1992

ABSTRACT

This article discusses named pipes and their implementation in MicrosoftÒ LAN Manager. It provides guidelines for improving the performance of a client-server application. These guidelines include reducing the network traffic, avoiding polling operations, and minimizing second-class mailslot message traffic.

INTRODUCTION

The key to optimizing the performance of a client-server application is to optimize network I/O. In a typical LAN Manager client-server application, the majority of this I/O is through named pipes.

The performance of named pipes is based on two main concepts:

Named-pipe I/O is client driven. When the back-end* reads from or writes to a named pipe, it simply checks the locally allocated pipe buffer. When the front-end reads from or writes to a named pipe, it causes the redirector to send a remote request to the Server service; this generates a server message block (SMB) request and response. As a result, front-end I/O is significantly more expensive than back-end I/O.

There is no single, correct way to optimize. Techniques that are ideal for one application and platform can be disastrous for another. For example, a named-pipe front-end written for the MicrosoftÒ WindowsÔ graphical environment must balance the fact that nonblocking I/O generates more network traffic against the fact that blocking I/O does not yield the processor to other active applications.

This article assumes that the reader:

Is aware of, and believes in, the benefits of developing client-server applications.

Has a basic understanding of the Microsoft LAN Manager mechanisms used to develop client-server applications, specifically named pipes and mailslots.

Has access to the OS/2 Programmer’s Reference and the LAN Manager Programmer’s Reference for details on using these mechanisms.

SERVER SIDE PIPE REDIRECTION (SSPR)

To better understand how named pipes work in LAN Manager, we will discuss the implementation of server side pipe redirection (SSPR). SSPR is part of LAN Manager version 2.0 and provides significantly improved and completely transparent named-pipe I/O performance over LAN Manager version 1.x. In many cases, named-pipe I/O throughput is now bounded by the speed of the underlying network protocol and hardware.

What SSPR does for named-pipe I/O in LAN Manager version 2.0 is analogous to what HPFS386 does for file I/O: It moves the critical path of the processing for remote clients from ring 3 to ring 0, easing dependence on the OS/2 scheduler.

In LAN Manager version 1.x, the Server service manages remote named-pipe requests by opening the named pipes locally and performing the rest of the operations locally for the remote clients. An OS/2 user process running at ring 3 handles these tasks (see Figure 1). For example, when a client writes data to the named pipe, the Server service receives the data by means of a Receive network control block (NCB) that completes. The Server service examines the data in the NCB and determines that it contains data to be written to a named pipe. The Server service calls the OS/2 DosWrite function on its handle to the local named pipe, at which point OS/2 copies the data into the back-end’s buffer and notifies the back-end (for example, by scheduling the back-end’s thread that is blocked on a DosRead of the pipe). This circuitous and inefficient route is necessary because the Server service is an OS/2 user process.

Figure 1. Named-Pipe I/O in LAN Manager version 1.0

In LAN Manager version 2.0, the operation is still the same. However, because a portion of the Server service’s named-pipe code has been moved to an OS/2 device driver (running at ring 0), two of the ring transitions are no longer necessary (see Figure 2). Furthermore, if the back-end has a thread that is blocked on a DosRead of the named pipe, the data can be copied directly into the back-end’s buffer at interrupt time, immediately on completion of the Receive NCB.

Figure 2. Named-Pipe I/O in LAN Manager version 2.0

When optimizing named-pipe throughput, you can achieve the most substantial gains by minimizing the amount of network I/O (as expressed in the number of SMBs, not in the number of bytes). Let’s examine the amount of network I/O generated by different operations.

PROGRAMMING CONSIDERATIONS

As stated earlier, network named-pipe I/O is driven by the client. For small amounts of I/O, each function generates two network packets: an SMB request and an SMB response. However, certain functions accomplish more than others. For example, it is generally more efficient to use the DosTransactNmPipe or the DosCallNmPipe function than to use combinations of open, write, read, and close functions, as discussed in detail below. It is also more efficient to combine small pieces of data and send them as a single named-pipe message than to send them individually; five 100-byte messages take longer and generate more network traffic and work for the redirector and for the Server service than one 500-byte message.

If the amount of data transmitted in a single call exceeds a certain threshold, the buffer is automatically split into smaller chunks for delivery. Consequently, sending data in amounts a few bytes above this threshold gives about half the throughput of sending it in amounts a few bytes below the threshold. This threshold, known as the negotiated buffer size, is the smaller of the sizworkbuf parameter on the client and the sizreqbuf parameter on the server. You can determine the values of these parameters dynamically by calling the NetWkstaGetInfo and NetServerGetInfo functions (typically, these values are in the 1K–4K range, depending on the client type). There is a small amount of overhead with each write operation, so it is best to stay a few bytes below the threshold.

If you use DosTransactNmPipe instead of DosWrite followed by DosRead, the same operations are accomplished with half the number of SMBs (see Figure 3). Although this technique is not always appropriate (for example, DosTransactNmPipe does not work on byte-mode named pipes), in most cases the code is not only faster but also simpler.

Figure 3. DosTransactNmPipe vs. DosWrite + DosRead

Using DosCallNmPipe instead of the combination of DosOpen, DosWrite, DosRead, and DosClose provides more dramatic benefits (see Figure 4). Again, the code is not only faster but also smaller and simpler.

Figure 4. DosCallNmPipe vs. DosOpen/Write/Read/Close

Your decision on whether to use DosTransactNmPipe or DosCallNmPipe depends on the nature of the application. DosTransactNmPipe is preferable when the front-end and back-end have sustained communications and the ratio of open and close operations to write and read operations is low. DosCallNmPipe is preferable when the front-end and back-end have short-term, bursty communications with only one or two write and read operations for each session.

Both DosTransactNmPipe and DosCallNmPipe can block, even when called on a nonblocking named pipe. This is unavoidable because the write and the subsequent read are performed as a single operation from the client’s perspective. When this is a consideration (for example, when the front-end is heavily interactive or is a Windows-based application), the back-end should respond to such requests as quickly as possible.

NAMED PIPES AND SEMAPHORES

Polling operations, such as nonblocking reads and calls to DosPeekNmPipe, are also inefficient and should be avoided whenever possible. This is especially true on the client, where each operation generates an SMB request and response. For example, if the front-end needs four nonblocking reads on average before it receives the data, it generates four times the network traffic generated by a single blocking read. Furthermore, the Server service must process the request for each operation, so many clients performing polling operations simultaneously can impair the performance of the Server service. As discussed earlier, it is not always possible to use blocking operations, specifically in Windows-based applications. Use necessary polling operations with care and intelligence. For example, the front-end can contain a back-off algorithm to control the frequency of the polling.

One problem with interprocess communications in general and client-server applications in particular is the propagation of delay. That is, when one component is inactive because it is waiting for another component to do something (such as provide it with information), a small delay in one component can cause a larger delay in another component.

One cause of delay propagation is the inability of a front-end to open a named pipe because no instances of that named pipe are available. This can happen, for example, when the back-end is busy servicing the front-end on another client and is not making additional instances of the named pipe available (perhaps not even cleaning up after a session closed by yet another client). It is easy to see how a delay on one client can propagate to the server and then further propagate to other clients. You can remedy this problem by having the back-end always leave a few instances of the named pipe in the listening state, making it more likely for a client to connect. If the front-end tends to create short-lived sessions (for example, with DosCallNmPipe), additional instances may be needed. This should be balanced with the fact that creating additional named-pipe instances uses server resources and can impair the performance of the system. For example, a clever application can optimize the number of available pipe instances by having the front-end tell the back-end how long it took to open the named pipe (either in number of seconds or in number of attempts). The back-end can use this information to gauge whether there are too few or too many instances available.

Another cause of delay propagation is the use of nonblocking I/O on the server side. If the front-end of an application calls DosTransactNmPipe and the back-end does a nonblocking read every two seconds on that pipe instance, the front-end experiences an additional delay of up to two seconds. This form of delay can also occur, albeit less dramatically, if the back-end uses DosSetNmPipeSem and related functions to manage multiple pipe instances with a single thread. The use of blocking I/O operations by the back-end, in conjunction with the back-end’s implicit use of SSPR, can virtually eliminate this delay. In the previous example, if the back-end does a blocking read when the front-end calls DosTransactNmPipe, the data sent by the front-end is copied to the back-end’s buffer at interrupt time. The response written by the back-end is returned to the front-end immediately (because the blocking read is implicitly present as a result of the DosTransactNmPipe call).

Note, however, that having the back-end create numerous threads, each doing a blocking read on a named-pipe instance, can severely drain the resources of the server. Therefore, you should use this technique judiciously. In a clever application, the back-end can determine heuristically when a given front-end is about to become very active on the named pipe and switch the management of the corresponding pipe instance from a named pipe semaphore to a dedicated thread.

Subtleties in the behavior of named pipes can cause problems for code that is not written with named pipes in mind. The following scenarios are not strictly related to efficient use of named pipes but provide hints for avoiding nasty bugs in named-pipe applications:

Always call DosSemSet on a semaphore before calling DosQNmPipeSemState. If the back-end of an application returns from a DosSemWait (or DosMuxSemWait) call on a named-pipe semaphore (indicating that some event has happened on the pipe) and queries the pipe state before setting the semaphore, there is a window in which another event can occur before the semaphore is set. If an event occurs in this window, the subsequent call to DosSemWait blocks without acknowledging that event.

When reading from a byte-mode pipe, it is possible to receive less than the requested amount of data, even if the data has already been written to the pipe. This is because read-ahead optimizations are performed by the redirector. For example, let us say that the front-end of an application requests to read 150 bytes from the pipe. The client sends this request to the server, which determines that the back-end has written 200 bytes of data to the pipe. For efficiency, the client reads all 200 bytes of data from the server, returns 150 bytes to the front-end, and caches the remaining 50 bytes. If the front-end requests to read another 150 bytes from the pipe, the client now returns only the 50 bytes of cached data to the front-end and sends no request to the server. The result is that the front-end expects 150 bytes back on the second read but receives only 50 bytes. This is distinct from file I/O, where the number of bytes read is typically less than the number of bytes requested only when the end of the file has been reached. This is not an issue with message-mode pipes because of the inherent atomicity of each unit of data.

If the front-end of an application writes to a named pipe and closes its end of the pipe immediately afterward, the back-end may see the pipe state as closing (NPSS_CLOSE) the next time it queries the state (with DosQNmPipeSemState). If the implementation of the front-end causes this scenario to occur, the back-end should do a final read when the pipe is in the closing state.

When using the DosWaitNmPipe function to wait for an available instance of a named pipe, note that a successful return from that function does not guarantee that the subsequent attempt to open the named pipe will be unsuccessful. There is a window in which another client can connect to the available instance before the client that has been waiting. (The server or the network could also shut down before the waiting client opens the pipe.) To avoid this problem, place the open and wait calls in a loop.

MAILSLOTS

A typical client-server application uses mailslots to dynamically locate one or more servers containing a specified resource. (Think of this as a generalized extension to the NetServerEnum2 function.) Specifically, the back-end of the client-server application creates a request mailslot. When the front-end of this application is started, it creates a response mailslot and sends a mailslot message to the request mailslot on the entire domain (through second-class delivery). The back-end (or back-ends, if multiple servers run the application) receives this message and decides whether to respond. If so, it sends a mailslot message to the response mailslot through second-class delivery. (First-class delivery can be used if the front-end is guaranteed to be running on a LAN Manager server.)

Given this model, note two considerations:

Sending second-class mailslot messages to a domain can generate enough lower-level network traffic to impair the performance of all systems on the network. When using mailslots to poll for a resource (as in the request mailslot message described previously), don’t call the DosWriteMailslot function in a tight loop. Instead, put a delay in the loop to create a discrete time gap between the calls. For example, you can follow the DosWriteMailslot call by a DosReadMailslot call with a positive cTimeout value. This works if the front-end needs exactly one response from a back-end; a successful read means that no additional writes are needed, and a time-out means that the delay has elapsed and another write is necessary.

If several back-ends send mailslot messages to the response mailslot simultaneously, the front-end system may get flooded with responses and lose one or more of them. To avoid this, you can include a delay value in the request message. Each back-end that responds should select a pseudo-random number between zero and the delay value and sleep for that amount of time before sending the response.

You can include the name of the response mailslot in the request message rather than hard-coding it in the response software to support future compatibility. For example, if you produce a new version of the front-end, you can specify a new response mailslot name that a new back-end can use to distinguish between the versions.

*This article refers to the client side of a client-server application as the front-end and to the server side as the back-end. This practice prevents overuse of the terms client and server and any ensuing confusion. (For example, an OS/2Ò LAN Manager client can also be the server of a client-server application if it is running the Peer service.)