IO多路复用(select/epoll)

目录

一、概念

二、语法

1.select

1.1 select函数的语法

1.2 文件描述符集合操作

1.3 select函数的优缺点

2.epoll

2.1 epoll语法

2.2 epoll的工作模式

2.3 epoll的优缺点

三、select服务端代码

四、epoll服务端代码

五、客户端代码


 

一、概念

IO多路复用是一种同步的I/O模型,它允许一个进程同时监视多个文件描述符,一旦某个文件描述符就绪(可以进行I/O操作),就能够通知程序进行相应的读写操作。

IO多路复用有三种实现方式:select、poll、epoll。


 

二、语法

1.select

select是最早的I/O多路复用技术之一,它使用fd_set数据结构来存储和跟踪文件描述符。

select是基于线性方式处理待检测集合的,因此每次都要遍历集合;对于返回的集合,还需要判断文件描述符是否就绪。

 

1.1 select函数的语法

select ( int nfds,

            fd_set *readfds,
            fd_set *writefds,
            fd_set *exceptfds,
            struct timeval *timeout );

  • nfds:监控的文件描述符集里最大文件描述符加1。
  • readfds:文件描述符集合,内核会监视此集合中的文件是否有数据可读,传入传出参数。
  • writefds:文件描述符集合,内核会监视此集合中的文件是否有数据可写,传入传出参数。
  • exceptfds:文件描述符集合,用于监视此集合中的文件是否有异常情况,传入传出参数。
  • timeout:设置为NULL:阻塞,若检测到就绪的文件描述符则返回其数量;设置时间:等待固定时间,若没有就绪的文件描述符则返回0。

 

1.2 文件描述符集合操作

  • FD_ZERO(fd_set *set):清空文件描述符集合。
  • FD_SET(int fd, fd_set *set):将文件描述符fd添加到集合set中。
  • FD_CLR(int fd, fd_set *set):从集合set中移除文件描述符fd。
  • FD_ISSET(int fd, fd_set *set):检查文件描述符fd是否在集合set中。

 

1.3 select函数的优缺点

  • 优点:适用于多种操作系统,具有较好的兼容性。
  • 缺点:当文件描述符数量较多时,效率会比较低。此外,select有一个固定的文件描述符数量限制,通常为1024。

 

2.epoll

epoll是一种高效且可扩展的I/O多路复用技术。epoll是基于红黑树来处理待检测集合的,使用回调机制,处理效率高;其返回的集合中的文件描述符都是就绪的,无需再次进行检测。

epoll主要通过以下三个函数来实现其功能:epoll_create、epoll_ctl、epoll_wait。

 

2.1 epoll语法

①epoll_create函数

epoll_create (int size);

创建一个epoll对象,返回一个文件描述符,用于后续的操作,参数size大于0即可。

②epoll_ctl函数

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

向epoll对象中添加、修改或删除文件描述符。

  • epfd:epoll_create函数返回的epoll文件描述符。
  • op:操作类型,EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或EPOLL_CTL_DEL(删除)。
  • fd:要监听的文件描述符。
  • event:指向epoll_event结构体的指针,用于指定要监听的事件类型。
epoll_event和epoll_data:

struct epoll_event
{
  uint32_t events;
  epoll_data_t data;
};

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
};

epoll_event结构体用于描述事件,成员events表示事件类型:

  • EPOLLIN:读事件,接收数据,检测读缓冲区,如果可读则该文件描述符已就绪。
  • EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写则该文件描述符已就绪。
  • EPOLLET:设置为边沿模式,默认是水平模式。

epoll_data为联合体,通常使用int类型的fd,用于存储发生对应事件的文件描述符。

 ③epoll_wait函数

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

等待epoll对象中的事件发生,返回发生事件的文件描述符集合。

  • epfd:epoll_create函数返回的epoll文件描述符。
  • events:一个数组,用来存储就绪的事件信息。
  • maxevents:数组events的大小,即最多可以返回多少个就绪的文件描述符。
  • timeout:表示epoll_wait阻塞等待的时间(毫秒)。若为负数则会阻塞,直到有事件就绪;若为0则不阻塞,立即返回当前已就绪的事件。

 

2.2 epoll的工作模式

①水平触发(LT)模式:

水平触发是epoll的默认工作模式。在这种模式下,当文件描述符上的事件就绪时,epoll_wait函数会返回,并且如果该事件没有被处理,epoll_wait函数会在下一次调用时再次返回,直到该事件被处理。例如,当一个套接字上有数据可读时,epoll_wait会返回,并且如果数据没有被读取,epoll_wait会在下一次调用时再次返回,直到数据被读取。

②边沿触发(ET)模式:

边沿触发模式下,epoll_wait函数只会在文件描述符的状态发生变化时返回。例如,当一个套接字上有数据可读时,epoll_wait会返回,但是如果数据没有被读取,epoll_wait不会在下一次调用时再次返回,直到有新的数据到达。因此在边沿触发模式下,程序需要在一次epoll_wait调用返回后,立即处理所有就绪的事件。

③总结:
  • 边沿触发模式通常比水平触发模式的性能更高,因为它调用epoll_wait函数的次数比较少。
  • 水平触发模式更适合处理阻塞式I/O,而边沿触发模式更适合处理非阻塞式I/O。

epoll在边沿模式下,必须将套接字设置为非阻塞模式,此时需要循环读取读缓冲区的数据,读取完后recv函数会返回-1,此时需要特殊处理。

 

2.3 epoll的优缺点

  • 优点:高效的事件驱动模型,支持大量并发连接,没有固定的文件描述符数量限制。
  • 缺点:仅适用于Linux系统,不具备跨平台性。

 

三、select服务端代码

#include
#include
#include
#include
#include
#include

using namespace std;



int main(int argc, char* argv[])
{
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	if (lfd == -1) {
		cerr << "error" << endl;
		return -1;
	}
	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(8080);
	local.sin_addr.s_addr = INADDR_ANY;
	if (bind(lfd, (struct sockaddr*)&local, sizeof(local)) == -1) {
		cerr << "error" << endl;
		return -1;
	}

	if (listen(lfd, 128) == -1) {
		cerr << "error" << endl;
		return -1;
	}

	fd_set readset;
	FD_ZERO(&readset);
	FD_SET(lfd, &readset);
	int maxfd = lfd;
	while (true) {
		fd_set tmp = readset;
		int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
		if (FD_ISSET(lfd, &tmp)) {
			int cfd = accept(lfd, NULL, NULL);
			FD_SET(cfd, &readset);
			maxfd = max(maxfd, cfd);
			cout << "connect: " << cfd << endl;
		}
		for (int i = 0; i <= maxfd; i++) {
			if (i != lfd && FD_ISSET(i, &tmp)) {
				char buffer[1024] = { 0 };
				int len = recv(i, buffer, sizeof buffer, 0);
				if (len == -1) {
					cerr << "error" << endl;
					return -1;
				}
				else if (len == 0) {
					cout << "client disconnect: " << i << endl;
					FD_CLR(i, &readset);
					close(i);
					break;
				}
				cout << buffer << endl;
				for (int i = 0; i < len; i++) {
					buffer[i] = toupper(buffer[i]);
				}
				len = send(i, buffer, strlen(buffer) + 1, 0);
				if (len == -1) {
					cerr << "error" << endl;
					return -1;
				}
			}
		}
	}
	close(lfd);

	return 0;
}

 

四、epoll服务端代码

#include
#include
#include
#include
#include
#include
#include
#include


using namespace std;


int main(int argc, char* argv[])
{
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	if (lfd == -1) {
		cerr << "error" << endl;
		return -1;
	}

	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(8080);
	local.sin_addr.s_addr = INADDR_ANY;
	if (bind(lfd, (struct sockaddr*)&local, sizeof(local)) == -1) {
		cerr << "error" << endl;
		return -1;
	}

	if (listen(lfd, 128) == -1) {
		cerr << "error" << endl;
		return -1;
	}

	int epfd = epoll_create(1);
	struct epoll_event event;
	event.events = EPOLLIN;
	event.data.fd = lfd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &event);

	struct epoll_event events[1024];

	while (true) {
		int fds = epoll_wait(epfd, events, 1024, -1);
		for (int i = 0; i < fds; i++) {
			int fd = events[i].data.fd;
			if (fd == lfd) {
				int cfd = accept(lfd, NULL, NULL);
				struct epoll_event client_event;
				int flag = fcntl(cfd, F_GETFL);
				flag |= O_NONBLOCK;
				fcntl(cfd, F_SETFL, flag);
				client_event.events = EPOLLIN | EPOLLET;
				client_event.data.fd = cfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &client_event);
				cout << "connect: " << cfd << endl;
			}
			else {
				while(true) {
					char buffer[128] = { 0 };
					int len = recv(fd, buffer, sizeof buffer, 0);
					if (len == 0) {
						cout << "client disconnect: " << fd << endl;
						epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
						close(fd);
						break;
					}
					else if (len > 0) {
						cout << buffer << endl;
						for (int i = 0; i < len; i++) {
							buffer[i] = toupper(buffer[i]);
						}
						len = send(fd, buffer, strlen(buffer) + 1, 0);
						if (len == -1) {
							cerr << "error" << endl;
							return -1;
						}
					}
					else if (len == -1) {
						if (errno = EAGAIN) {
							cout << "Data read complete." << endl;
							break;
						}
						else {
							cerr << "error" << endl;
							return -1;
						}
					}
				}
			}
		}
	}
	close(lfd);

	return 0;
}

 

五、客户端代码

#include
#include
#include
#include
#include
#include

using namespace std;


int main(void)
{

	int client_socket = socket(AF_INET, SOCK_STREAM, 0);
	if (client_socket == -1) {
		cerr << "error" << endl;
		return -1;
	}

	struct sockaddr_in target;
	target.sin_family = AF_INET;
	target.sin_port = htons(8080);
	inet_pton(AF_INET, "127.0.0.1", &target.sin_addr.s_addr);


	if (connect(client_socket, (struct sockaddr*)&target, sizeof target) == -1) {
		cerr << "error" << endl;
		close(client_socket);
		return -1;
	}

	while (true) {
		char buffer1[1024] = { 0 };
		cout << "enter: ";
		cin >> buffer1;
		send(client_socket, buffer1, strlen(buffer1), 0);

		char buffer2[1024] = { 0 };
		int ret = recv(client_socket, buffer2, sizeof buffer2, 0);
		if (ret <= 0) {
			cout << "server disconnect." << endl;
		}
		cout << buffer2 << endl;

	}
	close(client_socket);

	return 0;
}

 

参考内容:
爱编程的大丙

你可能感兴趣的:(c++,select,epoll,linux)