一、I/O复用之select原理
I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一 个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不 做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll之类的系统调用来实现,这些函数都可以同时 监视多个描述符的读写就绪状况,这样,**多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用 同一个线程。
select系统调用的目的是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。poll和select应该被归类为这样的系统 调用,它们可以阻塞地同时探测一组支持非阻塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助 调用者寻找当前就绪的设备。
二、函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数说明
nfds: 被监听的文件描述符总数,它会比文件描述符表集合中的文件描述符表的最大值大1,因为文件描述符从0开始计数
readfds:需要监听的可读事件的文件描述符集合
writefds:需要监听的可写事件的文件描述符集合
exceptfds:需要监听的异常事件的文件描述符集合
timeout:即告诉内核select等待多长时间之后就放弃等待。一般设为NULL 表示无动静就阻塞等待
struct timeval
{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
};
返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。
Select()函数配套使用的四个宏
FD_CLR(int fd,fd_set* set);用来清除描述词组set中相关fd 的位
FD_ISSET(int fd,fd_set set);用来测试描述词组set中相关fd 的位是否为真
FD_SET(int fd,fd_setset);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set);用来清除描述词组set的全部位
三、代码实例
client.c
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9000);
if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0)
{
printf("inet_pton failed exit !\n");
exit(1);
}
if(connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("connect() failed exit!\n");
exit(1);
}
int len;
char recvline[1024];
char writeline[1024];
while(1)
{
memset(recvline, 0, sizeof(recvline));
memset(writeline, 0, sizeof(writeline));
//发送消息
printf("send to server:");
fgets(writeline,sizeof(writeline),stdin);
write(sockfd,writeline,strlen(writeline));
len = read(sockfd,recvline,1024);
if(len == 0)
{
printf("server is close");
}
printf("receive from server:%s\n",recvline);
}
close(sockfd); //关闭套接字
printf("end exit !\n");
return 0;
}
server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
int on = 1;
int i;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));//设置为可重复使用的端口
struct sockaddr_in serv_addr; //服务器的地址结构体
memset(&serv_addr,0,sizeof(serv_addr));
//设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
serv_addr.sin_family = AF_INET; //选择协议族为IPV4
serv_addr.sin_port = htons(9000); //绑定我们自定义的端口号,客户端程序和我们服务器程序通讯时,就要往这个端口连接和传送数据
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有的IP地址;INADDR_ANY表示的是一个服务器上所有的网卡(服务器可能不止一个网卡)多个本地ip地址都进行绑定端口号,进行侦听。
bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(listenfd, 32);
int maxfd = listenfd;
int client[FD_SETSIZE];//数组用于存放客服端fd 循环查询使用 FD_SETSIZE 是一个宏 默认已经最大1024
fd_set rset;//放在select中 集合中都是有动静的fd 一般就一个
fd_set allset;//只做添加fd
FD_ZERO(&rset);//清空的动作
FD_ZERO(&allset);//清空的动作
FD_SET(listenfd,&allset);
//初始化数组都为-1 应为标识符从0开始
for(i=0; i<FD_SETSIZE; i++)
{
client[i] = -1;
}
int nReady;
int connfd;
while(1)
{
rset = allset;//保证每次循环 select能监听所有的文件描述符 因为rset只会剩下有动静的
nReady = select(maxfd+1,&rset,NULL,NULL,NULL);
if(FD_ISSET(listenfd, &rset))
{
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
int len = sizeof(clientaddr);
connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
char ipstr[128];//打印用到
printf("client ip%s ,port %d\n",inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,ipstr,sizeof(ipstr)),
ntohs(clientaddr.sin_port));
for(i=0; i<FD_SETSIZE; i++)
{
if(client[i] < 0)
{
client[i] = connfd;
break;
}
}
FD_SET(connfd, &allset);
if(connfd > maxfd)
{
maxfd = connfd;
}
if(--nReady <= 0)
{
continue;
}
}
else
{
for(i=0; i<FD_SETSIZE; i++)
{
if(FD_ISSET(client[i], &rset))
{
connfd = client[i];
char buf[1024] = {0};
int nread;
nread = read(connfd, buf, sizeof(buf));
if(nread == 0)
{
//四步处理 打印说明 从集合中删除 从数组中删除 关闭客服端
printf("client is close..\n");
FD_CLR(connfd, &allset);
client[i] = -1;
close(connfd);
}
else
{
write(connfd,buf,nread);
memset(buf,0,1024);
}
if(--nReady <= 0)
break;
}
}
}
}
return 0;
}
四、select模型的缺点
最大并发数限制,因为一个进程所打开的 fd(文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024,并且集合描述符最大也只能为1024,因此 select 模型的最大并发数就被相应限制了。
效率问题,采用循环的方式匹配数组内的fd是否在产生的动静集合中,如果连接的客户端数量很多,那么效率可想而知。
内核 / 用户空间 内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法,在FD非常多的时候,非常的耗费时间。