epoll实现并发服务器

avatar
作者
筋斗云
阅读量:0

1、epoll是什么

epoll 是 Linux 上一种高性能的多路复用机制,用于监视大量文件描述符并在它们就绪时通知应用程 序。它是在 select 和 poll 的基础上进一步优化和改进而来的。

2、epoll的特点

1.没有文件描述符数量限制:

与 select 和 poll 不同,epoll 采用了基于事件的就绪通知机制, 没有预定义的文件描述符数量限制,可以支持更大规模的并发连接。

2.高效的事件通知:

epoll 使用了内核和用户空间共享的事件数据结构,将文件描述符的事件注册到内 核空间,当事件就绪时,内核直接将就绪的事件通知给用户空间,避免了每次调用都需要遍历整个文件 描述符数组的性能开销。

3.分离的就绪事件集合:

epoll 将就绪的事件从内核空间复制到用户空间,形成一个分离的就绪事件集 合,用户可以直接遍历这个集合来处理就绪的事件,而不需要遍历整个文件描述符数组。

4.支持边缘触发和水平触发:

epoll 提供了两种模式来处理事件,一种是边缘触发模式 (EPOLLET),只在状态发生变化时通知应用程序,另一种是水平触发模式(默认),在事件就绪期间一直通知应用程序。

5.更低的内存拷贝开销:

epoll 使用内存映射技术,避免了每次调用都需要将事件数据从内核复制到用 户空间的开销,从而减少了系统调用的次数和内存拷贝的开销。

6.支持较高精度的超时控制:

与 poll 不同,epoll 的超时参数以毫秒和纳秒为单位,提供了较高精 度的超时控制。

总体来说:epoll 在性能上相比于 select 和 poll 有较大的优势,特别适用于高并发场景下的网 络编程。它的高效事件就绪通知、支持大规模并发连接、较低的内存拷贝开销以及较高的超时精度,使 得它成为开发高性能服务器和网络应用的首选机制。

3、epoll实现高并发原理

在Linux内核中,epoll使用红黑树作为其主要的数据结构,用于维护注册的文件描述符集合。红黑树是 一种自平衡的二叉搜索树,具有较快的插入、删除和搜索操作的时间复杂度。通过使用红黑树,epoll能 够高效地检索和管理大量的文件描述符。

当文件描述符发生事件时,epoll通过红黑树的查找操作快速定位到相应的结点,并触发注册的回调函数 进行事件处理。使用红黑树的原因是它能够保持良好的平衡性,保证搜索、插入和删除操作的最坏情况时间复杂度为O(log n),从而保证了epoll的高性能和可伸缩性。

总结来说,epoll是利用红黑树作为其底层数据结构实现的,这使得它在处理大量并发连接时能够提供高 效的事件通知机制。

4、epoll相关函数

1、epoll_create

(1)epoll_create函数原型:
int epoll_create(int size);
功能:epoll_create 函数创建一个 epoll 实例,并返回一个文件描述符,用于标识该 epoll 实
例。

2、epoll_ctl

(2)epoll_ctl函数原型:

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

功能:操作epoll实例

3、epoll_wait

(3)epoll_wait()函数原型:

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

功能:等待epoll 文件描述符的IO事件

5、epoll实现并发服务器的步骤

当使用epoll实现并发服务器时,通常的步骤包括以下几个主要环节:

1.创建socket:使用socket函数创建一个监听套接字,用于接受客户端的连接请求。

2.绑定socket:使用bind函数将监听套接字绑定到一个特定的IP地址和端口。

3.监听连接:使用listen函数开始监听连接请求,指定服务器可接受的最大连接数。

4.创建epoll实例:使用epoll_create函数创建一个epoll实例,返回一个文件描述符。

5.将监听套接字添加到epoll实例:使用epoll_ctl函数将监听套接字添加到epoll实例中,并注册对 读事件的关注(默认为水平触发)。

6.进入事件循环:循环调用epoll_wait函数来等待事件的发生,该函数会阻塞当前进程直至有事件发 生。一旦有事件发生,它将返回一个就绪事件的列表。

7.处理就绪事件:遍历就绪事件列表,对每个事件进行处理。根据事件类型,可以进行接受连接、读取 数据、发送数据或关闭连接等操作。

8.根据需要添加或删除文件描述符:在处理完一个事件后,可以根据需要使用epoll_ctl函数动态地添 加或删除文件描述符,以便继续监听其他事件。

9.重复步骤6-8:继续循环执行步骤6-8,处理新的就绪事件,直到服务器主动关闭或出现错误条件为 止。

6、使用epoll实现并发服务器的代码

1、net.h

#ifndef _NET_H_ #define _NET_H_   //定义枚举,表示返回值  enum VALUE_RET { 	QUIT = -9, 	SOCK_ERROR = -8, 	BIND_ERROR, 	LISTEN_ERROR,     ACCEPT_ERROR, 	SEND_ERROR, 	RECV_ERROR, 	CONNECT_ERROR, 	ERROR, 	OK };  //添加所需的头文件以及函数声明   #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <strings.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h>   int server_initial_func(const char *IP, const char *Port); int client_initial_func(const char *IP, const char *Port); int server_com_func(int newfd); void parseString_func(char *buf, char **result);   #endif 

2、client.c

#include "./net.h"  int client_initial_func(const char *IP, const char *Port) { 	//创建套接字 	int sockfd = socket(AF_INET, SOCK_STREAM, 0); 	if(sockfd < 0) 	{ 		perror("socket error"); 		return SOCK_ERROR; 	} 	printf("socket ok!\n");  	//定义地址信息结构,存储服务器的IP地址和端口号 	struct sockaddr_in serverAddr; 	//清空 	memset(&serverAddr, '\0', sizeof(serverAddr)); 	//赋值  	serverAddr.sin_family = AF_INET; 	serverAddr.sin_port = htons((short)atoi(Port)); 	serverAddr.sin_addr.s_addr = inet_addr(IP); 	 	//发起连接请求 	int ret = connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); 	if(ret < 0) 	{ 		perror("connect error"); 		return CONNECT_ERROR; 	} 	printf("connect ok!\n");  	return sockfd; }  int client_com_func(int sockfd) { 	int send_count = 0, recv_count = 0; 	int wr_count = 0, rd_count = 0;     //请求客户端,完成业务办理 	char func[20] = {0}; 	char data[20] = {0};  	char buf[1024] = {0};  	char Data[1024] = {0};  	printf("请输入办理的业务和数据:"); 	scanf("%s%*c", func); 	scanf("%s", data); 	 	//将func和data写入到buf中,发送给服务器 	sprintf(buf, "%s#%s",func, data); 	 	send_count = send(sockfd, buf, strlen(buf), 0); 	if(send_count < 0) 	{ 		perror("send func+data to server error"); 		return ERROR; 	} 	printf("send func+data to server ok!\n");  	//挑选业务执行的空间 	if(0 == strncasecmp("download", func, 8)) 	{ 		//下载 		recv_count = recv(sockfd, Data, sizeof(Data), 0); 		if(recv_count < 0){ 			perror("recv error"); 			return ERROR; 		} 		//解析Data 		char *result[4] = {NULL}; 		parseString_func(Data, result); 		printf("服务器回应:业务编号%s 业务数据%s\n",result[0], result[1]);  		if(0 == strncasecmp("FILESIZE", result[0], 8)) 		{ 			//可以接收result[1]作为所需下载资源的大小 			long filesize = atol(result[1]); 			printf("服务器发送的资源大小为:%ld个字节!\n",filesize); 			 			//只写方式打开下载的文件 			int fw = open(data, O_WRONLY | O_CREAT | O_TRUNC, 0664); 			if(fw < 0){ 				perror("open error"); 				return ERROR; 			} 			printf("open_write %s ok!\n",data);  			char Message[512] = {0};  			while(1) 			{ 				//判断可读字节数是否为0 				if(0 == filesize) 				{ 					printf("下载成功!\n"); 					break; 				} 				memset(Message, 0, sizeof(Message)); 				//接收 				recv_count = recv(sockfd, Message, sizeof(Message), 0); 				if(recv_count < 0){ 					perror("recv error"); 					return ERROR; 				} 				else{  					printf("成功接收服务器发送%d个字节,即将写入...\n", recv_count);  					//写入 					wr_count = write(fw, Message, recv_count); 					if(wr_count < 0) 					{ 						perror("write error"); 						return ERROR; 					} 					else if(0 == wr_count){ 						printf("nothing was be written...\n"); 						return ERROR; 					} 					else{ 						printf("本次写入到文件成功,共计写入%d个字节!\n",wr_count); 					} 				    //更新filesize的可读字节数 					filesize -= wr_count; 					printf("剩余%ld个字节未被读取...\n", filesize); 				} 				printf("**************************************\n");	 			} 		 		}  	} 	return OK; }  //void parseString_func(char buf[], char *result[]); void parseString_func(char *buf, char **result)//download#1.txt { 	//将buf的首地址存储在result[0] 	int index = 0; 	result[index++] = buf;  	while(*buf) 	{ 		//判断*buf是否为'#' 		if('#' == *buf) 		{ 			*buf = '\0'; 			buf++; 			result[index++] = buf; 		} 		else 		{ 			buf++;	 		} 	} 	return; } int main(int argc, const char *argv[]) { 	//搭建TCP客户端  	     //客户端端初始化 	int sockfd = client_initial_func(argv[1], argv[2]); 	if(sockfd < 0) 	{ 		return ERROR; 	} 	printf("server_initial success!\n");  	//通信  	while(1) 	{ 		if(client_com_func(sockfd) < 0) 		{ 			break; 		} 	}  	//关闭套接字 	close(sockfd); 	return 0; } 

3、server.c

#include "./net.h"  int server_initial_func(const char *IP, const char *Port) { 	//创建套接字 	int sockfd = socket(AF_INET, SOCK_STREAM, 0); 	if(sockfd < 0) 	{ 		perror("socket error"); 		return SOCK_ERROR; 	} 	printf("socket ok!\n");  	//定义地址信息结构,存储服务器的IP地址和端口号 	struct sockaddr_in serverAddr; 	//清空 	memset(&serverAddr, '\0', sizeof(serverAddr)); 	//赋值  	serverAddr.sin_family = AF_INET; 	serverAddr.sin_port = htons((short)atoi(Port)); 	serverAddr.sin_addr.s_addr = inet_addr(IP); 	 	//绑定IP地址和端口 	if(bind(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) 	{ 		perror("bind error"); 		return BIND_ERROR; 	} 	printf("bind IP+Port ok!\n");  	//创建监听队列  	if(listen(sockfd, 5) < 0) 	{ 		perror("listen error"); 		return LISTEN_ERROR; 	} 	printf("listening......\n");  	return sockfd; }   int server_com_func(int newfd) { 	//处理客户端的业务 	char buf[1024] = {0}; 	char Data[1024] = {0};  	int send_count = 0, recv_count = 0; 	int wr_count = 0, rd_count = 0;  	int ret = recv(newfd, buf, sizeof(buf), 0); 	if(ret < 0){ 		perror("recv function+data error"); 		return ERROR; 	} 	printf("客户端业务+数据:%s\n",buf);  	//解析buf,拿到业务编号以及数据 	char *result[4] = {NULL};  	parseString_func(buf, result);  	//解析结果呈现 	printf("业务:%s\t业务数据:%s\n", result[0],result[1]);  	//根据业务进入不同操作空间 	if(0 == strncasecmp("logon", result[0], 5)) 	{ 		//登陆 		printf("登陆中...\n"); 	} 	else if(0 == strncasecmp("register", result[0], 8)) 	{ 		//注册 		printf("注册中...\n"); 	} 	else if(0 == strncasecmp("download", result[0], 8)) 	{ 		//上传 		//获取客户端指定下载文件的大小 		struct stat MyStat; 		long filesize = 0; 		if(lstat(result[1], &MyStat) < 0) 		{ 			perror("lstat error"); 			return ERROR; 		} 		printf("测试%s文件属性OK!\n", result[1]); 		//获取大小,赋值给filesize 		filesize = MyStat.st_size; 		printf("成功获取的文件%s大小为:%ld个字节\n", \ 				result[1], filesize);  		//将filesize发送给客户端(带上业务编号:FILESIZE) 		memset(Data, '\0', sizeof(Data)); 		sprintf(Data, "FILESIZE#%ld", filesize);  		//发送Data的内容(发送有效字符个数-->15个) 		send_count = send(newfd, Data, strlen(Data), 0); 		//send_count = send(newfd, Data, sizeof(Data), 0); 		if(send_count < 0) 		{ 			perror("send filesize to client error"); 			return ERROR; 		} 		printf("send filesize to client success!\n");  		//延时1秒 		sleep(1); 		//只读打开result[1] 		int fr = open(result[1], O_RDONLY); 		if(fr < 0){ 			perror("open_read error"); 			return ERROR; 		} 		printf("open_read ok!\n");  		//循环,边读边发 		char Message[512] = {0}; 		while(1) 		{ 			memset(Message, 0,sizeof(Message)); 			//读取 			rd_count = read(fr, Message, sizeof(Message)); 			if(rd_count < 0){ 				perror("read error"); 				return ERROR; 			} 			else if(0 == rd_count){ 				//读取到文件末尾 				printf("上传成功!\n"); 				break; 			} 			else{ 				printf("本次读取成功,共计读取%d个字节!\n",rd_count);  				//发送读取的字节内容给客户端 				send_count = send(newfd, Message, rd_count, 0); 				if(send_count < 0) 				{ 					perror("send data to client error"); 					return ERROR; 				} 				printf("send ok! 共计发送%d个字节!\n",send_count); 			} 			printf("*************************************\n"); 		} 	} 	else 	{ 		printf("业务无法响应~\n"); 		return ERROR; 	} 	return OK; }  //void parseString_func(char buf[], char *result[]); void parseString_func(char *buf, char **result)//download#1.txt { 	//将buf的首地址存储在result[0] 	int index = 0; 	result[index++] = buf;  	while(*buf) 	{ 		//判断*buf是否为'#' 		if('#' == *buf) 		{ 			*buf = '\0'; 			buf++; 			result[index++] = buf; 		} 		else 		{ 			buf++;	 		} 	} 	return; }  int main(int argc, const char *argv[]) { 	//搭建TCP服务器  	     //服务器端初始化 	int listenfd = server_initial_func(argv[1], argv[2]); 	if(listenfd < 0) 	{ 		return ERROR; 	} 	printf("server_initial success!\n");   	//epoll实现并发服务器  	int epfd = epoll_create(12);     if(-1 == epfd)     {         perror("epoll_create error");         return -1;     }     printf("epoll_create ok!\n");          //5、将监听套接字加入到epoll实例中(注册在红黑树中)     //定义epoll事件结构体变量     struct epoll_event event;     //赋值     event.events = EPOLLIN;//EPOLLIN表示对于其可读事件感兴趣  默认为水平触发     event.data.fd = listenfd;     if(epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event) == -1)     {         perror("add listenfd error");         return -1;     }     printf("add lisenfd ok!");          //6、进入事件循环     //定义数组,存储就绪的所有描述符     struct epoll_event events[MAXEVENTS];          //定义保存连接成功的新的客户端的地址信息     struct sockaddr_in newClientAddr;     memset(&newClientAddr, 0, sizeof(newClientAddr));     int len_NewClient = sizeof(newClientAddr);          int newfd;//存储每一连接成功的客户端对应的套接字          while(1)     {         //等待就绪事件发生         int numEventsReady = epoll_wait(epfd, events, MAXEVENTS, -1);//-1代表阻塞等待         if(-1 == numEventsReady)         {             perror("epoll_wait error");             return -1;         }         printf("有事件就绪,准备处理...\n");                  //根据返回值numEventsReady去遍历数组events         int i;         for(i=0; i<numEventsReady; i++)         {             //判断事件类型(监听套接字就绪or通信套接字就绪)             if(listenfd == events[i].data.fd)             {                 //有新的客户端发来了连接请求                 //获取与新的客户端通信的套接字newfd;                 newfd = accept(events[i].data.fd, (struct sockaddr *)&newClientAddr, &len_NewClient);                 if(newfd < 0)                 {                     perror("accept new client error");                     return -1;                 }                 printf("新客户端newfd = %d连接服务器成功, IP = %s\tPort = %d\n", \                       newfd, inet_ntoa(newClientAddr.sin_addr), ntohs(newClientAddr.sin_port));                                  //将新的newfd注册                 event.events = EPOLLIN;//EPOLLIN表示对于其可读事件感兴趣  默认为水平触发                 event.data.fd = newfd;                 if(epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &event) == -1)                 {                     perror("add newfd error");                     return -1;                 }                 printf("注册newfd = %d 到epoll实力中 ok!\n", newfd);             }             else             {                 //有客户端需要业务处理 				printf("客户端newfd = %d发来了业务,请求处理~\n", events[i].data.fd);                 if(server_com_func(events[i].data.fd) < 0)                 {                     //将该客户端的套接字从epoll实例(红黑树中)删除                     if(epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &events[i]) == -1)                     {                         perror("delete error");                         return -1;                     }                     printf("从epoll实例中删除fd = %d的客户端套接字成功!\n", events[i].data.fd);                     //关闭套接字                     close(events[i].data.fd);                 }             }         }     } 	     return 0;  } 	 

恁可以在Linux下建立一个epoll文件,再创建两个子文件。一个为server文件夹,存放server.c和net.h;另一个为client文件夹,存放client.c和net.h。

在server文件夹下,gcc server.c -o s,生成可执行文件s,再运行:./s 127.0.0.1 6666

127.0.0.1为ip 6666可以在1024~49151里随意选择。

在client文件夹下,gcc client.c -o c,生成可执行文件c,再运行:./c 127.0.0.1 6666

127.0.0.1为ip 6666可以在1024~49151里随意选择。

可以运行多个客户端,实现并发服务器。上面代码实现了文件的下载!

在客户端中使用download 文件名,下载server文件夹内已有的文件。

如图

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!