《UNIX网络编程:卷2》P47:图4-23 处理多个客户请求的FIFO服务器程序
/*P47 mainserver.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <errno.h> #include <string.h> #define MAXLINE 1024 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) #define SERV_FIFO "/tmp/fifo.serv" ssize_t readline(int fd, void *vptr, size_t maxlex); int main(int argc, char *argv[]) { int readfifo, writefifo, dummyfd, fd; char *ptr, buff[MAXLINE+1], fifoname[MAXLINE]; pid_t pid; ssize_t n; // 创建服务器的众所周知FIFO,就算它已经存在也没问题 if ((mkfifo(SERV_FIFO, FILE_MODE) < 0) && (errno != EEXIST)) { fprintf(stderr, "can't create %s: %s\n", SERV_FIFO, strerror(errno)); } // 打开服务器的众所周知FIFO读 if ((readfifo = open(SERV_FIFO, O_RDONLY, 0)) < 0) { fprintf(stderr, "open %s error: %s\n", SERV_FIFO, strerror(errno)); exit(1); } // 打开服务器的众所周知FIFO写;不用 if ((dummyfd = open(SERV_FIFO, O_WRONLY, 0)) < 0) { fprintf(stderr, "open %s error: %s\n", SERV_FIFO, strerror(errno)); exit(1); } // 每个客户请求是由进程ID、一个空格再加上路径名构成的单行 while ((n = readline(readfifo, buff, MAXLINE)) > 0) { if (buff[n-1] == '\n') n--; // 删除换行符 buff[n] = '\0'; // 搜索空格 if ((ptr = strchr(buff, ' ')) == NULL) { fprintf(stderr, "bogus request: %s\n", buff); continue; } *ptr++ = 0; // ptr增1后指向后跟的路径名的首字符 pid = atol(buff); // 将字符串转换成long类型 // 构建一个客户FIFO路径 snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long)pid); // 只写方式打开该客户FIFO if ((writefifo = open(fifoname, O_WRONLY, 0)) < 0) { fprintf(stderr, "cannot open: %s\n", fifoname); continue; } // 打开客户请求的文件 if ((fd = open(ptr, O_RDONLY)) < 0) { // 打开文件失败,构建出错字符串 snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n", strerror(errno)); n = strlen(ptr); // 通过客户的FIFO向客户返回一个出错消息 if (write(writefifo, ptr, n) != n) { fprintf(stderr, "write error: %s\n", strerror(errno)); } close(writefifo); } else { // 打开文件成功,把文件的内容复制到客户的FIFO中 while ((n = read(fd, buff, MAXLINE)) > 0) { if (write(writefifo, buff, n) != n) { fprintf(stderr, "write error: %s\n", strerror(errno)); } close(fd); // 关闭打开的文件 close(writefifo); // 关闭客户的FIFO,使得客户的read返回0(文件结束符) } } } exit(0); } ssize_t readline(int fd, void *vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { again: if ((rc = read(fd, &c, 1)) == 1) { *ptr++ = c; if (c == '\n') // 新的一行 break; } else if (rc == 0) { *ptr = 0; // 文件结束符,读取了(n-1)字节数据 return(n - 1); } else { if (errno == EINTR) goto again; return(-1); // 读取出错 } } *ptr = 0; // return(n); }
打开FIFO来写的原因:要使我们不这么做,那么每当有一个客户终止时,该FIFO就变为空,于是服务器的read返回0,表示一个文件结束符。我们将不得不close该FIFO,并以O_RDONLY标志再次调用open,不过该调用会一直阻塞到下一个客户请求到达为止。然而如果我们总是有一个该FIFO的描述符打开着用于写,那么当不再有客户请求存在时,服务器的read一定不会返回0以指示读到一个文件结束符。相反,服务器只是阻塞在read调用中,等待下一个客户请求。
服务器启动时,它的第一个open(使用O_RDONLY)将阻塞到第一个客户只写打开服务器的FIFO为止。它的第二个open(使用O_WRONLY)则立即返回,因为这时FIFO已经打开着用于读了。
《UNIX网络编程:卷2》P48:图4-24 服务器程序协同工作的客户程序
/* P48 mainclient.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <string.h> #include <errno.h> #define MAXLINE 1024 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) #define SERV_FIFO "/tmp/fifo.serv" int main(int argc, char *argv[]) { int readfifo, writefifo; size_t len; ssize_t n; char *ptr, fifoname[MAXLINE], buff[MAXLINE]; pid_t pid; pid = getpid(); // 获取进程ID // 构建客户FIFO路径 snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long)pid); // 创建客户FIFO if ((mkfifo(fifoname, FILE_MODE) < 0) && (errno != EEXIST)) fprintf(stderr, "can;t create %s\n", fifoname); // 客户的请求是由其进程ID、一个空格、一个路径名和一个换行符构成 snprintf(buff, sizeof(buff), "%ld ", (long)pid); len = strlen(buff); ptr = buff + len; // 路径名指代请求服务器发给本客户的文件,从标准输入读入 fgets(ptr, MAXLINE - len, stdin); len = strlen(buff); // 打开服务器的FIFO if ((writefifo = open(SERV_FIFO, O_WRONLY, 0)) < 0) { fprintf(stderr, "open %s error: %s\n", SERV_FIFO, strerror(errno)); } // 往服务器FIFO中写入请求 if (write(writefifo, buff, len) != len) { fprintf(stderr, "write error: %s\n", strerror(errno)); } // 打开本客户的FIFO if ((readfifo = open(fifoname, O_RDONLY, 0)) < 0) { fprintf(stderr, "open %s error: %s\n", fifoname, strerror(errno)); } // 从本客户的FIFO中读出服务器的应答,并写到标准输出 while((n = read(readfifo, buff, MAXLINE)) > 0) { if (write(STDOUT_FILENO, buff, n) != n) fprintf(stderr, "write error: %s\n", strerror(errno)); } close(readfifo); // 关闭该客户的FIFO unlink(fifoname); // 删除FIFO文件 exit(0); }
Makefile文件:
mainserver: gcc mainserver.c -o mainserver -Wall mainclient: gcc mainclient.c -o mainclient -Wall clean: rm mainserver mainclient
可以在一个窗口中启动服务器:
$ ./mainserver
在另一个窗口中运行客户:
$ ./mainclient /etc/shadow 一个我们不能读的文件 /etc/shadow: can't open, Permission denied $ ./mainclient Makefile 一个文本文件 mainserver: gcc mainserver.c -o mainserver -Wall mainclient: gcc mainclient.c -o mainclient -Wall clean: rm mainserver mainclient
我们还可以从shell中与服务器交互,因为FIFO在文件系统中有名字:
$ Pid=$$ 本shell的进程ID $ mkfifo /tmp/fifo.$Pid 创建客户的FIFO $ echo "$Pid Makefile" > /tmp/fifo.serv 将命令写入服务器FIFO $ cat < /tmp/fifo.$Pid 读出服务器的应答 mainserver: gcc mainserver.c -o mainserver -Wall mainclient: gcc mainclient.c -o mainclient -Wall clean: rm mainserver mainclient $ rm /tmp/fifo.$Pid 删除客户FIFO
在我们的shell例子中,服务器读出客户的请求后,会阻塞在对客户的FIFO的open调用中,因为客户(shell)还没有打开该FIFO来读。服务器对该FIFO的open调用一直阻塞到我们在以后某个时候执行cat命令为止,该命令打开这个FIFO,服务器对该FIFO的open调用随之返回。这种时间顺序关系还会导致拒绝服务型攻击。