【Linux网络编程】IO多路转接之poll

poll

  • 1.poll初始
  • 2.poll函数接口
  • 3.poll服务器
  • 4.poll的优点缺点

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1.poll初始

poll也是一种linux中多路转接的方案。它所对应的多路转接方案主要是解决select两个问题。

  1. select的文件描述符有上限的问题
  2. select每次都要重新设置关心的fd

下面通过poll接口来认识它是怎么解决select的问题的。

2.poll函数接口

在这里插入图片描述

struct pollfd * fds:这里可以把它想象一个动态数组、数组或者new/malloc出来的结构体数组

nfds_t nfds:代表这个数组的长度

int timeout:纯输入型,时间单位ms

  1. 大于0:在timeout以内 阻塞,超过timeout非阻塞返回一次
  2. 等于0 :非阻塞
  3. 小于<0:阻塞

这个和select一模一样的意思。用起来更简单了。

返回值:同select一模一样

  1. 大于0:表示有几个fd就绪了
  2. 等于0:表示超时了
  3. 小于0:表示poll等待失败了

poll的作用和select一模一样:只负责等待!

在这里插入图片描述

这个struct pollfd 结构体 在传给poll表示 用户->内核

int fd:你要关心一下这个fd哦

short events:关心的是这个fd的什么事件。我们把对应的事件设置进events里

输入看:fd+events

当poll返回时这个struct pollfd 结构体 表示内核->用户

你要关心的fd上面的events中有那些事件已经就绪啦

short revents:就绪事件由revents返回

输出看:fd+revents

很显然这种设计解决了这样的问题:

  1. 输入输出分离!

现在,用户->内核,内核->用户,events和revents的分离!以前select就用一张位图表示不同含义,因为输入输出分离了所以决定了poll不需要对参数进行重新设定

events和revents类型是整数,对应的事件如下:

在这里插入图片描述

其中对我们来说常用的是POLLIN、POLLOUT、POLLERR ,这些都是大写的宏每一个占一个比特位,不同比特位表示不同事件。

所以用户->内核,只要将events设置成要关心的宏值,那么操作系统就帮我们进行关心了。当操作系统返回时只要把revents设置成对应的宏值,不就把那些事件就绪不就告诉我们了吗。

因为它的类型是short而没有用操作系统自己封装的各种各样的结构体,所以对于事件的设计,我们自己用户检测事件有没有设置或者就绪一定要由我们自己来做,按位与,按位或这样的操作。

  1. select等待fd有上限的问题

struct pollfd *fds不是一个数组吗,nfds_t nfds不就是该数组大小也就是上限吗,你怎么说poll解决了select等待fd上限的问题?

select是一个具体的数据类型fd_set,既然是一个具体的类型那就直接决定了数据类型大小只能由你的编译环境自己定,今天不一样了,因为这个数组由我们自己说的算!

3.poll服务器

前面不是写了select服务器吗,现在我们把它改成poll服务器

错误码封装

#pragma onceenum
{USAGG_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};

日志封装

#pragma once#include<iostream>
#include<string>
#include<stdio.h>
#include <cstdarg>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include<fstream>#define DEBUG  0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"const char* level_to_string(int level)
{switch(level){case DEBUG: return "DEBUG";case NORMAL: return "NORMAL";case WARNING: return "WARNING";case ERROR: return "ERROR";case FATAL: return "FATAL";}
}//时间戳变成时间
char* timeChange()
{time_t now=time(nullptr);struct tm* local_time;local_time=localtime(&now);static char time_str[1024];snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \local_time->tm_min, local_time->tm_sec);return time_str;
}void logMessage(int level,const char* format,...)
{//[日志等级] [时间戳/时间] [pid] [message]//[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
#define NUM 1024//获取时间char* nowtime=timeChange();char logprefix[NUM];snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());//char logconten[NUM];va_list arg;va_start(arg,format);vsnprintf(logconten,sizeof logconten,format,arg);std::cout<<logprefix<<logconten<<std::endl;};

套接字封装

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "err.hpp"using namespace std;class Sock
{const static int backlog = 32;public:static int sock(){// 1. 创建socket文件套接字对象int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", sock);int opt = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));return sock;}static void Bind(int sock,int port){// 2. bind绑定自己的网络信息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;if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");}static void Listen(int sock){// 3. 设置socket 为监听状态if (listen(sock, backlog) < 0) {logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}static int Accept(int listensock, std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(listensock, (struct sockaddr *)&peer, &len);if (sock < 0)logMessage(ERROR, "accept error, next");else{logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?*clientip = inet_ntoa(peer.sin_addr);*clientport = ntohs(peer.sin_port);}return sock;}
};

调用逻辑

#include "pollServer.hpp"
#include "err.hpp"
#include <memory>static void usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}string service(string request)
{return request;
}int main(int argc,char* argv[])
{if(argc != 2){usage(argv[0]);exit(USAGG_ERR);}unique_ptr<pollServer> usl(new pollServer(service,atoi(argv[1])));usl->initServer();usl->start();return 0;

今天poll服务器,也是需要一个数组。只不过以前select数组纯纯的保存文件描述符,poll这里必须是保存struct pollfd结构体的数组。

一般我们如果把fd设置为-1或者小于0的值,操作系统就不会关注这样的文件描述符了。它只会关心大于等于0的fd。

在这里插入图片描述

因此我们要重新定义一个指针,构造析构都跟着改一下

class pollServer
{static const int defaultport = 8080;static const int defaultfd = -1;static const int defaultnum=2048;using func_t=function<string(string)>;public:pollServer(func_t f,int port = defaultport) : _cbs(f),_port(port), _listensock(-1), _rfds(nullptr){}~pollServer(){if (_listensock != defaultfd)close(_listensock);if (_rfds)delete[] _rfds;}private:int _listensock;int _port;struct pollfd* _rfds;func_t _cbs;
};

接下来初始化服务器这里创建结构体数组大小自己随意定

void initServer()
{// 1.创建套接字_listensock = Sock::sock();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);_rfds=new struct pollfd[defaultnum];//大小这里自己随便定for (int i = 0; i < defaultnum; ++i)//数组初始化{_rfds[i].fd = defaultfd;_rfds[i].events=0;_rfds[i].revents=0;}_rfds[0].fd = _listensock; // 这个位置后面就不变了_rfds[0].events=POLLIN; //告诉内核帮我关心_listensock读事件
}

打印这里也改一下

void print()
{for (int i = 0; i < defaultnum; ++i){if (_rfds[i].fd != defaultfd)cout << _rfds[i].fd << " ";}cout << endl;
}

现在当我们启动服务之后,就不需要每次调用select之前都需要重新设置fd了添加到读文件描述符集里面了,然后才能添加到select里面。现在直接把数组给poll。所以能明显感觉到poll比select简单

void start()
{int timenout=1000;for (;;){int n=poll(_rfds,defaultnum,timenout);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "poll error, code: %d, err string: %s", errno, strerror(errno));break;default:// 说明有事件就绪了,目前只有一个监听事件就绪了logMessage(NORMAL, "have event ready!");HandlerEvent();//这里不用传了,因为就绪事件就在_rfds里break;}}
}

今天这里我们只处理读事件就绪的情况

// 1.handler event _rfds 中,不仅仅是有一个fd是就绪的,可能存在多个
// 2.我们的poll目前只处理了read事件
void HandlerEvent()
{// 你怎么知道那些fd就绪了呢? 我不知道,我只能遍历for (int i = 0; i < defaultnum; ++i){// 不合法fdif (_rfds[i].fd == defaultfd)continue;// 合法fd,但必须曾经向内核设置过帮我关心对应fd读事件才能往下走if (!(_rfds[i].events & POLLIN))continue;if (_rfds[i].fd == _listensock && _rfds[i].revents & POLLIN)Accepter(_listensock);else if (_rfds[i].revents & POLLIN)Recver(i);}

处理_listensock读就绪事件

void Accepter(int listensock)
{logMessage(DEBUG, "Accepter in");// 走到这里,accept 函数,会不会被阻塞?// 走到这里就是, poll 告送我,_listensock就绪了,然后才能执行下面代码string clientip;uint16_t clientport;int sock = Sock::Accept(listensock, &clientip, &clientport);  accept = 等 + 获取if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);// 得到一个sock套接字后,然后我们可以直接进行read/recv吗? 不能,整个代码只有poll有资格检测事件是否就绪// 将新的sock 托管给poll!// 将新的sock,托管给poll的本质,其实就是将sock,添加到_rfds数组里!int i = 0;for (; i < defaultnum; ++i){if (_rfds[i].fd != defaultfd)continue;elsebreak;}if (i == defaultnum){logMessage(WARNING, "server if full, please wait");close(sock);}else{_rfds[i].fd = sock;_rfds[i].events = POLLIN;_rfds[i].revents = 0;}print();logMessage(DEBUG, "Accepter out");
}

处理普通sock读就绪事件

void ResetItem(int i)
{_rfds[i].fd = defaultfd;_rfds[i].events = 0;_rfds[i].revents = 0;
}void Recver(int pos)
{logMessage(DEBUG, "in Recver");// 1. 读取request// 这样读取是有问题的!char buffer[1024];ssize_t s = recv(_rfds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?if (s > 0)                                                      // 读取成功{buffer[s] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (s == 0) // 对方关闭了文件描述符{close(_rfds[pos].fd);ResetItem(pos);logMessage(NORMAL, "client quit");return;}else // 读取失败{close(_rfds[pos].fd);ResetItem(pos);logMessage(ERROR, "client quit: %s", strerror(errno));return;}// 2. 处理requeststd::string response = _cbs(buffer);// 3. 返回response// write bugwrite(_rfds[pos].fd, response.c_str(), response.size());logMessage(DEBUG, "out Recver");
}

在这里插入图片描述

自此poll服务器就已经写完了,很显然poll服务器主体代码和select服务器一模一样,只不过poll在进行事件监听的时候明显要比select简洁,而且数组没有上限。

poll服务器完整代码

#pragma once#include <iostream>
#include <functional>
#include <poll.h>
#include "sock.hpp"using namespace std;class pollServer
{static const int defaultport = 8080;static const int defaultfd = -1;static const int defaultnum = 2048;using func_t = function<string(string)>;public:pollServer(func_t f, int port = defaultport) : _cbs(f), _port(port), _listensock(-1), _rfds(nullptr){}void initServer(){// 1.创建套接字_listensock = Sock::sock();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);_rfds = new struct pollfd[defaultnum]; // 大小这里自己随便定for (int i = 0; i < defaultnum; ++i)    ResetItem(i);_rfds[0].fd = _listensock; // 这个位置后面就不变了_rfds[0].events = POLLIN;  // 告诉内核帮我关心_listensock读事件}void print(){for (int i = 0; i < defaultnum; ++i){if (_rfds[i].fd != defaultfd)cout << _rfds[i].fd << " ";}cout << endl;}void Accepter(int listensock){logMessage(DEBUG, "Accepter in");// 走到这里,accept 函数,会不会被阻塞?// 走到这里就是, poll 告送我,_listensock就绪了,然后才能执行下面代码string clientip;uint16_t clientport;int sock = Sock::Accept(listensock, &clientip, &clientport);  accept = 等 + 获取if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);// 得到一个sock套接字后,然后我们可以直接进行read/recv吗? 不能,整个代码只有poll有资格检测事件是否就绪// 将新的sock 托管给poll!// 将新的sock,托管给select的本质,其实就是将sock,添加到fdarray数组里!int i = 0;for (; i < defaultnum; ++i){if (_rfds[i].fd != defaultfd)continue;elsebreak;}if (i == defaultnum){logMessage(WARNING, "server if full, please wait");close(sock);}else{_rfds[i].fd = sock;_rfds[i].events = POLLIN;_rfds[i].revents = 0;}print();logMessage(DEBUG, "Accepter out");}void ResetItem(int i){_rfds[i].fd = defaultfd;_rfds[i].events = 0;_rfds[i].revents = 0;}void Recver(int pos){logMessage(DEBUG, "in Recver");// 1. 读取request// 这样读取是有问题的!char buffer[1024];ssize_t s = recv(_rfds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?if (s > 0)                                                      // 读取成功{buffer[s] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (s == 0) // 对方关闭了文件描述符{close(_rfds[pos].fd);ResetItem(pos);logMessage(NORMAL, "client quit");return;}else // 读取失败{close(_rfds[pos].fd);ResetItem(pos);logMessage(ERROR, "client quit: %s", strerror(errno));return;}// 2. 处理requeststd::string response = _cbs(buffer);// 3. 返回response// write bugwrite(_rfds[pos].fd, response.c_str(), response.size());logMessage(DEBUG, "out Recver");}// 1.handler event _rfds 中,不仅仅是有一个fd是就绪的,可能存在多个// 2.我们的poll目前只处理了read事件void HandlerEvent(){// 你怎么知道那些fd就绪了呢? 我不知道,我只能遍历for (int i = 0; i < defaultnum; ++i){// 不合法fdif (_rfds[i].fd == defaultfd)continue;// 合法fd,但必须曾经向内核设置过帮我关心对应fd读事件才能往下走if (!(_rfds[i].events & POLLIN))continue;if (_rfds[i].fd == _listensock && _rfds[i].revents & POLLIN)Accepter(_listensock);else if (_rfds[i].revents & POLLIN)Recver(i);}}void start(){int timenout = -1;for (;;){int n = poll(_rfds, defaultnum, timenout);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "poll error, code: %d, err string: %s", errno, strerror(errno));break;default:// 说明有事件就绪了,目前只有一个监听事件就绪了logMessage(NORMAL, "have event ready!");HandlerEvent(); // 这里不用传了,因为就绪事件就在_rfds里break;}}}~pollServer(){if (_listensock != defaultfd)close(_listensock);if (_rfds)delete[] _rfds;}private:int _listensock;int _port;struct pollfd *_rfds;func_t _cbs;
};

4.poll的优点缺点

poll的优点就不用过多介绍,输入输出分离,而且没有select上限的问题

poll的主要缺点依旧是遍历问题,因为我们交给poll多个文件描述符,poll在底层去遍历去查找。随着等待的文件描述符变多,poll要线性遍历的方式检测所有文件描述符,这势必会带来效率的降低 。

正是因为poll有这样的问题,所有才有了下一个多路转接之epoll

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

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

相关文章

Unity设计模式之工厂模式

什么是工厂模式&#xff1f; 工厂是一种创建型设计模式。通俗来讲就是提供一种封装对象创建的方式&#xff0c;将对象的创建和使用区分开。就是Unity里面通常用到的创建和管理对象。 工厂模式有什么优点&#xff1f; 1、封装对象的创建方式&#xff0c;使其更加灵活、易于管理…

【教学类-55-04】20240515图层顺序挑战(四格长条纸加黑色边框、4*4、7张,不重复5400张,16坐标点颜色哈希值去重、保留7色)

背景需求&#xff1a; 前文实现了7张色彩纸条加上黑色边框的需求。 【教学类-55-02】20240512图层顺序挑战&#xff08;四格长条纸加黑色边框、4*4、7张 、43200张去掉非7色有23040张&#xff0c;哈希算法快速去重剩余1221张&#xff09;-CSDN博客文章浏览阅读1k次&#xff0…

GPT-4o模型介绍和使用方法

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

9个优质免费视频素材网站推荐丨2024年最新资源合集

在短视频火爆的时代&#xff0c;高清、无水印、可商用的视频素材变得尤为重要。下面是我精心整理的9个常用免费视频素材网站&#xff0c;适合各类视频创作者。希望你能找到满意的素材&#xff01; 一、视频素材 1. 蛙学府 优点&#xff1a;丰富的正版商用素材&#xff0c;涵盖…

AI 绘画神器 Fooocus 高级用法:设置、风格、模型、高级设置

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 大家好&#xff0c;我是水滴~~ 本文精选了一系列高级技巧和细致调整&#xff0c;旨在提升 Fooocus 用户在利用 Stable Diffusion 模型进行图像生成时的…

vue使用marked和highlight.js实现代码高亮效果

marked是对markdown进行解析的插件&#xff0c;它可以把markdown语法解析成html语法&#xff0c;从而实现页面效果&#xff0c;而highlight.js是对解析出的代码实现高亮效果 效果&#xff1a; 安装&#xff1a;避免踩我走的坑&#xff0c;安装尽量按照这个版本安装 npm install…

Linux进程——进程地址空间

前言&#xff1a;在讲完环境变量后&#xff0c;相信大家对Linux有更进一步的认识&#xff0c;而Linux进程概念到这也快接近尾声了&#xff0c;现在我们了解Linux进程中的地址空间&#xff01; 本篇主要内容&#xff1a; 了解程序地址空间 理解进程地址空间 探究页表和虚拟地址空…

matlab使用教程(71)—控制坐标区布局

1.与位置相关的属性和函数 有几个属性和函数可用于获取和设置坐标区的大小与位置。下表摘要显示了这些属性和函数。 函数或属性描述 OuterPosition 属性 使用此属性可以查询或更改坐标区的外边界&#xff0c;包括标题、标签和边距。要更改外边界&#xff0c;请将此属性指定为…

MySQL、JDBC复盘及规划

数据库仍有习题尚未做完&#xff0c;策略从一天做完改为每天5到10题&#xff0c;以此达到掌握和复习的效果&#xff0c;JDBC的六部仍需每天练习&#xff0c;从明天开始正式进行JavaWeb的学习&#xff0c;预计持续到七月中旬&#xff0c;还会完成一个书城项目&#xff0c;六月底…

Vue 快速入门:Vue初级

语法规则 前端渲染 渲染有几种方式&#xff1a;原生js、js模板、Vue模板语法 原生js 使用字符串拼接 js模板语法 Vue.js 模板语法概述 Vue.js 是一个用于构建用户界面的渐进式框架&#xff0c;其模板语法非常灵活和直观。Vue 的模板语法基于 HTML&#xff0c;可以通过指令…

Java为什么会成为现在主流的编程语言

Java为什么会成为现在的主流语言 前言一、Java语言概述Java是什么为什么大多数人会选择从事Java为什么从事Java的工作者数量从年递减 二、Java语言的特点简单性面向对象分布式&#xff08;微服务&#xff09;健壮性安全性体系结构中立可移植性解释型高性能多线程动态性 三、Jav…

UDP多对多组播通信

广播和多播仅应用于UDP。TCP是一个面向连接的协议&#xff0c;TCP一定是点对点的&#xff0c;一点是两个主机来建立连接的&#xff0c;TCP肯定是单播。只有UDP才会使用广播和组播。 如下示例实现一个UDP多对多的组播通信&#xff0c;进程中有收、发两个线程&#xff0c;分别表…

6款电脑精选工具软件推荐!

AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/ 1.IP地址查看工具——纯真ip数据库 纯真IP数据库是一个易于操作的IP地址查询工具&#xff0c;它允许用户通过输入IP地址来查询其对应的地理位置…

Django创建网站的地基

相关文档 1、为新网站创建一个文件夹&#xff08;这里是&#xff1a;locallibrary&#xff09; D:\django>mkdir locallibraryD:\django>cd locallibraryD:\django\locallibrary>dirVolume in drive D is 新加卷Volume Serial Number is B68C-03F7Directory of D:\dj…

【Uniapp】简易封装提示框showToast/showModal

组件 // 封装提示框 export const showModal (content, showCancel false, title "提示") > {return new Promise((resolve, reject) > {uni.showModal({title: title,content: content,showCancel: showCancel,success: (res) > {resolve(res);},fail:…

结合多模态 AI 谷歌展示 AR 眼镜原型机;Meta 被曝开发带摄像头的 AI 耳机丨 RTE 开发者日报 Vol.204

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」&#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE&#xff08;Real Time Engagement&#xff09; 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、「…

如何修改android 项目显示的app名

修改vlaues下的string.xml文件 修改这个标签中的内容就行

国产银河麒麟V10SP1系统安装Qt和MySql步骤

安装软件&#xff1a;准备好Kylin-Desktop-V10-SP1-Release-hwe-2107-x86_64.iso和qt-opensource-linux-x64-5.14.2.run 安装步骤&#xff1a; 1、VMWare软件中新建一个虚拟机并安装Kylin-Desktop-V10-SP1-Release操作系统&#xff0c;安装时候输入密码的地方记得要用英文&am…

“网络安全新纪元:等保2.0的详细解读与实践”

网络安全等级保护基本要求》&#xff08;等保2.0&#xff09;于2019年6月发布&#xff0c;是我国网络安全等级保护制度的一项重要标准。等保2.0主要针对关键信息基础设施的网络安全保护&#xff0c;对数据安全和个人信息保护提出了更高的要求。本文将对等保2.0进行详细解读&…

动态路由实验新手入门:快速掌握核心知识点

大家好&#xff0c;这里是G-LAB IT实验室。今天带大家学习一下华为动态路由实验配置&#xff0c;新手入门&#xff0c;快速掌握核心知识点&#xff01; 01、实验拓扑 02、实验需求 1.根据拓扑运行对应的路由协议 2.每个路由器都有自己的环回口&#xff0c;宣告的区域不限制 …