Muduo 异步log的实现

news2024/9/27 9:21:58

1. logging

日志(logging) 有两个意思:

  • 诊断日志: 即是我们日常debug 使用的文本文件记录trace。常用的log 有log4j, logback, log4cpp, ezlogger等常用的日志库。
  • 交易日志: 即是数据库的write-ahead log, 文件系统的journaling 等, 用于记录状态的变更,通过回访日志可以逐步恢复每一次修改之后的状态。

这篇文章我们主要介绍下muduo的log的实现,我觉得挺有意思的。muduo的日志库是C++ stream 风格的, 不需要保持格式字符串和参数类型的一致性, 可以随用随写,而且是类型安全的。我们需要的日志功能主要有下面几个需求:

  1. 日志消息有多种级别(level), 如TRACE, DEBUG, INFO, WARN, FATAL 等。
  2. 调整日志级别不需要重新编译,也不需要重启进程, 只要调用setloglevel() 就能即时6生效。
  3. 往本地存储写文件时,考虑日志文件的滚动。文件大小(例如每写满1GB就换下一个文件)和时间(例如每天零点新建一个日志文件,不论前一个文件有没有写满)

注意: 往文件中写日志的一个常见的问题时,万一程序崩溃,那么最后若干条日志就丢失了,因为日志库不能每条消息都flush硬盘,更不能每条日志都open/close文件,这样性能开销太大。 muduo采用两个办法来应对这一点:

  1. 定期(默认3s)将缓冲区内的日志消息flush到硬盘;
  2. 每条内存的日志消息都带有cookie, 其值为某个函数的地址,这样通过core dump 文件中查找cookie就能找到尚未来得及写入磁盘的消息。

日志消息的格式有几个要点:

  1. 尽量每条日志占一行。
  2. 时间戳精确到微秒。 muduo的每条消息时通过gettimeofday(2) 获得当前时间, 这个函数不是系统调用, 不会有什么性能损失。
  3. 打印线程的id。
  4. 打印日志的级别
  5. 打印源文件名和行号。

推荐一个零声学院免费教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:链接

2. 性能需求

编写linux 服务端程序的时候,我们需要一个高效的日志库。

  • 每秒写几千上万条日志的时候没有明显的i性能损失
  • 能应对一个进程产生大量的日志数据的场景, 例如 1GM/min
  • 不阻塞正常的执行流程
  • 在多线程中,不造成race condition.

为了满足这样的性能指标, muduo日志库的实现有几个优化措施。

  1. 时间戳字符串中的日期和时间两部分时缓存的, 一秒之内的多条日志只需重新格式化微秒部分。
  2. 日志消息的前4个字段时定长的,避免在运行期求字符串的长度。
  3. 线程id时格式化为字符串。
  4. 每行日志消息的源文件名部分采用了编译器计算来获得basename, 避免运行期strchr开销。

3. muduo的日志库分析

muduo的日志库采用的是双缓冲(double buffering)的技术. 准备两块buffer: A和B, 前端负责往buffer A填充数据, 后端负责将buffer B的数据写入文件。

当buffer A写满之后,交换A和B, 让后端将buffer A的数据写入文件,而前端则往buffer B填入新的日志消息,如此往复。用两个buffer的好处是在新建日志消息的时候不必等待磁盘文件操作, 也避免每条新日志消息都触发后端日志线程。换言之,前端将多条日志消息拼成一个大的buffer传送给后端,相当于批处理,减少了线程唤醒的频度,降低开销。muduo异步日志的性能开销大约是前端每写一条日志消息耗时1.0us ~ 1.6us.

在介绍AsyncLogging 文件之前,我们先看下LogFileLoggerLogStreamFixBuffer 的实现。

LogFile

logfile 比较简单,主要是rollfile的实现比较有意思。 我们先看下头文件。

class LogFile : noncopyable  // 不可被复制
{
 public:
  LogFile(const string& basename,     // basename 文件名
          off_t rollSize,             // rollSize 设置roll的大小的阈值,大于这个size的大小就开始roll. 
          bool threadSafe = true,     // 是否为线程安全, 为mutex 服务
          int flushInterval = 3,      // 几秒一次flush
          int checkEveryN = 1024);    // 检查roll 或者 flush的 次数
  ~LogFile();
  void append(const char* logline, int len);   // 给外部调用的只有这三个函数
  void flush();
  bool rollFile();
 private:
  void append_unlocked(const char* logline, int len);
  static string getLogFileName(const string& basename, time_t* now);
  const string basename_;
  const off_t rollSize_;
  const int flushInterval_;
  const int checkEveryN_;
  int count_;
  std::unique_ptr<MutexLock> mutex_;
  time_t startOfPeriod_;
  time_t lastRoll_;
  time_t lastFlush_;
  std::unique_ptr<FileUtil::AppendFile> file_; // 文件操作

  const static int kRollPerSeconds_ = 60*60*24;
};

cpp文件, 构造函数比较简单。

LogFile::LogFile(const string& basename,
                 off_t rollSize,
                 bool threadSafe,
                 int flushInterval,
                 int checkEveryN)
  : basename_(basename),
    rollSize_(rollSize),
    flushInterval_(flushInterval),
    checkEveryN_(checkEveryN),
    count_(0),
    mutex_(threadSafe ? new MutexLock : NULL),
    startOfPeriod_(0),    // 记录每一次roll的时间, roll 完会更新
    lastRoll_(0),         // 记录上一次roll的时间
    lastFlush_(0)         // 记录上一次的flush/roll的时间
{
  assert(basename.find('/') == string::npos);
  rollFile();
}

我们看下append的实现,append 需要输入append的内容和长度, 如果是线程的安全的,加锁去append。

void LogFile::append(const char* logline, int len)
{
  if (mutex_)
  {
    MutexLockGuard lock(*mutex_);
    append_unlocked(logline, len);
  }
  else
  {
    append_unlocked(logline, len);
  }
}

这里调用了append_unlocked, 我们看下这个函数的实现。我们调用文件操作append写入len字节的logline, writtenBytes 会返回append的大小,如果 writtenBytes 大于设置的rollSize_, 我们就开始’roolsize 的操作, 否则就判断count的次数, 我们默认的是1024次。如果上一次lastFlush_ 超过了3s, 我们就写到文件中。

void LogFile::append_unlocked(const char* logline, int len)
{
  file_->append(logline, len);

  if (file_->writtenBytes() > rollSize_)
  {
    rollFile();
  }
  else
  {
    ++count_;
    if (count_ >= checkEveryN_)
    {
      count_ = 0;
      time_t now = ::time(NULL);
      time_t thisPeriod_ = now / kRollPerSeconds_ * kRollPerSeconds_;
      if (thisPeriod_ != startOfPeriod_)
      {
        rollFile();
      }
      else if (now - lastFlush_ > flushInterval_)
      {
        lastFlush_ = now;
        file_->flush();
      }
    }
  }
}

我们 看下真正rollsize的实现, 我们创建文件名+时间+主机名+ pid.log的文件名, 返回文件名和时间,如果此刻大于我们的上一次的roll的时间, 我们就新建一个文件。

bool LogFile::rollFile()
{
  time_t now = 0;
  string filename = getLogFileName(basename_, &now);
  time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;

  if (now > lastRoll_)
  {
    lastRoll_ = now;
    lastFlush_ = now;
    startOfPeriod_ = start;
    file_.reset(new FileUtil::AppendFile(filename));
    return true;
  }
  return false;
}
string LogFile::getLogFileName(const string& basename, time_t* now)
{
  string filename;
  filename.reserve(basename.size() + 64);
  filename = basename;

  char timebuf[32];
  struct tm tm;
  *now = time(NULL);
  gmtime_r(now, &tm); // FIXME: localtime_r ?
  strftime(timebuf, sizeof timebuf, ".%Y%m%d-%H%M%S.", &tm);
  filename += timebuf;

  filename += ProcessInfo::hostname();

  char pidbuf[32];
  snprintf(pidbuf, sizeof pidbuf, ".%d", ProcessInfo::pid());
  filename += pidbuf;

  filename += ".log";

  return filename;
}

Logger

我们看到的loglevel 有六个级别,用枚举类型来定义的, setLogLevelstatic 类型的, 所以在多线程的写多个文件, 也会全局生效。

class Logger
{
 public:
  enum LogLevel
  {
    TRACE,
    DEBUG,
    INFO,
    WARN,
    ERROR,
    FATAL,
    NUM_LOG_LEVELS,
  };

  // compile time calculation of basename of source file
  class SourceFile
  {
   public:
  Logger(SourceFile file, int line);
  Logger(SourceFile file, int line, LogLevel level);
  Logger(SourceFile file, int line, LogLevel level, const char* func);
  Logger(SourceFile file, int line, bool toAbort);
  ~Logger();
  LogStream& stream() { return impl_.stream_; }
  static LogLevel logLevel();
  static void setLogLevel(LogLevel level);
  typedef void (*OutputFunc)(const char* msg, int len);
  typedef void (*FlushFunc)();
  static void setOutput(OutputFunc);
  static void setFlush(FlushFunc);
  static void setTimeZone(const TimeZone& tz);
      
  private:
    class Impl
    {
     public:
      typedef Logger::LogLevel LogLevel;
      Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
      void formatTime();
      void finish();

      Timestamp time_;
      LogStream stream_;
      LogLevel level_;
      int line_;
      SourceFile basename_;
    };
 Impl impl_;
};
    
extern Logger::LogLevel g_logLevel;
inline Logger::LogLevel Logger::logLevel()
{
  return g_logLevel;
}
    

muduo使用预定义的方式定义了八种log 的级别, 这样我们调用LOG_TRACE 就会调用到logstream 流里面。

#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
  muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
  muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
  muduo::Logger(__FILE__, __LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()

我们再看下setLogLevel() , 就是把level 设置给全局的loglevel, 比较简单。

void Logger::setLogLevel(Logger::LogLevel level)
{
  g_logLevel = level;
}

LogStream

在logstream 类里面定义了两个类, 一个是FixedBuffer, 设置我们在stream 定义的缓存的大小,定义了小缓存为kSmallBuffer = 4k, 最大的缓存为4MB.

FixedBuffer 的定义了一个data_[size]的数组和一个指向当前写入的字节的指针, 作用很简单, 就是预先开辟出一块内存,然后往里面写入(append) len 个字节, cur 会指向当前已写入的字节的多少。

const int kSmallBuffer = 4000;
const int kLargeBuffer = 4000*1000;

template<int SIZE>
class FixedBuffer : noncopyable
{
 public:
  FixedBuffer()
    : cur_(data_)
  {
    setCookie(cookieStart);
  }

  ~FixedBuffer()
  {
    setCookie(cookieEnd);
  }
  void append(const char* /*restrict*/ buf, size_t len)
  {
    // FIXME: append partially
    if (implicit_cast<size_t>(avail()) > len)
    {
      memcpy(cur_, buf, len);
      cur_ += len;
    }
  }
  const char* data() const { return data_; }
  int length() const { return static_cast<int>(cur_ - data_); }
  // write to data_ directly
  char* current() { return cur_; }
  int avail() const { return static_cast<int>(end() - cur_); }
  void add(size_t len) { cur_ += len; }

  void reset() { cur_ = data_; }
  void bzero() { memZero(data_, sizeof data_); }

  // for used by GDB
  const char* debugString();
  void setCookie(void (*cookie)()) { cookie_ = cookie; }
  // for used by unit test
  string toString() const { return string(data_, length()); }
  StringPiece toStringPiece() const { return StringPiece(data_, length()); }

 private:
  const char* end() const { return data_ + sizeof data_; }
  // Must be outline function for cookies.
  static void cookieStart();
  static void cookieEnd();

  void (*cookie_)();
  char data_[SIZE];
  char* cur_;
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TPouZoNW-1681660657395)(C:\Users\zhen\AppData\Roaming\Typora\typora-user-images\image-20230416210559996.png)]

这里最重要的是logstream的类。定义了Buffer的大小为4k, 并且重载了若干个operator<<, 这样我们写log文件的时候可以使用LOGTRACE<< 来写入, 这里也很简单, 就是把字符串,数字等写入缓存buffer内。

class LogStream : noncopyable
{
  typedef LogStream self;
 public:
  typedef detail::FixedBuffer<detail::kSmallBuffer> Buffer;

  self& operator<<(bool v)
  {
    buffer_.append(v ? "1" : "0", 1);
    return *this;
  }

  self& operator<<(short);
  self& operator<<(unsigned short);
  self& operator<<(int);
  self& operator<<(unsigned int);
  self& operator<<(long);
  self& operator<<(unsigned long);
  self& operator<<(long long);
  self& operator<<(unsigned long long);

  self& operator<<(const void*);

  self& operator<<(float v)
  {
    *this << static_cast<double>(v);
    return *this;
  }
  self& operator<<(double);
  // self& operator<<(long double);

  self& operator<<(char v)
  {
    buffer_.append(&v, 1);
    return *this;
  }

  self& operator<<(const char* str)
  {
    if (str)
    {
      buffer_.append(str, strlen(str));
    }
    else
    {
      buffer_.append("(null)", 6);
    }
    return *this;
  }

  self& operator<<(const unsigned char* str)
  {
    return operator<<(reinterpret_cast<const char*>(str));
  }

  self& operator<<(const string& v)
  {
    buffer_.append(v.c_str(), v.size());
    return *this;
  }

  self& operator<<(const StringPiece& v)
  {
    buffer_.append(v.data(), v.size());
    return *this;
  }

  self& operator<<(const Buffer& v)
  {
    *this << v.toStringPiece();
    return *this;
  }

  void append(const char* data, int len) { buffer_.append(data, len); }
  const Buffer& buffer() const { return buffer_; }
  void resetBuffer() { buffer_.reset(); }
 private:
  void staticCheck();
  template<typename T>
  void formatInteger(T);
  Buffer buffer_;
  static const int kMaxNumericSize = 48;
};

接下来就是我们最重要的异步log的实现。

AsyncLogging

在AsyncLogging 中, 采用了四个缓冲区,这样可以进一步减少或者避免等待。

class AsyncLogging : noncopyable
{
 public:

  AsyncLogging(const string& basename,
               off_t rollSize,
               int flushInterval = 3);

  ~AsyncLogging()
  {
    if (running_)
    {
      stop();
    }
  }

  void append(const char* logline, int len);

  void start()
  {
    running_ = true;
    thread_.start();
    latch_.wait();
  }

  void stop() NO_THREAD_SAFETY_ANALYSIS
  {
    running_ = false;
    cond_.notify();
    thread_.join();
  }

 private:

  void threadFunc();

  typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer; // 4MB的缓冲
  typedef std::vector<std::unique_ptr<Buffer>> BufferVector; // 缓冲数组
  typedef BufferVector::value_type BufferPtr;  // 缓冲的指针

  const int flushInterval_;
  std::atomic<bool> running_;
  const string basename_;
  const off_t rollSize_;
  muduo::Thread thread_;
  muduo::CountDownLatch latch_;
  muduo::MutexLock mutex_;                     // 线程安全
  muduo::Condition cond_ GUARDED_BY(mutex_);
  BufferPtr currentBuffer_ GUARDED_BY(mutex_);   // 当前缓冲
  BufferPtr nextBuffer_ GUARDED_BY(mutex_);      // 预备缓冲
  BufferVector buffers_ GUARDED_BY(mutex_);     //待写入文件的已填满的缓冲
};

先来看发送方的代码。

AsyncLogging::AsyncLogging(const string& basename,
                           off_t rollSize,
                           int flushInterval)
  : flushInterval_(flushInterval),
    running_(false),
    basename_(basename),
    rollSize_(rollSize),
    thread_(std::bind(&AsyncLogging::threadFunc, this), "Logging"),
    latch_(1),
    mutex_(),
    cond_(mutex_),
    currentBuffer_(new Buffer),
    nextBuffer_(new Buffer),
    buffers_()
{
  currentBuffer_->bzero();
  nextBuffer_->bzero();
  buffers_.reserve(16);
}

void AsyncLogging::append(const char* logline, int len)
{
  muduo::MutexLockGuard lock(mutex_);
  if (currentBuffer_->avail() > len)   // 缓存没满, 写入缓冲中
  {
    currentBuffer_->append(logline, len);
  }
  else                               // 缓存满了
  {
    buffers_.push_back(std::move(currentBuffer_));   // 放入待写入的缓存中

    if (nextBuffer_)                         // 准备好另一块缓冲
    {
      currentBuffer_ = std::move(nextBuffer_);  // 把当前缓存的指针指向空buffer中
    }
    else
    {
      currentBuffer_.reset(new Buffer); // Rarely happens
    }
    currentBuffer_->append(logline, len);    // 当前缓冲继续append, 并通知
    cond_.notify();
  }
}

前端生成一条日志消息的时候会调用append()。 在这个函数中,如果当前缓冲(currentBuffer_)剩余空间足够大,则会直接把消息追加在到缓存中。否则说明当前缓冲区已满/不够,就把它送入buffers, 并把另一块备用的buffer设置为当前的缓冲区,然后追加消息并通知后端写入日志数据。如果写入速度太快,一下子把两块缓存都用完了,那么只好分配新的缓存作为当前缓存。

再看下接收方实现。

void AsyncLogging::threadFunc()
{
  assert(running_ == true);
  latch_.countDown();
  LogFile output(basename_, rollSize_, false); // LogfIle是真正写入的
  BufferPtr newBuffer1(new Buffer);      // 准备好第一块buffer
  BufferPtr newBuffer2(new Buffer);      // 准备好第二块buffer
  newBuffer1->bzero();
  newBuffer2->bzero();
  BufferVector buffersToWrite;
  buffersToWrite.reserve(16);
  while (running_)
  {
    assert(newBuffer1 && newBuffer1->length() == 0);
    assert(newBuffer2 && newBuffer2->length() == 0);
    assert(buffersToWrite.empty());

    {
      muduo::MutexLockGuard lock(mutex_);
      if (buffers_.empty())  // unusual usage!
      {
        cond_.waitForSeconds(flushInterval_);   // 等待条件触发
      }
      buffers_.push_back(std::move(currentBuffer_));  // 条件满足时, 先将当前缓冲区移入buffers
      currentBuffer_ = std::move(newBuffer1);        // 并立刻将空闲的newBuffer1 作为新的缓冲
      buffersToWrite.swap(buffers_);                 // 将空的buffersToWrite 填充
      if (!nextBuffer_)
      {
        nextBuffer_ = std::move(newBuffer2);    // 如果nextBuffer为空, 将buffer2 作为新的nextBuffer, 这样前端始终有一个buffer可供调配
      }
    }

    assert(!buffersToWrite.empty());

    if (buffersToWrite.size() > 25)  // 如果长度超过25, 丢弃一部分
    {
      char buf[256];
      snprintf(buf, sizeof buf, "Dropped log messages at %s, %zd larger buffers\n",
               Timestamp::now().toFormattedString().c_str(),
               buffersToWrite.size()-2);
      fputs(buf, stderr);
      output.append(buf, static_cast<int>(strlen(buf)));
      buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end());
    }

    for (const auto& buffer : buffersToWrite)   // 遍历数组,真正写入内存中
    {
      // FIXME: use unbuffered stdio FILE ? or use ::writev ?
      output.append(buffer->data(), buffer->length());
    }

    if (buffersToWrite.size() > 2)
    {
      // drop non-bzero-ed buffers, avoid trashing
      buffersToWrite.resize(2);         // buffersToWrite的大小设置为2
    }

    if (!newBuffer1)    // 将buffersToWrite内的buffer重新填充为newBuffer1 
    {
      assert(!buffersToWrite.empty());
      newBuffer1 = std::move(buffersToWrite.back());
      buffersToWrite.pop_back();
      newBuffer1->reset();
    }

    if (!newBuffer2) // 将buffersToWrite内的buffer重新填充为newBuffer2
    {
      assert(!buffersToWrite.empty());
      newBuffer2 = std::move(buffersToWrite.back());
      buffersToWrite.pop_back();
      newBuffer2->reset();
    }

    buffersToWrite.clear();
    output.flush();
  }
  output.flush();
}

我们看下运行的图示:

一开始我们先分配好四个缓存, A, B ,C ,D. 初始时都是空的。

  1. **缓冲A, B都没满, 但是超时。**写入频度不高, 后端3s 超市后将" 当前缓冲currentBuffer" 写入文件。在2.9s的时候, currBuffer使用了80%, 在第三秒的时候后端线程先把curr送到buffers, 再把newBuffer1设置为curr。 随后3s+, 交换buffers 和buffersToWrite, 后端开始将buffer A写入文件,写完再把new1再填上,供下次cond返回。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q61dWmWf-1681660657396)(C:\Users\zhen\AppData\Roaming\Typora\typora-user-images\image-20230416232620108.png)]

  1. **缓冲A满了,B未满。**在3s超时之前已经写满了当前缓冲,于是唤醒后端线程开始写入文件。在第1.5s的时候, currBuffer使用了80%, 第1.8s, curr写满,将当前缓冲送入buffers_, 并将nextbuffer_ 设置为当前缓冲,然后开始写入。当后端线程唤醒之后(1.8s+), 先将curr送入buffers_, 再将new1, new2设置为当前缓冲。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dn7MbboS-1681660657397)(C:\Users\zhen\AppData\Roaming\Typora\typora-user-images\image-20230416233005823.png)]

  1. **缓冲AB都满。**前端在短时间内写入大量的消息,用完了两个缓冲,并重新分配了一块新的缓冲。在第1.8s的时候, A已经写满, B也接近写满。并且已经notify() 后端线程,但是由于种种原因, 后端线程并没有立刻开始工作, 直到1.9s的之后, B也写满了, 前端线程重新分配了缓冲E, 到了1.8s+, 后端线程开始写入,将CD 两块缓冲交给前端,并开始将ABE写入文件。完成之后, 用AB重新填充那两块缓冲,释放缓冲E。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vCblPr7Q-1681660657397)(C:\Users\zhen\AppData\Roaming\Typora\typora-user-images\image-20230416233522121.png)]

  1. 文件写入速度慢,导致前端耗尽了两个缓冲。 前1.8s+ 和前面的第二种相同, 前端写满了一个缓冲,唤醒后后端线程开始写入文件。之后,后端花了较长时间才将数据写完。这期间前端又用完了两个缓冲(CD),并分配了新的缓冲(E), 这期间前端的notify已经丢失。当后端写完之后,发现buffers_ 不为空, 立刻进入下一循环。替换前端的两个缓冲,并开始一次将CDE写入。如果buffersToWrite的大小超过了两个, 将重新把buffersToWrite的大小设置为2个, 再将new1, new2填充缓冲。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aGLqxSFK-1681660657398)(C:\Users\zhen\AppData\Roaming\Typora\typora-user-images\image-20230416234324019.png)]

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

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

相关文章

软考 - IP地址与网络划分

一.IP组成 1.1 首个八位字节规则 1.2 地址掩码 IP地址掩码 标准地址掩码 A类&#xff1a;255.0.0.0 前1个字节是网络号 后3个字节是主机号 B类&#xff1a;255.255.0.0 前2个字节是网络号 后2个字节是主机号 C类&#xff1b;255.255.255.0 前3个字节是网络号 后1个字节是主机号…

nssctf web 入门(5)

这里通过nssctf的题单web安全入门来写&#xff0c;会按照题单详细解释每题。题单在NSSCTF中。 想入门ctfweb的可以看这个系列&#xff0c;之后会一直出这个题单的解析&#xff0c;题目一共有28题&#xff0c;打算写10篇。 目录 [鹤城杯 2021]EasyP [SWPUCTF 2021 新生赛]eas…

VR全景--720全景 助力行业数字化新模式

引言 720全景就是视角超过人的正常视角的图像&#xff0c;而我们这里说的全景特指水平360度&#xff0c;上下360度全能观看的&#xff0c;能看到“天、地”的全景。全景实际上只是一种对周围景象以某种几何关系进行映射生成的平面图片&#xff0c;只有通过全景播放器的矫正处理…

电力监控系统高扩展性的设计思路与方法

安科瑞 徐浩竣 江苏安科瑞电器制造有限公司 zx acrelxhj 电力监控软件的可扩展性设计 2023-04-19 14:00AcreXHJ 安科瑞 徐浩竣 江苏安科瑞电器制造有限公司 zx acrelxhj 摘要&#xff1a;本文根据安科瑞电力监控系统软件的设计过程&#xff0c;论述了电力监控系统高扩展…

JS 正则表达式(正则匹配RegExp)

JavaScript实现对象深拷贝的方法&#xff08;5种&#xff09; 知识回调&#xff08;不懂就看这儿&#xff01;&#xff09;场景复现核心干货举例引入关于RegExp对象语法修饰符——区分大小写和全局匹配方括号——查找某个范围内的字符元字符——拥有特殊含义的字符量词RegExp对…

4.Hadoop三件套(1)

如果说HDFS是存储,则Yarn就是cpu和内存,mapreduce就是程序。 1.HDFS文件块大小 HDFS中的文件在物理.上是分块存储(Block) ,block默认保存3份块的大小可以通过配置参数(dfs blocksize)来规定,默认大小在Hadoop2 .x版本中是128M,老版本中是64M。 解释:块的大小:10ms*100*…

代码随想录算法训练营第48天 | 动态规划 LeetCode198.打家劫舍,213.打家劫舍II,337.打家劫舍 III

代码随想录算法训练营第48天 | 动态规划 LeetCode198.打家劫舍&#xff0c;213.打家劫舍II&#xff0c;337.打家劫舍 III 198.打家劫舍 第一遍读题思考 dp[i]代表打劫到第i家的时候抢到的最多的钱数。 那么递推公式就跟dp[i-1] and dp[i-2]有关&#xff0c;如果抢i-1家的&am…

【梦回stm32】-寄存器篇-跑马灯代码研读-总结

前言&#xff1a;学就要学懂&#xff0c;别以为做出来就行&#xff0c;要弄懂通路&#xff0c;思路要清楚-谨记 1.先讲讲背景&#xff0c;此次需要研究的是stm32的跑马灯代码&#xff0c;下图是个本实验用的正点原子的代码目录 1&#xff09;HARDWARE-》LED 2&#xff09;SYS…

【COMP282 LEC 1-2】

LEC 1 Introduction to c Header file : 1. 不用写 " .h " Function : 数据类型有string了&#xff0c;就像java&#xff0c;需要#include<string> Output system : Explain by example: std::cout << hello << " is of length "…

电脑IP地址查询,3个实用方法!

案例&#xff1a;电脑IP地址怎么查询 【请问有没有朋友知道电脑IP地址怎么查询呀&#xff1f;想看看我的IP地址却不知道该怎么查询&#xff0c;哪位友友可以帮帮我&#xff01;】 在互联网世界中&#xff0c;IP地址是一种用于标识设备&#xff08;如电脑、手机等&#xff09;…

在OLED上显示各种各样的数据(文字、字母、图片)

今天来说一说OLED&#xff0c;也同时记录一下成果。 在学习OLED之前需要知道IIC协议&#xff0c;不了解的可以看&#xff08;IIC协议相关_dxdlnu的博客-CSDN博客&#xff09; 什么是OLED? OLED_百度百科 (baidu.com) 一.OLED写入指令和数据 因为OLED是有写入指令/数据的操作…

camunda工作流引擎多租户架构

Camunda支持多租户的方式有两种&#xff1a;共享引擎和多引擎。 1、共享引擎 在共享引擎的模式下&#xff0c;多个租户共享同一个Camunda引擎。每个租户的数据被存储在引擎中的不同表中&#xff0c;而引擎的配置和部署是共享的。可以通过以下方式实现共享引擎&#xff1a; &…

OpenAI-ChatGPT最新官方接口《微调ChatGPT模型》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(四)(附源码)

微调ChatGPT模型 前言Introduction 导言What models can be fine-tuned? 哪些模型可以微调&#xff1f;Installation 安装Prepare training data 准备训练数据CLI data preparation tool CLI数据准备工具Create a fine-tuned model 创建微调模型Use a fine-tuned model 使用微…

​HTTP和TCP协议的队头阻塞​

队头阻塞&#xff08;Head-of-line blocking&#xff09;其实有两种&#xff0c;一种是 TCP 队头阻塞&#xff0c;另一种是 HTTP 队头阻塞&#xff0c;而这两者之前其实还存在一定的联系&#xff0c;毕竟 HTTP1/2 是建立在 TCP 协议之上的应用层协议&#xff0c;另外还有HTTP3对…

【LeetCode 图论 一】初探有向图Directed Graph

今天主要介绍DAG &#xff08;Directed acyclic graph&#xff09;&#xff0c;有向无环图。 无向图的问题相对有向图比较简单&#xff0c;比如岛屿问题&#xff0c;迷宫问题等。 在有向图中&#xff0c;我们通常只关注环是否存在&#xff0c;因为有向图中环的存在会让我们的…

HBuilderX的快捷键 和 常见配置

快捷键说明ctrl /添加注释&#xff08;js&#xff1a;// 注释、css&#xff1a;/* 注释 */、vue/nvue模板&#xff1a; <!-- 注释 -->&#xff09;Ctrl C 拷贝当前行或者所选代码块到剪切板Ctrl X剪切当前行或所选代码块到剪切板Ctrl V粘贴剪切板上的内容Ctrl D 删除…

浏览器实时查看日志系统-log.io

标题 前言操作步骤1. Install server via npm安装Nodejs2. Launch server3. Install file input via npm4. Configure file input前言 log.io 是一个实时日志监控工具,采用 node.js + socket.io 开发,使用浏览器访问,每秒可以处理超过5000条日志变动消息。有一点要指出来的…

叔可忍婶不可忍!马斯克3月呼吁暂停人工智能,4月却创立TruthGPT

2018年马斯克退出了OpenAI团队。 2022年11月&#xff0c;ChatGPT在北美大陆问世。 2023年3月21日&#xff0c;马斯克在未来生命&#xff0c;签署并呼吁&#xff0c;暂停高级人工智能的研发。 2023年4月18日马斯克创立了TruthGPT。 同时&#xff0c;亚马逊&#xff0c;也创立了B…

提桶跑路转行到软件测试,鬼知道我经历了什么?很庆幸转行成功...

说来惭愧&#xff0c;我的大学不是985也不是211&#xff0c;不过地质学科群一直排名国内前几&#xff0c;2017年也藉由地质学学科评上了“双一流”。咱们土木工程跟着沾沾光&#xff0c;也能和别人吹吹牛逼&#xff0c;所以毕业后的工作基本上在省内是不愁的。 但是我还是低估了…

“全自主、全流程、全覆盖”2023实景三维新技术研讨会广州站圆满举行!

4月12日&#xff0c;由中国测绘学会、中国地理信息产业协会指导&#xff0c;广州省测绘学会主办&#xff0c;武汉大势智慧科技有限公司承办的全自主、全流程、全覆盖”2023实景三维新技术研讨会广州站在广州珠江宾馆隆重举行。 本次会议广东省测绘学会、地图院、测绘院、市城市…