网络编程: 高级IO与多路转接select,poll,epoll的使用与介绍

网络编程: 高级IO与多路转接select,poll,epoll的使用与介绍

  • 前言
  • 一.五种IO模型
    • 1.IO的本质
    • 2.五种IO模型
      • 1.五种IO模型
      • 2.同步IO与异步IO
      • 3.IO效率
  • 二.非阻塞IO
    • 1.系统调用介绍
    • 2.验证代码
  • 三.select多路转接
    • 1.系统调用接口
    • 2.写代码 : 基于select的TCP服务器
      • 1.封装的Socket接口
      • 2.开始写代码
        • 1.普通的echo服务器
        • 2.设计一下如何改进多线程的方案
        • 3.代码
      • 3.演示
    • 3.select的缺点
    • 4.select的代码
  • 四.poll
    • 1.系统调用接口
    • 2.第三方数组的选择: 智能指针的回顾
    • 3.代码
      • 1.框架
      • 2.代码
    • 4.poll的优缺点
  • 五.epoll
    • 1.系统调用
    • 2.站在系统角度理解epoll的系统调用
    • 3.LT和ET
    • 4.小总结
    • 5.封装epoll的系统调用接口
    • 6.epoll的使用
    • 7.epoll的优点
  • 六.补充: Log.hpp
    • 1.C语言版本
    • 2.C++版本

前言

注意 : 高级IO当中只关心文件IO/网络IO,因为五种IO模型就是针对于fd相关操作而由大佬发明/归纳出来的,都是为了提高效率/适用于不同的场景

我们学习Linux的过程当中也见到过其他的等待+处理的方式 :
wait/waitpid,lock,pthread_cond_wait,信号量的P操作…它们也都需要等待,但是跟fd无关,所以我们不考虑

其实仔细想想(我们不考虑wait/waitpid,它们可以根据信号那里的方法来解决等待问题):
需要进行lock,pthread_cond_wait,信号量的P操作…这些操作时,往往意味着对应的执行流无需执行其他任务,
因此降低为了它们占用CPU的时长,最好的方式就是让他们阻塞,所以才没有针对于这些操作提出对应的"IO"模型,来减少等待,因为没意义

换言之,多线程这里的等待是为了互斥与同步,而不是单纯的等待读/写事件就绪

一.五种IO模型

1.IO的本质

  • IO = 等+拷贝

进行文件操作和网络通信时,如果对应条件不满足(我们称为事件没有就绪),那么我们默认就会进行阻塞等待
比如 :

  1. read,recv,recvfrom : 典型的读事件没有就绪时就会等待,就绪之后把数据从文件级缓冲区/TCP/UDP的接收缓冲区拷贝到char数组当中
  2. write,send : 典型的写事件没有就绪时就会等待,就绪之后把数据从char数组当中拷贝到文件级缓冲区/TCP的发送缓冲区/UDP直接给sk_buff内核数据结构(sendto一般无需等待,极端情况下需要等待)
  3. accept : accept本质上是去OS维护的全连接队列当中取连接来进行通信,如果没有连接,那么就需要等待哦,而且accept跟listen套接字紧密相关,所以accept也可以看作一种"读IO"

2.五种IO模型

大佬们为了提高IO效率,提出了五种IO模型

1.五种IO模型

  • 如何提高IO效率呢 ? -> 单位时间内,减少等的比重
  • [1] 阻塞IO : 对应事件没有就绪则一直等待
  • [2] 非阻塞轮询式IO : 对应事件没有就绪时立即返回,可以先执行其他任务,一段时间后再次进行非阻塞IO操作
  • [3] 信号驱动IO : 对应事件就绪之后,由OS向进程发送信号通知进程进行IO操作
  • [4] 多路复用/多路转接IO : 多路转接可以同时等待多个文件描述符的就绪状态,并通知进行对应的IO操作
  • [5] 异步IO : 由OS在对应事件就绪之后,进行IO操作,完成之后通知对应进程

2.同步IO与异步IO

首先要说明 : 信号驱动IO属于同步IO还是异步IO是有争议的,而且到现在还没有达成统一,我们在这里认为信号驱动IO也属于同步IO

  • 同步IO就是 : 只要对应进程/线程需要参与IO过程(无论是等还是拷贝),它就是同步IO
  • 异步IO就是 : 对应进程/线程 发起IO操作,但是自己不参与(无论是等还是拷贝),它就是异步IO

因此 阻塞IO,非阻塞轮询式IO,多路转接IO,信号驱动IO都属于同步IO,而异步IO就是异步IO

对于信号驱动IO,它不用自己等待,但是拷贝操作还是必须由它自己去做

这里就有一个问题了 :
信号驱动IO不用自己等待,可是拷贝操作还是要由自己做,而拷贝操作必须/只能在对应事件就绪之后(等待完毕之后)才能进行啊

因此如果认为 : 信号驱动IO也是要由进程等待的啊,只不过不是它自己等待而已,此时信号驱动IO就像是一种同步IO
如果认为 : 信号驱动IO不是由进程自己等待,而是对应事件就绪之后由OS通知进程,在这段时间当中,进程/线程想执行什么任务就执行什么任务,此时信号驱动IO就像是一种异步IO(毕竟信号是异步的嘛)

信号驱动IO由于信号是异步的,所以具有异步IO的特点,但是拷贝操作要由自己完成,因此需要参与IO过程,又具有同步IO的特点… 我们就认为它是同步IO

信号的异步性并不意味着整个I/O操作也是异步的,因为数据拷贝这一关键步骤仍然需要进程同步地参与

3.IO效率

  • 阻塞IO,非阻塞IO,信号驱动IO : 它们的区别就是等的方式不同而已 :
  • 阻塞IO在等期间不做任何事情,就只是在对应资源的等待队列当中等待资源就绪,就绪之后才会被调度到CPU的运行队列当中
  • 非阻塞IO和信号驱动IO在等期间可以做其他事情
  • 只不过非阻塞IO需要主动在轮询时调用非阻塞IO接口来检查对应资源是否就绪
  • 而信号驱动IO是由OS发送信号通知进程/线程来进行拷贝操作

我们这里只关注IO操作的效率 !!

  • 无论你等待的方式如何,该等多长时间还是等多长时间,因此IO效率并未提高,只不过是等待期间能否/如何做其他任务而已

  • 而它们三者跟多路复用/多路转接IO的区别是 :

  • 多路转接可以同时等待多个文件描述符的就绪状态,将多次等待的时间进行重叠,

  • 单拿出具体每一个等待,该等多长时间还是等多长时间,只不过多路转接将IO的等待由原本的串行改为了并行,

  • 因此可以提前等待,因此就绪的时刻就会提前,所以多路转接在整体而言是减少了整体IO操作当中等的比重,从而提高了IO效率

  • 就像是开多线程去分别等待对应的文件描述符的就绪状态,只不过多路转接无需创建额外的线程,减少了线程创建,调度,切换的消耗

  • 多路转接通过提高IO操作的并行性,从而提高了整体的IO效率

  • 我们只关注IO操作的效率,在不考虑对多个文件描述符对应的IO操作进行异步IO交由OS负责时,

  • 多路复用可以同时等待多个文件描述符的就绪状态,因此这种情况下多路转接的效率是高于异步IO的

我们之前用的接口大多数默认都是阻塞IO : read,write,send,recv,recvfrom,accept… 80%的使用IO的场景都是阻塞IO
我们重点看多路转接,因为它效率高

二.非阻塞IO

1.系统调用介绍

对于文件IO: 我们可以open时设置O_NONBLOCK选项,来用非阻塞状态打开对应文件,返回一个非阻塞状态的fd
对于网络IO:
在这里插入图片描述
我们介绍一个统一且一劳永逸的方法 :
在这里插入图片描述

2.验证代码

下面还是写个代码才清楚

  1. 普通阻塞式IO
    在这里插入图片描述
  2. 非阻塞IO
    要包头文件: fcntl.h
    在这里插入图片描述
    使用errno来区分
    在这里插入图片描述
    在这里插入图片描述
    下面我们开始介绍今天的主角: 多路转接
    多路转接的OS提供的系统调用接口共有三个: select,poll,epoll
    它们是按照顺序发明的,因此功能等等愈加完善,用法也愈加好用,我们先介绍select

三.select多路转接

  • select只负责等待,不负责拷贝,拷贝操作需要由我们自行使用read,write,send,recv等等接口来进行拷贝
  • 因为select等待成功之后才通知我们,也就是说只要select通知我们,就意味着对应文件描述符的资源就绪了,
    因此我们的IO操作时就不需要等待了,而是直接拷贝即可

1.系统调用接口

在这里插入图片描述
注意: fd_set是一个具体的固定大小的类型,因此位图结构当中的比特位是有限的,因此能够等待的fd的个数也是有限的
这一点也是select不好的地方

说这么多,还是不如写一下更清楚,理论要跟实践相结合嘛

2.写代码 : 基于select的TCP服务器

  1. 我们想用一个模板方法类设计模式封装的Socket接口来封装一下原生套接字的方法,一起感受一下这个设计模式的魅力
  2. select和poll不是我们本篇博客的重点,因此我们就不把它们完善化了,只关心读事件,而且不解决粘包问题,只实现一个简单的echo服务器,用telnet充当客户端了
  3. 别担心,我们在后面用epoll实现reactor模式的时候会解决粘包问题,并且实现一个简单的英译汉,汉译英服务器和客户端
  4. 我们先把注意力放在select,pool和epoll接口的学习和使用上,能够实现多路转接即可

1.封装的Socket接口

下面这份代码对于大家而言并不难理解,毕竟都学到多路转接了,一个多态+套接字封装肯定没问题啊

#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <cstdlib>
#include "Log.hpp"
#include <cerrno>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
const int default_backlog = 5;
// 会话层
// 我自己取地址
#define Conv(sock_info) ((sockaddr *)&sock_info)
enum
{UsageErr = 0,SocketErr,BindErr,ListenErr,ReadErr
};
// 模板方法类设计模式
// 子类当中我们已经维护了socketfd,所以几乎所有接口都无需传入socketfd
class Socket
{
public:virtual int GetSocket() = 0;virtual void SetSocket(int socketfd) = 0;virtual Socket *AcceptSocket(string &src_ip, uint16_t &src_port) = 0; // server接收,无需数据,但是需要返回一个新的套接字fdvirtual void SendMessage(const string &message) = 0;                  // 发送数据,直接给我字符串即可virtual int RecvMessage(string &buf, int size) = 0;                  // 接收数据,需要传入sizevirtual void debug() = 0;virtual ~Socket(){}protected:virtual int CreateSocket() = 0;virtual void BindSocket(uint16_t port) = 0;                      // server才需要,而且server只需要port这一个数据virtual void ListenSocket(int backlog = default_backlog) = 0;    // 监听,只需要backlog这一个数据即可virtual bool ConnectSocket(const string &ip, uint16_t port) = 0; // client连接,需要传入server的ip和portpublic:// 创建监听套接字,serverint CreateListenSocket(uint16_t port, int backlog = default_backlog){int socketfd = CreateSocket();BindSocket(port);ListenSocket(backlog);return socketfd;}// 创建连接套接字,clientint CreateConnectSocket(const string &ip, uint16_t port){int socketfd = CreateSocket();ConnectSocket(ip, port);return socketfd;}
};class TcpSocket : public Socket
{
public:TcpSocket() = default;TcpSocket(int socketfd) : _socketfd(socketfd) {}virtual ~TcpSocket() override{if (_socketfd != -1)close(_socketfd);}virtual int GetSocket(){return _socketfd;}virtual int CreateSocket() override{_socketfd = socket(AF_INET, SOCK_STREAM, 0);if (_socketfd < 0){lg.LogMessageKeep(Fatal, "create socketfd fail, errno : %d , strerror : %s\n", errno, strerror(errno));exit(SocketErr);}return _socketfd;}virtual void SetSocket(int socketfd) override{_socketfd = socketfd;}virtual void BindSocket(uint16_t port) override{struct sockaddr_in sock_info;memset(&sock_info, 0, sizeof(sock_info));sock_info.sin_family = AF_INET;sock_info.sin_addr.s_addr = INADDR_ANY;sock_info.sin_port = htons(port);int n = bind(_socketfd, Conv(sock_info), sizeof(sock_info));if (n < 0){lg.LogMessageKeep(Fatal, "server bind fail, errno :%d ,strerror: %s\n", errno, strerror(errno));exit(BindErr);}cout << "bind success" << endl;}virtual void ListenSocket(int backlog) override{int n = listen(_socketfd, backlog);if (n < 0){lg.LogMessageKeep(Fatal, "server listen fail, errno :%d ,strerror: %s\n", errno, strerror(errno));exit(ListenErr);}cout << "listen success" << endl;}virtual TcpSocket *AcceptSocket(string &src_ip, uint16_t &src_port) override{struct sockaddr_in src_addr;socklen_t src_len = sizeof(src_addr);int socketfd = accept(_socketfd, Conv(src_addr), &src_len);src_ip = inet_ntoa(src_addr.sin_addr);src_port = ntohs(src_addr.sin_port);cout << "accept success" << endl;return new TcpSocket(socketfd);}virtual bool ConnectSocket(const string &ip, uint16_t port) override{struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(ip.c_str());server_addr.sin_port = htons(port);int n = connect(_socketfd, Conv(server_addr), sizeof(server_addr));if (n < 0)return false;cout << "connect success" << endl;return true;}virtual void SendMessage(const string &message) override{send(_socketfd, message.c_str(), message.size(), 0);}virtual int RecvMessage(string &buf, int size) override{char buffer[size];ssize_t n = recv(_socketfd, buffer, size - 1, 0);if (n > 0){buffer[n] = '\0';buf += string(buffer);}return n;}virtual void debug() override{cout << "socketfd: " << _socketfd << endl;}private:int _socketfd = -1;
};

2.开始写代码

我们先快速搞出一份普通的echo服务器,体会一下单/多线程的劣势,以及大佬为何要发明多路转接

1.普通的echo服务器
#include <iostream>
#include <memory>
#include <sys/select.h>
using namespace std;
#include "Socket.hpp"
#include <vector>// 记住一个原则 : 所有的fd都有可能需要等待,因此等待全交给select负责
const int defaultRecvSize = 1024;
class Select_server
{
public:Select_server(uint16_t port) : _port(port), _listensock(new TcpSocket){}// 创建监听套件字并进行监听void Init(){_listensock->CreateListenSocket(_port);}// 开始不断accept请求void Start(){while (true){string srcip;uint16_t srcport;shared_ptr<Socket> newsock(_listensock->AcceptSocket(srcip, srcport));Routine(newsock); // 这里往往是要开多线程跑的,因为单线程的话,在给一个用户提供服务时就无法给其他用户提供服务了}}private:void Routine(shared_ptr<Socket> newsock){// 死循环给用户提供服务,直到用户关闭连接时,我们才会退出while (true){string buf;int n = newsock->RecvMessage(buf, defaultRecvSize - 1);if (n > 0){buf[n] = '\0';lg.LogMessageKeep(Info, "recv messsage## %s\n", buf.c_str());// 此时buf就是读到的数据,将该数据echo回client即可newsock->SendMessage(buf);}else if (n == 0){// 说明client方关闭写,因此我们无需再从client读数据了,下面只需要向client发数据就行了// 当client关闭读时,通常情况下我们就可以关闭连接了//这里因为我们读完数据即可就会发给client,因此这里client不写了,我们也就不读了,也就不会给client发数据了,因此我们直接关闭连接即可lg.LogMessageKeep(Info, "client exit, i will disconnect with him\n");break;}else{// 说明写出现了异常,打印一下日志,断开连接lg.LogMessageKeep(Error, "recv message fail , errno: %d, strerror: %s\n", errno, strerror(errno));break;}}}private:uint16_t _port;shared_ptr<Socket> _listensock;
};

在这里插入图片描述
一个很基础的echo服务器搞出来,下面我们看一下单线程的劣势
可以看出,单线程时,server一次只能给一个用户提供服务,大家仔细一点观察会发现上一个client断开连接之后,下一个client的服务就能立刻响应,
可是我们的server当时明明是在给上一个client提供服务啊,它怎么能够拿到下一个client已经发送过的数据呢??

道理很简单,下一个client发送的数据放到了tcp的接收缓冲区当中,应用层想拿的时候拿就行了
在这里插入图片描述
如果给用户提供服务的Routine函数当中不打while(true)的话:
在这里插入图片描述
用户只要发一条消息,服务就结束了
这哪行,用户就没有体验了…
改多线程也很简单
加上两行代码就行
在这里插入图片描述
为了方便观察多线程,我们改一下给用户返回的消息
在这里插入图片描述
在这里插入图片描述
假设我是一个非常大的互联网公司,我手下的某个软件,最高情况下,全球有5亿用户同时访问,
难不成我要总共开5亿个多线程来为每个用户提供服务,5亿啊… 不现实

就算我通过反向代理服务器把业务负载均衡式部署到不同的服务器上,不同的服务器上面再开多进程,每个进程内部在开多线程
层层分解,但是执行流/线程总数依然少不了5亿啊,就算能搞定,代价也很高

而且我们知道: 线程一定越多越好吗?并不是,尽管线程切换是更加轻量的, 但是多线程需要维护同步和互斥,
加锁就会导致多线程执行临界区代码由并行变为串行,极端情况下效率还不如多进程

而且多线程+异常+网络+线程的同步和互斥,调试是相当复杂的…

总体而言,这个方案有待改进,一味使用多线程,代价是相当高的,因此多路转接应运而生

2.设计一下如何改进多线程的方案

下面我们站在大佬的角度来想一下, 这个多线程方案该如何改进呢?
我们搞多线程,是因为单线程的情况下,无法为多个用户同时提供服务,如果我们能为多个用户同时提供服务,不就不用开多线程了吗?
换言之,如果搞不定这个需求,那么多线程就真的无可替代了

我们一起看一下,这个死局,到底该怎么破?
在这里插入图片描述
我们先介绍select这个服务员,这个服务员的特点是:

  1. 优点: 一次性可以等待多个fd
  2. 最大缺点: fd_set参数属于输入输出型参数,每次返回时都会修改我们所设置的fd_set,因此我们每次使用时都要重置fd_set这个参数

监听套接字要不要交给服务员呢?
当然要啦,因为accept本质也是IO嘛

因为IO的本质就是等+拷贝, 服务的本质不也是等+提供服务嘛

select每次提供等待时, 要求我们给他三份清单(读清单,写清单,异常清单), 上面列出我们想要等待哪个fd的对应事件
一旦对应事件发生,select就会返回给我们给他的那个清单,上面列出了哪些fd的事件已经就绪

正因为如此,所以我们不能指望select保存我们要等待的用户/fd,而是要自己用一个数据结构来保存对应的fd
(这个数据结构通常是数组/vector)
因为给select清单时要用

3.代码

accept尽管也是读事件,但是它的处理方式是: 把对应的fd添加到我们的用户数组当中
而其他套接字的读事件就是做饭

因此我们就能写出这样的框架来

记住一个原则 : 所有的fd都有可能需要读等待,因此读等待全交给select负责

select还要我们告诉他清单上fd的最大值,因为他要遍历进程fd表,查看对应fd的状态
在这里插入图片描述
其实select就是一个系统调用接口而已,用起来熟了就都是套路
在这里插入图片描述

3.演示

让我们看一下select能否同时给多个用户提供服务
在这里插入图片描述
为了证明我们是单线程,打印一下线程ID

完美,的确能够一次性等待多个线程,这是select最大/唯一的优点

3.select的缺点

从代码当中体会
在这里插入图片描述
文字:
在这里插入图片描述

4.select的代码

#include <iostream>
#include <memory>
#include <sys/select.h>
using namespace std;
#include "Socket.hpp"
#include <vector>const int Default_Select_Array_Size = sizeof(fd_set) * 8; // 一个字节->8个比特位,因此select最多监听的fd总数为sizeof(fd_set) * 8// 记住一个原则 : 所有的fd都有可能需要等待,因此等待全交给select负责class Select_server
{
public:Select_server(uint16_t port) : _port(port), _listensock(new TcpSocket){Select_arr.resize(Default_Select_Array_Size);}// 创建监听套件字并进行监听void Init(){_listensock->CreateListenSocket(_port);}// 开始不断accept请求void Start(){int listenfd = _listensock->GetSocket();Select_arr[listenfd] = _listensock; // 先把listen套接字保存起来fd_set fset;                        // 读清单while (true){FD_ZERO(&fset);int maxfd = 0;for (int i = 0; i < Default_Select_Array_Size; i++){if (Select_arr[i].get()){FD_SET(i, &fset); // 填写清单maxfd = i;}}// struct timeval timeout;// timeout.tv_sec = 0;// timeout.tv_usec = 0;// 把清单给selectint num = select(maxfd + 1, &fset, nullptr, nullptr, nullptr); // timeout设置为nullptr,代表阻塞等待if (num > 0){lg.LogMessageKeep(Info, "select 等待成功, num: %d\n", num);Routine(num, fset); // select等待成功,通知厨师}else if (num == 0){lg.LogMessageKeep(Info, "超时");}else{lg.LogMessageKeep(Error, "select error, errno: %d , strerror : %s\n", errno, strerror(errno));}}}private:void Routine(int n, fd_set &fset){for (int i = 0, j = 0; i < Default_Select_Array_Size; i++){if (!FD_ISSET(i, &fset))continue;j++;// accept读事件if (i == _listensock->GetSocket()){string src_ip;uint16_t src_port;shared_ptr<Socket> sp(_listensock->AcceptSocket(src_ip, src_port));// accept成功之后把Select_arr当中的相应位置填充好int newsockfd = sp->GetSocket();Select_arr[newsockfd] = sp;lg.LogMessageKeep(Info, "accept success , newsockfd: %d\n", newsockfd);}// 普通读事件else{string buf;int num = Select_arr[i]->RecvMessage(buf, 1024);if (num > 0){lg.LogMessageKeep(Info, "read success , message# %s\n", buf.c_str());string threadid = std::__cxx11::to_string(pthread_self());Select_arr[i]->SendMessage(threadid + " " + buf); // 返回给用户响应}else if (num == 0){lg.LogMessageKeep(Info, "client exit, i will disconnect\n");Select_arr[i].reset(); // 断开连接,取消服务}else{lg.LogMessageKeep(Error, "recv message error, i will disconnect\n");Select_arr[i].reset(); // 断开连接,取消服务}}}}private:uint16_t _port;shared_ptr<Socket> _listensock;vector<shared_ptr<Socket>> Select_arr;
};

因为是清单是位图,所以我们采用哈希的直接定址法(下标就是fd)来组织这个第三方数组

四.poll

1.系统调用接口

在这里插入图片描述
poll服务员水平更高一些,它让我们给他提供一个大清单(struct pollfd), 上面的信息是 fd, 小清单: 你想监听哪些事件,还有一个空的小清单

调用poll时,我们填好fd和events即可, 返回时我们只需要看revents和fd即可
其他的逻辑跟select雷同,我们也需要维护一个第三方数组,用来记录要提供服务的客户
只不过我们不需要每次重置我们的events,因此使用起来比select更方便

注意: poll的清单不是位图结构组织的,而是一个可以动态扩容的指针

2.第三方数组的选择: 智能指针的回顾

  1. shared_ptr<struct pollfd*> _fds?
    因为它保存的是一个二级指针,这个二级指针指向的是一个一级指针,这个一级指针是管理我们new出来的struct pollfd* 这段连续区间的指针,因此想要使用就需要我们单独使用定制删除器
    (倒是也不难,这么写就行: )
pollfd *pfd = new pollfd[10];
for (int i = 0; i < 10; i++)
{pfd[i].fd = -1;pfd[i].events = pfd[i].revents = 0;
}
auto f = [](pollfd **pfd)
{delete[] (*pfd);
};
shared_ptr<pollfd *> _fds = shared_ptr<pollfd *>(&pfd, f);
for (int i = 0; i < 10; i++)
{cout << (*_fds.get())[i].fd << " " << (*_fds.get())[i].events << " " << (*_fds.get())[i].revents << endl;
}值得注意的是_fds.get()是一个二级指针,我们不能对他[i],否则就野指针了,需要先解引用,取出一级指针(数组首元素地址),然后再去[]拿到pollfd,然后.
  1. shared_ptr<struct pollfd[]> _fds
    它保存的是一个一级指针,这个一级指针就是管理我们new出来的struct pollfd* 这段连续区间的指针
 shared_ptr<struct pollfd[]> _fds;_fds = shared_ptr<struct pollfd[]>(new struct pollfd[10]);for (int i = 0; i < 10; i++){_fds[i].fd = -1;_fds[i].events =  _fds[i].revents = 0;}for (int i = 0; i < 10; i++){cout << _fds[i].fd << " " << _fds[i].events << " " << _fds[i].revents << endl;}_fds是一个一级指针,直接[i]取pollfd即可
  1. 其实直接用vector即可
    poll传参的时候第一个参数传& vec[0]即可…

反正这三种方法大家随意,我都试过了,我就用第三种vector了,还是vector好啊…
注意法一和法二不要用make_shared,因为:

法一: 
1. shared_ptr<struct pollfd[]> _fds指向的是一个数组本身,管理的是一个连续内存,而shared_ptr对它的删除器进行了偏特化,删除时是使用delete [] 而不是delete本身
2. make_shared传参时是采用shared_ptr<T> make_shared (Args&&... args)进行传参的,主要用于单个对象的分配和初始化法二:
make_shared不支持定制删除器,只有构造才支持

3.代码

1.框架

在这里插入图片描述

2.代码

代码比select简单多了,直接给了,用vector大家一看就懂

#include <iostream>
#include <memory>
#include <sys/select.h>
using namespace std;
#include "Socket.hpp"
#include <vector>
#include <poll.h>class Poll_server
{
public:Poll_server(uint16_t port) : _port(port), _listensock(new TcpSocket){}// 创建监听套件字并进行监听void Init(){_listensock->CreateListenSocket(_port);}// 开始不断accept请求void Start(){int listenfd = _listensock->GetSocket();set_pollfd_pollin(listenfd);while (true){int num = poll(&_fds[0], _fds.size(), -1);if (num > 0){lg.LogMessageKeep(Info, "poll 等待成功, num: %d\n", num);Routine(num);}else if (num == 0){lg.LogMessageKeep(Info, "超时");}else{lg.LogMessageKeep(Error, "poll error, errno: %d , strerror : %s\n", errno, strerror(errno));}}}private:// 这里为了方便设置监听,所有提取抽离出来一个set_pollfd_pollin函数void set_pollfd_pollin(int fd){if (!_invalids.empty()){int i = _invalids.back() - '0';_invalids.pop_back();_fds[i].fd = fd;_fds[i].events = POLLIN;_fds[i].revents = 0;}else{// 到这里代表需要新增了_fds.push_back(pollfd({fd, POLLIN, 0}));}lg.LogMessageKeep(Info, "成功将fd添加到_fds中: fd: %d\n", fd);}// 跟select的Routine几乎雷同void Routine(int n){for (int i = 0; i < _fds.size(); i++){if (_fds[i].fd == -1)continue;if (_fds[i].revents & POLLIN){int fd = _fds[i].fd;// accept读事件if (fd == _listensock->GetSocket()){string src_ip;uint16_t src_port;Socket *sp = _listensock->AcceptSocket(src_ip, src_port);// accept成功之后把Select_arr当中的相应位置填充好int newsockfd = sp->GetSocket();set_pollfd_pollin(newsockfd);lg.LogMessageKeep(Info, "accept success , newsockfd: %d\n", newsockfd);}// 普通读事件else{char buf[1024] = {0};// cout << "我要读数据啦 " << endl;int num = recv(fd, buf, sizeof(buf) - 1, 0);if (num > 0){buf[num] = '\0';lg.LogMessageKeep(Info, "read success , message# %s\n", buf);send(fd, buf, num, 0);}else{close(_fds[i].fd);_fds[i].fd = -1;_fds[i].events = 0;_fds[i].revents = 0;_invalids += i + '0';if (num == 0)lg.LogMessageKeep(Info, "client exit, i will disconnect\n");elselg.LogMessageKeep(Error, "recv message error, i will disconnect\n");}}}}}private:uint16_t _port;shared_ptr<Socket> _listensock;vector<pollfd> _fds;string _invalids;
};

演示:
在这里插入图片描述

4.poll的优缺点

在这里插入图片描述
poll其实使用起来相比于select已经挺不错了,但是总归效率还是不够好,大佬们当然是能优化则优化啦,因此设计出来了epoll

五.epoll

1.系统调用

在这里插入图片描述
events可以是以下几个宏的集合:

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

2.站在系统角度理解epoll的系统调用

在这里插入图片描述
单凭这个事件驱动+红黑树+就绪队列就已经秒杀select和epoll了,
相当于一个非常精明能干的服务员,等待清单自己维护,我们不用维护第三方数组,直接告诉他一声就行, 而且能让用户主动去跟他发起服务请求…逆天啊

后面还有更强的特点(ET模式)

3.LT和ET

在这里插入图片描述

=========================================================================================================
在这里插入图片描述

=========================================================================================================
在这里插入图片描述
epoll的默认模式是LT模式,想要搞成ET模式,需要给events设置选项EPOLLET

大家可以这么去理解ET模式

ET模式就相当于  qq当中的特别关心, 只会提示一次,而且我们必须一次把数据全读上来, 
而我们的进程/线程非常忙也非常注重效率,每天都要处理很多请求,,,异常而IO时等是比较慢的,因此使用epoll关心某个fd就相当于给某个联系人设置特别关心,
一旦消息到了,那么进程/线程就会收到通知, 就能够立刻给用户提供服务,从而提高效率

其实LT模式下如果程序员也能一次性把数据全都读完,那么LT模式的效率跟ET就没有差别了
可是LT对程序员没有强制约束力,所以ET这种带有强制约束力的模式就更加高效了

4.小总结

  1. 系统调用接口
    • epoll_create创建一个epoll句柄
    • epoll_ctl对监听fd以及对应监听事件类型进行注册
    • epoll_wait等待获取对应的监听fd
  2. 设置关心,通知用户,wait的理解
    • 设置epoll关心的方式就是将给红黑树插入对应的节点, 同理修改/删除关心就是修改/删除对应的红黑树上的节点
    • epoll通知用户的方式就是将对应节点从红黑树转移到就绪队列当中
    • 用户wait的本质就是将就绪队列当中的数据拷贝到用户空间当中,然后对应节点就会从就绪队列转移回红黑树当中
  3. LT实现通知多次的方式
    用户对应fd的数据只要还有或者fd的状态发生了变化,那就会把对应节点继续从红黑树移动回就绪队列,只要对应节点一直在就绪队列,就相当于一直给用户发送通知
  4. ET实现只通知一次的方式
    用户对应fd的数据只要不是少变多或者fd的状态没有发生变化,就不会把对应节点从红黑树移动回就绪队列,因此只给用户发送一次通知

5.封装epoll的系统调用接口

在这里插入图片描述
在这里插入图片描述

#pragma once
#include <sys/epoll.h>
#include <vector>
#include <string>
using namespace std;class Epoller
{
public:// 默认阻塞式等待Epoller(int timeout = -1) : _timeout(timeout){_epfd = epoll_create(1);}void add_Epoll(int fd, bool in = false, bool out = false, bool except = false){int i = _events_arr.size();if (!_invalids.empty()){i = _invalids.back() - '0';_invalids.pop_back();}else_events_arr.push_back(epoll_event());_events_arr[i].events = (in ? EPOLLIN : 0) | (out ? EPOLLOUT : 0) | (except ? EPOLLERR | EPOLLHUP : 0);_events_arr[i].data.fd = fd;epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &_events_arr[i]);}void removefromEpoll(int fd){for (int i = 0; i < _events_arr.size(); i++){if (_events_arr[i].data.fd == fd){_events_arr[i].data.fd = -1;_events_arr[i].events = 0;_invalids.push_back(i); // 删除的时候添加到invalids当中break;}}epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);}int wait(){return epoll_wait(_epfd, &_events_arr[0], _events_arr.size(), _timeout);}int getfd(int index){return _events_arr[index].data.fd;}uint32_t getevent(int index){return _events_arr[index].events;}private:int _epfd;vector<struct epoll_event> _events_arr;string _invalids;int _timeout;
};

6.epoll的使用

有了epoller之后,epoll使用起来简直不要太爽,比select和poll好用多了,直接给代码了,没啥难的

#pragma once
#include "Socket.hpp"
#include "Log.hpp"
#include <memory>
#include <vector>
#include "Epoller.hpp"class EpollServer
{
public:EpollServer(uint16_t port) : _port(port), _listensock(make_shared<TcpSocket>()){_listensock->CreateListenSocket(port);_epoll.add_Epoll(_listensock->GetSocket(), true);}void loop(){while (true){int n = _epoll.wait();if (n > 0){lg.LogMessageKeep(Info, "epoll wait success, n: %d\n", n);handler(n);}else{lg.LogMessageKeep(Error, "epoll wait error, errno: %d, strerror: %s\n", errno, strerror(errno));}}}private:void handler(int n){for (int i = 0; i < n; i++){int fd = _epoll.getfd(i);uint32_t event = _epoll.getevent(i);if (event & EPOLLIN){// 监听套接字if (fd == _listensock->GetSocket()){string src_ip;uint16_t src_port;Socket *newsock = _listensock->AcceptSocket(src_ip, src_port);_epoll.add_Epoll(newsock->GetSocket(), true);lg.LogMessageKeep(Info, "accept success , newsockfd: %d\n", newsock->GetSocket());}// 普通读事件else{char buf[1024]; // BUG// 1.数据包粘包没有解决// 2.如果本次读到的报文不完整,本来应该下次读取时拼接式读取,以拼接为完整报文// 但是呢..  这里的recv是覆盖式写入,因此就会导致报文无法完整// 所以需要我们在reactor的时候来解决这个问题int num = recv(fd, buf, sizeof(buf) - 1, 0);if (num > 0){buf[num] = 0;lg.LogMessageKeep(Info, "recv message## %s\n", buf);send(fd, buf, num, 0); // 发回去}else{if (num == 0)lg.LogMessageKeep(Info, "client exit, i will disconnect...\n");elselg.LogMessageKeep(Error, "recv message fail!!!\n");// 取消关心,并close对应的fd_epoll.removefromEpoll(fd);close(fd);}}}}}uint16_t _port;Epoller _epoll;shared_ptr<Socket> _listensock;
};

演示:
在这里插入图片描述

7.epoll的优点

在这里插入图片描述
多路转接的介绍就这些, 但是我们之前一直在回避一个问题: read的数据粘包,序列反序列化,写事件和异常事件怎么处理?
这些问题我们在reactor模型的博客当中重点介绍

在那里我们会一步步的想方设法解决问题,提出方案修改代码,当我们走完那条路时,我们写出来的东西就是传说中的reactor模型
就像是我们之前学https协议原理时,我们顺着那个路走到头,就是最终版本的CA证书加持版本

只不过reactor这条路比较长,篇幅比较多,而且我们还会扩展主从reactor模型,代码量都是1000多行,所以我们单独写篇博客了就

以上就是高级IO与多路转接select,poll,epoll的使用与介绍的全部内容,希望能对大家有所帮助!!!

六.补充: Log.hpp

1.C语言版本

#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <unordered_map>
#include <stdarg.h> //va_start的头文件
using namespace std;enum LogLevel // 日志等级
{Debug = 0, // 调试信息Info,      // 普通信息Warning,   // 报警信息Error,     // 错误信息Fatal      // 致命信息
};enum LogStyle // 日志打印风格
{Screen,    // 显示器打印One_file,  // 往同一个打印Class_File, // 分类进行打印
};const string base_filename = "log.";
const string default_dirname = "log";
const LogStyle default_style = Screen;class Log
{
public:Log() : _filename(base_filename), _style(Screen) {mkdir(default_dirname.c_str(), 0775);}void ChangeStyle(LogStyle style){_style=style;}string getLocalTime(){time_t t = time(nullptr);struct tm *tm = localtime(&t);char time_str[128];snprintf(time_str, sizeof(time_str), "%d-%d-%d %d:%d:%d",tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);return time_str;}// 当前level:日志等级,format:格式,...是类似于printf的方式进行传入void LogMessageKeep(LogLevel level, const char *format, ...) // 类C的一个日志接口{if (_levelMap.count(level) == 0){cout << "level not exist, please check your entry if correct" << endl;return;}// int vsnprintf(char *str, size_t size, const char *format, va_list ap)// 左半部分: 日志等级,当前时间,进程PID  右半部分:信息char leftbuffer[1024] = {'\0'}, rightbuffer[1024] = {'\0'};va_list args;                                              // 可变参数列表va_start(args, format);                                    // 让args指向可变参数列表vsnprintf(rightbuffer, sizeof(rightbuffer), format, args); // 让vsnprintf帮我写va_end(args);                                              // 释放argsstring level_str = _levelMap[level], cur_time_str = getLocalTime(), id_str = to_string(getpid());snprintf(leftbuffer, sizeof(leftbuffer), "[%s]  [%s]  [%s]  ", level_str.c_str(), cur_time_str.c_str(), id_str.c_str());string log_str(leftbuffer);log_str += rightbuffer;Write_to_log(level_str, log_str);}private:void Write_to_One_File(const string &log_name, const string &message){int fd = open(log_name.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);if (fd < 0){cout<<log_name<<endl;return;}write(fd, message.c_str(), message.size());close(fd);}void Write_to_Class_File(const string &level_str, const string &message){string logname = default_dirname + "/" + _filename + level_str;Write_to_One_File(logname, message);}void Write_to_log(const string &level_str, const string &message){switch (_style){case Screen:cout << message;break;case One_file:Write_to_Class_File("all", message);break;case Class_File:Write_to_Class_File(level_str, message);break;default:break;}}private:string _filename;LogStyle _style; // 打印风格,默认是往显示器打印static unordered_map<LogLevel, string> _levelMap;
};unordered_map<LogLevel, string> Log::_levelMap = {{Debug, "Debug"}, {Info, "Info"}, {Warning, "Warning"}, {Error, "Error"}, {Fatal, "Fatal"}};Log lg;class Conf
{
public:Conf(){lg.ChangeStyle(Screen);}
};Conf conf;

C语言版本利用了可变参数列表的知识,snprintf,vsnprintf等等函数

2.C++版本

#pragma once
#include <iostream>
#include <fstream> //ofstream不是一个单独的头文件,而只是一个类
#include <sstream>
#include <sys/stat.h>  //mkdir头文件
#include <sys/types.h> //mkdir头文件#include <unordered_map>namespace ns_helper
{class TimeHelper{public:static std::string timeStamp(){time_t time_stamp = time(nullptr);struct tm *local_tm = localtime(&time_stamp);std::string year = std::to_string(local_tm->tm_year + 1900);std::string month = std::to_string(local_tm->tm_mon + 1);std::string day = std::to_string(local_tm->tm_mday);std::string hour = std::to_string(local_tm->tm_hour);std::string minute = std::to_string(local_tm->tm_min);std::string second = std::to_string(local_tm->tm_sec);std::ostringstream oss; // 要往oss当中写数据,所以用ostringstreamoss << year << "-" << month << "-" << day << " " << hour << ":" << minute << ":" << second;return oss.str();}};
}const std::string log_dir = "./log";
const std::string log_base_name = "./log/log";enum LogLevel
{DEBUG,   // 调试日志(修复问题,编写调试代码时使用)INFO,    // 普通信息日志(服务运行时打印服务消息)WARNING, // 告警日志(代表服务时出现了一些问题,建议修复对应问题)ERROR,   // 错误日志(代表服务器出现大问题,但是服务依然可以正常运行,需要立刻修复)FATAL    // 致命日志(代表服务器出现大问题导致服务无法正常运行,需要立刻马上抓紧修复)
};enum LogStyle
{Screen,  // 把日志打印到显示器上Whole,   // 把日志打印到同一个文件当中,不做分类Classify // 把日志按照日志等级进行分类并打印
};class Log
{
public:Log() : _style(Screen){// 目录文件的默认权限775,为了防止unmask掩码的干扰,我们将它清零umask(0);mkdir(log_dir.c_str(), 0775);}std::ostream &log(LogLevel level, const std::string &level_str, const std::string &filename, int line){// 0. 添加线程tidstd::string message = "[" + std::__cxx11::to_string(pthread_self()) + "]" + " ";// 1. 添加日志等级message += "[" + level_str + "]" + " ";// 2. 添加文件名message += "[" + filename + "]" + " ";// 3. 添加所在行message += "[" + std::to_string(line) + "]" + " ";// 4. 添加时间戳message += "[" + ns_helper::TimeHelper::timeStamp() + "]" + " ";if (_style == Screen){// 6. 打印并返回std::cout << message;return std::cout;}else{// 5.如果LogStyle不是Screen : 传入LogLevel调用函数(GetStream[private成员函数])  拿到对应文件名std::string log_file = getFileName(level);static std::ofstream _ofs; // 懒汉,调用我这个函数,我才实例化(C++11之后局部静态变量的定义是线程安全的)// 用ofstream打开对应文件 : 以std::ios::out | std::ios::app的方式打开if (_ofs.is_open())_ofs.close();                                   // 先关闭已经打开的文件 // 先关闭已经打开的文件(不关的话后面的文件打不开,不关就是BUG)_ofs.open(log_file, std::ios::out | std::ios::app); // app: 追加写if (!_ofs.is_open()){std::cout << "keep log to file error, return cout ... , message: " << message;return std::cout;}// 6. 打印并返回_ofs << message;return _ofs; // 我们的ofs是静态成员变量,所以不怕引用返回}// 6. 打印并返回(注意: 我们不要加endl因为显示器是行刷新机制,遇到换行就刷新缓冲区,我们要把缓冲区的刷新交由调用方管理,因为我们要实现<<的链式调用)}void setStyle(LogStyle style){_style = style;}LogStyle getStyle() const{return _style;}private:std::mutex mtx;std::string getFileName(LogLevel level) const{// 整个懒汉
#ifndef SWITCHstatic std::unordered_map<LogLevel, std::string> _level_map = {{DEBUG, log_base_name + ".debug"}, {INFO, log_base_name + ".info"}, {WARNING, log_base_name + ".warning"}, {ERROR, log_base_name + ".error"}, {FATAL, log_base_name + ".fatal"}};
#endif// 整个打印if (_style == Whole)return log_base_name + ".all";// 分类打印else{
#ifdef SWITCHswitch (level){case DEBUG:return log_base_name + ".debug";case INFO:return log_base_name + ".info";case WARNING:return log_base_name + ".warning";case ERROR:return log_base_name + ".error";case FATAL:return log_base_name + ".fatal";}// 啥等级也不是return log_base_name + ".unknown";
#elseif (_level_map.count(level))return _level_map[level];// 啥等级也不是return log_base_name + ".unknown";
#endif}}LogStyle _style;// 静态成员变量 : 类内声明
};static Log lg_screen;
#define LOG_SCREEN(log_level) lg_screen.log(log_level, #log_level, __FILE__, __LINE__)static Log lg_whole;
#define LOG_WHOLE(log_level) lg_whole.log(log_level, #log_level, __FILE__, __LINE__)static Log lg_classify;
#define LOG_CLASSIFY(log_level) lg_classify.log(log_level, #log_level, __FILE__, __LINE__)
// 给一个对象的构造函数当中配置lg_whole和lg_classifyclass Conf
{
public:Conf(){lg_screen.setStyle(Screen);lg_whole.setStyle(Whole);lg_classify.setStyle(Classify);}
};
static Conf conf;

在这里插入图片描述
C++版本利用了宏,#预处理符号,ostream是ofstream的父类这些知识

大家想用哪个用哪个

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

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

相关文章

1-Maven-settings配置

1-Maven-settings配置 整理下Maven工具的使用。 【本地仓库、私服、镜像仓库、远程仓库、中央仓库】 本文基于阅读其他博客和对公司Maven配置的学习整理出来的。希望通过本此学习能对Maven有个整体性的掌控。 顺序&#xff1a;profile.repository > pom文件中的repository &…

asp.net core使用httpclient

主要讲解常见的get请求和post请求 GET var client new HttpClient(); //3秒钟不响应就超时 client.TimeoutTimeSpan.FromSeconds(3); using HttpResponseMessage response await client.GetAsync("todos/3"); var jsonResponse await response.Content.ReadAsSt…

postman测试接口(springboot+shiro)带token也不通的解决方案

前几天做项目遇到个问题&#xff0c;在系统可以正常使用的接口&#xff0c;拿postman带token访问时候确一直不通&#xff08;后台返回需要登录的提示信息&#xff09; 但是我明明加了token的呀 打断点发现 subject.isAuthenticated()false 查资料猜测可能是因为请求的安全上…

【WP】猿人学_16_js逆向_window蜜罐

https://match.yuanrenxue.cn/match/16 抓包分析 荷载一个加密参数&#xff0c;一个时间戳 时间: 2024-06-07 15:52:31时间戳: 1717746751 1717746751000时间戳和现在对得上&#xff0c;直接生成就行。 追栈 追栈找m的生成位置。 点进去打断点&#xff0c;重新点击其他…

C语言基础——函数

ʕ • ᴥ • ʔ づ♡ど &#x1f389; 欢迎点赞支持&#x1f389; 个人主页&#xff1a;励志不掉头发的内向程序员&#xff1b; 专栏主页&#xff1a;C语言基础&#xff1b; 文章目录 前言 一、函数的概念 二、库函数 2.1 库函数和头文件 2.2 库函数的使用/…

出售iPhone前的必做步骤:完全擦除个人数据的方法

当您准备在闲鱼上转售旧 iPhone、将其捐赠、送给朋友或通过 Apple 回收之前&#xff0c;您可能会选择执行“恢复”操作来擦除您的数据。但请注意&#xff0c;这一操作并不能真正删除设备中的数据。被“删除”或“格式化”的数据实际上仍存在于 iPhone 中&#xff0c;只是被系统…

SpringBoot+Vue在线文档管理系统(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 系统角色对应功能 员工管理员 系统功能截图

《精通ChatGPT:从入门到大师的Prompt指南》第1章:认识ChatGPT

第1章&#xff1a;认识ChatGPT 1.1 ChatGPT是什么 ChatGPT&#xff0c;全称为Chat Generative Pre-trained Transformer&#xff0c;是由OpenAI开发的一种先进的自然语言处理模型。它利用了深度学习中的一种技术——Transformer架构&#xff0c;来生成类人文本。ChatGPT通过对…

贪心算法-数组跳跃游戏(mid)

目录 一、问题描述 二、解题思路 1.回溯法 2.贪心算法 三、代码实现 1.回溯法实现 2.贪心算法实现 四、刷题链接 一、问题描述 二、解题思路 1.回溯法 使用递归的方式&#xff0c;找到所有可能的走步方式&#xff0c;并记录递归深度&#xff08;也就是走步次数&#x…

玩转ChatGPT:最全学术论文提示词分享【上】

学境思源&#xff0c;一键生成论文初稿&#xff1a; AcademicIdeas - 学境思源AI论文写作 在当今数字时代&#xff0c;人工智能&#xff08;AI&#xff09;技术正迅速改变各行各业的运作方式。特别是&#xff0c;OpenAI的ChatGPT等语言模型以其强大的文本生成能力&#xff0c;…

【CTF MISC】XCTF GFSJ0170 János-the-Ripper Writeup(文件提取+ZIP压缩包+暴力破解)

Jnos-the-Ripper 暂无 解法 用 winhex 打开&#xff0c;提到了 flag.txt。 用 binwalk 扫描&#xff0c;找到一些 zip 压缩包。 binwalk misc100用 foremost 提取文件。 foremost misc100 -o 100flag.txt 在压缩包里。 但是压缩包需要解压密码。 用 Ziperello 暴力破解。 不…

mac安装nigix且配置 vue/springboot项目(本地/服务器)

一、mac安装Nigix 1. 查看是否存在 nginx 执行brew search nginx 命令查询要安装的软件是否存在 brew search nginx 2. 安装nginx brew install nginx 3. 查看版本 nginx -v 4. 查看信息 查看ngxin下载的位置以及nginx配置文件存放路径等信息 brew info nginx 下载的存…

Angular 由一个bug说起之六:字体预加载

浏览器在加载一个页面时&#xff0c;会解析网页中的html和css&#xff0c;并开始加载字体文件。字体文件可以通过css中的font-face规则指定&#xff0c;并使用url()函数指定字体文件的路径。 比如下面这样: css font-face {font-family: MyFont;src: url(path/to/font.woff2…

IDEA 中设置 jdk 的版本

本文介绍一下 IDEA 中设置 jdk 版本的步骤。 一共有三处需要配置。 第一处 File --> Project Structure Project 和 Modules 下都需要指定一下。 第二处 File --> Settings 第三处 运行时的配置

Linux基础2-基本指令4(cp,mv,cat,tac)

上篇文章我们说到了rmdir,rm,man,echo.重定向等知识。 Linux基础1-基本指令3-CSDN博客 本文继续梳理其他基础指令 1.本章重点 1.使用cp命令拷贝文件 2.使用mv命令移动文件 3.使用cat&#xff0c;tac查看小文本文件 2.cp命令 在linux中使用cp命令来拷贝粘贴文件 cp src(原文…

解决Nginx出现An error occurred问题

每个人遇到Nginx的An error occurred情况可能都不一样&#xff08;见图1&#xff09;&#xff0c;Nginx造成该错误的原因&#xff1a; 1. 我在配置域名解析成IP时&#xff0c;没有把所有解析配置都修改&#xff0c;见图2&#xff1a;解析 *.hanxiaozhang.xyz 配置的是新IP地…

Nvidia Jetson/Orin/算能 +FPGA+AI大算力边缘计算盒子:潍柴雷沃智慧农业无人驾驶

潍柴雷沃智慧农业科技股份有限公司&#xff0c;是潍柴集团重要的战略业务单元&#xff0c;旗下收获机械、拖拉机等业务连续多年保持行业领先&#xff0c;是国内少数可以为现代农业提供全程机械化整体解决方案的品牌之一。潍柴集团完成对潍柴雷沃智慧农业战略重组后&#xff0c;…

轻松解决问题!教你文件怎么解除只读模式!

在日常使用电脑时&#xff0c;我们有时会遇到文件或文件夹被设定为只读模式的情况&#xff0c;这可能会限制我们对文件的修改和编辑。然而&#xff0c;解除只读模式并获得文件的完全控制是一个相对简单的过程&#xff0c;只需要掌握一些基本的技巧和方法。在本文中&#xff0c;…

java版spring cloud 知识付费平台的功能模块与子模块划分

随着互联网技术的飞速发展&#xff0c;知识付费平台已经成为了我国在线教育领域的一颗新星。这些平台以用户需求为出发点&#xff0c;围绕高质量的内容打造&#xff0c;利用互联网技术为用户提供了一个便捷、高效的学习环境。它们汇聚了丰富的专业知识&#xff0c;覆盖了职业技…

打造精美电子画册,提升企业形象的方法

在当今数字化时代&#xff0c;企业形象的表达方式正在发生深刻变革。精美电子画册作为一种新兴的传播媒介&#xff0c;不仅能够展现企业风采、提升品牌价值&#xff0c;还能够吸引潜在客户、增强市场竞争力。 接下来告诉大家一些简单的制作方法&#xff0c;可以收藏起来哦 1.首…