IO多路复用(select、poll、epoll网络编程)

news2024/11/14 22:05:15

目录

  • 一、高级IO相关
    • 1.1 同步通信和异步通信
    • 1.2 阻塞与非阻塞
    • 1.3 fcntl 函数
  • 二、五种IO模型
    • 2.1 阻塞式IO模型
    • 2.2 非阻塞式IO模型
    • 2.3 多路复用IO模型
    • 2.4 信号驱动式IO模型
    • 2.5 异步IO模型
  • 三、认识IO多路复用
  • 四、select
    • 4.1 认识select函数
    • 4.2 select函数原型
    • 4.3 select网络编程
    • 4.4 setsockopt函数(补充)
    • 4.5 select的特点
    • 4.6 select的缺点
  • 五、poll
    • 5.1 认识poll函数
    • 5.2 poll网络编程
    • 5.3 poll函数的优点
    • 5.4 poll函数的缺点
  • 六、epoll
    • 6.1 认识epoll函数
    • 6.2 epoll工作原理
    • 6.3 epoll工作模式
      • LT模式
      • ET模式
    • 6.4 epoll网络编程
    • 6.5 epoll的优点


一、高级IO相关

1.1 同步通信和异步通信

同步通信和异步通信是两种不同的通信方式,二者的概念如下:

  • 同步通信是指通信双方需要在某时刻达成一致,才进行数据交换。在同步通信中,发送方会在发送数据时等待接收方的响应,直到接收到响应后才会继续执行后续任务。同步通信可以保证数据传输的可靠性和一致性,但是可能造成系统的阻塞和资源浪费。

  • 异步通信是指通信双方可以独立的进行数据交换,不需要在某一时刻达成一致。在异步通信中,发送方会在发送数据后立即返回,而接收方会在接收到数据后立即进行处理。异步通信可以提高系统的并发性和效率,但是也可能会带来一些数据的不一致问题。

总的来说,同步通信适用于对数据传输的可靠性和一致性要求较高的场景,例如数据库的读写操作。而异步通信适用于对系统的并发性和效率要求较高的场景,如网络通信。具体选择哪种通信方式需要根据实际情况来考虑。

1.2 阻塞与非阻塞

阻塞和非阻塞是两种不同的操作方式,二者的概念如下:

  • 阻塞是指在进行某种操作时,如果当前操作无法完成,那么程序就会一直等待,直到操作完成或者出现错误时才返回结果。阻塞操作会阻塞当前线程或进程的执行,直到操作完成,因此会占用CPU资源,并且可能造成系统的阻塞。
  • 非阻塞是指在进行某种操作时,如果当前操作无法完成,则程序会立马返回,并且告诉调用者当前操作无法完成。非阻塞操作不会阻塞当前线程或进程的执行,因此不会占用CPU资源,并且可以让程序执行其他任务。

总的来说,阻塞和非阻塞是对于操作的执行方式的描述,阻塞操作会等待操作的完成,而非阻塞操作会立即返回。选择使用阻塞或非阻塞操作取决于应用程序的需求和实际情况。如果需要快速响应和处理多个并发请求,通常会使用非阻塞操作。而如果需要保证数据传输的可靠性和一致性,则可能需要使用阻塞操作。

1.3 fcntl 函数

fcntl是一个Unix/Linux系统编程中的函数,用于控制文件描述符的一些属性和操作。其定义如下:

#include <unistd.h>
#include <fcntl.h>

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

参数fd是被参数cmd操作(如下面的描述)的文件描述符,传入的cmd的值不同,fcntl后面追加的参数也不相同。

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)

此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞,如下面的例子:

基于fcntl,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞。

bool SetNoBlock(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;
}

先使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图),然后再使用F_SETFL将文件描述符设置回去,设置回去的同时,加上一个O_NONBLOCK参数,将这个文件描述符设置为非阻塞状态。

下面以轮询的方式读取标准输入:

#include <iostream>
#include <vector>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <fcntl.h>

using namespace std;

using func_t = std::function<void()>;

void func1()
{
    cout << "func1 " << endl;   
}

void func2()
{
    cout << "func2 " << endl;   
}

void func3()
{
    cout << "func3 " << endl;   
}

bool SetNoBlock(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;
}

int main()
{
    std::vector<func_t> funcs;
    funcs.push_back(func1);
    funcs.push_back(func2);
    funcs.push_back(func3);
    
    SetNoBlock(0);
    
    char buff[1024];
    while(true)
    {
        memset(buff, 0, sizeof buff);
        ssize_t read_size = read(0, buff, sizeof(buff) - 1);
        if(read_size < 0)
        {
            cerr << "errno:" << errno << "desc: " << strerror(errno) << endl;
            for(const auto& f : funcs)
            {
                f();
            }
        }
        else 
        {
            cout << "buff: " << buff << "read_size: " << read_size << endl;
        }
        sleep(1);
    }

    return 0;

}


可以发现此时不输入时就不会阻塞而直接处理另外一个逻辑。

如果此时取消调用SetNoBlock函数:

就按照阻塞的方式进行读取了。

二、五种IO模型

2.1 阻塞式IO模型

在阻塞式IO模型中,当应用程序发起一个IO请求时。程序会一直阻塞等待,直到数据传输完成才继续执行其他任务。这种模型的缺点是会造成CPU资源的浪费,降低系统的响应速度。但是阻塞式IO是最常见的IO模型,所有的套接字默认都是阻塞方式。

2.2 非阻塞式IO模型

在非阻塞式IO模型中,当应用程序发起一个IO请求时,即使数据没有准备好也会立即返回,并且通过轮询的方式不断地查询IO操作的状态,直到数据准备完成。这种模型可以减少CPU资源的浪费,但也会增加系统的负担,降低IO操作的效率。

2.3 多路复用IO模型

在多路复用IO模型中,应用程序可以将多个IO操作绑定到同一个事件轮询器中,然后等待IO操作完成的通知。这种模型可以有效地提高系统的并发性能,但是实现较为复杂。

2.4 信号驱动式IO模型

在信号驱动IO模型中,应用程序向操作系统注册一个信号,当IO操作完成时,操作系统会向应用程序发送信号通知。这种模型可以减少轮询带来的系统负担,提高系统的效率。

2.5 异步IO模型

在异步IO模型中,应用程序可以在发起IO请求后立即返回,并在IO操作完成后由操作系统通知应用程序。这种模型可以提高系统的并发性能和效率,但是实现较为复杂。

总之,不论是何种IO模型,都包含了两个步骤,第一是等待数据,第二是拷贝数据。而且在实际应用场景中,等待消耗的时间往往都远高于拷贝数据的时间。让IO变得更高效,最核心的办法就是尽量减少等待的时间。

三、认识IO多路复用

IO多路复用是一种高效的I/O处理机制,它允许在单个线程中同时监视和处理多个I/O操作,以提高程序的性能和可扩展性。

在传统的阻塞I/O模型中,每个I/O操作都会阻塞整个进程,直到该操作完成。这就意味着如果应用程序需要处理多个并发I/O操作,就需要建立多个线程或者进程来处理它们,这样就会导致系统的开销过高,并且导致可扩展性下降。

使用IO多路复用,应用程序可以将多个I/O操作注册到一个事件的循环中,然后使用一个线程来监视这些操作的状态。当其中任何一个操作就绪时,事件循环就会通知相应的应用程序,来执行相应的操作。这种方式允许应用程序同时处理多个I/O操作,并且无需创建多个线程或者进程,因此提高了系统的性能和可扩展性。

常见的IO多路复用技术包括selectpollepoll等,其中epoll是最常用的技术之一,因为它可以更好地处理大量的并发连接。

四、select

4.1 认识select函数

select()函数是一种在 Unix/Linux 系统中实现多路复用 IO 的一种机制,它可以等待多个文件描述符(socket、文件等)中任何一个变为”就绪“状态(可读、可写、异常),然后立即进行处理,而不是阻塞在一个文件描述符上等待数据到来。

4.2 select函数原型

select函数原型:

#include <sys/select.h>

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

参数说明:

  • nfds:需要监听的最大文件描述符的值加 1,即文件描述符集合中最大的文件描述符值加 1;
  • readfds:指向可读文件描述符集合的指针;
  • writefds:指向可写文件描述符集合的指针;
  • exceptfds:指向异常文件描述符集合的指针;
  • timeout:超时时间,若设置为NULL则表示永远等待,直到有文件描述符就绪。

函数返回值:

  • 若有文件描述符就绪,则返回就绪文件描述符的个数;
  • 若超时或被信号中断,则返回 0;
  • 若出现错误,则返回 -1,并且设置errno变量。

fd_set 结构体:
fd_set是一个用于表示文件描述符的结构体,它包含了一组标志位,每个标志位的值表示了一个文件描述符是否在集合中。fd_set结构体的定义如下:

/* The fd_set member is required to be an array of longs.  */
typedef long int __fd_mask;

/* fd_set for select and pselect.  */
typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

其实这个结构就是一个整数数组,更严格的说是一个"位图",使用位图中对应的位来表示要监视的文件描述符。

为了使得操作fd_set变得更加方便,操作系统还专门提供了一组接口:

  • FD_ZERO(fd_set *set):将集合中的所有位清零;
  • FD_SET(int fd, fd_set *set):将指定文件描述符加入集合中;
  • FD_CLR(int fd, fd_set *set):将指定文件描述符从集合中删除;
  • FD_ISSET(int fd, fd_set *set):判断指定的文件描述符是否在集合中。

timeval 结构体:

timeval结构体用于描述一段时间长度,如果在这段时间内,需要监听的文件描述符没有就绪则函数返回,返回值为 0。timeval结构体的定义如下:

/* A time value that is accurate to the nearest
   microsecond but also has a range of years.  */
struct timeval
  {
    __time_t tv_sec;		/* Seconds.  */
    __suseconds_t tv_usec;	/* Microseconds.  */
  };

理解socket就绪状态:

读就绪:

  • socket内核中,接收缓冲区中的字节数大于等于低水位标记SO_RCVLOWAT,此时就可以无阻塞的读取该缓冲区,并且读取的返回值大于 0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;
  • socket TCP通信中,对端断开连接,此时对该socket读,则返回0。

写就绪:

  • socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
  • socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未处理的错误。

异常就绪:

  • socket上收到带外数据 关于带外数据,和TCP紧急模式相关。

4.3 select网络编程

在TCP服务器中,监听socket,获取新连接的,本质需要先三次握手,即客户端向服务端发送SYN连接请求。建立连接的本质,其实也是IO操作。

一个建立好的连接我们称之为读事件就绪,而listensocket 也只需要关心读事件就绪!如果TCP服务器自己直接调用accept函数,如果此时客户端发送连接的请求还没有就绪,那么该进程就会阻塞式等待连接请求的数据就绪。并且建立连接后,每次读写数据还需要等待数据达到缓冲区的最低水位线才进行数据拷贝,这样势必也会导致服务器的性能低下。

因此,我们可以把listenSock,以及读写相关的sock交付给select()函数进行监管,以下是一个利用select()函数实现的多路复用TCP服务器:

简单封装Sock:

#pragma once

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

class Sock
{
public:

    static const int gbacklog = 3;

    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 sock, uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        local.sin_port = htons(port);
        
        if(bind(sock, (const sockaddr*)&local, sizeof local) < 0)
        {
            exit(2);
        }
    }

    static void Listen(int sock)
    {
        if(listen(sock, gbacklog) < 0)
        {
            exit(3);
        }
    }

    static int Accept(int sock, std::string* clientIp, uint16_t* clientPort)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sock_fd = accept(sock, (sockaddr*)&peer, &len);
        if(sock_fd < 0)
        {
            exit(4);
        }

        if(clientIp)
            *clientIp = inet_ntoa(peer.sin_addr);
        if(clientPort)
            *clientPort = ntohs(peer.sin_port);

        return sock_fd;
    }
};

服务端代码:

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

using namespace std;

#define DEL -1 // 设置默认的文件描述符

int fdsArray[sizeof(fd_set) * 8] = {0};            // 保存所有合法的fd
int gnum = sizeof(fdsArray) / sizeof(fdsArray[0]); // fdsArray中保存最多的fd个数

// 打印fdsArray中的文件描述符
static void showArray(int arr[], int n)
{
    cout << "当前合法的 sock list# ";
    for (int i = 0; i < n; ++i)
    {
        if (arr[i] == DEL)
            continue;
        else
            cout << arr[i] << " ";
    }
    cout << endl;
}

static void HandlerEvent(int listenSock, fd_set &readfds)
{
    // 首先判断fdsArray中的文件描述符是listenSock还是读文件描述符,并且过滤没有设置的文件描述符
    for (int i = 0; i < gnum; ++i)
    {
        if (fdsArray[i] == DEL)
            continue;

        if (i == 0 && fdsArray[i] == listenSock)
        {
            // 判断listenSock是否就绪
            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;

                // 把新的sock托管给select,设置进fdsArray数组
                int i = 0;
                for (; i < gnum; ++i)
                {
                    if (fdsArray[i] == DEL)
                        break;
                }
                if (i == gnum)
                {
                    cerr << "服务器已经达到上限,无法同时保持更多的连接!" << endl;
                    close(sock);
                }
                else
                {
                    fdsArray[i] = sock;
                    showArray(fdsArray, gnum);
                }
            }
        }
        else // 处理普通的IO事件
        {
            if (FD_ISSET(fdsArray[i], &readfds))
            {
                // 此时一个是一个普通合法的IO请求就绪了
                char buff[1024];

                // 存在bug,因为此时不会阻塞,如果数据量过大会导致读取数据不完整

                ssize_t s = recv(fdsArray[i], buff, sizeof(buff), 0);
                if (s > 0)
                {
                    buff[s] = 0;
                    cout << "clent[" << fdsArray[i] << "]# " << buff << endl;
                }
                else if (s == 0)
                {
                    cout << "client[" << fdsArray[i] << "] quit, server close!" << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DEL;
                    showArray(fdsArray, gnum);
                }
                else
                {
                    // 该文件描述符异常
                    cerr << "client[" << fdsArray[i] << "] error, server close! " << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DEL;
                    showArray(fdsArray, gnum);
                }
            }
        }
    }
}

void usage(std::string process)
{
    cerr << "\nUsage: " << process << " [port]\n"
         << endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(-1);
    }

    int listenSock = Sock::Socket();
    Sock::Bind(listenSock, atoi(argv[1]));
    Sock::Listen(listenSock);

    // 初始化fdsArray
    for (int i = 0; i < gnum; ++i)
        fdsArray[i] = DEL;
    fdsArray[0] = listenSock; // 默认第一个是listenSock

    while (true)
    {
        // 每次重新调用select的时候都要重新设定参数
        int maxFd = DEL;
        fd_set readfds;
        FD_ZERO(&readfds);

        for (int i = 0; i < gnum; ++i)
        {
            if (fdsArray[i] == DEL)
                continue;
            FD_SET(fdsArray[i], &readfds); // 将合法的fd设置进readfds

            // 更新最大fd
            if (fdsArray[i] > maxFd)
                maxFd = fdsArray[i];
        }

        timeval timeout = {3, 0};

        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);
            break;
        }
    }

    return 0;
}

运行结果:

4.4 setsockopt函数(补充)

在上面封装Sock的代码中使用到了setsockopt函数,以下是对其的补充介绍:

setsocketopt函数是一个用于设置套接字选项的系统调用函数。它可以用来设置套接字的各种选项,例如超时,缓冲区大小等。函数原型如下:

#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

其中,参数的含义如下:

  • sockfd:表示要设置的套接字描述符;
  • level:表示要设置的选项所在的协议层。通常为SOL_SOCKET表示设置的是套接字级别的选项,或者是某个具体协议的协议和;
  • optname:表示要设置的选项名1,具体的选项名取决于所设置的协议层;
  • optval:表示要设置的选项的值,是一个指向选项值的指针;
  • optlen:表示要设置的选项的值的长度。

返回值:

调用成功返回 0,失败则返回 -1,并且将错误信息设置进errno变量。

以下是一些常用的选项名和说明:

  • SO_REUSEADDR允许在同一个端口上启动同一服务器的多个实例,用于服务器程序重启后快速恢复到正常服务状态
  • SO_REUSEPORT:允许多个进程或线程在同一端口上绑定,实现端口共享;
  • SO_KEEPALIVE:开启TCP的keepalive机制,检测连接是否仍然存活;
  • SO_RCVBUFSO_SNDBUF:设置套接字接收和发送缓冲区的大小;
  • TCP_NODELAY:禁止Nagle算法,即数据发送时不缓存等待其他数据,直接发送。

例如,下面的代码设置了套接字的超时时间为10秒:

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct timeval timeout = {10, 0};
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

4.5 select的特点

select()函数具有以下特点:

  1. 可以同时监视多个文件描述符的状态变化,实现了一次等待多个IO事件的机制,避免了使用多个线程或进程进行IO操作时的复杂性和开销;
  2. 支持设置超时时间,可以等待一段时间后返回,避免了长时间等待IO事件而导致的程序阻塞;
  3. 可以同时等待多种IO事件的发生,包括可读、可写和异常事件,适用于各种类型的IO操作;
  4. 在等待IO事件的过程中,进程会被阻塞,直到有任意一个文件描述符就绪,但是在等待的过程中,其他进程可以继续执行,因此可以提高程序的效率;
  5. select()函数会修改传入的文件描述符集合,所以需要每次调用select()后重新设置集合。

4.6 select的缺点

虽然select()函数是一种多路复用IO机制,但是它也存在以下缺点:

  1. 受限于文件描述符数量:select()函数需要将所有需要监听的文件描述符都加入到一个文件描述符集合中,并将这个集合传递给函数,但在某些系统中,文件描述符集合的大小是由限制的,比如POSIX标准规定,文件描述符集合的大小默认不能超过FD_SETSIZE(一般是1024)。
  2. 需要不断轮询:在等待IO事件的过程中,select()函数需要轮询所有需要监听的文件描述符,这样会浪费CPU资源,并且导致调用方的延迟。当需要监听的文件描述符量很大时,这个缺点尤为明显;
  3. 无法处理大量的连接:当需要处理大量的连接时,使用select()函数可能会遇到性能问题,因为它需要在所有的连接直接进行切换,当连接数量非常大时,这个缺点尤为明显;
  4. 不方便扩展:当需要添加或者删除监听的文件描述符时,需要重新设置文件描述符集合,并重新调用select()函数,这样既会导致调用方的延迟还会浪费CPU资源。同时,在某些系统中,每次调用select()函数时,都需要将文件描述符集合从用户空间复制到内核空间,这也会导致一定的性能损失。

综上所述,select() 函数虽然是一种多路复用IO机制,但是在一些特定的场景下,它可能会存在一些性能问题和限制,因此需要根据具体的应用场景选择合适的多路复用机制。

五、poll

5.1 认识poll函数

poll函数和select函数一样,也是用于实现IO多路复用的系统调用函数,可以用于监视一组文件描述符中的任意一个变为可读、可写或者异常状态,并返回就绪的文件描述符个数。在Linux系统中,它可以替代阻塞式IO或者select函数,提高程序的效率和性能。

poll函数原型如下:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明:

  • fds:指向pollfd结构体数组的指针,每个pollfd结构体描述了一个待监视的文件描述符及其关注的事件;
  • nfdsfds数组中元素的数量;
  • timeout:超时时间,以毫秒为单位。如果设置为 “-1”,poll函数将一直阻塞直到至少有一个文件描述符就绪。

函数返回值:

  • 返回值大于0:就绪文件描述符的数量;
  • 返回值等于0:发生超时;
  • 返回值小于0:发生错误,并且将错误信息存储到errno变量中。

pollfd结构体:
pollfd结构体定义如下:

struct pollfd {
    int fd;         // 文件描述符
    short events;   // 关注的事件
    short revents;  // 实际发生的事件,与 events 的取值相同或为其子集。
};

eventsrevents的取值:

事件描述是否可作为输入是否可作为输出
POLLIN数据可读(包括普通和优先数据)
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux不支持)
POLLOUT数据可写(包括普通和优先数据)
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作。由GUN引入
POLLERR错误
POLLHUP挂起,比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件
POLLNAVL文件描述符没有打开

如果想要同时设置多个事件,可以用按位或操作进行合并。

5.2 poll网络编程

和上文使用select函数编写的服务器代码一样,只是将select函数替换成了poll函数。与select函数相比,使用poll实现服务器的时候不需要再每次调用poll的时候又重新设置参数,并且监视的文件描述符数量没有上限,由我们自己决定。

#include <iostream>
#include "sock.hpp"
#include <unistd.h>
#include <poll.h>

using namespace std;

#define DEL -1 // 设置默认的文件描述符

#define NUM 1024

pollfd fdsArray[NUM]; // 保存所有合法的fd

// 打印fdsArray中的文件描述符
static void showArray(pollfd arr[], int n)
{
    cout << "当前合法的 sock list# ";
    for (int i = 0; i < n; ++i)
    {
        if (arr[i].fd == DEL)
            continue;
        else
            cout << arr[i].fd << " ";
    }
    cout << endl;
}

static void HandlerEvent(int listenSock)
{
    // 首先判断fdsArray中的文件描述符是listenSock还是读文件描述符,并且过滤没有设置的文件描述符
    for (int i = 0; i < NUM; ++i)
    {
        if (fdsArray[i].fd == DEL)
            continue;

        if (i == 0 && fdsArray[i].fd == listenSock)
        {
            // 判断listenSock是否读就绪,与POLLIN进行按位与运算
            if (fdsArray[i].revents & POLLIN)
            {
                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;

                // 把新的sock托管给select,设置进fdsArray数组
                int i = 0;
                for (; i < NUM; ++i)
                {
                    if (fdsArray[i].fd == DEL)
                        break;
                }
                if (i == NUM)
                {
                    cerr << "服务器已经达到上限,无法同时保持更多的连接!" << endl;
                    close(sock);
                }
                else
                {
                    fdsArray[i].fd = sock;
                    fdsArray[i].events = POLLIN;
                    fdsArray[i].revents = 0;
                    showArray(fdsArray, NUM);
                }
            }
        }
        else // 处理普通的IO事件
        {
            if (fdsArray[i].revents & POLLIN)
            {
                // 此时一个是一个普通合法的IO请求就绪了
                char buff[1024];

                // 存在bug,因为此时不会阻塞,如果数据量过大会导致读取数据不完整

                ssize_t s = recv(fdsArray[i].fd, buff, sizeof(buff), 0);
                if (s > 0)
                {
                    buff[s] = 0;
                    cout << "clent[" << fdsArray[i].fd << "]# " << buff << endl;
                }
                else if (s == 0)
                {
                    cout << "client[" << fdsArray[i].fd << "] quit, server close!" << endl;
                    close(fdsArray[i].fd);
                    fdsArray[i].fd = DEL;
                    fdsArray[i].events = 0;
                    fdsArray[i].revents = 0;
                    showArray(fdsArray, NUM);
                }
                else
                {
                    // 该文件描述符异常
                    cerr << "client[" << fdsArray[i].fd << "] error, server close! " << endl;
                    close(fdsArray[i].fd);
                    fdsArray[i].fd = DEL;
                    fdsArray[i].events = 0;
                    fdsArray[i].revents = 0;
                    showArray(fdsArray, NUM);
                }
            }
        }
    }
}

void usage(std::string process)
{
    cerr << "\nUsage: " << process << " [port]\n"
         << endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(-1);
    }

    int listenSock = Sock::Socket();
    Sock::Bind(listenSock, atoi(argv[1]));
    Sock::Listen(listenSock);

    // 初始化fdsArray
    for (int i = 0; i < NUM; ++i)
    {
        fdsArray[i].fd = DEL;
        fdsArray[i].events = 0;
        fdsArray[i].revents = 0;
    }

    fdsArray[0].fd = listenSock;
    fdsArray[0].events = POLLIN; // listenSock只关心读操作
    // int timeout = -1;            // 设置为 "-1",`poll`函数将一直阻塞直到至少有一个文件描述符就绪

    int timeout = 1000;

    while (true)
    {
        // poll函数不需要每次重新设置参数
        int n = poll(fdsArray, NUM, 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);
            break;
        }
    }

    return 0;
}

运行结果:

5.3 poll函数的优点

  1. 支持同时监听多个文件描述符,可以在一个 poll() 调用中监听多个 I/O 事件,避免了多次系统调用。

  2. 能够监听复杂的 I/O 事件,如对于一个 TCP 连接,可以同时监听读和写事件。

  3. poll() 没有最大文件描述符数的限制,可以监听任意数量的文件描述符。

  4. 在处理大量文件描述符时,poll() 的效率比 select() 更高,并且代码实现起来更简单。

5.4 poll函数的缺点

  1. 调用时需要传入一个数组,数组长度取决于需要监听的文件描述符数,可能需要使用动态分配内存,导致一定的额外开销。

  2. poll() 不支持超时重连,即当一个 I/O 事件发生时,如果不立即处理,下一次 poll() 调用将不会通知你。

  3. poll() 函数是系统调用,与内核交互需要额外的开销。

  4. 不是所有的操作系统都支持 poll(),尤其是旧的操作系统。

六、epoll

6.1 认识epoll函数

epoll是Linux下一种高效的IO多路复用机制,可用于管理大量的文件描述符,能够处理大规模的并发连接,比传统的 selectpoll 函数更加高效。它几乎具备了前两者的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

epoll 可以分为以下三个函数:
1、epoll_create()
epoll_create()函数用于创建一个新的epoll实例,它的参数size是一个整数,表示需要监听的文件描述符数量。该函数返回一个整数类型的文件描述符,表示新创建的epoll实例。

int epoll_create(int size);

2、epoll_ctl()
epoll_ctl()函数用于向epoll实例中添加、修改或删除文件描述符。它的参数epollfdepoll实例的文件描述符,op是要执行的操作类型,fd是要添加、修改或删除的文件描述符,event是要监听的事件类型。

int epoll_ctl(int epollfd, int op, int fd, struct epoll_event *event);

其中,op的值可以是以下三种之一:

  • EPOLL_CTL_ADD:添加文件描述符到epoll实例中。
  • EPOLL_CTL_MOD:修改已经添加到 epoll 实例中的文件描述符的监听事件。
  • EPOLL_CTL_ADD:从epoll实例中删除文件描述符。

struct epoll_event 结构体类型用于存储需要监听的事件类型和文件描述符,它包含以下两个字段:

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;   /* 监听的事件类型 */
    epoll_data_t data;     /* 用户数据 */
};

其中,events的值可以是以下几种:

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

3、epoll_wait()
epoll_wait() 函数用于等待文件描述符上的事件发生。它的参数 epollfdepoll 实例的文件描述符,events 是一个数组,用于存储发生事件的文件描述符。

int epoll_wait(int epollfd, struct epoll_event *events, int maxevents, int timeout);

其中,maxevents表示events数组的长度,timeout表示等待事件的超时时间(以毫秒为单位)。如果timeout的值为 -1,则表示一直等待直到有文件描述符就绪。

epoll_wait() 函数返回一个整数类型的值,表示发生事件的文件描述符数量。

6.2 epoll工作原理

epoll之所以会比selectpoll有更高的效率和可扩展性,其原因在于它采用了以下三个重要的优化技术:

1、采用红黑树作为事件的存储的数据结构

当某一进程调用epoll_create时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

struct eventpoll{
	....
	/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
	struct rb_root rbr;
	/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
	struct list_head rdlist;
....
};

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

2、采用事件回调机制
避免了在内核态和用户态之间的频繁切换,当某个文件描述符上有事件发生时,内核直接回调用户注册的回调函数,通知应用程序。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。在epoll中,对于每一个事件,都会建立一个epitem结构体。

struct epitem
{
	struct rb_node rbn;//红黑树节点
	struct list_head rdllink;//双向链表节点
	struct epoll_filefd ffd; //事件句柄信息
	struct eventpoll *ep; //指向其所属的eventpoll对象
	struct epoll_event event; //期待发生的事件类型
}

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。这个操作的时间复杂度是O(1)。

3、采用边缘触发模式
只有在文件描述符状态发生变化时才通知应用程序,而不是像水平触发模式一样,只要文件描述符上有数据就通知应用程序。这样可以避免应用程序在处理事件时漏掉某些数据,减少CPU的无效操作。关于边缘触发模式和水平触发模式见下文。

epoll的工作流程可概括如下:

  1. 应用程序调用epoll_create创建一个epoll实例,获得一个文件描述符。

  2. 应用程序调用epoll_ctlepoll实例中添加、修改或删除的文件描述符及其对应的事件。

  3. 内核根据添加的文件描述符,建立一个红黑树,并把文件描述符和其对应的事件节点加入到红黑树中。

  4. 应用程序调用epoll_wait阻塞等待事件发生,当有文件描述符上的事件发生时,epoll_wait返回事件列表。

  5. 应用程序处理事件列表,处理完后回到第4步,继续等待事件的发生。

6.3 epoll工作模式

epoll的工作模式有的两种,LT(Level Triggered,水平触发模式)模式和 ET(Edge Triggered,边缘触发模式)模式,它们用于描述内核何时通知应用程序有关文件描述符的事件。

LT模式

在 LT 模式下,当文件描述符就绪时,epoll_wait 函数会返回,并将该文件描述符加入到就绪队列中,通知应用程序有数据可读或可写,应用程序需要不断读取或写入数据直到文件描述符中没有数据可读或可写。如果应用程序没有对就绪的文件描述符进行操作,则 epoll_wait 函数会一直阻塞等待。

ET模式

在 ET 模式下,epoll_wait 函数仅在文件描述符状态发生改变时才会返回,并将该文件描述符加入到就绪队列中,通知应用程序有新的数据可读或可写。应用程序需要立即对就绪的文件描述符进行操作,如果应用程序没有对就绪的文件描述符进行操作,则 epoll_wait 函数不会再次返回。在 ET 模式下,应用程序需要使用非阻塞 I/O 操作,以避免因为某个文件描述符的阻塞 I/O 操作而导致阻塞其他文件描述符

两种模式的对比:

  1. LT 模式更加简单,易于理解和实现,因为它类似于轮询。相比之下,ET 模式更为复杂,需要程序员具有更高的编程技能和经验。

  2. LT模式适用于需要长时间读取或写入的文件描述符,因为应用程序可以反复查询文件描述符是否已经准备好。相比之下,ET模式更适用于需要实时响应的场景,因为它只会在状态变化时通知应用程序。

  3. 在 LT 模式下,内核通知应用程序有关文件描述符的事件时,应用程序需要循环调用I/O函数,而在 ET 模式下,应用程序只需要在状态变化时调用一次I/O函数即可。

  4. 由于 ET 模式的事件处理方式更加实时,因此它在高并发、高性能的场景中表现更好。相比之下,LT 模式的轮询方式可能会导致效率降低。

ET模式为什么只支持非阻塞读写?

ET 模式只支持非阻塞 I/O 操作的原因是因为 ET 模式的工作方式是只在文件描述符上发生状态变化时通知应用程序。如果应用程序在 ET 模式下使用阻塞 I/O 操作,例如读取或写入数据时阻塞在系统调用中,那么即使文件描述符的状态已经发生了变化,应用程序也无法感知到这个变化,从而无法正确处理事件。这会导致应用程序的错误行为,甚至可能导致死锁等问题。

因此,在 ET 模式下,应用程序必须使用非阻塞 I/O 操作,以便在 epoll_wait 函数返回时,及时对就绪的文件描述符进行操作。在非阻塞 I/O 操作中,如果没有数据可读或可写,读取或写入函数会立即返回,并返回一个错误码(例如 EAGAINEWOULDBLOCK),应用程序需要根据错误码来确定是否继续等待数据可读或可写,或者是否进行其他操作。这种方式可以避免应用程序因为某个文件描述符的阻塞 I/O 操作而导致阻塞其他文件描述符,从而提高系统的并发处理能力。

需要注意的是,在使用 ET 模式时,应用程序需要处理 EAGAINEWOULDBLOCK 错误码,这种错误码是非阻塞 I/O 操作的正常情况。如果应用程序没有正确处理这些错误码,可能会导致应用程序的异常行为。

6.4 epoll网络编程

这里实现的服务器功能与前面使用select函数和poll实现的服务器功能一样,只是对服务器代码进行了简单的封装:

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstdlib>
#include "sock.hpp"
#include <unistd.h>
#include "Log.hpp"
#include <sys/epoll.h>

class EpollServer
{
    using func_t = std::function<int(int)>; // 回调函数
    static const int gsize = 128;           // 最大文件描述符数量
    static const int num = 256;             // event数组长度

public:
    EpollServer(uint16_t port, func_t func) : _port(port), _func(func), _listensock(-1), _epfd(-1)
    {
        Init();
    }
    ~EpollServer()
    {
        if (_listensock != -1)
            close(_listensock);
        if (_epfd != -1)
            close(_epfd);
    }

    void Init()
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);

        // 创建epoll实例
        _epfd = epoll_create(gsize);
        if (_epfd < 0)
        {
            logMsg(FATAL, "%d:%s", errno, strerror(errno));
            exit(3);
        }

        logMsg(DEBUG, "创建监听套接字成功: %d", _listensock);
        logMsg(DEBUG, "创建epoll实例成功: %d", _epfd);
    }

    void HandlerEvent(epoll_event revs[], int n)
    {
        for (int i = 0; i < n; ++i)
        {
            int sock = revs[i].data.fd;
            uint32_t revent = revs[i].events;
            if (revent & EPOLLIN) // 读事件就绪
            {
                if (sock == _listensock)
                {
                    // listensock
                    std::string clientip;
                    uint16_t clientport = 0;

                    // 监听socket就绪,获取新连接
                    int sockfd = Sock::Accept(_listensock, &clientip, &clientport);
                    if (sockfd < 0)
                    {
                        logMsg(FATAL, "%d:%s", errno, strerror(errno));
                        continue;
                    }

                    // 托管给epoll
                    epoll_event ev;
                    ev.data.fd = sockfd;
                    ev.events = EPOLLIN;
                    int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
                    assert(n == 0);
                    (void)n;
                }
                else
                {
                    // 普通IO

                    int n = _func(sock);
                    if (n < 0 || n == 0)
                    {
                        // 先移除,再关闭
                        int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        assert(n == 0);
                        (void)n;

                        logMsg(DEBUG, "client quit: %d", sock);
                        close(sock);
                    }
                }
            }
            else
            {
                //...
            }
        }
    }

    void Run()
    {
        // 1. 首先添加listensock到epoll
        epoll_event ev;
        ev.data.fd = _listensock;
        ev.events = EPOLLIN;
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock, &ev);
        assert(n == 0);
        (void)n;

        epoll_event revs[num];
        int timeout = 10000;
        while (true)
        {
            int n = epoll_wait(_epfd, revs, num, timeout);
            switch (n)
            {
            case 0:
                std::cout << " time out ... " << (unsigned long)time(nullptr) << std::endl;
                break;
            case -1:
                std::cerr << errno << ": " << strerror(errno) << std::endl;
                break;
            default:
                // 等待成功
                HandlerEvent(revs, n);
                break;
            }
        }
    }

private:
    int _listensock;
    int _epfd;
    int _port;
    func_t _func;
};

运行结果:

6.5 epoll的优点

相比于selectpollepoll在性能和功能上有许多优点:

  1. 高性能:epoll使用红黑树作为事件存储的数据结构,可以快速地添加、删除和查找事件。而selectpoll使用线性列表存储事件,每次查找事件都需要遍历整个列表,效率低下。

  2. 高并发:epoll使用事件通知机制,只有在事件发生时才会通知应用程序,可以避免轮询的开销,减少系统调用次数,同时支持多个文件描述符的并发操作。

  3. 可扩展性:epoll支持水平触发和边缘触发两种模式,可以根据不同场景灵活选择。而selectpoll只支持水平触发模式。

  4. 内存占用低:epoll通过事件通知机制避免了轮询的开销,同时只需要存储活动的文件描述符,相比之下,selectpoll需要存储全部的文件描述符。

  5. 更好的可读性:epoll使用事件驱动的编程模型,可以更清晰地描述应用程序的行为,代码更加可读性和易于维护。

总的来说,epoll在性能和可扩展性方面具有明显优势,适用于高并发、高性能的网络编程场景。而selectpoll在简单的网络编程场景下也可以使用,但在处理大量的并发连接时,效率会明显下降。

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

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

相关文章

脂肪毒性的新兴调节剂——肠道微生物组

谷禾健康 肠道微生物组与脂质代谢&#xff1a;超越关联 脂质在细胞信号转导中起着至关重要的作用&#xff0c;有助于细胞膜的结构完整性&#xff0c;并调节能量代谢。 肠道微生物组通过从头生物合成和对宿主和膳食底物的修饰产生了大量的小分子。 最近的研究表明&#xff0c;由…

[计算机操作系统(第四版 汤小丹 汤子瀛)]第一章 操作系统引论(学习复习笔记)

操作系统&#xff08;Operating System&#xff0c;OS&#xff09;是配置在计算机硬件上的第一层软件&#xff0c;是对硬件系统的首次扩充。操作系统的主要作用&#xff1a; 管理硬件设备&#xff0c;提高他们的利用率和系统吞吐量 利用率&#xff1a;使硬件充分忙碌系统吞吐量…

MQTT协议-心跳请求和心跳响应

MQTT协议-心跳请求和心跳响应 心跳请求是客户端发送给服务器的&#xff0c;心跳响应是服务器回应给客户端的 作用是保持客户端与服务器之间的连接 CONNECT报文的可变报头是由协议名&#xff08;Procotol Name&#xff09;协议级别&#xff08;Procotol Level&#xff09;连接…

冰刃杀毒工具使用实验(29)

实验目的 &#xff08;1&#xff09;学习冰刃的基本功能&#xff1b; &#xff08;2&#xff09;掌握冰刃的基本使用方法&#xff1b;预备知识 windows操作系统的基本知识&#xff0c;例如&#xff1a;进程、网络、服务和文件等的了解。 冰刃是一款广受好评的ARK工…

MYSQL 中 ROW_NUMBER 的实现

由于mysql没有 row_number函数实现&#xff0c;然后我们后台表里没给可以排序的字段&#xff0c;考虑用mysql实现一个row_number, 由于想使用视图进行实时展示&#xff0c;但是row_number在视图中不生效&#xff0c;所有我们最后还是找了后台程序伙伴沟通加了排序字段。 row_nu…

MySQL的图形化界面开发工具DataGrip的下载安装

在日常的开发中&#xff0c;会借助于MySQL的图形化界面&#xff0c;来简化开发&#xff0c;提高开发效率。目前mysql主流的图形化界面工具&#xff0c;有Navicat、SQLyog、DataGrip等&#xff0c;最后一种DataGrip&#xff0c;这种图形化界面工具&#xff0c;功能更加强大&…

Android Framework-管理Activity和组件运行状态的系统进程—— ActivityManagerService(AMS)

ActivityManagerService&#xff08;AMS&#xff09;是Android提供的一个用于管理Activity&#xff08;和其他组件&#xff09;运行状态的系统进程 AMS功能概述 和WMS一样&#xff0c;AMS也是寄存于systemServer中的。它会在系统启动时&#xff0c;创建一个线程来循环处理客户…

uni-app中使用vue3语法详解

全局创建 app.use(createPina()).mount 全局方法 通过app.config.globalProperties.xxx可以创建 这里我们写了一个字符串翻转的全局方法 main.js里面添加一个全局方法 不要忘了加$ 否则会报错 // #ifdef VUE3 //导入创建app import { createSSRApp } from vue //导入创建ap…

数据结构刷题(二十一):131分割回文串、78子集

1.分割回文串题目链接思路&#xff1a;回溯算法的组合方法&#xff08;分割问题类似组合问题&#xff09;。流程图&#xff1a;红色竖杠就是startIndex。 for循环是横向走&#xff0c;递归是纵向走。回溯三部曲&#xff1a;递归函数参数&#xff1a;字符串s和startIndex&#…

Vue(10-20)

1Vue赋值方式 Object.defineProperty <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" conten…

如何从Outlook向Teams共享电子邮件

从 Outlook 或 web 上的 Outlook,你可以将电子邮件共享到Teams中的聊天和频道。 注意: 你必须具有“共享到Teams”的Outlook”加载项。它为Teams用户自动安装。此外,移动设备当前不支持该功能。外接程序当前不支持共享邮箱。如果你安装了Teams桌面客户端,并且使用的是Outl…

精、稳、敏、融,步入人民金融时代 | 易观银行业数字化转型年度趋势报告

易观分析&#xff1a;2022年&#xff0c;在深化金融供给侧结构性改革和高质量增长要求的指引下&#xff0c;赋能实体、公平普惠、审慎经营成为银行业转型发展的关键词。一方面面临内外部复杂的经济环境和不确定性风险&#xff0c;银行主打稳健策略&#xff0c;数字化转型仍在持…

软件测试用例篇(5)

测试是否运行代码去划分&#xff1f; 1)静态测试: 不运行代码&#xff0c;检查代码的风格&#xff0c;格式是否符合公司的标准规范&#xff0c;检查代码的逻辑结构是否满足需求要实现的功能 看代码&#xff0c;不运行代码&#xff0c;通过静态分析代码的语法&#xff0c;编写规…

谷歌Google Pixel6Pro/7pro手机刷入ROOT权限-开通Volte+破解5G网络高速刷机教程

谷歌pixel6和pixel7系列&#xff0c;出厂并不带volte功能&#xff0c;也不支持完美的5G效果&#xff0c;对于我们这种身处大陆&#xff0c;又想体验高清通话的小伙伴来说&#xff0c;并不友好&#xff0c;所以第三方破解实现就显得非常重要了。通过实际测试&#xff0c;两款机型…

Java 文件上传 MultipartFile RequestPart 方式上传 HttpClient

Java springboot/springCloud项目&#xff0c;后端接口是用RequestPart 注解MultipartFile类型和实体类型的参数&#xff0c;目的是同时提交文件和表单参数。前端调用方式&#xff1a;需要使用表单&#xff08;form-data&#xff09;方式进行提交&#xff0c;content-type设置为…

Hive排序,窗口函数

数据排序1&#xff0c;全局排序&#xff08;order by&#xff09;&#xff1a;类似于标准SQL&#xff0c;只使用一个Reducer执行全局数据排序&#xff1b;速度慢,应提前做好数据过滤 &#xff1b;支持使用case when或表达式&#xff1b;支持按位置编号排序desc升序&#xff0c;…

拉链表

每日的用户更新表获取的三种方式&#xff1a;一是监听mysql库数据的变化&#xff0c;比如用canal合并每日的变化&#xff0c;获取到最后的一个状态二是每天获得一份切片数据&#xff0c;可以通过去两天切片数据的不同来作为每日更新表&#xff0c;可以对所有字段先进性concat&a…

Pixhawk RPi CM4 Baseboard 树莓派CM4安装Ubuntu20.04 server 配置ros mavros mavsdk

文章目录硬件安装Ubuntu Server20.04下载rpiboot工具下载imager刷写系统配置USB配置WIFI开机安装桌面配置wifi配置串口安装ROS安装mavros安装MAVSDK-PythonInternet设置最后参考&#xff1a; https://docs.holybro.com/autopilot/pixhawk-baseboards/pixhawk-rpi-cm4-baseboard…

Linux下C/C++ 网络扫描(主机扫描技术)

主机扫描是网络扫描的基础&#xff0c;通过对目标网络中主机IP地址的扫描&#xff0c;从一堆主机中扫描出存活的主机&#xff0c;然后以他们为目标进行后续的攻击。一般会借助于ICMP、TCP、UDP等协议的工作机制&#xff0c;检查打开的进程&#xff0c;开放的端口号等等。 主机…

SpringBoot使用缓存

目录 简介 Spring 的缓存主要有如下几个注解 红色标注的注解为最常用的注解&#xff0c;必须熟练掌握 Cacheable/CachePut/CacheEvict 主要的参数 SpEL 提供的运算符 实现步骤 补充 注意 整合 EHCACHE 简介 Spring 框架已经具备了缓存机制&#xff0c;虽然我们可以使用 …