select多路复用
1.1 select实现原理
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
在初学socket编程的时候,经常就是流程化的写socket,bind,listen,accept,其中server端在创建好文件描述符之后就阻塞在accept等待客户端的连接,函数不能立即返回。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
基于select的I/O复用模型的是单进程执行可以为多个客户端服务,这样可以减少创建线程或进程所需要的CPU时间片或内存资源的开销;此外几乎所有的平台上都支持select(),其良好跨平台支持是它的另一个优点select函数实现I/O端口的复用,传递给 select函数的参数会告诉内核:
- 所需要关系的文件描述符
- 所希望监控文件描述符的属性变化,包括readfds(文件描述符有数据到来可读),writefds(可写),exceptfds(异常)。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间)发生函数才返回。当select()函数返回后,可以通过遍历 fdset,来找到究竟是哪些文件描述符就绪。
- 带三个参数是select的超时时间,设置NULL则永不超时。
第一个参数表示最大文件描述符加一(maxfd+1)Linux内核从0开始到max_fd-1扫描文件描述
符,如果有数据出现事件(读、写、异常)将会返回;假设需要监测的文件描述符是8,9,10,那么Linux内核实际也要监测0~7,此时真正测试的文件描述符为0-10总共11个,即max(8,9,10)+1,所以第一个参数是所有要监听的文件描述符中最大的+1.
中间的三个集合fd_set实际上可以理解成特定的位来标识文件描述符,对于集合可以使用四个函数来操作。
- FD_ZERO(fd_set* fds) //清空集合
- FD_SET(int fd, fd_set* fds) //将给定的描述符加入集合
- FD_ISSET(int fd, fd_set* fds) //判断指定描述符是否在集合中
- FD_CLR(int fd, fd_set* fds) //将给定的描述符从文件中删除
理解select函数:
取fd_set长度为1个字节,fd_set中的每一个bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
- 执行FD_ZERO(&fd_set);表示把集合清0,用位表示位0000 0000
- 执行FD_SET(fd,&fd_set);表示吧fd加入到需要监听的集合中,如果有三个集合分别为fd=1,fd=3,fd=5.则fd_set用位表示变成0001 0101。
- select(6,&set,0,0,0)阻塞等待
- 如果5没有发生事件,只有1和3发生事件,则select返回后,fd_set变成0000 0101。未发生事件的fd被清空。由于会把之前加入集合但是没有发生时间的fd清空所以,在select之前会使用一个fds_arry数组来保存fd,每次开始再把fds_arry中的数据加入fd_set。
第三个参数设置select的超时事件NULL表示永不超时时间设置是通过结构体来实现
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
};
函数返回:
- 当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,内核(I/O)根据状态修改文件描述符集,并返回 发生事件的文件描述符。
- 当超时后则返回0。
- 发生错误返回负值。
select的缺点:
- 单个进程能够监视的文件描述符的数量存在最大限制,在Linux内核有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数中通常是1024,可以通过setrlimit()、修改宏定义等方法提示限制,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差。
- 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
- select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件,假设数组中只有7、8、9三个文件描述符发生事件但是select还是回访问前面的0-7个文件描述符;
1.2 select流程图
1.3 select编写socket服务器程序
/*********************************************************************************
* Copyright: (C) 2021 jiaoer237
* All rights reserved.
*
* Filename: socket_server_select.c
* Description: This file
*
* Version: 1.0.0(11/23/2021)
* Author: yanp <2405204881@qq.com>
* ChangeLog: 1, Release initial version on "11/23/2021 04:35:39 PM"
*
********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <pthread.h>
#include <getopt.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define MSG_STR "Hello yanp\n"
#define BACKLOG 13
void printf_usage(char *program);
int socket_init(char *listen_ip,int listen_port);
int main(int argc,char **argv)
{
int daemon_run=0;
char *program;
int serv_port=0;
int listen_fd;
int clifd=-1;
int rv=-2;
int opt;
fd_set rdset;
int fds_array[1024];
int i,maxfd=0;
struct timeval timeout={300,0};/*设置超时时间为300s*/
char buf[1024];
struct sockaddr_in cliaddr;
socklen_t cliaddr_len;
struct option long_options[] =
{
{"daemon", no_argument, NULL, 'b'},
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
program=argv[0];
while((opt = getopt_long(argc, argv, "bp:h", long_options, NULL)) != -1)
{
switch(opt)
{
case 'b':
daemon_run=1;
break;
case 'p':
serv_port = atoi(optarg);/*将字符串转换成整型*/
break;
case 'h':
printf_usage(program);
break;
default:
break;
}
}
if(!serv_port)
{
printf_usage(argv[0]);
return -1;
}
if((listen_fd=socket_init(NULL,serv_port))<0)/*socket初始化*/
{
printf("socket_init failure error:%s",strerror(errno));
return -2;
}
if(daemon_run)
{
daemon(0,0);
}
for(i=0;i<1024;i++)/*创建一个存放文件描述符的数组并赋值为-1*/
{
fds_array[i]=-1;
}
fds_array[0]=listen_fd;/*将listen_fd作为第一个需要监听的文件描述符*/
while(1)
{
printf("start accept new client income or client send message...\n");
FD_ZERO(&rdset);/*将rdset清0*/
for(i=0;i<1024;i++)/*更新fd_set和maxfd,将数组fds_arry中的fd加入fd_set集合中*/
{
if(fds_array[i]<0)
continue;
maxfd=fds_array[i]>maxfd?fds_array[i]:maxfd;
FD_SET(fds_array[i],&rdset);
}
rv=select(maxfd+1,&rdset,NULL,NULL,&timeout);/*这里只关心rdset,每当有fd发生事件则返回*/
if(rv<0)
{
printf("select failure:%s\n",strerror(errno));
continue;
}
else if(0==rv)
{
printf("select timeout\n");
continue;
}
else
{
if(FD_ISSET(listen_fd, &rdset))/*listen_fd发生事件,新的客户端连接*/
{
if((clifd=accept(listen_fd,(struct sockaddr *)&cliaddr,&cliaddr_len))<0)
{
printf("listen_fd accept new client failure:%s",strerror(errno));
return -5;
}
int found=0;
for(i=0;i<1024;i++)/*只要数组元素为-1则表示数组的这个位可用,则将数组的元素设置为fd的值,保存在数组中,用于更新select的fd_set,且select的个数不能大于1024个*/
{
if(fds_array[i]<0)
{
printf("accept new client[%d] and add it to arry\n",clifd);
fds_array[i]=clifd;
found=1;
break;
}
}
if(!found)
{
printf("accept new client[%d],but arry full\n",clifd);
close(clifd);
}
}
else/*已经连接的客户端发生反应则发生读写操作*/
{
for(i=0;i<1024;i++)
{
if(fds_array[i]<0||!FD_ISSET(fds_array[i],&rdset))
continue;
if((rv=read(fds_array[i],buf,sizeof(buf)))<=0)
{
printf("read from client[%d] failure\n",fds_array[i]);
close(fds_array[i]);
fds_array[i]=-1;
}
else
{
int j;
printf("socket[%d] read get %d bytes data\n", fds_array[i], rv);
for(j=0; j<rv; j++)
{
buf[j]=toupper(buf[j]);
}
if( write(fds_array[i], buf, rv) < 0 )
{
printf("socket[%d] write failure: %s\n", fds_array[i], strerror(errno));
close(fds_array[i]);
fds_array[i] = -1;
}
}
}
}
}
}
}
void printf_usage(char *program)
{
printf("使用方法:%s【选项】 \n", program);
printf(" %s是一个服务器程序,用来等待客户端的连接\n",program);
printf("\n传入参数\n");
printf(" -b[daemon]设置程序在后台运行\n");
printf(" -p[port ] 指定连接的端口号\n");
printf(" -h[help ] 打印帮助信息\n");
printf("\n例如: %s -b -p 8900\n", program);
return;
}
int socket_init(char *listen_ip,int listen_port)
{
int listenfd;
struct sockaddr_in servaddr;
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
printf("socket_server to create a TCP socket fd failure:[%s]\n",strerror(errno));
return -1;
}
printf("create a tcp socket fd[%d] success\n",listenfd);
int on=1;
if((setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0)
{
printf("setsockopt failure:%s",strerror(errno));
return -2;
}
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(listen_port);
if(!listen_ip)
{
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
}
else
{
servaddr.sin_addr.s_addr=htonl(listen_port);
}
if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))<0)
{
printf("socket[%d] bind on port[%d] for ip address failure:%s\n",listenfd,listen_port,strerror(errno));
return -3;
}
printf("socket[%d] bind on port[%d] for ip address success\n",listenfd,listen_port);
listen(listenfd,BACKLOG);
printf("start listen on port[%d]\n",listen_port);
return listenfd;
}