Linux网络编程——C/C++Web服务器(二):IO多路复用select/poll/epoll实现服务器监听多客户端事件

本文主要是介绍Linux网络编程——C/C++Web服务器(二):IO多路复用select/poll/epoll实现服务器监听多客户端事件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

环境配置:windows电脑用户可以安装WSL配置Linux环境,并且安装vscode及wsl的插件通过vscode连接本机电脑的Linux。

前置内容:

Linux网络编程——C/C++Web服务器(一):不断创建新线程处理多客户端连接和通信-CSDN博客

目录

同步IO多路复用——使用select/poll/epoll实现服务器同时监听多客户端的事件,通过单线程循环处理事件

一、select监听多客户端的单线程服务器

服务器实现流程

使用客户端测试的结果

select存在的问题与适用场景

二、poll监听多客户端的单线程服务器

poll相对于select改进的地方

三、epoll监听多客户端的单线程服务器

epoll相对于select/poll改进的地方

epoll的核心函数

使用epoll监听多客户端单线程服务器流程

后续


同步IO多路复用——使用select/poll/epoll实现服务器同时监听多客户端的事件,通过单线程循环处理事件

 服务器功能:客户端连接服务器并发送数据,服务器端将小写字母转大写并返回给客户端。

一、select监听多客户端的单线程服务器

select相关函数:

void FD_ZERO(fd_set *set);清空文件描述符集合
void FD_SET(int fd, fd_set *set);将待监听的文件描述符加入监听集合中
void FD_CLR(int fd, fd_set *set);将监听集合中删除某个文件描述符
int FDISSET(int fd, fd_set *set);判断某个文件描述符是否在监听集合中
int select(int nfds, fd_set *readfds, fdset *writefds, fd_set *exceptfds, struct timeval *timeout);nfds:       监听的所有文件描述符中,最大的文件描述符+1readfds:    读 文件描述符的集合地址是传入传出参数,传入要监听的集合,返回有时间发生的集合(覆盖式)writefds:   写 文件描述符的集合地址是传入传出参数,可为NULLexceptfds:  异常 文件描述符集合地址是传入传出参数,可为NULL  timeout:    >0设置超时时长,0为非阻塞,NULL为阻塞监听返回值:     >0为监听到有事件发生的文件描述符个数

服务器实现流程

select可以实现在单进程中同时连接多个客户端,select可以同时监测多个客户端是否有事件发生,如果有事件发生则通过循环遍历rset集合来确定哪个客户端有事件发生,并处理发生的事件。

1.服务器创建socket、设置端口复用、绑定IP地址与端口号、设定服务器监听上限。(与上节内容一致)

// 初始化服务器,创建socket、绑定IP地址、端口号、设置端口复用
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
int cfd;
struct sockaddr_in address, temp_client_addr;
socklen_t addr_len = sizeof(temp_client_addr);
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(8000);char buf[16], ip_addr[16], read_buf[1024];
inet_ntop(AF_INET, &address.sin_addr, buf, sizeof(buf));
printf("net is: %s:\n", buf);int flag = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); // 端口复用
int ret = bind(listenfd, (const struct sockaddr *)&address, sizeof(address));
ret = listen(listenfd, 5);

2.初始化存储客户端文件描述符和地址的数组,并初始化cfd=-1为默认值。

// 定义用来存储客户端cfd和IP地址的结构体
struct ClientInfo {int cfd;struct sockaddr_in adress;
};// 初始化存储客户端cfd的数组,全部cfd置为-1
struct ClientInfo client_info[CLIENT_MAX_NUM];
for (int i = 0; i < 1024; i++) {client_info[i].cfd = -1;
}

3.初始化select的传入传出参数,rset是监听读事件的集合,执行完select会改变。

// 初始化rset与allset,其中rset是监听读事件的集合,是传入传出参数
fd_set rset, allset;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
int max_fd = listenfd;  // 最大监听文件描述符

4.单进程不断循环监听是否有客户端连接,如果有客户端请求连接,就建立连接,并向存储客户端文件描述符和地址的数组中存入:建立连接的cfd和请求连接客户端的地址。

while (1)
{rset = allset; // 因为每次rset作为传出参数,会变成有读事件的集合,因此要重新赋值为allsetint nready = select(max_fd + 1, &rset, NULL, NULL, NULL); // 设置阻塞监听if (nready < 0)perror("select error");if (FD_ISSET(listenfd, &rset)) {  // 有客户端连接cfd = accept(listenfd, (struct sockaddr *)&temp_client_addr, &addr_len);// 成功建立起通讯,将cfd加入allset集合中,下次监听FD_SET(cfd, &allset);max_fd = max_fd < cfd ? cfd : max_fd;  // 更新max_fd// 获取客户端的ip地址inet_ntop(AF_INET, &temp_client_addr.sin_addr, ip_addr, sizeof(ip_addr));printf("IP(%s) client is connected\n", ip_addr);// 将连接上的客户端的cfd和地址信息存入客户端数组中for (int j = 0; j < CLIENT_MAX_NUM; j++) {if (client_info[j].cfd == -1) {   // 寻找空位,进行存储client_info[j].cfd = cfd;client_info[j].adress.sin_addr = temp_client_addr.sin_addr;break;}}// 如果只监听到了一个事件,并且已经是listenfd了,就跳过循环if (nready == 1)continue;}

5.通过单进程不断循环查看rset集合中监听到有事件客户端的cfd,如果有写入数据,则进行小写转大写,否则就跳过。

    // 循环查看rset集合中监听到有事件的cfd,如果有,处理事件for (int i = listenfd + 1; i < max_fd + 1; i++) {// 此时的i就是cfdif (FD_ISSET(i, &rset)) {int ret = read(i, read_buf, sizeof(read_buf));if (ret < 0) {perror("Read error");exit(-1);}else if (ret == 0) {   // 如果客户端断开连接close(i);FD_CLR(i, &allset);// 遍历客户端数组,将指定cfd的位置重新置空,并打印xx客户端断开连接for (int j = 0; j < CLIENT_MAX_NUM; j++) {if (client_info[j].cfd == i) {   // 寻找cfd,进行置空client_info[j].cfd = -1;inet_ntop(AF_INET, &client_info[j].adress.sin_addr, ip_addr, sizeof(ip_addr));printf("IP(%s) client is closed\n", ip_addr);break;}}} else {     // 如果读取到数据// 将读取到的数据小写转大写for (int j = 0; j < ret; j++) {read_buf[j] = toupper(read_buf[j]);}write(i, read_buf, ret);  // 转换后的数据写回客户端}}}
}

 服务器端最终完整代码:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <ctype.h>#define CLIENT_MAX_NUM 1024// 定义用来存储客户端cfd和IP地址的结构体
struct ClientInfo {int cfd;struct sockaddr_in adress;
};int main(){// 初始化服务器,创建socket、绑定IP地址、端口号、设置端口复用int listenfd = socket(PF_INET, SOCK_STREAM, 0);int cfd;struct sockaddr_in address, temp_client_addr;socklen_t addr_len = sizeof(temp_client_addr);bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = htonl(INADDR_ANY);address.sin_port = htons(8000);char buf[16], ip_addr[16], read_buf[1024];inet_ntop(AF_INET, &address.sin_addr, buf, sizeof(buf));printf("net is: %s:\n", buf);int flag = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); // 端口复用int ret = bind(listenfd, (const struct sockaddr *)&address, sizeof(address));ret = listen(listenfd, 5);// 初始化存储客户端cfd的数组,全部cfd置为-1struct ClientInfo client_info[CLIENT_MAX_NUM];for (int i = 0; i < 1024; i++) {client_info[i].cfd = -1;}// 初始化rset与allset,其中rset是监听读事件的集合,是传入传出参数fd_set rset, allset;FD_ZERO(&allset);FD_SET(listenfd, &allset);int max_fd = listenfd;  // 最大监听文件描述符while (1){rset = allset; // 因为每次rset作为传出参数,会变成有读事件的集合,因此要重新赋值为allsetint nready = select(max_fd + 1, &rset, NULL, NULL, NULL); // 设置阻塞监听if (nready < 0)perror("select error");if (FD_ISSET(listenfd, &rset)) {  // 有客户端连接cfd = accept(listenfd, (struct sockaddr *)&temp_client_addr, &addr_len);// 成功建立起通讯,将cfd加入allset集合中,下次监听FD_SET(cfd, &allset);max_fd = max_fd < cfd ? cfd : max_fd;  // 更新max_fd// 获取客户端的ip地址inet_ntop(AF_INET, &temp_client_addr.sin_addr, ip_addr, sizeof(ip_addr));printf("IP(%s) client is connected\n", ip_addr);// 将连接上的客户端的cfd和地址信息存入客户端数组中for (int j = 0; j < CLIENT_MAX_NUM; j++) {if (client_info[j].cfd == -1) {   // 寻找空位,进行存储client_info[j].cfd = cfd;client_info[j].adress.sin_addr = temp_client_addr.sin_addr;break;}}// 如果只监听到了一个事件,并且已经是listenfd了,就跳过循环if (nready == 1)continue;}// 循环查看rset集合中监听到有事件的cfd,如果有,处理事件for (int i = listenfd + 1; i < max_fd + 1; i++) {// 此时的i就是cfdif (FD_ISSET(i, &rset)) {int ret = read(i, read_buf, sizeof(read_buf));if (ret < 0) {perror("Read error");exit(-1);}else if (ret == 0) {   // 如果客户端断开连接close(i);FD_CLR(i, &allset);// 遍历客户端数组,将指定cfd的位置重新置空,并打印xx客户端断开连接for (int j = 0; j < CLIENT_MAX_NUM; j++) {if (client_info[j].cfd == i) {   // 寻找cfd,进行置空client_info[j].cfd = -1;inet_ntop(AF_INET, &client_info[j].adress.sin_addr, ip_addr, sizeof(ip_addr));printf("IP(%s) client is closed\n", ip_addr);break;}}} else {     // 如果读取到数据// 将读取到的数据小写转大写for (int j = 0; j < ret; j++) {read_buf[j] = toupper(read_buf[j]);}write(i, read_buf, ret);  // 转换后的数据写回客户端}}}}close(listenfd);return 0;
}

使用客户端测试的结果

使用上节的Linux系统命令:nc 地址 端口号,测试服务器是否可以实现多客户端连接。测试结果如下所示,可以完美实现多客户端与服务器连接并实现通信:

select存在的问题与适用场景

1.循环遍历全部cfd,性能差。每次都需要循环遍历到最大的文件描述符+1的位置,如果许多客户端一直没有事件发生,只有个别活跃的客户端,则性能会差。

2.代码编写麻烦。因为rset作为传入传出参数,每次循环rset都会被改变,需要增加个额外的allset进行存储全部文件描述符。而且在使用过程中还需要调用FD_ZERO、FD_SET、FD_ISSET、FD_CLR这些函数,比较麻烦。

适用场景:

1.少量客户端连接,且客户端都很活跃。

2.对跨平台支持更好。

二、poll监听多客户端的单线程服务器

poll相对于select改进的地方

取消了fd_set类型,使用用pollfd类型的结构体数组,存储需要监听的客户端文件描述符、监听事件、监听结果的返回值。如果监听到了有事件发生,在pollfd类型的结构体中监听结果的返回值。

poll函数具体如下:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);fds是结构体数组bfds是监测数组的最大个数timeout是设置阻塞等待(为-1)、超时返回(>0)或不阻塞(为0)返回值:返回满足监听事件的个数。

pollfd结构体如下:

struct pollfd{int fd;           // 待监听的文件描述符short events;     // 待监听的事件:POLLIN、POLLOUT、POLLERRshort revents;    // 传入时设为0,如果满足监听的事件,传出时为(POLLIN、POLLOUT、POLLERR)
}

相比于select只是将传入传出参数rset给取消了,可以少定义一个allset。但是本质上仍然需要循环遍历所有的cfd,性能依然差。

由于poll并没什么大改进,基于poll实现监听多客户端的单线程服务器需要核心改动的地方省略。

三、epoll监听多客户端的单线程服务器

epoll相对于select/poll改进的地方

创建一棵红黑树,将文件描述符和监听事件存在红黑树上,阻塞等待如果有监听事件发生,返回在数组中。这样在后续处理事件时,避免了循环遍历全部已有的文件描述符,只需要遍历有监听事件发生的文件描述符数组,即可处理事件。

在大量客户端连接且少量客户端活跃的情况下(这是大部分应用场景),性能大幅提高。

epoll的核心函数

int epoll_create(int size);size:是创建红黑树监听节点的数量(供内核参考)返回值:指向创建的红黑树根节点的文件描述符fd。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epfd:  epoll_create函数的返回值,红黑树根节点文件描述符op:    对监听红黑树所作的操作EPOLL_CTL_ADD:  添加监听fdEPOLL_CTL_MOD:  修改监听fdEPOLL_CTL_DEL:  取消监听fdfd:    待监听的fdevent: 监听的事件,是struct epoll_event结构体events: EPOLLIN / EPOLLOUT / EPOLLERRdata: 联合体  int fd   对应监听事件的fd
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int);epfd:       epoll_create函数的返回值,红黑树根节点。events:     传出参数,是个数组,满足监听条件的文件描述符结构体数组。maxevents:  数组元素的总个数。例如1024,struct epoll_event events[1024];timeout:-1为阻塞,0为不阻塞,>0为超时时间(毫秒)。返回值:      > 0 是满足监听的总个数。

使用epoll监听多客户端单线程服务器流程

流程与seletc基本一致,完整代码如下:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <ctype.h>#define CLIENT_MAX_NUM 1024// 定义用来存储客户端cfd和IP地址的结构体
struct ClientInfo {int cfd;struct sockaddr_in adress;
};int main(){// 初始化服务器,创建socket、绑定IP地址、端口号、设置端口复用int listenfd = socket(PF_INET, SOCK_STREAM, 0);int cfd;struct sockaddr_in address, temp_client_addr;socklen_t addr_len = sizeof(temp_client_addr);bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = htonl(INADDR_ANY);address.sin_port = htons(8000);char buf[16], ip_addr[16], read_buf[1024];inet_ntop(AF_INET, &address.sin_addr, buf, sizeof(buf));printf("net is: %s:\n", buf);int flag = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); // 端口复用int ret = bind(listenfd, (const struct sockaddr *)&address, sizeof(address));ret = listen(listenfd, 5);// 初始化存储客户端cfd的数组,全部cfd置为-1struct ClientInfo client_info[CLIENT_MAX_NUM];for (int i = 0; i < 1024; i++) {client_info[i].cfd = -1;}// 创建用于epoll监听的红黑树int epfd = epoll_create(100);// 将listenfd加入红黑树中,监测客户端的连接struct epoll_event temp_event;temp_event.events = EPOLLIN;temp_event.data.fd = listenfd;epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &temp_event);struct epoll_event result_events[CLIENT_MAX_NUM];while (1){int nready = epoll_wait(epfd, result_events, CLIENT_MAX_NUM, -1); // 设置阻塞监听if (nready < 0)perror("select error");// 循环遍历result_events数组中监听到的事件的fdfor (int i = 0; i < nready; i++) {int now_fd = result_events[i].data.fd;   // 获取当前连接的fd为now_fd// 如果fd是listenfd,证明有新的客户端发起了连接if (now_fd == listenfd) {cfd = accept(listenfd, (struct sockaddr *)&temp_client_addr, &addr_len);// 成功建立起通讯,将cfd加入allset集合中,下次监听temp_event.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp_event);// 获取客户端的ip地址inet_ntop(AF_INET, &temp_client_addr.sin_addr, ip_addr, sizeof(ip_addr));printf("IP(%s) client is connected\n", ip_addr);// 将连接上的客户端的cfd和地址信息存入客户端数组中for (int j = 0; j < CLIENT_MAX_NUM; j++) {if (client_info[j].cfd == -1) {   // 寻找空位,进行存储client_info[j].cfd = cfd;client_info[j].adress.sin_addr = temp_client_addr.sin_addr;break;}}// 如果只监听到了一个事件,并且已经是listenfd了,就跳过循环if (nready == 1)continue;}// 如果不是listenfd,此时的now_fd就是cfdint ret = read(now_fd, read_buf, sizeof(read_buf));if (ret < 0) {perror("Read error");exit(-1);}else if (ret == 0) {   // 如果客户端断开连接close(now_fd);epoll_ctl(epfd, EPOLL_CTL_DEL, now_fd, NULL);  // 将当前cfd从红黑树中删除// 遍历客户端数组,将指定cfd的位置重新置空,并打印xx客户端断开连接for (int j = 0; j < CLIENT_MAX_NUM; j++) {if (client_info[j].cfd == now_fd) {   // 寻找cfd,进行置空client_info[j].cfd = -1;inet_ntop(AF_INET, &client_info[j].adress.sin_addr, ip_addr, sizeof(ip_addr));printf("IP(%s) client is closed\n", ip_addr);break;}}} else {     // 如果读取到数据// 将读取到的数据小写转大写for (int j = 0; j < ret; j++) {read_buf[j] = toupper(read_buf[j]);}write(now_fd, read_buf, ret);  // 转换后的数据写回客户端}}}close(listenfd);return 0;
}

此代码是在select代码基础上进行改动的,经测试,与select的效果一致,完美实现了epoll!

后续

后续将实现线程池的功能,让epoll监听到多个客户端有事件发生时不像当前单线程这样循环遍历处理事件,而是通过线程池分配线程去实现多客户端事件处理,大幅提升效率。

这篇关于Linux网络编程——C/C++Web服务器(二):IO多路复用select/poll/epoll实现服务器监听多客户端事件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1111521

相关文章

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

禁止平板,iPad长按弹出默认菜单事件

通过监控按下抬起时间差来禁止弹出事件,把以下代码写在要禁止的页面的页面加载事件里面即可     var date;document.addEventListener('touchstart', event => {date = new Date().getTime();});document.addEventListener('touchend', event => {if (new

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi