6.4 Pipes and the Client-Server Model

Team-FLY

The client-server model is a standard pattern for process interaction. One process, designated the client , requests a service from another process, called the server . The chapters in Part 4 of the book develop and analyze applications that are based on the client-server model with network communication. This section introduces client-server applications that use named pipes as the communication vehicle. We look at two types of client-server communication simple-request and request-reply . In simple-request, the client sends information to the server in a one-way transmission; in request-reply the client sends a request and the server sends a reply.

Programs 6.7 and 6.8 illustrate how the simple-request protocol can be useful in logging. The client writes logging information to a named pipe rather than to standard error. A server reads from the named pipe and writes to a file. At first glance, the use of the named pipe appears to have added an extra step with no benefit. However, pipes and FIFOs have a very important property writes of no more than PIPE_BUF bytes are guaranteed to be atomic. That is, the information is written as a unit with no intervening bytes from other writes. In contrast, an fprintf is not atomic, so pieces of the messages from multiple clients might be interspersed.

The server of Program 6.7 creates the pipe if it does not already exist. The server opens the pipe for both reading and writing, even though it will not write to the pipe. When an attempt is made to open a pipe for reading, open blocks until another process opens the pipe for writing. Because the server opens the pipe for reading and writing, open does not block. The server uses copyfile to read from the pipe and to write to standard output. To write to a file, just redirect standard output when the server is started. Since the server has the pipe open for writing as well as reading, copyfile will never detect an end-of-file. This technique allows the server to keep running even when no clients are currently writing to the pipe. Barring errors, the server runs forever.

Program 6.7 pipeserver.c

The program reads what is written to a named pipe and writes it to standard output .

 #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> #include "restart.h" #define FIFOARG 1 #define FIFO_PERMS (S_IRWXU  S_IWGRP S_IWOTH) int main (int argc, char *argv[]) {    int requestfd;    if (argc != 2) {    /* name of server fifo is passed on the command line */       fprintf(stderr, "Usage: %s fifoname > logfile\n", argv[0]);       return 1;    }                          /* create a named pipe to handle incoming requests */    if ((mkfifo(argv[FIFOARG], FIFO_PERMS) == -1) && (errno != EEXIST)) {        perror("Server failed to create a FIFO");        return 1;    }                     /* open a read/write communication endpoint to the pipe */    if ((requestfd = open(argv[FIFOARG], O_RDWR)) == -1) {        perror("Server failed to open its FIFO");        return 1;    }    copyfile(requestfd, STDOUT_FILENO);    return 1; } 

The client in Program 6.8 writes a single line to the pipe. The line contains the process ID of the client and the current time. Multiple copies of Program 6.8 can run concurrently. Because of the atomic nature of writes to the pipe, pieces of the messages from different clients are not interleaved.

Program 6.8 pipeclient.c

The client writes an informative message to a named pipe .

 #include <errno.h> #include <fcntl.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <time.h> #include <unistd.h> #include <sys/stat.h> #include "restart.h" #define FIFOARG 1 int main (int argc, char *argv[]) {    time_t curtime;    int len;    char requestbuf[PIPE_BUF];    int requestfd;    if (argc != 2) {  /* name of server fifo is passed on the command line */       fprintf(stderr, "Usage: %s fifoname", argv[0]);       return 1;    }    if ((requestfd = open(argv[FIFOARG], O_WRONLY)) == -1) {        perror("Client failed to open log fifo for writing");        return 1;    }    curtime = time(NULL);    snprintf(requestbuf, PIPE_BUF, "%d: %s", (int)getpid(), ctime(&curtime));    len = strlen(requestbuf);    if (r_write(requestfd, requestbuf, len) != len) {       perror("Client failed to write");       return 1;    }    r_close(requestfd);    return 0; } 
Exercise 6.11

How would you start Program 6.7 so that it uses the pipe mypipe and the log file it creates is called mylog ? When will the program terminate?

Answer:

 pipeserver mypipe > mylog 

The program does not terminate unless it is killed . You can kill it by typing Ctrl-C at the keyboard. No client error can cause the server to terminate.

Exercise 6.12

Start the pipeserver of Program 6.7 and run several copies of the pipeclient of Program 6.8 and observe the results.

We now consider a second example of the client-server model with named pipes, a simple time (sequence number) server that illustrates some of the difficulties in using the client-server model with pipes and FIFOs.

The implementation uses two named pipesa request pipe and a sequence pipe. Clients write a byte to a request pipe (e.g., 'g' ). The server responds by writing a sequence number to the sequence pipe and incrementing the sequence number. Unfortunately, reading from a pipe is not an atomic operation. Since the sequence number is more than one byte, it is possible (though unlikely ) that a client may not get all of the bytes of a sequence number in one read. Depending on the interleaving of the client processes, the next client may get part of the previous sequence number. To handle this possibility, a client that does a partial read of the sequence number immediately transmits an error designator (e.g., 'e' ) on the request pipe. When the server encounters the error character, it closes and unlinks the pipes. The other clients then detect an error.

As before, the server opens both pipes for reading and writing. The server terminates only when it receives an 'e' byte from a client. When that happens, future clients block when they try to open the request pipe for writing. Pending clients receive an error when they try to write to the request pipe since no process has this pipe open. When a process writes to a pipe or FIFO that no process has open for reading, write generates a SIGPIPE signal. Unless the process has specifically prevented it, the signal causes the process to terminate immediately. Section 8.4 explains how to respond to these types of signals.

Programs 6.9 and 6.10 illustrate the difficulties of implementing a request-reply protocol by using named pipes. When multiple clients make requests, the server replies can be read by any client. This allows a sequence number meant for one process to be read by another process. Second, because reads are not atomic, a partial read by one client causes the next client to receive incorrect results. The solution in Program 6.9 and Program 6.10 is for the client to send an error code, which causes the server to terminate. This strategy may suffice for closely cooperating processes, but it is not applicable in general. A malicious client could cause the protocol to behave incorrectly without detecting an error. In most cases, the client should never be able to cause the server to fail or exit. The exercise of Section 6.10 explores an alternative strategy in which the server creates a separate named pipe for each distinct client. Now each pipe only has a single reader, eliminating the two problems described above.

Program 6.9 seqserverbad.c

A sequence server reads a character from the request pipe and transmits a sequence number to the sequence pipe. (See text for a discussion.)

 #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> #include "restart.h" #define ERROR_CHAR'e' #define OK_CHAR 'g' #define REQUEST_FIFO 1 #define REQ_PERMS (S_IRUSR  S_IWUSR  S_IWGRP  S_IWOTH) #define SEQUENCE_FIFO 2 #define SEQ_PERMS (S_IRUSR  S_IWUSR  S_IRGRP S_IROTH) int main (int argc, char *argv[]) {    char buf[1];    int reqfd, seqfd; long seqnum = 1;    if (argc != 3) {            /* names of fifos passed on the command line */       fprintf(stderr, "Usage: %s requestfifo sequencefifo\n", argv[0]);       return 1;    }                          /* create a named pipe to handle incoming requests */    if ((mkfifo(argv[REQUEST_FIFO], REQ_PERMS) == -1) && (errno != EEXIST)) {        perror("Server failed to create request FIFO");        return 1;    }    if ((mkfifo(argv[SEQUENCE_FIFO], SEQ_PERMS) == -1) && (errno != EEXIST)){        perror("Server failed to create sequence FIFO");        if (unlink(argv[REQUEST_FIFO]) == -1)           perror("Server failed to unlink request FIFO");        return 1;    }    if (((reqfd = open(argv[REQUEST_FIFO], O_RDWR)) == -1)         ((seqfd = open(argv[SEQUENCE_FIFO], O_RDWR)) == -1)) {       perror("Server failed to open one of the FIFOs");       return 1;    }    for ( ; ; ) {       if (r_read(reqfd, buf, 1) == 1) {          if ((buf[0] == OK_CHAR) &&              (r_write(seqfd, &seqnum, sizeof(seqnum)) == sizeof(seqnum)))             seqnum++;          else if (buf[0] == ERROR_CHAR)             break;       }    }    if (unlink(argv[REQUEST_FIFO]) == -1)       perror("Server failed to unlink request FIFO");    if (unlink(argv[SEQUENCE_FIFO]) == -1)       perror("Server failed to unlink sequence FIFO");    return 0; } 
Program 6.10 seqclientbad.c

The client writes a request to a request pipe and reads the sequence number from the sequence pipe. This client can cause the server to exit .

 #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <unistd.h> #include <sys/stat.h> #include "restart.h" #define ERROR_CHAR 'e' #define OK_CHAR 'g' #define REPEAT_MAX 100 #define REQUEST_FIFO 1 #define SEQUENCE_FIFO 2 #define SLEEP_MAX 5 int main (int argc, char *argv[]) {    int i;    char reqbuf[1];    int reqfd, seqfd;    long seqnum;    if (argc != 3) {            /* names of pipes are command-line arguments */       fprintf(stderr, "Usage: %s requestfifo sequencefifo\n", argv[0]);       return 1;    }    if (((reqfd = open(argv[REQUEST_FIFO], O_WRONLY)) == -1)         ((seqfd = open(argv[SEQUENCE_FIFO], O_RDONLY)) == -1)) {        perror("Client failed to open a FIFO");        return 1;    }    for (i = 0; i < REPEAT_MAX; i++) {        reqbuf[0] = OK_CHAR;        sleep((int)(SLEEP_MAX*drand48()));        if (r_write(reqfd, reqbuf, 1) == -1) {           perror("Client failed to write request");           break;        }        if (r_read(seqfd, &seqnum, sizeof(seqnum)) != sizeof(seqnum) ) {            fprintf(stderr, "Client failed to read full sequence number\n");            reqbuf[0] = ERROR_CHAR;            r_write(reqfd, reqbuf, 1);            break;        }        fprintf(stderr, "[%ld]:received sequence number %ld\n",                (long)getpid(), seqnum);     }    return 0; } 

The situation with nonatomic reads from pipes can actually be worse than described here. We have assumed that a read becomes nonatomic as follows .

  1. The server gets two requests and writes two sequence numbers (4-byte integers) to the pipe.

  2. One client calls read for the sequence pipe requesting four bytes, but read returns only two bytes.

  3. The second client calls read for the sequence pipe to read the next four bytes. These four bytes consist of the last two bytes from the first sequence number and the first two bytes of the second sequence number.

Under these circumstances the first client detects an error, and the server shuts down. The second client may or may not know an error occurred.

However, another scenario is technically possible, although it is very unlikely. Suppose the server writes two 4-byte integer sequence numbers and the bytes in the pipe are abcdefgh . The POSIX standard does not exclude the possibility that the first client will read the bytes abgh and the second one will read the bytes cdef . In this case, the sequence numbers are incorrect and the error is not detected at all.

Exercise 6.13

Try running one copy of Program 6.9 ( seqserverbad ) and two copies of Program 6.10 ( seqclientbad ). What happens?

Answer:

This should work correctly. The two copies of seqclientbad should get disjoint sets of sequence numbers.

Exercise 6.14

Try running two copies of Program 6.9 ( seqserverbad ) and one copy of Program 6.10 ( seqclientbad ). What happens?

Answer:

Either server can respond to a request for a sequence number. It is possible that the client will get the same sequence number twice.

Exercise 6.15

Change the seqclientbad to have a SLEEP_MAX of 0 and a REPEAT_MAX of 1,000,000. Comment out the last fprintf line. Run two copies of the client with one copy of the server. What happens?

Answer:

It is possible, but unlikely, that the server will terminate because one of the clients received an incorrect number of bytes when requesting the sequence number.

Team-FLY


Unix Systems Programming
UNIX Systems Programming: Communication, Concurrency and Threads
ISBN: 0130424110
EAN: 2147483647
Year: 2003
Pages: 274

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net