如何实现网络的IO复用
一请求一线程服务器的优劣
对于一请求一线程,其优势主要在于代码逻辑简单,但由于该方案对在连接与线程绑定,面对大并发的需求,会造成极大的线程切换开销和内存占用。故而我们需要设计更加适配并发的服务器方案
SELECT方案
他的设计思想是把所有关注的IO都放进一个fd_set类型的变量里,然后通过select函数将fd_set从用户态copy到内核态,根据就绪事件进行修改然后再传回,根据传回的结果,先判断是否要新建连接,再判断是否要读、写、异常处理。
select由一个结构体fd_set,一个函数select,四个集合操作宏FD_ZERO、FD_SET、FD_CLR、FD_ISSET组成
- fd_set:用位图存储fd
- FD_ZERO(&set):清空集合
- FD_SET(fd, &set):添加集合
- FD_CLR(fd, &set):删除集合
- FD_ISSET(fd, &set):判断fd是否在集合中
- select(int maxfd+1,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout):将位图从用户态送进内核态修改的系统调用
fd_set rfds,rset;
FD_ZERO(&rfds);
FD_SET(sockfd,&rfds);
int maxfd = sockfd;
INFO("maxfd:%d\n",maxfd);
while(1){
rset = rfds;
//获取就绪集合
int nready = select(maxfd+1, &rset,NULL,NULL,NULL);//集合上界,关注(读,写,异常),超时
INFO("nready:%d\n",nready);
//当监听套接字在就绪集合内,创建连接
if(FD_ISSET(sockfd,&rset)){
int clientfd = accept(sockfd,(struct sockaddr*)&client_addr,&len);
FD_SET(clientfd,&rfds);
if(clientfd > maxfd)maxfd = clientfd;
}
//遍历所有连接,按照就绪集合处理数据
int i=0;
for(i=sockfd+1 ;i<=maxfd ; i++){
if(FD_ISSET(i,&rset)){
char buffer[1024]={0};
int count = recv(i,buffer,1024,0);
//连接结束,关闭连接
if (count == 0){
INFO("client disconnect\n");
close(i);
FD_CLR(i,&rfds);
continue;
}
INFO("recv:%s,%d byte(s)\n",buffer,count);
count = send(i,buffer,count,0);
INFO("SEND:%d\n",count);
}
}
poll方案
他的设计思想和select类似,也是将完整集合从用户态copy进内核态修改成就绪集合,不过它和select不同点他的结构体struct pollfd区分了events和revents,接受就绪集合,不会把原本的完整集合覆盖掉,这就意味着他不需要像select那样每轮复制一次集合
poll由一个结构体,多种时间标志,以及poll()函数组成,
struct pollfd:监控数组,每项包含fd,events,revents,每项仅包含该fd的关注事件和就绪事件
POLLIN,POLLOUT等:事件标识符
poll(fds, 数组项数, 超时毫秒数):等待就绪事件
实现代码如下
struct pollfd fds[1024] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;
int maxfd = sockfd;
while(1){
int nready = poll(fds,maxfd+1,-1);//fd集合,集合大小,超时
//入池判断,监听也是POLLIN,接受数据也是POLLIN
if(fds[sockfd].revents & POLLIN){
int clientfd = accept(sockfd,(struct sockaddr*)&client_addr,&len);
INFO("accept finished:%d\n",clientfd);
//FD_SET(clientfd,&rfds);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if(clientfd > maxfd)maxfd = clientfd;
}
//遍历,执行
int i=0;
for(i=sockfd+1 ;i<=maxfd ; i++){
if(fds[i].revents & POLLIN){
char buffer[1024]={0};
int count = recv(i,buffer,1024,0);
//断连:关闭连接,初始化fds[i]元素
if (count == 0){//disconnect
INFO("client disconnect\n");
close(i);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
INFO("recv:%s,%d byte(s)\n",buffer,count);
count = send(i,buffer,count,0);
INFO("SEND:%d\n",count);
}
}
}
epoll方案
作为Linux系统从2.5版本一直沿用至今的方案,linux能成为目前云服务器的主流选择,它功不可没
它的设计思想同前两者不同的地方在于它不需要把整个关注集合都copy到内核态中修改,取而代之的是直接把本轮就绪结果从内核传回到用户数组。这个epoll就像快递驿站一样存放已就绪的事件,将需要监听的套接字、新建的连接通过epollctl函数加入epoll,在epoll_wait()检测到就绪事件传回就执行,监听或者是收发数据
其组成主要有一个结构体和三个函数
- struct epoll_event :events(关注事件),data(保存所有自定义信息,例如文件描述符,连接对象地址)
- epoll_create1():创建
- epoll_ctl():添加、修改、删除所需要的fd
- epoll_wait():等待就绪事件,返回就绪数量并填充结果数组
实现代码如下
//创建epoll
int epfd = epoll_create(1);
//创建struct epoll_event结构体存储关注套接字和关注事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
//连接入epoll
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);
while(1){
//存储就绪事件
struct epoll_event events[1024] = {0};
epoll_wait(epfd,events,1024,-1);
int i=0;
for(i=0;i<1024;i++){
int connfd = events[i].data.fd;
//就绪事件中存在监听套接字,创建新连接入epoll
if(connfd == sockfd){
int clientfd = accept(sockfd,(struct sockaddr*)&client_addr,&len);
INFO("accept finished:%d\n",clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
//处理就绪事件
}else if (events[i].events & EPOLLIN){
char buffer[1024]={0};
int count = recv(connfd,buffer,1024,0);
if (count == 0){//disconnect
INFO("client disconnect\n");
close(connfd);
epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
continue;
}
INFO("recv:%s,%d byte(s)\n",buffer,count);
count = send(connfd,buffer,count,0);
INFO("SEND:%d\n",count);
}
}
}