1 select 函数原型
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
/*
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; //seconds
long tv_usec; // microseconds
};
返回值:
> 0 : 所有监听集合中,满足对于事件的总数
0 : 没有满足监听条件的文件描述符
-1 : errno
*/
void FD_CLR(int fd, fd_set *set); //把文件描述符集合set里fd位清0
int FD_ISSET(int fd, fd_set *set); //文件描述符集合set里fd是否置1 返回值:1-已经置1; 0-没有置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
2 select实现多路IO转接(代码)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <ctype.h>
int main()
{
int listenfd, connfd; // 监听套接字、连接套接字
int maxfd = 0; // 最大的套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listenfd)
{
perror("socket error");
exit(1);
}
// 端口复用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in saddr, caddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8000);
saddr.sin_addr.s_addr = inet_addr("192.168.71.132");
int ret = bind(listenfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (-1 == ret)
{
perror("bind error");
close(listenfd);
exit(1);
}
ret = listen(listenfd, 5);
if (-1 == ret)
{
perror("listen error");
exit(1);
}
maxfd = listenfd; // 最大文件描述符
fd_set rset, allset; // 定义读集合rset、备份集合allset
FD_ZERO(&rset); FD_ZERO(&allset); // 清空集合
FD_SET(listenfd, &allset); // 将listenfd添加到allset集合中
int nReady = 0;
while (1)
{
rset = allset;
nReady = select(maxfd+1, &rset, NULL, NULL, NULL); // 阻塞监听等待有事件发生
if (-1 == nReady)
{
perror("select error");
exit(1);
}
else if(0 == nReady) // 因为是阻塞的监听,所以此条件一定不满足
{
}
else
{
if (FD_ISSET(listenfd, &rset)) // 满足监听 读事件
{
socklen_t caddrLen = sizeof(caddr);
connfd = accept(listenfd, (struct sockaddr*)&caddr, &caddrLen); // 与客户端连接
if (-1 == connfd)
{
perror("accept error");
exit(1);
}
printf("建立连接成功:%s:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
FD_SET(connfd, &allset); // 将新产生的fd,添加到allset集合中,监听数据读事件
if (maxfd < connfd) // 修改maxfd
{
maxfd = connfd;
}
if (nReady == 1) // 说明select只返回一个,并且是listenfd,无需后续执行
{
continue;
}
}
for (int ii = listenfd+1; ii <= maxfd; ++ii) // 处理满足读事件的fd
{
if (FD_ISSET(ii, &rset)) // 找到满足读事件的fd
{
// 通信
char buf[1024] = {0};
memset(buf, 0, sizeof(buf));
int n = recv(ii, buf, sizeof(buf), 0);
if (-1 == n)
{
perror("recv error");
exit(1);
}
else if (0 == n) // 监测到客户端已经关闭连接
{
printf("断开连接\n");
FD_CLR(ii, &allset); // 移除出监听集合,关闭fd
close(ii);
continue;
}
printf("Recv:%s\n", buf);
}
}
}
}
close(listenfd);
return 0;
}
3 select优缺点
-
缺点
- 监听上限文件描述符。最大1024
- 监测满足条件的fd是轮询模式,需要自己添加业务逻辑提高效率,增加了编码难度
-
优点
- 跨平台。win、Linux、macOS、Unix、类Unix、mips
4 优化
监测满足条件的fd是轮询模式,需要自己添加业务逻辑提高效率。
可以通过添加一个数组,将需要监听的connfd(与客户端连接的套件字)加入到此数组中,通过轮询数组而不是轮询到最大的文件描述符。以此来提高效率。
以下为参考代码
//IO多路复用技术select函数的使用(优化)
// INET_ADDRSTRLEN /* #define INET_ADDRSTRLEN 16 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
int main()
{
int i;
int n;
int lfd;
int cfd;
int ret;
int nready;
int maxfd;//最大的文件描述符
char buf[FD_SETSIZE]; // FD_SETSIZE默认为1024
socklen_t len;
int maxi; //有效的文件描述符最大值
int connfd[FD_SETSIZE]; //有效的文件描述符数组
fd_set tmpfds, rdfds; //要监控的文件描述符集
struct sockaddr_in svraddr, cliaddr;
//创建socket
lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd<0)
{
perror("socket error");
return -1;
}
//允许端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
//绑定bind
svraddr.sin_family = AF_INET;
svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
svraddr.sin_port = htons(8888);
ret = bind(lfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));
if(ret<0)
{
perror("bind error");
return -1;
}
//监听listen
ret = listen(lfd, 5);
if(ret<0)
{
perror("listen error");
return -1;
}
//文件描述符集初始化
FD_ZERO(&tmpfds);
FD_ZERO(&rdfds);
//将lfd加入到监控的读集合中
FD_SET(lfd, &rdfds);
//初始化有效的文件描述符集, 为-1表示可用, 该数组不保存lfd
for(i=0; i<FD_SETSIZE; i++)
{
connfd[i] = -1;
}
maxfd = lfd;
len = sizeof(struct sockaddr_in);
//将监听文件描述符lfd加入到select监控中
while(1)
{
//select为阻塞函数,若没有变化的文件描述符,就一直阻塞,若有事件发生则解除阻塞,函数返回
//select的第二个参数tmpfds为输入输出参数,调用select完毕后这个集合中保留的是发生变化的文件描述符
tmpfds = rdfds;
nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
if(nready>0)
{
//发生变化的文件描述符有两类, 一类是监听的, 一类是用于数据通信的
//监听文件描述符有变化, 有新的连接到来, 则accept新的连接
if(FD_ISSET(lfd, &tmpfds))
{
cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd<0)
{
if(errno==ECONNABORTED || errno==EINTR)
{
continue;
}
break;
}
//先找位置, 然后将新的连接的文件描述符保存到connfd数组中
for(i=0; i<FD_SETSIZE; i++)
{
if(connfd[i]==-1)
{
connfd[i] = cfd;
break;
}
}
//若连接总数达到了最大值,则关闭该连接
if(i==FD_SETSIZE)
{
close(cfd);
printf("too many clients, i==[%d]\n", i);
//exit(1);
continue;
}
//确保connfd中maxi保存的是最后一个文件描述符的下标
if(i>maxi)
{
maxi = i;
}
//打印客户端的IP和PORT
char sIP[16];
memset(sIP, 0x00, sizeof(sIP));
printf("receive from client--->IP[%s],PORT:[%d]\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, sIP, sizeof(sIP)), htons(cliaddr.sin_port));
//将新的文件 描述符加入到select监控的文件描述符集合中
FD_SET(cfd, &rdfds);
if(maxfd<cfd)
{
maxfd = cfd;
}
//若没有变化的文件描述符,则无需执行后续代码
if(--nready<=0)
{
continue;
}
}
//下面是通信的文件描述符有变化的情况
//只需循环connfd数组中有效的文件描述符即可, 这样可以减少循环的次数
for(i=0; i<=maxi; i++)
{
int sockfd = connfd[i];
//数组内的文件描述符如果被释放有可能变成-1
if(sockfd==-1)
{
continue;
}
if(FD_ISSET(sockfd, &tmpfds))
{
memset(buf, 0x00, sizeof(buf));
n = read(sockfd, buf, sizeof(buf));
if(n<0)
{
perror("read over");
close(sockfd);
FD_CLR(sockfd, &rdfds);
connfd[i] = -1; //将connfd[i]置为-1,表示该位置可用
}
else if(n==0)
{
printf("client is closed\n");
close(sockfd);
FD_CLR(sockfd, &rdfds);
connfd[i] = -1; //将connfd[i]置为-1,表示该位置可用
}
else
{
printf("[%d]:[%s]\n", n, buf);
write(sockfd, buf, n);
}
if(--nready<=0)
{
break; //注意这里是break,而不是continue, 应该是从最外层的while继续循环
}
}
}
}
}
//关闭监听文件描述符
close(lfd);
return 0;
}