muduo源码学习(2):异步日志——异步日志的实现

2023-05-16

目录

什么是异步日志

异步日志的实现

前端与后端

前端与后端的交互

资源回收

后端与日志文件

滚动日志

自动flush缓冲区

开启异步日志功能

总结


        在前文中分析了日志消息的存储和输出,不过并没有涉及到异步日志,下面就来分析一下异步日志是如何实现的。

什么是异步日志

        在默认的情况下,日志消息都是直接打印到终端屏幕上,但是实际应用中,日志消息都应该写到本地文件,方便记录以及查询。

        最简单的方式就是每产生一条日志消息,都将其写到相应的文件中,然而这种方式效率低下,如果很多线程在某一段时间内需要输出大量日志,那么显然日志输出的效率是很低的。之所以效率低,就是因为每条日志消息都需要通过write这类的函数写出到本地磁盘,这就导致频繁调用IO函数,而磁盘操作本身就比较费时,这样一来后面的代码就只能阻塞住,直到前一条日志写出成功

        为了优化上述问题,一个比较好的办法就是:当日志消息积累到一定量的时候再写到磁盘中,这样就可以显著减少IO操作的次数,从而提高效率

        换句话说,当日志消息需要输出时,并不会立即将其写出到磁盘上,而是先把日志消息存储,直到达到”写出时机“才会将存储的日志消息写出到磁盘,这样一来,当日志消息生成时,只需要将其进行存储而不需要写出,后续代码也不会被阻塞,相对于前面的那种阻塞式日志,这种就是非阻塞式日志

        muduo的异步日志核心思想正是如此。当需要输出日志的时候,会先将日志存下来,日志消息存储达到某个阈值时将这些日志消息全部写到磁盘。需要考虑的是,如果日志消息产生比较慢,可能很长一段时间都达不到这个阈值,那就相当于这些日志消息一直无法写出到磁盘,因此,还应当设置一个超时值如3s,每过3s不管日志消息存储量是否达到阈值,都会将已经存储的日志消息写出到磁盘中。即日志写出到磁盘的两个时机:1、日志消息存储量达到写出阈值;2、每过3秒自动将存储的日志消息全部写出。

        这种非阻塞式日志也是异步的,因为产生日志的线程只负责产生日志,并不需要去管它产生的这条日志何时写出,写往何处...

       

异步日志的实现

       muduo中通过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;
  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_); //当前缓冲区   4M大小
  BufferPtr nextBuffer_ GUARDED_BY(mutex_);   //预备缓冲区,主要是在调用append向当前缓冲添加日志消息时,如果当前缓冲放不下,当前缓冲就会被移动到前端缓冲队列中国,此时预备缓冲区用来作为新的当前缓冲
  BufferVector buffers_ GUARDED_BY(mutex_);//前端缓冲区队列
};

        注意到这里typedef了一个新类型为Buffer类型,根据其定义可知,它就是前文所说的FixedBuffer缓冲区类型,而这个缓冲区大小由kLargeBuffer指定,大小为4M,因此,Buffer就是大小为4M的缓冲区类型。

        这里定义了currentBuffer_和nextBuffer_,这两个缓冲区就是上面所说的”前端“,用来暂时存储生成的日志消息,只不过nextBuffer_用作预备缓冲区,当currentBuffer_不够用时用nextBuffer_来补充currentBuffer_。

        然后就是buffers_,这是一个vector,它用来存储”准备写到后端“的缓冲区,举个例子,如果currentBuffer_写满了,那么就会把写满的currentBuffer_放到buffers_中。

       如上所述,”前端“会将日志消息全部存到currentBuffer_中,如果放不下了,就会把currentBuffer_放到buffers_中以备”后端“读取。可想而知,异步日志的”后端“,就主要负责去和buffers_进行交互,将buffers中的缓冲区中的内容全部写出到磁盘,因此,就需要开启另一个线程,来执行”后端“的任务,下文将其称为”后端线程“。

        后端线程由thread_成员封装,在构造函数中指定其线程函数为threadFunc,如下所示:

AsyncLogging::AsyncLogging(const string& basename,
                           off_t rollSize,
                           int flushInterval)
  : 
    thread_(std::bind(&AsyncLogging::threadFunc, this), "Logging"),  
    ...
{
  ...
}

       

前端与后端的交互

         现在来看一下前端和后端之间是如何交互的。

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_);
        } else//如果预备缓冲区无法使用,就重新分配一个新的缓冲区(如果日志写的速度很快,但是IO速度很慢,那么前端日志缓冲区就会积累,但是后端还没有来得及处理,此时预备缓冲区也还没有归还,就会产生这种情况
        {
            currentBuffer_.reset(new Buffer); // Rarely happens
        }
        currentBuffer_->append(logline, len);//向新的当前缓冲区中写入日志消息
        cond_.notify();
    }
}

void AsyncLogging::threadFunc()  //写日志线程,将缓冲区队列中的数据调用LogFile的append
{
  assert(running_ == true);
  latch_.countDown();   //计数变量latch减1
  LogFile output(basename_, rollSize_, false);   //指定输出的日志文件
  BufferPtr newBuffer1(new Buffer);//用来填充移动后的currentBuffer_
  BufferPtr newBuffer2(new Buffer);//用来填充使用后的nextBuffer_
  newBuffer1->bzero(); //缓冲区清零
  newBuffer2->bzero(); //缓冲区清零
  BufferVector buffersToWrite;//后端缓冲区队列,初始大小为16
  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!    如果前端缓冲区队列为空,就休眠flushInterval_的时间
      {
        cond_.waitForSeconds(flushInterval_);//如果前端缓冲区队列中有数据了就会被唤醒
      }
      buffers_.push_back(std::move(currentBuffer_));
	  currentBuffer_ = std::move(newBuffer1); //当前缓冲区获取新的内存
      buffersToWrite.swap(buffers_); //前端缓冲区队列与后端缓冲区队列交换
      if (!nextBuffer_) //如果预备缓冲区为空,那么就使用newBuffer2作为预备缓冲区,保证始终有一个空闲的缓冲区用于预备
      {
        nextBuffer_ = std::move(newBuffer2);
      }
    }

    assert(!buffersToWrite.empty());

    if (buffersToWrite.size() > 25) //如果最终后端缓冲区的缓冲区太多就只保留前三个
    {
      char buf[256];//buf作为缓冲区太多时的错误提示字符串
      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)));//将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);
    }

    if (!newBuffer1)//如果newBuffer1为空 (刚才用来替代当前缓冲了)
    {
      assert(!buffersToWrite.empty());
      newBuffer1 = std::move(buffersToWrite.back()); //把后端缓冲区的最后一个作为newBuffer1
      buffersToWrite.pop_back(); //最后一个元素的拥有权已经转移到了newBuffer1中,因此弹出最后一个
      newBuffer1->reset(); //重置newBuffer1为空闲状态(注意,这里调用的是Buffer类的reset函数而不是unique_ptr的reset函数)
    }

    if (!newBuffer2)//如果newBuffer2为空
    {
      assert(!buffersToWrite.empty());
      newBuffer2 = std::move(buffersToWrite.back());
      buffersToWrite.pop_back();
      newBuffer2->reset();
    }

    buffersToWrite.clear();//清空后端缓冲区队列
    output.flush();//清空文件缓冲区
  }
  output.flush();
}

        对于前端,只需要调用append函数即可,如果currentBuffer_足以放下当前日志消息就调用缓冲区的append函数放入消息,如果放不下,就会将currentBuffer_放入buffer_中,注意,这里使用的是移动,移动后currentBuffer_为NULL,此时如果预备缓冲区nextBuffer_尚未使用,那么就会将nextBuffer_的拥有权转移给currentBuffer_,转移后nextBuffer_为NULL,意为已被使用;而如果预备缓冲区本身就为NULL,这种情况会出现在非常频繁调用append函数,导致连续多次填满currentBuffer_的时候,此时nextBuffer_已无法为currentBuffer_提供预备空间,因此只能为currentBuffer_重新分配新的空间。(实际上这种情况很少发生,因为默认的每条日志消息的大小最大为4K,而currentBuffer_的大小为4M,除非连续写入8M以上的日志消息,而后端来不及处理这些消息,才会发生这种情况)。当前端向buffers_中移入缓冲区后,就会唤醒条件变量。

        接着来看看后端,通过threadFunc函数可知,后端线程会循环去检查buffers_,如果buffers为空,那么后端线程就会休眠最多为flushInterval指定的秒数(默认为3秒),如果在此期间buffers中有了数据,后端线程就会被唤醒,否则就一直休眠直到超时,不管是哪种唤醒,都会将currentBuffer移入buffers中,这是因为后端线程每次操作都是准备将所有日志消息进行输出,而currentBuffer中大多数情况下都存有日志消息,因此即使其未满也会被放入buffers中,然后用newBuffer1来补充currentBuffer。

        接下来就需要注意buffersToWrite这个vector,和buffers是相同的类型,buffersToWrite就是后端缓冲区队列,负责将前端buffers中的数据拿过来,然后把这些数据写出到磁盘。因此,当currentBuffer被移入buffersToWrite后,就会立刻调用swap函数交换buffersToWrite和buffers,这一部交换了这两个vector中的内存指针,相当于二者交换了各自的内容,buffers变成了空的,而前面所有存有日志消息的缓冲区,则全部到了buffersToWrite中。

        然后,如果此时预备缓冲区为空,说明已经被使用过,就会用newBuffer2来补充它,至此,互斥锁释放。这里互斥锁的释放位置是个值得思考的地方,考虑到并发效率,互斥锁持有的临界区大小不应太大(不应简单的去锁住每一轮循环),在buffersToWrite获得了buffers的数据之后,其它线程就可以正常的调用append来添加日志消息了,因为此时buffers重置为空,并且buffersToWrite是局部变量,二者互不影响。

资源回收

        接着就是很自然的步骤了:将buffersToWrite中所有的缓冲区内容写到本地磁盘中,这一点后面再分析。

        在写出结束后,buffersToWrite中缓冲区的内容就已经没价值了,不过废物依然可以回收:由于前面newBuffer1和newBuffer2都有可能被使用过而为空,因此可以将buffersToWrite中的元素用来填充newBuffer1和newBuffer2。

        实际上,在正常情况下(指的是日志消息产生速度不会连续爆掉两块currentBuffer),currentBuffer、nextBuffer、newBuffer1和newBuffer2是不需要二次分配空间的,因为它们之间通过buffers和buffersToWrite恰好可以构成一个资源使用环:前端将currentBuffer移入buffers后用nextBuffer填补currentBuffer,后端线程将新的currentBuffer再次移入buffers,然后用newBuffer1和newBuffer2去填充currentBuffer和nextBuffer,最后又从buffersToWrite中获取元素来填充newBuffer1和newBuffer2,可见,资源的消耗端在currentBuffer和nextBuffer,而资源的补充端在newBuffer1和newBuffer2,如果这个过程是平衡的,那么这4个缓冲区都无需再分配新的空间,然后,这一点并不能得到保证,如果预备缓冲区数量越多,越能保证这一点,不过带来的就是空间上的消耗了。

后端与日志文件

        后端线程将缓冲区内容写出到日志文件通过调用LogFile类的append函数实现,但是muduo中与磁盘文件交互最紧密的并不是LogFile类,而是AppendFile类,该类含有一个文件指针指向外部文件,其最主要的函数就是append函数,定义如下:

class AppendFile : noncopyable
{
 public:
  explicit AppendFile(StringArg filename);

  ~AppendFile();

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

  void flush();

  off_t writtenBytes() const { return writtenBytes_; }

 private:

  size_t write(const char* logline, size_t len);

  FILE* fp_;
  char buffer_[64*1024];//缓冲区大小为64K,默认的是4K
  off_t writtenBytes_;//标识当前文件一共写入了多少字节的数据,如果超过了rollsize,LogFile就会进行rollFile,创建新的日志文件,而这个文件就不会再写入了
};

void FileUtil::AppendFile::append(const char* logline, const size_t len)
{
  size_t n = write(logline, len);  //写出日志消息
  size_t remain = len - n;  //计算未写出的部分
  while (remain > 0)//循环直到全部写出
  {
    size_t x = write(logline + n, remain);  //实际调用fwrite_unlock
    if (x == 0)
    {
      int err = ferror(fp_);
      if (err)
      {
        fprintf(stderr, "AppendFile::append() failed %s\n", strerror_tl(err)); //stderr不带缓冲,会立刻输出
      }
      break;
    }
    n += x;
    remain = len - n; // remain -= x
  }

  writtenBytes_ += len;
}

       可见,AppendFile类的append函数进行了IO操作,writtenBytes会记录下写出到fp_对应的文件的字节数。

       LogFile类中通过unique_ptr包装了一个AppendFile类实例file_,在后端线程写出时所调用的LogFile类的append函数中,就会通过该实例调用AppendFile类的append函数来将后端缓冲区中的内容全部写出到日志文件中,如下所示:

void LogFile::append_unlocked(const char* logline, int len)
{
  file_->append(logline, len);//将缓冲区内容写出到日志文件中

  if (file_->writtenBytes() > rollSize_)//如果写出的字节数大于了rollsize,就通过rollFile新建一个文件
  {
    rollFile();
  }
  else
  {
    ++count_;
    if (count_ >= checkEveryN_)   //每调用一次append计数一次,每调用1024次检查是否需要隔天rollfile或者flush缓冲区
    {
      count_ = 0;
      time_t now = ::time(NULL);
      time_t thisPeriod_ = now / kRollPerSeconds_ * kRollPerSeconds_;
      if (thisPeriod_ != startOfPeriod_)
      {
        rollFile();
      }
      else if (now - lastFlush_ > flushInterval_)  //外部文件流是全缓冲的,因此fwrite并不能立刻将数据写出到外部文件中,因此需要设定一个flush间隔,每隔一段时间将数据flush到外部文件中
      {
        lastFlush_ = now;
        file_->flush();
      }
    }
  }
}

 

         在后端与日志文件的交互中,除了写出数据到日志文件,还进行了两个重要的操作:滚动日志、自动flush缓冲区。

滚动日志

        日志滚动通过rollFile函数实现,如下所示:

bool LogFile::rollFile()
{

  time_t now = 0;
  string filename = getLogFileName(basename_, &now);//得到输出日志的文件名
  time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;//计算现在是第几天 now/kRollPerSeconds求出现在是第几天,再乘以秒数相当于是当前天数0点对应的秒数

  if (now > lastRoll_)
  {
      rollcnt++;
    lastRoll_ = now;//更新lastRoll
    lastFlush_ = now;//更新lastFlush
    startOfPeriod_ = start;
    file_.reset(new FileUtil::AppendFile(filename));//让file_指向一个名为filename的文件,相当于新建了一个文件
    return true;
  }
  return false;
}

        可以看到,rollFile的作用,就是创建一个新文件,然后让file_去指向这个新文件,新文件的命名方式为:basename + time + hostname + pid + ".log",在此之后所有日志消息都将写到新文件中。

        回到LogFile的append函数中,可以看到rollFile发生在两种情况下:1.当写出到日志文件的字节数达到滚动阈值,这个阈值由AsyncLogging构造时指定,并用来构造LogFile;2.每到新的一天就滚动一次。

        需要注意的是第2点,并不是到了新的一天的第一条日志消息就会导致rollFile,而是每调用1024次append函数时会去检查是否到了新的一天。可见这种方式还是有点问题的,因为可能存在到了新的一天但是没有达到1024次调用的情况,不过如果连1024次都没有达到,说明日志消息很少,也没有什么必要创建一个新的日志文件。此外,如果每次调用append都去判断是否是新的一天,那么每次都需要通过gmtime、gettimeofday这类的函数去获取时间,这样一来可能就显得得不偿失了。(在muduo中,由于是通过gmtime来获取时间的,因此会在0时区0时,即北京时间8时才算是”新的一天“)。

自动flush缓冲区

        为什么需要flush缓冲区?这是因为通过与日志文件交互的文件流是全缓冲的,只有当文件缓冲区满或者flush时才会将缓冲区中的内容写到文件中。而对于日志消息这种需要频繁写出的情况,如果不调用flush,那么就只有缓冲区满了才会将数据写出到文件中,如果进程突然崩溃,缓冲区中还未写出的数据就丢失了,而如果调用flush的次数过多,无疑又会影响效率。

        因此,muduo通过flushInterval变量来设置flush的间隔,默认为3s,即至少过3s才会自动flush,之所以说是”至少“,是因为判断间隔是否达到3秒,也需要调用时间获取函数去获取时间,如果每一次append都来判断一次,那么也是得不偿失的,因此,是否需要flush也是每append1024次再来进行判断。

 

开启异步日志功能

         通过前文可以知道,每一条日志消息实际上都是基于Logger类实现的,因此,要想实现异步日志,就需要将日志消息成功存入”前端缓冲区“,而这一点,只需要将Logger的g_output设置为AsyncLogging的append函数即可,如下所示:

muduo::AsyncLogging* g_asyncLog = NULL;

void asyncOutput(const char* msg, int len)  
{
  g_asyncLog->append(msg, len);
}

muduo::Logger::setOutput(asyncOutput);

         这样就可以将每条日志消息成功存储前端缓冲区,接着还需要开启后端线程,调用AsyncLogging类的start函数即可。

 

总结

        异步日志的实现,在Logger类的基础上,还需要AsyncLogging、LogFile、AppendFile类。

        其中AppendFile类用于将缓冲区数据写出到日志文件;

        LogFile中包含了AppendFile的实例,并且实现了滚动文件和自动flush缓冲区的功能;

        AsyncLogging包含了异步日志的前端和后端,前端与Logger相连接,通过Logger来获得每一条日志消息并进行存储,后端线程创建LogFile局部实例,从前端缓冲区中得到日志消息后通过LogFile局部实例将日志消息写出到日志文件中。

 

 

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

muduo源码学习(2):异步日志——异步日志的实现 的相关文章

  • aiohttp 异步http请求-1.快速入门 get 请求示例

    前言 在 python 的众多 http 请求库中 xff0c 大家最熟悉的就是 requests 库了 xff0c requests 库上手非常容易 xff0c 适合入门学习 如果平常工作中对发请求不追求效率和并发的情况下 xff0c r
  • Flask 学习-67.钩子函数before_request 和 before_first_request 的使用

    前言 学过pytest框架的肯定知道什么叫钩子 xff08 hook xff09 函数 钩子函数的作用是在程序运行的过程中插入一段代码做一些事情 四个钩子 请求钩子是通过装饰器的形式实现 xff0c Flask支持如下四种请求钩子 xff1
  • 30岁自学嵌入式找工作,可行吗?前景怎么样?

    大家好 xff0c 我是张巧龙 xff0c 在知乎上看到一个问题 xff1a 30岁自学嵌入式找工作 xff0c 可行吗 xff1f 看看一个高赞回答 xff1a 注 xff1a 以下内容不代表本公众号观点 xff0c 仅供参考 不可行 嵌
  • 0基础在ROS系统中实现RRT算法(四)URDF集成gazebo并搭建gazebo仿真环境

    小白一枚 xff0c 毕设突发奇想加入了ROS的内容 xff0c 不知道自己还能不能毕业 以下均为通过看视频 xff0c 翻博客等整理而成的笔记 xff0c 并非我的原创 可能会出现一些报错的修改或者简单的代码是我自己做的 哈哈 Gazeb
  • 如何在vscode中优雅的编写C语言

    如何在vscode中优雅的编写C语言 各位好 xff0c 我认为vscode编辑器在windows环境下除了Pycharm外是最方便的IDE了 xff0c 但在初学C语言时很少有人的第一个C语言软件使用的是vscode来编译与运行 xff0
  • Unity 使用RVO2(orca)算法

    RVO算法官方下载 https github com snape RVO2 CS git 官方版本的RVO只支持增加移动代理和障碍物 xff0c 不支持删除移动代理和障碍物 不太符合实际应用 我拓展了删除移动代理与障碍物的方法 示例项目 x
  • 51单片机串口通讯UART

    1 串行通信的的基本知识 在实际的工业生产 xff0c 或者生活中 xff0c 计算机的CPU要与外部的设备之间进行信息的交流 xff0c 数据的交换 xff0c 所有的这些信息交换均可称为通信 通信的方式有两种 xff0c 分别为串行通信
  • 库函数开发与寄存器开发

    在以前 8 位机时代的程序开发中 xff0c 一般直接配置芯片的寄存器 xff0c 控制芯片的工作方式 xff0c 如中断 xff0c 定时器等 配置的时候 xff0c 常常要查阅寄存器表 xff0c 看用到哪些配置位 xff0c 为了配置
  • Arduino修改Serial接收缓冲区大小

    看到网上有资料说 xff0c 直接添加以下宏定义就可以了 xff1a span class token macro property span class token directive keyword define span SERIAL
  • RT-Thread nmealib库WH-GN100模块设置仅支持北斗

    RT Thread nmealib库主页 在nmea thread init函数的末尾 xff0c 添加以下代码块 xff0c 发送配置指令 xff0c 仅使用北斗卫星 xff0c 即可配置成仅GPS卫星工作模式 span class to
  • C#中字符串判断为空或者空格

    最近遇到这个问题 xff0c 来大概说一下C 中字符串判断为空或者空格这个问题 xff08 1 xff09 字符串为空null xff0c 怎么讲就是内存中没有放东西 xff0c 比如新创建的字符串就为空null xff0c string
  • 【冷知识】火车票座位分布知识点

    最近到了每年过年 xff0c 春运火车高峰期的时候了 xff0c 有的人想知道自己具体的位置在哪里 xff08 比如硬座是不是靠窗的 xff0c 座位的大小号排序等 xff09 xff0c 现在来讲讲这方面的知识点 xff0c 个人整理 列
  • QT中的自定义信号以及自定义函数

    信号与槽函数是QT的一大创新 xff0c 通过自定义信号与槽函数可以实现自己想实现的功能 标准的信号与槽写法如下 xff1a connect amp button amp QPushButton clicked this amp QWidg
  • 如何摆放PCB元器件?(建议收藏)

    PCB设计 xff0c 既是科学也是艺术 其中有非常多关于布线线宽 布线叠层 原理图等等相关的技术规范 xff0c 但当你涉及到PCB设计中具有艺术特质元器件布局问题时 xff0c 问题就变得有趣起来了 事实上 xff0c 关于元器件摆放限
  • 【MFC开发(6)】复选框按钮控件Check Box

    1 新建复选框 直接拖拽即可 xff0c 设置名字可修改caption内容 2 设置默认选中 复选框可多选 xff0c 所以可以给很多复选框按钮进行选中 xff0c 代码如下所示 xff0c 放在dlg初始化函数中实现 获取多选框香蕉的指针
  • 【MFC开发(15)】进度条控件Progress Control

    1 进度条控件的常用方法 首先给控件添加一个变量 在dlg初始化函数钟进行方法的实现 进度条显示区域 设置进度条的范围 m progress SetRange 0 100 设置进度条当前的位置 m progress SetPos 75 获取
  • 【MFC开发(16)】树形控件Tree Control

    1 树形控件的属性配置 xff08 1 xff09 Check Boxes xff1a 默认为false xff0c 如果选择为true的话每个节点前面会带有一个方框 xff08 2 xff09 Edit Labels xff1a 默认为f
  • 【MFC开发(17)】高级列表控件List Control

    1 介绍 ListCtrl 高级列表控件也是我们平时编程过程中很常用的一个控件 xff0c 一般涉及到报表展示 记录展示之类的 xff0c 都需要ListCtrl 高级列表控件 例如 xff1a 任务管理器啊 xff0c 文件列表啊 xff
  • STM32L4单片机连接语音模块NVC的源码

    这周写了一下STM32L4的语音模块 xff0c 使用的语音芯片是NVC系列芯片 xff0c 提供一下代码给以后需要的朋友们 xff0c 不喜勿喷 头文件NVC h ifndef NVC H define NVC H 音源 define S
  • oled显示模块ssd1306

    管脚定义 GND 电源地 VCC xff1a 供电电源3 3v 5v都可以 D0 xff1a 串行输入时钟CLK D1 xff1a 串行输入数据 RES xff1a 复位 DC xff1a 控制输入数据 命令 xff08 高电平1为数据 低

随机推荐

  • 上位机串口数据检验方式(一)——校验和

    最近还是在写上位机软件 xff0c 还是有一堆问题 xff0c 因为是第一次做这个东西 xff0c 有些东西只能到论坛上来查 xff0c 最近做到了数据通信 xff0c 刚开始没有想到数据协议这些东西 xff0c 现在涉及到了 xff0c
  • c#上位机开发(三)——串口通信上位机开发1

    今天主要做一个跟市面上差不多的稍微简单点的上位机软件 xff0c 效果如下图所示 1 功能概述 xff08 1 xff09 端口扫描 xff0c 主要是扫描出可用的端口用来连接 xff08 2 xff09 波特率的选择 xff0c 使用一个
  • 使用python执行外部命令subprocess

    1 使用python执行外部命令subprocess subprocess模块是Python自带的模块 xff0c 无须再另行安装 xff0c 它主要用来取代一些旧的模块或方法 xff0c 如os system os spawn os po
  • #Qt on android#使用Qt 获取GPS信号

    注意事项 xff1a 1 Qt版本一定要大于等于5 3 xff0c 因为低于5 3的版本对于android系统来说并不能成功获取gps信号 2 环境正确搭建 xff0c 一定要注意 xff01 构建 xff08 build xff09 的系
  • 2023年TI杯全国大学生电子设计竞赛通知正式发布

    关于组织2023年 全国大学生电子设计竞赛的通知 xff08 电组字 2023 01号 xff09 各赛区组织委员会 各有关高等学校 xff1a 全国大学生电子设计竞赛 xff08 以下简称全国竞赛 xff09 组委会在认真总结往届电子设计
  • HTTPClient调用https请求,通过基本认证用户名密码(Basic Auth)

    本文来源是Apache官网例子 xff1a httpcomponents client ClientAuthentication java at 4 5 x apache httpcomponents client GitHub 之前找过很
  • c中结构体数据对齐问题

    1 为什么需要数据对齐 提升CPU读取数据的效率 CPU每次都是从以4字节 xff08 32位CPU xff09 或是8字节 xff08 64位CPU xff09 的整数倍的内存地址中读进数据的 xff08 例如32位的只能0x000000
  • js打开新窗口的方法总结

    Window open 方法 完整代码 window span class token punctuation span span class token function open span span class token punctu
  • 一文详解堆栈(二)——内存堆与内存栈

    前言 xff1a 我们经常听见一个概念 xff0c 堆 xff08 heap xff09 和栈 xff08 stack xff09 xff0c 其实在数据结构中也有同样的这两个概念 xff0c 但是这和内存的堆栈是不一样的东西哦 xff0c
  • 《动手学ROS2》3.2ROS2工作空间介绍

    本系列教程作者 xff1a 小鱼 公众号 xff1a 鱼香ROS QQ交流群 xff1a 139707339 教学视频地址 xff1a 小鱼的B站 完整文档地址 xff1a 鱼香ROS官网 版权声明 xff1a 如非允许禁止转载与商业用途
  • Ubuntu18.04 realsenseD435i深度摄像头外参标定的问题

    Ubuntu18 04 realsenseD435i深度摄像头外参标定的问题 鱼香ROS介绍 xff1a 鱼香ROS是由机器人爱好者共同组成的社区 xff0c 欢迎一起参与机器人技术交流 进群加V xff1a fishros2048 文章信
  • STM-32:USART串口协议、串口外设—数据发送/数据发送+接收

    目录 一 串口通信1 1通信接口1 2串口通信1 2 1简介1 2 2硬件电路1 2 3串口参数及时序 二 STM32的USART外设2 1USART简介2 2USART框图 三 数据传输3 1数据帧3 2输入数据策略3 2 1起始位侦测3
  • 大疆M3508、M2006必备CAN总线知识与配置方法

    大疆M3508 M2006必备CAN总线知识与配置方法 文章目录 大疆M3508 M2006必备CAN总线知识与配置方法前言 xff1a 0x00 需要 额外的 CAN收发器 xff01 xff01 xff01 0x01 硬件层面分析为什么
  • 换个角度聊聊PID吧,很干。

    01 前言 大家好 xff0c 前面发了几篇关于PID的文章 xff1a 点击图片即可阅读 教你10分钟完成智能小车的PID调速 快速调试PID参数的3种方法 02 自动控制系统 在直流有刷电机的基础驱动中 xff0c 如果电机负载不变 x
  • linux发起http请求,GET、POST

    GET请求 curl 推荐 curl v 34 https test com login username 61 tyw amp password 61 123 34 curl 34 https test com 34 URL指向的是一个文
  • VSCode对C++的DEBUG调试配置

    C 43 43 vscode上的调试配置 1 调试配置2 修改编译模式 按照本 的流程可在vscode平台上实现像在windows系统下VS调试C 43 43 程序的效果 1 调试配置 当写好代码和 CMakeLists txt 之后 xf
  • VSCode的C/C++扩展功能

    VSCode的C C 43 43 扩展功能 1 在 Linux 上 使用 C 43 43 1 1 创建 Hello World1 2 探索 IntelliSense1 3 构造 helloworld cpp1 3 1 运行 build1 3
  • 从源码理解智能指针(二)—— shared_ptr、weak_ptr

    目录 计数器 Ref count Ref count del Ref count del alloc Ptr base Ptr base的成员变量 构造函数 赋值重载 获取引用计数 减少引用计数 Reset函数 Resetw函数 share
  • muduo源码学习(1):异步日志——日志消息的存储及输出

    目录 前言 日志存储的实现 日志输出的实现 总结 前言 muduo中的日志 xff0c 是诊断日志 用于将代码运行时的重要信息进行保存 xff0c 方便故障诊断和追踪 日志一般有两种 xff0c 一种是同步日志 xff0c 一种是异步日志
  • muduo源码学习(2):异步日志——异步日志的实现

    目录 什么是异步日志 异步日志的实现 前端与后端 前端与后端的交互 资源回收 后端与日志文件 滚动日志 自动flush缓冲区 开启异步日志功能 总结 在前文中分析了日志消息的存储和输出 xff0c 不过并没有涉及到异步日志 xff0c 下面