muduo网络库定时器的实现

2023-11-12


一:函数介绍

常见的与时间相关的函数有:sleep,alarm,usleep,nanosleep,clock_nanosleep,gettimer/settitimer,timer_create/timer_settime/timer_gettime/timer_delete,还有muduo使用的timerfd_create/timerfd_gettime/timerfd_settime函数。

为什么选择timerfd_*函数呢?

>>>sleep/alarm/usleep在实现时有可能用了信号SIGALRM,在多线程程序中处理信号是个相当麻烦的事情,应该尽量避免。

>>>nanosleep和clock_nanosleep是线程安全的,但是在非阻塞网络编程中,绝对不能用让线程挂起的方式来等待一段时间。程序会失去响应,正确的做法是注册一个时间回调函数。

>>>getitimer和timer_create也是用信号来deliver超时,在多线程程序中也会有麻烦。

>>>timer_create可以指定信号的接收方是进程还是线程,算是一个进步,不过在信号处理函数(signal handler)中能做的事情实在是很受限。

>>>timerfd_create把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,这样就能很方便的融入到select/poll框架中,用统一的方式来处理I/O和超时事件,这正是Reactor模式的长处。实际上如果非要处理信号的话,使用signalfd将信号转变成文件描述符处理。

比较:libevent定时器采用本地unix域通信,当时间最小堆中超时事件发生后,写一个字节给本地socket,使得描述符可读,epoll检测到并处理该事件。


首先看timerfd_create()函数:

 int timerfd_create(int clockid, int flags);
1.参数1的clockid是指,CLOCK_REALTIME or CLOCK_MONOTONIC时间。前者是我们平时所看到的时间,后者是石英钟时间也就是晶振决定的时间。所以假如我们制订了3分钟的定时,如果我们直接修改时间到三分钟后,那么前者会触发,而后者不会触发,它是晶振决定的相对时间,不取决于外部更改。所以一般定时器采用后者

2.参数2有两个选项TFD_NONBLOCK和TFD_CLOEXEC,这个就不用解释了。


第二个函数是timerfd_settime()函数

  int timerfd_settime(int fd, int flags,
                           const struct itimerspec *new_value,
                           struct itimerspec *old_value);
1.参数1是文件描述符。

2.参数2是参加timerfd_create()函数。

3.参数3是新值,itermerspec结构体有两个字段,解释见下面。

4.参数4是旧值,结构体见下面。

           struct timespec {
               time_t tv_sec;                /* Seconds */
               long   tv_nsec;               /* Nanoseconds */
           };


           struct itimerspec {
               struct timespec it_interval;  /* Interval for periodic timer */   //定时间隔,如果只发生一次,可设置为0
               struct timespec it_value;     /* Initial expiration */  //最初的过期值
           };


       new_value.it_value  specifies  the  initial  expiration  of the timer, in seconds and nanoseconds.  Setting either field of

       new_value.it_value to a nonzero value arms the timer.  Setting both fields of new_value.it_value to zero disarms the timer.


二:定时器

为什么网络编程中需要定时器呢?

在开发Linux网络程序时,通常需要维护多个定时器,如维护客户端心跳时间、检查多个数据包的超时重传等。如果采用Linux的SIGALARM信号实现,则会带来较大的系统开销,且不便于管理。

Muduo 的 TimerQueue 采用了最简单的实现(链表)来管理定时器,它的效率比不上常见的 binary heap 的做法,如果程序中大量(10 个以上)使用重复触发的定时器,或许值得考虑改用更高级的实现。由于目前还没有在一个程序里用过这么多定时器,暂时也不需要优化 TimerQueue。

mudo的定时器由三个类实现,TimerId,Timer,TimerQueue,用户只能看到第一个类,其它两个类都是内部实现细节。

(1)TimerId被设计用来取消Timer的,它的结构很简单,只有一个Timer指针和其序列号。其中还声明了TimerQueue为其友元,可以操作其私有数据。

它的实现如下:

class TimerId : public muduo::copyable
{
 public:
  TimerId()
    : timer_(NULL),
      sequence_(0)
  {
  }
  TimerId(Timer* timer, int64_t seq)
    : timer_(timer),
      sequence_(seq)
  {
  }
  // default copy-ctor, dtor and assignment are okay
  friend class TimerQueue;

 private:
  Timer* timer_;
  int64_t sequence_;
};
(2)Timer类

Timer是对定时器的高层次抽象,封装了定时器的一些参数,例如超时回调函数、超时时间、超时时间间隔、定时器是否重复、定时器的序列号。其函数大都是设置这些参数,run()用来调用回调函数,restart()用来重启定时器(如果设置为重复)。

主要实现:

class Timer : boost::noncopyable
{
 public:
  Timer(const TimerCallback& cb, Timestamp when, double interval)
    : callback_(cb),
      expiration_(when),
      interval_(interval),
      repeat_(interval > 0.0),   //如果大于0就重复
      sequence_(s_numCreated_.incrementAndGet())  //先加后获取,由于初始值s_numCreated为0,所以序号这里从1开始
  { }

#ifdef __GXX_EXPERIMENTAL_CXX0X__
  Timer(TimerCallback&& cb, Timestamp when, double interval)
    : callback_(std::move(cb)),
      expiration_(when),
      interval_(interval),
      repeat_(interval > 0.0),
      sequence_(s_numCreated_.incrementAndGet())
  { }
#endif

  void run() const  //调用回调函数
  {
    callback_();
  }

  Timestamp expiration() const  { return expiration_; }
  bool repeat() const { return repeat_; }
  int64_t sequence() const { return sequence_; }

  void restart(Timestamp now);

  static int64_t numCreated() { return s_numCreated_.get(); }

 private:
  const TimerCallback callback_;  //定时器回调函数,TimerCallback在callback.h文件中定义,类型void()
  Timestamp expiration_;   //下一次的超时时刻
  const double interval_;     //超时时间间隔,如果是一次定时器,该值为0
  const bool repeat_;          //是否重复
  const int64_t sequence_;     //定时器序号

  static AtomicInt64 s_numCreated_;   //定时器计数,当前已创建的定时器数量,原子int64_t类型,初始值为0
};
.cpp:

#include <muduo/net/Timer.h>

using namespace muduo;
using namespace muduo::net;

AtomicInt64 Timer::s_numCreated_;

void Timer::restart(Timestamp now)   //重启
{
  if (repeat_)   //如果是重复的,那么就从当前时间计算下一次的超时时刻
  {
    expiration_ = addTime(now, interval_);  //当前时间加上时间间隔?
  }
  else
  {
    expiration_ = Timestamp::invalid();   //获取一个无效事件戳,即值为0
  }
}

实际上最重要的是接下来要介绍的这个类。

(3)TimerQueue数据结构的选择 

TimerQueue的接口很简单,只有两个函数addTimer()和cancel()。它的内部有channel,和timerfd相关联。添加新的Timer后,在超时后,timerfd可读,会处理channel事件,之后调用Timer的回调函数;在timerfd的事件处理后,还有检查一遍超时定时器,如果其属性为重复还有再次添加到定时器集合中。

时序图:


TimerQueue需要高效地组织目前尚未到期的Timer,能快速地根据当前时间找到已经到期的Timer,也要能高效地添加和删除Timer。因而可以用二叉搜索树(例如std::set/std::map),把Timer按到期时间先后排好序,其操作的复杂度是O(logN),但我们使用时还要处理两个Timer到期时间相同的情况(map不支持key相同的情况),做法如下:
//两种类型的set,一种按时间戳排序,一种按Timer的地址排序
//实际上,这两个set保存的是相同的定时器列表

typedef std::pair<Timestamp, Timer*> Entry;
typedef std::set<Entry> TimerList;

typedef std::pair<Timer*, int64_t> ActiveTimer;
typedef std::set<ActiveTimer> ActiveTimerSet;

看它的头文件:

class TimerQueue : boost::noncopyable
{
 public:
  TimerQueue(EventLoop* loop);
  ~TimerQueue();

  ///
  /// Schedules the callback to be run at given time,
  /// repeats if @c interval > 0.0.
  ///
  /// Must be thread safe. Usually be called from other threads.
  //一定是线程安全的,可以跨线程调用。通常情况下被其他线程调用
  TimerId addTimer(const TimerCallback& cb,
                   Timestamp when,
                   double interval);
#ifdef __GXX_EXPERIMENTAL_CXX0X__
  TimerId addTimer(TimerCallback&& cb,
                   Timestamp when,
                   double interval);
#endif

  void cancel(TimerId timerId);  //可以跨线程调用

 private:

  // FIXME: use unique_ptr<Timer> instead of raw pointers.
  //下面两个set可以说保存的是相同的东西,都是定时器,只不过排序方式不同
  typedef std::pair<Timestamp, Timer*> Entry;  //set的key,是一个时间戳和定时器地址的pair
  typedef std::set<Entry> TimerList;    //按照时间戳排序
  typedef std::pair<Timer*, int64_t> ActiveTimer;  //定时器地址和序号
  typedef std::set<ActiveTimer> ActiveTimerSet;  //按照定时器地址排序

  //以下成员函数只可能在其所属的I/O线程中调用,因而不必加锁
  //服务器性能杀手之一就是锁竞争,要尽可能少使用锁
  void addTimerInLoop(Timer* timer);
  void cancelInLoop(TimerId timerId);
  // called when timerfd alarms
  void handleRead();  //定时器事件产生回调函数
  // move out all expired timers
  std::vector<Entry> getExpired(Timestamp now);  //返回超时的定时器列表
  void reset(const std::vector<Entry>& expired, Timestamp now);   //对超时的定时器进行重置,因为超时的定时器可能是重复的定时器

  bool insert(Timer* timer);  //插入定时器

  EventLoop* loop_;    //所属的event_loop
  const int timerfd_;    //就是timefd_create()所创建的定时器描述符?
  Channel timerfdChannel_;   //这是定时器事件的通道
  // Timer list sorted by expiration
  TimerList timers_;   //定时器set,按时间戳排序

  // for cancel()
  ActiveTimerSet activeTimers_;  //活跃定时器列表,按定时器地址排序
  bool callingExpiredTimers_; /* atomic */  //是否处于调用处理超时定时器当中
  ActiveTimerSet cancelingTimers_;    //保存的是被取消的定时器
};
实现文件:

#ifndef __STDC_LIMIT_MACROS
#define __STDC_LIMIT_MACROS
#endif

#include <muduo/net/TimerQueue.h>

#include <muduo/base/Logging.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/Timer.h>
#include <muduo/net/TimerId.h>

#include <boost/bind.hpp>

#include <sys/timerfd.h>

namespace muduo
{
namespace net
{
namespace detail
{

int createTimerfd()
{
  int timerfd = ::timerfd_create(CLOCK_MONOTONIC,
                                 TFD_NONBLOCK | TFD_CLOEXEC);
  if (timerfd < 0)
  {
    LOG_SYSFATAL << "Failed in timerfd_create";
  }
  return timerfd;
}

//计算超时时刻与当前时间的时间差
struct timespec howMuchTimeFromNow(Timestamp when)
{
  int64_t microseconds = when.microSecondsSinceEpoch()
                         - Timestamp::now().microSecondsSinceEpoch();  //超时时刻微秒数-当前时间微秒数
  if (microseconds < 100)  //不能小于100,精确度不需要
  {
    microseconds = 100;
  }
  struct timespec ts;  //转换成这个结构体返回
  ts.tv_sec = static_cast<time_t>(
      microseconds / Timestamp::kMicroSecondsPerSecond);
  ts.tv_nsec = static_cast<long>(
      (microseconds % Timestamp::kMicroSecondsPerSecond) * 1000);
  return ts;
}

//从timerfd读取,避免定时器事件一直触发
void readTimerfd(int timerfd, Timestamp now)
{
  uint64_t howmany;
  ssize_t n = ::read(timerfd, &howmany, sizeof howmany);  //从timerfd读取4个字节,这样timerfd就不会一直触发了  
  LOG_TRACE << "TimerQueue::handleRead() " << howmany << " at " << now.toString();
  if (n != sizeof howmany)
  {
    LOG_ERROR << "TimerQueue::handleRead() reads " << n << " bytes instead of 8";
  }
}

//重置定时器超时时刻
void resetTimerfd(int timerfd, Timestamp expiration)  
{
  // wake up loop by timerfd_settime()
  struct itimerspec newValue;   
  struct itimerspec oldValue;
  bzero(&newValue, sizeof newValue);
  bzero(&oldValue, sizeof oldValue);
  newValue.it_value = howMuchTimeFromNow(expiration);   //将时间戳类转换成it_value的形式
  int ret = ::timerfd_settime(timerfd, 0, &newValue, &oldValue);  //设置进去,到期之后会产生一个定时器事件
  if (ret)
  {
    LOG_SYSERR << "timerfd_settime()";
  }
}

}
}
}

using namespace muduo;
using namespace muduo::net;
using namespace muduo::net::detail;

TimerQueue::TimerQueue(EventLoop* loop)
  : loop_(loop),
    timerfd_(createTimerfd()),    //创建定时器,调用timerfd_create,返回timerfd
    timerfdChannel_(loop, timerfd_),
    timers_(),
    callingExpiredTimers_(false)
{
/*Channel timerfdChannel_;   //这是定时器事件的通道*/
	//设置定时器类型通道的读回调函数。
  timerfdChannel_.setReadCallback(
      boost::bind(&TimerQueue::handleRead, this));   
  // we are always reading the timerfd, we disarm it with timerfd_settime.
  timerfdChannel_.enableReading();  //注册,底层是一系列update,你懂的。
}

TimerQueue::~TimerQueue()
{
  timerfdChannel_.disableAll();
  timerfdChannel_.remove();
  ::close(timerfd_);
  // do not remove channel, since we're in EventLoop::dtor();
  for (TimerList::iterator it = timers_.begin();
      it != timers_.end(); ++it)
  {
    delete it->second;  //析构函数只释放一次,因为两个set保存的是一样的
  }
}

//增加一个定时器
TimerId TimerQueue::addTimer(const TimerCallback& cb,
                             Timestamp when,
                             double interval)
{
  Timer* timer = new Timer(cb, when, interval);   //构造一个定时器对象,interval>0就是重复定时器
  loop_->runInLoop(
      boost::bind(&TimerQueue::addTimerInLoop, this, timer));
  return TimerId(timer, timer->sequence());
}

#ifdef __GXX_EXPERIMENTAL_CXX0X__
TimerId TimerQueue::addTimer(TimerCallback&& cb,
                             Timestamp when,
                             double interval)
{
  Timer* timer = new Timer(std::move(cb), when, interval);
  loop_->runInLoop(
      boost::bind(&TimerQueue::addTimerInLoop, this, timer));
  return TimerId(timer, timer->sequence());
}
#endif

//执行线程退出的回调函数
void TimerQueue::cancel(TimerId timerId)
{
  loop_->runInLoop(
      boost::bind(&TimerQueue::cancelInLoop, this, timerId)); 
}

void TimerQueue::addTimerInLoop(Timer* timer)
{
  loop_->assertInLoopThread();
  //插入一个定时器游客能会使得最早到期的定时器发生改变,比如当前插入一个最早到期的,那就要重置定时器超时时刻
  bool earliestChanged = insert(timer);

  if (earliestChanged)  //如果改变了
  {
    resetTimerfd(timerfd_, timer->expiration());  //重置定时器超时时刻
  }
}

void TimerQueue::cancelInLoop(TimerId timerId)
{
  loop_->assertInLoopThread();
  assert(timers_.size() == activeTimers_.size());
  ActiveTimer timer(timerId.timer_, timerId.sequence_);
  //查找该定时器
  ActiveTimerSet::iterator it = activeTimers_.find(timer);
  if (it != activeTimers_.end())
  {
  	//删除该定时器
    size_t n = timers_.erase(Entry(it->first->expiration(), it->first));
    assert(n == 1); (void)n;
    //如果用unique_ptr这里就不需要手工删除了
    delete it->first; // FIXME: no delete please
    activeTimers_.erase(it);
  }
  else if (callingExpiredTimers_)   //如果在定时器列表中没有找到,可能已经到期,且正在处理的定时器
  {
    //已经到期,并且正在调用回调函数的定时器
    cancelingTimers_.insert(timer);
  }
  assert(timers_.size() == activeTimers_.size());
}

//可读事件处理
void TimerQueue::handleRead()
{
  loop_->assertInLoopThread();  //断言I/O线程中调用
  Timestamp now(Timestamp::now());
  readTimerfd(timerfd_, now);   //清除该事件,避免一直触发,实际上是对timerfd做了read

  //获取该时刻之前所有的定时器列表,即超时定时器列表,因为实际上可能有多个定时器超时,存在定时器的时间设定是一样的这种情况
  std::vector<Entry> expired = getExpired(now);

  callingExpiredTimers_ = true;  //处于处理定时器状态中
  cancelingTimers_.clear();  
  // safe to callback outside critical section
  for (std::vector<Entry>::iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    it->second->run();   //调用所有的run()函数,底层调用Timer类的设置了的超时回调函数
  }
  callingExpiredTimers_ = false;

  reset(expired, now);   //如果移除的不是一次性定时器,那么重新启动它们
}

//返回当前所有超时的定时器列表
//返回值由于rvo优化,不会拷贝构造vector,直接返回它
std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{
  assert(timers_.size() == activeTimers_.size());
  std::vector<Entry> expired;
  Entry sentry(now, reinterpret_cast<Timer*>(UINTPTR_MAX));//创建一个时间戳和定时器地址的集合
  //返回第一个未到期的Timer的迭代器
  //lower_bound返回第一个值>=sentry的
  //即*end>=sentry,从而end->first > now,而不是>=now,因为pair比较的第一个相等后会比较第二个,而sentry的第二个是UINTPTR_MAX最大
  //所以用lower_bound没有用upper_bound
  TimerList::iterator end = timers_.lower_bound(sentry);
  assert(end == timers_.end() || now < end->first);  //now < end->first
  std::copy(timers_.begin(), end, back_inserter(expired));  //将到期的定时器插入到expired中
  timers_.erase(timers_.begin(), end);  //删除已到期的所有定时器

  //从activeTimers_中也要移除到期的定时器
  for (std::vector<Entry>::iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    ActiveTimer timer(it->second, it->second->sequence());
    size_t n = activeTimers_.erase(timer);
    assert(n == 1); (void)n;
  }

  assert(timers_.size() == activeTimers_.size());
  return expired;
}

void TimerQueue::reset(const std::vector<Entry>& expired, Timestamp now)
{
  Timestamp nextExpire;

  for (std::vector<Entry>::const_iterator it = expired.begin();
      it != expired.end(); ++it)
  {
    ActiveTimer timer(it->second, it->second->sequence());
    //如果是重复的定时器,并且是未取消定时器,则重启该定时器
    if (it->second->repeat()
        && cancelingTimers_.find(timer) == cancelingTimers_.end())
    {
      it->second->restart(now);  //restart()函数中会重新计算下一个超时时刻
      insert(it->second);
    }
    else
    {
    	//一次性定时器或者已被取消的定时器是不能重置的,因此删除该定时器
      // FIXME move to a free list
      delete it->second; // FIXME: no delete please  
    }
  }

  if (!timers_.empty()) 
  {
    //获取最早到期的超时时间
    nextExpire = timers_.begin()->second->expiration();
  }

  if (nextExpire.valid())
  {
  	//重新设定timerfd的超时时间
    resetTimerfd(timerfd_, nextExpire);
  }
}

bool TimerQueue::insert(Timer* timer)
{
  loop_->assertInLoopThread();
  assert(timers_.size() == activeTimers_.size());  //这两个存的是同样的定时器列表,成员函数中分析过了
  bool earliestChanged = false;
  //检测最早到期时间是否改变
  Timestamp when = timer->expiration();
  TimerList::iterator it = timers_.begin();   //第一个定时器,timers是set实现的,所以第一个就是最早,空的也算
  if (it == timers_.end() || when < it->first)
  {
    earliestChanged = true;  //如果插入定时器时间小于最早到期时间
  }
  //下面两个插入的set保存的是一样的,都是定时器,只不过对组的另一个辅助成员不一样
  {
    //利用RAII机制
    //插入到timers_中,result是临时对象,需要用它来保证插入成功
    std::pair<TimerList::iterator, bool> result
      = timers_.insert(Entry(when, timer));
    assert(result.second); (void)result;
  }
  {
    //插入到activeTimers中
    std::pair<ActiveTimerSet::iterator, bool> result
      = activeTimers_.insert(ActiveTimer(timer, timer->sequence()));
    assert(result.second); (void)result;
  }

  assert(timers_.size() == activeTimers_.size());
  return earliestChanged;  //返回是否最早到期时间改变
}


三:定时器的使用

主要是在EventLoop中使用,EventLoop中为我们提供了四个函数,供用户使用,我们来看一下:

/在时间戳为time的时间执行,0.0表示一次性不重复
TimerId EventLoop::runAt(const Timestamp& time, const TimerCallback& cb)
{
  return timerQueue_->addTimer(cb, time, 0.0);  
}

//延迟delay时间执行的定时器
TimerId EventLoop::runAfter(double delay, const TimerCallback& cb)
{
  Timestamp time(addTime(Timestamp::now(), delay));  //合成一个时间戳
  return runAt(time, cb);
}

//间隔性的定时器,起始就是重复定时器,间隔interval需要大于0
TimerId EventLoop::runEvery(double interval, const TimerCallback& cb)
{
  Timestamp time(addTime(Timestamp::now(), interval));
  return timerQueue_->addTimer(cb, time, interval);
}
//直接调用timerQueue的cancle
void EventLoop::cancel(TimerId timerId)
{
  return timerQueue_->cancel(timerId);  
}

而timerQueue是EventLoop类的成员:

 boost::scoped_ptr<TimerQueue> timerQueue_;

所以用户操作这几个函数就可以利用定时器实现相应的功能。



本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

muduo网络库定时器的实现 的相关文章

  • 【星海随笔】计组数学小课堂

    计算机组成原理 https www bilibili com video BV1ps4y1d73V p 8 16的负一次方既为1 16 16 1 16进制转换为10进制 例如 5 8 5 16 1 8 16 1 十进制转N进制 则除以N 然
  • Transformers中文本生成方法model.generate()参数解释

    本博客仅作为记录 参考 LLM 大语言模型 解码时是怎么生成文本的 爱码网
  • python字符串中所有符合条件的索引

    使用re库中的finditer import re s 1111ah11111ah test re finditer ah s print i for i in test
  • mvp关联activity生命周期_Android MVP架构从入门到精通-真枪实弹

    Android MVP架构从入门到精通 真枪实弹 一 前言 你是否遇到过Activity Fragment中成百上千行代码 完全无法维护 看着头疼 你是否遇到过因后台接口还未写而你不能先写代码逻辑的情况 你是否遇到过用MVC架构写的项目进行
  • VMware Workstation 16 Player 安装Centos 7

    环境准备 VMware Workstation 16 Player 官方下载 https www vmware com products workstation player workstation player evaluation ht
  • Cesium 源码解析 Model(一)

    Cesium中对于3DTiles会解析成Model 特别是3DTile中的B3DM 过程主要是对gltf在Cesium中是如何解析并生成绘制命令的 content model new Model gltf gltfView gltf数据 c
  • 全球数字治理白皮书 附下载

    当今世界 科技革命和产业变革日新月异 数字经济蓬勃发展 深刻改变着人类生产生活方式 对各国经济社会发展 全球治理体系 人类文明进程影响深远 在经济全球化遭遇逆流 保护主义 单边主 义上升的背景下 数字化驱动的新一轮全球化仍蓬勃发展 已成为助
  • QT学习笔记(七)

    第12章 输入与输出 Qt提供了读写字节块的设备类QIODevice QIODevice类是抽象的 无法被实例化 一般是使用它的子类 它包括如下子类 其中 QProcess QTcpSocket QUdpSocket QSslSocket都
  • Java 优先级队列

    文章目录 Java 优先级队列 PriorityQueue简介 继承关系 PriorityQueue示例 Comparable比较器 Comparable接口 Comparator比较器 Comparator接口 底层原理 Java 优先级
  • Unity3d 游戏优化 mono等

    GC问题 1 C 变量分为两种类型 值类型和引用类型 值类型分配在栈区 引用类型分配在堆区 GC关注引用类型 2 GC卡顿原因 堆内存垃圾回收 向系统申请新的堆内存 3 GC触发条件 堆内存分配而当内存不足时 按频率自动触发 手动强行触发
  • 【Vue】 前端上传图片时限制只可以按文件夹上传图片( webkitdirectory )

    前言 最近再对公司前后端不分离的 C Winform 系统进行 Java Web 重构 为了保持客户使用习惯所以要高度还原 其中有一个功能就是上传图片时 以文件夹进行上传 要读取文件夹内所有的图片 看了挺多大神的博客 也看了 Element
  • Keepalived与HaProxy的协调合作原理分析

    Keepalived与HaProxy的协调合作原理分析 keepalived与haproxy合作场景 更好的理解方式 协调合作中考虑的问题 一 Keepalived 以TCP IP模型角度来分析 二 HaProxy 总结 协调合作中考虑的问
  • Go-Python-Java-C-LeetCode高分解法-第四周合集

    前言 本题解Go语言部分基于 LeetCode Go 其他部分基于本人实践学习 个人题解GitHub连接 LeetCode Go Python Java C Go Python Java C LeetCode高分解法 第一周合集 Go Py
  • 由于找不到d3dx9_43.dll无法继续执行-修复教程

    d3dx9 43 dll是一个非常重要的DLL文件 它属于微软DirectX 9 0c的一部分 它主要用于游戏应用程序的开发和运行 在使用Windows平台的玩家中非常常见 这个文件通过DirectX接口提供了一些相关的功能 比如图像和声音
  • 该服务器因不稳定无法正常反应,服务器连接异常

    服务器连接异常 内容精选 换一换 ELB的常见异常返回码有400 403 502 504等 若遇到这些返回码建议您先直接访问后端云服务器 查看是否是后端云服务器的异常 若后端云服务器响应正常 请参考表1排查处理 如果仍无法解决 请联系客服人
  • 非关系型数据库、关系型数据库mysql简介

    Nosql 非关系数据库 数据存储不需要固定的模式 NoSql特点 易扩展 数据之间无关系 大数据量高性能 细粒度 cache性能高 多样灵活的数据模型 增删字段容易 关系数据库 vs NoSql 传统关系数据库 ACID 事务所具有的四个
  • 【华为OD机试】组成最大数 (C++ Python Java)2023 B卷

    题目描述 小组中每位都有一张卡片 卡片上是6位内的正整数 将卡片连起来可以组成多种数字 计算组成的最大数字 输入描述 号分割的多个正整数字符串 不需要考虑非数字异常情况 小组最多25个人 输出描述 最大的数字字符串 用例1 输入 22 22
  • 机械电子工程用不用学c语言,机械电子工程到底学什么 毕业以后能干什么

    机械电子工程专业俗称机电一体化 是机械工程与自动化的一种 下文有途网小编给大家整理了机械电子工程的就业方向及前景 供参考 机械电子工程专业主要学什么 机械电子工程要学习得课程有 电工与电子技术 机械制图 工程力学 机械设计基础 机械制造基础
  • 红队打靶,红日系列,红日靶场5

    文章目录 靶场详情 外网渗透 端口扫描 漏洞发现与利用 获取shell 内网渗透 提权 内网信息收集 横向移动 第一种使用CS 第二种使用msf 路由转发与代理通道 Psexec 攻击 靶场详情 此次靶场虚拟机共用两个 一个外网一个内网 用

随机推荐

  • java实现二分查找-两种方式

    二分查找是一种查询效率非常高的查找算法 又称折半查找 起初在数据结构中学习递归时实现二分查找 实际上不用递归也可以实现 毕竟递归是需要开辟额外的空间的来辅助查询 本文就介绍两种方法 二分查找算法思想 有序的序列 每次都是以序列的中间位置的数
  • 看融合数学教学的steam教育模式

    传统意义上的 教育 德育 教学双肩挑 是要求教师能够处理好 德育 和 教学 两方面的工作要求 这实质上是通过人为划分的两个途径实现对学生的培养 STEAM教育的跨学科整合思维能够很好的迁移在 德育 与 教学 的整合上 即在教师引导下的学生参
  • OWASP Top 10 2021 榜单出炉,安全人员必看

    作为一个安全人员 如果你还不知道OWASP Top 10是什么 那么请你接着往下看 OWASP 开放式Web应用程序安全项目 是一个开源的 非营利性的全球性安全组织 致力于改进Web应用程序的安全 这个组织最出名是 它总结了10种最严重的W
  • 数据结构小白之队列与环形队列的模拟

    队列的简单介绍 队列是一个有序列表 可以使用数组或者链表来实现 队列遵循先入先出的原则 先存入队列的数据要先取出 后存入的后取出 使用数组来模拟队列 说明 实现思路 示意图 代码 1 说明 队列本身是有序列表 若使用数组的结构来存储队列的数
  • 安装和更新node的正确姿势

    干货时刻 本文主要讲解了如何安装node 以及如何更新node的版本 node js 是什么 简称node 是基于Chrome V8引擎的JavaScript JS 运行时环境 node 安装 进入node 官网 点击如下图所示的安装包即可
  • 前端之HTML

    一 概念 1 页面组成 结构 HTML Hyper Text Markup Language 超文本标记语言 页面原始和内容 表现 CSS 网页原始的外观和位置等页面样式 如颜色 大小等 行为 JavaScript 网页模型的定义与交互 简
  • 计算机组成原理慕课测试-第四单元

    计算机字长32位 主存容量为128MB 按字编址 其寻址范围为 0 32M 1 1 B 8b 1 kB 1024 B kB kilobajt 千 1 MB 1024 kB MB megabajt 兆 1 MB 1024 kB MB mega
  • IPV6 阿里DDNS

    IPV6 阿里DDNS 因为需要在家搭建一套环境 并且需要公网能访问 国内的ipv4的地址 各大运营商基本都不会分配ipv4地址 电信宽带好像有地方可以 但是听说很贵 而且是动态的 每过段时间就会改变 发现移动宽带的公网ipv6地址是可以获
  • 微信为什么更受欢迎?

    想必大家都和我一样 曾是一个QQ的忠实用户 认为QQ是最受欢迎的社交软件 其实不然 微信比QQ更受欢迎 只是我们根本不知道微信 所以 我们来谈谈微信NB在那里吧 1 QQ的用户是年轻化 娱乐性强 而微信让不是qq用户的人也加入进来 变得更加
  • CentOS通过nvm安装管理node

    今天搭建CentOS node 环境 原本打算源码安装 环境编译一直出错 为节省时间 直接用nvm 来下载和管理node nvm 是一个开源软件 大家可以在github 上面 下载它的源码https github com creationi
  • 【AI with ML】第 14 章 :在 iOS 应用程序中使用 TensorFlow Lite

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore
  • Applications(4)

    CONTENTS Other Applications In this section we cover a few other types of applications of deep learning that are differe
  • java使用smb操作win共享文件夹

    package com zky util import jcifs smb SmbException import jcifs smb SmbFile import jcifs smb SmbFileInputStream import j
  • 新手LearnOpenGL纹理不显示的部分解决方法

    项目场景 在LearnOpenGL学习中遇到的一些问题 照着写但是纹理加载不出来或者两张纹理只加载一张 问题描述 lt 纹理加载不出来 gt 1 文件路径是否正确 是否输出texture load fail等提示 设置了成功检查才会有提示
  • RK3568 Camera 使用

    RK3568 Camera 使用 RK3568 Sensor驱动开发移植 1 RK3568 Sensor驱动开发移植 2 RK3568 Sensor驱动开发移植 3 MIPI CSI用法 RK3568平台仅有一个标准物理mipi csi2
  • 修改pip下载源

    pip国内的一些镜像 阿里云 中国科学技术大学 豆瓣 清华大学 中国科学技术大学 修改源方法一 linux 修改 pip pip conf 如无就创建一个新的 修改内容为 global index url https pypi tuna t
  • 三、C语言进阶:二维指针

    3 二维指针 3 1 什么是二维指针 二维指针与一维指针一样都是保存变量的地址 实例 一维指针存放变量地址 二维指针存放一维指针地址 include
  • Java 多线程启动为什么调用 start() 方法而不是 run() 方法?

    多线程在工作中多多少少会用到 我们知道启动多线程调用的是 start 方法 而不是 run 方法 你知道原因吗 在探讨这个问题之前 我们先来了解一些多线程的基础知识 线程的状态 Java 中 定义了 6 种线程状态 在 Thread 类可以
  • 德标螺纹规格对照表_螺栓螺母德标 欧标 国标对照表

    新德标 旧德标 英文名 中文名 国标 DINENISO4014 DIN931 1 Hexagonheadbolts ProductgradesAandB I SO4014 1999 六角头螺栓 GB T5782 2000 DINENISO4
  • muduo网络库定时器的实现

    一 函数介绍 常见的与时间相关的函数有 sleep alarm usleep nanosleep clock nanosleep gettimer settitimer timer create timer settime timer ge