【网络编程】深度好文(I/O复用:select、poll、epoll)

本文探讨了epoll反应堆技术在服务器中的高效应用,对比了与select/poll的差异,并引入线程池的概念,以提升高并发场景下的处理能力。通过实例展示了如何利用epoll事件驱动和线程池来管理连接,提升服务器响应效率。

1.I/O模型

阻塞等待

多进程、多线程实现的并发,消耗资源,cpu未充分利用

系统为每个并发的客户分配一个进程,该进程只处理该客户信息。由于IO事件的时间较慢,所以进程通常都是阻塞在IO处,浪费系统资源,cpu未充分利用。

在这里插入图片描述

非阻塞等待(轮询)

只有一个进程,依次轮询每个连接套接字connfd的状态。状态发生改变时,应用层进行处理。浪费cpu(轮询)

在这里插入图片描述

多路I/O复用

只有一个进程,所有套接字(包括listenfd和connfd)都交给内核监听,套接字状态发生时,通知应用层处理。

内核监听的函数通常有:select,poll,epoll
在这里插入图片描述

2 select

原理

  1. select 能监听的文件描述符个数受限于 FD_SETSIZE,一般为 1024

  2. 解决 1024 以下客户端时使用 select 是很合适的,但如果链接客户端过多,select 采用的是轮询模型,会大 大降低服务器响应效率,不应在 select 上投入更多精力。

  3. (1)单进程可以打开fd有限制,1024,内核FD_SETSIZE决定;

    (2)对文件描述符进行扫描时是线性扫描,即采用轮询的方法,效率较低;

    (3)用户空间和内核空间的复制非常消耗资源;

  4. 如果开启大量客户端连接后,任何又关闭,nfds将被撑大,轮询时间变长(可用文件描述符重定向解决 dup2())

  5. 大量并发,少量活跃,效率低

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    nfds:		监控的文件描述符集里最大文件描述符加 1,因为此参数会告诉内核检测前多少个文件描述符的状态
    readfds:	监控有读数据到达文件描述符集合,传入传出参数
    writefds:	监控写数据到达文件描述符集合,传入传出参数
    exceptfds:	监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
    timeout:	定时阻塞监控时间,3 种情况
        1.NULL,永远等下去
        2.设置 timeval,等待固定时间
        3.设置 timeval 里时间均为 0,检查描述字后立即返回,轮询

struct timeval {
    long tv_sec; /* seconds */
    long tv_usec; /* microseconds */
};

int FD_ISSET(int fd, fd_set *set);	//文件描述符集合里 fd 是否置1  (判断fd是否在文件描述符集合中)
void FD_CLR(int fd, fd_set *set); 	//文件描述符集合里 fd 位置0	(把fd从文件描述符中删除)
void FD_SET(int fd, fd_set *set); 	//文件描述符集合里 fd 位置1	(把fd添加到文件描述符集合中)
void FD_ZERO(fd_set *set); 			//文件描述符集合里所有位置 0   (把文件描述符集合清空)

在这里插入图片描述

sever.cpp

#include<iostream>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/select.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include"wrap.h"
using namespace std;

int main(int argc, char *argv[]){
	//1.创建套接字socket
	int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

	//2.创建ipv4地址,并和socket绑定 
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");//my(seirver) addr
	//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
	int on = 1;
	if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
		cout<<"setsockopt reuseaddr error!"<<endl;
	Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
	
	//3.监听
	Listen(listenfd,10);
	
	//4.初始化要检测的文件描述符集合
	fd_set readset,tempset;
	FD_ZERO(&readset);//清理	
	FD_SET(listenfd,&readset);//将listenfd添加到读集合中
	int nfds = listenfd+1;//监控的文件描述符集里最大文件描述符

	struct sockaddr_in clientaddr;//客户端地址
	socklen_t clientlen =sizeof(clientaddr);	
	char rec[1024]={0};
	while(1){
		tempset=readset;
		int cnum = select(nfds,&tempset,NULL,NULL,NULL);//响应的个数
		for(int i=listenfd;i<nfds;i++){//遍历集合
			cout<<"轮询文件描述符:"<<i<<endl;
			if(i==listenfd&&FD_ISSET(listenfd,&tempset)){//listenfd在集合中
				int connfd = Accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
				char rback[]="Connect successly!";
				write(connfd,rback,sizeof(rback));
				memset(rback,0,sizeof(rback));
				cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
				cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
				FD_SET(connfd,&readset);
				nfds = connfd+1>nfds?connfd+1:nfds;
				memset(&clientaddr,0,sizeof(clientaddr));
				if(--cnum<=0)continue;//如果只有listenfd响应了,后面的步骤跳过
			}
			else{
				if(FD_ISSET(i,&tempset)){
					int readlen = Readline(i,rec,sizeof(rec));
					if(readlen==0){//接收的数据长度为0时,退出子进程
						FD_CLR(i,&readset);
						Close(i);
						cout<<"close a client"<<endl;
						continue;
					}
					getpeername(i, (struct sockaddr*)&clientaddr,&clientlen);
					cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
                    cout<<" port:"<<ntohs(clientaddr.sin_port);
					cout<<"  "<<rec<<endl;
					memset(rec,0,sizeof(rec));	
					memset(&clientaddr,0,sizeof(clientaddr));
				}
			}
		}	
	}

	return 0;
}

3 poll

原理

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
	fds:		监听数组(存放pollfd的结构体)的首元素地址
    nfds:		数组有效元素的最大下标+1	监控数组中有多少文件描述符需要被监控
    timeout:	超时时间 毫秒级等待
				0:  立即返回,不阻塞进程
    			-1: 阻塞, 检测的fd有变化解除阻塞
    			>0: 阻塞时长, 单位毫秒
返回值:
    -1: 失败
    >0(n): 检测的集合中有n个文件描述符发生状态变化

struct pollfd {
    int fd; /* 文件描述符 */
    short events; /* 监控的事件 */
    short revents; /* 监控事件中满足条件返回的事件 */
};
    // events事件
    - POLLIN -> 检测读
    - POLLOUT -> 检测写
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT; // 检测读写
struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

在这里插入图片描述

调用过程和select类似

相较于 select 而言,poll 的优势:

  1. 传入、传出事件分离。无需每次调用时,重新设定监听事件。
  2. 采用链表的方式替换原有fd_set数据结构,而使其没有连接数的上限,能监控的最大上限数可使用配置文件调整。

server.cpp

#include<iostream>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/select.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<poll.h>
#include"wrap.h"
using namespace std;

#define OPEN_MAX 100


int main(int argc, char *argv[]){
	//1.创建套接字socket
	int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

	//2.创建ipv4地址,并和socket绑定 
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");//my(seirver) addr
	//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
	int on = 1;
	if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
		cout<<"setsockopt reuseaddr error!"<<endl;
	Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
	
	//3.监听
	Listen(listenfd,100);
	
	//4.poll
	struct pollfd client[OPEN_MAX];
	struct sockaddr_in clientaddr;
	socklen_t clientlen = sizeof(clientaddr);
	memset(&clientaddr,0,clientlen);

	client[0].fd=listenfd;
	client[0].events=POLLIN;

	for(int i =1;i<OPEN_MAX;i++){
		client[i].fd=-1;//初始化,fd=-1的位置不监听
	}
	int maxi=0;//client[]中有效位置的最大下标

	char rec[1024]={0};
	memset(rec,0,sizeof(rec));
	while(1){
		int cnum=poll(client,maxi+1,-1);
		cout<<"cnum:"<<cnum<<endl;
		if(client[0].revents&POLLIN){//有客户连接,为什么用&搞不懂
			int connfd = Accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
			char rback[]="Connect successly!";
			write(connfd,rback,sizeof(rback));
			memset(rback,0,sizeof(rback));
			cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
			cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
			memset(&clientaddr,0,clientlen);
			//将新连接的connfd加入client[]中
			for(int i=1;i<OPEN_MAX;i++){
				if(client[i].fd==-1){//找到一个最近的空位置,将新的连接放入
					client[i].fd=connfd;
					client[i].events=POLLIN;
					maxi=maxi>i?maxi:i;
					cout<<"maxi:"<<maxi<<endl;
					break;
				}
			}
			if(--cnum<=0)continue;//如果只有linstenfd响应,后面的就跳过
		}
		for(int i=1;i<=maxi;i++){
			cout<<"轮询:"<<i<<endl;
			if(client[i].fd==-1)continue;
			if(client[i].revents&POLLIN){
				int readlen = Readline(client[i].fd,rec,sizeof(rec));
				if(readlen<0){
					cout<<"read error!"<<endl;
					if(--cnum<=0) break;//若只有这一件事响应,则执行完退出轮询
					continue;//此处事件为读异常,后面的读事件不用执行
				}
				if(readlen==0){//接收的数据长度为0时,退出子进程
					Close(client[i].fd);
					client[i].fd=-1;
					cout<<"close a client"<<endl;
					if(--cnum<=0) break;//若只有这一件事响应,则执行完退出轮询
					continue;//此次响应为客户端断开,后面的读事件不用执行
				}
				getpeername(client[i].fd, (struct sockaddr*)&clientaddr,&clientlen);
				cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
                cout<<" port:"<<ntohs(clientaddr.sin_port);
				cout<<"  "<<rec<<endl;
				memset(rec,0,sizeof(rec));	
				memset(&clientaddr,0,sizeof(clientaddr));			
				if(--cnum<=0) break;//若只有这一件事响应,则执行完退出轮询
			}
		}
	}
	Close(listenfd);
	return 0;
}

4 epoll

int epoll_create(int size);

struct epoll_event;

typedef union epoll_data;

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

原理

步骤:
#include<sys/epoll.h>
(1)创建红黑树模型
	int epoll_create(int size);
	参数	size:要监听的文件描述符上限,2.6版本后写1 即可,可自动扩展
	返回	成功,返回树的句柄,失败: -1

(2)上树,下树,修改节点
    struct epoll_event {
        uint32_t events; /* Epoll events */
        epoll_data_t data; /* User data variable */
    };
    typedef union epoll_data {
        void *ptr;
        int fd; // 常用的一个成员
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    - events:
        - EPOLLIN: 读事件, 检测文件描述符的读缓冲区, 检测有没有数据
        - EPOLLOUT: 写事件, 检测文件描述符的写缓冲区, 检测是不是可写(有内存空间就可写)
    - data.fd 为 epoll_ctl() 第三个参数的的fd的值
    
	int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    参数:
        - epfd: epoll_create() 函数的返回值, 找到epoll的实例
        - op:
            - EPOLL_CTL_ADD: 添加新节点
            - EPOLL_CTL_MOD: 修改已经添加到树上的节点是属性,比如原来检测的是 读事件, 可以修改为写事件
            - EPOLL_CTL_DEL: 将节点从树上删除
        - fd: 要操作的文件描述符
        - event:上述的结点,设置要检测的文件描述符的什么事件,结点与文件描述符绑定
    返回值:成功: 0 失败: -1

(3)监听
	// 这是一个阻塞函数
	// 委托内核检测epoll树上的文件描述符状态, 如果没有状态变化, 该函数默认一直阻塞
	// 有满足条件的文件描述符被检测到, 函数返回
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    参数:
    - epfd epoll_create() 函数的返回值, 找到epoll的实例,理解为树根结点
    - events: 传出参数, 里边记录了当前这轮检测epoll模型中有状态变化的文件描述符信息
    	- 这个参数是一个结构体数组的地址
    - maxevents: 指定第二个参数 events 数组的容量
    - timeout: 超时时长, 单位 ms, 和poll是一样的
    - -1: 委托内核检测epoll树上的文件描述符状态, 如果没有状态变化, 该函数默认一直阻塞,有满足条件的文件描述符被检测到, 函数返回
    - 0: epoll_wait() 调用之后, 函数马上返回
    - >0: 委托内核检测epoll树上的文件描述符状态, 如果没有状态变化,但是timeout时间到达了,函数被强制解除阻塞
    返回值:成功: 有多少文件描述符发生了状态变化

(4)返回监听变化,应用层处理
	应用层对返回的 events[](有状态变化的文件描述符信息) 进行处理

在这里插入图片描述
在这里插入图片描述

跨平台: 不支持, 只支持linux

检测的连接数 和内存有关系

检测方式和效率 树状( 红黑树 )模型, 检测效率很高 内部处理是基于事件的, 和libevent是对应的

委托epoll检测的文件描述符集合用户和内核使用的是同一块内存, 没有数据的拷贝 使用了共享内存

传出的信息的量: 有多少文件描述符发送变化了 -> 返回值 可以精确的知道到底是哪个文件描述符发生了状态变化

server.cpp LT模式

#include<iostream>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include"wrap.h"
using namespace std;
/*
epoll 测试 服务器端
LT 水平工作模式
	工作模式默认是水平模式,缓冲区有数据就调用一次epoll_wait
*/

int main(int argc, char *argv[]){
	//1.创建套接字socket
	int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

	//2.创建ipv4地址,并和socket绑定 
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");//my(seirver) addr
	//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
	int on = 1;
	if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
		cout<<"setsockopt reuseaddr error!"<<endl;
	Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
	
	//3.监听
	Listen(listenfd,100);
	
	//4.epoll
	//(1)创建epoll模型,创建红黑树
	int epfd = epoll_create(1);
	
	//(2)入树,将要检测的文件描述符入树
	struct epoll_event ev;
	ev.events=EPOLLIN;
	ev.data.fd=listenfd;
	epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
	
	//(3)监测
	struct epoll_event evs[1024];//用于接收epoll监听的返回值
	struct sockaddr_in clientaddr;
	socklen_t clientlen =sizeof(clientaddr);
	char rec[1024]={0};	
	while(1){
		cout<<"epoll_wait()..."<<endl;
		int cnum=epoll_wait(epfd,evs,1024,-1);
		cout<<"epoll_wait()完成 "<<"cnum:"<<cnum<<endl;
		for(int i=0;i<cnum;i++){
			//cout<<"i:"<<i<<endl;
			if(!(evs[i].events&EPOLLIN))continue;//若不是读事件忽略
			if(evs[i].data.fd==listenfd){//lfd响应,新客户连接
				int connfd=Accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
        		ev.data.fd=connfd;
        		ev.events=EPOLLIN;//工作模式默认是水平模式,缓冲区有数据就调用一次epoll_wait
        		epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
				char rback[]="Connect successly!";
                write(connfd,rback,sizeof(rback));
				cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
				cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
				memset(&clientaddr,0,sizeof(clientaddr));
				memset(rback,0,sizeof(rback));
				continue;
			}
			else{
				cout<<"read... ";
				int readlen = Readline(evs[i].data.fd,rec,sizeof(rec));
				cout<<"readlen="<<readlen<<endl;
				if(readlen<0){cout<<"read error!";continue;}
				if(readlen==0){//接收的数据长度为0时,退出子进程
					epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL);
					Close(evs[i].data.fd);
					cout<<"close a client"<<endl;
					continue;
				}
				getpeername(evs[i].data.fd, (struct sockaddr*)&clientaddr,&clientlen);
				cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
                cout<<" port:"<<ntohs(clientaddr.sin_port);
				cout<<"  "<<rec<<endl;
				memset(rec,0,sizeof(rec));	
				memset(&clientaddr,0,sizeof(clientaddr));	
			}
			cout<<endl;
		}
	} 
	Close(listenfd);
	return 0;
}

epoll 的工作模式

两种工作模式:水平模式(LT),边沿模式(ET)

  • 水平模式, 默认的工作的模式是水平模式

    • LT(level triggered)

    • 阻塞和非阻塞的套接字都是支持的

    • 阻塞指定的接收和发送数据的状态

      • read/recv
      • write/send
  • 边沿模式

    • ET(edge-triggered)
    • 效率高, 只支持非阻塞的套接字

LT模式

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket

内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

epoll默认是水平模式 LT

/*
- 读事件: 
	在这种场景下, 只要是epoll_wait检测到读缓冲区有数据, 就会通知用户一次
		- 不管数据有没有读完, 只要有数据就通知
		- 通知就是 epoll_wait() 函数返回, 我们就可以处理传出参数中的文件描述符的状态
- 写事件:
	检测写缓冲区是否可用(是否有容量), 只要是可写(有容量)epoll_wait()就会返回
*/

下面是客户端发送1234567890和abcdefg两次数据,服务器端接收rec[4] (一次最多接收4个字符),LT模式下可以看到多次调用epoll_wait(),文件描述符的读缓冲区有数据就会一直调用epoll_wait(),直至读完
在这里插入图片描述

ET模式

ET(edge-triggered)是高速工作方式只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll通知一次

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

/*
特点: epoll_wait()检测的次数变少了, 效率变高了(有满足条件的新状态才会通知)
- 读事件: 
	- 接收端每次收到一条(***新的***)数据, epoll_wait() 只会通知一次,不管你有没有将数据读完
- 写事件:
	- 检测写缓冲区是否可用(是否有容量),通知一次
	- 写缓冲区原来是不可用(满了), 后来缓冲区可用(不满), epoll_wait()检测到之后通知一次(唯一)
*/

下面是客户端发送1234567890和abcdefg两次数据,服务器端接收rec[4] (一次最多接收4个字符),ET模式下可以看到两次调用epoll_wait(),因为客户端只发送两两次数据,对应文件描述符发生两次变化
在这里插入图片描述

如何设置边沿模式?

// 在struct epoll_event 结构体的成员变量 events 事件中额外设置 EPOLLET
// 往epoll模型上添加新节点
int cfd = accept(lfd, NULL, NULL);
// cfd 添加到检测的原始集合中
ev.events = EPOLLIN | EPOLLET; // 设置文件描述符的边沿模式
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);

通过测试如果epoll_wait()只通知一次, 并且接收端接收数据的缓存比较小, 导致服务器端通信的文件描述符中的数据越来越多, 数据如果不能全部读出, 就无法处理客户端请求, 如果解决这个问题?

// 解决方案, 在epoll_wait() 通知的这一次中, 将客户端发送的数据全部读出
    - 循环的进行数据接收
        - 需要使用这种方案, 但是有问题, 会导致服务器端程序的阻塞在read()
            while(1)
            {
                int len = read(cfd, buf, sizeof(buf));
                // 读完之后需要跳出循环
                // 如果客户端和服务器的连接还保持着, 如果数据接收完毕, read函数阻塞
                // 服务器端程序的单线程/进程的, read阻塞会导致整个服务器程序阻塞
            }
		- 解决上述的问题: 将数据的接收动作修改为非阻塞
            - read()/recv(), write()/send()阻塞是函数行为, 还是操作的文件描述符导致的?
            - 调用这些函数都是去检测操作的文件描述符的读写缓冲区 => 是文件描述符导致的

如何设置文件描述符的非阻塞?

// 使用fcntl函数设置文件描述符的非阻塞
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
// 因为文件描述符行为默认是阻塞的, 因此要追加非阻塞行为
// 获取文件描述符的flag属性
int flag = fcntl(cfd, F_GETFL);
// 给flag追加非阻塞
flag = flag | O_NONBLOCK; // 或 flag |= O_NONBLOCK;
// 将新的flag属性设置到文件描述符中
fcntl(cfd, F_SETFL, flag);

在非阻塞模式下读完数据时遇到的错误,此时应该跳出循环

// recv error: Resource temporarily unavailable -> 资源不可用, 因为内存中没有数据了
// 错误出现的原因:
#include<errno.h>
	while(1){
        int raadlen = read();
        if(readlen==-1&&errno = EAGAIN)//read()本来阻塞在这里,这时应该跳出循环
    }
循环的读数据, 当通信的文件描述符对应读缓冲区数据被读完, recv/read 不会阻塞, 继续读缓冲区
但是缓冲区中没有数据, 这时候read/recv 调用就失败了, 返回 -1, 这时候错误号 errno的值为:
 errno = EAGAIN or EWOULDBLOCK , 一般情况下使用 EAGAIN 判断就可以了

server.cpp ET模式

#include<iostream>
#include<errno.h>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include"wrap.h"
using namespace std;
/*
epoll 测试 服务器端
ET 边沿工作模式
	发生变化时只调用一次epoll_wait
*/

int main(int argc, char *argv[]){
	//1.创建套接字socket
	int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

	//2.创建ipv4地址,并和socket绑定 
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");//my(seirver) addr
	//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
	int on = 1;
	if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
		cout<<"setsockopt reuseaddr error!"<<endl;
	Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
	
	//3.监听
	Listen(listenfd,100);
	
	//4.epoll
	//(1)创建epoll模型,创建红黑树
	int epfd = epoll_create(1);
	
	//(2)入树,将要检测的文件描述符入树
	struct epoll_event ev;
	ev.events=EPOLLIN|EPOLLET;
	ev.data.fd=listenfd;
	epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
	
	//(3)监测
	struct epoll_event evs[1024];//用于接收epoll监听的返回值
	struct sockaddr_in clientaddr;
	socklen_t clientlen =sizeof(clientaddr);
	char rec[4]={0};	
	while(1){
		cout<<"epoll_wait()..."<<endl;
		int cnum=epoll_wait(epfd,evs,1024,-1);
		cout<<"epoll_wait()完成 "<<"cnum:"<<cnum<<endl;
		for(int i=0;i<cnum;i++){
			//cout<<"i:"<<i<<endl;
			while(1){
				if(!(evs[i].events&EPOLLIN))break;//若不是读事件忽略
				if(evs[i].data.fd==listenfd){//lfd响应,新客户连接
					int connfd=Accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
        			//因为文件描述符行为默认是阻塞的, 因此要追加非阻塞行为
					//fcntl系统调用:对已打开的文件描述符进行控制操作,改变已打开文件的各种属性
					int flag=fcntl(connfd,F_GETFL);//获取文件描述符的flag属性
					flag |= O_NONBLOCK;//给flag追加非阻塞
					fcntl(connfd,F_SETFL,flag);//将新的flag属性设置到文件描述符中
					ev.data.fd=connfd;
        			ev.events=EPOLLIN|EPOLLET;
        			epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
					char rback[]="Connect successly!";
					write(connfd,rback,sizeof(rback));
					cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
					cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
					memset(&clientaddr,0,sizeof(clientaddr));
					memset(rback,0,sizeof(rback));
					break;
				}	
				else{//connfd响应的读事件
					cout<<"read... ";
					int readlen = Readline(evs[i].data.fd,rec,sizeof(rec));
					cout<<"readlen="<<readlen<<endl;
					if(readlen<0){
						//缓冲区读干净了,若设置阻塞套接字时read会阻塞在这里
						//设置非阻塞套接字,read会返回错误信息EAGAIN,此时跳出循环
						if(errno==EAGAIN) break;
						//若为其他错误,关闭套接字,下树,跳出循环
						cout<<"read error!";
						epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL);
                        Close(evs[i].data.fd);
						break;
					}
					if(readlen==0){//接收的数据长度为0时,退出子进程
						epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL);
						Close(evs[i].data.fd);
						cout<<"close a client"<<endl;
						break;
					}
					getpeername(evs[i].data.fd, (struct sockaddr*)&clientaddr,&clientlen);
					cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
                	cout<<" port:"<<ntohs(clientaddr.sin_port);
					cout<<"  "<<rec<<endl;
					memset(rec,0,sizeof(rec));
					memset(&clientaddr,0,sizeof(clientaddr));
				}
			}
			cout<<endl;
		}
	} 
	Close(listenfd);
	return 0;
}

5 client.cpp

客户端代码,select、poll、epoll相同

#include<iostream>
#include<string>
#include<string.h>
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<fcntl.h>
#include"wrap.h"
using namespace std;

int main(int argc,char* argv[]){
	//1.make socket
	int sockfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);	
	//2.make ip addr and connect
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	//void *memset(void *s,int c,size_t n)
	//总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c。
	servaddr.sin_family= AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");
	cout<<"Connect ..."<<endl;
	Connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
	char rback[1024]={0};
	read(sockfd,rback,sizeof(rback));
	cout<<rback<<endl;
	//3.after connect	
	char sendbuf[1024] = {0};
	char recvbuf[1024] = {0};
	int iconnect =0;
	while(1){
		fgets(sendbuf,sizeof(sendbuf),stdin);
		Writen(sockfd,sendbuf,strlen(sendbuf));
		memset(sendbuf,0,sizeof(sendbuf));
	}
	Close(sockfd);
	return 0;
}

6 select、poll、epoll总结

时间复杂度
select、poll、epoll时间复杂度
select==>时间复杂度O(n)
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长
poll==>时间复杂度O(n)
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
epoll==>时间复杂度O(1)
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
同步I/O

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

如何选择

1、epoll是Linux所特有,select则应该是POSIX所规定,windows、linux都有

2、多连接,少活跃,选择epoll

3、在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,因为epoll的通知机制需要很多函数回调。

4、select低效是因为每次它都需要轮询,但低效也是相对的,视情况而定,也可通过良好的设计改善

7 epoll反应堆

原理

前面的epoll对应每个事件的发生,都需要去处理(如果是IO事件,处理较慢,相当于阻塞),epoll反应堆的思想就是当某个时间发生了,自动的去处理这 个事件(先从内核区拷贝到用户区,后面可以用户再处理)

这样的思想对我们的编码来说就是设置回调,将文件描述符,对应的事件,和事件产生时的处理函数封装 到一起,这样当某个文件描述符的事件发生了,回调函数会自动被触发,这就是所谓的反应堆思想。

在这里插入图片描述
epoll反应堆流程
在这里插入图片描述
一般我们先会给epoll_event中的data进行赋值,然后再epoll_wait返回后取出来进行使用

切记不要同时给data.fd和data.ptr赋值,会覆盖,先赋值的那个将无意义,data.fd和data.ptr只能用其中一个

//**epoll反应堆流程**
epoll_create(); // 创建监听红黑树
epoll_ctl(); // 向书上添加监听fd
epoll_wait(); // 监听
有客户端连接上来--->lfd调用acceptconn()--->将cfd挂载到红黑树上监听其读事件--->
epoll_wait()返回cfd--->cfd回调recvdata()--->将cfd摘下来监听写事件--->...--->
epoll_wait()返回cfd--->cfd回调senddata()--->将cfd摘下来监听读事件--->...--->
//目前的代码是只监听读事件

epoll反应堆 server.cpp

//自定义结构体
struct myevent{
	int fd;
	int events;//要处理的时间
	void *arg;//指向自己结构体指针
	void (*call_back)(int fd,void *arg);//回调函数,函数指针
	int status;//0:未上树,1:已上树
	char buf[BUFLEN];
	int len;
};
//创建一个myevent结构体
void set_myevent(struct myevent *myev,int fd,void (*call_back)(int fd,void *arg),int events);
//构造一个对应myevent的内核结构体epoll_event,并将epoll_event上树
void eventadd(int efd,struct myevent *myev);
//从epoll红黑树上删除一个epoll_event结构体,下树,并修改对应的myevent
void eventdel(int efd,struct myevent *myev);
//当listenfd文件描述符就绪, 调用该函数完成与客户端的连接
void acceptconn(int lfd,void *arg);
//当connfd文件描述符就绪,且为读事件,调用此函数完成读
void recvdata(int fd,void *arg);
//当connfd文件描述符就绪,且为写事件,调用此函数完成写
void senddata(int fd,void *arg);
#include<iostream>
#include<errno.h>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include"wrap.h"
using namespace std;

//epoll reactor 测试epoll反应堆 服务器端

#define MAX_EVENTS 1024 //监听数上限
#define BUFLEN 1024 //buf[]数组长度,缓冲区大小

struct myevent{
	int fd;
	int events;//要处理的时间
	void *arg;//指向自己结构体指针
	void (*call_back)(int fd,void *arg);//回调函数,函数指针
	int status;//0:未上树,1:已上树
	char buf[BUFLEN];
	int len;
	long last_active;
};

//全局变量
int efd;//红黑树树根
struct myevent myevs[MAX_EVENTS+1];//与epoll_event对应的自定义结构体数组,用来调用回调函数 

void recvdata(int fd,void *arg);
void senddata(int fd,void *arg);

//创建一个myevent结构体
void set_myevent(struct myevent *myev,int fd,void (*call_back)(int fd,void *arg),int events){
	myev->fd=fd;
    myev->events=events;
    myev->arg=myev;//指向自己结构体本身的指针
    myev->call_back=call_back;
    myev->status=0;//0表示未上树
    memset(myev->buf,0,sizeof(myev->buf));//清空buf
	myev->len=0;//长度置0
    myev->last_active=time(NULL);
}

//构造一个对应myevent的内核结构体epoll_event,并将epoll_event上树
void eventadd(int efd,struct myevent *myev){
	struct epoll_event ev;
    //data.fd和data.ptr只能用其中一个,不要两个都赋值了
	ev.data.ptr = myev;//ev的ptr指针指向myev结构体
	ev.events=myev->events;
	if(myev->status==0){//如果myev的status为0,表示未上树
		if(epoll_ctl(efd,EPOLL_CTL_ADD,myev->fd,&ev)<0)//将ev上树
			cout<<"Add ev to efd error!"<<endl;
		else	cout<<"Add ev successly!"<<endl;
	}
	myev->status=1;
}

//从epoll红黑树上删除一个epoll_event结构体,下树,并修改对应的myevent
void eventdel(int efd,struct myevent *myev){
	struct epoll_event ev;
	if(myev->status!=1) return;//myev并没所有上树,不处理
	ev.data.ptr=NULL;
	myev->status=0;
	if(epoll_ctl(efd,EPOLL_CTL_DEL,myev->fd,NULL)<0)
		cout<<"Delete ev error!"<<endl;
	else	cout<<"Delete ev successly!"<<endl;
}

//当listenfd文件描述符就绪, 调用该函数完成与客户端的连接
void acceptconn(int lfd,void *arg){
	struct sockaddr_in clientaddr;
	socklen_t len =sizeof(clientaddr);
	int connfd;
	connfd=Accept(lfd,(struct sockaddr*)&clientaddr,&len);
	int i;
	//从下标0开始寻找一个未上树的结构体myevent
	for(i=0;i<MAX_EVENTS;i++){
		if(myevs[i].status==0)break;
	}
	if(i==MAX_EVENTS){
		cout<<"over max connect limit"<<endl;
		return;
	}
	fcntl(connfd,F_SETFL,O_NONBLOCK);
	set_myevent(&myevs[i],connfd,recvdata,EPOLLIN|EPOLLET);//创建myevent结构体
	eventadd(efd,&myevs[i]);//上树
	char rback[]="connect successly!";
	write(connfd,rback,sizeof(rback));
	cout<<rback<<endl;
	cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
	cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
	memset(&clientaddr,0,sizeof(clientaddr));
	memset(rback,0,sizeof(rback));
}

//当connfd文件描述符就绪,且为读事件,调用此函数完成读
void recvdata(int fd,void *arg){
	struct myevent *myev = (struct myevent*)arg;
	int readlen=Readline(fd,myev->buf,sizeof(myev->buf));
	cout<<"readlen:"<<readlen<<endl;
	struct sockaddr_in clientaddr;
        socklen_t clientlen =sizeof(clientaddr);
        getpeername(myev->fd, (struct sockaddr*)&clientaddr,&clientlen);
	if(readlen<0){
		eventdel(efd,myev);
		Close(myev->fd);
		return;
	}
	if(readlen==0){
        eventdel(efd,myev);
        Close(myev->fd);
		cout<<"Client Close:";
		cout<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
		cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
		memset(&clientaddr,0,sizeof(clientaddr));
        return;
        }
	cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
    cout<<" port:"<<ntohs(clientaddr.sin_port);
	cout<<"  "<<myev->buf<<endl;
	memset(&clientaddr,0,sizeof(clientaddr));
}
//当connfd文件描述符就绪,且为写事件,调用此函数完成写
void senddata(int fd,void *arg){
}

int main(int argc, char *argv[]){
	//1.创建套接字socket
	int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

	//2.创建ipv4地址,并和socket绑定 
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("192.168.109.128");//my(seirver) addr
	//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
	int on = 1;
	if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
		cout<<"setsockopt reuseaddr error!"<<endl;
	fcntl(listenfd, F_SETFL, O_NONBLOCK);//设置非阻塞
	Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
	
	//3.监听
	Listen(listenfd,100);

	//epoll 反应堆
	efd=epoll_create(MAX_EVENTS+1);
	struct epoll_event evs[MAX_EVENTS + 1];//用来接收epoll_wait返回的事件

	//构建listenfd对应的myevent结构体,回调函数为acceptconn
	set_myevent(&myevs[MAX_EVENTS],listenfd,acceptconn,EPOLLIN|EPOLLET);
	//将listenfd对应的myevent对应到epoll_event,再将其上树
	eventadd(efd,&myevs[MAX_EVENTS]);
	while(1){
		cout<<"wait..."<<endl;
		int cnum=epoll_wait(efd,evs,MAX_EVENTS+1,-1);
		cout<<"wait successly, cnum:"<<cnum<<endl;
		for(int i=0;i<cnum;i++){
			struct myevent *myev = (struct myevent*)evs[i].data.ptr;
			if(evs[i].events & EPOLLIN){
				myev->call_back(myev->fd,myev);
			}
			if(evs[i].events & EPOLLOUT){
				myev->call_back(myev->fd,myev);
			}
			cout<<endl;
		}
	}
	Close(listenfd);
	return 0;
}

8 epoll线程池

线程池是一个抽象概念,可以简单的认为若干线程在一起运行,线程不退出,有任务时线程执行任务,无任务时等待。

为什么要有线程池?

  1. 利用多线程处理高并发时,对于多个请求每次都去建立线程,这样线程的创建和销毁也会有很大的系统开销,使用上效率很低。
  2. 创建线程并非多多益善,所以提前创建好若干个线程,不退出,等待任务的产生,去接收任务处理后等待下一个任务。

**线程池如何实现?**需要思考 2 个问题?

  1. 假设线程池创建了,线程们如何去协调接收任务并且处理?
  2. 线程池上的线程如何能够执行不同的请求任务?

上述**问题 1 解决思路是操作系统资源分配思路,助互斥锁和条件变量来搞定。**就很像我们之前学过的生产者和消费者模型,客户端对应生产者,服务器端这边的线程池对应消费者,再用互斥锁和条件变量来搞定。 问题 2 解决思路就是利用回调机制,我们同样可以借助结构体的方式,对任务进行封装,比如任务的数据和任 务处理回调都封装在结构体上,这样线程池的工作线程拿到任务的同时,也知道该如何执行了。
在这里插入图片描述

任务队列中的任务包含了要执行的事件,及其回调函数等信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值