上一篇博客我们通过多进程和多线程可以为多个客户端提供服务,但是开多个进程或者线程消耗也增多了。其实我们只要一个进程就可以实现对多个客户端的监听,那就是多路IO复用,今天介绍其中的select函数(因为poll和epoll我还没学到哈哈)。
函数原型int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds为 最大文件描述符+1,因为函数内部要对监听的文件描述符遍历,需要for循环上限(最大文件描述符+1)
readfds为 读文件描述符集,其类型是fd_set,其实这个和信号机制中的信号屏蔽字比较像,是用位图存储信息,每个二进制位表示该位的文件描述符是否存在(1为存在 ,0为不存在)。
writefds为 写文件描述符集,exceptfds为 异常文件描述符集,这三个文件描述符集为传入传出参数,传入的是需要负责监听的文件描述符,传出的是发生事件的文件描述符
其虽然为位图的数据结构,但我们并不能对其位进行操作,而需要使用系统为我们提供的操作函数来操作:
void FD_CLR(int fd, fd_set *set);//将文件描述符从set中去除
int FD_ISSET(int fd, fd_set *set);//判断文件描述符是否在set中
void FD_SET(int fd, fd_set *set);//将文件描述符加入到set中
void FD_ZERO(fd_set *set);//将set清零,即每位置0
timeout为select阻塞等待时长,传NULL表示阻塞,传0表示非阻塞,传值大于0值则等待固定时长。
其结构体内容为:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:成功返回三个文件描述符集中包含的文件描述符个数。
一般都是客户端向服务端发送建立连接请求或者发送数据,服务端对应的第一处理动作是读,所以一般我们对select的 读文件描述符集应用较多,没有特殊需求一般writefds和exceptfds两个参数都传NULL。
需要说明的是,select函数的功能只是让内核帮我们监听客户端的请求,相应的处理还是需要我们在代码实现
下面通过代码说明select函数的一般用法:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/select.h>
4 #include<arpa/inet.h>
5 #include<ctype.h>
6 #include"wrap.h"
7 #define SERVER_PORT 6666
8 int main(void)
9 {
10 int lfd,cfd,maxfd,ret;
11 fd_set rset,allset;
12 char buf[BUFSIZ];
13 int fds[1024],maxi = -1;
14
15 lfd = Socket(AF_INET,SOCK_STREAM,0);//建立套接字
16
17 struct sockaddr_in serv_addr,client_addr;
18 socklen_t client_addr_len = sizeof(client_addr);
19
20 //绑定ip和port
21 serv_addr.sin_family = AF_INET;
22 serv_addr.sin_port = htons(SERVER_PORT);
23 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
24 Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
25
26 Listen(lfd,128);//设置允许同时连接上限
27
28 FD_ZERO(&allset);//将allset集合清零
29 FD_SET(lfd,&allset);//将lfd加入到allset集合中
30 maxfd = lfd;
31
32 for(int i = 0; i < 1024;i++)//将fds数组初始化,1024为文件描述符的上限
33 fds[i] = -1;
34 while(1)
35 {
36 rset = allset;//因为每次select都会将rset改变,所以需要将所有文件描述符赋值给他
37 ret = select(maxfd+1,&rset,NULL,NULL,NULL);
38 if(ret < 0)
39 perr_exit("select error");
40 if(FD_ISSET(lfd,&rset))
41 {
42 cfd = accept(lfd,(struct sockaddr
43 *)&client_addr,&client_addr_len);
44 FD_SET(cfd,&allset);
45 for(int i = 0; i < 1024 ;i++)//将数据通信的文件描述符存到fds数组中
46 {
47 if(fds[i] == -1)
48 {
49 fds[i] = cfd;
50 if(maxi < i)//保存最大下标
51 maxi = i;
52 break;
53 }
54 }
55 if(maxfd < cfd)//保存最大文件描述符
56 maxfd = cfd;
57
58 if(--ret == 0)//若只返回一个文件描述符,且为lfd,则无需执行后续代码
59 continue;
60 }
61 for(int i = 0;i <= maxi;i++)//遍历文件描述符数组,看那个文件描述符在返回的rset集合中
62 {
63 if(fds[i] == -1)//跳过为空的数组单元
64 continue;
65 if(FD_ISSET(fds[i],&rset))//若存在则处理客户端请求
66 {
67 int n = Read(fds[i],buf,sizeof(buf));
68 if(n == 0)
69 {
70 close(fds[i]);//关闭文件描述符
71 FD_CLR(fds[i],&allset);//将文件描述符从allset集合中去除
72 fds[i] == -1;//将文件描述符从fds数组中去除
73 }
74 for(int i = 0;i < n;i++)
75 buf[i] = toupper(buf[i]);
76 Write(fds[i],buf,n);
77 if(--ret == 0)//若已经将返回的读文件描述符处理完了,则不用继续向后循环
78 break;
79 }
80 }
81 }
82 return 0;
83 }
其中头文件wrap.h是对socket建立通信中常用到的函数的一个出错处理封装,上篇博客里有对应的.c和.h代码,这里就不再给出。