select函数
1、select的作用
系统提供select函数来实现多路复用输入/输出模型。select系统调用是用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变。关于文件句柄,其实就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。
2、select的函数格式
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int nfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数值无所谓,可以设置不正确。
fd_set* readfds:是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
fd_set* writefds:是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
fe_set* errorfds:同上面两个参数的意图,用来监视文件错误异常。
struct timeval* timeout:是select的超时时间,这个参数至关重要,它可以使select处于三种状态。
timeout的三种状态:
NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件。
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
3、两个结构体
第一:struct fd_set:可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以,毫无疑问,一个socket就是一个文件,socket句柄就是一个文件描述符。
第二:struct timeval:是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个毫秒数。
4、宏提供了处理这三种描述词组的方式:
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
FD_CLR(inr fd,fd_set* set):用来清除描述词组set中相关fd 的位。
FD_ISSET(int fd,fd_set *set):用来测试描述词组set中相关fd 的位是否为真。
FD_SET(int fd,fd_set*set):用来设置描述词组set中相关fd的位。
FD_ZERO(fd_set *set):用来清除描述词组set的全部位。
5、select函数返回值
负值:select错误。
正值:某些文件可读写或出错。
0:等待超时,没有可读写或错误的文件。
6、select函数模型特点
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个 参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
7、select优点
Select函数在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、 accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
8、select缺点
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
(3)select支持的文件描述符数量太小了,默认是1024。
9、select函数编写的简易服务器
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
int array_fds[1024]; //用来存储文件描述符
static void Usage(const char *proc)
{
printf("Usage: %s [local_ip] [local_port]\n",proc);
}
int startup(char *_ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}
int flg = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &flg, sizeof(flg));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local,sizeof(local)) < 0){
perror("bind");
exit(3);
} //绑定socket
if(listen(sock, 10) < 0){
perror("listen");
exit(4);
}//监听socket
return sock;
}
int main(int argc, char *argv[])
{
if(argc != 3){
Usage(argv[0]);
return 0;
}
int listenSock = startup(argv[1], atoi(argv[2]));
int maxfd = 0;//最大文件描述符的值
fd_set rfds; //文件描述符的读属性,可读返回0
int array_size = sizeof(array_fds)/sizeof(array_fds[0]);//文件描述符的个数
array_fds[0] = listenSock; //套接字本质是文件描述符
int i = 1;
for(; i<array_size; i++)
{
array_fds[i] = -1;
}
while(1){
struct timeval _timeout = {0, 0}; //非阻塞
FD_ZERO(&rfds);//清空集合,否则不能检查描述符的变化
maxfd = -1; //初始化文件描述符最大值为-1
for(i=0; i<array_size; i++){
if(array_fds[i] > 0){
FD_SET(array_fds[i], &rfds); //添加文件描述符
if(array_fds[i] > maxfd){
maxfd = array_fds[i]; //更新最大文件描述符
}
}
}
switch(select(maxfd+1, &rfds, NULL, NULL, NULL)){
case 0:
printf("timeout...\n");
break;
case -1:
perror("select");
break;
default:
{
int j = 0;
for(; j<array_size; j++){
if(array_fds[j] < 0){
continue;
}
if(j == 0 && FD_ISSET(array_fds[j], &rfds)){
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_fd = accept(array_fds[j],\
(struct sockaddr*)&client, &len);
char buf[1024];
if(new_fd < 0){
perror("accept");
continue;
}else{
if(FD_ISSET(array_fds[j], &rfds)){
write(array_fds[j], buf, strlen(buf));
}
printf("get a new client:(%s:%d)\n",\
inet_ntoa(client.sin_addr),\
ntohs(client.sin_port));
int k = 1;
for(; k<array_size; k++){
if(array_fds[k] < 0){
array_fds[k] = new_fd;
break;
}
}
if(k == array_size){ //数组已满
close(new_fd);
}
}
}//fi
else if(j != 0 &&\
FD_ISSET(array_fds[j], &rfds)){
char buf[10240];
ssize_t s = read(array_fds[i],\
buf, sizeof(buf)-1);
if(s > 0){
buf[s] = 0;
printf("client say: %s\n", buf);//打印数据
}else if(s == 0){
printf("client quit!\n");
close(array_fds[j]);
array_fds[j] = -1;
}
}else{
}
}
}
break;
}
}
}