Linux多路转接之select

news2024/12/23 11:09:17

文章目录

  • 一、IO的理解
  • 二、五种IO模型
    • 1.阻塞式IO
    • 2.非阻塞式IO
    • 3.信号驱动式IO
    • 4.IO多路转接
    • 5.异步IO
    • 6.五种IO模型的总结
  • 三、非阻塞式IO
    • 1.fcntl函数
  • 四、IO多路转接之select的介绍
  • 五、编写select服务器
    • 1.将获取连接时设置为select多路转接
    • 2.获取连接成功后的读取数据
  • 六、select多路转接的总结
    • 1.select的编写代码规律
    • 2.select的优点和缺点

一、IO的理解

我们在调用一些读或者写的函数接口时,比如说调用read或者recv函数接口从缓冲区里读取数据,它是有两个阶段的。首先第一个阶段是等待阶段,如果当前缓冲区里没有数据,read函数或者recv函数必须阻塞式的等待缓冲区的数据。第二个阶段是数据拷贝阶段,当缓冲区有数据到来的时候,read函数就可以将缓冲区的数据读取上来了。这个所谓的读取动作,实质上就是将缓冲区的数据拷贝到read函数自己的缓冲区。

所以IO一共有两个阶段组成,分别是等待阶段和数据拷贝阶段。

如果IO操作的过程中,大部分时间都在等待缓冲区的数据,这种IO事实上是非常低效的。所以什么叫高效的IO呢?单位时间内等待时间所占的比重越小,IO的效率越高效。

二、五种IO模型

1.阻塞式IO

阻塞式IO就是指当我们进行IO操作时,比如调用recvfrom函数时,如果缓冲区内没有数据,我们就必须阻塞式地等待,在等待的这段时间里我们什么也不能做就是阻塞在那里,等待数据的到来。当缓冲区有数据到来的时候,我们再将缓冲区的数据拷贝上来,自此也就完成了IO操作。

在这里插入图片描述

2.非阻塞式IO

非阻塞式IO指的是当我们在进行IO操作时,如果缓冲区还没有数据,进程也会在等待数据,但不是阻塞式地等待,而是调用recvfrom函数时发现缓冲区没有数据就直接返回,继续去做其它事情,过一会再来调用recvfrom函数检测缓冲区是否有数据,如果有数据到来了就将数据拷贝上来,没有数据的话就继续返回,下一次继续检测,一直不断地轮询检测,直到缓冲区有数据到来。

在这里插入图片描述

3.信号驱动式IO

信号驱动式IO指的是先建立SIGIO的信号处理程序,用来检测缓冲区当前是否有数据,当缓冲区没有数据的时候,进程不需要调用recvfrom函数阻塞式等待数据,可以去做其他事情。一旦缓冲区有数据到来了,信号处理程序会发送信号过来,当进程接收到了该信号以后,再调用回调函数执行recvfrom函数,此时由于缓冲区有数据,所以IO操作不需要等待,直接就可以将缓冲区的数据拷贝上来。

在这里插入图片描述

4.IO多路转接

因为IO操作一共有两个阶段,分别是等待数据阶段和拷贝数据阶段。多路转接就是将这两个阶段分开,调用select函数来等待缓冲区的数据,当缓冲区有数据时再调用recvfrom函数从缓冲区中拷贝数据。

在这里插入图片描述

5.异步IO

Boost库中也为我们提供了一些异步IO的接口,所谓异步IO就是不需要该进程自己等待数据就绪,即使数据就绪了也不需要自己拷贝数据,也就是说IO全程该进程都不参与也不关心,只要拿到IO的结果即可。比如我们调用aio_read函数接口,向操作系统指定缓冲区,操作系统会为我们等待缓冲区的数据就绪,进程可以做其它事情。当缓冲区数据就绪时,操作系统会将缓冲区的数据帮我们拷贝到我们的缓冲区,当操作系统将数据拷贝上来时会告诉进程,进程收到信号之后去处理数据就可以了。

在这里插入图片描述

6.五种IO模型的总结

五种IO模型中,效率最高的是多路转接。

其中阻塞式IO、非阻塞式IO、信号驱动式IO、IO多路转接统称为同步IO。同步IO中的同步概念与多进程多线程的同步概念不一样,同步IO是指自己参与到了IO操作当中,异步IO是指自己没有参与到IO操作中,而是让别人帮自己操作。所以同步IO和异步IO的区别就在于自己有没有参与IO操作。

三、非阻塞式IO

1.fcntl函数

一个文件被打开以后都会被分配一个文件描述符,一个文件描述符的默认工作方式都是阻塞式IO。fcntl函数可以设置指定文件描述符的工作方式,函数原型如下:

int fcntl(int fd, int cmd, ...);

其中形参中fd代表的是需要设置哪一个文件描述符,cmd是选项参数,传入的cmd不同,代表的功能也不同。第三个参数是状态参数,这是个可变参数。fcntl函数有5种功能:

  1. 复制一个现有的文件描述符:cmd = F_DUPFD
  2. 获得/设置文件描述符标记:cmd = F_GETFD 或 cmd = F_SETFD
  3. 获得/设置文件状态标记:cmd = F_GETFL 或 cmd = F_SETFL
  4. 获得/设置异步IO所有权:cmd = F_GETOWN 或 cmd = F_SETOWN
  5. 获得/设置记录锁:cmd = F_GETLK 或 cmd = F_SETLK

利用fcntl函数可以将文件描述符设置成非阻塞式IO,首先我们需要用fcntl函数获得指定描述符的文件状态标记,然后在这个状态标记的基础上再设置文件状态标记,添加上O_NONBLOCK标记即可将该文件描述符设置为非阻塞式IO。

bool SetNonBlock(int sock)
{
	int flag = fcntl(sock, F_GETFL);
	if(flag == -1) return false;
	int n = fcntl(sock, F_SETFL, flag | O_NONBLOCK);
	if(n == -1) return false;
	return true;
}

四、IO多路转接之select的介绍

多路转接可以让我们在等待IO资源就绪的时候一次等待多个文件描述符。首先介绍的第一种多路转接方案是select方案。select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。当我们调用select函数时,进程会停在select函数这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

所以多路转接就是在等待文件描述符的状态变化,所谓的状态变化指的就是比如不可读状态变成可读状态,可写状态变成不可写状态等,这些状态变化它都能检测到。

select函数原型:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select函数是帮助我们等待IO事件就绪的,分别可以等待读事件就绪、写事件就绪和异常事件就绪。下面介绍一下select函数的参数:

  1. int nfds:该参数填的是要等待的文件描述符集合中最大的文件描述符+1,比如文件描述符的集合是3、5、7、8,那么该参数就应该填9。
  2. fd_set * readfds:这是个输入输出型参数,fd_set是文件描述符集合,这是一个位图结构,对应的比特位被置为1代表该文件描述符在这个集合中。当我们输入这个参数时是告诉select函数哪些文件描述符是需要等待读事件就绪的。当该参数输出时是告诉我们哪些文件描述符的读事件已经就绪了。
  3. fd_set * writefds:这也是一个输入输出型参数,当我们输入这个参数时是告诉select函数哪些文件描述符是需要等待写事件就绪的。当该参数输出时是告诉我们哪些文件描述符的写事件已经就绪了。
  4. fd_set * exceptfds:这也是一个输入输出型参数,当我们输入这个参数时是告诉select函数哪些文件描述符是需要等待异常事件就绪的。当该参数输出时是告诉我们哪些文件描述符的异常事件已经就绪了。
  5. struct timeval * timeout:这也是一个输入输出型参数,这个参数可以用来控制select函数的等待策略。在等待IO的时候等待策略一般有三种,阻塞式等待、非阻塞式等待和设定deadline,deadline时间之内,阻塞式等待,一旦超时了,立马会返回。这个参数输入的时候是填充一个timeval结构体,填充等待的时间。如果在deadline时间之内等待成功了,该参数就会输出剩余时间,并且select函数的返回值会返回一个整数,代表有多少个文件描述符的事件等待成功。如果超时了,select函数会返回0,如果函数出错了会返回-1。另外还需要提到的是,该参数在设置的时候,如果将等待时间全部设置为0,那么select会非阻塞式等待。如果该参数填为nullptr,那么select会永久阻塞式等待,所谓永久阻塞指的就是当select函数在等待的文件描述符集合中,只要有一个文件描述符没有就绪,就都会永久阻塞式地等待。只有文件描述符集合里的所有文件描述符都等待成功了,函数才会成功返回。

五、编写select服务器

下面我们通过编写select服务器来演示一下select多路转接的使用:

1.将获取连接时设置为select多路转接

首先我们利用TCP套接字搭建一个服务器,开始的步骤是获取套接字、bind网络信息、将其设置为listen监听状态。这些步骤完成以后,就可以让服务器循环获取连接了。但是,accept获取连接的时候,如果此时没有客户端连接服务器,服务器是会阻塞在accept函数这里的,原因是accept函数本质上也是IO操作,它也分为两个阶段,即等待连接到来和获取连接两个阶段,所以我们将accept的等待事件就绪行为交给select函数去做,当事件就绪时再让accept去获取连接,这样accept就不会阻塞了。

Sock.hpp:

#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

class Sock
{
public:
    static const int gbacklog = 20;

    static int Socket()
    {
        int listenSock = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock < 0)
        {
            exit(1);
        }
        int opt = 1;
        setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        return listenSock;
    }
    static void Bind(int socket, uint16_t port)
    {
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(socket, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }
    }
    static void Listen(int socket)
    {
        if (listen(socket, gbacklog) < 0)
        {
            exit(3);
        }
    }

    static int Accept(int socket, std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        int serviceSock = accept(socket, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            // 获取链接失败
            return -1;
        }
        if(clientport) *clientport = ntohs(peer.sin_port);
        if(clientip) *clientip = inet_ntoa(peer.sin_addr);
        return serviceSock;
    }
};

SelectServer.cc:

#include <iostream>
#include <sys/select.h>
#include "Sock.hpp"

using namespace std;

int main()
{
    // 获取套接字
    int listen_sock = Sock::Socket();
    // bind网络信息
    Sock::Bind(listen_sock, 8081);
    // 设置监听状态
    Sock::Listen(listen_sock);

    while (true)
    {
        // 获取连接的时候,本质上是在等待listen_sock套接字的事件就绪
        // 所以需要使用多路转接
        int max_fd = listen_sock + 1;
        fd_set read_fds;
        FD_ZERO(&read_fds);
        FD_SET(listen_sock, &read_fds);
        struct timeval time_out;
        time_out.tv_sec = 5;
        time_out.tv_usec = 0;

        int n = select(max_fd + 1, &read_fds, nullptr, nullptr, &time_out);
        // select的返回值有以下几种情况:
        // 如果在等待时间内正常返回,n返回的是有多少个文件描述符等待成功
        // 如果超时了,n返回0
        // 如果等待失败,n返回-1
        switch (n)
        {
        case 0:
            cout << "time out ... : " << (unsigned long)time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << " : " << strerror(errno) << endl;
            break;
        default:
            cout << "等待listen_sock成功... " << endl;
            break;
        }
    }

    return 0;
}

2.获取连接成功后的读取数据

在获取数据成功之后,我们需要读取数据了。之前我们实现的服务器是accept获取连接之后,就要创建多进程或者多线程,让新进程或者新线程去执行读取数据的动作,原来的进程继续accept获取连接。这样做的原因是如果只是单一执行流,在accept获取连接成功之后调用read读取数据,有可能会阻塞在这里,此时如果有其它连接到来服务器就无法获取新连接了,这样是不合理的,所以必须要使用多个进程或者多个线程。

但是今天我们使用多路转接的方案,就不再需要使用多进程或者多线程了,原因是阻塞等待的阶段我们不用自己做了,而是让select函数帮我们等待。当文件描述符的事件就绪时,我们就可以直接调用read函数进行数据拷贝。

但是又会有一个新的问题,select函数的参数readfds读文件描述符集合是一张位图,并且它是输入输出型参数,也就意味着如果我们一开始输入进去的有100个文件描述符需要select帮我们等待读事件就绪,在规定时间内只有一个文件描述符就绪的话,最后输出的位图就只有那一个文件描述符了,其它的文件描述符就因此丢失了。所以我们必须维护一张文件描述符表,用来保存我们需要等待的文件描述符。

维护这个文件描述符表还有另外一个必要之处就是,在获取连接成功之后,会得到一个新的文件描述符,我们就是从这个文件描述符的套接字文件中读取数据的,所以它也需要加入到select函数的文件描述符集合中被等待,同时我们还要继续获取新的连接,所以一开始的listensock套接字也需要继续在select函数的文件描述符集合中被等待,这样就会导致等待成功的文件描述符我们无法确定哪个是用来获取连接的,哪个是用来读取数据的。所以需要维护一张文件描述符表,我们规定好表的第一个元素就是用来获取连接的文件描述符,其它的元素都是用来读取数据的文件描述符。

SelectServer.cc:

#include <iostream>
#include <sys/select.h>
#include "Sock.hpp"

int fdsArray[sizeof(fd_set) * 8] = {0}; // 保存历史上所有的合法fd
int gnum = sizeof(fdsArray) / sizeof(fdsArray[0]);

#define DFL -1

using namespace std;

static void showArray(int arr[], int num)
{
    cout << "当前合法sock list# ";
    for (int i = 0; i < num; i++)
    {
        if (arr[i] == DFL)
            continue;
        else
            cout << arr[i] << " ";
    }
    cout << endl;
}

static void usage(std::string process)
{
    cerr << "\nUsage: " << process << " port\n"
         << endl;
}
// readfds: 现在包含就是已经就绪的sock
static void HandlerEvent(int listensock, fd_set &readfds)
{
    for (int i = 0; i < gnum; i++)
    {
        if (fdsArray[i] == DFL)
            continue;
        if (i == 0 && fdsArray[i] == listensock)
        {
            // 我们是如何得知哪些fd,上面的事件就绪呢?
            if (FD_ISSET(listensock, &readfds))
            {
                // 具有了一个新链接
                cout << "已经有一个新链接到来了,需要进行获取(读取/拷贝)了" << endl;
                string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞
                if (sock < 0)
                    return;
                cout << "获取新连接成功: " << clientip << ":" << clientport << " | sock: " << sock << endl;

                // read/write -- 不能,因为你read不知道底层数据是否就绪!!select知道!
                // 想办法把新的fd托管给select?如何托管??
                int i = 0;
                for (; i < gnum; i++)
                {
                    if (fdsArray[i] == DFL)
                        break;
                }
                if (i == gnum)
                {
                    cerr << "我的服务器已经到了最大的上限了,无法在承载更多同时保持的连接了" << endl;
                    close(sock);
                }
                else
                {
                    fdsArray[i] = sock; // 将sock添加到select中,进行进一步的监听就绪事件了!
                    // showArray(fdsArray, gnum);
                }
            }
        } // end if (i == 0 && fdsArray[i] == listensock)
        else
        {
            // 处理普通sock的IO事件!
            if(FD_ISSET(fdsArray[i], &readfds))
            {
                // 一定是一个合法的普通的IO类sock就绪了
                // read/recv读取即可
                // TODO bug
                char buffer[1024];
                ssize_t s = recv(fdsArray[i], buffer, sizeof(buffer), 0); // 不会阻塞
                if(s > 0)
                {
                    buffer[s] = 0;
                    cout << "client[" << fdsArray[i] << "]# " << buffer << endl; 
                }
                else if(s == 0)
                {
                    cout << "client[" << fdsArray[i] << "] quit, server close " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL; // 去除对该文件描述符的select事件监听
                    // showArray(fdsArray, gnum);
                }
                else
                {
                    cout << "client[" << fdsArray[i] << "] error, server close " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL; // 去除对该文件描述符的select事件监听
                    // showArray(fdsArray, gnum);
                }
            }
        }
    }
}

// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    // 是一种类型,位图类型,能定义变量,那么就一定有大小,就一定有上限
    // fd_set fds; // fd_set是用位图表示多个fd的
    // cout << sizeof(fds) * 8 << endl;
    int listensock = Sock::Socket();
    Sock::Bind(listensock, atoi(argv[1]));
    Sock::Listen(listensock);

    for (int i = 0; i < gnum; i++)
        fdsArray[i] = DFL;
    fdsArray[0] = listensock;
    while (true)
    {
        // 在每次进行select的时候进行我们的参数重新设定
        int maxFd = DFL;
        fd_set readfds;
        FD_ZERO(&readfds);
        for (int i = 0; i < gnum; i++)
        {
            if (fdsArray[i] == DFL)
                continue;                  // 1. 过滤不合法的fd
            FD_SET(fdsArray[i], &readfds); // 2. 添加所有的合法的fd到readfds中,方便select统一进行就绪监听
            cout << "---------------------" << endl;
            showArray(fdsArray, gnum);
            if (maxFd < fdsArray[i])
                maxFd = fdsArray[i]; // 3. 更新出最大值
        }

        struct timeval timeout = {100, 0};
        // 如何看待监听socket,获取新连接的,本质需要先三次握手,前提给我发送syn -> 建立连接的本质,其实也是IO,一个建立好的
        // 连接我们称之为:读事件就绪!listensocket 只(也)需要关心读事件就绪!
        // accept: 等 + "数据拷贝"
        // int sock = Sock::Accept(listensock, );
        // 编写多路转接代码的时候,必须先保证条件就绪了,才能调用IO类函数!
        int n = select(maxFd + 1, &readfds, nullptr, nullptr, &timeout);
        switch (n)
        {
        case 0:
            cout << "time out ... : " << (unsigned long)time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << " : " << strerror(errno) << endl;
            break;
        default:
            HandlerEvent(listensock, readfds);
            // 等待成功
            // 1. 刚启动的时候,只有一个fd,listensock
            // 2. server 运行的时候,sock才会慢慢变多
            // 3. select 使用位图,采用输出输出型参数的方式,来进行 内核<->用户 信息的传递, 每一次调用select,都需要对历史数据和sock进行重新设置!!!
            // 4. listensock,永远都要被设置进readfds中!
            // 5. select 就绪的时候,可能是listen 就绪,也可能是普通的IO sock就绪啦!!
            break;
        }
    }

    return 0;
}

六、select多路转接的总结

1.select的编写代码规律

  1. select之前要进行所有参数的重置,然后将所有需要等待的文件描述符添加到select的参数结构中。
  2. select需要用户自己维护第三方数组,来保存所有的合法文件描述符,方便select进行批量化处理。
  3. 一旦特点的文件描述符事件就绪,本次的读取或者写入就不会被阻塞。

2.select的优点和缺点

select函数的优点是:使用select之后不需要像以前一样使用多进程或者多线程,所以与多进程或者多线程相比,select占用资源少,并且高效。

select函数的缺点是:

  1. 每一次调用select函数之前都要进行大量的重置工作,效率比较低;
  2. select函数每一次能够检测的文件描述符数量是有上限的,因为fd_set是位图结构,所以它有1024个比特位,select能处理的文件描述符数量上限就是1024,虽然可以采用多进程或者多线程来解决这个问题,但是效率相对就比较低了;
  3. 每一次调用select函数都需要在用户和内核中互相传递位图参数,当传递较为频繁时,这就是大量的数据拷贝工作,效率也会比较低;
  4. select的多路转接方案编写代码比较复杂;select底层需要通过遍历的方式,检测所有需要等待的文件描述符,当连接越来越多时,select的效率就会因此下降。

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

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

相关文章

商家订单之Java版SpringCloud+SpringBoot+Mybatis+Vue+Uniapp 分布式、微服务、多商家入驻b2b2c电子商务云平台

一个好的SpringCloudSpringBoot b2b2c 电子商务平台涉及哪些技术、运营方案&#xff1f;以下是我结合公司的产品做的总结&#xff0c;希望可以帮助到大家&#xff01; 搜索体验小程序&#xff1a;海哇 1. 涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买…

linu0.12-9-blk_drv

[394页] 第9章 块设备驱动程序 395–9-1-总体功能 395–9-1-1-块设备请求项和请求队列 397–9-1-2-块设备访问调度处理 397–9-1-3-块设备操作方式 398–9-2-blk.h文件 398–9-2-1-功能描述 399–9-2-2-代码注释 403–9-3-hd.c程序 403–9-3-1-功能描述 405–9-3-2-代码注释…

离散数学_九章:关系(6)

&#x1fa90;9.6 偏序 1、⛺偏序关系和偏序集⛲偏序关系⛲偏序&#xff08;关系&#xff09;的例子 a. “大于或等于” 关系b. “整除” 关系c. “包含” 关系 &#x1f3ac;偏序集&#x1f3ac;可比性&#xff08;comparability&#xff09; " ≼ " 符号a. 可比 &a…

【工具】如何判断两个二进制文件是否相同

&#x1f41a;作者简介&#xff1a;花神庙码农&#xff08;专注于Linux、WLAN、TCP/IP、Python等技术方向&#xff09;&#x1f433;博客主页&#xff1a;花神庙码农 &#xff0c;地址&#xff1a;https://blog.csdn.net/qxhgd&#x1f310;系列专栏&#xff1a;善假于物&#…

wx自定义组件

自定义组件的意义&#xff1a; 提供一系列的样式&#xff0c;&#xff0c;通用的样式提供一系列的骨架&#xff0c;&#xff0c;通用标签避免重复写一些业务逻辑 小程序自定义组件中 &#xff1a; slot &#xff1a; 修改组件内容 外部样式类&#xff1a; 修改组件样式 slot…

案例7:Java茶叶销售网站设计与实现开题报告

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

手术麻醉信息管理系统源码(简称手麻系统源码)php + mysql + vue2 B/S网页版

手术麻醉信息管理系统源码&#xff08;简称手麻系统源码&#xff09; 手术麻醉信息管理系统&#xff08;简称手麻系统&#xff09;是指专用于住院患者手术与麻醉的申请、审批、安排&#xff0c;术前、术中和术后有关信息的记录和跟踪以及手术麻醉室内部管理等功能的计算机应用…

Lucene中的Field域、索引维护、搜索、相关度排序和中文分词器讲解

Field域 Field属性 Field是文档中的域&#xff0c;包括Field名和Field值两部分&#xff0c;一个文档可以包括多个Field&#xff0c;Document只是Field的一个承载体&#xff0c;Field值即为要索引的内容&#xff0c;也是要搜索的内容。 是否分词(tokenized) 是&#xff1a;作…

事实证明,国产BI软件的财务数据分析性价比极高!

国产BI软件做财务数据分析的性价比极高&#xff0c;主要得益于两个因素&#xff0c;一个是国产BI软件按功能模块购买&#xff0c;大幅度降低BI大数据分析平台的使用成本&#xff1b;另一个则是国产BI软件已打磨出标准化、系统化的财务数据分析方案&#xff0c;低成本、低风险、…

Blender基础技巧小结

官网下载 https://www.blender.org/download/lts/2-83/ 我下载的版本&#xff1a;LTS Release 2.83.20 Windows – Portable Ogre导出插件 https://github.com/OGRECave/blender2ogre 安装插件 将blender2ogre\io_ogre复制到&#xff1a;blender-2.83.20-windows-x64\2.8…

论文阅读-17-Deep Long-Tailed Learning: A Survey---3.1Class Re-balancing

文章目录 1. Re-sampling1.1 Class-balanced re-sampling(1) Decoupling① 网络架构② Sampling策略③ Classifier的学习策略 (2) SimCal① 比较 (3) DCL(4) Balanced meta-softmax(5) FASA(6) LOCE(7) VideoLT 1.2 Scheme-oriented sampling(1) LMLE(2) PRS(3) BBN(4) LTML(5)…

技术赋能光伏组件检测“大尺寸” “高精度”,维视智造SNEC亮点抢先看!

2023.5.24-5.26 全球最具影响力的 国际化、专业化、规模化光伏盛会 第十六届&#xff08;2023&#xff09; SNEC光伏大会暨(上海)展览会 即将开展 维视智造深耕机器视觉行业20年 解决方案落地众多光伏头部企业 如今作为光伏组件视觉检测系统行业领先者 此次展会维视将…

(文章复现)《高比例清洁能源接入下计及需求响应的配电网重构》(含matlab代码)

1.引言 配电网重构作为配电网优化运行的手段之一&#xff0c;通过改变配电网的拓扑结构&#xff0c;以达到降低网损、改善电压分布、提升系统的可靠性与经济性等目的。近年来&#xff0c;随着全球能源消耗快速增长以及环境的日趋恶化&#xff0c;清洁能源飞速发展&#xff0c;分…

从【创作者】转变为【博客专家】-- 内含详细申请过程

从【创作者】转变为【博客专家】 0、引言1、创作身份认证1.1 起因1.2 违背祖宗的决定1.3 认证创作身份1.3.0 好处1.3.1 条件1.3.2 认证信息1.3.3 后台审核 2、博客专家认证2.1 好处2.2 条件2.3 认证信息2.4 后台审核2.5 实体证书 3、 反思与总结 ⚠申明&#xff1a; 未经许可&a…

哪个年龄段人群喜欢养宠物?18-25岁占比最高,达31%

上一期&#xff0c;我们通过可视化互动平台分析了萌宠经济下宠物食品的发展现状&#xff0c;这一期我们接着来分析一下&#xff0c;在萌宠经济下&#xff0c;我国宠物医疗产业的市场情况。 由于现在很多家庭都喜欢饲养宠物&#xff0c;宠物数量的快速增长从而拉动了宠物经济的…

晶飞FLA5000光谱仪.FlaSpec文件数据解析

引言 首先说明下晶飞上位机软件存在的问题&#xff0c;实验所采用的FLA5000型号光谱仪&#xff0c;光谱波段从280-970nm&#xff0c;FWHM值为2.4nm。 1、上位机软件中的光谱数据复制功能基本是废的&#xff0c;最多只能到599.9nm&#xff0c;后面的数据全部消失。 2、上位机软…

NOSQL和REDIS配置与优化

关系数据库与非关系型数据库 ●关系型数据库&#xff1a; 关系型数据库是一个结构化的数据库&#xff0c;创建在关系模型&#xff08;二维表格模型&#xff09;基础上&#xff0c;一般面向于记录。 SQL 语句&#xff08;标准数据查询语言&#xff09;就是一种基于关系型数据库…

在外远程访问公司局域网用友畅捷通T财务软件 - 远程办公

文章目录 前言1.本地访问简介2. cpolar内网穿透3. 公网远程访问4. 固定公网地址 前言 用友畅捷通T适用于异地多组织、多机构对企业财务汇总的管理需求&#xff1b;全面支持企业对远程仓库、异地办事处的管理需求&#xff1b;全面满足企业财务业务一体化管理需求。企业一般将其…

ML@基础概念@模型评估和选择理论基础

refs 参考经典机器学习资料西瓜书 主要符号 x x x:标量 x \boldsymbol{x} x:向量(注意是 x x x的粗体形式) x \mathbf{x} x:变量集(正粗体) A \mathbf{A} A:矩阵(正粗体) I \mathbf{I} I:单位阵(正粗体) χ \chi χ:样本空间或状态空间 D \mathcal{D} D:概率分布…

【fly-iot飞凡物联】(5):开源项目Apache IoTDB,开源项目学习,原来还有这样的项目,关于IOT的几个开源项目汇总下

目录 前言1&#xff0c;关于&#xff1a;开源项目Apache IoTDB2&#xff0c;还有个admin后台3&#xff0c;thinglinks项目4&#xff0c;thingsboard-ui-vue项目5&#xff0c;apache pulsar项目6&#xff0c;ActorCloud项目 前言 本文的原文连接是: https://blog.csdn.net/freew…