Linux--高级IO--select--0326

目录

IO为什么低效?

1.快速理解五种IO模式

2.五种IO模型

3.非阻塞IO

fcntl()

 4.IO多路转接 select

select

fd_set类型

struct timeval*类型

5.Select的代码测试

5.1 问题一:一开始,我们只有一个listen套接字

5.2 问题二:建立连接成功了,可以accept()了,可以直接读/写吗?

5.2.1 对于更新位图结构的修改

5.2.2 对于处理事件逻辑的修改

 5.3 完整代码

5.4 测试结果

 6.select的优缺点


本文代码部分中使用的Sock.hpp 和 log.hpp在,本文把Sock.hpp的方法全部变成了静态成员函数

Linux相关博文中使用的头文件_cout头文件 linux_Gosolo!的博客-CSDN博客


之前提到过,我们所谓的IO接口,本质上就是拷贝函数,仅是将内核级缓冲区的数据拷贝过来,或者拷给内核级缓冲区。

IO为什么低效?

当我们在向屏幕输入数据时,如果我们不敲击键盘,那么进程在干嘛?阻塞。

当我们read/recv的时候,如果底层缓冲区没有数据,IO接口就会阻塞,也就是等待资源就绪。

当read/recv的时候,如果底层缓冲区中有数据,IO接口就会直接进行拷贝。

单位时间内,大部分时间IO类的接口其实都在等待。

所以IO操作,本质上就是等+拷贝。那么如何才叫高效的IO呢?

在单位的时间内,让等的比重变得很低,IO效率就会变高。

1.快速理解五种IO模式

以钓鱼为例。

张三:钓鱼时,眼睛一直死死的盯着鱼钩。鱼一咬钩,他就进行钓动作。

李四:钓鱼时,他会看一下鱼钩有没有晃动,如果没有,他就低头看会儿手机。每隔一段时间就会在看看鱼钩,这样往复。

王五:钓鱼时,他在鱼钩上绑了一个铃铛,然后就沉迷手机,等待铃铛响之后,他会进行钓动作。

赵六:钓鱼时,他一个人拿了一百竿鱼竿,同时观察这一百竿是否有鱼上钩,如果有,他就会对上钩的那个鱼竿进行钓动作。

田七:看到别人钓鱼,他也想吃鱼。于是让手底下的小王去钓鱼,给了他一杆鱼竿,诱饵等,又给了他一部电话。说“等调到鱼了你给我打电话”。于是田七离开了河边。

首先,回答一个问题。这五个人谁的效率最高?

赵六。在单位时间内,赵六的等待时间在概率上来说是最小的。所以高效。

然后我们把这五种钓鱼的方式套到IO模式上来

张三:阻塞式。

李四:非阻塞式。

王五:信号驱动。

赵六:多路复用。

田七和小王:异步IO

由于异步IO和同步IO有争议,我们这里以下方这个原则介绍。

IO=等+拷贝 所谓的参与,实际上要么是参与了等,要么是参与了拷贝,要么是两个都参与了

2.五种IO模型

阻塞式

数据没就位,阻塞挂起。操作系统觉察数据就绪时,会唤醒执行流。

非阻塞式

数据没就绪,执行流不断的询问。一旦数据就绪后,操作系统会把资源放进缓冲区,执行流会自己去询问。

信号驱动

内核将数据准备好时,使用SIGIO信号通知应用程序进行IO操作。

多路复用

进程受阻于select调用,等待可能多个套接字中的任何一个变为可读。等待过程是同时发生的,但是一旦有一个资源就绪,拷贝是原子性的,一段时间内只能执行一个。

异步IO

执行流通过一定的接口,将指定的缓冲区和要求交给操作系统,操作系统检测到资源就位,会自行完成拷贝。然后通过信号/其他方式通知执行流。

3.非阻塞IO

首先,之前recv等系统调用接口,本事就可以通过传入的参数进行非阻塞。但现在提供了一个通用的接口,只要需要非阻塞IO就可以通过下面这个接口进行非阻塞设置。

fcntl()

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd,int cmd,.../*arg*/);

传入的cmd不同,后面追加的参数也不同

cmd参数作用
F_DUPFD复制一个现有的描述符
F_GETFD或F_SETFD获得/设置文件描述符标记
F_GETFL或F_SETFL获得/设置文件状态标记
F_GETOWN或F_SETOWN获得/设置异步IO所有权
F_GETLK,F_SETLK或F_SETLKW获得/设置记录锁

这里我们使用第三种功能。

 一个简单的阻塞式IO

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
using std::cout;
using std::endl;

bool SetNonBlock(int fd)
{
    int f1=fcntl(fd,F_GETFL);//在底层获取当前fd对应的文件读写标志位
    if(f1<0) return false;
    fcntl(fd,F_SETFL,f1|O_NONBLOCK);//设置非阻塞
    return true;
}
int main()
{
    //SetNonBlock(0);//只用设置一次 后续就都是非阻塞了
    char buffer[1024];
    while(true)
    {
        ssize_t s=read(0,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            cout<<"echo: "<<buffer<<endl;
        }
        else
        {
            cout<<"read fail "<<endl;
        }
    }

    return 0;
}

将SetNonBlock放出来之后

 4.IO多路转接 select

select

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
                fd_set *exceptfds, struct timeval *timeout);
参数解释
返回值返回就绪的个数
nfds

等待的所有文件中,最大的文件描述+1

readfds

传进一个fd_set类型,表示fd_set里面设置

的文件描述符集,当读资源就绪时,select

可以捕获。

writefds

当写资源就绪时,select可以捕获

exceptfds

select捕获文件描述集中所有的文件出现的异常。

timeout

select等待多个fd,等待策略可以选择,

阻塞式传入nullptr,非阻塞式传入(0,0)

自定义等待时间(5,0),时间到了立马返回。如果在此期间有

fd就绪,timeout会输出距离下一次timeout的时间

fd_set类型

fd_set和信号集中sigset_t类型类似,都是一个位图结构。表示文件描述符集。当我们想要select关心一些文件的写操作是否就绪,就需要先把这些文件的文件描述符设置在一个fd_set的类型里面,然后将这个类型,传给writedfs参数。

上述三个fd_set类型的参数:

a.输入时,用户告诉内核,你要帮我关心哪个sock里面的哪一种事件。

b.输出时,内核告诉用户,我所关心的sock里,哪些sock上的哪类事件就绪了。

fd_set类型位图的操作函数

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

 注意:用户和内核都会修改同一个位图结构,这个参数用一次之后,一定需要进行重新设定。

fd_set是一个固定大小的位图,直接决定了select可以同时关心的fd的个数是有限的。

在我测试的环境中,大小为128字节,可以表示128*8个比特位,即128*8个文件描述符。

struct timeval*类型

struct timeval
{
    __time_t tv_sec; //秒
    __susecond_t tv_usec;//微秒
};

5.Select的代码测试

注:一开始的代码有很多问题,会在文中一边分析原因一边解决。

5.1 问题一:一开始,我们只有一个listen套接字

而且他是用来获取新连接的一个参数。

如何看待listen套接字?

可以将它看做读事件,只有底层的资源就绪之后,才能进行获取。那如果一开始没有新连接呢?那不就阻塞了吗?所以不能上来就调用accept();而需要让select()帮我们等,所以也就需要在调用select()之前,创建fd_set类型,并进行初始化。

using namespace std;

class SelectServer
{
public:
    SelectServer(const uint16_t &port=8080)
        :_port(port)
    {
        _listensock=Sock::Socket();
        Sock::Bind(_listensock,_port);//ip缺省值为 0.0.0.0
        Sock::Listen(_listensock);
        logMessage(DEBUG,"%s","create base socket success");
    }
    void Start()
    {
        fd_set rfds;
        FD_ZERO(&rfds);
        while(true)
        {
            //int sock=Sock::Accept();
            //放在里面是 因为当timeout的时间等待够3秒时,
            //由于其为输入输出型参数,下次循环中的timeout会变为{0,0}
            struct timeval timeout={3,0};
            FD_SET(_listensock,&rfds);
            int n=select(_listensock+1,&rfds,nullptr,nullptr,&timeout);
            switch(n)
            {
            case 0:
                logMessage(DEBUG,"time out...");
                break;
            case -1:  
                logMessage(WARNING,"select errno: %d : %s",errno,strerror(errno));
                break;
            default:
                //成功
                //具体细节后面详谈
                break;  
            }
        }
    }
    ~SelectServer()
    {
        if(_listensock>=0) close(_listensock);
    }
private:
    uint16_t _port;
    int _listensock;

};

5.2 问题二:建立连接成功了,可以accept()了,可以直接读/写吗?

不能!因为建立连接成功了,我们不知道数据什么时候会发送过来。recv(),read()可能被阻塞,导致服务器不能处理接下来的连接请求!

那select()清楚资源有没有就绪,怎么把我accept()来的文件描述符交给select呢?

nfds表示最大的文件描述符,随着获取的sock越来越多,添加到select中的套接字也会更多,也就决定着nfds每次都可能会变化,我们需要对他进行动态计算!

rfds/writefds/exceptfds:都是输入输出型参数,输入输出不一定是一样的。

比如让select关心1-10号文件描述符,但是其中有3,4就绪,那么select输出的rfds就会设置为3,4,其余的文件描述符就会被丢弃掉。所以注定了我们每一次都要对rfds进行重新设置。timeout同理。

综上,我们必须自己把合法的文件描述符单独保存起来,用来支持1.更新最大nfds 2.更新位图结构

5.2.1 对于更新位图结构的修改

//新增宏
#define NUM sizeof(fd_set)*8
#define FD_DONE -1

//新增成员变量
private:
    int _fd_array[NUM];


//------------分界线-------------------

class SelectServer
{
public:
    SelectServer(const uint16_t &port=8080)
        :_port(port)
    {
        _listensock=Sock::Socket();
        Sock::Bind(_listensock,_port);//ip缺省值为 0.0.0.0
        Sock::Listen(_listensock);
        logMessage(DEBUG,"%s","create base socket success");
        for(int i=0;i<NUM;i++)
        {
            _fd_array[i]=FD_NONE;
        }
        //做一个规定 _fd_array[0]=_listensock
        _fd_array[0]=_listensock;

    }
    void Start()
    {
        while(true)
        {
            DebugPrint();
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd=_listensock;
            for(int i=0;i<NUM;i++)
            {
                if(_fd_array[i]==FD_NONE) continue;
                FD_SET(_fd_array[i],&rfds);
                if(maxfd<_fd_array[i]) maxfd=_fd_array[i];
            }
            
            int n=select(maxfd+1,&rfds,nullptr,nullptr,nullptr);
            switch(n)
            {   
            case 0:
                logMessage(DEBUG,"time out...");
                break;
            case -1:  
                logMessage(WARNING,"select errno: %d : %s",errno,strerror(errno));
                break;
            default:
                //成功
                logMessage(DEBUG, "get a new link event..."); 
                HandlerEvent(rfds);

                break;  
            }
    }
    ~SelectServer()
    {
        if(_listensock>=0) close(_listensock);
    }

private:
    void HandlerEvent(const fd_set& rfds)
    {
        string clientip;
        uint16_t clientport=0;
        //FD_ISSET用于判断 _listensock在rfds中是否被设置 即是否就绪
        if(FD_ISSET(_listensock,&rfds))
        {
            //_listensock上面的读事件就绪了 表示可以读取了 即获取连接
            int sock=Sock::Accept(_listensock,&clientip,&clientport);
            if(sock<0)
            {
                logMessage(WARNING,"accept error");
                return;
            }
            logMessage(DEBUG,"get a new link success :[%s:%d] :%d",clientip.c_str(),clientport,sock);

            //找一个位置添加 我刚刚得到的sock套接字 好让select帮我关心
            int pos=1;
            for(;pos<NUM;pos++)
            {
                if(_fd_array[pos]==FD_NONE) break;
            }
            if(pos==NUM)
            {
                //已经满了 不能再关心其他的了
                logMessage(WARNING,"%s:%d","select server already full,close fd: %d",sock);
                close(sock);
            }
            else
            {
                _fd_array[pos]=sock;
            }
        }
    }
    void DebugPrint()
    {
        cout<<"_fd_array[]: ";
        for(int i=0;i<NUM;i++)
        {
            if(_fd_array[i]==FD_NONE) continue;
            cout<<_fd_array[i]<<" ";
        }
        cout<<endl;
    }
private:
    uint16_t _port;
    int _listensock;
    int _fd_array[NUM];

};

 当前逻辑只处理了一个套接字,即listensock,连接时候的处理动作。可随着套接字的不断增多,肯定也有读或者写的情况,所以HandlerEvent仍需要进行处理。

5.2.2 对于处理事件逻辑的修改

 只需要将我们刚才写的那一大串处理逻辑封装为Accept()

现在我们完成输入事件的处理逻辑

注意

测试时,我们发送的资源都是字符串,方便测试。按道理来说这里也需要制定协议,以保证我们读到的是一个完整的报文。

    void Recver(int pos)
    {
        // 读事件就绪:INPUT事件到来、recv,read
        logMessage(DEBUG, "message in, get IO event: %d", _fd_array[pos]);
        char buffer[1024];
        int n = recv(_fd_array[pos], buffer, sizeof(buffer)-1, 0);
        if(n > 0)
        {
            buffer[n] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fd_array[pos], buffer);
        }
        else if(n == 0)
        {
            logMessage(DEBUG, "client[%d] quit, me too...", _fd_array[pos]);
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
        else
        {
            logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[pos], errno, strerror(errno));
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
    }

 5.3 完整代码

#pragma once
#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__

#include <iostream>
#include <string>
#include <vector>
#include <sys/select.h>
#include <sys/time.h>
#include "log.hpp"
#include "Sock.hpp"

#define NUM sizeof(fd_set)*8
#define FD_NONE -1

using namespace std;

class SelectServer
{
public:
    SelectServer(const uint16_t &port=8080)
        :_port(port)
    {
        _listensock=Sock::Socket();
        Sock::Bind(_listensock,_port);//ip缺省值为 0.0.0.0
        Sock::Listen(_listensock);
        logMessage(DEBUG,"%s","create base socket success");
        for(int i=0;i<NUM;i++)
        {
            _fd_array[i]=FD_NONE;
        }
        //做一个规定 _fd_array[0]=_listensock
        _fd_array[0]=_listensock;

    }
    void Start()
    {
        while(true)
        {
            DebugPrint();
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd=_listensock;
            for(int i=0;i<NUM;i++)
            {
                if(_fd_array[i]==FD_NONE) continue;
                FD_SET(_fd_array[i],&rfds);
                if(maxfd<_fd_array[i]) maxfd=_fd_array[i];
            }
            
            int n=select(maxfd+1,&rfds,nullptr,nullptr,nullptr);
            switch(n)
            {   
            case 0:
                logMessage(DEBUG,"time out...");
                break;
            case -1:  
                logMessage(WARNING,"select errno: %d : %s",errno,strerror(errno));
                break;
            default:
                //成功
                logMessage(DEBUG, "get a new link event..."); 
                HandlerEvent(rfds);
                break;  
            }

        }
    }
    ~SelectServer()
    {
        if(_listensock>=0) close(_listensock);
    }

private:
    void Acceptr()
    {
        string clientip;
        uint16_t clientport=0;
        int sock=Sock::Accept(_listensock,&clientip,&clientport);
        if(sock<0)
        {
            logMessage(WARNING,"accept error");
            return;
        }
        logMessage(DEBUG,"get a new link success :[%s:%d] : %d",clientip.c_str(),clientport,sock);

        //找一个位置添加 我刚刚得到的sock套接字 好让select帮我关心
        int pos=1;
        for(;pos<NUM;pos++)
        {
            if(_fd_array[pos]==FD_NONE) break;
        }
        if(pos==NUM)
        {
            logMessage(WARNING,"%s:%d","select server already full,close fd: %d",sock);
            close(sock);
        }
        else
        {
            _fd_array[pos]=sock;
        }
    
    }
    void Recver(int pos)
    {
        // 读事件就绪:INPUT事件到来、recv,read
        logMessage(DEBUG, "message in, get IO event: %d", _fd_array[pos]);
        // 暂时先不做封装, 此时select已经帮我们进行了事件检测,fd上的数据一定是就绪的,即 本次 不会被阻塞
        // 这样读取有bug吗?有的,你怎么保证以读到了一个完整包文呢?
        char buffer[1024];
        int n = recv(_fd_array[pos], buffer, sizeof(buffer)-1, 0);
        if(n > 0)
        {
            buffer[n] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fd_array[pos], buffer);
        }
        else if(n == 0)
        {
            logMessage(DEBUG, "client[%d] quit, me too...", _fd_array[pos]);
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
        else
        {
            logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[pos], errno, strerror(errno));
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
    }

    void HandlerEvent(const fd_set& rfds)
    {
        for(int i=0;i<NUM;i++)
        {
            //没让select关心这个文件
            if(_fd_array[i]==FD_NONE) continue; 

            //让关心了 但是需要知道他是否就绪
            if(FD_ISSET(_fd_array[i],&rfds))
            {
                //就绪
                //现在需要判断他是链接事件  accept
                //还是输入事件  recv read
                if(_fd_array[i]==_listensock)
                {
                    Acceptr();
                }
                else
                {
                    Recver(i);
                }
            }
        }
    }
    void DebugPrint()
    {
        cout<<"_fd_array[]: ";
        for(int i=0;i<NUM;i++)
        {
            if(_fd_array[i]==FD_NONE) continue;
            cout<<_fd_array[i]<<" ";
        }
        cout<<endl;
    }
private:
    uint16_t _port;
    int _listensock;
    int _fd_array[NUM];

};

#endif

5.4 测试结果

 6.select的优缺点

优点:

效率高。适用于有大量连接,但是只有少量是活跃的场景。

缺点:

为了维护第三方数组,充满多次遍历数组。

而且每次调用select都需要手动设置fd集合,从接口使用角度来说也不方便。

每次调用select,都需要把fd集合从用户态拷贝到内核态,开销比较大。

同时每次调用select都需要在内核遍历传递进来的所有fd,开销比较大。

能够同时管理的fd是有上限的。

编码比较复杂。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《项目管理知识体系指南(PMBOK)》第7版之8大绩效域

项目绩效域被定义为一组对有效交付项目成果至关重要的相关活动。 《项目管理知识体系指南&#xff08;PMBOK&#xff09;》第7版将项目管理划分为干系人、团队、开发方法和生命周期、规划、项目工作、交付、测量、不确定性共8大绩效域。 一、干系人绩效域 解决与干系人相关的…

【对YOLOv8(ultralytics)打印测试结果的调整】(1)使得map值打印显示从0.551变为55.08 (2)打印出FPS

目录1. 最终打印效果2. 做两处更改2.1 修改map显示&#xff0c;在ultralytics-main/ultralytics/yolo/v8/detect/val.py中操作2.2 打印FPS&#xff0c;在ultralytics-main/ultralytics/yolo/engine/validator.py中操作❗❗❗ 兄弟姐妹们&#xff0c;如果看习惯了运行train.py时…

PMP应该如何备考?

PMP现在是新考纲&#xff0c;PMP新版大纲加入了 ACP 敏捷管理的内容&#xff0c;而且还不少&#xff0c;敏捷混合题型占到了 50%&#xff0c;前不久官方也发了通知 8月启用第七版《PMBOK》&#xff0c;大家都觉得考试难度提升了&#xff0c;我从新考纲考完下来&#xff0c;最开…

Moonbeam隆重推出您的个人开发小助手 — — Kapa.ai

Moonbeam为开发者提供内容详细的开发者文档和全天候的Discord支持。但假如&#xff1a;有人可以24/7查看Discord并在15秒之内就回复您的问题 — — 新推出的Kapa.ai机器人使这个假如成为现实。Kapa.ai是一款由ChatGPT支持的AI机器人&#xff0c;可以回答关于在Moonbeam上构建的…

【redis】单线程redis为什么这么快

本文以收录专栏 redis核心技术 前言 本专栏为了帮助大家更好的了解学习redis&#xff0c;同时也是自己记录学习redis的内容&#xff0c;包含了大部分的redis核心技术&#xff0c;分布式锁&#xff0c;主从复制等 目录 专题2-单线程redis为什么这么快 2.1redis只有单线程吗&a…

剑指offer-替换空格

替换空格一、解题思想二、代码的实现三、总结一、解题思想 题目&#xff1a;请实现一个函数 &#xff0c;把字符串中的每个空格替换成”%20“。例如&#xff1a;输入”We are happy.“&#xff0c;则输出”We%20are%20happy.“。 看到这个题目&#xff0c;我第一想到的是&#…

博客1:YOLOv5车牌识别实战教程:引言与准备工作

摘要:本篇博客介绍了本教程的目标、适用人群、YOLOv5简介和车牌识别的意义和应用场景。为后续章节打下基础,帮助读者了解YOLOv5和车牌识别的相关背景知识。 正文: 车牌识别视频 引言 欢迎来到YOLOv5车牌识别实战教程!在这个教程中,我们将一步步教你如何使用YOLOv5进行车…

【Git Bash】项目开发过程中需要知道 git stash 的用法

目录1. git stash的应用场景2. 常用git stash命令2.1 git stash2.2 git stash save "message"2.3 git stash list2.4 git stash show2.5 git stash show -p2.6 git stash apply2.7 git stash pop2.8 git stash drop stash{num}2.9 git stash clear3. stash只会保存已…

简单记录一下软著申请流程

模板我就不放了&#xff0c;网上很多&#xff0c;随便下几个结合就行了 总体来说&#xff0c;我是2023.2.19号寄出&#xff0c;2023.4.6看到成功了&#xff0c;总共50天左右。 大家确实不需要网上买那种服务&#xff0c;我也是第一次申请&#xff0c;感觉还是挺简单的。只要按…

洛谷B2038奇偶ASCII值判断

洛谷B2038 题目描述 任意输入一个字符&#xff0c;判断其 ASCII 是否是奇数&#xff0c;若是&#xff0c;输出 YES&#xff0c;否则&#xff0c;输出 NO 。 例如&#xff0c;字符 A 的 ASCII 值是 65&#xff0c;则输出 YES&#xff0c;若输入字符 B(ASCII 值是 66)&#xff0…

从零开始学习Kotlin,带你快速掌握该编程语言

前言 Kotlin是一种跨平台的静态编程语言&#xff0c;它可以在JVM、Android、浏览器、iOS等多个平台上运行。Kotlin的语法简洁易懂&#xff0c;具有高度的可读性和可维护性&#xff0c;同时还具有Java所不具备的许多优点。 Kotlin是一种静态类型、面向对象、函数式编程语言&am…

iOS 项目嵌入Flutter 运行

一 创建Flutter 模块命令行flutter create --template module my_flutter创建完成后&#xff0c;该模块和普通的Flutter项目一直&#xff0c;可以通过Android Studio或VSCode打开、开发、运行&#xff1b;和之前项目不同的iOS和Android项目是一个隐藏文件&#xff0c;并且我们…

多模态 |COGMEN: COntextualized GNN based Multimodal Emotion recognitioN论文详解

论文&#xff1a;COGMEN: COntextualized GNN based Multimodal Emotion recognitioN COGMEN: 基于GNN的多模态情感识别技术 论文实现可参考另外一篇论文&#xff1a; 本文主要分为俩部分&#xff0c;一是对论文的简单概括&#xff0c;二是对论文的翻译。 论文总结 论文翻译…

【学习笔记】SpringAOP的用法全解

文章目录Spring的AOP一、 Spring对AOP的实现包括以下3种方式**什么是AspectJ?**二、使用Spring的AOP1、准备工作2、尝试写一个简单的AOP demo3、代码如下&#xff1a;spring.xml业务类切面类测试类4、复习切面表达式1&#xff09;所有方法2&#xff09;指定路径下某个包及其子…

开心档之C++ 运算符

目录 C 运算符 算术运算符 实例 实例 关系运算符 实例 实例 逻辑运算符 实例 实例 位运算符 实例 实例 赋值运算符 实例 实例 杂项运算符 C 中的运算符优先级 实例 实例 运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 内置了丰富的运算符&…

算法设计-二分

一、有序和单调 ​ 二分本质上是一种更加智能的搜索状态空间的方式&#xff0c;他需要状态空间的状态呈现一种“有序的一维数组”的形式&#xff0c;然后再进行搜索。所以一开始的排序是无法避免的。 ​ 因为二分的写法问题&#xff0c;所以应当怎样排序也是有一定讲究的&…

黑马程序员 linux 学习笔记入门部分合集

ubuntu 安装 本课程使用 ubuntu 系统。 ubuntu 官网 - download。 上面会显示有两个版本&#xff0c;每年 ubuntu 发布两个版本&#xff0c;LTS 是长期维护版&#xff0c;所以相对会较稳定。 介绍 Linux 发行版本 不管什么版本&#xff0c;内核都是一样的。 RPM based&a…

“遥感+”蓝碳储量估算、红树林信息提取与论文写作

详情点击链接&#xff1a;“遥感”蓝碳储量估算、红树林信息提取与论文 一&#xff0c;光谱遥感数据及预处理 .1高光谱遥感数据 高光谱分辨率遥感是用很窄而连续的光谱通道对地物持续遥感成像的技术。在可见光到短波红外波段其光谱分辨率高达纳米数量级。高光谱图像数据…

Linux-Vim

一、Vim 配置 ​ vim界面打开以后很丑就不提了&#xff0c;关键有很多基本功能没有办法实现&#xff0c;所以需要自己配置&#xff0c;如果是linux系统&#xff0c;那么应该找到 /usr/share/vim/.vimrc​ 如果是windows装完git以后会自动一个vim&#xff0c;此时应该找到 Gi…

电子招标采购系统—企业战略布局下的采购寻源

​ 智慧寻源 多策略、多场景寻源&#xff0c;多种看板让寻源过程全程可监控&#xff0c;根据不同采购场景&#xff0c;采取不同寻源策略&#xff0c; 实现采购寻源线上化管控&#xff1b;同时支持公域和私域寻源。 询价比价 全程线上询比价&#xff0c;信息公开透明&#xff…
最新文章