17.5. An Open Server, Version 1Using file descriptor passing, we now develop an open server: a program that is executed by a process to open one or more files. But instead of sending the contents of the file back to the calling process, the server sends back an open file descriptor. This lets the server work with any type of file (such as a device or a socket) and not simply regular files. It also means that a minimum of information is exchanged using IPC: the filename and open mode from the client to the server, and the returned descriptor from the server to the client. The contents of the file are not exchanged using IPC. There are several advantages in designing the server to be a separate executable program (either one that is executed by the client, as we develop in this section, or a daemon server, which we develop in the next section).
The client process creates an s-pipe (either a STREAMS-based pipe or a UNIX domain socket pair) and then calls fork and exec to invoke the server. The client sends requests across the s-pipe, and the server sends back responses across the s-pipe. We define the following application protocol between the client and the server.
This is an example of a process sending an open descriptor to its parent. In Section 17.6, we'll modify this example to use a single daemon server, where the server sends a descriptor to a completely unrelated process. We first have the header, open.h (Figure 17.26), which includes the standard headers and defines the function prototypes. Figure 17.26. The open.h header#include "apue.h" #include <errno.h> #define CL_OPEN "open" /* client's request for server */ int csopen(char *, int); The main function (Figure 17.27) is a loop that reads a pathname from standard input and copies the file to standard output. The function calls csopen to contact the open server and return an open descriptor. Figure 17.27. The client main function, version 1#include "open.h" #include <fcntl.h> #define BUFFSIZE 8192 int main(int argc, char *argv[]) { int n, fd; char buf[BUFFSIZE], line[MAXLINE]; /* read filename to cat from stdin */ while (fgets(line, MAXLINE, stdin) != NULL) { if (line[strlen(line) - 1] == '\n') line[strlen(line) - 1] = 0; /* replace newline with null */ /* open the file */ if ((fd = csopen(line, O_RDONLY)) < 0) continue; /* csopen() prints error from server */ /* and cat to stdout */ while ((n = read(fd, buf, BUFFSIZE)) > 0) if (write(STDOUT_FILENO, buf, n) != n) err_sys("write error"); if (n < 0) err_sys("read error"); close(fd); } exit(0); } The function csopen (Figure 17.28) does the fork and exec of the server, after creating the s-pipe. Figure 17.28. The csopen function, version 1#include "open.h" #include <sys/uio.h> /* struct iovec */ /* * Open the file by sending the "name" and "oflag" to the * connection server and reading a file descriptor back. */ int csopen(char *name, int oflag) { pid_t pid; int len; char buf[10]; struct iovec iov[3]; static int fd[2] = { -1, -1 }; if (fd[0] < 0) { /* fork/exec our open server first time */ if (s_pipe(fd) < 0) err_sys("s_pipe error"); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0) { /* child */ close(fd[0]); if (fd[1] != STDIN_FILENO && dup2(fd[1], STDIN_FILENO) != STDIN_FILENO) err_sys("dup2 error to stdin"); if (fd[1] != STDOUT_FILENO && dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO) err_sys("dup2 error to stdout"); if (execl("./opend", "opend", (char *)0) < 0) err_sys("execl error"); } close(fd[1]); /* parent */ } sprintf(buf, " %d", oflag); /* oflag to ascii */ iov[0].iov_base = CL_OPEN " "; /* string concatenation */ iov[0].iov_len = strlen(CL_OPEN) + 1; iov[1].iov_base = name; iov[1].iov_len = strlen(name); iov[2].iov_base = buf; iov[2].iov_len = strlen(buf) + 1; /* +1 for null at end of buf */ len = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len; if (writev(fd[0], &iov[0], 3) != len) err_sys("writev error"); /* read descriptor, returned errors handled by write() */ return(recv_fd(fd[0], write)); } The child closes one end of the pipe, and the parent closes the other. For the server that it executes, the child also duplicates its end of the pipe onto its standard input and standard output. (Another option would have been to pass the ASCII representation of the descriptor fd[1] as an argument to the server.) The parent sends to the server the request containing the pathname and open mode. Finally, the parent calls recv_fd to return either the descriptor or an error. If an error is returned by the server, write is called to output the message to standard error. Now let's look at the open server. It is the program opend that is executed by the client in Figure 17.28. First, we have the opend.h header (Figure 17.29), which includes the standard headers and declares the global variables and function prototypes. Figure 17.29. The opend.h header, version 1#include "apue.h" #include <errno.h> #define CL_OPEN "open" /* client's request for server */ extern char errmsg[]; /* error message string to return to client */ extern int oflag; /* open() flag: O_xxx ... */ extern char *pathname; /* of file to open() for client */ int cli_args(int, char **); void request(char *, int, int); The main function (Figure 17.30) reads the requests from the client on the s-pipe (its standard input) and calls the function request. Figure 17.30. The server main function, version 1#include "opend.h" char errmsg[MAXLINE]; int oflag; char *pathname; int main(void) { int nread; char buf[MAXLINE]; for ( ; ; ) { /* read arg buffer from client, process request */ if ((nread = read(STDIN_FILENO, buf, MAXLINE)) < 0) err_sys("read error on stream pipe"); else if (nread == 0) break; /* client has closed the stream pipe */ request(buf, nread, STDOUT_FILENO); } exit(0); } The function request in Figure 17.31 does all the work. It calls the function buf_args to break up the client's request into a standard argv-style argument list and calls the function cli_args to process the client's arguments. If all is OK, open is called to open the file, and then send_fd sends the descriptor back to the client across the s-pipe (its standard output). If an error is encountered, send_err is called to send back an error message, using the clientserver protocol that we described earlier. Figure 17.31. The request function, version 1#include "opend.h" #include <fcntl.h> void request(char *buf, int nread, int fd) { int newfd; if (buf[nread-1] != 0) { sprintf(errmsg, "request not null terminated: %*.*s\n", nread, nread, buf); send_err(fd, -1, errmsg); return; } if (buf_args(buf, cli_args) < 0) { /* parse args & set options */ send_err(fd, -1, errmsg); return; } if ((newfd = open(pathname, oflag)) < 0) { sprintf(errmsg, "can't open %s: %s\n", pathname, strerror(errno)); send_err(fd, -1, errmsg); return; } if (send_fd(fd, newfd) < 0) /* send the descriptor */ err_sys("send_fd error"); close(newfd); /* we're done with descriptor */ } The client's request is a null-terminated string of white-space-separated arguments. The function buf_args in Figure 17.32 breaks this string into a standard argv-style argument list and calls a user function to process the arguments. We'll use the buf_args function later in this chapter. We use the ISO C function strtok to tokenize the string into separate arguments. Figure 17.32. The buf_args function#include "apue.h" #define MAXARGC 50 /* max number of arguments in buf */ #define WHITE " \t\n" /* white space for tokenizing arguments */ /* * buf[] contains white-space-separated arguments. We convert it to an * argv-style array of pointers, and call the user's function (optfunc) * to process the array. We return -1 if there's a problem parsing buf, * else we return whatever optfunc() returns. Note that user's buf[] * array is modified (nulls placed after each token). */ int buf_args(char *buf, int (*optfunc)(int, char **)) { char *ptr, *argv[MAXARGC]; int argc; if (strtok(buf, WHITE) == NULL) /* an argv[0] is required */ return(-1); argv[argc = 0] = buf; while ((ptr = strtok(NULL, WHITE)) != NULL) { if (++argc >= MAXARGC-1) /* -1 for room for NULL at end */ return(-1); argv[argc] = ptr; } argv[++argc] = NULL; /* * Since argv[] pointers point into the user's buf[], * user's function can just copy the pointers, even * though argv[] array will disappear on return. */ return((*optfunc)(argc, argv)); } The server's function that is called by buf_args is cli_args (Figure 17.33). It verifies that the client sent the right number of arguments and stores the pathname and open mode in global variables. Figure 17.33. The cli_args function#include "opend.h" /* * This function is called by buf_args(), which is called by * request(). buf_args() has broken up the client's buffer * into an argv[]-style array, which we now process. */ int cli_args(int argc, char **argv) { if (argc != 3 || strcmp(argv[0], CL_OPEN) != 0) { strcpy(errmsg, "usage: <pathname> <oflag>\n"); return(-1); } pathname = argv[1]; /* save ptr to pathname to open */ oflag = atoi(argv[2]); return(0); } This completes the open server that is invoked by a fork and exec from the client. A single s-pipe is created before the fork and is used to communicate between the client and the server. With this arrangement, we have one server per client. |