操作系统的五种IO模型

高级IO

阻塞IO

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

非阻塞IO

如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询,不过这对CPU来说浪费较大,一般只有特定的场景下才使用。

信号驱动IO

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

IO多路转接

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

异步IO

由内核在数据拷贝完成时,通知应用程序(信号驱动时告诉应用程序何时可以开始拷贝数据,这里的异步IO是内核已经完成拷贝)。

同步和异步通信

同步和异步关注的是消息通知机制

  • 同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了,即由调用者主动等待这个调用的结果
  • 异步则相反,调用在发出之后,调用直接返回,无返回结果,调用者不会立刻得到结果;而在调用发出后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用

阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
  • 非阻塞调用是指在不能立刻得到结果之前,该调用不会阻塞当前进程

高级IO

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

非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO.

非阻塞IO

设定非阻塞

  1. 让IO非阻塞,打开的时候,就可以指定非阻塞接口
  2. 统一的方式来进行非阻塞设置fcnt(),阻塞和非阻塞都是文件的一种属性

image-20230727143526713

对一个文件描述符进行指定命令式的操作,如果失败返回-1。

轮询方式读取标准输入(非阻塞)

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>// 设置非阻塞接口
bool SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位,放在flif (fl < 0)return false;fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置选项,将fl设置为非阻塞,这个非阻塞属性是新增的return true;
}
int main()
{// 标准输入 0 默认是阻塞的SetNonBlock(0); // 只要设置一次,后续都是非阻塞char buffer[1024];while (true){sleep(1);errno = 0;// 非阻塞的时候,我们是以出错的形式返回,告知上层数据没有就绪// a. 如何甄别是真的出错了// b. 还是数据仅仅没有就绪呢// 数据有的话,正常读取就行ssize_t s = read(0, buffer, sizeof(buffer) - 1); // 出错,不仅仅是错误返回值,errno变量也会被设置,表明出错原因if (s > 0){buffer[s - 1] = 0;std::cout << "echo# " << buffer << " errno[---]: " << errno << " errstring: " << strerror(errno) << std::endl;}else{// 如果失败的errno值是11,就代表其实没错,只不过是底层数据没就绪//  std::cout << "read \"error\" "  << " errno: " << errno << " errstring: " << strerror(errno) << std::endl;if(errno == EWOULDBLOCK || errno == EAGAIN){std::cout << "当前0号fd数据没有就绪, 请下一次再来试试吧" <<std::endl;continue;}else if(errno == EINTR){std::cout << "当前IO可能被中断, 再试一试吧" << std::endl;continue;}else{//进行差错处理}sleep(1);}}return 0;
}

image-20230727150614070

IO多路转接之select

系统提供select函数来实现多路复用输入/输出模型

  • select系统调用是用来让程序监视多个文件描述符的状态变化的
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或者多个发生了状态改变

select函数原型

image-20230727161426488

  1. nfds是需要监视的最大的文件描述符值+1
  2. 帮用户一次等待多个文件sock
  3. 当哪些文件sock就绪了,select就要通知用户,对应就绪的sock有哪些,然后,用户再调用recv/recvfrom/read等进行数据读取
struct timeval *timeout的意义:
  • select等待多个fd,等待策略可以选择三种
    1. 阻塞式 nullptr
    2. 非阻塞式 {0,0}
    3. 可以设置timeout时间,时间内阻塞,时间到立马返回 {5,0}
  • timeout是一个输出型参数,如果等待时间内,有fd就绪,表示距离下一次timeout剩余多长时间。
fd_set 文件描述符集

本质是一个位图结构

image-20230727161920060

readfds为例:

  • 输入时:用户 -> 内核,比特位的位置:文件描述符值,比特位内容:是否关心

    00001010 则表示关心2号比特位和4号比特位的文件描述符的读事件,不关心其他比特位的文件描述符的读事件。

  • 输出时:内核 -> 用户,比特位的位置:文件描述符值,比特位内容:是否就绪

    0000 1000 则表示,后续用户可以直接读取3号,而不会被阻塞。

image-20230727174740044

编写代码注意事项
  1. 关于timeout:用户和内核都会修改同一个位图结构,因此这个参数用一次之后,一定需要进行重新设定。

    struct timeval timeout = {5, 0};
    
  2. 关于nfds:随着获取的sock越来越多,添加到select的sock也会越来越多,此时nfds一定是动态变化,所以要对nfds进行动态计算

  3. rfds/writefds/exceptfds:都是输入输出型参数,输入输出不一定一致,因此每一次要对rfds进行重新添加

  4. 由于2.3点,则:

    • 需要有一个第三方数组,用来保存所有的合法fd
    #define BITS 8
    #define NUM (sizeof(fd_set)*BITS)
    #define FD_NONE -1
    int _fd_array[NUM];//初始化
    for(int i = 0; i < NUM; i++) _fd_array[i] = FD_NONE;
    
    • 在while里面:① 遍历数组,更新出最大值 ② 遍历数组,添加所有需要关心的fd到fd_set位图中 ③ 调用select进行事件检测
    fd_set rfds;
    FD_ZERO(&rfds); // 初始化 清空
    int maxfd = _listensock;
    for (int i = 0; i < NUM; i++)
    {if (_fd_array[i] == FD_NONE)continue;FD_SET(_fd_array[i], &rfds);if (maxfd < _fd_array[i])maxfd = _fd_array[i];
    }
    int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
    
    • 遍历数组,找到就绪的事件,根据就绪的事件,完成对应动作
        void HandlerEvent(const fd_set &rfds){for (int i = 0; i < NUM; i++){if (_fd_array[i] == FD_NONE)continue;if (FD_ISSET(_fd_array[i], &rfds)){// 指定的fd,读事件就绪// 读事件就绪:连接事件到来,acceptif (_fd_array[i] == _listensock) Accepter();                else Recver(i);           }}}
    

server.hpp

#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include <vector>
#include <string>
#include "Log.hpp"
#include "Sock.hpp"#define BITS 8
#define NUM (sizeof(fd_set) * BITS)
#define FD_NONE -1
using namespace std;
// select 只完成读取,写入和异常不做处理 -- epoll(写完整)
class SelectServer
{
public:SelectServer(const uint16_t &port = 8080) : _port(port){_listensock = Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);logMessage(DEBUG, "%s", "create base socket success");for (int i = 0; i < NUM; i++)_fd_array[i] = FD_NONE;// 规定_fd_array[0] = _listensock;_fd_array[0] = _listensock;}void Start(){while (true){// struct timeval timeout = {0, 0};// int sock = Sock::Accept(_listensock); //不能直接调用accept// 加入select中,让select等// FD_SET(_listensock, &rfds);DebugPrint();fd_set rfds;FD_ZERO(&rfds); // 初始化 清空int maxfd = _listensock;for (int i = 0; i < NUM; i++){if (_fd_array[i] == FD_NONE)continue;FD_SET(_fd_array[i], &rfds);if (maxfd < _fd_array[i])maxfd = _fd_array[i];}int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);switch (n){case 0:logMessage(DEBUG, "%s", "time out...");break;case -1:logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));default:// 成功的logMessage(DEBUG, "get a new link event...");HandlerEvent(rfds);break;}}}~SelectServer(){if (_listensock >= 0)close(_listensock);}private:void HandlerEvent(const fd_set &rfds){for (int i = 0; i < NUM; i++){if (_fd_array[i] == FD_NONE)continue;if (FD_ISSET(_fd_array[i], &rfds)){// 指定的fd,读事件就绪// 读事件就绪:连接事件到来,acceptif (_fd_array[i] == _listensock) Accepter();                else Recver(i);           }}}void Accepter(){string clientip;uint16_t clientport = 0;// listensock上面的读事件就绪了,表示可以读取了// 获取新连接int sock = Sock::Accept(_listensock, &clientip, &clientport);if (sock < 0){logMessage(WARNING, "accept error");return;}logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);int pos = 1;for (; pos < NUM; pos++){if (_fd_array[pos] == FD_NONE)break;}if (pos == NUM){logMessage(WARNING, "%s:%d", "select server already full, close: %d", sock);close(sock);}else{_fd_array[pos] = sock;}}void Recver(int pos){// 读事件就绪:INPUT事件到来,recv,readlogMessage(DEBUG, "messsage in, get IO event: %d", _fd_array[pos]);// 先不考虑阻塞char buffer[1024];int n = recv(_fd_array[pos], buffer, sizeof(buffer)-1, 0);if(n > 0){buffer[n] = 0;logMessage(DEBUG, "client[%d]# %s", _fd_array[pos], buffer);}else if(n == 0){logMessage(DEBUG, "cilent[%d] quit, me too...", _fd_array[pos]);// 1. 不让select关心当前的fd了close(_fd_array[pos]);_fd_array[pos] = FD_NONE;}else{logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[pos], errno, strerror(errno));// 1. 不让select关心当前的fd了close(_fd_array[pos]);_fd_array[pos] = FD_NONE;}}void DebugPrint(){cout << "_fd_array[]: ";for (int i = 0; i < NUM; i++){if (_fd_array[i] == FD_NONE)continue;cout << _fd_array[i] << " ";}cout << endl;}private:uint16_t _port;int _listensock;int _fd_array[NUM];
};#endif

socket就绪条件

读就绪
  • socket内核中,接收缓冲区中的字节数,大于等于低水平标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
  • 监听的socket上有新的连接请求
  • socket上有未处理的错误
写就绪
  • socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水平标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
  • socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
  • socket使用非阻塞connect连接成功或失败之后
  • socket上有未读取的错误
select优缺点

通过编写代码可知select的优缺点:

优点:

  • 相比多进程多线程效率高
  • 应用场景:有大量连接,但是只有少量活跃时,省资源

缺点:

  • 为维护第三方数组,select服务器会充满大量的遍历
  • 每一次都要对select输出参数进行重新设定
  • 能够同时管理的fd的个数是有上限的,取决于sizeof(fd_set)的值,字节长度乘以8比特则为支持的最大文件描述数
  • 因为几乎每一个参数都是输入输出的,select一定会频繁的进行用户到内核,内核到用户的参数数据拷贝
  • 编码比较复杂

IO多路转接之poll

poll

在select上的改进:
  • 将输入输出参数进行了分离
  • 解决了select管理上限问题
  • 位图结构变为结构体结构

image-20230728204056316

struct pollfd

image-20230728200027990

//pollServer.hpp#ifndef __POLL_SVR_H__
#define __POLL_SVR_H__#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include <vector>
#include <string>
#include <poll.h>
#include "Log.hpp"
#include "Sock.hpp"#define FD_NONE -1
using namespace std;
// select 只完成读取,写入和异常不做处理 -- epoll(写完整)
class PollServer
{public:static const int nfds = 100;
public:PollServer(const uint16_t &port = 8080) : _port(port), _nfds(nfds){_listensock = Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);logMessage(DEBUG, "%s", "create base socket success");_fds = new struct pollfd[_nfds];for(int i = 0; i < _nfds; i++){_fds[i].fd = FD_NONE;_fds[i].events = _fds[i].revents = 0;       }_fds[0].fd = _listensock;_fds[0].events = POLLIN;_timeout = 1000;}void Start(){while (true){int n = poll(_fds, _nfds, _timeout);switch (n){case 0:logMessage(DEBUG, "%s", "time out...");break;case -1:logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));default:// 成功的HandlerEvent();break;}}}~PollServer(){if (_listensock >= 0)close(_listensock);if (_fds) delete [] _fds;}private:void HandlerEvent(){for (int i = 0; i < _nfds; i++){if (_fds[i].fd == FD_NONE)continue;if (_fds[i].revents & POLLIN){// 指定的fd,读事件就绪// 读事件就绪:连接事件到来,acceptif (_fds[i].fd == _listensock) Accepter();                else Recver(i);           }}}void Accepter(){string clientip;uint16_t clientport = 0;// listensock上面的读事件就绪了,表示可以读取了// 获取新连接int sock = Sock::Accept(_listensock, &clientip, &clientport);if (sock < 0){logMessage(WARNING, "accept error");return;}logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);int pos = 1;for (; pos < _nfds; pos++){if (_fds[pos].fd == FD_NONE)break;}if (pos == _nfds){// 对struct pollfd进行自动扩容logMessage(WARNING, "%s:%d", "select server already full, close: %d", sock);close(sock);}else{_fds[pos].fd = sock;_fds[pos].events = POLLIN;}}void Recver(int pos){// 读事件就绪:INPUT事件到来,recv,readlogMessage(DEBUG, "messsage in, get IO event: %d", _fds[pos]);// 先不考虑阻塞char buffer[1024];int n = recv(_fds[pos].fd, buffer, sizeof(buffer)-1, 0);if(n > 0){buffer[n] = 0;logMessage(DEBUG, "client[%d]# %s", _fds[pos].fd, buffer);}else if(n == 0){logMessage(DEBUG, "cilent[%d] quit, me too...", _fds[pos].fd);// 1. 不让select关心当前的fd了close(_fds[pos].fd);_fds[pos].fd = FD_NONE;_fds[pos].events = 0;}else{logMessage(WARNING, "%d sock recv error, %d : %s", _fds[pos].fd, errno, strerror(errno));// 1. 不让select关心当前的fd了close(_fds[pos].fd);_fds[pos].fd = FD_NONE;_fds[pos].events = 0;}}void DebugPrint(){cout << "_fd_array[]: ";for (int i = 0; i < _nfds; i++){if (_fds[i].fd == FD_NONE)continue;cout << _fds[i].fd << " ";}cout << endl;}private:uint16_t _port;int _listensock;struct pollfd *_fds;int _nfds;int _timeout;
};#endif
poll的优缺点

优点:

  • 效率高
  • 有大量的连接,但是只有少量的是活跃的,节省资源
  • pollfd结构包含了要监视的event和发生的event,不再使用select”参数-值“传递的方式,接口使用比select更方便
  • 输入输出参数分离,不需要大量的重置
  • poll参数级别,对于可管理的fd的数量无上限

缺点:

  • poll依旧需要不少的遍历,在用户层检测时间就绪,与内核检测fd就绪,都是一样
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中
  • poll代码编写也比较复杂(比select容易 )

IO多路转接之epoll

1 相关接口

1.1 epoll_create

创建一个epoll句柄,用完之后必须调用close()关闭

image-20230729130039552

1.2 epoll_ctl 事件注册函数

对创建的epoll模型进行相关操作

不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是先在这里注册要监听的事件类型

image-20230729125950261

op有三个取值:

  • EPOLL_CTL_ADD 增新的fd
  • EPOLL_CTL_DEL 删fd
  • EPOLL_CTL_MOD 修改已经注册的fd的监听事件

struct epoll_event的结构

image-20230805122323158

其中events可以是以下几个宏的集合:

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

在特定的epoll当中获取已经就绪的事件,

image-20230729135646570

关于epoll_wait的返回值:

设epoll_wait的返回值为n,每一次返回的n为就绪的事件的数量,并且存储在*events中,从0下标开始,按顺序存储。因此每次读 0 ~ (n-1) 即可读完就绪的事件,不会造成其他的浪费。

2 epoll优点

  • 接口使用方便,虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离

  • 数据拷贝轻量,只在合适的时候调用EPOLL_CTL_ADD将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll)是每次循环都有进行拷贝

  • 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪,这个操作时间复杂度O(1),即时文件描述符数目很多,效率也不会受到影响

  • 可监听文件描述符数目无上限

3 工作原理

OS如何知道,网卡里面有数据,或者键盘有用户输入?以从网卡读入为例,下面是模拟示意图

image-20230729143100747

用户告诉内核需要维护的fd已经相关事件,内核会维护一个红黑树用来存储。同时,会创造一个就绪队列。

在系统中,每一个epoll模型对应的都是一个eventpoll结构。

image-20230805102253269

注:

  1. 此处的红黑树的key值是描述符
  2. 用户只需要设置关系,获取结果即可,不用关心任何对fd与event的管理细节
  3. 底层要有fd就绪,OS会自动建立节点,连入到就绪队列中,上层只需要不断地从就绪队列中将数据拿走,就完成了获取就绪事件的任务(生产者与消费者模型)。
  4. 就绪队列的本质是一个共享资源,epoll已经保证所有的epoll接口都是线程安全的。

4 epoll服务器的封装

由于epoll中关心的文件描述符都必须是合法的文件描述符,因此当客户端断开连接时,首先应该在epoll中移除对该sock文件描述符的关心,再close该文件描述符,否则会出错。

为了保证每一回合的正确读取,每一个socket都要有自己的缓冲区。

5 epoll工作方式

epoll有两种工作方式,水平触发(LT)和边缘触发(ET)。select,poll,epoll的默认模式都是LT模式。

1. LT(Level Triggered,水平触发)

工作模式 在LT模式下,当epoll_wait()检测到描述符上有事件发生时,会重复通知应用程序,直到应用程序处理完所有的事件并将相应的文件描述符设置为非阻塞状态后,epoll_wait()才会返回。这意味着,如果有一个文件描述符上有多个事件发生,但应用程序没有一次性处理完所有的事件,那么epoll_wait()将继续通知应用程序该文件描述符上尚未处理的事件。

LT模式适用于处理普通的I/O事件,即不需要立即响应的事件。使用LT模式,应用程序可以在任何时候处理事件,而不必担心错过任何事件。

2. ET(Edge Triggered,边缘触发)工作模式

在ET模式下,当epoll_wait()检测到描述符上有事件发生时,只会通知应用程序一次,直到应用程序处理完该描述符上所有待处理事件之后,才会再次通知应用程序有新的事件发生。在ET模式下,应用程序需要立即响应事件,否则将会错过事件。

ET模式适用于处理高速、高流量的I/O事件,即需要尽可能快地响应事件。使用ET模式,应用程序可以尽可能地多次处理事件,从而提高效率。在ET模式下,应用程序需要对每个文件描述符上的所有事件进行处理,否则将会错过事件。

ET模式更高效:

  • 更少的返回次数
  • ET模式会倒逼程序员尽快将接收缓冲区中的数据全部取走,应用层尽快的取走了缓冲区的数据,那么在单位时间,该模式下的工作的服务器,就可以在一定程度上,给发送发送一个更大的接收窗口,所以发送方就可以有更大的滑动窗口,一次向接收方发送更多的数据,提高IO吞吐。

select、poll、epoll优缺点总结

select

优点:

  1. 跨平台支持:select是传统的I/O多路复用机制,几乎在所有操作系统上都有支持。
  2. 简单易用:使用select相对简单,只需使用一个文件描述符集合进行监听,并由内核通知就绪的文件描述符。
  3. 支持小规模连接:适用于连接数较少的情况,例如几十到几百个。

缺点:

  1. 文件描述符限制:select使用的文件描述符集合是一个位图,其大小被限制为FD_SETSIZE,默认一般较小。
  2. 效率低下:每次调用select,都需要将所有待监听的文件描述符从用户空间拷贝到内核空间,造成性能损耗。
  3. 遍历开销大:当就绪事件较少时,select需要遍历整个文件描述符集合,效率较低。

poll

优点:

  1. 跨平台支持:与select一样,poll也是具有跨平台特性的多路复用机制。
  2. 无文件描述符限制:poll没有select的文件描述符数量限制。
  3. 简单易用:使用一个pollfd数组进行监听,通过判断revents字段来确定就绪事件。

缺点:

  1. 效率问题:poll仍需要将所有待监听的文件描述符从用户空间拷贝到内核空间,造成性能损耗。
  2. 遍历开销大:当就绪事件较少时,poll需要遍历整个pollfd数组,效率较低。

epoll

优点:

  1. 高性能:epoll利用了内核的事件通知机制,只返回就绪的文件描述符,避免了遍历整个文件描述符集合的开销。
  2. 处理大规模连接:适用于连接数较大的情况,例如成千上万个连接。
  3. 零拷贝:epoll支持操作系统的零拷贝技术,减少了数据拷贝的次数,提高了性能。

缺点:

  1. Linux特定:epoll是Linux下的特有机制,不具备跨平台能力。
  2. 编程复杂:相对于select和poll,使用epoll需要更加复杂的编程方式。
  3. 内存占用:epoll需要维护一个用于存储事件的数据结构(epoll_event数组),占用一定的内存空间。

综上所述,epoll相较于select和poll,在性能和扩展性方面具有明显优势,但在跨平台和编程复杂性方面存在一些限制。因此,在选择使用哪种多路复用机制时,需要根据具体的应用场景和需求进行权衡和选择。

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

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

相关文章

TCP四次挥手

四次挥手发生在断开连接的时候&#xff0c;在程序中当调用了close()会使用TCP协议进行四次挥手。 客户端和服务器端都可以主动发起断开连接&#xff0c;谁先调用close()谁就是发起。 因为在TCP连接的时候&#xff0c;采用三次握手建立的的连接是双向的&#xff0c;在断开的时候…

电脑被删除的文件怎么恢复?2023年数据恢复方法分享

大多数人在使用电脑时都可能会遇到误删文件的情况。一不小心&#xff0c;重要的文件或数据就消失了&#xff0c;情急之下&#xff0c;大多会感到慌乱和无助。但其实&#xff0c;文件误删除并非不可挽回的灾难。本文将为大家介绍几种有效的文件恢复方法&#xff0c;以帮助大家在…

【力扣】42. 接雨水

这道题我卡了差不多1个小时&#xff0c;不是不会做&#xff0c;是不知道怎么能用栈来实现&#xff0c;后面看了一个博主的视频&#xff0c;豁然开朗&#xff0c;我主要的纠结点在于当指针指到7的时候&#xff0c;我计算出4到7的水块是2&#xff0c;但实际上是0&#xff0c;因为…

SpringBoot 可以同时处理多少请求

目录 一、前言 二、相关配置 1、配置信息 2、配置说明 3、案例说明 三、代码验证 1、测试代码 2、测试步骤 四、总结 一、前言 首先&#xff0c;在Spring Boot应用中&#xff0c;我们可以使用 Tomcat、Jetty、Undertow 等嵌入式 Web 服务器作为应用程序的运行容器。这…

Vue中如何进行拖拽与排序功能实现

在Vue中实现拖拽与排序功能 在Web应用程序中&#xff0c;实现拖拽和排序功能是非常常见的需求&#xff0c;特别是在管理界面、任务列表和图形用户界面等方面。Vue.js作为一个流行的JavaScript框架&#xff0c;提供了许多工具和库来简化拖拽和排序功能的实现。本文将介绍如何使…

抖捧自动直播市场火热,实体行业如何实现高效开播?

在AI数字人热度之后&#xff0c;最近抖捧场景自动直播开始火遍全网&#xff0c;因为操作简单成本极低&#xff0c;只需要一部手机放在店里&#xff0c;就可以高效开播&#xff0c;深受广大实体行业老板的认可&#xff0c;那么抖捧实景自动直播的方式&#xff0c;具体是怎么实现…

P1014 [NOIP1999 普及组] Cantor 表

#include <bits/stdc.h> using namespace std; int main() {int n,k1;cin>>n;while (n>k) {nn-k;k;}if(k%20) cout<<n<<"/"<<(k1-n);else cout<<k1-n<<"/"<<n;return 0; }

IDEA添加Vue文件模板

代码模板&#xff1a; <!-- *${COMPONENT_NAME} *author niemengshi *date ${DATE} ${TIME} --> <template> #[[$END$]]# </template> <script> export default { name: "${COMPONENT_NAME}", props: { }, components: {}, mounted: {}, d…

数据压缩与管理:掌握Linux VDO和LVM的力量

1.逻辑卷(LVM&#xff0c;Logical Volume Management) 动态的为服务器磁盘添加空间&#xff0c;而不会影响原磁盘的数据&#xff0c;也不需要对原始磁盘重新分区。 1.1 LVM介绍 以下是LVM的示意图&#xff1a; 我们拿到一块硬盘后首先对齐进行划分分区&#xff0c;也就得到…

CocosCreator3.8研究笔记(二十三)CocosCreator 动画系统-动画编辑器相关功能面板说明

国庆假期&#xff0c;闲着没事&#xff0c;在家研究技术~ 上一篇&#xff0c;我们介绍了动画剪辑、动画组件以及基本的使用流程&#xff0c;感兴趣的朋友可以前往阅读&#xff1a; CocosCreator 动画系统-动画剪辑和动画组件介绍。 今天&#xff0c;主要介绍动画编辑器相关功能…

FastThreadLocal 快在哪里 ?

FastThreadLocal 快在哪里 &#xff1f; 引言FastThreadLocalset如何获取当前线程私有的InternalThreadLocalMap &#xff1f;如何知道当前线程使用到了哪些FastThreadLocal实例 ? get垃圾回收 小结 引言 FastThreadLocal 是 Netty 中造的一个轮子&#xff0c;那么为什么放着…

一篇理解网络分层原理

一、网络分层的必要性。 如图是一个数据的传输过程&#xff0c;在这个途中会有很多的原因导致数据丢失&#xff0c;网络分层就要可以很大程度的避免这个现象。 网络分层的必要性体现在以下几个方面&#xff1a; 抽象复杂度&#xff1a;网络分层将网络功能按照不同的层次进行分…

SQL进阶 - SQL的编程规范

性能优化是一个很有趣的探索方向&#xff0c;将耗时耗资源的查询优化下来也是一件很有成就感的事情&#xff0c;但既然编程是一种沟通手段&#xff0c;那每一个数据开发者就都有义务保证写出的代码逻辑清晰&#xff0c;具有很好的可读性。 目录 引子 小试牛刀 答案 引言 …

IDEA 2021.2.2设置自动热部署

1.导入包坐标 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency> 2.pom.xml添加piugins插…

车载网络诊断应如何测试?

文章目录 一、前言二、测试内容三、测试设备和台架方案四、测试脚本及工程五、其他一、前言 目前车上主流的网络有CAN、LIN、ETH(以太网)。 按照测试环境可以划分为单件测试,系统测试,整车测试。 我们来看下CAN和以太网的分层图: CAN的分层图: 以太网的分层图: 最好的…

【C++】STL详解(十二)—— 用哈希表封装出unordered_map和unordered_set

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;C学习 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【C】STL…

基于Dockerfile搭建LNMP

目录 一、基础环境准备 1、环境前期准备 二、部署nginx&#xff08;容器IP 为 172.18.0.10&#xff09; 1、配置Dockerfile文件 2、配置nginx.conf文件 3、构建镜像、启动镜像 三、部署mysql 1、配置Dockerfile文件 2、配置my.conf文件 3、构建镜像、启动镜像 5、验…

经典算法----迷宫问题(找出所有路径)

目录 前言 问题描述 算法思路 定义方向 回溯算法 代码实现 前言 前面我发布了一篇关于迷宫问题的解决方法&#xff0c;是通过栈的方式来解决这个问题的&#xff08;链接&#xff1a;经典算法-----迷宫问题&#xff08;栈的应用&#xff09;-CSDN博客&#xff09;&#xff…

One Thread One Loop主从Reactor模型⾼并发服务器

One Thread One Loop主从Reactor模型⾼并发服务器 文章目录 One Thread One Loop主从Reactor模型⾼并发服务器一些补充HTTP服务器Reactor 模型eventfd通用类Any 目标功能模块划分&#xff1a;SERVER模块Buffer模块&#xff1a;编写思路&#xff1a;接口设计&#xff1a;具体实现…