IO多路复用的引入
上文提到,当主机没有操作系统时,或者说程序不能使用多进程或多线程完成任务的并发操作时,
我们可以引入IO多路复用的技术,完成多任务并发执行的操作。
一、select函数
功能:阻塞等待文件描述符集合中是否有事件产生,如果有事件产生,则解除阻塞。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数1:文件描述符集合中,最大的文件描述符加1
参数2、参数3、参数4:分别表示读集合、写集合、异常处理集合的起始地址
由于对于写操作而言,我们也可以转换读操作,所以,只需要使用一个集合就行
对于不使用的集合而言,直接填NULL即可
参数5:超时时间,如果填NULL表示永久等待,如果想要设置时间,需要定义一个如下结构体类型
的变量,并将地址传递进去
struct timeval {
long tv_sec; /* 秒数 */
long tv_usec; /* 微秒 */
};
struct timespec {
long tv_sec; /* 秒数 */
long tv_nsec; /* 纳秒 */
};
返回值:
>0:成功返回解除本次阻塞的文件描述符的个数
=0:表示设置的超时时间,时间已经到达,但是没有事件事件产生
=-1:表示失败,置位错误码
注意:当该函数解除阻塞时,文件描述符集合中,就只剩下本次触发事件的文件描述符,其余的文
件描述符就被删除了
专门针对于文件描述符集合fd_set提供的函数
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实现TCP并发服务器案例
#include<myhead.h>
#define SER_IP "192.168.137.140"
#define SER_PORT 8888
int main(int argc, const char *argv[]){
//创建套接字描述符
int sfd=socket(AF_INET,SOCK_STREAM,0);
if(sfd==-1){
perror("socket error");
return -1;
}
printf("socket success, sfd = %d\n",sfd);
//填充服务端的地址信息结构体
sockaddr_in sin;
sin.sin_addr.s_addr=inet_addr(SER_IP);
sin.sin_family=AF_INET;
sin.sin_port=htons(SER_PORT);
socklen_t socklen=sizeof(sin);
//绑定IP地址和端口号
if(bind(sfd,(sockaddr*)&sin,socklen)==-1){
perror("bind error");
return -1;
}
printf("bind success\n");
//建立监听
if(listen(sfd,128)==-1){
perror("listen error");
return -1;
}
printf("listen success\n");
//定义文件描述符集合
fd_set tempfds,readfds;
FD_ZERO(&readfds); //清空容器
FD_SET(0,&readfds); //将文件描述符(对应stdin)放入容器
FD_SET(sfd,&readfds); //将文件描述符sfd放入容器
int maxfd=sfd; //maxfd表示tempfds集合中文件描述符的最大值
int newfd=-1; //接收客户端的连接请求后,建立的通信套接字文件描述符
sockaddr_in cin_arr[1024]; // 数组:[文件描述符:地址信息结构体]
while(1){
tempfds=readfds; // 将reafds备份一份放入tempfds中
// 调用阻塞函数,完成对文件描述符集合的管理工作
int res=select(maxfd+1,&tempfds,NULL,NULL,NULL);
if(res==-1){
perror("select error");
return -1;
}else if(res==0){
printf("time out\n");
return -1;
}
//如果程序执行到这里,则说明有至少一个文件描述符产生了事件,其余没有触发事件的文件描述符被删除
//此时我们只需要判断哪个文件描述符还在集合中,就说明该文件描述符触发了事件
if(FD_ISSET(sfd,&tempfds)){ //如果是sfd触发了事件,说明有客户端请求建立连接
sockaddr_in cin;
socklen_t len=sizeof(cin);
//接收客户端的连接请求
newfd=accept(sfd,(sockaddr*)&cin,&len);
if(newfd==-1){
perror("accept error");
return -1;
}
printf("[%s,%d]:connected\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
cin_arr[newfd]=cin; //将该客户端对应的套接字地址信息结构体放入数组对应的位置上
FD_SET(newfd,&readfds); //将新的套接字描述符加入文件描述符集合
maxfd = newfd>maxfd?newfd:maxfd;
}
if(FD_ISSET(0,&tempfds)){ //如果是stdin触发了事件,说明服务端要向客户端发送信息
char sbuf[128]="";
fgets(sbuf,sizeof(sbuf),stdin);
for(int cli=4;cli<=maxfd;cli++){
send(cli,sbuf,strlen(sbuf),0); //向客户端发送信息
}
}
for(int cfd=4;cfd<=maxfd;cfd++){
if(FD_ISSET(cfd,&tempfds)){ //如果客户端cfd有事件触发
char buf[128]="";
bzero(buf,sizeof(buf));
//接收数据
int res=recv(cfd,buf,sizeof(buf),0);
if(res==0){ //连接断开
printf("[%s,%d]:leave\n",\
inet_ntoa(cin_arr[cfd].sin_addr),ntohs(cin_arr[cfd].sin_port));
//关闭与该客户端的连接请求
close(cfd);
//将该套接字从文件描述符中去除
FD_CLR(cfd, &readfds);
//重新更新maxfd的值
for(int k=maxfd;k>=0;k--){
//倒序遍历文件描述符,如果该文件描述符还在集合中,则说明该文件描述符的最大文件描述符
if(FD_ISSET(k,&readfds)){
maxfd=k;
break;
}
}
continue;
}
printf("[%s:%d]:%s\n", inet_ntoa(cin_arr[cfd].sin_addr),\
ntohs(cin_arr[cfd].sin_port), buf);
strcat(buf,"*_*");
//数据发送到客户端
if(send(cfd,buf,strlen(buf),0)==-1){
perror("send error");
return -1;
}
}
}
}
//关闭套接字文件描述符
close(sfd);
return 0;
}
二、poll函数
功能:阻塞等待文件描述符集合中是否有事件产生,如果有,则解除阻塞,返回本次触发事件的文件描述符个数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数1:文件描述符集合容器的起始地址,是一个结构体数组,结构体类型如下
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 要等待的事件:由用户填写 */
short revents; /* 实际发生的事件 :调用函数结束后,内核会自动设置*/
};
关于事件对应的位:
POLLIN:读事件
POLLOUT:写事件
参数2:集合中文件描述符的个数
参数3:超时时间,负数表示永久等待,0表示非阻塞
返回值:
>0:表示触发本次解除阻塞事件的文件描述符的个数
=0:表示超时
=-1:出错,置位错误码
poll实现TCP客户端案例
#include<myhead.h>
#define SER_IP "192.168.137.142"
#define SER_PORT 8888
#define CLI_IP "192.168.137.142"
#define CLI_PORT 9999
int main(int argc, const char *argv[]){
//创建套接字描述符
int sfd=socket(AF_INET,SOCK_STREAM,0);
if(sfd==-1){
perror("socket error");
return -1;
}
//创建客户端地址信息结构体
sockaddr_in cin;
cin.sin_addr.s_addr=inet_addr(CLI_IP);
cin.sin_family=AF_INET;
cin.sin_port=htons(CLI_PORT);
//绑定地址信息结构体
if(bind(sfd,(sockaddr*)&cin,sizeof(cin))==-1){
perror("bind error");
return -1;
}
//创建服务器端地址信息结构体
sockaddr_in sin;
sin.sin_addr.s_addr=inet_addr(SER_IP);
sin.sin_family=AF_INET;
sin.sin_port=htons(SER_PORT);
//请求连接
if(connect(sfd,(sockaddr*)&sin,sizeof(sin))==-1){
perror("connect error");
return -1;
}
pollfd pfds[2]; //使用poll完成终端输入和套接字接收数据的并发执行
pfds[0].fd=0; //表示检测0号
pfds[0].events=POLLIN; //表示检测的是读事件
pfds[1].fd=sfd;
pfds[1].events=POLLIN;
char buf[128]="";
while(1){
int res=poll(pfds,2,-1);
// 功能:阻塞等待文件描述符集合中是否有事件产生
// 参数1:文件描述符集合起始地址
// 参数2:文件描述符个数
// 参数3:表示永久等待
if(res==-1){
perror("poll error");
return -1;
}
//程序执行至此,说明pfds容器中有事件产生
if(pfds[0].revents==POLLIN){ //0号文件描述符触发了事件
//清空buf数组
bzero(buf,sizeof(buf));
//从终端读取数据
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1]=0;
//发送数据
if(send(sfd,buf,strlen(buf),0)==-1){
perror("send error");
return -1;
}
}
if(pfds[1].revents==POLLIN){ //sfd文件描述符触发事件
buf[strlen(buf)-1]=0;
if(strcmp(buf,"quit")==0) break;
//发送数据
recv(sfd,buf,sizeof(buf),0);
printf("[%s,%d]:%s\n",inet_ntoa(sin.sin_addr),ntohs(sin.sin_port),buf);
}
}
//关闭文件描述符
close(sfd);
return 0;
}
三、epoll函数
epoll全称为eventpoll,是内核实现IO多路复用的一种实现方式。在其中一个或多个事件的到满足
时,可以解除阻塞。
epoll是select和poll的升级版,在嵌入式等领域用的更多,相比于select和poll而言,epoll改进了工作方式(底层是红黑树),效率更高。
头文件:#include <sys/epoll.h>
(1)epoll_create函数
功能:创建一个epoll实例,并返回该实例的句柄,是一个文件描述符
int epoll_create(int size);
参数size:epoll实例中能够容纳的最大节点个数,自从linux 2.6.8版本后,size可以忽略,但是
必须要是一个大于0的数字
返回值:成功返回控制epoll实例的文件描述符,失败返回-1并置位错误码
(2)epoll_ctl函数
功能:完成对epoll实例的各种控制
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数1:通过epoll_create创建的epoll实例文件描述符
参数2:op表示要进行的操作3> epoll实现TCP并发服务器的模型
EPOLL_CTL_ADD:向epoll树上添加新的要检测的文件描述符
EPOLL_CTL_MOD:改变epoll树上的文件描述符检测的事件
EPOLL_CTL_DEL:删除epoll树上的要检测的文件描述符,此时参数3可以省略填NULL
参数3:要检测的文件描述符
参数4:要检测的事件,是一个结构体变量地址,属于输入变量
typedef union epoll_data {
void *ptr; //提供的解释性数据
int fd; //文件描述符(常用)
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* 要检测的事件 */
epoll_data_t data; /* 用户有效数据,是一个共用体 */
};
要检测的事件:
EPOLLIN:读事件
EPOLLOUT:写事件
EPOLLERR:异常事件
EPOLLET:表示设置epoll的模式为边沿触发模式(默认方式是水平模式)
如何设置边沿触发:在将文件描述符放入到epoll树中时,需要加一个属性
struct epoll_event ev;
ev.event = EPOLLIN|EPOLLEV;
(3)epoll_wait函数
功能:阻塞检测epoll实例中是否有文件描述符准备就绪,如果准备就绪了,就解除阻塞
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数1:epoll实例对于的文件描述符
参数2:文件描述符集合,当有文件描述符产生事件时,将所有产生事件的文件描述符,放入到该集合中
参数3:参数2的大小
参数4:超时时间,以毫秒为单位的超时时间,如果填-1表示永久阻塞
返回值:>0:表示解除本次操作的触发的文件描述符个数
=0:表示超时,但是没有文件描述符产生事件
=-1:失败,置位错误码
epoll实现TCP并发服务器案例
#include<myhead.h>
#include<map>
#define SER_IP "192.168.137.142"
#define SER_PORT 8888
int main(int argc, const char *argv[]){
map<int,sockaddr_in> mp;
//创建套接字描述符
int sfd=socket(AF_INET,SOCK_STREAM,0);
if(sfd==-1){
perror("socket error");
return -1;
}
printf("socket success, sfd = %d\n",sfd);
sockaddr_in sin;
sin.sin_addr.s_addr=inet_addr(SER_IP);
sin.sin_family=AF_INET;
sin.sin_port=htons(SER_PORT);
if(bind(sfd,(sockaddr*)&sin,sizeof(sin))==-1){
perror("bind error");
return -1;
}
printf("bind success\n");
if(listen(sfd,128)==-1){
perror("listen error");
return -1;
}
printf("listen success\n");
sockaddr_in cin;
socklen_t socklen=sizeof(cin);
//创建epoll实例,用于检测文件描述符
int epfd=epoll_create(1);
if(epfd==-1){
perror("epoll create error");
return -1;
}
//将sfd放入到检测集合中
epoll_event ev;
ev.data.fd=sfd; //要检测的文件描述符
ev.events=EPOLLIN; //要检测读事件
/*
功能:将sfd放入到检测集合中
参数1:epoll实例的文件描述符
参数2:epoll操作,表示要添加文件描述符
参数3:要检测文件描述符的值
参数4:要检测的事件
*/
epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&ev);
//该数组用于接收返回事件集合
epoll_event evs[1024];
//数组大小
int size=sizeof(evs)/sizeof(evs[0]);
while(1){
//阻塞检测文件描述符集合中是否有事件产生
int num=epoll_wait(epfd,evs,size,-1);
//参数1:epoll实例的文件描述符
//参数2:返回触发事件的集合
//参数3:集合的大小
//参数4:是否阻塞,-1表示阻塞
printf("num = %d\n",num);
for(int i=0;i<num;i++){
int fd=evs[i].data.fd;
if(fd==sfd){
int newfd=accept(sfd,(sockaddr*)&cin,&socklen);
if(newfd==-1){
perror("accept error");
return -1;
}
mp[newfd]=cin;
printf("[%s,%d]:connect\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
epoll_event ev;
ev.data.fd=newfd;
ev.events=EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&ev);
}else{
char buf[128]="";
bzero(buf,sizeof(buf));
int res=recv(fd,buf,sizeof(buf),0);
if(res==0){
printf("[%s,%d]:leave\n",
inet_ntoa(mp[fd].sin_addr),ntohs(mp[fd].sin_port));
//将客户端从epoll树中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
printf("[%s,%d]:%s\n",
inet_ntoa(mp[fd].sin_addr),ntohs(mp[fd].sin_port),buf);
strcat(buf,"*_*\n");
if(send(fd,buf,strlen(buf),0)==-1){
perror("send error");
return -1;
}
}
}
}
close(sfd); //关闭监听
close(epfd); //关闭epoll实例
return 0;
}
1749

被折叠的 条评论
为什么被折叠?



