IO多路转接之select和poll

目录

一. IO多路转接的概念

二. 通过select实现IO多路转接

2.1 select接口

2.2 Select服务器的实现

2.3 select实现IO多路转接的优缺点

三. 通过poll实现IO多路转接

3.1 poll接口

3.2 Poll服务器的实现

3.3 poll实现IO多路转接的优缺点

四. 总结


一. IO多路转接的概念

在IO操作中,如果我们阻塞式的等待某个文件描述符资源就绪,那么在等待的过程中,就会浪费大量的时间,造成程序运行的效率低下。在实际的工程应用中,可能存在同时有多个文件描述符就绪的情况,这时如果阻塞等待其他未就绪的文件描述符,其它已经就绪的文件描述符也就暂时无法进行处理。

相比与单纯地阻塞式IO,IO多路转接能够实现这样的功能:当用户所关心的多个文件描述符的其中之一就绪时,就对这个就绪的进行处理。IO多路转接能够大大降低阻塞等待的时间,提高程序IO操作的效率。

图1.1 IO多路转接的实现逻辑

二. 通过select实现IO多路转接

2.1 select接口

函数原型:int select(int nfds, struct fd_set* readfds, struct fd_set* writefds, struct fd_set* exceptfds, struct timval* timeout)

头文件:#include <sys/select.h>

函数参数:

  • nfds:所关注的值最大的文件描述符值+1。
  • readfds/writefds/exceptfds:输入输出型参数,设置关注的读/写/异常文件描述符。
  • timeout:输入输出型参数,设置最长阻塞时间,获取剩余时间。

返回值:如果执行成功,返回就绪的文件描述符个数,等待超时返回0,等待失败返回-1。

在使用select函数时,有以下几点需要注意:

  • readfds/writefds/exceptfds均为输入输出型参数,作为输入型参数时告知内核需要关系哪些文件描述符,作为输出型参数时由内核告知用户哪些文件描述符已经就绪。因此,每次调用select之前,都需要对readfds/writefds/exceptfd重新进行设置。
  • readfds/writefds/exceptfds的底层是由位图实现的,但是,不可以通过简单的按位与1操作设置关心特定文件描述符,而是应当通过下面四个接口,来实现对某个fd_set对象的操作:
  1. FD_SET(int fd, fd_set* set):将指定fd添加到fd_set类型对象中去。
  2. FD_ISSET(int fd, fd_set* set):检查指定fd是否出现在fd_set对象中。
  3. FD_ZERO(fd_set* set):将set对象设置关注的文件描述符全部清空。
  4. FD_CLR(int fd, fd_set* set):清除fd_set对象中的指定文件描述符。
  • timeout为最长的阻塞等待时间,如果设置为nullptr则表示为一直阻塞,struct timeval类型的定义,如果select成功执行,那么timeout的值变为了剩余多长时间没有用,比如:设置了5s的最长等待时间,但是2s就有文件描述符就绪,还剩下3s,那么当select运行结束后,timeout就被设置为3s。
struct timeval 
{long tv_sec;    /* seconds */long tv_usec;   /* microseconds */
};

代码2.1展示了如何使用select接口,设置关注标准输入、标准输出和标准错误的读状态,设置最长阻塞时间1s,每次调用select前对fd_set对象和timeout重新设置,避免上一层调用select输出覆盖,在检查到select返回值>0时,还应使用FD_ISSET进一步检查所关注的文件描述符是否真正就绪,下面的代码真正所关系的文件描述符是标准输入0。

代码2.1:select接口的使用方法

#include <iostream>
#include <cstring>
#include <sys/select.h>
#include <unistd.h>int main()
{// 读取缓冲区char buffer[1024] = { 0 };while(true){// 每次调用select之前,重新设置fd_set类型参数和Timeval阻塞时间fd_set rfd;FD_SET(0, &rfd);FD_SET(1, &rfd);FD_SET(2, &rfd);    // 设置关心标准输入、标准输出和标准错误// 设置最长阻塞时间为1sstruct timeval timeout;timeout.tv_sec = 1; timeout.tv_usec = 0;// 调用select进行IO多路转接int n = select(3, &rfd, nullptr, nullptr, &timeout);if(n > 0){// 如果标准输入没有就绪,那么直接到下一轮循环中去if(!FD_ISSET(0, &rfd)){continue;}ssize_t sz = read(0, buffer, 1023);buffer[sz - 1] = '\0';std::cout << "Show message# " << buffer << std::endl;std::cout << "Remain time: " << timeout.tv_sec << " seconds," << timeout.tv_usec << " microseconds" << std::endl;if(strcmp(buffer, "quit") == 0){break;}}else if(n == 0){std::cout << "WARNING, Time out!" << std::endl;}}return 0;
}

2.2 Select服务器的实现

本文实现一个基于TCP协议,可以从客户端读取数据的Select服务器。Select服务器的声明见代码2.2,其中包含基本的构造函数和析构函数,还有Handler函数在检测到有就绪的文件描述符后进行处理、Accepter函数用于接受对端连接请求、Reciever函数用于从指定文件描述符中读取数据。

代码2.2:SelectServer的声明(SelectServer.hpp头文件)

#pragma once#include "Sock.hpp"
#include <vector>
#include <sys/select.h>
#include <unistd.h>static const int FD_CAPACITY = 8 * sizeof(fd_set);
static const int NON_FD = -1;class SelectServer
{
public:SelectServer(uint16_t port, const std::string& ip = "");   // 构造函数void start();     // Select服务器启动运行函数~SelectServer();  // 析构函数private:void Handler(fd_set& rfd);  // 处理就绪文件描述符函数void Reciever(int pos);     // 内容读取函数void Accepter();            // 链接接收函数void ShowFdArray();         // 文件描述符输出函数 -- 用于DEBUGint _listenSock;    // 监听套接字fduint16_t _port;     // 服务器端口号std::string _ip;    // ip地址std::vector<int> _fd_array;    // 文件描述符序列
};

下面为Select服务器每个成员函数的实现需要注意的一些事项:

  • 在Class SelectServer中,需要有一个_fd_array数组,其中记录需要关注的文件描述符,用于每次调用select之前设置fd_set对象,其中_fd_array可以是C语言数组或顺序表vector。在构造函数中要为_fd_array预先开辟好一块空间,并将每个位置的值设置为一个负数值NON_FD,表示这个位置没有存放关注的文件描述符fd。
  • 在构造函数中,要执行基于TCP协议服务器的基本操作:获取listen套接字、绑定端口号、设置监听状态。
  • start函数为服务器运行函数,由于服务器是一个常驻进程,因此start执行while死循环,在每轮循环中,都遍历_fd_array来设置fd_set对象,并且要检查select的返回值是否大于0,即:检查是否有就绪的文件描述符。如果有,就调用handler函数进行处理。
  • Handler的功能是处理已经就绪的文件描述符,在Handler中要遍历_fd_array的每个fd,通过FD_ISSET检查是否就绪,如果就就绪,还要分为listen文件描述符和普通文件描述符两种情况来讨论。
  • Accepter函数用于接收客户端的连接请求,Reciever用于读取客户端发送的数据。

代码2.3:日志打印函数的实现(log.hpp头文件)

#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>#define DEBUG  0
#define NORMAL 1
#define WARING 2
#define ERROR  3
#define FATAL  4// 日志等级
static const char* g_levelMap[5] = 
{"DEBUG","NORMAL","WARING","ERROR","FATAL"
};// 日志打印哈数,level为日志等级,后面为格式化可变参数
static void logMessage(int level, const char *format, ...)
{// 1. 输出常规部分time_t timeStamp = time(nullptr);struct tm *localTime = localtime(&timeStamp);printf("[%s]  %d-%d-%d, %02d:%02d:%02d\n", g_levelMap[level], localTime->tm_year, localTime->tm_mon, \localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec);// 2. 输出用户自定义部分va_list args;va_start(args, format);vprintf(format, args);va_end(args);
}

代码2.4:网络通信Socket相关函数的实现(Sock.hpp头文件)

#pragma once#include "log.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class Sock
{
public:// 创建socket文件描述符static int Socket(){int sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){logMessage(FATAL, "socket error, sock:%d\n", sock);return -1;}logMessage(NORMAL, "socket success, sock:%d\n", sock);return sock;}// 绑定端口号static int Bind(int sock, const std::string &ip, uint16_t port){struct sockaddr_in tmp;memset(&tmp, 0, sizeof(tmp));tmp.sin_family = AF_INET;    // 网络协议族tmp.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());  // ip地址tmp.sin_port = htons(port);  // 端口号socklen_t len = sizeof(tmp);if(bind(sock, (struct sockaddr *)&tmp, len) < 0){logMessage(FATAL, "bind error!\n");return -1;}logMessage(NORMAL, "bind success!\n");return 0;}// 设置监听状态static int Listen(int sock, int backlog = 10){if(listen(sock, backlog) < 0){logMessage(FATAL, "listen error!\n");return -1;}logMessage(NORMAL, "listen success!\n");return 0;}// 接受连接static int Accept(int sock, std::string& ip, uint16_t& port){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);int fd = accept(sock, (struct sockaddr *)&peer, &len);if(fd < 0) return -1;ip = inet_ntoa(peer.sin_addr);port = ntohs(peer.sin_port);logMessage(NORMAL, "accept success, [%s-%d]\n", ip.c_str(), port);return fd;}// 连接对方static int Connect(int sock, const std::string &ip, uint16_t port){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));peer.sin_family = AF_INET;peer.sin_addr.s_addr = inet_addr(ip.c_str());peer.sin_port = htons(port);socklen_t len = sizeof(peer);if(connect(sock, (const struct sockaddr *)&peer, len) < 0){logMessage(FATAL, "conect error!\n");return -1;}logMessage(NORMAL, "connect success!\n");return 0;}
};

代码2.5:SelectServer的实现(SelectServer.cc源文件)

#include "SelectServer.hpp"SelectServer::SelectServer(uint16_t port, const std::string& ip): _listenSock(-1), _port(port), _ip(ip), _fd_array(FD_CAPACITY, NON_FD)
{// 获取监听套接字_listenSock = Sock::Socket();         if(_listenSock < 0){exit(2);}// 绑定端口号if(Sock::Bind(_listenSock, ip, _port) < 0){exit(3);}// 设置监听状态if(Sock::Listen(_listenSock) < 0){exit(4);}         
}// Select服务器启动运行函数
void SelectServer::start()
{while(true){fd_set rfd;     // select所关注的读取fdFD_ZERO(&rfd);  // 将文件描述符清零_fd_array[0] = _listenSock;   // 默认设置_fd_array[0]为listenSockShowFdArray();// 将_fd_array中的有效文件描述符记录到rfd中去int maxFd = _listenSock;   // 最大文件描述符for(const auto fd : _fd_array){if(fd != NON_FD){FD_SET(fd, &rfd);   // 添加文件描述符if(fd > maxFd) maxFd = fd;   // 更新最大放大}}// 设置select,监视文件描述符就绪状态(暂时设置为阻塞)int n = select(maxFd + 1, &rfd, nullptr, nullptr, nullptr);switch(n){case 0:     // 没有文件描述符就绪logMessage(DEBUG, "Time out, without any interest fd prepared!\n");break;case -1:    // select发生错误logMessage(ERROR, "Select error, errno:%d, errMsg:%s\n", errno, strerror(errno));break;default:    // 有至少一个文件描述符就绪Handler(rfd);break;}}
}// 析构函数
SelectServer::~SelectServer()
{if(_listenSock >= 0)close(_listenSock);
}// 就绪文件描述符处理函数
void SelectServer::Handler(fd_set& rfd)
{// 遍历_fd_array,检查有哪个fd就绪了,进行处理for(int i = 0; i < FD_CAPACITY; ++i){if(_fd_array[i] != NON_FD && FD_ISSET(_fd_array[i], &rfd)){// 分为listen套接字和普通套接字来处理if(_fd_array[i] == _listenSock) Accepter();else Reciever(i);}}
}// 数据读取函数
void SelectServer::Reciever(int pos)
{char buffer[1024];ssize_t n = recv(_fd_array[pos], buffer, 1023, 0);if(n > 0)   // 读取成功{buffer[n] = '\0';printf("Recieve message from Client:%s\n", buffer);}else if(n == 0)   // 对端关闭{logMessage(DEBUG, "Client closed, fd:%d\n", _fd_array[pos]);close(_fd_array[pos]);_fd_array[pos] = NON_FD;}else  // 读取失败{logMessage(ERROR, "Recv error, errno:%d, errMsg:%s\n", errno, strerror(errno));}
}// 链接接收函数
void SelectServer::Accepter()
{std::string cli_ip;uint16_t cli_port;   // 客户端ip和端口号int fd = Sock::Accept(_listenSock, cli_ip, cli_port);// 连接获取失败 -- fd < 0 if(fd < 0){logMessage(ERROR, "Aeecpt fail, errno:%d, errMsg:%s\n", errno, strerror(errno));}else  // 连接获取成功{// 将获取到的新连接的fd添加到_fd_array中去int index = 0;for(; index < FD_CAPACITY; ++index){if(_fd_array[index] == NON_FD){_fd_array[index] = fd;break;}if(index == FD_CAPACITY){logMessage(DEBUG, "_fd_array is already full, insert new fd fail, fd:%d\n", fd);}else{logMessage(NORMAL, "Insert new fd success, fd:%d\n", fd);}}}
}// 打印输出_fd_array
void SelectServer::ShowFdArray()
{std::cout << "_fd_array[]: " << std::flush;for(const auto fd : _fd_array){if(fd != NON_FD) std::cout << fd << " ";}std::cout << std::endl;
}

2.3 select实现IO多路转接的优缺点

缺点:

  • 每一次调用select之前,都需要重新设置fd_set对象的值,因为调用select会覆盖掉原来的值。
  • 用户向内核传递关注的文件描述符信息时,需要从用户态转换到内核态,存在较大开销。
  • select返回时,由内核将关注的文件描述符的状态告知用户,需要从内核态转换到用户态,存在较大开销。
  • fd_set对象底层是位图结构,位图中能够记录的文件描述符数量存在限制,不能同时关注太多的文件描述符,能够管理的资源受限。

优点:适用于存在大量fd,但是只要少量处于活跃状态的场景。

三. 通过poll实现IO多路转接

3.1 poll接口

函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout)

头文件:#include <poll.h>

函数参数:

  • fds:struct pollfd类型数组,更确切的应当写为struct pollfd fds[]
  • nfds:所关注的文件描述符数量。
  • timeout:最长阻塞等待时间,以秒为单位,如果传-1表示一直阻塞等待直到有fd就绪。

返回值:如果执行成功返回就绪的文件描述符个数,返回0表示等待超时,执行失败返回-1。

下面是struct pollfd的定义式,其中成员fd表示文件描述符,events表示请求事件,即用户告知内核需要关注哪些文件描述符,revents为响应时间,即内核告知用于哪些文件描述符已经就绪。

events是用户传给内核的信息,revents是内核传给用户的信息,他们互不干扰,因此即使这里的的fds依旧是输入输出型参数,也不需要每次调用poll之前重新设定struct poll对象的值,这是poll相对于select的一大优势。

struct pollfd 
{int   fd;         /* file descriptor */short events;     /* requested events */short revents;    /* returned events */
};

events和revents的值及其对于的含义见表3.2,如果为0表示不关注某个fd或这个fd尚未就绪,如果非0,events用于用户告知OS内核需要关注fd的哪些操作(读/写/异常),revents用户OS内核告知用户fd的哪些状态已经就绪。

表4.3 events/revents的值及对应含义
events/revents含义
POLLIN数据(包括普通数据和高优先级数据)可读。
POLLNORMAL普通数据可读。
POLLPRI高优先级数据可读,如TCP带有紧急指针的报文。
POLLOUT数据(包括普通数据和高优先级数据)可写。
POLLOUTNORMAL普通数据可写。
POLLRDHUP对方关闭TCP,或对端关闭写操作。
POLLERR发生错误。
POLLNVAL文件描述符没有打开。

3.2 Poll服务器的实现

Poll服务器的实现与Select服务器的实现十分类似,代码3.1为Poll服务器类的声明,与Select不同的是,其中有一个struct pollfd* _fd_array成员变量,这个成员变量为struct pollfd类型数组,用于告知内核哪些fd需要关心,哪些fd已经就绪。其余包括构造函数、析构函数、服务器运行函数start、就绪文件描述符处理函数Handler、获取客户端连接函数Accepter、读取数据函数Reciever。

代码3.1:PollServer服务器声明(PollServer.hpp头文件)

#pragma once#include "Sock.hpp"
#include <poll.h>
#include <unistd.h>static const int FD_CAPACITY = 100;
static const int NON_FD = -1;class PollServer
{
public:PollServer(uint16_t port, const std::string& ip = "");void Start();   // 服务器启动函数~PollServer();  // 析构函数private:void Handler();          // 就绪文件描述符处理函数void Reciever(int pos);  // 接收信息函数void Accepter();         // 接收连接请求void ShowFdArray();      // _fd_array打印函数 -- 用于DEBUGint _listenSock;    // 监听套接字uint16_t _port;     // 服务器进程端口号std::string _ip;    // 服务器ipstruct pollfd* _fd_array;   // 文件描述符序列
};

关于Poll服务器的实现,有以下几点需要注意:

  • Poll是基于TCP协议的,在构造函数中,要获取listen套接字、绑定端口号、设置监听状态。
  • 在start函数中,要死循环调用poll,检查是否有就绪的文件描述符,如果有就调用Handler函数来处理就绪文件描述符。在Handler函数中,遍历_fd_array检查就绪的文件描述符,在后续处理中分为listen文件描述符和普通文件描述符处理。
  • Accepter用于接收连接,新获取的文件描述符要添加到_fd_array中去,Reciever用于读取数据,如果检测到对端关闭,要调用close关闭对应fd,并将其在_fd_array中清除。

代码3.2:PollServer的实现(PollServer.cc源文件)

#include "PollServer.hpp"// 构造函数
PollServer::PollServer(uint16_t port, const std::string& ip): _listenSock(-1), _port(port), _ip(ip), _fd_array(new pollfd[FD_CAPACITY])
{// 获取listen套接字_listenSock = Sock::Socket();if(_listenSock < 0) {exit(2);}// 绑定端口号if(Sock::Bind(_listenSock, _ip, _port) < 0) {exit(3);}// 设置监听状态if(Sock::Listen(_listenSock) < 0) {exit(4);}// 初始化pollfd序列for(int i = 0; i < FD_CAPACITY; ++i){_fd_array[i].fd = -1;_fd_array[i].events = _fd_array[i].revents = 0;}// 对listenSock设置读取关心状态_fd_array[0].fd = _listenSock;_fd_array[0].events = POLLIN;
}// 服务器启动函数
void PollServer::Start()
{while(true){ShowFdArray();int n = poll(_fd_array, FD_CAPACITY + 1, -1);switch(n){case 0:     // 尚无就绪的文件描述符logMessage(DEBUG, "No fd has prepared!\n");break;case -1:    // poll失败logMessage(ERROR, "Poll error, errno:%d, errMsg:%s\n", errno, strerror(errno));break;default:    // 有文件描述符就绪Handler();  break;}}
}// 析构函数
PollServer::~PollServer()
{if(_listenSock >= 0) close(_listenSock);delete[] _fd_array;
}// 就绪文件描述符处理函数
void PollServer::Handler()   
{// 遍历查找,有哪一个fd处于就绪状态for(int i = 0; i < FD_CAPACITY; ++i){if(_fd_array[i].fd != NON_FD && _fd_array[i].revents == POLLIN){// 分接收连接请求和读取信息两种情况讨论if(_fd_array[i].fd == _listenSock) Accepter();else Reciever(i);}}
}// 接收信息函数
void PollServer::Reciever(int pos)
{char buffer[1024];ssize_t n = recv(_fd_array[pos].fd, buffer, 1023, 0);// 信息读取成功if(n > 0){buffer[n] = '\0';logMessage(NORMAL, "PollServer recieve message success!\n");printf("Client Message# %s\n", buffer);}else if(n == 0)   // 对端关闭{logMessage(DEBUG, "Client closed, fd:%d", _fd_array[pos].fd);close( _fd_array[pos].fd);_fd_array[pos].fd = NON_FD;_fd_array[pos].events = _fd_array[pos].revents = 0;}else  // 读取失败{logMessage(ERROR, "Get message from Client[%d] success, errno:%d, errMsg:%s\n", _fd_array[pos].fd, errno, strerror(errno));}
}// 接收连接请求
void PollServer::Accepter()
{std::string cli_ip;  uint16_t cli_port;   int fd = Sock::Accept(_listenSock, cli_ip, cli_port);if(fd < 0) {exit(5);}// 将新的fd添加到_fd_array中去int index = 0;for(; index < FD_CAPACITY; ++index){// 检查_fd_array的空缺位置if(_fd_array[index].fd == NON_FD){_fd_array[index].fd = fd;_fd_array[index].events = POLLIN;break;}}if(index == FD_CAPACITY) { logMessage(DEBUG, "The fd_array has already full, insert new fd fail, fd:%d\n", fd);}else {logMessage(NORMAL, "Insert new fd success, _fd_array[%d]:%d\n", index, fd);}
}// _fd_array打印函数 -- 用于DEBUG
void PollServer::ShowFdArray()
{std::cout << "_fd_array[] " << std::flush;for(int i = 0; i < FD_CAPACITY; ++i){if(_fd_array[i].fd != NON_FD)std::cout << _fd_array[i].fd << " ";}std::cout << std::endl;
}

3.3 poll实现IO多路转接的优缺点

优点:

  • 使用struct pollfd替代select使用fd_set进行传参和返回信息,不需要再每次调用poll之前都对输入输出型参数重新进行设置。
  • 相比于select,poll可以管理的文件描述符没有上限。

缺点:

  • 与select相同,poll在返回后需要轮询检测_fd_array来确定哪个文件描述符就绪,消耗较大。
  • 在向poll传参和poll返回时,需要进行 用户态 -> 内核态、内核态 -> 用户态的切换,频繁进行状态切换会消耗资源。
  • 当管理的fd数目较多时,会降低程序的性能。

四. 总结

  • 相比于阻塞式IO,多路转接能够在有其中一个文件描述符就绪的情况下就进行对应的处理,能大幅提高IO的效率。
  • selet和poll是实现多路转接IO的两种方式。
  • select使用fd_set类型来管理文件描述符,缺点是每次调用select都需要重新设置参数,可且管理的文件描述符数量受限,适用于连接多、但处于活跃状态的连接少的场景。
  • poll相比于select不需要每次调用前都设置参数,且可以管理大量的文件描述符,但在处理就绪文件描述符时依然躲不掉遍历操作。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

制作Go程序的Docker容器

今天突然遇到需要将 Go 程序制作成 Docker 的需求&#xff0c;所以进行了一些研究。方法很简单&#xff0c;但是官方文档和教程有些需要注意的地方&#xff0c;所以写本文进行记录。 源程序 首先介绍一下示例程序&#xff0c;示例程序是一个 HTTP 服务器&#xff0c;会显示si…

【Linux从入门到放弃】环境变量

&#x1f9d1;‍&#x1f4bb;作者&#xff1a; 情话0.0 &#x1f4dd;专栏&#xff1a;《Linux从入门到放弃》 &#x1f466;个人简介&#xff1a;一名双非编程菜鸟&#xff0c;在这里分享自己的编程学习笔记&#xff0c;欢迎大家的指正与点赞&#xff0c;谢谢&#xff01; 文…

ESP32网络开发实例-非接触式水位监测

非接触式水位监测 文章目录 非接触式水位监测1、HC-SR04介绍2、软件准备3、硬件准备4、代码实现在本文中,我们将使用 HC-SR04 超声波传感器和 ESP32 创建一个水位监测网络服务器。 这将是一个非接触式水位测量系统。 首先,我们将介绍HC-SR04 与 ESP32 连接。 使用ESP32对超声…

PaddlePaddle:开源深度学习平台

深度学习作为人工智能领域的重要分支&#xff0c;正在全球范围内得到广泛应用。而在构建和训练深度学习模型时&#xff0c;选择一个高效、易用且功能强大的开源平台是至关重要的。PaddlePaddle&#xff08;即飞桨&#xff09;作为国内领先的深度学习平台&#xff0c;一直以来都…

深度学习数据集—细胞、微生物、显微图像数据集大合集

最近收集了一大波关于细胞、微生物、显微图像数据集&#xff0c;有细胞、微生物&#xff0c;细菌等。 接下来是每个数据的详细介绍&#xff01;&#xff01; 1、12500张血细胞增强图像&#xff08;JPEG&#xff09;数据集 该数据集包含12500张血细胞增强图像&#xff08;JPE…

JVM虚拟机:通过日志学习PS+PO垃圾回收器

我们刚才设置参数的时候看到了-XXPrintGCDetails表示输出详细的GC处理日志&#xff0c;那么我们如何理解这个日志呢&#xff1f;日志是有规则的&#xff0c;我们需要按照这个规则来理解日志中的内容&#xff0c;它有两个格式&#xff0c;一个格式是GC的格式&#xff08;新生代&…

统计量及抽样分布

1.常用统计量 &#xff08;1&#xff09;样本均值 反映总体X数学期望的信息&#xff0c;是最常用的统计量。 &#xff08;2&#xff09;样本方差 反映总体X方差的信息。 &#xff08;3&#xff09;样本变异系数 反映总体变异系数C的信息&#xff0c;用来刻画离散程度。 &am…

风光能互补发电庭院路灯系统技术原理

风光互补发电系统是由风力发电机组配合太阳能电池组件组成&#xff0c;通过专用的控制逆变器&#xff0c;将风力发电机输出的低压交流电整流成直流电&#xff0c;并与光伏电池组件输出的直流电汇集在一起&#xff0c;充入蓄电池组&#xff0c;实现稳压、蓄能和逆变全过程&#…

C#单例模式懒汉式与饿汉式

单例模式一般分为懒汉模式和饿汉模式&#xff0c;懒汉式单例在第一次引用时创建实例&#xff0c;不是在类加载时&#xff1b;饿汉式单例模式是一种在类加载时就创建实例的方式&#xff0c;因此也称为静态初始化。 单例模式实现的技巧时构造私有&#xff0c;向外提供静态实例。…

mock测试数据

1.下载一个jar 架包 地址&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1G5rVF5LlIYpyU-_KHsGjOA?pwdab12 提取码&#xff1a;ab12 2.配置当前电脑java环境变量 3.在同一文件目录下创建json 数据4.在终端切换到当前目录下启动服务&#xff0c; java -jar ./moco-r…

C++构造函数和析构函数

构造函数和析构函数的由来 手机有出厂设置&#xff0c;在不用这个手机的时候我们也会清理这个手机的隐私内容。C面向对象的思想来源于生活&#xff0c; 每个对象也应该有初始设置 和 对象销毁前要清理个人隐私数据。 类的构造函数对应于手机的初始化。 类的析构函数对应于手机…