【网络编程】高级IO

news2024/7/6 20:06:13

 

文章目录

  • 一、五种IO模型的基本理念
  • 二、IO重要概念
    • 1.同步通信与异步通信的对比
    • 2.阻塞VS非阻塞
  • 三丶非阻塞IO的代码演示
  • 四丶IO多路转接select
  • 总结


一、五种IO模型的基本理念

首先IO就是 等 + 数据拷贝,还记得我们之前实现服务器用的read/recv接口的,当时我们就说过,这个接口如果有数据,那么read/recv会拷贝完成之后进行返回,如果没有数据,则会阻塞式等待,等待的目的就是等待资源就绪一旦有资源就进行数据拷贝。

1.阻塞IO

在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.

 当进程调用recvfrom进行系统调用来读取内核中的数据,如果数据还没有准备好,recv就会直接阻塞等待数据就绪,一旦数据准备好就将数据从内核拷贝到用户空间,拷贝完成会返回成功的指示。

2.非阻塞IO

如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符 , 这个过程称为 轮询 . 这对 CPU 来说是较大的浪费 , 一般只有特定场景下才使用。

 当进程调用recvfrom进行系统调用来读取内核中的数据,如果数据没有准备好那么recv就会返回错误码,因为是非阻塞的所以需要过一段时间就来询问内核数据是否准备好,在其他时间可以让这个进程干一些其他的事情比如打印日志什么的,只需要隔一段时间去询问数据是否准备好,如果没准备好还是发送错误码,准备好就把数据从内核拷贝到用户空间并且返回成功指示。

3.信号驱动IO

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

 当数据还没有准备好时,我们可以让进程对sigaction做捕捉,一旦准备好了我们就捕捉到这个信号去拷贝数据。在没有准备好期间依旧可以干一些其他的事情。

4.IO多路转接

在于IO多路转接能够同时等待多个文件描述符的就绪状态

 注意:多路转接的原理是一次可以等待多个文件描述符,所以以前的接口不可以使用了,必须使用新的select系统调用。而select以及poll和epoll都是IO中等的那一步,一旦等成功了那么还是调用recvfrom进行数据拷贝即可。并且多路转接中recvfrom不会再阻塞,只要select等待成功recvfrom会直接进行数据的拷贝。

5.异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

 异步IO的原理就是让系统去等待数据,有数据了就给我拷贝到我指定的缓冲区当中,我只负责在缓冲区拿数据。这就相当于前面几个IO都是关注如何做饭,而异步IO只关注如何吃饭,对饭是怎么来的并不关心。

任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往 往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少。

二、IO重要概念

1.同步通信 vs 异步通(synchronouscommunication/asynchronous communication)

同步和异步关注的是消息通信机制 .
所谓同步,就是在发出一个 调用 时,在没有得到结果之前,该 调用 就不返回 . 但是一旦调用返回,就得到返回值了; 换句话说,就是由 调用者 主动等待这个 调用 的结果 ;
异步则是相反, 调用 在发出之后,这个调用就直接返回了,所以没有返回结果 ; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在 调用 发出后, 被调用者 通过状态、通知来通知调用者,或通过回调函数处理这个调用.
另外 , 我们回忆在讲多进程多线程的时候 , 也提到同步和互斥 . 这里的同步通信和进程之间的同步是完全不想干的概念.
进程 / 线程同步也是进程 / 线程之间直接的制约关系,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候 .
在看到 " 同步 " 这个词 , 一定要先搞清楚大背景是什么 . 这个同步 , 是同步通信异步通信的同步 , 还是同步与互斥的同步.

2.阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态 .
阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

三.非阻塞IO的代码演示

首先我们认识一下fcntl接口:

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的 cmd 的值不同 , 后面追加的参数也不相同 .
fcntl 函数有 5 种功能 :
复制一个现有的描述符(cmd=F_DUPFD).
获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
我们此处只是用第三种功能 , 获取 / 设置文件状态标记 , 就可以将一个文件描述符设置为非阻塞
void setNonBlock(int fd)
{
    int n = fcntl(fd,F_GETFL);
    if (n<0)
    {
        std::cerr<<"fcntl: "<<strerror(errno)<<std::endl;
        return;
    }
    fcntl(fd, F_SETFL, n | O_NONBLOCK);
}

F_GETFD获取文件描述符的状态标记位,函数返回-1表示设置失败

F_SETFL可以设置文件描述符的状态标记位,比如设置读或者设置写,如下图:

最后的O_NONBLOCK就是设置为非阻塞的选项。 当我们将设置文件描述符为非阻塞的函数写好后,先演示阻塞状态的结果,在演示非阻塞状态的结果:

int main()
{
    char buffer[1024];
    while (true)
    {
        printf(">>> ");
        fflush(stdout);
        ssize_t s = read(0,buffer,sizeof(buffer)-1);
        if (s>0)
        {
            buffer[s] = 0;
            std::cout<<"echo# "<<buffer<<std::endl;
        }
        else if (s == 0)
        {
            std::cout<<"read end"<<std::endl;
            break;
        }
        else 
        {

        }
    }
    return 0;
}

 我们直接死循环式的读取,首先创建一个缓冲区,然后将0号标准输入文件描述符内的数据读到我们自己的缓冲区,读取成功时在文件结尾放上\0然后打印即可。看到结果我们可知这是阻塞式读取,因为一旦我们不向标准输入文件描述符内打印内容就会阻塞在read函数,下面我们看看非阻塞的结果:

int main()
{
    char buffer[1024];
    setNonBlock(0);
    while (true)
    {
        printf(">>> ");
        fflush(stdout);
        ssize_t s = read(0,buffer,sizeof(buffer)-1);
        if (s>0)
        {
            buffer[s] = 0;
            std::cout<<"echo# "<<buffer<<std::endl;
        }
        else if (s == 0)
        {
            std::cout<<"read end"<<std::endl;
            break;
        }
        else 
        {

        }
        sleep(1);
    }
    return 0;
}

首先将0号描述符设置为非阻塞,因为测试的时候打印>>>太快了为了演示我们sleep1秒:

 可以看到即使我们不向0号文件描述符输入函数依旧会死循环的执行,体现的结果就是如果不输入就会持续打印>>>符号,并且我们输入的过程中也会打印>>>符号,这就是非阻塞!我们不用再阻塞到read接口等待数据输入了。

注意:我们完全可以写一些简单的函数比如打印日志什么的在循环内运行,效果与上图是一样的,如下图所示:

还记得刚开始我们说非阻塞IO如果数据没有准备好就返回错误码吗,我们知道read接口读取失败返回-1,下面我们验证一下:

 从结果上我们可以看到确实返回了错误码-1,下面我们把报错原因打印出来:

 ​​​​​​

 可以看到虽然返回了-1,但是并不是错误而是说资源没有就绪。实际上操作系统是给我们准备了一些错误码的:

 比如EAGAIN就是资源未就绪,EINTR是数据没读完被中断了,并不算错误:

 所以说实际上正确的写法是上面这样,因为这样我们才能知道此时没有出错只是资源没就绪。

以上就是非阻塞IO的代码演示了,下面我们介绍IO多路转接的select接口。

 四.IO多路转接select

系统提供 select 函数来实现多路复用输入 / 输出模型 .
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
int select(int nfds, fd_set *readfds, fd_set *writefds,
 fd_set *exceptfds, struct timeval *timeout);

因为select可以一次等待多个文件描述符,而文件描述符的本质就是数组下标,所以第一个参数就是需要检视的最大的文件描述符+1,+1是因为底层会遍历文件描述符。

readfds和writefds和exceptfds分别是读文件描述符集合,写文件描述符集合,异常文件描述符集合。

timeout是一个结构体,是用来设置select的等待时间的。下面我们看看timeval是什么:

什么意思呢。比如说我们传timeout={0,0}表示非阻塞的监视文件描述符,timeout=nullptr表示阻塞式的监视文件描述符,timeout={5,0}表示5s内阻塞式监视文件描述符,超过5秒非阻塞返回,并且后续timeout{5,0}变成{0,0}。 

就比如刚开始演示的阻塞式读取代码中,如果用select设置5,0就会是5s内只显示>>>等待用户输入,5s后返回错误码,返回后就和非阻塞一样的持续打印>>>

select返回值如果大于0表示有几个文件描述符就绪了,如果返回值等于0表示超时返回,如果返回值小于0说明select调用出现错误。

实际上我们的fd_set类型就是一个位图,当某个文件描述符读事件就绪,那么位图中的这个文件描述符的位置被置为1,写事件和异常事件同理,如下图:

 当我们调用的时候传入表示用户告诉内核哪些文件描述符需要被关心。

 当函数执行完,位图中哪个比特位被置为1就代表哪个文件描述符的事件已经就绪了。

下面是操作位图的接口:

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的全部位

认识了以上接口后我们就实现一下select服务器:

首先我们将创建套接字,绑定,监听,获取新链接四步分别封装成函数:

enum
{
    SOCKET_ERR = 2,
    USE_ERR,
    BIND_ERR,
    LISTEN_ERR
};
const uint16_t gport = 8080;
class Sock
{
private:

public:
    const static int gbacklog = 32;
    static int createSock()
    {
        // 1.创建文件套接字对象
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock == -1)
        {
            logMessage(FATAL, "create socket error");
            exit(SOCKET_ERR);
        }
        logMessage(NORMAL, "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,uint16_t port)
    {
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY绑定任意地址IP
        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)
    {
        if (listen(sock, gbacklog) < 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);
        //  sock是和client通信的fd
        int sock = accept(listensock, (struct sockaddr *)&peer, &len);
        // accept失败也无所谓,继续让accept去获取新链接
        if (sock < 0)
        {
            logMessage(ERROR, "accept error,next");
        }
        else 
        {
            logMessage(NORMAL, "accept a new link success");
            *clientip = inet_ntoa(peer.sin_addr);
            clientport = ntohs(peer.sin_port);
        }
        return sock;
    }
};

上面所有关于服务器的函数接口我们在实现TCP服务器的时候都讲过,不懂得可以去看看:

namespace select_ns
{
    static const int defaultport = 8080;
    class SelectServer
    {
    private:
        int _port;
        int _listensock;
    public:
        SelectServer(int port = defaultport)
        :_port(port)
        ,_listensock(-1)
        {

        }
        void initServer()
        {
            _listensock = Sock::createSock();
            Sock::Bind(_listensock,_port);
            Sock::Listen(_listensock);
        }
        void start()
        {
            for (;;)
            {
                fd_set rfds;
                FD_ZERO(&rfds);
                // 把lsock添加到读文件描述符集中
                FD_SET(_listensock, &rfds);
                struct timeval timeout = {1, 0};
                int n = select(_listensock+1,&rfds,nullptr,nullptr,&timeout);
                switch (n)
                {
                    case 0:
                        logMessage(NORMAL,"time out.....");
                        break;
                    case -1:
                        logMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                        break;
                    default:
                        //说明有事件就绪了
                        logMessage(NORMAL,"get a new link");
                        break;
                }
                sleep(1);
                /* std::string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(_listensock,&clientip,clientport);
                if (sock<0)
                {
                    continue;
                }
                //开始进行服务器的处理逻辑 */
            }
        }
        ~SelectServer()
        {
            if (_listensock != -1)
            {
                close(_listensock);
            }
        }
    };
}

上面是我们利用封装好的接口实现一个select服务器的框架,在服务器启动的函数中,我们需要创建文件描述符位图读对象,然后用FD_ZERO初始化为0,注意我们作为演示只演示如何读取,实际上写和异常都是和读一样的。设置1秒内阻塞式读取,我们通过select的返回值分为3种情况,1.select超时2.select错误3.检测到有事件就绪,一旦有事件就绪我们就打印一下。下面我们运行起来:

 没连接的时候肯定是打印time_out,当有连接时就打印get new:

 那么为什么会打印这么多get a new 呢?这是因为我们没有处理这个select获取到的文件描述符,导致位图中这个文件描述符的值一直为1所以一直打印,下面我们写一个处理函数专门处理已经就绪的文件描述符:

void HanderEvent(fd_set &rfds)
        {
            if (FD_ISSET(_listensock, &rfds))
            {
                //listensock必然就绪
                std::string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(_listensock, &clientip, clientport);
                if (sock < 0)
                {
                    return;
                }
                logMessage(NORMAL,"accept success [%s:%d]",clientip.c_str(),clientport);
            }
        }

当listen文件描述符读事件就绪,我们就获取新连接并且打印客户端的ip和端口号:

下面我们运行起来:

 

 我们可以看到一旦获取新连接成功这次就不像之前那样重复打印获取到新连接,而是继续等待新连接,这是因为读事件就绪我们处理了这个事件。

处理了这一点后,我们思考一下如何让select处理其他的文件描述符呢,比如我们现在要用accept返回的文件描述符通信,当客户端发送数据我们服务器显示这个数据即可,实际上一般要使用select,是需要程序员自己维护一个保存所有合法fd的数组,下面我们就实现一下:

首先我们创建一个数组和一个默认值,这个默认值用来初始化数组内的所有元素:

 fd_num代表这个数组所能存放的最大文件描述符个数,这个个数就是fd_set*8这么大。

void initServer()
        {
            _listensock = Sock::createSock();
            if (_listensock == -1)
            {
                logMessage(NORMAL,"createSock error");
                return;
            }
            Sock::Bind(_listensock,_port);
            Sock::Listen(_listensock);
            fdarray = new int[fd_num];
            for (int i = 0;i<fd_num;i++)
            {
                fdarray[i] = defaultfd;
            }
            fdarray[0] = _listensock;
        }

我们在初始化的时候需要开空间并且初始化所有值为-1(为什么是负数呢?因为文件描述符从0开始,如果是正数有可能影响某个文件描述符),既然开了空间那么不用了肯定是要析构的,所以还有析构函数,当然我们的监听套接字一定要在初始化的时候放在数组中管理起来:

 ~SelectServer()
        {
            if (_listensock != -1)
            {
                close(_listensock);
            }
            if (fdarray)
            {
                delete[] fdarray;
                fdarray = nullptr;
            }
        }

在start函数中当某个事件就绪了我们就执行hander函数,因为我们现在是用一个数组管理所有的文件描述符,所以hander方法变成了下面这样:

void HanderEvent(fd_set &rfds)
        {
            if (FD_ISSET(_listensock, &rfds))
            {
                //listensock必然就绪
                std::string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(_listensock, &clientip, clientport);
                if (sock < 0)
                {
                    return;
                }
                logMessage(NORMAL,"accept success [%s:%d]",clientip.c_str(),clientport);
                // 开始进行服务器的处理逻辑
                // 将accept返回的文件描述符放到自己管理的数组中,本质就是放到了select管理的位图中
                int i = 0;
                for (i = 0; i < fd_num; i++)
                {
                    if (fdarray[i] != defaultfd)
                    {
                        continue;
                    }
                    else
                    {
                        break;
                    }
                }
                if (i == fd_num)
                {
                    logMessage(WARNING, "server is full ,please wait");
                    close(sock);
                }
                else
                {
                    fdarray[i] = sock;
                }
                print();
            }
        }

 第一步首先判断监听套接字的读事件是否就绪,只有就绪了我们才做下面的操作。再得到新连接返回的用于通信套接字时,我们要将这个套接字放到select中的位图管理起来,所以首先遍历数组找到合法的文件描述符(如果使用的是默认值那么说明是非法的),找到合法描述符后我们首先判断刚刚遍历的过程中是否到数组结尾,如果到数组结尾说明数组中所有文件描述符都是合法的,这个时候需要记录日志数组已满,需要等待。如果没有到数组结尾则把刚刚accept返回的新文件描述符放到数组指定位置即可。后面我们加了一个打印函数为了方便看到结果:

void print()
        {
            std::cout << "fd list: ";
            for (int i = 0; i < fd_num; i++)
            {
                if (fdarray[i] != defaultfd)
                {
                    std::cout << fdarray[i] << " ";
                }
            }
            std::cout << std::endl;
        }

这个函数只会打印合法的文件描述符,当然还有一处地方没有修改,还记得select的第一个参数吗,这个参数是最大文件描述符+1,所以修改如下:

 首先假设最大文件描述符是监听套接字,然后去遍历数组,找到合法文件描述符把这个合法的文件描述符添加到读位图中,然后判断是否大于maxfd.下面我们看看效果吧:

 可以看到是没有问题的,每次新连接到来都会给我们把新连接的文件描述符添加到数组中,最后数组会将这些合法的文件描述符放到select中监视。

下面我们继续修改代码让我们的select服务器支持正常的IO通信:

因为我们需要处理所有文件描述符,所以我们在hander函数中将accept部分封装起来,然后根据不同的文件描述符实现对应的功能:

void HanderEvent(fd_set &rfds)
        {
            for (int i = 0;i<fd_num;i++)
            {
                //过滤掉非法的文件描述符
                if (fdarray[i] == defaultfd) 
                    continue;
                //如果是listensock事件就绪,就去监听新连接获取文件描述符,如果不是listensock事件,那么就是普通的IO事件就绪了 
                if (FD_ISSET(fdarray[i], &rfds) && fdarray[i] == _listensock)
                {
                    Accepter(_listensock);
                }
                else if (FD_ISSET(fdarray[i], &rfds))
                {
                    Recver(fdarray[i],i);
                }
                else 
                {

                }
            }
        }

当是listensock文件描述符就绪时,我们就调用accept函数去处理监听新连接,如果是普通文件描述符就绪那么就执行读数据函数:

void Accepter(int listensock)
        {
            // listensock必然就绪
            std::string clientip;
            uint16_t clientport = 0;
            int sock = Sock::Accept(listensock, &clientip, clientport);
            if (sock < 0)
            {
                return;
            }
            logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
            // 开始进行服务器的处理逻辑
            // 将accept返回的文件描述符放到自己管理的数组中,本质就是放到了select管理的位图中
            int i = 0;
            for (i = 0; i < fd_num; i++)
            {
                if (fdarray[i] != defaultfd)
                {
                    continue;
                }
                else
                {
                    break;
                }
            }
            if (i == fd_num)
            {
                logMessage(WARNING, "server is full ,please wait");
                close(sock);
            }
            else
            {
                fdarray[i] = sock;
            }
            print();
        }

accept就是刚刚hander函数内的代码,我们直接讲解如何处理数据:

 void Recver(int sock,int pos)
        {
            //注意:这样的读取有问题,由于没有定协议所以我们不能确定是否能读取一个完整的报文,并且还有序列化反序列化操作...
            //由于我们只做演示所以不再定协议,在TCP服务器定制的协议大家可以看看
            char buffer[1024];
            ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
            if (s>0)
            {
                buffer[s] = 0;
                logMessage(NORMAL,"client# %s",buffer);
            }
            else if (s == 0)
            {
                //对方关闭文件描述符,我们也要关闭并且下次不让select关心这个文件描述符了
                close(sock);
                fdarray[pos] = defaultfd;
                logMessage(NORMAL,"client quit");
            }
            else 
            {
                //读取失败,关闭文件描述符
                close(sock);
                fdarray[pos] = defaultfd;
                logMessage(ERROR,"client quit: %s",strerror(errno));
            }
            //2.处理 request
            std::string response = func(buffer);

            //3.返回response
            write(sock,response.c_str(),response.size());
        }

首先我们对数据的处理是有问题的,因为正常情况下需要定制协议保证读到的是一个完整的报文,并且还要序列化和反序列化,今天为了演示我们就不再做这些工作。读取到数据后我们在服务端进行一个回显打印,如果读取失败或者客户端关闭文件描述符,这个时候我们服务器也应该关闭对应的文件描述符,并且我们要将数组中的这个文件描述符设置为非法状态,这样的话下次select就不会再监视这个文件描述符了。拿到客户端的消息后我们直接调用一个func函数去处理,func是我们新加的一个用于演示的函数,如下图:

 

 可以看到我们就只是简单的将客户端的消息进行返回,而实际上这个函数的作用是处理客户端请求并且有一个响应经过序列化和反序列化后发送给客户端。

拿到响应后我们直接write写回到用于通信的文件描述符中。这样我们就将代码修改完毕,下面运行起来看看:

 可以看到程序运行起来也是没有问题的。


总结

下面我们总结一下select服务器的特点:

1.select能同时等待的文件描述符是有上限的,改内核只能提高一点上限,并不能完全解决。

2.select服务器必须借助第三方数组来维护合法的文件描述符。

3.select的大部分参数是输入输出型的,调用select前,要重新设置所有的文件描述符,调用之后,我们还要检查更新所有的文件描述符,这带来的就是遍历的成本。

4.select的第一个参数为什么是最大文件描述符+1呢?这是因为在内核层面也需要遍历文件描述符

5.select采用位图,所以会频繁的从内核态切换为用户态,再从用户态切换为内核态来回的进行数据拷贝,是有拷贝成本的问题的。

那么如何解决上面的问题呢?后面的poll和epoll服务器会解决这个问题。

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

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

相关文章

Codeforces Round 892 (Div. 2)

A.最大值只能由自己除&#xff0c;所以无解的情况只能是全部相同&#xff0c;否则直接最大值放c即可 #include<bits/stdc.h> using namespace std; const int N 2e510,mod998244353; #define int long long typedef long long LL; typedef pair<int, int> PII;in…

【TI毫米波雷达笔记】MMWave配置流程避坑

【TI毫米波雷达笔记】MMWave配置流程避坑 在TI SDK目录下的mmwave.h文档说明中 强调了要按以下配置&#xff1a; mmWave API The mmWave API allow application developers to be abstracted from the lower layer drivers and the mmWave link API.The mmWave file should b…

macOS使用ffmpeg与QT进行音视频推拉流

1.先启动流服务器 ./mediamtx 2.开始推流: ffmpeg -re -stream_loop -1 -i /Users/hackerx/Desktop/test.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://127.0.0.1:8554/stream 3. 安装ffmpeg 4.4 brew install ffmpeg4 4.添加ffmpeg头文件目录与库目录 5.链接ffmpeg相关库…

【Rust】Rust学习 第十一章编写自动化测试

Rust 是一个相当注重正确性的编程语言&#xff0c;不过正确性是一个难以证明的复杂主题。Rust 的类型系统在此问题上下了很大的功夫&#xff0c;不过它不可能捕获所有种类的错误。为此&#xff0c;Rust 也在语言本身包含了编写软件测试的支持。 编写一个叫做 add_two 的将传递…

⑤ Axios网络请求

Axios安装 cnpm install --save axios post请求需要用到的&#xff1a; cnpm install --save querystring(用来转换格式的) 引入 一般是全局引入&#xff0c;在main.js中引入 全局引入后的get和post方式使用 get请求方式 post请求方式 先引入&#xff1a; axios封装…

Android之版本号、版本别名、API等级列表(全)(一百六十二)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

前端性能优化——包体积压缩,打包速度提升,提升浏览器响应的速率

前端代码优化 –其他的优化可以具体在网上搜索 压缩项目打包后的体积大小、提升打包速度&#xff0c;是前端性能优化中非常重要的环节&#xff0c;结合工作中的实践总结&#xff0c;梳理出一些 常规且有效 的性能优化建议 ue 项目可以通过添加–report命令&#xff1a; "…

Java进阶-Oracle(二十)(1)

&#x1f33b;&#x1f33b; 目录 一、Oracle 数据库介绍1、Oracle 的概述2、Oracle 的结构2、Oracle的功能 二、安装与卸载1、卸载2、安装 三、使用&#xff08;需要关注得只有下面这两个&#xff09;四、PLSQL 的简单使用五、DBeaver 的简单使用 一、Oracle 数据库介绍 1、O…

ORB-SLAM2第一节---地图初始化

单目初始化 1.前提条件&#xff08;640*480&#xff09; 参与初始化的两帧各自的特征点数目都需要大于100.两帧特征点成功匹配的数目需要大于或等于100.两帧特征点三角化成功的三维点数目需要大于50. 2.针对条件三 流程如下 记录当前帧和参考帧&#xff08;第一帧&#xff…

MyBaits动态SQL

MyBaits动态SQL <include>用法 <where>用法 <if>用法 CONCATlike使用${...}使用#{...}使用CONCAT()函数连接参数形式 <choose><when><otherwise>例子 limit 字段变量&#xff0c;内部属性“refid”&#xff0c;后跟自定义的一段内容的名字…

什么是管程?

前言 在并发编程领域&#xff0c;最核心的两个理念就是同步和互斥&#xff0c;并发编程就是围绕这两个核心概念来完成的。 互斥&#xff1a;同一时刻只能有一个线程持有共享资源同步&#xff1a;多个线程之间协调、互作 在最初&#xff0c;人们利用信号量机制来实现互斥和同步…

多元最短路(Floyd)

是一个基于动态规划的全源最短路算法。它可以高效地求出图上任意两点之间的最短路 时间复杂度 O(n^3) 状态转移方程 f[i][j]min(f[i][j],f[i][k]f[k][j]) 核心代码 void floyd(){for(int k1;k<n;k)for(int i1;i<n;i)for(int j1;j<n;j)s[i][j]min(s[i][j],s[i][k…

使用巴特沃兹滤波器的1D零相位频率滤波研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

使用vscode在vue项目中重命名文件选择了更新导入路径仍有部分导入路径没有更新

背景: 将一个js文件重命名&#xff0c;vscode弹出是否更新导入路径&#xff0c;选择更新导入后&#xff0c;发现js文件中导入路径都自动更新&#xff0c;vue文件中路径都没有更新。 解决方案&#xff1a; 在设置中搜索updateimport&#xff0c;将最下面的Vue>Update Imports…

2023/08/13_____JMM JAVA Memory Model JAVA内存模型

JMM JAVA Memory Model java内存模型 作用&#xff1a;缓存一致性协议&#xff0c;用于定义数据读写的规则&#xff08;遵守&#xff0c;找到这个规则&#xff09; JMM定义了线程2工作内存和主内存之间的抽象关系&#xff1a;线程之间的共享变量存储在主内存&#xff08;main …

(贪心) 剑指 Offer 63. 股票的最大利润 ——【Leetcode每日一题】

❓剑指 Offer 63. 股票的最大利润 难度&#xff1a;中等 假设把某股票的价格按照时间先后顺序存储在数组中&#xff0c;请问买卖该股票一次可能获得的最大利润是多少&#xff1f; 示例 1: 输入: [7,1,5,3,6,4] 输出: 5 解释: 在第 2 天&#xff08;股票价格 1&#xff09;的…

在 Windows 中恢复数据的 5 种方法

发生数据丢失的原因有多种。无论是因为文件被意外删除、文件系统或操作系统损坏&#xff0c;还是由于软件或硬件级别的存储故障&#xff0c;数据都会在您最意想不到的时候丢失。今天我们重点介绍五种数据恢复方法&#xff0c;以应对意外情况的发生。 1.从另一台机器启动硬盘 如…

24近3年内蒙古大学自动化考研院校分析

今天给大家带来的是内蒙古大学控制考研分析 满满干货&#xff5e;还不快快点赞收藏 一、内蒙古大学 学校简介 内蒙古大学位于内蒙古自治区首府、历史文化名城呼和浩特市&#xff0c;距北京400余公里&#xff0c;是中华人民共和国成立后党和国家在民族地区创办的第一所综合大…

阿里云轻量应用服务器使用教程(从购买配置、连接到网站上线)

阿里云轻量应用服务器怎么使用&#xff1f;阿里云百科分享轻量应用服务器从购买、配置建站环境、轻量服务器应用服务器远程连接、开端口到网站上线全流程&#xff1a; 目录 阿里云轻量应用服务器使用教程 步骤一&#xff1a;购买一台轻量应用服务器 步骤二&#xff1a;重置…

深入理解 Vue 3 计算属性:优雅地处理响应式数据计算

计算属性的定义 在 Vue3 的 HTML 模板中是支持 JavaScript 表达式的&#xff0c;例如&#xff1a; <h2>买5个共计&#xff1a;{{ price * 5 }} 元</h2>但是如果当表达式过于复杂时&#xff0c;模板代码就会变得非常臃肿并且可读性就会变差&#xff0c;恰巧&#…