导航

网络编程相关(IO多路复用)

Posted on 2023-06-16 21:09  koodu  阅读(13)  评论(0编辑  收藏  举报

select poll epoll的一些比较

select的fd_set通过bitmap1024位表示存入的文件描述符,通过01表示存入的文佳描述符,且是从0下标开始,如存入的文件描述符是12579,则在bitmap里表示是0110010101000...

由于bitmap从0下标开始,存入的文件描述符从1开始,从0到存入的最大文件描述符,范围是max+1

select主要是把bitmap从用户态拷贝到内核态,由内核态来判断,没有数据变化,select会阻塞,有变化,bitmap的fd会被置位

fd_set不可重用


poll

struct pollfd
{
int fd;
short events;//事件
short revents;//有变化被置位,什么事件,置为什么事件,最后要将revents重新置为0
}

epoll句柄,用户态核内核态共享

select

关于select,主要是把服务端的根sockfd也加入到fd_set,FD_SET(listenSocket,&socketSet); readSet=socketSet;
writeSet=socketSet; 把加入的fd描述符更新到读写set中

FD_ZERO(&socketSet);
//将文件描述符放入 socketSet,
//用于accept  
FD_SET(listenSocket,&socketSet); 
//统计最大的socket 
int maxfd = listenSocket;
int conNum = 1;
//数组存储连接的socket
int connectArray[1024]={0};
while(true)  
{  
    //清空读写集合
    FD_ZERO(&readSet);  
    FD_ZERO(&writeSet);
    //读写都监听
    readSet=socketSet;  
    writeSet=socketSet;

有新连接时:FD_SET(connectArray[conNum],&socketSet);放到socketSet后面while又更新到read_set

if(FD_ISSET(listenSocket,&readSet))
    {  
        acceptSocket=accept(listenSocket,(sockaddr*)&addr,&len);  
        if(acceptSocket==INVALID_SOCKET)  
        {  
            return false;  
        }  
        else  
        {  
            //大于我们最大的监听数量了
            if(conNum > 1024)
            {
                return false;
            }
            //更新数组
            connectArray[conNum] = acceptSocket;
            //设置非阻塞
            fcntl(connectArray[conNum], F_SETFL, O_NONBLOCK );
            //加到socketset里,以后赋值给读写集合
            FD_SET(connectArray[conNum],&socketSet);
            if(acceptSocket > maxfd)
            {
                maxfd = acceptSocket;
            }
            conNum++;

poll

poll与select不同在于:poll将要监听的对象存到数组中,且数组中的元素是结构体(存fd,要监听的事件,返回监听到的事件)

epoll

epoll查找基于红黑树,返回已经变化的文件描述符

一开始就监听服务端的fd(把服务端的fd事件加到epoll的监听树上(水平模式,只要有连接就通知服务端读事件)(监听读事件)),如果有客户端要建立连接,会触发epoll读事件

epoll_wait()接着通过for循环判断读事件结构体事件的fd是否是服务器的fd,若是就要建立连接的客户端数量:通过accept()接收客户端的连接,创建与客户端连接的sockfd,(边缘模式,非阻塞,加入epoll树,)epoll只是通知有事件要处理,具体处理要具体实现。;;若是触发epoll读事件的是要与客户端通信的sockfd,则在epoll_wait()数量的for里判断,进行通信

epoll边缘模式中,有要建立连接的客户端请求,将建立连接的sockfd1的结构体的事件设为读事件同时为边缘模式

connfd=accept(...);
event.data.fd = connfd;
event.events = EPOLLIN |EPOLLET;//读与边缘(服务端与客户端通信的fd的事件)
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);

若是在已建立连接的客户端与服务端进行通信,由于服务端的sockfd是边缘模式,要一次性读完;同时读完后,服务端要与该客户端进行通信的connfd改为写事件(读完客户端的信息后,要进行回应,改为写边缘),服务端有数据发送回客户端触发写事件,一次性写完。

//更改为写模式
event.data.fd = connfd;
event.events = EPOLLOUT | EPOLLET;
//EPOLL_CTL_MOD,根据已有的connfd,修改事件结构体
epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &event);

边缘模式下accept() do{accept(); 读完退出}while(1)要一次读完,直到返回值为<=0

边缘模式,写缓冲区满后,不写,contine 下一个epoll_event,下一次epoll_wait()写缓冲区变为非空后会触发写事件。

int nready = epoll_wait(epollfd, epoll_eventsList, eventsize, -1);
举一个例子假设epoll_event队列中有1000个文件描述符,第一次调用
epoll_wait返回5,那么表示队列前五个元素就绪了,如果不处理第三个就绪事件,其他的都处理。第二次调用epoll_wait会返回8,那么这8个epoll_event也是按顺序排列的。
所以认为epoll_wait这个函数做了内部的优化排序,返回给用户按顺序拍好的内存

epoll_wait最多返回eventsize个epoll_event

考虑把服务端的根sockfd设为水平模式,只要accept()里还有连接就触发,不用在do里全部读完accept()

epoll的一个示例:
#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<vector>
#include<arpa/inet.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<cstring>
#include<unistd.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<memory.h>
#include<errno.h>
#define SIZE 20
using namespace std;

int main(int argc,char* argv[])
{
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	struct sockaddr_in serveraddr;
	memset(&serveraddr,0,sizeof(serveraddr));
	serveraddr.sin_family=AF_INET;
	serveraddr.sin_port=htons(atoi(argv[1]));
	serveraddr.sin_addr.s_addr=INADDR_ANY;

	if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0)
	{
		perror("bind error");
		exit(1);
	}

	if(listen(sockfd,128)<0)
	{
		perror("listen error");
		exit(1);
	}

	int epollfd=epoll_create(1);//创建epoll句柄

	struct epoll_event event;
	event.data.fd=sockfd;//将服务端的fd放到epoll中
	event.events=EPOLLIN;//水平读
	epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);//事件加入到epoll树

	vector<int> clientfds;
	struct epoll_event* ev=(struct epoll_event*)malloc(sizeof(struct epoll_event)*SIZE);//存epoll树返回的事件

	struct sockaddr_in client;
	int connfd;
	socklen_t len=sizeof(client);

	while(1)
	{
		int nearby=epoll_wait(epollfd,ev,SIZE,-1);//等待事件触发
		if(nearby==-1)//出错
		{
			if(errno==EINTR) continue;
		}
		if(nearby==0)//超时
		{
			continue;
		}
		for(int i=0;i<nearby;i++)
		{
			if(ev[i].data.fd==sockfd)//有新连接
			{
				connfd=accept(sockfd,(struct sockaddr*)&client,&len);
				char buff[1024]={0};
				inet_ntop(AF_INET,&client.sin_addr.s_addr,buff,sizeof(buff));
				cout<<"new connect:"<<buff<<endl;
				clientfds.push_back(connfd);
				event.data.fd=connfd;
				event.events=EPOLLIN|EPOLLET;//读加边缘模式,读完后改为写
				epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
			}else if(ev[i].events&EPOLLIN)//读事件
			{
				connfd=ev[i].data.fd;
				char buff[1024];
				int ret;
				do{
					memset(buff,0,sizeof(buff));
					ret=read(connfd,buff,sizeof(buff));
					if(ret<=0) break;//读完,退出
					puts(buff);
				}while(1);
				event.data.fd=connfd;
				event.events=EPOLLOUT|EPOLLET;//读完改为写
				epoll_ctl(epollfd,EPOLL_CTL_MOD,connfd,&event);
			}else if(ev[i].events&EPOLLOUT)//write
			{
				connfd=ev[i].data.fd;
                const char* buff="1234566788qqwersda";
                int ret;
                 do{
                   ret=write(connfd,buff,strlen(buff)+1);
                   if(ret<=0) break;//写完,退出
                 }while(1);
                 event.data.fd=connfd;
                 event.events=EPOLLIN|EPOLLET;//写完改为读
                 epoll_ctl(epollfd,EPOLL_CTL_MOD,connfd,&event);
		  }
		}
	}
}

epoll反应堆

epoll反应堆主要是读完数据后,改为写事件触发

主要是利用epoll_data的void*ptr该指针指向一个自定义结构体,该结构体存fd,events,回调函数

typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

因此监听的对象的fd在自定义的结构体上

将void*ptr指向自定义的结构体的epoll_event上树,后面调用epoll_wait(),返回的 epoll_event结构体是在树上的结构体的复制体(将变化的节点拷贝下来)
.........