Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
社区首页 >专栏 >C++实现epoll echo服务器

C++实现epoll echo服务器

原创
作者头像
evenleo
修改于 2020-09-17 02:49:50
修改于 2020-09-17 02:49:50
3.2K0
举报
文章被收录于专栏:C++的沉思C++的沉思

epoll简介

通常来说,实现处理tcp请求,为一个连接一个线程,在高并发的场景,这种多线程模型与Epoll相比就显得相形见绌了。epoll是linux2.6内核的一个新的系统调用,epoll在设计之初,就是为了替代select, poll线性复杂度的模型,epoll的时间复杂度为O(1), 也就意味着,epoll在高并发场景,随着文件描述符的增长,有良好的可扩展性。

  • selectpoll监听文件描述符list,进行一个线性的查找 O(n)
  • epoll: 使用了内核文件级别的回调机制O(1)

关键函数

  • epoll_create1: 创建一个epoll实例,返回文件描述符
  • epoll_ctl: 将监听的文件描述符添加到epoll实例中,实例代码为将标准输入文件描述符添加到epoll中
  • epoll_wait: 等待epoll事件从epoll实例中发生, 并返回事件以及对应文件描述符

epoll 关键的核心数据结构如下:

```cpp

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 /

};

```

epoll高效原理

epoll使用RB-Tree红黑树去监听并维护所有文件描述符,RB-Tree的根节点

调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.

epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已.

那么,这个准备就绪list链表是怎么维护的呢?

当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

epoll相比于select并不是在所有情况下都要高效,例如在如果有少于1024个文件描述符监听,且大多数socket都是出于活跃繁忙的状态,这种情况下,select要比epoll更为高效,因为epoll会有更多次的系统调用,用户态和内核态会有更加频繁的切换。

epoll高效的本质在于:

  • 减少了用户态和内核态的文件句柄拷贝
  • 减少了对可读可写文件句柄的遍历
  • mmap 加速了内核与用户空间的信息传递,epoll是通过内核与用户mmap同一块内存,避免了无谓的内存拷贝
  • IO性能不会随着监听的文件描述的数量增长而下降
  • 使用红黑树存储fd,以及对应的回调函数,其插入,查找,删除的性能不错,相比于hash,不必预先分配很多的空间

epoll实现echo server

借鉴TCP Echo Server Example in C++ Using Epoll的实现

代码语言:txt
复制
#ifndef __EPOLLER_H__
#define __EPOLLER_H__

#include <string>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <unordered_map>

#define MAX_PENDING 1024
#define BUFFER_SIZE 1024

class Handler {
public:
	virtual ~Handler() {}
	virtual int handle(epoll_event e) = 0;
};

/**
 * epoll 事件轮询
 */ 
class IOLoop {
public:
	static IOLoop *Instance()
	{
		static IOLoop instance;
		return &instance;
	}
	~IOLoop() 
	{
		for (auto it : handlers_) {
			delete it.second;
		}
	}

	void start()
	{
		const uint64_t MAX_EVENTS = 10;
		epoll_event events[MAX_EVENTS];
		while (true)
		{
			// -1 只没有事件一直阻塞
			int nfds = epoll_wait(epfd_, events, MAX_EVENTS, -1/*Timeout*/);
			for (int i = 0; i < nfds; ++i) {
				int fd = events[i].data.fd;
				Handler* handler = handlers_[fd];
				handler->handle(events[i]);
			}
		}
	}

	void addHandler(int fd, Handler* handler, unsigned int events)
	{
		handlers_[fd] = handler;
		epoll_event e;
		e.data.fd = fd;
		e.events = events;

		if (epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &e) < 0) {
			std::cout << "Failed to insert handler to epoll" << std::endl;
		}
	}

	void modifyHandler(int fd, unsigned int events) 
	{
		struct epoll_event event;
		event.events = events;
		epoll_ctl(epfd_, EPOLL_CTL_MOD, fd, &event);
	}

	void removeHandler(int fd) 
	{
		Handler* handler = handlers_[fd];
		handlers_.erase(fd);
		delete handler;
		//将fd从epoll堆删除
		epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, NULL);
	}

private:
	IOLoop()
	{
		epfd_ = epoll_create1(0);  //flag=0 等价于epll_craete
		if (epfd_ < 0) {
			std::cout << "Failed to create epoll" << std::endl;
			exit(1);
		}
	}

private:
	int epfd_;
	std::unordered_map<int, Handler*> handlers_;
};

class EchoHandler : public Handler {
public:
	EchoHandler() {}
	virtual int handle(epoll_event e) override
	{
		int fd = e.data.fd;
		if (e.events & EPOLLHUP) {
			IOLoop::Instance()->removeHandler(fd);
			return -1;
		}

		if (e.events & EPOLLERR) {
			return -1;
		}

		if (e.events & EPOLLOUT)
		{
			if (received > 0)
			{
				std::cout << "Writing: " << buffer << std::endl;
				if (send(fd, buffer, received, 0) != received)
				{
					std::cout << "Error writing to socket" << std::endl;
				}
			}

			IOLoop::Instance()->modifyHandler(fd, EPOLLIN);
		}

		if (e.events & EPOLLIN)
		{
			std::cout << "read" << std::endl;
			received = recv(fd, buffer, BUFFER_SIZE, 0);
			if (received < 0) {
				std::cout << "Error reading from socket" << std::endl;
			}
			else if (received > 0) {
				buffer[received] = 0;
				std::cout << "Reading: " << buffer << std::endl;
				if (strcmp(buffer, "stop") == 0) {
					std::cout << "stop----" << std::endl;
				}
			}

			if (received > 0) {
				IOLoop::Instance()->modifyHandler(fd, EPOLLOUT);
			} else {
				std::cout << "disconnect fd=" << fd << std::endl;
				IOLoop::Instance()->removeHandler(fd);
			}
		}

		return 0;
	}

private:
	int received = 0;
	char buffer[BUFFER_SIZE];
};

class ServerHandler : public Handler {
public:
	ServerHandler(int port)
	{
		int fd;
		struct sockaddr_in addr;
		memset(&addr, 0, sizeof(addr));

		if ((fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
		{
			std::cout << "Failed to create server socket" << std::endl;
			exit(1);
		}

		addr.sin_family = AF_INET;
		addr.sin_addr.s_addr = htonl(INADDR_ANY);
		addr.sin_port = htons(port);

		if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
		{
			std::cout << "Failed to bind server socket" << std::endl;
			exit(1);
		}

		if (listen(fd, MAX_PENDING) < 0)
		{
			std::cout << "Failed to listen on server socket" << std::endl;
			exit(1);
		}
		setnonblocking(fd);

		IOLoop::Instance()->addHandler(fd, this, EPOLLIN);
	}

	virtual int handle(epoll_event e) override
	{
		int fd = e.data.fd;
		struct sockaddr_in client_addr;
		socklen_t ca_len = sizeof(client_addr);

		int client = accept(fd, (struct sockaddr*)&client_addr, &ca_len);

		if (client < 0)
		{
			std::cout << "Error accepting connection" << std::endl;
			return -1;
		}

		std::cout << "accept connected: " << inet_ntoa(client_addr.sin_addr) << std::endl;
		Handler* clientHandler = new EchoHandler();
		IOLoop::Instance()->addHandler(client, clientHandler, EPOLLIN | EPOLLOUT | EPOLLHUP | EPOLLERR);
		return 0;
	}

private:
	void setnonblocking(int fd)
	{
		int flags = fcntl(fd, F_GETFL, 0);
		fcntl(fd, F_SETFL, flags | O_NONBLOCK);
	}
};

#endif /* __EPOLLER_H__ */

启动一个服务只需如下:

代码语言:txt
复制
#include "Epoller.h"

int main(int argc, char** argv) {
	ServerHandler serverhandler(8877);
	IOLoop::Instance()->start();
	return 0;
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
epoll入门
epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数: 所用到的数据结构 typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __ui
李海彬
2018/03/22
8480
IO复用(Reactor模式和Preactor模式)——用epoll来提高服务器并发能力
上篇线程/进程并发服务器中提到,提高服务器性能在IO层需要关注两个地方,一个是文件描述符处理,一个是线程调度。 IO复用是什么?IO即Input/Output,在网络编程中,文件描述符就是一种IO操作。 为什么要IO复用? 1.网络编程中非常多函数是阻塞的,如connect,利用IO复用可以以非阻塞形式执行代码。 2.之前提到listen维护两个队列,完成握手的队列可能有多个就绪的描述符,IO复用可以批处理描述符。 3.有时候可能要同时处理TCP和UDP,同时监听多个端口,同时处理读写和连接
Aichen
2018/05/18
2K0
epoll使用详解
 Linux平台上传统的I/O复用模型有select和poll模型,但二者在解决大量并发请示时却表现不佳。与select/poll相比,epoll的优点体现在以下三个方面:
王亚昌
2018/08/03
3.7K2
多路IO—POll函数,epoll服务器开发流程
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
莫浅子
2023/11/01
3050
多路IO—POll函数,epoll服务器开发流程
C++ socket epoll初识
在这个连接的生命周期里,绝大部分时间都是空闲的,活跃时间(发送数据和接收数据的时间)占比极少,这样独占一个服务器是严重的资源浪费。事实上所有的服务器都是高并发的,可以同时为成千上万个客户端提供服务,这一技术又被称为IO复用。
SimpleAstronaut
2022/11/03
9470
C++ socket epoll初识
linux网络编程之socket(十三):epoll 系列函数简介、与select、poll 的区别
一、epoll 系列函数简介 #include <sys/epoll.h> int epoll_create(int size); int epoll_create1(int flags); i
s1mba
2017/12/28
2.1K0
epoll()函数总结
http://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html
bear_fish
2018/09/20
2K0
epoll()函数总结
Linux下的I/O复用与epoll详解
I/O多路复用有很多种实现。在linux上,2.4内核前主要是select和poll,自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术。尽管他们的使用方法不尽相同,但是本质上却没有什么区别。本文将重点探讨将放在EPOLL的实现与使用详解。
用户7678152
2020/09/16
2K0
【Linux】I/O多路复用-SELECT/POLL/EPOLL
I/O多路复用 前言 文本相关参考资料及部分内容来源 《Linux高性能服务器编程》 《TCP/IP网络编程》 《Linux/UNIX系统编程手册》 ---- I/O多路复用核心思想为,使用一个线程,来处理多个客户端的请求。 或者说,使用一个特殊的fd,监视多个fd。 使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。 通常,网络程序在下列情况下需要使用I/O多路复用技术。 客户端程序需要同时处理多个socket。 客户端程序要同时处理用户输入和网络连接。 TCP服务器要同
半生瓜的blog
2023/05/13
1K0
【Linux】I/O多路复用-SELECT/POLL/EPOLL
socket网络编程(四)——epoll多路复用问题
问大家一个问题,如果要设计一款有着千万级别并发的系统,你的客户端和服务端的网络通信底层该怎么设计?我在上一篇文章(socket网络编程(三)——select多路复用问题)中有说到用select可以实现IO多路复用,但是select的设计有瓶颈所在,超过十万的并发效率就非常慢。那么着又该怎么办呢?
一点sir
2024/01/10
3680
用C写一个web服务器(二) I/O多路复用之epoll
枕边书
2018/01/04
8770
用C写一个web服务器(二) I/O多路复用之epoll
select和epoll模型
转自https://www.cnblogs.com/lojunren/p/3856290.html
大学里的混子
2019/03/14
1.1K0
linux网络编程系列(九)--epoll的基本使用
完全靠内核驱动,只要某个文件描述符有变化,就会一直通知我们的应用程序,直到处理完毕为止。
cpp加油站
2021/04/16
7800
linux网络编程系列(九)--epoll的基本使用
linux 下经典 IO 复用模型 -- epoll 的使用
epoll 是 linux 内核为处理大批量文件描述符而对 poll 进行的改进版本,是 linux 下多路复用 IO 接口 select/poll 的增强版本,显著提高了程序在大量并发连接中只有少量活跃的情况下的CPU利用率。 在获取事件时,它无需遍历整个被侦听描述符集,只要遍历被内核 IO 事件异步唤醒而加入 ready 队列的描述符集合就行了。 epoll 除了提供 select/poll 所提供的 IO 事件的电平触发,还提供了边沿触发,,这样做可以使得用户空间程序有可能缓存 IO 状态,减少 epoll_wait 或 epoll_pwait 的调用,提高程序效率。
用户3147702
2022/06/27
7030
linux 下经典 IO 复用模型 -- epoll 的使用
UNIX网络编程学习指南--epoll函数
epoll是select/poll的强化版,都是多路复用的函数,epoll有了很大的改进。 epoll的功能 1、支持监听大数目的socket描述符 一个进程内,select能打开的fd是有限制的,有宏FD_SETSIZE设置,默认值是1024.z在某些时候,这个数值是远远不够用的。解决方法有两种,已是修改宏然后再重新编译内核,但与此同时会引起网络效率的下降;二是使用多进程来解决,但是创建多个进程是有代价的,而且进程间数据同步没有多线程间方便。而epoll没有这个限制,它所支持的最大FD上限远远大于
用户1174963
2018/01/17
1.2K0
epoll的使用实例
  在网络编程中通常需要处理很多个连接,可以用select和poll来处理多个连接。但是select都受进程能打开的最大文件描述符个数的限制。并且select和poll效率会随着监听fd的数目增多而下降。
xcywt
2022/05/09
7820
Linux内核编程--常见IO模型与select/poll/epoll编程
套接字上的数据传输分两步执行:第一步,等待网络中的数据送达,将送达后的数据复制到内核中的缓冲区。第二步,把数据从内核中的缓冲区拷贝到应用进程的缓冲区。整个过程的运行空间是从应用进程空间切换到内核进程空间然后再切换回应用进程空间。
Coder-ZZ
2022/06/23
1.4K0
Linux内核编程--常见IO模型与select/poll/epoll编程
详解I/O多路转接模型:select & poll & epoll
多路转接是IO模型的一种,这种IO模型通过select、poll或者epoll进行IO等待,可以同时等待多个文件描述符,当某个文件描述符的事件就绪,便会通知上层处理对应的事件。
二肥是只大懒蓝猫
2023/10/13
6570
详解I/O多路转接模型:select & poll & epoll
epoll使用具体解释(精髓)
在linux的网络编程中,非常长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。 相比于select,epoll最大的优点在于它不会随着监听fd数目的增长而减少效率。由于在内核中的select实现中,它是採用轮询来处理的,轮询的fd数目越多,自然耗时越多。而且,在linux/posix_types.h头文件有这种声明:
全栈程序员站长
2021/12/23
5010
《Linux高性能服务器编程》学习小结(1)
TCP客户端 // 定义 _GNU_SOURCE 是为了获得对 EPOLLRDHUP 即 TCP链接被对方关闭或者对方关闭了写操作的 这一事件类型的支持 #define _GNU_SOURCE 1 // 下面是系统调用需要依赖的头文件 #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #inc
wanyicheng
2021/01/31
2.8K0
相关推荐
epoll入门
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档