当前位置: 首页 > article >正文

【Linux】第十七章 多路转接(select+poll+epoll)


文章目录

  • select
    • fd_set
    • struct timeval
    • 实例-select服务器
      • socket
      • 初始化
        • 服务器初始化
        • 位图初始化
      • 开始监听
      • 超时测试
      • 事件处理
    • select 的特点
  • poll
    • struct pollfd
      • 事件 events 和 revents
    • 实例-poll服务器
      • socket
      • 初始化
      • 开始监听
      • 事件处理
    • poll 的特点
  • epoll-重点
    • epoll_create
    • epoll_ctl
      • epoll_event
      • 事件
    • epoll_wait
    • epoll工作原理-重点
    • epoll服务器
      • socket
      • 初始化
      • 开始监听
      • 事件处理
    • epoll 特点
    • epoll工作方式
      • 水平触发 LT-Level Triggered
      • 边缘触发 ET-Edge Triggered
      • LT和ET
      • ET 和非阻塞


select

一个多路转接接口,同时监视多个文件描述符的上的事件是否就绪,核心工作就是等

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要监视的最大文件描述符+1,例如文件描述符有1,7,10,那么nfds设置为10+1=11
  • readfds:输入输出型参数,监视文件描述符的事件是否就绪,返回的读事件已经就绪。
  • writefds:输入输出型参数,监视文件描述符的事件是否就绪,返回的写事件已经就绪
  • exceptfds:输入输出型参数,监视文件描述符的异常事件是否就绪,返回的异常事件已经就绪
  • timeout:输入输出型参数,设置select的等待时间,返回时表示timeout的剩余时间
    • NULL/nullptr:永不超时
    • 0:非阻塞等待,仅检测文件描述符的状态,不管什么情况都会立即返回
    • 特定的时间值:在指定的时间内进行阻塞等待,有文件事件则返回,超出特定时间就会返回

返回值

  • 成功,则返回就绪的文件描述符个数

  • timeout时间耗尽,则返回0

  • 失败,则返回-1,同时错误码会被设置

    • EBADF:文件描述符为无效的或该文件已关闭

    • EINTR:此调用被信号所中断

    • EINVAL:参数nfds为负值

    • ENOMEM:核心内存不足

编写流程

  • select之前要进行所有参数重置FD_SET
  • 需要定义第三方数组保存合法fd更新fd,方便select批量处理
  • select之后要便利所有合法参数进行检测FD_ISSET

fd_set

一个位图结构,最多监控1024歌文件描述符

  • 输入:用户告诉操作系统,需要帮我监控那几个文件描述符,在需要监控的文件描述符上置 1
  • 输出:系统告诉用户,那些文件描述符的相关事件就绪了
void FD_CLR(int fd, fd_set *set);  // 清空位图中fd的位
int  FD_ISSET(int fd, fd_set *set);// 判断相关fd的位是否为真
void FD_SET(int fd, fd_set *set);  // 设置对应位置的fd的位
void FD_ZERO(fd_set *set);         // 清空整个位图

struct timeval

用来计算select 等待的时间是秒与微秒的和

struct timeval {
    long    tv_sec;         /* 秒 */
    long    tv_usec;        /* 微秒 */
};

实例-select服务器

  • 每次调用select函数之前都需要对readfds进行重新设置,定义fdsArray数组用于保存监听套接字和已经与客户端建立连接的套接字
  • 循环调用select函数,检测读事件是否就绪,如果就绪则执行对应的操作
  • 定义一个读文件描述符集readfds,并将fdsArray当中的文件描述符依次设置进readfds当中,表示让select帮我们监视这些文件描述符的读事件是否就绪
  • 就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接,并将该连接对应的套接字添加到fdsArray数组当中
  • 就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出

socket

先初始化服务器,完成套接字的创建、绑定和监听。

setsockopt函数是让端口可以被复用,适用于服务器快速重启的情况

#include <stdio.h>
#include <vector>
#include <unistd.h>
#include <iostream>
#include <fstream>
#include <set>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include <vector>
#include <queue>
#include <pthread.h>
#include <ctime>
#include<semaphore.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

using namespace std;
class Sock
{
public:
    //创建套接字
    static int SocketInit()
    {
        int sock  = socket(AF_INET, SOCK_STREAM, 0);
        if (sock  < 0)
        {
            exit(1);
        }
        // 设置端口复用
        int opt = 1;
        setsockopt(sock , SOL_SOCKET, SO_REUSEADDR , &opt, sizeof(opt));
        return sock ;
    }
    //绑定
    static void Bind(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;

        // 2.2 本地socket信息, 写入sock_对应的内核区域
        if (bind(sock, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            exit(2);
        }
    }
    //监听
    static void Listen(int sock, int backlog)
    {
        if (listen(sock, backlog) < 0)
        {
            exit(3);
        }
    }
    //获取连接
        static int Accept(int sock, std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        int serviceSock = accept(sock, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            // 获取链接失败
            return -1;
        }
        if(clientport) *clientport = ntohs(peer.sin_port);
        if(clientip) *clientip = inet_ntoa(peer.sin_addr);
        return serviceSock;
    }
};

初始化

服务器初始化

运行并指明端口号

static void Usage(std::string proc)
{
	std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 端口
int main(int argc, char *argv[])
{
	if (argc != 2)
	{
		Usage(argv[0]);
		exit(1);
	}
	// 初始化socket, 获取socket fd并绑定端口
	int listensock = Sock::SocketInit();
	Sock::Bind(listensock, atoi(argv[1]));
	Sock::Listen(listensock, 5); 
	//位图初始化
}
位图初始化

数组用于保存监听套接字和已经与客户端建立连接的套接字,初始化为-1,第一个下标设置为listensock

	//位图初始化
	int fdsArray[sizeof(fd_set) * 8] = {0}; // 保存需要被监视读事件是否就绪的文件描述符,最多1024
	int fdsArraySz = sizeof(fdsArray) / sizeof(fdsArray[0]);
	// 将数组里面的文件描述符都初始化为默认值, 并将第一个下标设置为listensocket
	for (int i = 0; i < fdsArraySz; i++)
	{
		fdsArray[i] = -1;
	}
	fdsArray[0] = listensock;
	// 开始监听

开始监听

  • 传入select函数的参数都是输入输出参数,所以每次调用select函数都要对参数重新设置
  • 每次调用select函数之前,需要对readfds进行重新设置,并将fdsArray当中的文件描述符依次设置进readfds当中
  • 循环开始就调用select函数,检测读事件是否就绪
  • 如果读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已经建立好的连接
	// 开始监听
	fd_set readfds;
	while (1)
	{
		int maxFd = -1;
		FD_ZERO(&readfds);// 清空位图
		// 设置超时时间为5秒
		struct timeval timeout;
		timeout.tv_sec = 5;
		timeout.tv_usec = 0;
		// 遍历全局数组, 将有效的fd都添加进去, 并更新maxfd
		for (int i = 0; i < fdsArraySz; i++)
		{
			// 1. 过滤不合法的fd
			if (fdsArray[i] == -1)
				continue;
			// 2. 添加所有的合法的fd到readfds中, 方便select统一进行就绪监听
			FD_SET(fdsArray[i], &readfds);
			if (maxFd < fdsArray[i])
			{
				// 3. 更新出fd最大值
				maxFd = fdsArray[i]; 
			}
		}
		// 调用select开始监听
		int sret = select(maxFd + 1, &readfds, nullptr, nullptr, &timeout);
		switch (sret)
		{
		case 0: // 等待超时
			cout << "time out ... : " << endl;
			break;
		case -1: // 等待失败
			cerr << errno << " : " << strerror(errno) << endl;
			break;
		default:
			// 等待成功,正常的事件处理
			cout << "wait success: " << sret << endl;
                	//HandlerEvent(listensock,readfds,fdsArraySz,fdsArray);
			break;
		}
	}

超时测试

运行服务器发现5秒超时跳出阻塞态

[aaa@VM-8-14-centos file]$ ./test 端口
time out ... :
time out ... :
time out ... :

使用 telnet 命令来链接当前服务,select 检测到 listensock 文件描述符就绪,会立刻返回

//第一个窗口
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
//第二个窗口
[aaa@VM-8-14-centos file]$ 
wait success: 1
wait success: 1

事件处理

HandlerEvent函数,当读事件就绪后就调用该函数进行事件处理

  • 遍历fdsArray数组当中的文件描述符,依次判断各个文件描述符对应的事件是否就绪
  • 处理已有连接,调用accept函数将底层的连接获取上来。将对应的文件描述符添加到fdsArray数组当中,托管给select函数
  • 处理新连接,调用read函数读取客户端发来的数据,如果读取成功则将读到的数据在服务器端进行打印。如果调用read函数读取失败或者客户端关闭了连接,那么select服务器也应该调用close函数关闭对应的连接,但此时光光关闭连接也是不够的,还应该将该连接对应的文件描述符从fdsArray数组当中清除

打印数组中的文件描述符

//打印数组中的文件描述符
static void ShowArray(int *arr, int num)
{
	cout << "当前合法sock list: ";
	for (int i = 0; i < num; i++)
	{
		if (arr[i] == -1)
			continue;
		else
			cout << arr[i] << " ";
	}
	cout << endl;
}

遍历整个链接数组,并判断当前位置是否是有效的文件描述符,无效直接跳过

static void HandlerEvent(int listensock, fd_set &readfds, int fdsArraySz, int *fdsArray)
{
	for (int i = 0; i < fdsArraySz; i++)//遍历所有参数
	{
		if (fdsArray[i] == -1)
			continue; // 跳过无效的位置
		//listensock
	}
}

处理新连接

  • 判断是否有在 select 中监听该文件描述符
  • 开始进行 accept 获取新的链接
  • 不能直接 read/write, 而是应该通过数组交付给 select 帮我们监听事件
		//处理新连接
		if (fdsArray[i] == listensock)
		{
			// 判断listensocket有没有事件监听
			if (!FD_ISSET(listensock, &readfds))
			{
				cerr << "listensocket not set in readfds" << endl;
				continue;
			}
			// 具有了一个新链接
			cout << "get new connection" << endl;
			string clientip;
			uint16_t clientport = 0;
			int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞
			if (sock < 0)
				return; // 出错了, 直接返回
			// 成功获取新连接
			cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;

			// 这里我们不能直接对这个socket进行独写, 因为新链接来了并不代表新数据一并过来了
			// 所以需要将新的文件描述符利用全局数组, 交付给select
			// select 帮我们监看socket上的读事件是否就绪
			int i = 0;
			for (i = 0; i < fdsArraySz; i++)
			{
				if (fdsArray[i] == -1)
					break;
			}
			// 达到上限了
			if (i == fdsArraySz)
			{
				cerr << "reach the maximum number of connections" << endl;
				close(sock);
			}
			else // 没有达到
			{
				fdsArray[i] = sock; // 新的链接, 插入到数组中, 下次遍历就会添加到select监看中
				ShowArray(fdsArray, fdsArraySz);
			}
		}
		// 处理已有连接

处理已有连接,当读事件就绪的时候,我们通过 read 读取已有的数据

		// 处理已有连接
		else
		{
			if (FD_ISSET(fdsArray[i], &readfds))
			{
				char buffer[1024];
				ssize_t s = read(fdsArray[i], buffer, sizeof(buffer)-1); // 不会阻塞
				if (s > 0)//读取成功
				{
					buffer[s] = 0;
					cout << "client[" << fdsArray[i] << "]# " << buffer << endl;
				}
				else if (s == 0) // 对端关闭
				{
					cout << "client[" << fdsArray[i] << "] quit, server close " << fdsArray[i] << endl;
					close(fdsArray[i]);
					fdsArray[i] = -1; // 去除对该文件描述符的select事件监听
					ShowArray(fdsArray, fdsArraySz);
				}
				else // 异常了
				{
					cout << "client[" << fdsArray[i] << "] error, server close " << fdsArray[i] << endl;
					close(fdsArray[i]);
					fdsArray[i] = -1; // 去除对该文件描述符的select事件监听
					ShowArray(fdsArray, fdsArraySz);
				}
			}
		}

测试

用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收

//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
wait success: 1
get new connection
new conn:127.0.0.1:54684 | sock: 4
当前合法sock list: 3 4 
wait success: 1
listensocket not set in readfds
client[4]# 123

//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123

select 的特点

  • 占用资源少
  • 每次循环都得遍历整个数组,效率较低
  • select可监控的文件描述符数量太少

poll

#include <poll.h>
int poll(struct pollfd *fds,  nfds_t nfds,  int timeout);
  • fds:一个poll函数监视的结构列表,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合

  • nfds:表示fds数组的长度

  • timeout:表示poll函数的超时时间,单位是毫秒

    • -1:阻塞等待

    • 0:非阻塞等待,仅检测文件描述符的状态,不管什么情况都会立即返回

    • 特定的时间值:在指定的时间内进行阻塞等待,有文件事件则返回,超出特定时间就会返回

返回值

  • 调用成功,则返回有事件就绪的文件描述符个数

  • timeout时间耗尽,则返回0

  • 调用失败,则返回-1

    • EFAULT:fds数组不包含在调用程序的地址空间中。

    • EINTR:此调用被信号所中断。

    • EINVAL:nfds值超过RLIMIT_NOFILE值。

    • ENOMEM:核心内存不足。

struct pollfd

struct pollfd {
    int   fd;         /* 文件描述符 fd */
    short events;     /* 用户告诉内核需要监看的事件 events */
    short revents;    /* 内核返回的就绪事件 revents */
};

事件 events 和 revents

事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux不支持)
POLLPRI高优先级数据可读,比如TCP带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入
POLLERR错误
POLLHUP挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件
POLLNVAL文件描述符没有打开

实例-poll服务器

socket

先初始化服务器,完成套接字的创建、绑定和监听。直接用select代码

初始化

不需要自己维护一个 int 文件描述符数组了,定义一个fds数组,该数组当中的每个位置都是一个struct pollfd结构

static void Usage(std::string proc)
{
	std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 端口
int main(int argc, char *argv[])
{
	if (argc != 2)
	{
		Usage(argv[0]);
		exit(1);
	}
	// 初始化socket, 获取socket fd并绑定端口
	int listensock = Sock::SocketInit();
	Sock::Bind(listensock, atoi(argv[1]));
	Sock::Listen(listensock, 5);
	// 位图初始化
	struct pollfd fdsArray[1024];
	for (int i = 0; i < 1024; i++)
	{
		fdsArray[i].fd = -1;
		fdsArray[i].events = 0;
		fdsArray[i].revents = 0;
	}
        //服务器刚开始运行时只需要监视监听套接字的读事件
	fdsArray[0].fd = listensock;
	fdsArray[0].events = POLLIN;
	// 开始监听

}

开始监听

poll服务器就不断调用poll函数监视读事件是否就绪

	// 开始监听
	while (1)
	{
		// 设置超时时间为5毫秒
		int timeout = 5;
		// 调用select开始监听
		int sret = poll(fdsArray, 1024, timeout);
		switch (sret)
		{
		case 0: // 等待超时
			cout << "time out ... : " << endl;
			break;
		case -1: // 等待失败
			cerr << errno << " : " << strerror(errno) << endl;
			break;
		default:
			// 等待成功,正常的事件处理
			cout << "wait success: " << sret << endl;
			HandlerEvent(listensock, fdsArray);
			break;
		}
	}

事件处理

遍历fds数组,跳过无效的位置,判断处理新连接还是旧连接

static void HandlerEvent(int listensock, struct pollfd *fds)
{
	for (int i = 0; i < 1024; i++) // 遍历所有参数
	{
		if (fds[i].fd == -1)
			continue; // 跳过无效的位置
		// 处理新连接
	}
}

处理新连接,调用accept函数将底层建立好的连接获取上来,并将获取到的套接字添加到fds数组当中

		// 处理新连接
		if (fds[i].fd == listensock && fds[i].revents == POLLIN)
		{
			// 具有了一个新链接
			cout << "get new connection" << endl;
			string clientip;
			uint16_t clientport = 0;
			int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞
			if (sock < 0)
				return; // 出错了, 直接返回
			// 成功获取新连接
			cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;

			int i = 0;
			for (i = 0; i < 1024; i++)
			{
				if (fds[i].fd == -1)
					break;
			}
			// 达到上限了
			if (i == 1024)
			{
				cerr << "reach the maximum number of connections" << endl;
				close(sock);
			}
			else // 没有达到
			{
				fds[i].fd = sock; // 将sock添加到数组中
				fds[i].events = POLLIN;
				fds[i].revents = 0;
			}
		}
		// 处理已有连接

处理旧连接,调用read函数读取客户端发来的数据,调用read函数失败则poll服务器也直接关闭对应的连接,并将该连接对应的文件描述符从fds数组当中清除

		// 处理已有连接
		else
		{
			if (fds[i].revents == POLLIN)
			{
				char buffer[1024];
				ssize_t s = read(fds[i].fd, buffer, sizeof(buffer) - 1); // 不会阻塞
				if (s > 0)// 读取成功
				{
					buffer[s] = 0;
					cout << "client:" << buffer << endl;
				}
				else if (s == 0) // 对端关闭
				{
					cout << "client quit" << endl;
					close(fds[i].fd);
					fds[i].fd = -1;
					fds[i].events = 0;
					fds[i].revents = 0;
				}
				else // 异常了
				{
					cerr << "read error" << endl;
					close(fds[i].fd);
					fds[i].fd = -1;
					fds[i].events = 0;
					fds[i].revents = 0;
				}
			}
		}

测试,用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收

//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
time out ... : 
wait success: 1
get new connection
new conn:127.0.0.1:54684 | sock: 4
time out ... : 
wait success: 1
client:123

//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123

poll 的特点

  • poll 没有最大文件描述符限制
  • poll 无需用户额外维护一个单独的文件描述符数组,直接沿用该结构体数组即可
  • 需要遍历检测,数据多的时候遍历时间长

epoll-重点

epoll_create

创建 epoll 文件句柄

#include <sys/epoll.h>
int epoll_create(int size);
  • size的值必须设置为大于0的值
  • 错误的时候返回 -1 并设置 errno, 正确的时候返回文件描述符
  • 错误的时候返回 -1 并设置 errno, 正确的时候返回文件描述符

epoll_ctl

对 epoll 中需要监看的文件描述符进行设置

#include <sys/epoll.h>
int epoll_ctl(int epfd,  int op,  int fd,  struct epoll_event *event);
  • epfd:epoll_create 的返回值

  • op:表示具体的动作

    • EPOLL_CTL_ADD:将新的文件描述符添加到 epfd 中

    • EPOLL_CTL_MOD:修改已有文件描述符的监听事件

    • EPOLL_CTL_DEL:删除已有文件描述符

  • fd:目标文件描述符

  • event:需要监视该文件描述符上的哪些事件

返回值

  • 函数调用成功返回0,调用失败返回-1

epoll_event

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

struct epoll_event {
    uint32_t     events;      /* 需要监视的事件 */
    epoll_data_t data;        /* 联合体结构,一般选择使用该结构当中的fd,表示需要监听的文件描述符 */
};

事件

events 可以是下面的这些选项

事件说明
EPOLLIN对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
EPOLLOUT对应的文件描述符可以写;
EPOLLPRI对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR对应的文件描述符发生错误;
EPOLLHUP对应的文件描述符被挂断;
EPOLLET将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的;
EPOLLONESHOT只监听一次事件, 当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要手动再次把这个 socket 加入到 EPOLL 队列里;

epoll_wait

收集在 epoll 监控的事件中,已经就绪的事件

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd:epoll_create 的返回值
  • events:是一个输出型参数,将已经就绪的事件拷贝到events数组当中
  • maxevents:events数组中的元素个数,不可以超过 epoll_create 的 size;
  • timeout:超时时间,也是毫秒(0 非阻塞,-1 阻塞 特定的时间值)

返回值

  • 成功返回 IO 事件就绪的文件描述符数量,0 代表超时,负数代表失败

epoll工作原理-重点

image-20230826160246637

调用 epoll_create 时,在底层创建一个epoll模型,内部包含了就绪队列和一个红黑树

  • 调用 epoll_ctl 的时候,红黑树存放需要监视的事件
  • 添加到红黑树中的事件都会与设备的网卡驱动程序建立回调消息,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中
  • 调用 epoll_wait的时候,就绪队列存放需要返回给用户的事件,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可,获取就绪事件的时间复杂度是 O(1)

epoll服务器

socket

先初始化服务器,完成套接字的创建、绑定和监听。直接用select代码

初始化

初始化socket和epoll初始化

static void Usage(std::string proc)
{
	std::cerr << "Usage: " << proc << " port" << std::endl;
}
//./test 8080
int main(int argc, char *argv[])
{
	if (argc != 2)
	{
		Usage(argv[0]);
		exit(1);
	}
	// 初始化socket, 获取socket fd并绑定端口
	int listensock = Sock::SocketInit();
	Sock::Bind(listensock, atoi(argv[1]));
	Sock::Listen(listensock, 5);
	// epoll初始化
	int epfd;
	epfd = epoll_create(256);
	if (epfd < 0)
	{
		std::cerr << "epoll_create error" << std::endl;
		exit(5);
	}
	// 开始监听
}

开始监听

  • 在epoll服务器开始死循环调用epoll_wait函数之前,需要先调用epoll_ctl将监听套接字添加到epoll模型当中
  • epoll服务器就不断调用epoll_wait函数监视读事件是否就绪,并返回就绪的文件描述符数量
  • epoll_wait函数的返回值等于0,则说明timeout时间耗尽,此时直接准备进行下一次epoll_wait调用即可
	// 开始监听
	struct epoll_event ev;
	ev.events = EPOLLIN;
	ev.data.fd = listensock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, listensock, &ev);
	while (1)
	{
		struct epoll_event revs[64];
		int sret = epoll_wait(epfd, revs, 64, -1);//返回就绪fd个数
		switch (sret)
		{
		case 0: // 等待超时
			cout << "time out ... : " << endl;
			break;
		case -1: // 等待失败
			cerr << errno << " : " << strerror(errno) << endl;
			break;
		default:
			// 等待成功,正常的事件处理
			cout << "wait success: " << sret << endl;
			HandlerEvent(revs, sret);
			break;
		}
	}

事件处理

遍历所有文件描述符,说明该文件描述符对应的读事件就绪,判断该文件描述符是监听套接字还是与客户端建立的套接字

static void HandlerEvent(struct epoll_event revs[], int sret, int listensock, int epfd)
{
	for (int i = 0; i < sret; i++) // 遍历所有文件描述符
	{
		int fd = revs[i].data.fd; // 就绪的文件描述符
		// 处理新连接
	}
}

处理新连接,调用accept函数将底层建立好的连接获取上来,调用epoll_ctl函数将获取到的套接字添加到epoll模型当中

		// 处理新连接
		if (fd == listensock && revs[i].events == EPOLLIN)
		{
			// 具有了一个新链接
			cout << "get new connection" << endl;
			string clientip;
			uint16_t clientport = 0;
			int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞
			if (sock < 0)
				return; // 出错了, 直接返回
			// 成功获取新连接
			cout << "new conn:" << clientip << ":" << clientport << " | sock: " << sock << endl;
			
			//将获取到的套接字添加到epoll模型中,并关心其读事件
			struct epoll_event ev;
			ev.events = EPOLLIN;
			ev.data.fd = sock;
			epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
		}
		// 处理已有连接

处理旧连接,调用read函数读取客户端发来的数据,调用read函数失败则调用epoll_ctl函数将该连接对应的文件描述符从epoll模型中删除

		// 处理已有连接
		else
		{
			if (revs[i].events == EPOLLIN)
			{
				char buffer[1024];
				ssize_t s = read(fd, buffer, sizeof(buffer) - 1); // 不会阻塞
				if (s > 0)												 // 读取成功
				{
					buffer[s] = 0;
					cout << "client:" << buffer << endl;
				}
				else if (s == 0) // 对端关闭
				{
					cout << "client quit" << endl;
					close(fd);
					epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
				}
				else // 异常了
				{
					cerr << "read error" << endl;
					close(fd);
					epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
				}
			}
		}

测试,用设备1启动服务器,用设备2进行连接后发送消息123,设备1成功接收

//设备1
[aaa@VM-8-14-centos file]$ ./test 8080
wait success: 1
get new connection
new conn:127.0.0.1:48782 | sock: 5
wait success: 1
client:123

//设备2
[aaa@VM-8-14-centos file]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
123

epoll 特点

  • 数据拷贝轻量:调用epoll_ctl处理文件描述符和事件,select和poll每次都需要重新将需要监视的事件从用户拷贝到内核
  • 事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中
  • 没有数量限制:文件描述符没有数量限制
  • 线程安全:有mutex 锁
  • 接口使用方便,虽然拆分了 3 个函数,但是每个函数的功能非常明确

epoll工作方式

水平触发 LT-Level Triggered

image-20240824174039249
  • 只要底层有事件就绪,epoll就会一直通知用户,只有所有的数据都被处理完毕,epoll 才不会继续通知
  • epoll默认状态下就是LT工作模式
  • select和poll其实就是工作是LT模式下的
  • 支持阻塞和非阻塞读写

边缘触发 ET-Edge Triggered

image-20240824174045934
  • 只有底层就绪事件数量发生变化的时候,epoll才会通知用户
  • 只有当电平由低变高的那一瞬间才会触发,将epoll改为ET工作模式,则需要在添加事件时设置EPOLLET选项
  • 在ET工作模式下,底层就绪事件发生变化的时候会通知用户,只有一次机会,所以收到事件后必须立即处理
  • ET 模式下 epoll_wait 返回的次数更少,所以 ET 的性能远高于 LT
  • 只支持非阻塞读写

LT和ET

  • ET 模式下,事件就绪就必须一次性处理完数据
  • ET 的代码复杂度会增加

ET 和非阻塞

为什么 ET 必须要将文件描述符设置成非阻塞呢?

答:ET模式只有在变化的时候才会通知用户,如果是阻塞模式,客户端通知服务器取数据,然后服务器只取部分数据,剩下缓冲区中数据没有取出,文件描述符不会返回给客户端,客户端没有收到应答而不会继续发送数据,所以必须要采用循环读取 + 非阻塞的方式来将缓冲区读完



http://www.kler.cn/a/281748.html

相关文章:

  • SpringBoot登录功能实现思路(验证码+拦截器+jwt)
  • pytest结合allure做接口自动化
  • Label-studio-ml-backend 和YOLOV8 YOLO11自动化标注,目标检测,实例分割,图像分类,关键点估计,视频跟踪
  • java8 快捷方式
  • 【动手学深度学习Pytorch】6. LeNet实现代码
  • 【ArcGISPro】使用AI模型提取要素-提取车辆(目标识别)
  • uniapp(微信小程序如何使用单选框、复选框)
  • DevExpress 表格再新增行后滚动条自动移动到新增行
  • 建筑业AI的崛起The Rise of AI and Machine Learning in Construction
  • Android Compose 下拉选择框 ExposedDropdownMenu下拉选择
  • 超越传统:探索Visual Basic在操作系统插件开发的新境界
  • 少儿编程Python系列课程——003python注释
  • Ubuntu 22安装和配置PyCharm详细教程(图文详解)
  • 歌曲分享平台|基于SprinBoot+vue的原创歌曲分享平台系统(源码+数据库+文档)
  • Android实现自定义方向盘-8自定义view的相关问题
  • KOLLMORGEN科尔摩根驱动器AKD-P00607-NBPN-0000
  • 三防平板:定制化服务的趋势——以智慧医疗为例
  • 【Java】—— Java面向对象基础:Java中类的构造器与属性初始化,Student类的实例
  • 一、基于Vue3的开发-环境搭建【pnpm】安装
  • Java-多线程IO工具类
  • Matlab矩阵基础操作
  • LLM大模型入门天花板!《大模型入门:技术原理与实战应用》一本书让你轻松入门大模型(附PDF)
  • 什么是Dropout在机器学习中?
  • JVM类加载机制—类加载器和双亲委派机制详解
  • easyExcel 导入时,校验每个单元格数据
  • C语言 之 自定义类型:结构体、结构体内存对齐、修改默认对齐参数 详细说明 可以来看看哟