I/O多路转接之select
一、基础概念
1、select
使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况–读写或是异常。
函数原型:
参数解释:
(1)参数nfds是需要监视的最大的文件描述符值+1;
(2)rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
(3)参数timeout为结构timeval,用来设置select()的等待时间
返回值:
(1) 负值:select错误
(2)正值:某些文件可读写或出错
(3)0:等待超时,没有可读写或错误的文件
timeout的取值:
(1) NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
(2)0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
(3)特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回
2、fd_set 结构
我们在select函数的参数中可以发现,第二、三、四个参数都是一个关于fd_set的结构体指针。
fd_set结构体:
fd_set 其实就是一个整形数组,更严格地说,是一个“位图”,使用位图中对应的位来表示要监视的文件描述符。
系统中也提供了一些操作fd_set的函数接口:
void FD_CLR(int fd, fd_set *set); // ⽤用来清除描述词组set中相关fd 的位 int FD_ISSET(int fd, fd_set *set); // ⽤用来测试描述词组set中相关fd 的位是否为真 void FD_SET(int fd, fd_set *set); // ⽤用来设置描述词组set中相关fd的位 void FD_ZERO(fd_set *set); // ⽤用来清除描述词组set的全部位
timeval结构:
select的第五个参数timeout的类型是timeval,timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的文件描述符没有事件发生则函数返回,返回值为0。
struct timeval{
_time_t tv_sec;
_suseconds_t tv_usec;
函数返回值:
(1)执行成功则返回文件描述词状态已改变的个数
(2)如果返回0代表在描述词状态改变前已超过timeout时间,没有返回 (3)当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout 的值变成不可预测。
错误值可能为: * EBADF 文件描述词为无效的或该文件已关闭 * EINTR 此调用被信号所中断 * EINVAL 参数n 为负值。 * ENOMEM 核心内存不⾜
3、select的执行过程
select函数的调用过程如下图:
在调用select函数之前需要做一些准备工作:
(1)设置文件描述符
select可以同时监视多个文件描述符(套接字)。
此时需要先将文件描述符集中到一起。集中时也要按照监视项(接收,传输,异常)进行区分,即按照上述3种监视项分成三类。
使用fd_set数组变量执行此项操作,该数组是存有0和1的位数组。
为1表示用户告诉操作系统要监视该文件描述符,为0则不需要关心
(2)设置监视范围及超时
在调用select函数之前,我们还需要确定两件事情:
“文件描述符的监视范围是?”
文件描述符的监视范围与第一个参数有关,实际上,select函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在fd_set变量中的文件描述符数。但每次新建文件描述符时,其值都会增1,故只需将最大的文件描述符值加1再传递到select函数即可。(加1是因为文件描述符的值从0开始)
“如何设定select函数的超时时间?”
超时时间与 最后一个参数有关,本来select函数只有在监视文件描述符发生变化时才返回,未发生变化会进入阻塞状态。指定超时时间就是为了防止这种情况发生。
不想设置超时最后一个参数只需要传递NULL。
(3)查看调用结果
如果select返回值大于0,说明文件描述符发生了变化。
关于文件描述符变化:
文件描述符变化是指监视的文件描述符中发生了相应的监视事件。
例如通过select的第二个参数传递的集合中存在需要读取数据的描述符时,就意味着文件描述符发生变化。
select 函数调用结束会告诉用户哪些文件描述符上的哪些事件就绪了
4、socket就绪条件
读就绪:
1)socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0; 2)socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
3)监听的socket上有新的连接请求;
4)socket上有未处理的错误;
写就绪
1)socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩), 大于等于低水位标记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
2)socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发 SIGPIPE信号;
3)socket使⽤非阻塞connect连接成功或失败之后;
4)socket上有未读取的错误;
异常就绪
socket上收到带外数据.
二、编写简单的select服务器
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 #include<unistd.h>
5 #include<sys/socket.h>
6 #include<sys/select.h>
7 #include<netinet/in.h>
8 #include<arpa/inet.h>
9
10 #define INIT_DATA -1
11 #define MAX_FD 1024
12
13 static void InitArray(int fdArray[],int size)
14 {
15 int i=0;
16 for(;i<size;i++)
17 fdArray[i]=INIT_DATA;
18 }
19
20 int startUp(int port)
21 {
22 int sock=socket(AF_INET,SOCK_STREAM,0);
23 if(sock<0)
24 {
25 perror("socket");
26 exit(2);
27 }
28 int opt=1;
29 setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
30 //local
31 struct sockaddr_in local;
32 local.sin_family=AF_INET;
33 local.sin_addr.s_addr=htonl(INADDR_ANY);
34 local.sin_port=htons(port);
35 if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
36 {
37 perror("bind");
38 exit(3);
39 }
40 if(listen(sock,5)<0)
41 {
42 perror("listen");
43 exit(4);
44 }
45 return sock;
46 }
47
48 static int AddtoArray(int fd,int array[],int num)
49 {
50 int i=0;
51 for(i=0;i<num;i++)
52 {
53 if(array[i]==INIT_DATA)
54 {
55 array[i]=fd;
56 return i;
57 }
58 }
59 return -1;
60 }
61
62 static int AddtoFdset(int array[],int num,fd_set*rfds)
63 {
64 int i=0;
65 int max_fd=INIT_DATA;
66 for(;i<num;i++)
67 {
68 if(array[i]>=0)
69 {
70 FD_SET(array[i],rfds);
71 if(max_fd<array[i])
72 max_fd=array[i];
73 }
74 }
75 return max_fd;
76 }
77
78 static void serverice_select(int array[],int num,fd_set *rfds)
79 {
80 int i=0;
81 for(;i<num;i++)
82 {
83 if(array[i]>INIT_DATA)
84 {//listen_sock & normal
85 int fd=array[i];
86 if(i==0&&FD_ISSET(array[i],rfds))
87 {
88 //listen_sock ready
89 struct sockaddr_in client;
90 socklen_t len=sizeof(client);
91 int sock=accept(fd,(struct sockaddr*)&client,&len);
92 if(sock<0)
93 {
94 perror("accept");
95 continue;
96 }
97 printf("get a connect[%s:%d]\n",inet_ntoa(client.sin_addr),\
98 ntohs(client.sin_port));
99 int ret=AddtoArray(sock,array,num);
100 if(ret==-1)//array full
101 close(sock);
102 }
103 else if(i!=0&&FD_ISSET(array[i],rfds))
104 {
105 //normal fd ready
106 printf("read:)\n");
107 char buf[1024];
108 ssize_t s=read(fd,buf,sizeof(buf)-1);
109 if(s<0)
110 {
111 perror("read");
112 close(fd);
113 array[i]=INIT_DATA;
114 }
115 else if(s==0)
116 {
117 perror("read the end\n");
118 close(fd);
119 array[i]=INIT_DATA;
120 printf("client quit\n");
121 }
122 else
123 {
124 printf("read success!\n");
125 buf[s]=0;
126 printf("client:> %s\n",buf);
127 }
128 }
129 else
130 {
131 //array[i] exsit not ready
132 }
133 }
134 }
135 }
136 // ./server port
137 int main(int argc,char*argv[])
138 {
139 if(argc!=2)
140 {
141 printf("Usage:%s[port]\n",argv[0]);
142 return 1;
143 }
144 //tcp---select
145 int listen_sock=startUp(atoi(argv[1]));
146 int fdArray[MAX_FD];
147 int size=sizeof(fdArray)/sizeof(fdArray[0]);
148 InitArray(fdArray,size);
149 AddtoArray(listen_sock,fdArray,size);
150
151 fd_set read_fds;
152 for(;;)
153 {
154 FD_ZERO(&read_fds);
155 int max_fd=AddtoFdset(fdArray,size,&read_fds);
156 struct timeval timeout={0};
157 int ret=select(max_fd+1,&read_fds,NULL,NULL,NULL);
158 switch(ret)
159 {
160 case -1:
161 {
162 perror("select");
163 break;
164 }
165 case 0:
166 {
167 perror("Time_Out");
168 break;
169 }
170 default:
171 {
172 serverice_select(fdArray,size,&read_fds);
173 break;
174 }
175 }
176 }
177
178
179 }
在命令行输入./server 9999将该服务器起开:
我们会发现此时有一个端口号为9999的tcp服务器处于运行状态
紧接着,我们可以远程登录:
在新打开的终端输入telnet 127.0.0.1 9999,就可以与该服务器连接:
可以看到select服务器读到了client发来的数据。
三、select的优缺点
1、优点
(1)select函数可以处理多个socket描述符。
(2)select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
(3)select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024(软上限),可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低,也就是说1024个描述符的集合数量大小刚刚好。
2、缺点
(1)每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
(2) 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(3)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(4)select支持的文件描述符数量太小
上一篇: 使用Python编写vim插件的简单示例