select的使用
select函数对数据结构fd_set进行操作,fd_set是一个bitmap,该集合由打开的文件描述符构成。
对于管道而言,select是通过检查管道是否阻塞,来进行监听的。只要所监听的管道非阻塞,select就能立马获知,并将其在fd_set中的相应为置1。管道非阻塞只有两种情况,一种是客户端向管道写东西,另一种是客户端将管道关闭(写端)。当然,对于非阻塞的情况,select能监听到,但它并不会知道非阻塞是因为客户端关闭管道还是客户端向管道写消息。这需要我们自己来判断。通常是通过read系统调用的返回值来判断。read返回0,说明是客户端关闭了管道,read返回正值,说明是客户端向管道写东西了。
也许你会产生这样的疑问,如果有n个客户端,那么我们不是也可以fork出n-1个子进程,共n个进程,用于接收客户端消息吗?实际上这是不可行的,因为我们一个程序最多只能打开1024个进程。因此,此处使用select是很合理的。
实现多个客户端向服务端发送消息,服务端将消息打印在标准输出上。如果有3个客户端,就事先建立3个管道,每个客户端分别使用一个管道向服务器发送消息。而在服务器端使用select系统调用,只要监测到某一管道有消息写入,服务器就将其read,并显示在标准输出上。
//server.c
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <sys/select.h> #include <sys/time.h> int main(int argc, char* argv[]) { /* 打开管道 */ int fd1, fd2, fd3 ; fd1= open(argv[1], O_RDONLY); fd2= open(argv[2], O_RDONLY); fd3= open(argv[3], O_RDONLY); printf("server: %d \n", getpid()); /* 设置select需要监听的集合fd_set */ fd_set readset ,bak_set ; FD_ZERO(&bak_set); FD_SET(fd1, &bak_set); FD_SET(fd2, &bak_set); FD_SET(fd3, &bak_set); /* 设置select轮巡时间 */ struct timeval tm ; tm.tv_sec = 10 ; tm.tv_usec = 0 ; /* 可以在程序开始,使得服务器端同时读到3个消息,此处可有可无 */ sleep(10); /* select是通过检查管道是否阻塞,来进行监听的。如果均为阻塞,select在轮巡时间内返回0。 * 如果有管道不阻塞,则select返回不阻塞的管道个数。 * 注意:当某个客户端向管道发送数据,或者关闭管道,均属于非阻塞状况。 * 我们可以根据read的返回值来区分这两种情况 */ int nret ; /* select返回值 */ char buf[1024]; while(1){ /* 每次select后都会改变select所监听集合的值,因此需要在每次select的开始重新设置监听集合。 * 如果不进行设置,那么上次select后监听集合置1的描述符仍然为1。 * 注意:在Linux系统下,时间也需要重新设置。 */ readset = bak_set ; tm.tv_sec = 10 ; tm.tv_usec = 0 ; nret = select(6 , &readset, NULL, NULL, &tm ); if(nret > 0) { printf("%d active ! \n", nret); if(FD_ISSET(fd1, &readset)) /* select返回值大于0时,必然有管道不阻塞了,此时需要查看是哪个管道*/ { memset(buf, 0 , 1024); if(0 == read(fd1, buf, 1024)) { FD_CLR(fd1, &bak_set); /* 如果一个用户其写端已经关闭,那么将其描述符从监听集合中去除 */ }else { write(1, buf, strlen(buf)); } } if(FD_ISSET(fd2, &readset)) { memset(buf, 0 , 1024); if(0 == read(fd2, buf, 1024) ) { FD_CLR(fd2, &bak_set); }else { write(1, buf, strlen(buf)); } } if(FD_ISSET(fd3, &readset)) { memset(buf, 0 , 1024); if(read(fd3, buf, 1024) == 0) { FD_CLR(fd3, &bak_set); }else { write(1, buf, strlen(buf)); } } }else if(nret == 0) { printf("time out ! \n"); } }
}
//client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <time.h> int main(int argc, char *argv[]) { /* 开启管道 */ printf("%d start ! \n", getpid()); int fd_sent; fd_sent = open(argv[1],O_WRONLY); if(fd_sent == -1) { perror("open"); exit(1); } printf("connect success! \n"); /* 客户端向管道发送消息,按ctrl+D退出循环 */ char line[1024]; /* 从标准输入获得要发送的消息 */ char msg[1024]; /* 给消息头加一个pid,表示身份 */ while(memset(line,0,1024), fgets(line,1024,stdin) != NULL) { memset(msg,0,1024); sprintf(msg, "%d : %s \n", getpid(), line); write(fd_sent, msg, strlen(msg)); } close(fd_sent); return 0; }
mkfifo 1.fifo 2.fifo 3.fifo;
./server.exe 1.fifo 2.fifo 3.fifo
./client 1.fifo ./client 2.fifo ./client 3.fifo
如果将以下代码去除:
if(0 == read(fd1, buf, 1024)) { FD_CLR(fd1, &bak_set); /* 如果一个用户其写端已经关闭,那么将其描述符从监听集合中去除 */ }
fd2与fd3的也去除,则程序运行,会陷入死循环。原因如下:我们假设客户端关闭了管道1.fifo,那么select就会监听到1.fifo非阻塞,会执行while循环中的if语句,检测到是fd1非阻塞(原因是客户端在写端关闭管道了),那么会执行write(1, buf, strlen(buf)); 因为buf为空,什么也不输出。此时,由于while(1)死循环,select接着监听,由于没有将fd1从监听集合中去除,select立马要监听到其非阻塞…如此循环往复,陷入死循环。当有一个客户端关闭写端,退出后,程序输出结果为:
1 active! 1 active! 1 active! ...
如果不强制退出,服务端程序永远不会退出去。客户端可以关闭管道退出。也可以在关闭后继续开启管道,与服务器相连。
继续改进程序
我们会让服务器拥有一个管道,专门用于从客户端接收消息(上线通知,发送需要服务器转发的消息以及下线通知)。服务器需要维护一个列表(使用结构体),记录哪些用户已经连上服务器用于接收消息的管道。当客户端启动,会向服务器发送上线消息,同时将自己的pid发送给server,server会将其添加到列表,以后会转发消息给在列表上的客户端。与此同时,客户端需要创建一个管道,用于接收服务器转发的消息;注意,要将其创建的管道名称告知服务器,以便server打开管道写端,告知管道名称可以在客户端向server发送上线消息时一并发送。 当客户端下线时也要告诉server,以便服务器将其从列表删除,这样以后不会再转发消息给它。如果不删,服务器向一个关闭读端的管道发送消息,会使服务器挂掉!(PIPE信号)
注意
我们假设现在有3个客户端,服务器用于接收消息的管道称为A。由于服务器只拥有一个管道A用于接收从客户端发送的消息,那么所有的客户端都会在管道A的另一端开启写端。也就是说所有客户端的3个写端对服务器的1个读端。通过上一篇博文,我们已经知道,select是通过阻塞与否来监听管道的。只有当管道非阻塞时,select才能获得消息,将fd_set中的相应文件描述符置1。当3个写端对1个读端时,非阻塞的情况如下:
1.当任意一个客户端的写端向管道发送消息时,该管道非阻塞,select可以监听到,read返回读到的字数。
2.所有3个写端都关闭,该管道非阻塞,select可以监听到,read返回0。注意,仅仅关闭某个客户端的写端,select是检测不到的,原因就是因为select只能检测到非阻塞的状况。
到底非阻塞是属于1或者2那种情况,select并不会知道,需要我们自己判断,通常通过read的返回值判断。当read返回值为0时,我们会在if语句中使用continue,因为服务器不能挂,客户端关闭的读端,可以在之后开启。
server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> #include <unistd.h> #include <sys/time.h> #include <sys/select.h> typedef struct tag { int s_id; /* 进程ID */ int s_fd; /* 进程描述符 */ int s_flag; /* server列表里用户是否有效 */ }USR,*pUSR; int main(int argc, char *argv[]) { /* 打开管道 */ int fd_server; fd_server = open(argv[1], O_RDONLY); if(fd_server == -1) { perror("error"); exit(1); } /* 初始化server用户列表 */ USR ulist[1024]; memset(ulist,0,sizeof(ulist)); /* 定义select参数各项参数 */ fd_set read_set,ready_set; /* ready_set是read_set的备份 */ FD_ZERO(&read_set); /* 清空fd_set */ FD_SET(fd_server, &read_set); /* 将服务器用于接收消息的管道添加到监听集合中 */ struct timeval tm; /* select轮巡时间*/ int nret; /* 记录select返回值 */ char buf[1024]; /* 存放从管道中读取的消息 */ while(1) { /* 重设select各项参数 */ tm.tv_sec = 0; tm.tv_usec = 1000; ready_set = read_set; nret = select(fd_server + 1, &ready_set, NULL, NULL, &tm); /* 在select的轮巡时间内,管道阻塞,则nret返回0 */ if(nret == 0) { continue; } if(FD_ISSET(fd_server, &ready_set)) //实际上此处if可以省略,因为只监听了一个管道 { //nret不为0,一定是该管道非阻塞 memset(buf, 0, 1024); if(0 == read(fd_server, buf,1024)) { continue; }else { if(strncmp(buf,"on",2) == 0) //on pid { int pid; char pipename[32] = ""; //存放管道名 sscanf(buf+3, "%d", &pid); //管道名义pid.pipe命名 printf("%d on \n", pid); //将客户上线消息输出在屏幕上 sprintf(pipename,"%d.fifo", pid); /* 从用户列表中,找一个无效的结构体将其存入 */ int index; for(index = 0; index < 1024; index++) { if(ulist[index].s_flag == 0) { break; } } if(index == 1024) { printf("full !\n"); }else { ulist[index].s_id = pid; ulist[index].s_fd = open(pipename,O_WRONLY); /* 打开服务器端的写端,用于转发消息给客户端 */ ulist[index].s_flag = 1; } }else if(strncmp(buf,"off",3) == 0) //off pid { int pid; sscanf(buf+4,"%d",&pid); printf("%d off!\n", pid); int index; for(index = 0;index < 1024; index++) { if(ulist[index].s_id == pid) { ulist[index].s_flag = 0; close(ulist[index].s_fd); break; } } }else { int index; for(index = 0; index < 1024; index++) { if(ulist[index].s_flag == 1) { write(ulist[index].s_fd, buf, strlen(buf)); } } } } } } }
client.c
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<stdlib.h> #include<string.h> #include<fcntl.h> int main(int argc,char *argv[]) { /* 打开上传消息给服务器的管道 */ int fd_send; fd_send=open(argv[1],O_WRONLY); if(fd_send==-1) { perror("open"); exit(1); } /* 注意一定要在向服务器发送上线消息之前创建好客户端自己的管道,不然服务端找不到该管道*/ /* pipename存放客户端自己所创建的管道,命名统一为pid.fifo */ char pipename[32]=""; sprintf(pipename,"%d.fifo",getpid()); /* 客户端创建接受消息的管道 */ if(-1==mkfifo(pipename,0666)) { perror("mkfifo"); exit(1); } /* 将上线消息写入管道 */ char msg[1024]=""; sprintf(msg,"on %d !\n",getpid()); write(fd_send,msg,strlen(msg)); /* 打开客户端自己的管道 */ int fd_rcv; fd_rcv=open(pipename,O_RDONLY); if(fd_rcv==-1) { perror("open client"); exit(1); } /* 子进程用于接收服务器转发的消息 */ if(fork()==0) { close(fd_send); while(memset(msg,0,1024),read(fd_rcv,msg,1024)>0) { printf("msg>>:"); fflush(stdout); write(1,msg,strlen(msg)); } /* 当客户端下线,服务器将其从列表中删除,并关闭客户端管道的写端。 当服务器关闭该管道的写端时,即退出while循环 */ close(fd_rcv); exit(1); } /* 父进程用于发送消息 */ close(fd_rcv); while(memset(msg,0,1024),fgets(msg,1024,stdin)!=NULL) { write(fd_send,msg,strlen(msg)); } /* 按ctrl+D退出循环,之后客户端下线 */ memset(msg,0,1024); sprintf(msg,"off %d\n",getpid()); write(fd_send,msg,strlen(msg)); close(fd_send); wait(NULL); }
运行程序,输入:
make 1.fifo ./server.exe 1.fifo ./client.exe 1.fifo ./client.exe 1.fifo ...