多路IO技术:select,同时监听多个文件描述符,将监控的操作交给内核去处理。
数据类型fd_set:文件描述符集合。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
函数介绍:委托内核监控该文件描述符对应的写,读或者错误事件的发生。
参数说明:
nfds:最大的文件描述符+1;
readfds:读集合,是一个传入传出参数
传入:指的是告诉内核哪些文件描述符需要监控
传出:指的是内核告诉应用程序哪些文件描述符发生了变化(是否有可读事件发生)
writefds:写集合,是一个传入传出参数
传入:指的是告诉内核哪些文件描述符需要监控
传出:指的是内核告诉应用程序哪些文件描述符发生了变化
exceptfds:传入传出参数,一般表示异常事件;
Timeout:
超时时间:
NULL:表示永久阻塞,直到有事件发生
0:表示不阻塞,不管有没有事件发生,都回立刻返回
>0:表示阻塞的时长,若超过时长会立刻返回
返回值:
成功返回发生变化的文件描述符个数。
void FD_CLR(int fd, fd_set *set);
说明:将fd从set集合中移除;
int FD_ISSET(int fd, fd_set *set);
说明:判断fd是否在set集合中
void FD_SET(int fd, fd_set *set);
说明:将fd添加到set中
void FD_ZERO(fd_set *set);说明:将set清零
使用select的开发服务端大致流程:
1 创建socket,获取监听文件描述符lfd---socket();
2 设置端口复用---setsockopt();
3 将lfd与IP,PORT绑定---bind();
4 设置监听---listen();
5 fd_set readfds;//定义文件描述符集
FD_ZERO(&readfds);//清零
将lfd加入到readfds集合中---FD_SET(lfd,&readfds);
6 fd_set tmpfds;//定义一个临时文件描述符集
maxfd = lfd;
while (1)
{
tmpfds = readfds;//赋值
nready = select(maxfd,&tmpfds,NULL,NULL,NULL);//传出修改的tmpfds(有哪些可读事件)
if(nready<0)
{
if (errno == EINTR)//被信号中断
{
continue;
}
break;
}
//可读事件有两种:
//1 客户端连接到来--->监听文件描述符lfd发生变化
if (FD_ISSET(lfd, &tmpfds))
{
//接收新的客户端连接请求
cfd = accept(lfd, NULL, NULL);
//将cfd加入到readfds集合中
FD_SET(cfd, &readfds);
if (maxfd < cfd)
{
maxfd = cfd;//修改内核监控的文件描述符的范围
}
if (--nready == 0)
{
continue;//只有一个可读事件且已经执行完,就没必要继续执行下去
}
}
//有客户端发数据过来
for (i = lfd + 1; i <= maxfd; i++)
{
if (FD_ISSET(i, &tmpfds))//看那个通信描述符发生变化
{
//读数据
int n = read(i, buf, sizeof(buf));//read函数不会阻塞,因为肯定有数据发过来
if (n <= 0)
{
close(i);
//将文件描述符i从内核中去除
FD_CLR(i, &readfds);
}
write(i, buf, strlen(buf));
}
}
close(lfd);
return 0;
}
完整代码:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <errno.h>
#include<ctype.h>
int main()
{//int socket(int domain, int type, int protocol);int lfd=socket(AF_INET,SOCK_STREAM ,0);if(lfd<0){perror("socket error");return -1;}// int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);int opt=1;setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));//设置端口复用//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);struct sockaddr_in sev;sev. sin_family=AF_INET;sev. sin_port=htons(8888);inet_pton(AF_INET,"192.168.230.130",&sev.sin_addr.s_addr);int ret=bind(lfd,(struct sockaddr*)&sev,sizeof(sev));if(ret<0){perror("bind error");return -1;}ret=listen(lfd,128);if(ret<0){perror("listen error");return -1;}//int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);int maxfd=lfd;//定义最大fdfd_set readfds;//定义一个读文件描述符集FD_ZERO(&readfds);//清零FD_SET(lfd,&readfds);//加入readfds让内核监听lfdfd_set tmpfds;//定义一个传出读文件描述符集FD_ZERO(&tmpfds);int cfd;//通信描述符int i;int j;int n;char buf[64];int nready;//定义可读事件的数量while(1){tmpfds=readfds;
//让tmpfds成为传入传出参数
//输入:告诉内核要监听哪些文件描述符
//输出:内核告诉你有哪些文件描述符发生变化nready=select(maxfd+1,&tmpfds,NULL,NULL,NULL);if(nready<0){if(errno==EINTR)//被信号打断的错误不视为错误{continue;}else {break;}}if(FD_ISSET(lfd,&tmpfds))//判断lfd是否发生变化,若变化说明有客户端连接{cfd=accept(lfd,NULL,NULL);//接受连接,获取一个通信文件描述符if(maxfd<cfd){maxfd=cfd;//修改最大的maxfd}FD_SET(cfd,&readfds);//将cfd加入readfds,让内核监控是否变化if(--nready==0)//为真说明只有一个可读事件,继续循环{continue;}}for(i=lfd+1;i<=maxfd;i++){if(FD_ISSET(i,&tmpfds))//判断cfd是否发生变化,若变化则说明客户端发信息{memset(buf,0x00,sizeof(buf));n=read(i,buf,sizeof(buf));if(n<=0){printf("read error or client closer,n==[%d]\n",n);close(i);//关闭文件描述符FD_CLR(i,&readfds);//从内核中移除,取消监控continue;//继续循环}printf("n==[%d],buf==[%s]\n",n,buf);for(j=0;j<n;j++){buf[j]=toupper(buf[j]);}write(i,buf,n);if(--nready==0)//减少循环次数{break;}}}}close(lfd);return 0;
}
思考:如果有效的通信文件描述符过少,会让循环的次数变多。
优化方法:
int fd[100];
for()
{
fd[i]=-1;//先把内容都初始化为-1,证明没被占用
}
select优点:
1 一个进程可以支持多个客户端
2 select支持跨平台
select缺点:
1 代码编写困难
2 会涉及用户区到内核区来回拷贝
3 当客户端多个连接,但少数活跃,select效率低
4 最多支持1024个客户端连接