Linux网络编程——多路复用之epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

目前epell是linux大规模并发网络程序中的热门首选模型。

epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

目录

  • Linux网络编程——多路复用之epoll
  • 基础API
  • 实例一、epoll实现在线聊天
  • 实例二、epoll实现在客户端断开后服务端能一直运行,客户端可以多次重连

基础API

  1. 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。

#include <sys/epoll.h>

int epoll_create(int size) size:监听数目

  1. 控制某个epoll监控的文件描述符上的事件:注册、修改、删除。

#include <sys/epoll.h>

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

epfd: 为epoll_creat的句柄

op: 表示动作,用3个宏来表示:

EPOLL_CTL_ADD (注册新的fd到epfd),

EPOLL_CTL_MOD (修改已经注册的fd的监听事件),

EPOLL_CTL_DEL (从epfd删除一个fd);

event: 告诉内核需要监听的事件

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;

EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

EPOLLOUT: 表示对应的文件描述符可以写

EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

EPOLLERR: 表示对应的文件描述符发生错误

EPOLLHUP: 表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

  1. 等待所监控文件描述符上有事件的产生,类似于select()调用。

#include <sys/epoll.h>

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

events: 用来存内核得到事件的集合,

maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

timeout: 是超时时间

-1: 阻塞

0: 立即返回,非阻塞

>0: 指定毫秒

返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

实例一、epoll实现在线聊天

tcp_server.c

#include <func.h>

int main(int argc,char* argv[])
{
	ARGS_CHECK(argc,3);
	int socketFd;
	socketFd = socket(AF_INET,SOCK_STREAM,0);
	ERROR_CHECK(socketFd, -1, "socket");
	struct sockaddr_in ser;
	bzero(&ser, sizeof(ser));
	ser.sin_family = AF_INET;
	ser.sin_port = htons(atoi(argv[2]));
	ser.sin_addr.s_addr = inet_addr(argv[1]);//点分十进制转为32位的网络字节序
	int ret;
	ret = bind(socketFd, (struct sockaddr*)&ser, sizeof(ser));
	ERROR_CHECK(ret, -1, "bind");
	listen(socketFd, 10);//缓冲区的大小,一瞬间能够放入的客户端连接信息
	int new_fd;
	struct sockaddr_in client;
	bzero(&client, sizeof(client));
	int addrlen = sizeof(client);
	new_fd = accept(socketFd, (struct sockaddr*)&client, &addrlen);
	ERROR_CHECK(new_fd, -1, "accept");
	printf("client ip=%s, port=%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
	char buf[128] = {0};
	int epfd = epoll_create(1);//参数size表示监听的数目大小
	ERROR_CHECK(epfd, -1, "epoll_create");
	struct epoll_event event, evs[2];
	event.events = EPOLLIN; //表示对应的文件描述符可读,监控读事件
	event.data.fd = STDIN_FILENO;
	ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);//一次注册永久生效
	ERROR_CHECK(ret, -1, "epoll_ctl");
	event.data.fd = new_fd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &event);
	int i, readyFdNum;
	while(1){
		memset(evs, 0, sizeof(evs));
		readyFdNum = epoll_wait(epfd, evs, 2, -1);//大小为2,-1表示永久阻塞,返回值是需要处理的事件数目
		for(i = 0;i < readyFdNum;i++){
			if(evs[i].data.fd == new_fd){
				bzero(buf, sizeof(buf));
				ret = recv(new_fd, buf, sizeof(buf), 0);
				ERROR_CHECK(ret, -1, "recv");
				if(ret == 0){
					printf("byebye!\n");
					goto chatOver;
				}			
				printf("%s\n", buf);
			}
			if(0 == evs[i].data.fd){
				memset(buf, 0, sizeof(buf));
				ret = read(STDIN_FILENO, buf, sizeof(buf));
				if(ret == 0){
					printf("byebye!\n");
					goto chatOver;
				}
				ret = send(new_fd, buf, strlen(buf) - 1, 0);
				ERROR_CHECK(ret, -1, "send");
			}
		}
	}
chatOver:
	close(new_fd);
	close(socketFd);
	return 0;
}

实例二、epoll实现在客户端断开后服务端能一直运行,客户端可以多次重连

#include <func.h>

int main(int argc,char* argv[])
{
	ARGS_CHECK(argc,3);
	int socketFd;
	socketFd = socket(AF_INET,SOCK_STREAM,0);
	ERROR_CHECK(socketFd, -1, "socket");
	struct sockaddr_in ser;
	bzero(&ser, sizeof(ser));
	ser.sin_family = AF_INET;
	ser.sin_port = htons(atoi(argv[2]));
	ser.sin_addr.s_addr = inet_addr(argv[1]);//点分十进制转为32位的网络字节序
	int ret;
	ret = bind(socketFd, (struct sockaddr*)&ser, sizeof(ser));
	ERROR_CHECK(ret, -1, "bind");
	listen(socketFd, 10);//缓冲区的大小,一瞬间能够放入的客户端连接信息
	int new_fd;
	struct sockaddr_in client;
	bzero(&client, sizeof(client));
	int addrlen = sizeof(client);
	char buf[128] = {0};
	int epfd = epoll_create(1);//参数size表示监听的数目大小
	ERROR_CHECK(epfd, -1, "epoll_create");
	struct epoll_event event, evs[3];
	event.events = EPOLLIN; //表示对应的文件描述符可读,监控读事件
	event.data.fd = STDIN_FILENO;
	ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);//一次注册永久生效
	ERROR_CHECK(ret, -1, "epoll_ctl");
	event.data.fd = socketFd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, socketFd, &event);
	int i, readyFdNum;
	while(1){
		memset(evs, 0, sizeof(evs));
		readyFdNum = epoll_wait(epfd, evs, 3, -1);//大小为3,-1表示永久阻塞,返回值是需要处理的事件数目
		for(i = 0;i < readyFdNum;i++){
			if(evs[i].data.fd == socketFd){
				addrlen = sizeof(client);
				new_fd = accept(socketFd, (struct sockaddr*)&client, &addrlen);
				ERROR_CHECK(new_fd, -1, "accept");
				printf("client ip=%s, port=%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
				event.data.fd = new_fd;
				ret = epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &event);
				ERROR_CHECK(ret, -1, "epoll_ctl");
			}
			if(evs[i].data.fd == new_fd){  //new_fd中有数据
				memset(buf, 0, sizeof(buf));
				ret = recv(new_fd, buf, sizeof(buf), 0);
				ERROR_CHECK(ret, -1, "recv");
				if(ret == 0){
					printf("byebye!\n");
					event.events = EPOLLIN;
					event.data.fd = new_fd;
					ret = epoll_ctl(epfd, EPOLL_CTL_DEL, new_fd, &event);
					//printf("line = %d\n", __LINE__);
					ERROR_CHECK(ret, -1, "epoll_ctl");
					continue;
				}
				printf("%s\n", buf);
			}
			if(0 == evs[i].data.fd){//标准输入
				memset(buf, 0, sizeof(buf));
				ret = read(STDIN_FILENO, buf, sizeof(buf));
				if(ret == 0){
					printf("byebye\n");
					goto chatOver;
				}
				ret = send(new_fd, buf, strlen(buf) - 1, 0);
				ERROR_CHECK(ret, -1, "send");
			}
		}
	}
chatOver:
	close(new_fd);
	close(socketFd);
	return 0;
}