【Linux】IO多路转接——poll接口

目录

poll初识

poll函数

poll服务器

poll的优点

poll的缺点


poll初识

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

  • poll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪,和select的定位是一样的,适用场景也是一样的。

poll函数

poll函数

poll函数的函数原型如下:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明:

  • fds:一个poll函数监视的结构列表,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合。
  • nfds:表示fds数组的长度。
  • timeout:表示poll函数的超时时间,单位是毫秒(ms)。

参数timeout的取值:

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

返回值说明:

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

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

  • EFAULT:fds数组不包含在调用程序的地址空间中。
  • EINTR:此调用被信号所中断。
  • EINVAL:nfds值超过RLIMIT_NOFILE值。
  • ENOMEM:核心内存不足。

struct pollfd结构

struct pollfd结构当中包含三个成员:

  • fd:特定的文件描述符,若设置为负值则忽略events字段并且revents字段返回0。
  • events:需要监视该文件描述符上的哪些事件。
  • revents:poll函数返回时告知用户该文件描述符上的哪些事件已经就绪。

【Linux】IO多路转接——poll接口_第1张图片

events和revents的取值:

【Linux】IO多路转接——poll接口_第2张图片

这些取值实际都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。

【Linux】IO多路转接——poll接口_第3张图片

  • 因此在调用poll函数之前,可以通过或运算符将要监视的事件添加到events成员当中。
  • 在poll函数返回后,可以通过与运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪。

poll服务器

poll的工作流程和select是基本类似的,这里我们也实现一个简单poll服务器,该服务器也只是读取客户端发来的数据并进行打印。

PollServer类

PollServer类当中也只需要包含监听套接字和端口号两个成员变量,在poll服务器绑定时直接将IP地址设置为INADDR_ANY尽即可。

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

代码如下:

#pragma once

#include "socket.hpp"
#include 

#define BACK_LOG 5

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

 运行服务器

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

  • 首先,在poll服务器开始死循环调用poll函数之前,需要定义一个fds数组,该数组当中的每个位置都是一个struct pollfd结构,后续调用poll函数时会作为参数进行传入。先将fds数组当中每个位置初始化为无效,并将监听套接字添加到fds数组当中,表示服务器刚开始运行时只需要监视监听套接字的读事件。
  • 此后,poll服务器就不断调用poll函数监视读事件是否就绪。如果poll函数的返回值大于0,则说明poll函数调用成功,此时已经有文件描述符的读事件就绪,接下来就应该对就绪事件进行处理。如果poll函数的返回值等于0,则说明timeout时间耗尽,此时直接准备进行下一次poll调用即可。如果poll函数的返回值为-1,则说明poll调用失败,此时也让服务器准备进行下一次poll调用,但实际应该进一步判断错误码,根据错误码来判断是否应该继续调用poll函数。
     

代码如下:

#pragma once

#include "socket.hpp"
#include 

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

class PollServer{
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
public:
	void Run()
	{
		struct pollfd fds[NUM];
		ClearPollfds(fds, NUM, DFL_FD); //清空数组中的所有位置
		SetPollfds(fds, NUM, _listen_sock); //将监听套接字添加到数组中,并关心其读事件
		for (;;){
			switch (poll(fds, NUM, -1)){
			case 0:
				std::cout << "timeout..." << std::endl;
				break;
			case -1:
				std::cerr << "poll error" << std::endl;
				break;
			default:
				//正常的事件处理
				//std::cout<<"有事件发生..."<

事件处理

当poll检测到有文件描述符的读事件就绪,就会在其对应的struct pollfd结构中的revents成员中添加读事件并返回,接下来poll服务器就应该对就绪事件进行处理了,事件处理过程如下:

  • 首先遍历fds数组中的每个struct pollfd结构,如果该结构当中的fd有效,且revents当中包含读事件,则说明该文件描述符的读事件就绪,接下来就需要进一步判断该文件描述符是监听套接字还是与客户端建立的套接字。
  • 如果是监听套接字的读事件就绪,则调用accept函数将底层建立好的连接获取上来,并将获取到的套接字添加到fds数组当中,表示下一次调用poll函数时需要监视该套接字的读事件。
  • 如果是与客户端建立的连接对应的读事件就绪,则调用read函数读取客户端发来的数据,并将读取到的数据在服务器端进行打印。
  • 如果在调用read函数时发现客户端将连接关闭或read函数调用失败,则poll服务器也直接关闭对应的连接,并将该连接对应的文件描述符从fds数组当中清除,表示下一次调用poll函数时无需再监视该套接字的读事件。

代码如下:

#pragma once

#include "socket.hpp"
#include 

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

class PollServer{
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
public:
	void HandlerEvent(struct pollfd fds[], int num)
	{
		for (int i = 0; i < num; i++){
			if (fds[i].fd == DFL_FD){ //跳过无效的位置
				continue;
			}
			if (fds[i].fd == _listen_sock&&fds[i].revents&POLLIN){ //连接事件就绪
				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 (!SetPollfds(fds, NUM, sock)){ //将获取到的套接字添加到fds数组中,并关心其读事件
					close(sock);
					std::cout << "poll server is full, close fd: " << sock << std::endl;
				}
			}
			else if (fds[i].revents&POLLIN){ //读事件就绪
				char buffer[1024];
				ssize_t size = read(fds[i].fd, 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(fds[i].fd);
					UnSetPollfds(fds, i); //将该文件描述符从fds数组中清除
				}
				else{
					std::cerr << "read error" << std::endl;
					close(fds[i].fd);
					UnSetPollfds(fds, i); //将该文件描述符从fds数组中清除
				}
			}
		}
	}
private:
	bool SetPollfds(struct pollfd fds[], int num, int fd)
	{
		for (int i = 0; i < num; i++){
			if (fds[i].fd == DFL_FD){ //该位置没有被使用
				fds[i].fd = fd;
				fds[i].events |= POLLIN; //添加读事件到events当中
				return true;
			}
		}
		return false; //fds数组已满
	}
	void UnSetPollfds(struct pollfd fds[], int pos)
	{
		fds[pos].fd = DFL_FD;
		fds[pos].events = 0;
		fds[pos].revents = 0;
	}
};

说明一下:

  • 因为这里将fds数组的大小是固定设置的,因此在将新获取连接对应的文件描述符添加到fds数组时,可能会因为fds数组已满而添加失败,这时poll服务器只能将刚刚获取上来的连接对应的套接字进行关闭。

poll服务器测试

运行poll服务器时也需要先实例化出一个PollServer对象,对poll服务器进行初始化后就可以运行服务器了。

代码如下:

#include "poll_server.hpp"
#include 

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]);
	PollServer* svr = new PollServer(port);
	svr->InitPollServer();
	svr->Run();
	
	return 0;
}

因为我们编写的poll服务器在调用poll函数时,将timeout的值设置成了-1,因此运行服务器后如果没有客户端发来连接请求,那么服务器就会在调用poll函数后进行阻塞等待。

【Linux】IO多路转接——poll接口_第4张图片

当我们用telnet工具连接poll服务器后,poll服务器调用的poll函数在检测到监听套接字的读事件就绪后就会调用accept获取建立好的连接,并打印输出客户端的IP和端口号,此时客户端发来的数据也能够成功被poll服务器收到并进行打印输出。

【Linux】IO多路转接——poll接口_第5张图片

此外,poll服务器也是一个单进程服务器,但是它也可以同时为多个客户端提供服务。

【Linux】IO多路转接——poll接口_第6张图片

当服务器端检测到客户端退出后,也会关闭对应的连接,并将对应的套接字从fds数组当中清除。

【Linux】IO多路转接——poll接口_第7张图片

poll的优点

  • struct pollfd结构当中包含了events和revents,相当于将select的输入输出型参数进行分离,因此在每次调用poll之前,不需要像select一样重新对参数进行设置。
  • poll可监控的文件描述符数量没有限制。
  • 当然,poll也可以同时等待多个文件描述符,能够提高IO的效率。

说明一下:

  • 虽然代码中将fds数组的元素个数定义为1024,但fds数组的大小是可以继续增大的,poll函数能够帮你监视多少个文件描述符是由传入poll函数的第二个参数决定的。
  • 而fd_set类型只有1024个比特位,因此select函数最多只能监视1024个文件描述符。

poll的缺点

  • 和select函数一样,当poll返回后,需要遍历fds数组来获取就绪的文件描述符。
  • 每次调用poll,都需要把大量的struct pollfd结构从用户态拷贝到内核态,这个开销也会随着poll监视的文件描述符数目的增多而增大。
  • 同时每次调用poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。

你可能感兴趣的:(Linux,linux,网络,服务器,运维)