操作系统中的IO多路复用

操作系统中的IO多路复用

  • 1. IO多路复用的概念和原理
  • 2. 五种IO模型
  • 3. select函数
  • 4. poll函数
  • 5. epoll函数

1. IO多路复用的概念和原理

IO多路复用是一种基于事件驱动的IO模型,它允许一个进程同时监视多个IO事件,并在有事件发生时进行响应。这种模型的核心在于使用一组系统调用同时监听多个文件描述符的IO状态,包括可读、可写、异常等,从而实现高效的IO操作管理。

2. 五种IO模型

  1. 阻塞IO:IO操作会一直阻塞当前线程,直到数据准备完毕才返回结果。
  2. 非阻塞IO:IO操作立即返回,如果数据尚未准备好则返回一个错误码。
  3. IO多路复用:同时监听多个IO事件,当有事件就绪时通知应用程序进行处理,避免了轮询带来的CPU浪费。
  4. 信号驱动IO:IO操作完成时,内核向应用程序发送信号通知,应用程序进行后续处理。
  5. 异步IO:IO操作完成后内核直接将数据拷贝到用户空间,并通过回调函数或其他机制通知应用程序,实现完全异步的IO操作。

3. select函数

select 函数是用于实现I/O多路复用的系统调用之一,其作用是在一组文件描述符上进行监视,以确定是否有输入输出操作可以进行而不会阻塞。

具体来说,select 的作用包括:

多路复用select允许同时监视多个文件描述符,可以在这些文件描述符中的任何一个上等待数据的到达,而不需要为每个文件描述符创建一个单独的线程或进程来处理。
非阻塞等待:使用 select 可以在一组文件描述符上进行非阻塞的等待,当其中任何一个文件描述符准备好进行I/O操作时,select 函数返回,并告知哪些文件描述符已经就绪。
异步通知select 函数可以在指定的一段时间内等待文件描述符的就绪状态,或者一直等待直到文件描述符就绪为止。这种等待过程是异步的,不会阻塞整个程序的执行。
多种事件select 不仅可以检测文件描述符是否可读,还可以检测文件描述符是否可写、是否出现异常等。

select函数原型

#include <sys/select.h>

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

nfds:这是一个整数类型的参数,表示监视的文件描述符的最大值加1。在Unix系统中,文件描述符是从0开始的,因此nfds通常设置为需要监视的最大文件描述符加1。

fd_set 结构:
使用 fd_set 类型时,实际上使用了一种位图的数据结构。这个位图中的每一位对应一个文件描述符,位图的内容表示该文件描述符是否在集合中。如果一个位是 1,则表示对应位置的文件描述符在集合中,有效;如果是 0,则表示对应位置的文件描述符不在集合中,无效。
fd_set 是一个位图,其大小是固定的,通常上限是 1024 个文件描述符

fd_set 提供了一系列操作宏来操作文件描述符集合:
FD_ZERO(fd_set *set):将文件描述符集合清空。
FD_SET(int fd, fd_set *set):将文件描述符 fd 添加到集合中。
FD_CLR(int fd, fd_set *set):从集合中移除文件描述符 fd
FD_ISSET(int fd, fd_set *set):检查文件描述符 fd 是否在集合中。

fd_set *readfds、fd_set *writefds、fd_set * exceptfds: 这三个参数都是指向 fd_set 类型的指针。fd_set 是一个位图,用于表示文件描述符集合。这三个参数分别表示待检查可读、可写和异常条件的文件描述符集合。

struct timeval 结构体:

struct timeval {
    time_t tv_sec;  // 秒
    suseconds_t tv_usec;  // 微秒
};

tv_sec 表示等待的秒数,tv_usec 表示等待的微秒数。如果 timeout 参数为 NULLselect 函数将会一直阻塞,直到有文件描述符就绪或者被信号中断。

基于 select 的多路复用服务器:

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#include <vector>
#include <algorithm>

constexpr int MAX_CLIENTS = 10;
constexpr int BUFFER_SIZE = 1024;

int main() {
    // 创建监听套接字
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        std::cerr << "Error: Could not create socket\n";
        return 1;
    }

    // 绑定地址和端口
    sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(8080);

    if (bind(server_socket, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address)) == -1) {
        std::cerr << "Error: Could not bind to address\n";
        close(server_socket);
        return 1;
    }

    // 监听连接
    if (listen(server_socket, MAX_CLIENTS) == -1) {
        std::cerr << "Error: Could not listen on socket\n";
        close(server_socket);
        return 1;
    }

    // 准备客户端套接字集合
    fd_set client_fds;
    FD_ZERO(&client_fds);
    FD_SET(server_socket, &client_fds);
    int max_fd = server_socket;

    while (true) {
        // 复制客户端套接字集合以供select修改
        fd_set read_fds = client_fds;

        // 使用select等待读事件
        if (select(max_fd + 1, &read_fds, nullptr, nullptr, nullptr) == -1) {
            std::cerr << "Error: Select failed\n";
            break;
        }

        // 检查服务器套接字是否有新的连接
        if (FD_ISSET(server_socket, &read_fds)) {
            sockaddr_in client_address;
            socklen_t client_address_len = sizeof(client_address);
            int client_socket = accept(server_socket, reinterpret_cast<sockaddr*>(&client_address), &client_address_len);
            if (client_socket == -1) {
                std::cerr << "Error: Could not accept connection\n";
            } else {
                std::cout << "New connection from " << inet_ntoa(client_address.sin_addr) << ":" << ntohs(client_address.sin_port) << std::endl;
                FD_SET(client_socket, &client_fds);
                max_fd = std::max(max_fd, client_socket);
            }
        }

        // 检查现有连接是否有数据可读
        for (int i = server_socket + 1; i <= max_fd; ++i) {
            if (FD_ISSET(i, &read_fds)) {
                char buffer[BUFFER_SIZE];
                int bytes_received = recv(i, buffer, BUFFER_SIZE, 0);
                if (bytes_received <= 0) {
                    if (bytes_received == 0) {
                        std::cout << "Connection closed by client\n";
                    } else {
                        std::cerr << "Error: Could not receive data\n";
                    }
                    close(i);
                    FD_CLR(i, &client_fds);
                } else {
                    std::cout << "Received: " << std::string(buffer, bytes_received);
                }
            }
        }
    }

    close(server_socket);
    return 0;
}

select 函数的缺点包括:

  1. 效率低下:在大量文件描述符时,性能下降。
  2. 文件描述符数量限制:监视的文件描述符数量有限。
  3. 用户空间与内核空间据拷贝增加系统开销。

4. poll函数

poll 是用于实现I/O多路复用的系统调用,它和 select 在实现上有一些区别,但都具有相似的作用。

作用:

实现I/O多路复用selectpoll 都允许同时监视多个文件描述符,以确定是否有输入输出操作可以进行而不会阻塞。
非阻塞等待:使用 selectpoll 可以在一组文件描述符上进行非阻塞的等待,当其中任何一个文件描述符准备好进行I/O操作时,函数返回,并告知哪些文件描述符已经就绪。
异步通知selectpoll 可以在指定的一段时间内等待文件描述符的就绪状态,或者一直等待直到文件描述符就绪为止。这种等待过程是异步的,不会阻塞整个程序的执行。
用于网络编程和文件I/Oselectpoll 可以用于网络编程中的服务器端和客户端,也可以用于管道、套接字等文件描述符的I/O操作。

select 和 poll 区别:

数据结构不同:
select 使用 fd_set 结构来管理文件描述符集合。
poll 使用 pollfd 结构数组来管理文件描述符及其关注的事件。
事件通知方式不同:
select 中就绪事件通过修改 fd_set 集合来返回。
poll 中就绪事件通过设置 pollfd 结构体中的 revents 字段来返回。
文件描述符数量限制:
select 需要指定文件描述符的最大值加1,并且有限制(通常为1024)。
poll 则不需要指定文件描述符的最大值,并且没有文件描述符数量的限制。
性能差异:
在文件描述符较多时,poll 通常比 select 更高效,因为 poll 使用了更为简洁的数据结构,在内核中的实现也更为高效。

poll函数原型:

#include <poll.h>

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

struct pollfd结构体:

struct pollfd {
    int   fd;       // 要监视的文件描述符
    short events;   // 要监视的事件
    short revents;  // 实际发生的事件
};

fd:表示要监视的文件描述符。当 fd 为负数时,表示忽略此项,即此 pollfd 结构体不会监视任何文件描述符。

events:表示要监视的事件,是一个位掩码,用于指定感兴趣的事件。常见的事件包括:
POLLIN:表示可读事件。
POLLOUT:表示可写事件。
POLLERR:表示错误事件。
POLLHUP:表示挂起事件。
POLLNVAL:表示无效事件。

revents:表示实际发生的事件,是 poll 函数返回时填充的字段。它是由内核填充的位掩码,用于指示发生了哪些事件。在调用 poll 函数后,应用程序需要检查 revents 字段来确定哪些事件发生了。

fds:一个指向 struct pollfd 结构体数组的指针,用于指定待监视的文件描述符及其事件。
nfds:监视的文件描述符数量,即 fds 数组中元素的个数。
timeout:超时时间,单位为毫秒。如果设置为负数,表示 poll 将永远阻塞,直到有事件发生;如果设置为 0,表示 poll 立即返回,即便没有任何事件发生;如果设置为正数,表示 poll 在超时时间内等待事件发生。

基于 poll 的多路复用服务器

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <vector>
#include <algorithm>

constexpr int MAX_CLIENTS = 10;
constexpr int BUFFER_SIZE = 1024;

int main() {
    // 创建监听套接字
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        std::cerr << "Error: Could not create socket\n";
        return 1;
    }

    // 绑定地址和端口
    sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(8080);

    if (bind(server_socket, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address)) == -1) {
        std::cerr << "Error: Could not bind to address\n";
        close(server_socket);
        return 1;
    }

    // 监听连接
    if (listen(server_socket, MAX_CLIENTS) == -1) {
        std::cerr << "Error: Could not listen on socket\n";
        close(server_socket);
        return 1;
    }

    // 准备客户端套接字集合
    std::vector<pollfd> client_fds(1);
    client_fds[0].fd = server_socket;
    client_fds[0].events = POLLIN;

    while (true) {
        // 使用poll等待读事件
        if (poll(client_fds.data(), client_fds.size(), -1) == -1) {
            std::cerr << "Error: Poll failed\n";
            break;
        }

        // 检查服务器套接字是否有新的连接
        if (client_fds[0].revents & POLLIN) {
            sockaddr_in client_address;
            socklen_t client_address_len = sizeof(client_address);
            int client_socket = accept(server_socket, reinterpret_cast<sockaddr*>(&client_address), &client_address_len);
            if (client_socket == -1) {
                std::cerr << "Error: Could not accept connection\n";
            } else {
                std::cout << "New connection from " << inet_ntoa(client_address.sin_addr) << ":" << ntohs(client_address.sin_port) << std::endl;
                client_fds.push_back({client_socket, POLLIN});
            }
        }

        // 检查现有连接是否有数据可读
        for (size_t i = 1; i < client_fds.size(); ++i) {
            if (client_fds[i].revents & POLLIN) {
                char buffer[BUFFER_SIZE];
                int bytes_received = recv(client_fds[i].fd, buffer, BUFFER_SIZE, 0);
                if (bytes_received <= 0) {
                    if (bytes_received == 0) {
                        std::cout << "Connection closed by client\n";
                    } else {
                        std::cerr << "Error: Could not receive data\n";
                    }
                    close(client_fds[i].fd);
                    client_fds.erase(client_fds.begin() + i);
                    --i; // 因为erase后迭代器会失效,需要调整索引
                } else {
                    std::cout << "Received: " << std::string(buffer, bytes_received);
                }
            }
        }
    }

    close(server_socket);
    return 0;
}

poll 函数的缺点包括:

  1. 低效性poll 在处理大量文件描述符时效率较低。每次调用 poll 都需要遍历所有注册的文件描述符,无论它们是否就绪,因此随着文件描述符数量的增加,poll 的性能会逐渐下降。
  2. 复制开销poll 使用的是用户空间的数据结构来管理事件,每次调用 poll 都需要将文件描述符集合复制到内核空间,这个复制过程会带来一定的开销。
  3. 不支持边缘触发poll 不支持边缘触发(Edge-Triggered,简称 ET)模式,只支持水平触发(Level-Triggered,简称 LT)模式。在高并发场景下,边缘触发模式更加高效,因为它只会在状态发生变化时通知应用程序。

5. epoll函数

epollLinux 内核提供的一种事件通知机制,用于实现高性能的I/O多路复用。它是 select 和 poll 的改进版本,在处理大量并发连接时具有更好的性能和扩展性。

主要特点:

高性能epoll 可以有效地管理大量的文件描述符,监视它们的I/O事件并通知应用程序,避免了 selectpoll 在大规模连接时的性能瓶颈。
事件驱动epoll 是事件驱动的,当文件描述符上发生对应的事件时,内核会将该事件通知给应用程序,从而实现了异步的I/O操作。
支持多种模式epoll 支持边沿触发(Edge-Triggered,简称 ET)和水平触发(Level-Triggered,简称 LT)两种模式,可以根据需要选择合适的模式。
仅通知活跃事件:与 pollselect 不同,epoll 只会通知活跃的事件,而不是遍历所有注册的文件描述符,因此在有大量非活跃文件描述符的情况下,epoll 的性能更好。
使用内核空间管理事件epoll 利用了 Linux 内核空间的数据结构来管理事件,避免了大量文件描述符的复制和遍历,从而提高了性能。

epoll 原理:

epoll 使用三种核心数据结构来管理事件
红黑树Red-Black Tree):用于存储监听的文件描述符,通过文件描述符来快速定位到对应的事件结构。
就绪链表Ready List):用于存储已经就绪的事件,当有文件描述符上的事件发生时,将对应的事件结构添加到就绪链表中。
事件结构Event Structure):用于存储文件描述符的事件状态(读、写、异常等)以及其他相关信息。

注册事件:
应用程序通过调用 epoll_create 创建一个 epoll 实例,并使用 epoll_ctl 函数将需要监听的文件描述符注册到 epoll 实例中,同时指定关注的事件类型(读、写、错误等)。

事件监听:
当文件描述符上的事件发生时,内核会将对应的事件结构添加到就绪链表中。 epoll_wait 函数被调用时,内核会检查就绪链表,将其中的事件返回给应用程序,并在就绪链表中移除这些事件。

效率优化:
epoll 的核心优化在于使用了红黑树来存储文件描述符,这使得 epoll 能够高效地处理大量的文件描述符。
epoll 只通知活跃的事件,避免了遍历所有文件描述符的开销,从而提高了效率。

事件触发模式:

epoll 支持两种事件触发模式:水平触发(Level-Triggered,简称 LT)和边缘触发(Edge-Triggered,简称 ET)。
水平触发模式下,当文件描述符上的事件处于就绪状态时,每次调用 epoll_wait 都会返回该事件。
边缘触发模式下,只有在状态发生变化时才会通知应用程序,因此需要应用程序自己管理事件的状态。

epoll 函数是 Linux 提供的一组系统调用,用于实现高性能的I/O多路复用。它主要包括以下几个函数:

  1. epoll_create函数
#include <sys/epoll.h>

int epoll_create(int size);

功能:创建一个 epoll 实例,返回一个文件描述符,用于后续的 epoll 操作。
参数:
size:只需传入一个大于0的值。
返回值:成功时返回一个非负整数,表示 epoll 实例的文件描述符;失败时返回 -1,并设置 errno。

  1. epoll_ctl函数
#include <sys/epoll.h>

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

功能:控制 epoll 实例中的文件描述符,用于注册、修改或删除文件描述符的监听事件。
参数
epfdepoll 实例的文件描述符,由 epoll_create 返回。
op:操作类型,可以是 EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或 EPOLL_CTL_DEL(删除)。
fd:待操作的文件描述符。
event:指向 epoll_event 结构体的指针,用于设置监听事件。
返回值:成功时返回 0;失败时返回 -1,并设置 errno。

epoll_event 结构体:

struct epoll_event {
    uint32_t events; // 监听的事件类型,可由 EPOLLIN、EPOLLOUT、EPOLLERR 等组合而成
    epoll_data_t data; // 与事件相关的数据,可以是文件描述符或者指针等
};

epoll_data_t 是一个联合体(union),用于存储与 epoll 事件相关的数据。它的定义如下:

typedef union epoll_data {
    void *ptr;         // 指向任意数据类型的指针
    int fd;            // 文件描述符
    uint32_t u32;      // 32位整数值
    uint64_t u64;      // 64位整数值
} epoll_data_t;
  1. epoll_wait函数
#include <sys/epoll.h>

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

功能:等待文件描述符上的事件发生,当有事件发生时,将事件信息填充到指定的数组中。
参数:
epfdepoll 实例的文件描述符,由 epoll_create 返回。
events(输出型参数):指向 epoll_event 结构体数组的指针,用于存储事件信息,即就绪队列
maxevents:指定 events 数组的大小,即最多可以存储多少个事件。
timeout:超时时间,单位为毫秒,可以是 -1(无限等待),0(立即返回),或一个正整数(等待指定的时间)。
返回值:成功时返回就绪的事件数量;失败时返回 -1,并设置 errno

基于 epoll 的多路复用服务器

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <cstring>

constexpr int MAX_EVENTS = 10;
constexpr int BACKLOG = 5;
constexpr int BUFFER_SIZE = 1024;

int main() {
    // 创建监听套接字
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        std::cerr << "Error: Could not create socket\n";
        return 1;
    }

    // 设置为非阻塞模式
    fcntl(server_socket, F_SETFL, O_NONBLOCK);

    // 绑定地址和端口
    sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(8080);

    if (bind(server_socket, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address)) == -1) {
        std::cerr << "Error: Could not bind to address\n";
        close(server_socket);
        return 1;
    }

    // 监听连接
    if (listen(server_socket, BACKLOG) == -1) {
        std::cerr << "Error: Could not listen on socket\n";
        close(server_socket);
        return 1;
    }

    // 创建epoll实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        std::cerr << "Error: Could not create epoll instance\n";
        close(server_socket);
        return 1;
    }

    epoll_event event;
    event.events = EPOLLIN | EPOLLET; // 关注可读事件,边沿触发模式
    event.data.fd = server_socket;

    // 将服务器套接字添加到epoll实例中
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) == -1) {
        std::cerr << "Error: Could not add server socket to epoll\n";
        close(server_socket);
        close(epoll_fd);
        return 1;
    }

    epoll_event events[MAX_EVENTS];

    while (true) {
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < num_events; ++i) {
            if (events[i].data.fd == server_socket) {
                // 新的连接
                sockaddr_in client_address;
                socklen_t client_address_len = sizeof(client_address);
                int client_socket = accept(server_socket, reinterpret_cast<sockaddr*>(&client_address), &client_address_len);
                if (client_socket == -1) {
                    std::cerr << "Error: Could not accept connection\n";
                    continue;
                }

                // 设置为非阻塞模式
                fcntl(client_socket, F_SETFL, O_NONBLOCK);

                // 将客户端套接字添加到epoll实例中
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) == -1) {
                    std::cerr << "Error: Could not add client socket to epoll\n";
                    close(client_socket);
                }
                std::cout << "New connection from " << inet_ntoa(client_address.sin_addr) << ":" << ntohs(client_address.sin_port) << std::endl;
            } else {
                // 已有连接的读事件
                char buffer[BUFFER_SIZE];
                int bytes_received = recv(events[i].data.fd, buffer, BUFFER_SIZE, 0);
                if (bytes_received == -1) {
                    std::cerr << "Error: Could not receive data\n";
                } else if (bytes_received == 0) {
                    // 客户端关闭连接
                    std::cout << "Connection closed by client\n";
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);
                    close(events[i].data.fd);
                } else {
                    std::cout << "Received: " << std::string(buffer, bytes_received);
                }
            }
        }
    }

    close(server_socket);
    close(epoll_fd);
    return 0;
}

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

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

相关文章

在Vue项目使用kindEditor富文本编译器以及上传图片

第一步 npm install kindeditor第二步&#xff0c;建立kindeditor.vue组件 <template><div class"kindeditor"><textarea :id"id" name"content" v-model"outContent"></textarea></div> </templa…

【C语言__结构体__复习篇5】

目录 前言 一、结构体基础知识 1.1 结构体的语法形式 1.2 创建结构体变量 1.3 结构体变量的初始化 1.4 点(.)操作符和箭头(->)操作符 二、匿名结构体 三、结构体自引用 四、结构体内存对齐 4.1 内存对齐的规则 4.2 出现结构体内存对齐的原因 4.3 修改默认对齐数 五、结…

JavaScript高级

一、JavaScript 面向对象 面向对象编程介绍ES6 中的类和对象类的继承面向对象案例 1. 面向对象编程介绍 1.1 两大编程思想 面向过程面向对象 1.2 面向过程编程 POP(Process-oriented programming) 面向过程 就是分析出解决问题所需要的步骤&#xff0c;然后用函数把这些步…

shell脚本编程的例子(50例子)-1

前言 为了提高教学质量&#xff0c;并且能够让童鞋们更好的理解和运用shell脚本以及相关编程&#xff0c;特编写了50个shell例子&#xff0c;目前还在整理过程ing&#xff0c;计划分三期完成。请有需要的同学收藏。后续会申请VIP阅读。…… ^.^ …… ^…^ 实验环境&#xff1…

javaWeb项目-智慧餐厅点餐管理系统功能介绍

项目关键技术 开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 1、JavaScript Java…

C语言进阶课程学习记录-内存操作经典问题分析

C语言进阶课程学习记录-内存操作经典问题分析 实验-示例1优化 实验-示例2优化 实验-示例3实验-示例4小结 本文学习自狄泰软件学院 唐佐林老师的 C语言进阶课程&#xff0c;图片全部来源于课程PPT&#xff0c;仅用于个人学习记录 实验-示例1 #include <stdio.h> #include…

ChatGPT研究论文提示词集合1-【主题选择与问题研究、文献综述】

点击下方▼▼▼▼链接直达AIPaperPass &#xff01; AIPaperPass - AI论文写作指导平台 目录 1.主题选择与问题定义 2.文献综述 3.书籍介绍 AIPaperPass智能论文写作平台 近期小编按照学术论文的流程&#xff0c;精心准备一套学术研究各个流程的提示词集合。总共14个步骤…

HTTP请求中的cookie与session(servlet实现登录页面的表单验证)

一、cookie 与 session 1&#xff09;cookie 与 session 的定义 2&#xff09;相关的servlet中的 方法 二、代码实现 登录页面 1&#xff09;先用 vscode 编写登录页面 注意文件的路径 在webapp路径下 <!DOCTYPE html> <html lang"en"><head>&…

ai写作强大,ai写作哪个软件最好用?

在当今数字化时代&#xff0c;ai技术的发展正以惊人的速度改变着我们的生活和工作方式。其中&#xff0c;ai写作作为一项令人瞩目的创新&#xff0c;展示了强大的文本生成能力。然而&#xff0c;随着各种ai写作软件的涌现&#xff0c;人们不禁困惑&#xff1a;哪个软件才是最好…

【网络设备巡检命令】--思科、华为、H3C、锐捷

【网络设备巡检命令】--思科、华为、H3C、锐捷 一、思科二、华为三、H3C四、锐捷 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 一、思科 1、查看系统信息&#xff1a; show version2、查看时间&#xff1a; show clock3、查看序列号&a…

Zed 捕获图像+测距

Zed 捕获图像测距 1. 导入相关库2. 相机初始化设置3. 获取中心点深度数据4. 计算中心点深度值5. 完整代码5. 实验效果 此代码基于官方代码基础上进行改写&#xff0c;主要是获取zed相机深度画面中心点的深度值&#xff0c;为yolo测距打基础。 1. 导入相关库 import pyzed.sl …

C语言 ─── 操作符详解

目录 1. 算术操作符 2. 移位操作符 2.1 左移操作符 2.2 右移操作符 3. 位操作符 4. 复合赋值符 5. 单目操作符 6. 逗号表达式 7. 隐式类型转换 7.1 整型提升的意义&#xff1a; 7.2 如何进行整体提升呢&#xff1f; 8. 算术转换 ★★★数组名 1. 算术操作符 -…

redis与etcd的对比

1.redis是一种高级的key&#xff1a;value存储系统&#xff0c;其中value支持五种数据类型&#xff1a; 1.1 字符串&#xff08;strings&#xff09; 1.2 字符串列表&#xff08;lists&#xff09; 1.3 字符串集合&#xff08;sets&#xff09; 1.4 有序字符串集合&#xff08;…

管理 nodejs 版本工具 nvm

nvm 方便切换不同版本的 node 及 对应的 npm 版本 一、安装nvm nvm官网 &#xff08;内含下载的文件&#xff0c;点击进去下载&#xff0c;并按照 网站文档步骤 操作即可&#xff09; 二、nvm 基础命令 nvm arch&#xff1a;显示node是运行在32位还是64位。nvm install <…

centos修改启动项加载不同内核

一.背景&#xff1a; 虚拟机中有时需要编译好几个内核版本&#xff0c;make install后系统存在几个内核版本。需要再哪个内核上开发调试就启动特定的内核版本。这就需要修改启动时的内核版本&#xff0c;再物理机或虚拟机启动时可以上下键选择。但有时是docket云环境中或远程时…

CANoe中LIN工程主节点的配置(如何切换调度表)

1&#xff1a;前置条件 1&#xff09;工程已经建立&#xff0c;simulation窗口已经配置好&#xff08;包括且不限于通道mappin好&#xff0c;数据库文件已经添加&#xff09; 2&#xff09;我已系统自带sampleCfg工程&#xff0c;作为例子。如下图 2 &#xff1a;主节点的配置…

前端css中table表格的属性使用

前端css中table表格的属性使用 一、前言二、常见的表格属性1.边框的样式2.布局和对齐3.间距和填充4.背景和颜色5.字体的样式6.边框的圆角 三、简单的表格&#xff0c;例子11.源码12.源码1效果截图 四、给表格添加动画效果&#xff0c;例子21.源码22.源码2的运行效果 五、结语六…

【热门话题】探索与心得:深入体验Microsoft Edge浏览器

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 探索与心得&#xff1a;深入体验Microsoft Edge浏览器一、Edge浏览器概述1.1 发…

通快霍廷格TruPlasma MF中频电源培训PPT课件内容下图

通快霍廷格TruPlasma MF中频电源培训PPT课件内容下图

美业连锁门店收银系统源码-如何查看收款门店对应的加盟商?

美业管理系统源码 博弈美业SaaS系统 连锁多门店美业收银系统源码 多门店管理 / 会员管理 / 预约管理 / 排班管理 / 商品管理 / 促销活动 PC管理后台、手机APP、iPad APP、微信小程序 第一步&#xff1a; 登录pc管理后端 第二步&#xff1a; 进入企业组织管理-门店管理&a…
最新文章