socket之epoll

一、select的缺陷
1、高并发的核心解决方案是1个线程处理所有连接的“等待消息准备好”,这一点上epoll和select是无争议的。但select预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select的使用方法是这样的:返回的活跃连接 ==select(全部待监控的连接)。

2、什么时候会调用select方法呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,调用select在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被“频繁”二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select就完全力不从心了。

3、 此外,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数(1024)。

4、内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。看到这里,您可能要要问了,你为什么不提poll?笔者认为select与poll在内部机制方面并没有太大的差异。相比于select机制,poll只是取消了最大监控文件描述符数限制,并没有从根本上解决select存在的问题。
接下来我们看张图,当并发连接为较小时,select与epoll似乎并无多少差距。可是当并发连接上来以后,select就显得力不从心了。

二、epoll

1、到Linux 2.6才出现由内核直接支持的实现方法epoll,它几乎具备之前所说的一切优点,被公认为Linux 2.6下性能最好的多路I/O就绪通知方法。

2、相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

3、epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,它就不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但代码实现相当复杂。

4、epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

5、另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行描述,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知

三、epoll函数
1.int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

2.int epoll_ctl(int epfd, int op,int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

//保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
typedef union epoll_data {
   void *ptr;
   int fd;
   __uint32_t u32;
   __uint64_t u64;
} epoll_data_t;

// 感兴趣的事件和被触发的事件
struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3.int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
收集在epoll监控的事件中已经发生的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时
epfd:epoll_create()的返回值。
events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。
maxevents:指定最多监听多少个事件
timeout:指定epoll的超时时间,单位是毫秒。当timeout为-1是,epoll_wait调用将永远阻塞,直到某个时间发生。当timeout为0时,epoll_wait调用将立即返回。
返回值:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno

四、epoll工作原理
1、epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

2、另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

五、例子:

#include <sys/socket.h>
#include <sys/types.h>
#include <sys/poll.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include<netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include<sys/time.h>

#include <sys/epoll.h>
#include <sys/select.h>


#define BUF_SIZE 100
#define SERV_PORT 8888
#define MAX_LISTEN_QUE 100
#define MAX_EVENTS 100

//服务器向客户端发送数据
void *ServerToClient(void *arg)
{
	char wdbuf[BUF_SIZE] = {0};
	int timep;
	int writefd = *(int*)arg;
	
	struct tcp_info info; 
	int len=sizeof(info); 
	int status = 0;
	
	while(1)
	{
		
		status = getsockopt(writefd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len); 
		
		if((info.tcpi_state == TCP_ESTABLISHED) )  							//判断客户端未断开  else 断开
		{
			memset(wdbuf, 0, BUF_SIZE );
			timep = time(NULL);
			snprintf(wdbuf, sizeof(wdbuf), "%s", ctime((time_t *)&timep));
			write(writefd, wdbuf, strlen(wdbuf));
			printf("writefd:%d\n", writefd);	
			info.tcpi_state = 0;			
			sleep(5);
		}
		else
		{
			printf("write close:%d\n", writefd);
			pthread_exit(0);
		}			
	}
}


int mz_ipv4_tcp_create_socket(void)
{
	int listenfd, sockfd, opt = 1;
	struct sockaddr_in server;
	socklen_t len;
	int timep;
	int ret;
 
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if(listenfd < 0){
		perror("Create socket fail.");
		return -1;
	} 
 
	if((ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) < 0){
		perror("Error, set socket reuse addr failed");  
		return -1;
	}
 
	bzero(&server, sizeof(server));
	server.sin_family = AF_INET;
	server.sin_port   = htons(SERV_PORT);
	server.sin_addr.s_addr  = htonl(INADDR_ANY);
	
	len = sizeof(struct sockaddr);
	if(bind(listenfd, (struct sockaddr *)&server, len)<0){
			  perror("bind error.");
		return -1;
	}
	  
	listen(listenfd, MAX_LISTEN_QUE);
 
	return listenfd;
}
 
int mz_process_data(int sockfd)
{
	int bytes, flag = 1;
	char buf[100];
 
	while(1)
	{
		//bytes = recv(sockfd, buf, sizeof(buf), 0);
		//memset(buf, 0, sizeof(buf));
		bytes = read(sockfd, buf, sizeof(buf));
		if(bytes < 0)
		{
			perror("recv err:");
			return -1;
		}
		if(bytes == 0)
			return -2;
		
		printf("sockfd:%d bytes:%d : %s",sockfd, bytes, buf);
	}
	
	//send(sockfd, buf, len, 0);	
	return 0;
}
 
/*
int epoll_create ( int size );

int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );
	fd:要操作的文件描述符
     op:指定操作类型
操作类型:
     EPOLL_CTL_ADD:往事件表中注册fd上的事件
     EPOLL_CTL_MOD:修改fd上的注册事件
     EPOLL_CTL_DEL:删除fd上的注册事件
	 
     event:指定事件,它是epoll_event结构指针类型
     epoll_event定义:
	 
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
	返回:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
     timeout:指定epoll的超时时间,单位是毫秒。当timeout为-1是,epoll_wait调用将永远阻塞,直到某个时间发生。当timeout为0时,epoll_wait调用将立即返回。
     maxevents:指定最多监听多少个事件
     events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。
*/
 
int main(int argc, char *argv[])
{
	int listenfd, sockfd;
	int epollfd, fds;
	struct epoll_event ev, events[MAX_EVENTS];	//ev用于注册事件,数组用于返回要处理的事件
	int i, rv;
	struct sockaddr_in client;
	int len;
 
	len = sizeof(struct sockaddr_in);
	epollfd = epoll_create(MAX_EVENTS);			//新建epoll描述符
	if(epollfd < 0)
	{
		perror("epoll_create err:");
		return -1;
	}
 
	listenfd = mz_ipv4_tcp_create_socket();
 
	fcntl(listenfd, F_SETFL, O_NONBLOCK);		//设置文件状态为不阻塞
 
	ev.data.fd = listenfd;
	ev.events = EPOLLIN;
	rv = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);	//EPOLL_CTL_ADD添加待监控者
	if(rv < 0)
	{
		perror("epoll_ctl err:");
		return -1;
	}
 
	pthread_t tid;
	while(1)
	{	
		fds = epoll_wait(epollfd, events, MAX_EVENTS, -1);		//返回活跃的客户端连接个数,timeout==-1阻塞等待:epoll将会把发生的活跃事件赋值到events数组中
		if(fds < 0){
			perror("epoll_wait err:");
			return -1;
		}
 
		for(i = 0; i < fds; i++)
		{
			if(events[i].data.fd == listenfd)
			{
				sockfd = accept(listenfd, (struct sockaddr *)&client, &len);
				if(sockfd < 0)
				{
					perror("accept err:");
					continue;
				}
				ev.data.fd = sockfd;
				ev.events = EPOLLIN | EPOLLET;									//监听读状态同时设置ET模式
				epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);			
				pthread_create(&tid, NULL, ServerToClient, (void*)&sockfd);	//创建线程并发送时间信息给客户端
				continue;
			}else
			{
				rv = mz_process_data(events[i].data.fd);
				if(rv == -2)
				{
					epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
					printf("close socket: %d\n", events[i].data.fd);
					close(events[i].data.fd);
					continue;
				}
			}
		}
	}
}


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值