5.9-selcct_poll_epoll 和 reactor 的模拟实现

5.9-select_poll_epoll

本文演示 select 等 io 多路复用函数的应用方法,函数具体介绍可以参考我过去写的博客。

先绑定监听的文件描述符

c 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));

serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2052);

if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)))
{
    perror("bind");
    return -1;
}

listen(sockfd, 10);

1. select

select 函数适用于 win/linux 平台,但是使用时每次都需要检查位图内所有客户端的状态变化情况,属于针对于每一个文件进行处理而非针对事件处理,效率较低。打个比方:如果客户端是小区内的住户,那么 selcet 作为快递员,会从快递仓库中挑选出要被配送到该小区指定住户的快递,并对于每一个住户是否有快递/寄快递。

演示如下:

c 复制代码
fd_set rfds, rset;

FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);

int maxfd = sockfd;
while (1)
{
    rset = rfds;
    int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);

    if (FD_ISSET(sockfd, &rset))
    {

        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(struct sockaddr_in);

        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);

        printf("sockfd: %d\n", clientfd);

        FD_SET(clientfd, &rfds);
        maxfd = clientfd;

    }


    int i = 0;
    for (i = sockfd + 1; i <= maxfd; i++)
    {
        if (FD_ISSET(i, &rset))
        {
            char buffer[128] = { 0 };
            int count = recv(i, buffer, 128, 0);
            if (0 == count)
            {
                printf("disconnect\n");
                close(i);

                FD_CLR(i, &rfds);
                break;
            }

            send(i, buffer, count, 0);
            printf("clientfd: %d, count: %d, buffer: %s\n", i, count, buffer);

        }
    }
}

2. poll

poll 函数适用于 Linux 平台,效率相比于 select 有所提升。如果 selcet 是快递员要对于每一个住户确定一次需求,poll 则是可以直接锁定不同客户的需求。

演示如下:

c 复制代码
struct pollfd fds[1024] = { 0 };

fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;

int maxfd = sockfd;

while (1)
{
    int nready = poll(fds, maxfd + 1, -1);

    if (fds[sockfd].revents & POLLIN)
    {
        struct  sockaddr_in clientaddr;
        int len = sizeof(struct sockaddr_in);

        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);

        printf("hello, %d\n", clientfd);
        fds[clientfd].fd = clientfd;
        fds[clientfd].events = POLLIN;

        maxfd = clientfd;

    }
    int i = 0;
    for (i = sockfd + 1; i <= maxfd; i++)
    {
        if (fds[i].revents & POLLIN)
        {
            char buffer[128] = { 0 };
            int count = recv(fds[i].fd, buffer, 128, 0);
            if (count == 0)
            {
                printf("disconnect\n");
                close(i);
                fds[i].fd = -1;
                fds[i].events = 0;

                continue;
            }

            send(i, buffer, count, 0);
            printf("clientfd: %d, sount: %d, lbuffer: %s\n", i, count, buffer);

        }
    }
}

3.1 epoll

在支持 Linux 的函数中,epoll 是最高效的。还是上面的比方,epoll 则是在一定程度上结合了前两者的优点,并且底层使用红黑树,查找速度更快。

演示如下:

c 复制代码
int epfd = epoll_create(1);

struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;


epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

struct epoll_event events[1024] = { 0 };

while (1)
{
    int nready = epoll_wait(epfd, events, 1024, -1);

    int i = 0;
    for (i = 0; i < nready; i++)
    {
        int curfd = events[i].data.fd;
        if (sockfd == curfd)
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(struct sockaddr_in);

            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);

            ev.events = EPOLLIN;
            ev.data.fd = clientfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);

            printf("clientfd: %d\n", clientfd);
        }
        else if (events[i].events & EPOLLIN)
        {
            char buffer[10] = { 0 }; // 只要有数据就会一直触发,因此会回复多次
            int count = recv(curfd, buffer, 10, 0);
            if (count == 0)
            {
                printf("disconnect\n");
                close(curfd);
                epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);

                continue;
            }

            send(curfd, buffer, count, 0);
            printf("clientfd: %d, sount: %d, buffer: %s\n", curfd, count, buffer);
        }

    }

}

3.2 基于 epoll 模拟实现面对事件的 reactor 的底层原理

定义结构体,为了方便,直接把 epfd 定位全局变量

c 复制代码
#define BUFFER_LENGTH	1024

typedef int (* RCALLBACK)(int fd);

// save buffer data
struct conn_item
{
	int fd;
	char rbuffer[BUFFER_LENGTH];
	int rlen;
	char wbuffer[BUFFER_LENGTH];
	int wlen;

	union // 联合,在后续代码中会用到
	{
		RCALLBACK accept_callback;
		RCALLBACK recv_callback;
	}recv_t;
    
	RCALLBACK send_callback;
};

struct conn_item connlist[1024];

#if 1
int epfd;
#elif
struct reactor
{
	int epfd;
	struct conn_item connlist[1024];
};
#endif

reactor 的模拟借助以下回调函数实现,可以简化代码。

c 复制代码
int set_cb(int fd, int event, int flag);
int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);

具体实现如下,体现出面对事件的处理思想。

c 复制代码
/*
1. listenfd 触发 EPOLLIN 事件 -> 执行 accept_cb
2. client 触发 EPOLLIN 事件 -> recv_cb
3. client 触发 EPOLLOUT 事件 -> send_cb
*/

// ADD: flag == 1 else 0
int set_cb(int fd, int event, int flag)
{
	if (flag)
	{
		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;
	
	
		epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
		
	}
	else
	{
		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;

		epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);

	}
}


int accept_cb(int fd)
{
	struct sockaddr_in clientaddr;
	socklen_t len = sizeof(struct sockaddr_in);

	int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);

	set_cb(clientfd, EPOLLIN, 1);

	
	// set connlist
	connlist[clientfd].fd = clientfd;
	memset(connlist[clientfd].wbuffer, 0, BUFFER_LENGTH);
	connlist[clientfd].wlen = 0;
	memset(connlist[clientfd].rbuffer, 0, BUFFER_LENGTH);
	connlist[clientfd].rlen = 0;

	// set callback
	connlist[clientfd].recv_t.recv_callback = recv_cb;
	connlist[clientfd].send_callback = send_cb;

	
	printf("clientfd: %d\n", clientfd);
	return clientfd;
}

int recv_cb(int fd)
{
	char * buffer = connlist[fd].rbuffer;
	int index = connlist[fd].rlen;

	int count = recv(fd, buffer + index, BUFFER_LENGTH - index, 0);
	
	if (count == 0)
	{
		printf("disconnect\n");
		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
		close(fd);

		return -1;
	}

	connlist[fd].rlen += count;

#if ENABLE_HTTP_RESPONSE

// 此处可以自行修改,使对应不同输入实现特定输出,如 http 相应:
http_response(&connlist[fd]);


#else

// 不做处理直接发送返回
memcpy(connlist[fd].wbuffer, connlist[fd].rbuffer, connlist[fd].rlen);
connlist[fd].wlen = connlist[fd].rlen;

#endif

	// event
	set_cb(fd, EPOLLOUT, 0);

	return count;
}

int send_cb(int fd)
{
	char * buffer = connlist[fd].wbuffer;
	int index = connlist[fd].wlen;

	int count = send(fd, buffer, index, 0);


	// 事件来一次执行一次

	set_cb(fd, EPOLLIN, 0);

	return count;
}

mainloop 部分

c 复制代码
int main()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(struct sockaddr_in));

	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(2048);

	if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)))
	{
		perror("bind");
		return -1;
	}

	listen(sockfd, 10);

	connlist[sockfd].fd = sockfd;
	connlist[sockfd].recv_t.accept_callback = accept_cb;
	// epoll 边缘触发

	/*

	对于 IO 处理:随着时间增多会越来越复杂
	if(listenfd)
	{
	...
	}
	else // clientfd
	{
	...
	}

	对于事件处理 -> reactor 对事件反应更缓和
	if (events & EPOLLIN)
	{
	...
	}
	else if (events & EPOLLOUT)
	{
	...
	}

	*/

	epfd = epoll_create(1); // int size
	set_cb(sockfd, EPOLLIN, 1);

	struct epoll_event events[1024] = { 0 };

	while (1) // main loop
	{
		int nready = epoll_wait(epfd, events, 1024, -1);

		int i = 0;
		for (i = 0; i < nready; i++)
		{
			int curfd = events[i].data.fd;
            
            // 由于结构体内 accept_callback() 与 recv_callback() 共享同一块内存,故此处 if 条件判断可以省略。
            
			// if (sockfd == curfd)
			// {
			// 	// accept_cb()
			// 	// int clientfd = accept_cb(sockfd);
			// 	int clientfd = connlist[sockfd].recv_t.accept_callback(sockfd);
			// 	printf("client: %d\n", clientfd);
				
			// }
			if (events[i].events & EPOLLIN)
			{
				// int count = recv_cb(curfd);
				int count = connlist[curfd].recv_t.recv_callback(curfd);

				if (count != -1)
				printf("recv <- clientfd: %d, count: %d, buffer: %s\n", curfd, count, connlist[curfd].rbuffer);

			}
			else if (events[i].events & EPOLLOUT)
			{
				// int count = send_cb(curfd);
				int count = connlist[curfd].send_callback(curfd);
				printf("send -> clientfd: %d, count: %d, buffer: %s\n", curfd, count, connlist[curfd].wbuffer);

			}
			
		}

	}

	return 0;
}

附:本文的 http_response() 函数定义,以供测试使用。

c 复制代码
#include <time.h>

typedef struct conn_item connection_t;

int http_response(connection_t *conn)
{
    const char *html_body = "<html><head><title>chipen.com</title></head><body><h1>chipen</h1></body></html>";
    int content_length = strlen(html_body);

    // 生成符合 HTTP 标准格式的 Date 字符串
    time_t now = time(NULL);
    struct tm *gmt = gmtime(&now);
    char date_str[128];
    strftime(date_str, sizeof(date_str), "Date: %a, %d %b %Y %H:%M:%S GMT\r\n", gmt);

    // 构建完整的 HTTP 响应
    conn->wlen = snprintf(conn->wbuffer, BUFFER_LENGTH,
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html\r\n"
        "Content-Length: %d\r\n"
        "Accept-Ranges: bytes\r\n"
        "%s"
        "\r\n"  // Header 与 Body 的分隔符
        "%s",   // HTML 内容
        content_length,
        date_str,
        html_body
    );

    return conn->wlen;
}
相关推荐
南隅。26 分钟前
【Linux】用户管理
linux
云边有个稻草人1 小时前
【Linux系统】第四节—详解yum+vim
linux·vim·yum·软件包管理器·linux软件生态·linux编辑器-vim使⽤·yum具体操作
小陶来咯1 小时前
【高级IO】多路转接之单线程Reactor
服务器·网络·数据库·c++
dz小伟5 小时前
vim的配置
linux·编辑器·vim
云攀登者-望正茂5 小时前
AKS 网络深入探究:Kubenet、Azure-CNI 和 Azure-CNI(overlay)
网络·azure
时序数据说5 小时前
IoTDB磁盘I/O性能监控与优化指南
大数据·网络·数据库·时序数据库·iotdb
江湖人称-杰6 小时前
CentOS配置了镜像源之后依旧下载元数据失败
linux·运维·centos
阿运河7 小时前
如何配置 VScode 断点调试Linux 工程代码
linux·ide·vscode
cocogogogo7 小时前
Jupyter Notebook / Lab 疑难杂症记:从命令找不到到环境冲突与网络阻塞的排查实录
网络·ide·jupyter
Xudde.8 小时前
加速pip下载:永久解决网络慢问题
网络·python·学习·pip