IO多路转接 ——— selectpollepoll

Posted 2021dragon

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IO多路转接 ——— selectpollepoll相关的知识,希望对你有一定的参考价值。

文章目录

I/O多路转接之select

select初识

select是系统提供的一个多路转接接口。

  • select系统调用可以让我们的程序同时监视多个文件描述符的上的事件是否就绪。
  • select的核心工作就是等,当监视的多个文件描述符中有一个或多个事件就绪时,select才会成功返回并将对应文件描述符的就绪事件告知调用者。

select函数

select函数

select函数的函数原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数说明:

  • nfds:需要监视的文件描述符中,最大的文件描述符值+1。
  • readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。
  • writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。
  • exceptfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪。
  • timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。

参数timeout的取值:

  • NULL/nullptr:select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
  • 0:selec调用后t进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。
  • 特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。

返回值说明:

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数。
  • 如果timeout时间耗尽,则返回0。
  • 如果函数调用失败,则返回-1,同时错误码会被设置。

select调用失败时,错误码可能被设置为:

  • EBADF:文件描述符为无效的或该文件已关闭。
  • EINTR:此调用被信号所中断。
  • EINVAL:参数nfds为负值。
  • ENOMEM:核心内存不足。

fd_set结构

fd_set结构与sigset_t结构类似,fd_set本质也是一个位图,用位图中对应的位来表示要监视的文件描述符。


调用select函数之前就需要用fd_set结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中,这个添加的过程本质就是在进行位操作,但是这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于对fd_set类型的位图进行各种操作。

如下:

void FD_CLR(int fd, fd_set *set);      //用来清除描述词组set中相关fd的位
int  FD_ISSET(int fd, fd_set *set);    //用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set);      //用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set);             //用来清除描述词组set的全部位

timeval结构

传入select函数的最后一个参数timeout,就是一个指向timeval结构的指针,timeval结构用于描述一段时间长度,该结构当中包含两个成员,其中tv_sec表示的是秒,tv_usec表示的是微秒。

socket就绪条件

读就绪

  • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于0。
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0。
  • 监听的socket上有新的连接请求。
  • socket上有未处理的错误。

写就绪

  • socket内核中,发送缓冲区中的可用字节数,大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
  • socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
  • socket使用非阻塞connect连接成功或失败之后。
  • socket上有未读取的错误。

异常就绪

  • socket上收到带外数据。
注:带外数据和TCP的紧急模式相关,TCP报头当中的URG标志位和16位紧急指针搭配使用,就能够发送/接收带外数据。

select基本工作流程

如果我们要实现一个简单的select服务器,该服务器要做的就是读取客户端发来的数据并进行打印,那么这个select服务器的工作流程应该是这样的:

  • 先初始化服务器,完成套接字的创建、绑定和监听。
  • 定义一个fd_array数组用于保存监听套接字和已经与客户端建立连接的套接字,刚开始时就将监听套接字添加到fd_array数组当中。
  • 然后服务器开始循环调用select函数,检测读事件是否就绪,如果就绪则执行对应的操作。
  • 每次调用select函数之前,都需要定义一个读文件描述符集readfds,并将fd_array当中的文件描述符依次设置进readfds当中,表示让select帮我们监视这些文件描述符的读事件是否就绪。
  • 当select检测到数据就绪时会将读事件就绪的文件描述符设置进readfds当中,此时我们就能够得知哪些文件描述符的读事件就绪了,并对这些文件描述符进行对应的操作。
  • 如果读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接,并将该连接对应的套接字添加到fd_array数组当中。
  • 如果读事件就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出。
  • 当然,服务器与客户端建立连接的套接字读事件就绪,也可能是因为客户端将连接关闭了,此时服务器应该调用close关闭该套接字,并将该套接字从fd_array数组当中清除,因为下一次不需要再监视该文件描述符的读事件了。

说明一下:

  • 因为传入select函数的readfds、writefds和exceptfds都是输入输出型参数,当select函数返回时这些参数当中的值已经被修改了,因此每次调用select函数时都需要对其进行重新设置,timeout也是类似的道理。
  • 因为每次调用select函数之前都需要对readfds进行重新设置,所以需要定义一个fd_array数组保存与客户端已经建立的若干连接和监听套接字,实际fd_array数组当中的文件描述符就是需要让select监视读事件的文件描述符。
  • 我们的select服务器只是读取客户端发来的数据,因此只需要让select帮我们监视特定文件描述符的读事件,如果要同时让select帮我们监视特定文件描述符的读事件和写事件,则需要分别定义readfds和writefds,并定义两个数组分别保存需要被监视读事件和写事件的文件描述符,便于每次调用select函数前对readfds和writefds进行重新设置。
  • 服务器刚开始运行时,fd_array数组当中只有监听套接字,因此select第一次调用时只需要监视监听套接字的读事件是否就绪,但每次调用accept获取到新连接后,都会将新连接对应的套接字添加到fd_array当中,因此后续select调用时就需要监视监听套接字和若干连接套接字的读事件是否就绪。
  • 由于调用select时还需要传入被监视的文件描述符中最大文件描述符值+1,因此每次在遍历fd_array对readfds进行重新设置时,还需要记录最大文件描述符值。

这其中还有很多细节,下面我们就来实现这样一个select服务器。

select服务器

Socket类

首先我们可以编写一个Socket类,对套接字相关的接口进行一定程度的封装,为了让外部能够直接调用Socket类当中封装的函数,于是将这些函数定义成了静态成员函数。

代码如下:

#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>

class Socket
public:
	//创建套接字
	static int SocketCreate()
	
		int sock = socket(AF_INET, SOCK_STREAM, 0);
		if (sock < 0)
			std::cerr << "socket error" << std::endl;
			exit(2);
		
		//设置端口复用
		int opt = 1;
		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
		return sock;
	
	//绑定
	static void SocketBind(int sock, int port)
	
		struct sockaddr_in local;
		memset(&local, 0, sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(port);
		local.sin_addr.s_addr = INADDR_ANY;
		
		socklen_t len = sizeof(local);

		if (bind(sock, (struct sockaddr*)&local, len) < 0)
			std::cerr << "bind error" << std::endl;
			exit(3);
		
	
	//监听
	static void SocketListen(int sock, int backlog)
	
		if (listen(sock, backlog) < 0)
			std::cerr << "listen error" << std::endl;
			exit(4);
		
	
;

SelectServer类

现在编写SelectServer类,因为我当前使用的是云服务器,所以编写的select服务器在绑定时不需要显示绑定IP地址,直接将IP地址设置为INADDR_ANY就行了,所以类当中只包含监听套接字和端口号两个成员变量。

  • 在构造SelectServer对象时,需要指明select服务器的端口号,当然也可以在初始化select服务器的时候指明。
  • 在初始化select服务器的时候调用Socket类当中的函数,依次进行套接字的创建、绑定和监听即可。
  • 在析构函数中可以选择调用close函数将监听套接字进行关闭,但实际也可以不进行该动作,因为服务器运行后一般是不退出的。

代码如下:

#pragma once

#include "socket.hpp"
#include <sys/select.h>

#define BACK_LOG 5

class SelectServer
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
public:
	SelectServer(int port)
		: _port(port)
	
	void InitSelectServer()
	
		_listen_sock = Socket::SocketCreate();
		Socket::SocketBind(_listen_sock, _port);
		Socket::SocketListen(_listen_sock, BACK_LOG);
	
	~SelectServer()
	
		if (_listen_sock >= 0)
			close(_listen_sock);
		
	
;

运行服务器

服务器初始化完毕后就应该周期性的执行某种动作了,而select服务器要做的就是不断调用select函数,当事件就绪时对应执行某种动作即可。

  • 首先,在select服务器开始死循环调用select函数之前,需要先定义一个fd_array数组,先把数组中所有的位置初始化为无效,并将监听套接字添加到该数组当中,fd_array数组当中保存的就是需要被select监视读事件是否就绪的文件描述符。
  • 此后,select服务器就不断调用select函数监视读事件是否就绪,每次调用select函数之前都需要重新设置readfds,具体设置过程就是遍历fd_array数组,将fd_array数组当中的文件描述符添加到readfds当中,并同时记录最大的文件描述符值maxfd,因为后续调用select函数时需要将maxfd+1作为第一个参数传入。
  • 当select函数返回后,如果返回值为0,则说明timeout时间耗尽,此时直接准备进行下一次select调用即可。如果select的返回值为-1,则说明select调用失败,此时也让服务器准备进行下一次select调用,但实际应该进一步判断错误码,根据错误码来判断是否应该继续调用select函数。
  • 如果select的返回值大于0,则说明select函数调用成功,此时已经有文件描述符的读事件就绪,接下来就应该对就绪事件进行处理。

代码如下:

#pragma once

#include "socket.hpp"
#include <sys/select.h>

#define BACK_LOG 5
#define NUM 1024
#define DFL_FD - 1

class SelectServer
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
public:
	void Run()
	
		fd_set readfds; //读文件描述符集
		int fd_array[NUM]; //保存需要被监视读事件是否就绪的文件描述符
		ClearFdArray(fd_array, NUM, DFL_FD); //将数组中的所有位置设置为无效
		fd_array[0] = _listen_sock; //将监听套接字添加到fd_array数组中的第0个位置
		for (;;)
			FD_ZERO(&readfds); //清空readfds
			//将fd_array数组当中的文件描述符添加到readfds当中,并记录最大的文件描述符
			int maxfd = DFL_FD;
			for (int i = 0; i < NUM; i++)
				if (fd_array[i] == DFL_FD) //跳过无效的位置
					continue;
				FD_SET(fd_array[i], &readfds); //将有效位置的文件描述符添加到readfds当中
				if (fd_array[i] > maxfd) //更新最大文件描述符
					maxfd = fd_array[i];
			
			switch (select(maxfd + 1, &readfds, nullptr, nullptr, nullptr))
				case 0:
					std::cout<<"timeout..."<<std::endl;
					break;
				case -1:
					std::cerr << "select error" << std::endl;
					break;
				default:
					//正常的事件处理
					std::cout<<"有事件发生..."<<std::endl;
					//HandlerEvent(readfds, fd_array, NUM);
					break;
			//end switch
		//end for
	
private:
	void ClearFdArray(int fd_array[], int num, int default_fd)
	
		for (int i = 0; i < num; i++)
			fd_array[i] = default_fd;
		
	
;

说明一下: 为了测试timeout不同取值时的不同效果,当有事件就绪时这里先只打印一句提示语句。

timeout测试

在运行服务器时需要先实例化一个SelectServer类对象,对select服务器进行初始化后就可以调用Run成员函数运行服务器了。

代码如下:

#include "select_server.hpp"
#include <string>

static void Usage(std::string proc)

	std::cerr << "Usage: " << proc << " port" << std::endl;

int main(int argc, char* argv[])

	if (argc != 2)
		Usage(argv[0]);
		exit(1);
	
	int port = atoi(argv[1]);

	SelectServer* svr = new SelectServer(port);
	svr->InitSelectServer();
	svr->Run();
	
	return 0;

由于当前服务器调用select函数时直接将timeout设置为了nullptr,因此select函数调用后会进行阻塞等待。而服务器在第一次调用select函数时只让select监视监听套接字的读事件,所以运行服务器后如果没有客户端发来连接请求,那么读事件就不会就绪,而服务器则会一直在第一次调用的select函数中进行阻塞等待。

当我们借助telnet工具向select服务器发起连接请求后,select函数就会立马检测到监听套接字的读事件就绪,此时select函数便会成功返回,并将我们设置的提示语句进行打印输出,因为当前程序并没有对就绪事件进行处理,此后每次select函数一调用就会检测到读事件就绪并成功返回,因此会看到屏幕不断打印输出提示语句。

如果服务器在调用select函数时将timeout的值设置为0,那么select函数调用后就会进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select检测后都会立即返回。

此时如果select监视的文件描述符上有事件就绪,那么select函数的返回值就是大于0的,如果select监视的文件描述符上没有事件就绪,那么select的返回值就是等于0的。

struct timeval timeout =  0, 0 ; //每隔0秒timeout一次
switch (select(maxfd + 1, &readfds, nullptr, nullptr, &timeout))
	case 0:
		std::cout << "timeout..." << std::endl;
		break;
	case -1:
		std::cerr << "select error" << std::endl;                                   
		break;
	default:
		//正常的事件处理                                                          
		std::cout << "有事件发生..." << std::endl;
		//HandlerEvent(readfds, fd_array, NUM);                                   
		break;

运行服务器后如果没有客户端发来连接请求,那么select服务器就会一直调用select函数进行轮询检测,但每次检测时读事件都不就绪,因此每次select函数的返回值都是0,因此就会不断打印“timeout…”提示语句。

当有客户端发来连接请求后,select在某次轮询检测时就会检测到监听套接字的读事件就绪,此时select函数便会成功返回,并将我们设置的提示语句进行打印输出。

如果服务器在调用select函数时将timeout的值设置为特定的时间值,比如我们这里将timeout的值设置为5秒,那么select函数调用后的5秒内会进行阻塞等待,如果5秒后依旧没有读事件就绪,那么select函数将会进行超时返回。

我们可以将select函数超时返回和成功返回时timeout的值进行打印,以验证timeout是一个输入输出型参数。

struct timeval timeout =  5, 0 ; //每隔5秒timeout一次
switch (select(maxfd + 1, &readfds, nullptr, nullptr, &timeout))
case 0:
	std::cout << "timeout: " << timeout.tv_sec << std::endl;
	break;
case -1:
	std::cerr << "select error" << std::endl;
	break;
default:
	//正常的事件处理                                                          
	std::cout << "有事件发生... timeout: " << timeout.tv_sec << std::endl;
	//HandlerEvent(readfds, fd_array, NUM);                                   
	break;

运行服务器后如果没有客户端发来连接请求,那么每次select函数调用5秒后都会进行超时返回,并且每次打印输出timeout的值都是0,也就意味着timeout的时间是被耗尽了的。

当有客户端发来连接请求后,在某次调用select函数时就会检测到监听套接字的读事件就绪,此时select函数便会成功返回,并将我们设置的提示语句进行打印输出。

因为当前程序并没有对就绪事件进行处理,因此在第一次select检测到读事件就绪后,之后每次select函数一调用就会检测到读事件就绪并成功返回,因此会看到屏幕不断打印输出提示语句,并且后续打印输出timeout的值都是4,表示本次select检测到读事件就绪时timeout的剩余时间为4秒。

因为timeout和readfds、writefds与exceptfds一样,它们都是输入输出型参数,因此如果要使用timeout参数,那么在每次调用select函数之前也都需要对timeout的值进行重新设置。

事件处理

当select检测到有文件描述符的读事件就绪并成功返回后,接下来就应该对就绪事件进行处理了,这里编写一个HandlerEvent函数,当读事件就绪后就调用该函数进行事件处理。

  • 在进行事件处理时需要遍历fd_array数组当中的文件描述符,依次判断各个文件描述符对应的读事件是否就绪,如果就绪则需要进行事件处理。
  • 当一个文件描述符的读事件就绪后,还需要进一步判断该文件描述符是否是监听套接字,如果是监听套接字的读事件就绪,那么就应该调用accept函数将底层的连接获取上来。但是光光调用accept将连接获取上来还不够,为了下一次调用select函数时能够让select帮我们监视新连接的读事件是否就绪,在连接获取上来后还应该将该连接对应的文件描述符添加到fd_array数组当中,这样在下一次调用select函数前对readfds重新设置时就能将该文件描述符添加进去了。
  • 如果是与客户端建立的连接对应的读事件就绪,那么就应该调用read函数读取客户端发来的数据,如果读取成功则将读到的数据在服务器端进行打印。如果调用read函数读取失败或者客户端关闭了连接,那么select服务器也应该调用close函数关闭对应的连接,但此时光光关闭连接也是不够的,还应该将该连接对应的文件描述符从fd_array数组当中清除,否则后续调用的select函数还会帮我们监视该连接的读事件是否就绪,但实际已经不需要了。

代码如下:

#pragma once

#include "socket.hpp"
#include <sys/select.h>

#define BACK_LOG 5
#define NUM 1024
#define DFL_FD - 1

class SelectServer
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
public:
	void HandlerEvent(const fd_set& readfds, int fd_array[], int num)
	
		for (int i = 0; i < num; i++)
			if (fd_array[i] == DFL_FD) //跳过无效的位置
				continue;
			
			if (fd_array[i] == _listen_sock&&FD_ISSET(fd_array[i], &readfds)) //连接事件就绪
				//获取连接
				struct sockaddr_in peer;
				memset(&peer, 0, sizeof(peer));
				socklen_t len = sizeof(peer);
				int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
				if (sock < 0) //获取连接失败
					std::cerr << "accept error" << std::endl;
					continue;
				
				std::string peer_ip = inet_ntoa(peer.sin_addr);
				int peer_port = ntohs(peer.sin_port);
				std::cout << "get a new link[" << peer_ip << ":" << peer_port << "]" << std::endl;

				if (!SetFdArray(fd_array, num, sock)) //将获取到的套接字添加到fd_array当中
					close(sock);
					std::cout << "select server is full, close fd: " << sock << std::endl;
				
			
			else if (FD_ISSET(fd_array[i], &readfds)) //读事件就绪
				char buffer[1024];
				ssize_t size = read(fd_array[i], buffer, sizeof(buffer)-1);
				if (size > 0) //读取成功
					buffer[size] = '\\0';
					std::cout << "echo# " << buffer << std::endl;
				
				else if (size == 0) //对端连接关闭
					std::cout << "client quit" << std::endl;
					close(fd_array[i]);
					fd_array[i] = DFL_FD; //将该文件描述符从fd_array中清除
				
				else
					std::cerr << "read error" << std::endl;
					close(fd_array[i]);
					fd_array[i] = DFL_FD; //将该文件描述符从fd_array中清除
				
			
		
	IO多路复用之selectpollepoll

IO多路复用之selectpollepoll之间的区别总结

IO多路复用之selectpollepoll之间的区别总结

python之IO多路复用——selectpollepoll详解

#导入Word文档图片# Linux下IO多路复用: Selectpollepoll

聊聊IO多路复用之selectpollepoll详解