select
(IO多路复用)
- select系统调⽤是⽤来让我们的程序监视多个⽂件描述符的状态变化的;
- 程序会停在select这⾥等待,直到被监视的⽂件描述符有⼀个或多个发⽣了状态改变;
#include <sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,
fd_set *exceptfds,struct timeval *timeout);
//fd_set文件描述符集合 ( 位图 )
参数:
1. 需要监视的集合里边的最大值文件描述符 +1(减少遍历次数,增加效率)
2. 文件描述符集合的 读就绪
3. 文件描述符集合的 写就绪
4. 文件描述符集合的 异常就绪
5. timeout取值
- NULL:则表⽰select()没有timeout,select将⼀直被阻塞,直到某个⽂件描述符上发⽣了事件;
- 0:仅检测描述符集合的状态,然后⽴即返回,并不等待外部事件的发⽣。
- 特定的时间值:如果在指定的时间段⾥没有事件发⽣,select将超时返回
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的全部位
读就绪
- socket内核中, 接收缓冲区中的字节数, ⼤于等于低⽔位标记SO_RCVLOWAT. 此时可以⽆阻塞的读该⽂件描述符, 并且返回值⼤于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
写就绪
- socket内核中, 发送缓冲区中的可⽤字节数(发送缓冲区的空闲位置⼤⼩), ⼤于等于低⽔位标记SO_SNDLOWAT, 此时可以⽆阻塞的写, 并且返回值⼤于0;
- socket的写操作被关闭(close或者shutdown). 对⼀个写操作被关闭的socket进⾏写操作, 会触发SIGPIPE信号;
- socket使⽤⾮阻塞connect连接成功或失败之后;
- socket上有未读取的错误
异常就绪
- TCP首部的紧急指针
select实现多客户端访问的回显服务器
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
typedef struct FdSet{
fd_set set;
int max_fd;
} FdSet;
void FdSetInit(FdSet* fds){
fds->max_fd = -1;
FD_ZERO(&fds->set);
}
void FdSetAdd(FdSet* fds,int fd){
FD_SET(fd,&fds->set);
if(fd > fds->max_fd){
fds->max_fd = fd;
}
}
//删除完找最大,此处采用从前遍历,还可采用从后遍历
//后效率相对高
void FdSetDel(FdSet* fds,int fd){
FD_CLR(fd,&fds->set);
int maxfd = -1;
int i = 0;
for(;i <= fds->max_fd;++i){
if(!FD_ISSET(i,&fds->set)){
continue;
}
if(i > maxfd){
maxfd = i;
}
}
fds->max_fd = maxfd;
}
int ServerInit(const char* ip,short port){
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0){
perror("socket");
return -1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
int ret = bind(fd,(sockaddr*)&addr,sizeof(addr));
if(ret < 0){
perror("bind");
return -1;
}
ret = listen(fd,10);
if(ret < 0){
perror("listen");
return -1;
}
return fd;
}
int ProcessRequest(int new_sock){
char buf[1024] = {0};
ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
if(read_size < 0){
perror("read");
return -1;
}
if(read_size == 0){
printf("[client %d] disconnected\n",new_sock);
//如果没有关闭文件描述符,即四次挥手没有完成,文件描述符泄露,服务器出现大量的CLOSE_WAIT
close(new_sock);
return 0;
}
buf[read_size] = '\0';
printf("[client %d]%s\n",new_sock,buf);
write(new_sock,buf,strlen(buf));
return 1;
}
int main(int argc,char* argv[]){
if(argc != 3){
printf("Usage: ./server [IP] [prot]\n");
return 1;
}
int listen_sock = ServerInit(argv[1],atoi(argv[2]));
if(listen_sock < 0){
printf("ServerInit faild\n");
return 1;
}
printf("ServerInit OK!\n");
FdSet fds;
FdSetInit(&fds);
FdSetAdd(&fds,listen_sock);
while(1){
//备份 解决文件描述符丢失
FdSet tmp = fds;
int ret = select(fds.max_fd + 1,&tmp.set,NULL,NULL,NULL);
if(ret < 0){
perror("select");
continue;
}
if(FD_ISSET(listen_sock,&tmp.set)){
//有新客户端连上服务器
sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock,(sockaddr*)&peer,&len);
if(new_sock < 0 ){
perror("accept");
continue;
}
FdSetAdd(&fds,new_sock);
printf("[client %d] connect!\n",new_sock);
}
else{
int i = 0;
for(;i<=tmp.max_fd;++i ){
if(!FD_ISSET(i,&tmp.set)){
continue;
}
int ret = ProcessRequest(i);
if(ret == 0){
FdSetDel(&fds,i);
}
}
}//else
}
return 0;
}
select特点
- 可监控的⽂件描述符个数取决与sizeof(fd_set)的值. (centos7测 128*8 = 1024)(位图,故*8)
-
- 将fd加⼊select监控集的同时,还要再使⽤⼀个数据结构array保存放到select监控集中的fd,
- ⼀是⽤于再select 返回后,array作为源数据和fdset进⾏FDISSET判断。
- ⼆(备份)是select返回后会把以前加⼊的但并⽆事件发⽣的fd清空,则每次开始select前都要重新从array取得fd逐⼀加⼊(FD_ZERO最先),扫描array的同时取得fd最⼤值maxfd,⽤于select的第⼀个参数。
select缺点
- 每次调⽤select, 都需要⼿动设置fd集合, 从接⼝使⽤⾓度来说也⾮常不便.
- 每次调⽤select,都需要把fd集合从⽤户态拷⻉到内核态,这个开销在fd很多时会很⼤
- 同时每次调⽤select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很⼤
- select⽀持的⽂件描述符数量太⼩