IO模型:
阻塞等待模型(BIO):
1)优点:不占用CPU时间片,阻塞时,cpu时间片交给别人,缺点:但同时只能处理同一个操作。
2)可以使用多线程/多进程实现并发处理多个客户端请求,每个线程对应一个客户端,线程里进行读写操作,主线程则可以继续运行监听客户端。 (但每个线程里 其实也会存在阻塞问题)
while(1)
{
accept(lfd,...); (blocking)
create thread->read/write; (blocking)
}
但是缺点:a.线程/进程会消耗资源,b.调度线程/进程会消耗CPU资源。
非阻塞IO模型:(忙轮询 NIO)
1)accept read write 非阻塞,没读到继续往下执行,不断轮询。优点:提高了程序执行效率。缺点:需要占用更多的CPU和系统资源。
while(1)
{
accept(lfd,...);
读写现有的已accept的fd, 遍历轮询read/write; ->浪费系统资源。
}
2)使用IO多路复用技术来改进。
IO多路复用:
使程序能够同时监听多个文件描述符(监听内核缓冲区),提高程序性能。(可以判断同时有多少客户端来和我通信)。(告诉内核要监听的fd集合,而非自己对所有fd轮询read write).
select/poll
1.select
但是select poll内核不会告诉使用者具体是哪些快递到了,只会告诉总数。底层使用二进制位表示某个fd有无数据,相比read write判断有无数据效率更高。
1. 将要监听的fd添加到fd列表。
2. 调用select(),内核会监听列表中的fd,当有某个/多个fd进行IO操作,函数返回。 (阻塞函数)
3. 返回时,会告诉进程有多少fd要进行IO操作。
select()将我们想监听的读写异常fd交给内核去检测。 (fd集合最大1024个)
对fd集合二进制位的操作
工作过程
readfds等是指针,指明要检测的fd的集合,如0010.内核检测后会将其改变为检测到的结果。如第二个有数据可以读,第3个没有,则修改为0100;
缺点:
1)每次调用select,要将fds从用户态拷贝到内核态。fds很大时开销会很大。
2)每次调用select,内核要对fds进行遍历。fds很大时开销会很大。再拷贝回用户态,也开销很大。 用户态又要遍历判断是哪些fd发生了改变。 O(n)
3)select支持的fd最大数量1024,(fd_set 数组大小128字节,1024位)太小(最多支持1024客户端)。
4)fd集合不能重用(被内核修改了),为了下次检测每次都需要重置。
代码实现:
client.cpp
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
using namespace std;
int main()
{
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd==-1)
{
perror("socket:");
exit(-1);
}
sockaddr_in serveraddr;
inet_pton(AF_INET,"127.0.0.1",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port=htons(9090);
serveraddr.sin_family=AF_INET;
int ret=connect(fd,(sockaddr*)&serveraddr,sizeof(serveraddr));
if(ret==-1)
{
perror("connect: ");
exit(-1);
}
int n=0;
while(1)
{
if(n<9)
n++;
else
n=0;
char sendBuf[50]="hello,this is ";
sendBuf[14]=(char)(n+'0');
write(fd,sendBuf,strlen(sendBuf));
char readBuf[80]="client received ";
int len=read(fd,sendBuf,sizeof(sendBuf));
if(len==0)
{
cout<<"server closed\n";
break;
}
else if(len==-1)
{
perror("read: ");
exit(-1);
}
strcat(readBuf,sendBuf);
cout<<readBuf<<endl;
sleep(2);
}
close(fd);
}
select.cpp
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/select.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
int main()
{
int lfd=socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port=htons(9090);
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr=INADDR_ANY;
int ret=bind(lfd,(sockaddr*)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("bind");
exit(-1);
}
ret=listen(lfd,8);
if(ret==-1)
{
perror("listen");
exit(-1);
}
fd_set rdset,tmp_set;//要检测的fd集合
FD_ZERO(&rdset);//初始化
FD_SET(lfd,&rdset);//添加要检测的fd
int maxfd=lfd;
while(1)
{
tmp_set=rdset; //使内核修改fd不影响我们要检测的fd
//让内核检测fd集合 永久阻塞,直到有变化
int ret=select(maxfd+1,&tmp_set,nullptr,nullptr,nullptr);
if(ret==-1)
{
perror("select");
exit(-1);
}
//检测到有fd对应的缓冲区的数据发生了改变。
//判断监听的lfd 是否有客户端要连接
if(FD_ISSET(lfd,&tmp_set))
{
sockaddr_in clientaddr;
int len=sizeof(clientaddr);
int cfd=accept(lfd,(sockaddr*)&clientaddr,(socklen_t*)&len);
//将通信fd加入rdset。
FD_SET(cfd,&rdset);
maxfd = maxfd>cfd ? maxfd : cfd;
}
//遍历其他的cfd,判断是否有数据读 从监听的lfd+1开始遍历。
for(int i=lfd+1;i<maxfd+1;i++)
{
if(FD_ISSET(i,&tmp_set))
{
char buf[1024]={0};
int len=read(i,buf,sizeof(buf));
if(len==-1)
{
perror("read");
exit(-1);
}
else if(len==0)
{
printf("%d client closed\n",i); //客户端断开,将其从fd集合删去
close(i);
FD_CLR(i,&rdset);
}
else
{
std::cout<<"read buf= "<<buf<<" from "<<i<<std::endl;
write(i,buf,strlen(buf)+1);
}
}
}
}
close(lfd);
}
2.poll
通过events和revents,内核修改时会修改revents,使得events可以重用。并且没有1024的限制。
缺点:
1)基本和select一致,但是没有1024最大连接数的限制。(基于链表存储)
2)events集合可重用。
3)水平触发。如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
代码实现
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/poll.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
int main()
{
int lfd=socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port=htons(9090);
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr=INADDR_ANY;
int ret=bind(lfd,(sockaddr*)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("bind");
exit(-1);
}
ret=listen(lfd,8);
if(ret==-1)
{
perror("listen");
exit(-1);
}
pollfd fdset[2048]; //要监听的fd集合结构体
for(int i=0;i<2048;i++)
{
fdset[i].fd=-1;
fdset[i].events=POLLIN;//检测读事件
}
fdset[0].fd=lfd; //将监听lfd加入集合。
int maxfd=0;
while(1)
{
//让内核检测fd集合 阻塞,直到有变化
int ret=poll(fdset,maxfd+1,-1);
if(ret==-1)
{
perror("poll");
exit(-1);
}
//检测到有fd对应的缓冲区的数据发生了改变。
//判断监听的lfd 是否有客户端要连接
if(fdset[0].revents & POLLIN)
{
sockaddr_in clientaddr;
int len=sizeof(clientaddr);
int cfd=accept(lfd,(sockaddr*)&clientaddr,(socklen_t*)&len);
//将通信fd加入rdset。 遍历是为了重用在连接过程中已使用又断开的fd位置。
for(int i=1;i<2048;i++)
{
if(fdset[i].fd==-1)
{
fdset[i].fd=cfd;
fdset[i].events=POLLIN;
maxfd = maxfd>i ? maxfd : i; //更新最大的fd索引
break;
}
}
}
//遍历其他的cfd,判断是否有数据读 从监听的lfd+1开始遍历。
for(int i=1;i<maxfd+1;i++)
{
if(fdset[i].revents&POLLIN)
{
char buf[1024]={0};
int len=read(fdset[i].fd,buf,sizeof(buf));
if(len==-1)
{
perror("read");
exit(-1);
}
else if(len==0)
{
printf("%d client closed\n",fdset[i].fd); //客户端断开,将其从fd集合删去
close(fdset[i].fd);
fdset[i].fd=-1;
}
else
{
std::cout<<"read buf= "<<buf<<" from "<<fdset[i].fd<<std::endl;
write(fdset[i].fd,buf,strlen(buf)+1);
}
}
}
}
close(lfd);
}
epoll
用epoll_create直接在内核创建一个epoll实例eventpoll,通过epoll_create返回的fd控制这个实例。减少select poll将fd集合从用户态拷贝到内核态的切换。
rbr 记录需要检测的fd, 底层红黑树,遍历效率非常快。
rdlist 就绪fd的链表。可以告诉用户具体改变的fd。
epoll_ctl 管理epoll实例
epoll_data中的fd可以在遍历变化的fd作判断时用到
代码实现
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
int main()
{
int lfd=socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port=htons(9090);
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr=INADDR_ANY;
int ret=bind(lfd,(sockaddr*)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("bind");
exit(-1);
}
ret=listen(lfd,8);
if(ret==-1)
{
perror("listen");
exit(-1);
}
//创建eventpoll实例 参数>0就可以
int epfd=epoll_create(100);
//将监听Lfd相关信息加入epfd实例
epoll_event epev;
epev.events=EPOLLIN;
epev.data.fd=lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);
//被监听后,发生改变的fd集合
epoll_event epevs[1024];
while(1)
{
//让内核检测epev实例 阻塞,直到有变化
int ret=epoll_wait(epfd,epevs,1024,-1);
if(ret==-1)
{
perror("epoll_wait");
exit(-1);
}
//检测到有ret个fd对应的缓冲区的数据发生了改变。
printf("ret = %d\n",ret);
for(int i=0;i<ret;i++)
{
//判断监听的lfd 是否有客户端要连接 这里就用到了epoll_data中的fd
if(epevs[i].data.fd==lfd)
{
sockaddr_in clientaddr;
int len=sizeof(clientaddr);
int cfd=accept(lfd,(sockaddr*)&clientaddr,(socklen_t*)&len);
epev.events=EPOLLIN;
epev.data.fd=cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
}
else //cfd 有数据到达,需要通信
{
// if(epev[i].data.fd & EPOLLOUT)
// {
// continue;
//}
//有数据可读
char buf[1024]={0};
int len=read(epevs[i].data.fd,buf,sizeof(buf));
if(len==-1)
{
perror("read");
exit(-1);
}
else if(len==0)
{
printf("%d client closed\n",epevs[i].data.fd); //客户端断开,将其从fd集合删去
close(epevs[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,epevs[i].data.fd,nullptr);
}
else
{
std::cout<<"read buf= "<<buf<<" from "<<epevs[i].data.fd<<std::endl;
write(epevs[i].data.fd,buf,strlen(buf)+1);
}
}
}
}
close(lfd);
close(epfd); //关闭epoll实例
}
epoll的工作模式:
1) LT (默认)
水平触发,支持阻塞和非阻塞socket。内核通知用户某fd就绪后,用户未操作或只操作了一部分数据,下次epoll_wait仍会通知用户该fd就绪,直到用户操作完缓冲区数据,fd变为未就绪。
2)ET
边沿触发,只支持非阻塞socket。内核通知用户某fd就绪后,用户未操作或只操作了一部分数据,下次epoll_wait不会通知用户该fd就绪,直到用户操作后使fd变为非就绪,下次fd再次就绪才会通知用户。
所以在用户操作该fd时,需要循环读取完其所有数据,否则数据未读完下次不被通知就会一直处理不到。在读取过程中,为了不断read时不会阻塞,ET只支持非阻塞SOCKET。
优点:减少了epoll事件被重复触发的次数,效率高于LT。
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
using namespace std;
int main()
{
int fd=socket(AF_INET,SOCK_STREAM,0);
if(fd==-1)
{
perror("socket:");
exit(-1);
}
sockaddr_in serveraddr;
inet_pton(AF_INET,"127.0.0.1",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port=htons(9090);
serveraddr.sin_family=AF_INET;
int ret=connect(fd,(sockaddr*)&serveraddr,sizeof(serveraddr));
if(ret==-1)
{
perror("connect: ");
exit(-1);
}
int n=0;
while(1)
{
// if(n<9)
// n++;
// else
// n=0;
// char sendBuf[50]="hello,this is ";
// sendBuf[14]=(char)(n+'0');
char sendBuf[1024]={0};
fgets(sendBuf,sizeof(sendBuf),stdin);
write(fd,sendBuf,strlen(sendBuf)+1);
char readBuf[80]="client received ";
int len=read(fd,sendBuf,sizeof(sendBuf));
if(len==0)
{
cout<<"server closed\n";
break;
}
else if(len==-1)
{
perror("read: ");
exit(-1);
}
strcat(readBuf,sendBuf);
cout<<readBuf<<endl;
}
close(fd);
}
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <fcntl.h>
#include <errno.h>
int main()
{
int lfd=socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port=htons(9090);
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr=INADDR_ANY;
int ret=bind(lfd,(sockaddr*)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("bind");
exit(-1);
}
ret=listen(lfd,8);
if(ret==-1)
{
perror("listen");
exit(-1);
}
//创建eventpoll实例 参数>0就可以
int epfd=epoll_create(100);
//将监听Lfd相关信息加入epfd实例
epoll_event epev;
epev.events=EPOLLIN;
epev.data.fd=lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);
//被监听后,发生改变的fd集合
epoll_event epevs[1024];
while(1)
{
//让内核检测epev实例 阻塞,直到有变化
int ret=epoll_wait(epfd,epevs,1024,-1);
if(ret==-1)
{
perror("epoll_wait");
exit(-1);
}
//检测到有ret个fd对应的缓冲区的数据发生了改变。
printf("ret = %d\n",ret);
for(int i=0;i<ret;i++)
{
//判断监听的lfd 是否有客户端要连接 这里就用到了epoll_data中的fd
if(epevs[i].data.fd==lfd)
{
sockaddr_in clientaddr;
int len=sizeof(clientaddr);
int cfd=accept(lfd,(sockaddr*)&clientaddr,(socklen_t*)&len);
printf("new client\n");
//设置cfd非阻塞
int flag=fcntl(cfd,F_GETFL);
flag|=O_NONBLOCK;
fcntl(cfd,F_SETFL,flag);
//设置ET模式
epev.events=EPOLLIN|EPOLLET;
epev.data.fd=cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
}
else //cfd 有数据到达,需要通信
{
char buf[5]={0};
int len=0;
//循环读取所有数据
while((len=read(epevs[i].data.fd,buf,sizeof(buf)))>0)
{
std::cout<<"read buf= "<<buf<<" from "<<epevs[i].data.fd<<std::endl;
write(epevs[i].data.fd,buf,strlen(buf)+1);
}
if(len==-1)
{
if(errno==EAGAIN) //数据读完了
printf("data over\n");
else
{
perror("read");
exit(-1);
}
}
else if(len==0)
{
printf("%d client closed\n",epevs[i].data.fd); //客户端断开,将其从fd集合删去
close(epevs[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,epevs[i].data.fd,nullptr);
}
}
}
}
close(lfd);
close(epfd); //关闭epoll实例
}