Beginning Client/Server Programming: Named Pipes

From EDM2
Jump to: navigation, search

Written by Steve Lacy

Beginning Client/Server Programming: Named Pipes

This article is an introduction to client/server applications, and their interface in OS/2. Throughout this article, click on "Forward" to read the next paragraph. I've assumed that you've already browsed through the supplied source files, "client.c" and "server.c" just so you can see generally what's going on in this program, even though you might not know what the actual function calls do, you can look at the names (like DosCreateNPipe) and see just what they do, in this case, creating a named pipe.

Introduction

One of the current hip words used when talking about OS/2 is "client/server" Well, as a programmer, I know I've wondered myself, "what exactly is a Client/Server program?" Well, from my experience, its not a black and white definition, since some people seem to define one style to be client/server whereas other people wouldn't consider that particular example to be a client/server application.

One thing to keep in mind is that the design of client/server applications is usually more work than the programming of them. Designing a multithreaded program which uses named pipes and semaphores elegantly is quite a task, we'll be getting into those topics in later articles.

For our little example, we're going to be developing a "reverse this string" client server application. Basically, we'll have a server that accepts connections on a named pipe, reads in a string, and spits that string back out onto the pipe, reversed. Note that in our example we're not using a multithreaded server. This means that if two clients are trying to connect to the same pipe at the same time, that only one of them will get through, and that the other will have to wait for the other to finish, until it can continue.

One thing is known for sure, that client server applications use the following OS/2 kernel functions: Named Pipes, Threads/Processes, Semaphores, and Shared Memory. Here's a brief description of four mechanisms

In this article, I don't attempt to define client/server applications, but I do show you the basics as to what does define a "typical" client/server program, if such a beast does exist.

Named Pipes

A named pipe is a mechanism for controlling inter-process (or inter-thread) communication under OS/2 2.0. Named pipes share a lot of the characteristics of UNIX sockets, but in my opinion, their programming interface is a lot more user friendly, and it doesn't have as much programming overhead as UNIX sockets do.

In this article, we're going to dulge into the basics of named pipes, and the OS/2 functions used to program a named pipe application.

Threads/Processes

A standard OS/2 program, something that you would write with the standard library of C functions, has what you call a "single thread of execution." A multithreaded program, as you might think, has multiple threads of execution. Basically, this means that your program is running in two different places at the same time. Starting a thread has very little overhead, since all the code for you program is already in memory. The overhead in starting a thread has been compared to the overhead for calling a standard C function. Your program, or some thread of your program, can start up another thread by issuing a DosCreateThread call.

Processes are basically equivalent to "programs" One program can start up another program by issuing a DosStartSession call. Remember that there is some significant overhead in loading and starting another program, even if its another copy of the program that is currently executing. When processes are started, they always start with the main() function of your code.

Semaphores

A semaphore is an inter-process or inter-thread signaling mechanism. There are three types of semaphores in OS/2, you don't particularly need to know about them now, so I won't go into the details. The way semaphores work is that you can either post or wait on an open semaphore. When you're waiting, you stop waiting until someone else posts. The neat thing is that it can be anyone whatsoever, in any process, in any thread.

Design of the applications

This section deals with the design of our application, and gives an introduction to the Named pipe OS/2 services that we'll be using in our application. We have to keep in mind that dealing with named pipes is in fact a lot like dealing with standard files, that is, files on disk. For example, the client end of our program uses only the DosOpen, DosWrite, DosRead, and DosClose system calls, the exact same calls that would be used if we were writing to a file. We distinguish a named pipe from a file by the name that we send to the system calls. The name for pipes must be of the form "\pipe\*" Generally, its a good idea to try to pick pipe names that you don't think other people will be using too, since this would create problems. If you're really paranoid, you'll do something like include the process number (which is essentially unique) in the pipe name, so you know that no one else could possibly have the same name as you. For our example, that would be just a little bit of overkill, so we're just going to be using the name "\pipe\reverse\npipe". I find it a good idea to choose pipe names so that they're of the form "\pipe\application-name\pipe-name".

Pipe Semantics

If we're going to have interprocess communication, we have to design our application so that the communication takes place properly, that all the data is transmitted, and that there are "clean" open and close operations. Browsing through the documentation, the most obvious looking function calls are: DosCreateNPipe, DosConnectNPipe, and DosDisConnectNPipe. So, we have functions to create pipes, "connect" to them (whatever that may mean) and to disconnect from them. Thinking of pipes as files, the create routine creates the pipe, the connect waits for the client to connect to the pipe that we've just connected. When the entire process has finished, we disconnect from the pipe, and we can either end our application, or continue with the connect cycle.

Server Client Pipe status
... ... Nonexistant
DosCreateNPipe ... Created
DosConnectNPipe ... Blocking for connections
... DosOpen Opened
DosRead DosWrite Open
DosWrite DosRead Open
... DosClose Closing
... ... Closed
DosDisConnectNPipe ... Dosconnected (same as created state)

Server implementation

So, that's basically an outline of what our programs should do, but we still need a few little modifications. The main one being that we have to make sure that the DosDisconnectNPipe happens after the DosClose happens on the client side. The simple way to synchronize this it to add another DosRead on the server side. What will happen is that the DosRead will fail with an End Of File (EOF) error, then we know that the pipe has definitely closed, and we can then do our DosDisConnectNPipe. The way that we know that DosRead has returned an end of file error is when it returns zero in the ulBytes field, which usually contains the number of bytes that we've read from the pipe. So, generally the code for our server looks like this:

  DosCreateNPipe(...);
  while (1) {
      DosConnectNPipe(...);
  /* Read the input data */
      DosRead(...);
  /* This is where we figure out what
     the output will be */
      DosWrite(...);
  /* Now we're reading for an end of file. */
      DosRead(...);
  /* If we're not at the end of the file
     that is, number of bytes read
     isn't zero, then something has happened */
      if (ulBytes!=0) error();
      rc=DosDisConnectNPipe(...);
  }

Other than the parameters for the functions, that's the program. The reason that we have a while(1) in our program is that after one client has finished, we want the server to reset and be able to accept more connections from other clients.

Client Implementation

The client, as mentioned before, reads and writes to the created pipe - treating it as a file, not as a pipe. So, we open the file, write to it, read from it, and close it. Here's a brief version of the code:

  for (i=1;i<argc;i++) {
      DosWaitNPipe(...);
      DosOpen(...);
      DosWrite(...);
      DosRead(...);
      DosClose(...);
  }

The only strange bit at this point should be the DosWaitNPipe call. This is OS/2 solution to a simple problem: What to do if someone else is already using the pipe? Well, you have to wait until the pipe frees up, that is, wait until the server has issued a DosCreateNPipe call.

Conclusion

Well, from this you should be able to write a simple client/server program, basically by using the code supplied here along with the reference information in the next session. The OS/2 calls, although they may have long names, large numbers of arguments, or something else that you might find initially discouraging, they're actually quite easy to use. Just keep plugging, and have fun! Next time, we'll make the server actually do something, and we'll introduce semaphores.

Reference Section

This is the reference section for this article. You should note that pipe handles and file handles are interchangeable, so the file handle returned by DosOpen can be passed to DosWaitNPipe, which requires a pipe handle as its argument.

DosOpen

PSZ pszFileName;
PHFILE ppshfFileHandle;
PULONG pActionTaken;
ULONG ulFileSize,ulFileAttribute,ulOpenFlag,ulOpenMode;
PEAOP2 pEABuf;
APIRET rc;

rc=DosOpen(pszFileName,
           ppshfFileHandle,
           ulFileSize,
           ulFileAttribute,
           ulOpenFlag,
           ulOpenMode);

pszFileName is the name of the file that we're opening. ppshfFileHandle is the place where the opened file's file handle ps placed.

pActionTaken is the plce where the action taken value is placed. It is one if the following:

FILE_EXISTED
FILE_CREATED
FILE_TRUNCATED

ulFileSize is the initial size of the file, if you're creating one. ulFileAttribute is one or more of the following:

FILE_ARCHIVED
FILE_DIRECTORY
FILE_SYSTEM
FILE_HIDDEN
FILE_READONLY
FILE_NORMAL

ulOpenFlag specifies an open action. It is one of the following:

OPEN_ACTION_FAIL_IF_NEW
OPEN_ACTION_CREATE_IF_NEW
OPEN_ACTION_FAIL_IF_EXISTS
OPEN_ACTION_OPEN_IF_EXISTS
OPEN_ACTION_REPLACE_IF_EXISTS

ulOpenMode specifies the mode for opening the file. It is one of the following:

OPEN_FLAGS_DASD
   (direct open flag)
OPEN_FLAGS_WRITE_THROUGH
   (if set, accesses don't go through cache)
OPEN_FLAGS_FAIL_ON_ERROR
OPEN_FLAGS_NO_CACHE
OPEN_FLAGS_NO_LOCALITY
   (don't know info. about locality)
OPEN_FLAGS_SEQUENTIAL
   (mostly sequential access file)
OPEN_FLAGS_RANDOM
   (mostly random access file)
OPEN_FLAGS_RANDOMSEQUENTIAL
   (random with some locality)
OPEN_FLAGS_NOINHERIT
   (children don't inherit handle access)
OPEN_SHARE_DENYREADWRITE
OPEN_SHARE_DENYWRITE
OPEN_SHARE_DENYREAD
OPEN_SHARE_DENYNONE
OPEN_ACCESS_READONLY
OPEN_ACCESS_WRITEONLY
OPEN_ACCESS_READWRITE

pEABuf Is a pointer to Extended Attribute information.

In most cases, you should specify OPEN_ACTION_OPEN_IF_EXISTS for ulOpenFlag and OPEN_SHARE_DENYNONE | OPEN_ACCESS_READWRITE for the ulOpenMode.

DosClose

HFILE FileHandle;
APIRET rc;

rc=DosClose(FileHandle);

Where FileHandle is the handle for the file that you want to close.

DosRead

HFILE FileHandle;
PVOID pBufferArea;
ULONG ulBufferLength;
PULONG pBytesRead;
APIRET rc;

rc=DosRead(FileHandle,pBufferArea,
           ulBufferLength,pBytesRead);

Where FileHandle is the handle of the open file. pBufferArea is the place where the data is to be read into. ulBufferLength is the size of pBufferArea. pBytesRead is the number of bytes read from the file. (on return)

DosWrite

HFILE FileHandle;
PVOID pBufferArea;
ULONG ulBufferLength;
PULONG pBytesRead;
APIRET rc;

rc=DosWrite(FileHandle,pBufferArea,
            ulBufferLength,pBytesRead);

Where FileHandle is the handle of the open file. pBufferArea is the place where the data is to be read from. ulBufferLength is the size of pBufferArea. pBytesRead is the number of bytes written to the file. (on return)

DosCreateNPipe

PSZ pszFileName;
PHPIPE pphpipePipeHandle;
ULONG ulOpenMode,ulPipeMode,ulOutBufSize,
      ulInBufSize,ulTimeOut;
APIRET rc;

rc=DosCreateNPipe(pszFileName,
                  pphpipePipeHandle,
                  ulOpenMode,
                  ulPipeMode,
                  ulOutBufSize,
                  ulInBufSize,
                  ulTimeOut);

Where pszFileName is the name of the pipe to be created. ulOpenMode is one or more of the following:

NP_WRITEBEHIND
NP_NOWRITEBEHIND
NP_INHERIT
NP_NOINHERIT
NP_ACCESS_INBOUND
NP_ACCESS_OUTBOUND
NP_ACCESS_DUPLEX

ulPipeMode is one or more of the following:

NP_WAIT
NP_NOWAIT
NP_TYPE_BYTE
NP_TYPE_MESSAGE
NP_READMODE_BYTE
NP_READMODE_MESSAGE

You should also bitwise-or this with the number of instances of the pipe that you would like. In our case, this will always be one.

ulOutBufSize is the size of the outgoing buffer. ulInBufSize is the size of the incoming buffer.

ulTimeOut is the default timeout value, in microseconds, that reads and writes will use. Setting this to -1 gives indefinite timeout. In other words, reads and writes will wait forever.

DosConnectNPipe

HPIPE PipeHandle;
APIRET rc;

rc=DosConnectNPipe(PipeHandle);

Where PipeHandle is the pipe handle that we want to begin accepting connections on.

DosDisConnectNPipe

HPIPE PipeHandle;
APIRET rc;

rc=DosDisConnectNPipe(PipeHandle);

Where PipeHandle is the pipe handle that we want to stop accepting connections on.

DosWaitNPipe

HPIPE PipeHandle;
APIRET rc;

rc=DosWaitNPipe(PipeHandle);

Where PipeHandle is the pipe handle that we want to wait for an available connection on.