网络编程学习——IO多路复用

目录

​编辑

一,多路复用

1,IO的分类

 2,IO的效率

 二,Linux环境下实现通信的多路复用

1,select

select的特点:

参数:

操作函数:

返回值:

使用select实现网络通信:

2,poll

poll的特点:

poll的参数:

返回值:

 pollfd结构体:

使用poll进行网络通信:

3,epoll

epoll介绍:

epoll中的关键函数:


 

一,多路复用

1,IO的分类

在系统当中,IO的方式有多种。如:

1,阻塞式IO

2,非阻塞轮询式IO

3,多路复用/多路转接式IO

4,信号驱动式IO

5,异步IO

这些IO的基本使用方法,大家可以去搜索了解一下。今天我们来重点的谈谈多路复用IO。

 2,IO的效率

IO的本质其实就是等待+拷贝。拷贝一般都是要拷贝的,但是等待时间是可以减少的。减少等待时间的IO方式就是高级IO。减少等待时间也就意味着IO的效率的提高。

 二,Linux环境下实现通信的多路复用

要实现网络通信,要让一个服务端的能够对接多个客户端的请求。如何做到呢?多进程?多线程?以上的方式都可以做到。但是,可不可以就开一个进程来实现一个服务端给多个客户端进行服务呢?答案当然是可以的。解决方案就叫作多路复用。

1,select

select的原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
select的特点:

select函数就是为了实现多路复用而设计的,所以select可以一次性等待多个文件描述符

参数:

nfds:这个参数表示接收到的文件描述符+1。

所以在接收发来请求的文件描述符时我们需要去找到这些文件描述符的最大值。(所以文件描述符也要被管理起来,所以需要使用一个辅助数组)。

 readfds:设置为读状态的文件描述符便被设置在这个readfds位图内。

writefds:设置为写状态的文件描述符被设置进入这个位图内。

exceptfds:设置为异常状态的文件描述符被设置在这个位图内。

timeout:时间结构体对象,通过这个参数能够设置select等待的时间,一般这个参数被设置为nullptr表示阻塞式等待。

操作函数:

1,位图操作函数

 void FD_CLR(int fd, fd_set *set);// 将位图的某一个位置变成0.
int  FD_ISSET(int fd, fd_set *set);
//查看某一个文件描述符是否被设置
 void FD_SET(int fd, fd_set *set);
//设置某一个文件描述符到位图中
 void FD_ZERO(fd_set *set);
//将位图清零
返回值:

返回值为-1:代表等待失败

返回值为0:代表超过等待时间

返回值为其他数字:代表等待的文件描述符的个数

使用select实现网络通信:

1,准备工作

首先,按照规则我们得先把套接字创建出来然后bind并设置为监听状态。代码如下:

#include"log.hpp"
#include"Socket.hpp"
#include<iostream>class SelectServer
{public:SelectServer(){ }void Init(){Sock_.Sock();//创建套接字Sock_.Bind();//绑定套接字Sock_.Listen();//监听套接字}~SelectServer(){}private:Socket Sock_;};

当然,这里我使用了封装后的Socket类,在后面会有这个Socket类的源码。

在搭建好这个SelectServer服务器的框架后便可以开始启动服务器开始服务了。

2,Start函数

 void Start(){fd_set rfds;                     // 创建读状态位图fd_array[0] = Sock_.sockfd_;  //把0位置的值设为监听套接字while (1){FD_ZERO(&rfds);                 //清空位图FD_SET(Sock_.sockfd_, &rfds);  // 把监听套接字设置进入到rfds位图里面int max_fd = fd_array[0];for (int i = 0; i < max_num; i++) // 第二个循环,找到最大的fd,并且要将位图更新。{int fd = fd_array[i];if (fd == -1){continue;}FD_SET(fd, &rfds);if (fd > max_fd){max_fd = fd;lg(Info, "max_fd change");}}int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);switch (n)//根据返回值来判断做什么处理{case -1:lg(Info, "select err");break;case 0:lg(Info, "timeout");break;default:lg(Info, "get a link");//开始处理事件break;}}}

在Start函数这里,我们要做的便是要将select函数用起来。所以在前期会有很多的准备工作:

1,创建位图,将位图清空,还要将监听套接字设置到位图内。

2,要找到最大的文件描述符,就要循环遍历的找。所以需要一个辅助数组来存储历史上的文件描述符。

3,调用select接口后要根据返回值来决定执行的操作。

#注意位图的清空工作是要循环进行的,如果不循环进行就会发生错误,因为这里的位图是一个输入输出型参数,在select以后会改变。当然,我们也可以使用一个temp位图来接收rfds并代替rfds传入select中。这样也就不需要每次都要清空rfds了。

3,事件处理操作

void HandleEvents(fd_set &rfds) // 处理事件{for (int i = 0; i < max_num; i++)//循环遍历的把fd_array数组里面的描述符遍历一遍{int fd = fd_array[i];if (fd == defaultfd)//如果是-1那就继续遍历continue;if(FD_ISSET(fd,&rfds))//如果已经被设置到位图当中了{if (fd == Sock_.sockfd_) // 如果是监听套接字{std::string clientip;int port;int sock = Sock_.Accept(&clientip, &port);if (sock < 0){lg(Fatal, "accept err");continue;}lg(Info, "get a new link,ip:%s,port:%d", clientip.c_str(), port);int pos = 1;for (; pos < max_num; pos++){if (fd_array[pos] != -1){continue;}break;}if (pos == max_num){lg(Warning, "fd_arry is full");}else{fd_array[pos] = sock; // 加入到数组内}}else{// 开始读取char buffer[1024];//这里的读取是有bug的,因为可能会粘包或者读取不完整,要定制协议才能比较好的解决。int n = read(fd, buffer, sizeof(buffer));if (n == -1) // 读取失败{fd_array[i] = defaultfd; // 重新置为-1close(fd);lg(Warning, "read err");}else if (n == 0) // 客户端断开连接{fd_array[i] = defaultfd; // 重新置为-1close(fd);lg(Warning, "client break");}else{buffer[n] = 0;std::cout << "get a message: " << buffer << std::endl;}}}}}

 上述的事件处理操作只处理了读的情况,也可以自己写代码处理一下写的情况。

2,select的优缺点

 优点:

  select的优点便是可以等待多个文件描述符,这样便可以让accept不需要等待直接获取文件描述      符。减少等待时间,变成高级IO。

  缺点:

  1,select等待的文件描述符的个数是有上限的。因为select中的位图是一个结构体,在linux中这个位图能够放下的文件描述符最多为1024个。

  2,使用select时拷贝的次数过多。

2,poll

poll的特点:

为了解决select的等待的文件描述符数量有上限的问题,后面便发展出来了poll。poll也是一个实现多路复用的函数。并且,poll不需要多次的重置


#include <poll.h>int poll(struct pollfd fds[], nfds_t nfds, int timeout);
poll的参数:
  • fds[]:一个指向 struct pollfd 数组的指针,其中包含要监视的文件描述符以及它们关联的事件。
  • nfdsfds[] 数组中元素的数量。这个参数的类型其实就是一个整型变量,所以用户想填多少就填多少。而且,因为第一个参数其实是一个指针所以可以扩容,所以实现了可以无限的接收文件描述符的特点。
  • timeout:在毫秒级别指定 poll() 调用的超时时间。传递 -1 表示永远等待,传递 0 表示立即返回,传递正整数表示等待指定的毫秒数。
返回值:

  poll() 函数的返回值表示发生事件的文件描述符的数量,或者出现错误时返回 -1

 pollfd结构体:
struct pollfd {int fd;         // 要监视的文件描述符short events;   // 事件掩码(要监视的事件)short revents;  // 实际发生的事件(由内核填充)
};

参数:

  • fd:要监视的文件描述符。
  • events:要监视的事件掩码,可以是 POLLIN(可读事件)、POLLOUT(可写事件)、POLLERR(错误事件)等。
  • revents:由内核填充的实际发生的事件,可能是 POLLINPOLLOUTPOLLERR 等。、
使用poll进行网络通信:

这一份代码其实是在select的基础上改动的,所以不做仔细地介绍。体会一下polll的特点就可以了。代码:

#pragma once
#include "log.hpp"
#include "Socket.hpp"
#include<poll.h>  //引入头文件
#include <iostream>#define max_num 1024
#define defaultfd -1class SelectServer
{
public:SelectServer(){for (int i = 0; i < max_num; i++) // 初始化数组{rfds[i].fd = -1;}}void Init(){Sock_.Sock();   // 创建套接字Sock_.Bind();   // 绑定套接字Sock_.Listen(); // 监听套接字}void HandleEvents() // 处理事件{for (int i = 0; i < max_num; i++){int fd = rfds[i].fd;if (fd == defaultfd)continue;if (rfds[i].revents&POLLIN) // 如果事件已经是可读状态,内核告诉用户{if (fd == Sock_.sockfd_) // 如果是监听套接字{std::string clientip;int port;int sock = Sock_.Accept(&clientip, &port);if (sock < 0){lg(Fatal, "accept err");continue;}lg(Info, "get a new link,ip:%s,port:%d", clientip.c_str(), port);int pos = 1;for (; pos < max_num; pos++)//第三个循环{if (rfds[pos].fd != -1){continue;}break;}// std::cout << pos << std::endl;if (pos == max_num){lg(Warning, "fd_arry is full");}else{rfds[pos].fd = sock; // 加入到数组内rfds[pos].events = POLLIN;//设置事件为可读}}else{// 开始读取char buffer[1024];// std::cout << "read in" << std::endl;int n = read(fd, buffer, sizeof(buffer));if (n == -1) // 读取失败{rfds[i].fd = defaultfd; // 重新置为-1close(fd);lg(Warning, "read err");}else if (n == 0) // 客户端断开连接{rfds[i].fd = defaultfd; // 重新置为-1close(fd);lg(Warning, "client break");}else{buffer[n] = 0;std::cout << "get a message: " << buffer << std::endl;}}}}}void Start(){rfds[0].fd = Sock_.sockfd_;rfds[0].events = POLLIN;   while (1){int n = poll(rfds, max_num, 1000); // 时间填-1代表阻塞switch (n) // 根据返回值来判断做什么处理{case -1:lg(Info, "select err");break;case 0:lg(Info, "timeout");break;default:// 开始处理事件HandleEvents();break;}}}~SelectServer(){}private:Socket Sock_;struct pollfd rfds[max_num];//加入一个结构体类型数组
};

#注意:文件描述符是否就绪时内核告诉用户的,所以使用的是revent,不是event

rfds[i].revents&POLLIN

3,epoll

epoll介绍:

epoll是在select和poll的基础上发展起来的。epoll可以解决

epoll中的关键函数:

1,int epoll_create(int size);

作用:申请一个epoll的空间。

返回值:返回一个文件描述符,这个文件描述符便标识了这个epoll空间。

参数:szie表示用户需要申请空间的大小,但是在现在的epoll_create中这个参数已经无效。

2, int epoll_wait(int epfd, struct epoll_event *even,int maxevents, int timeout);

作用:等待epoll事件在epoll实例当中发生,返回事件和文件描述符。

返回值:当返回值为-1时表示等待失败,返回值为0时表示等待超时,返回值为其它时表示就绪的文件描述符的个数。

参数:

epfd:epoll的标识符。

event:epoll事件,是一个结构体数组指针,在添加到epoll实例之前要设置文件fd和事件的状态。

maxevents:能等待的事件的最大个数。

timeout:最长等待时间。

使用这个函数等待的值都会被设置进入到events数组里面,在epoll的模型内部这个数组便是一个就绪队列。

问题:这个就绪队列是有最大长度的,如何保证我们的就绪队列不会爆范围呢?

: 这个就绪队列在读到最大值后就会返回,剩余的数据会在下一次读取时再读取。

3,epoll_ctl:   int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

作用:对epoll进行操作,操作的方式有三种:

EPOLL_CTL_ADD 

EPOLL_CTL_DEL

EPOLL_CTL_MOD

返回值:成功时返回数字0,失败时返回一个-1。

参数:

epfd:epoll的标识符。

op:操作,填上面三个选项。

fd:文件描述符。

event:事件,在添加之前要对这个事件进行设置(fd,events)。

这个函数是对epoll模型内的红黑树进行操作,会将事件放到红黑树的内部。是在对红黑树进行操作。

使用epoll进行网络通信代码:

#pragma once
#include "log.hpp"
#include "Socket.hpp"
#include <iostream>
#include <sys/epoll.h>#define num 128class EpollServer
{
public:EpollServer(){}void Init()//初始化{Sock_.Sock();Sock_.Bind();Sock_.Listen();epollfd_ = epoll_create(num); // 申请epoll空间。创建epoll模型if (epollfd_ < 0){lg(Fatal, "create epollfd_ err epollfd_:%d", epollfd_);return;}lg(Info, "create epollfd_ sucess!");}void Handeler()//开始处理事件{for (int i = 0; i < num;i++)//遍历数组{uint32_t fd = events_[i].data.fd;int eve = events_[i].events;if(eve&EPOLLIN)//如果是事件是读事件{if(fd == Sock_.sockfd_)//处理监听{std::string clientip;int clientport;int sock = Sock_.Accept(&clientip,&clientport);if(sock<0){lg(Fatal, "sock create err");continue;}lg(Info, "clientip:%s,clientport:%d", clientip.c_str(), clientport);//添加事件到epoll模型当中struct epoll_event eve;eve.data.fd = sock;eve.events = EPOLLIN;int n = epoll_ctl(epollfd_, EPOLL_CTL_ADD, sock, &eve);if(n<0){lg(Warning, "epoll_ctl err");}}else {char buffer[1024];int n = read(fd, buffer, sizeof(buffer) - 1);if(n == -1){lg(Warning, "read err");epoll_ctl(epollfd_, EPOLL_CTL_DEL, fd, events_);close(fd);}else if(n == 0){lg(Warning, "read err");epoll_ctl(epollfd_, EPOLL_CTL_DEL, fd, events_);close(fd);}else {buffer[n] = 0;std::cout << "get a message:" << buffer << std::endl;}}}}}void Start(){// 设置和添加监听套接字events_[0].data.fd = Sock_.sockfd_;events_[0].events = EPOLLIN;int n = epoll_ctl(epollfd_, EPOLL_CTL_ADD, Sock_.sockfd_, &events_[0]); for (;;){int n = epoll_wait(epollfd_, events_, 1024, -1); // 获取事件switch (n){case -1:lg(Fatal, "epoll_wait err");break;case 0:lg(Warning, "time out");default:// 处理事件Handeler();break;}}}~EpollServer(){Sock_.Close();}private:Socket Sock_;//套接字int epollfd_;//epoll的标识符struct epoll_event events_[num];//epoll结构体数组,epoll模型的红黑树也是这个类型
};

在这里,如果觉得使用epoll的接口难受的话也可以和我一样对epoll进行封装,得到一个Epoller类:

#pragma once
#include<sys/epoll.h>
#include"log.hpp"
#include<iostream>class Epoller
{static const int size = 128;public:Epoller(){epollfd = epoll_create(size);if(epollfd<0){lg(Error, "epoll create err");}else {lg(Info, "epoll create ok");}}int Epoller_Wait(){int n = epoll_wait(epollfd,events,size,-1);//等待epoll里面的事件就绪if(n>=0){lg(Info,"epoll wait sucess");}else{lg(Error, "epoll wait err");}return n;}int Epoll_Ctl(int oper,int sock,struct epoll_event* evs = nullptr){if(oper == EPOLL_CTL_DEL){int n = epoll_ctl(epollfd, oper, sock, nullptr);//删除掉某个文件描述符if(n == 0){lg(Info, "epoll delete success");}else{lg(Info, "epoll delete fail");}}else {int n = epoll_ctl(epollfd, oper, sock, evs); // 增加或修改掉某个文件描述符if (n == 0){lg(Info, "epoll  moodify success");}else{lg(Info, "epoll moodify success");}}}~Epoller(){if(epollfd>=0)//关掉文件描述符{close(epollfd);}}public:int epollfd;struct epoll_event events[size];//数组};

这样便可以在后面的使用当中更加方便了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/826707.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

B树和B+树试题解析

一、单项选择题 01&#xff0e;下图所示是一棵&#xff08;A ). A.4阶B树 B.3阶B树 C.4阶B树 D.无法确定 02.下列关于m阶B树的说法中&#xff0c;错误的是( C ). A.根结点至多有m棵子树 B.所有叶结点都在同一层次上 C.非叶结点至…

JAVAEE——IP协议

文章目录 IP协议IP协议报头格式IP协议报头的各个区段四位版本四位首部长度八位服务类型16位总长度16位标识&#xff0c;3位标志&#xff0c;13位片偏移八位生存时间八位协议 地址管理IP地址解决提议1&#xff1a;动态分配Ip地址解决提议2&#xff1a;NAT机制 IP协议 IP协议报头…

超越GPT-4V,苹果多模态大模型上新,神经形态计算加速MLLM(二)

上文介绍基于MINOnets神经网络架构加速多模态大模型的策略&#xff0c;本文将以Spinnaker2多核神经网络芯片EGRU架构为起点&#xff0c;覆盖存内计算架构&#xff0c;介绍新型计算架构在加速大模型推理的作用。SpiNNaker 2是一个设计用于大规模异步处理的多核神经形态芯片&…

openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置

文章目录 openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置267.1 操作步骤 openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置 本章节主要介绍openGauss数据库内核基于鲲鹏服务器和openE…

T3BI T3BI RS-232通讯操作指南与培训PPT课件

T3BI T3BI RS-232通讯操作指南与培训PPT课件

Python连接Oracle数据库问题解决及Linux服务器操作知识

背景说明 最近在做一个视频分析的项目&#xff0c;然后需要将视频分析的数据写入到oracle数据库&#xff0c;直接在服务器上测试数据库连接的时候出现了这个bug提示&#xff0c;自己通过不断的研究探讨&#xff0c;最终把这个问题成功进行了解决&#xff0c;在这里进行一下记录…

701强连通分量(python)

看见题目知道时间复杂度不超过&#xff08;mlogm&#xff09;。 这题用强连通分量 Tarjan 算法&#xff0c;强联通&#xff1a;对于任意两个点u和v&#xff0c;u可以到达v&#xff0c;v也可以到达u。这题需要考虑有重边&#xff0c;自环&#xff0c;同样别忘记可能会有两个点u…

Linux 操作系统编译器、静态库、动态库

1、编辑器 1.1、vim的安装 指令&#xff1a;sudo apt-get install vim 1.2 vim的使用 格式&#xff1a;vim 文件名 如果文件存在&#xff0c;只打开&#xff0c;文件不存在&#xff0c;创建并打开 vim的4中模式&#xff1a; 命令模式&#xff0c;插入模式&#xff0c;底行模…

Excel数据处理:高级筛选、查找定位、查找函数(VLOOKUP)

高级筛选 先去选中筛选区域 如果筛选的条件在同一行那么就是且的关系 如果筛选的条件不在同一行那么就是或的关系 查找定位空值 使用VLOOKUP函数

渗透测试入门教程,从零基础入门到精通(非常详细)

目录 什么是渗透测试 渗透测试的重要性 渗透测试的前置技能 开始入门学习路线 什么是渗透测试 渗透测试&#xff0c;通常被视为模拟黑客的一种安全评估行为&#xff0c;其目的在于全面挖掘目标网站或主机的潜在安全漏洞。与真实的黑客攻击不同&#xff0c;渗透测试旨在发现…

Spring定时器 Cron表达式的用法

前言 Cron表达式是一种用于描述定时任务执行时间的字符串格式&#xff0c;这种表达式基于时间字段来定义任务应该在哪些时间点执行&#xff0c;通常包含六个或七个用空格隔开的字段&#xff0c;分别代表秒、分钟、小时、日期、月份和星期&#xff08;年份是可选的&#xff09;…

系统架构最佳实践 -- 相关JAVA架构

1. java 类加载器架构 2. JVM 架构 3. Java 技术体系 4. 线程运行架构 5. Java 体系&#xff08;编译与运行&#xff09;结构 6. JMS 技术架构 7. JMX 技术架构 8. Spring 架构 9. Hibernate 架构 10. ibatis 架构 11. Struts2 架构 12. Struts1 架构 13. JBPM 14. EJB 技术架构…

万字总结!Docker简介及底层关键技术剖析

本文首发在个人博客上&#xff1a;万字总结&#xff01;Docker简介及底层关键技术剖析 Docker 简介 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言 并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#x…

滚动条详解:跨平台iOS、Android、小程序滚动条隐藏及自定义样式综合指南

滚动条是用户界面中的图形化组件&#xff0c;用于指示和控制内容区域的可滚动范围。当元素内容超出其视窗边界时&#xff0c;滚动条提供可视化线索&#xff0c;并允许用户通过鼠标滚轮、触屏滑动或直接拖动滑块来浏览未显示部分&#xff0c;实现内容的上下或左右滚动。它在保持…

补档 -- 测试的分类(1)

最近有很多人私信我说: 灰灰你什么时候写测试分类阿, 本来我要开始肝性能测试的, 我一看, 奥, 之前摸鱼忘写了, 所以这里补档(叶问指着一边笑.jpg). 总览 标红的需要注意一下. 为什么要对软件测试进行分类? 软件测试是软件生命周期的一个重要环节, 具有较高的复杂性, 对于软…

排序 “叁” 之交换排序

目录 1. 基本思想 2.冒泡排序 2.1 基本思想 2.2 代码示例 2.3 冒泡排序的特性总结 3.快速排序 3.1 基本思想 &#x1f335;hoare版本 &#x1f335;挖坑法 ​编辑 &#x1f335;前后指针版本 ​编辑 3.2 快速排序优化 &#x1f33b;三数取中法选key 3.4 快速排序…

如何在群晖NAS部署office系统办公服务并实现无公网IP远程编辑文件

文章目录 本教程解决的问题是&#xff1a;1. 本地环境配置2. 制作本地分享链接3. 制作公网访问链接4. 公网ip地址访问您的分享相册5. 制作固定公网访问链接 本教程解决的问题是&#xff1a; 1.Word&#xff0c;PPT&#xff0c;Excel等重要文件存在本地环境&#xff0c;如何在编…

Umi.js:登录之后需要手动刷新权限菜单才能渲染

在使用Umi.js开发后台管理页面时&#xff0c;用户登录之后&#xff0c;总是需要手动刷新一次页面&#xff0c;才能够拿到全局状态/权限信息。 问题描述 结合使用umi/plugin-layout和umi/plugin-access&#xff0c;登录进入页面&#xff0c;配置的权限菜单未渲染&#xff0c;需…

javaWeb项目-大药房管理系统功能介绍

项目关键技术 开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 1、Java语言简介 Ja…

【已解决】win10系统 Docker 提示Docker Engine stopped解决全过程记录

【已解决】win10系统 Docker 提示Docker Engine stopped解决全过程记录 一、检查服务是否开启 找到 【Docker Desktop Service】&#xff0c;然后&#xff0c;启动他&#xff1b; 你也可以直接设置为“自动” 找到服务&#xff0c;右键》属性》启动类型&#xff1a;自动》点击…