两种异步日志方案的介绍

news2024/11/18 11:18:13

文章目录

  • 一、日志写入逻辑
    • 1.1 相关接口函数
    • 1.2 写入逻辑
  • 二、log4cpp 日志框架
    • 2.1 下载和编译
    • 2.2 日志级别
    • 2.3 日志格式
    • 2.4 日志输出
    • 2.5 日志回滚
  • 三、muduo 异步日志库
    • 3.1 异步日志机制
    • 3.2 双缓冲机制
    • 3.3 前端日志写入
    • 3.4 后端日志落盘
    • 3.5 coredump 查找未落盘的日志
    • 3.6 总结

一、日志写入逻辑

1.1 相关接口函数

fwrite() 函数
用于将数据块按字节写入到文件中,返回实际成功写入的数据块个数。

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

参数说明:
ptr:指向要写入的数据块的指针。
size:每个数据块的字节数。
count:要写入的数据块个数。
stream:指向要写入的文件的指针。

使用 fwrite() 函数时,它会将 size 字节的数据块从 ptr 写入到指定的文件流 stream 中,并重复这个过程 count 次。

fread() 函数
用于从文件中按字节读取数据块,返回实际成功读取的数据块个数。

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

参数说明:
ptr:指向存储读取数据的内存块的指针。
size:每个数据块的字节数。
count:要读取的数据块个数。
stream:指向要读取的文件的指针。

使用 fread() 函数时,它会从指定的文件流 stream 中读取 size 字节的数据块,并重复这个过程 count 次,将读取的数据存储到 ptr 指向的内存块中。

fclose()函数
用于关闭打开的文件流,若成功关闭文件,则返回0;若关闭文件失败,则返回非零值。

int fclose(FILE *stream);

使用 fclose() 函数时,它会关闭指定的文件流,并刷新缓冲区中的数据。在关闭文件之前,它会将缓冲区中的数据写入到文件中。

fflush()函数
用于刷新输出缓冲区。若成功刷新缓冲区,则返回0;若刷新缓冲区失败,则返回非零值。

int fflush(FILE *stream);

参数说明:
stream:指向要刷新缓冲区的文件的指针。如果传入 NULL,则会刷新所有打开的文件流的缓冲区。

使用 fflush() 函数时,它会将输出缓冲区的内容立即写入到文件中(对于输出流)或者屏幕上(对于标准输出流 stdout)。这个函数通常用于确保数据被及时写入,而不是等到缓冲区满或者程序结束时才自动刷新。

fsync()函数
用于将文件数据同步到磁盘上的永久存储空间。若成功同步到磁盘,则返回0;若同步失败,则返回非零值。

int fsync(int fd);

参数说明:
fd:要同步到磁盘的文件描述符。

使用 fsync() 函数时,它会强制将文件缓冲区中的数据写入到磁盘上的永久存储空间。这个函数主要用于确保在系统崩溃或断电等异常情况下,文件数据不会丢失或损坏。
需要注意的是,fsync() 函数的调用可能会导致性能下降,因为它会强制进行磁盘写入操作。

setbuf()函数
用于在打开的文件上设置自定义的缓冲区。

void setbuf(FILE *stream, char *buffer);

参数说明:
stream:指向要设置缓冲区的文件的指针。
buffer:指向用于设置缓冲区的字符数组的指针。如果传入 NULL,则禁用缓冲区。

使用 setbuf() 函数时,它可以用于将自定义的字符数组作为缓冲区与文件关联。缓冲区提供了一种临时存储数据的方式,可以提高文件的读写性能。
需要注意的是,如果传入的 buffer 参数为 NULL,则会禁用缓冲区,即设置为无缓冲模式。在无缓冲模式下,每次写入或读取都会立即进行 I/O 操作,而不会暂存数据。这种模式适合于需要实时数据交换或者文件较小的情况。

write()函数
用于将数据写入文件或文件描述符的系统调用函数。成功时,返回写入的字节数。失败时,返回-1。

ssize_t write(int fd, const void *buf, size_t count);

参数说明:
fd:文件描述符,表示要写入的目标文件或设备。
buf:指向要写入数据的缓冲区的指针。
count:要写入的字节数。

write() 函数会尽可能将指定数量的字节从缓冲区 buf 写入到文件或设备中。它是一个阻塞函数,即在数据完全写入之前会一直等待。

1.2 写入逻辑

fwrite 与 write 的区别

  • fwrite() 是库函数,默认使用缓冲区,即将数据先写入缓冲区,可以使用 setbuf() 或 setvbuf() 函数自定义缓冲区。缓冲区可以提高写入效率,在遇到文件关闭、缓冲区满、调用 fflush() 等情况时会触发真正的写入操作,一次写入磁盘。
  • write() 函数 是系统调用,无缓冲的,数据直接写入到磁盘,涉及用户态和内核态的切换。

fwrite通过fflush(内部触发 write )把用户缓冲区数据刷新到内核缓冲区中,通过fsync把内核缓冲区数据刷新到磁盘。
在这里插入图片描述

二、log4cpp 日志框架

log4cpp是个基于LGPL的开源项⽬,移植⾃Java的⽇志处理跟踪项⽬log4j,并保持了API上的⼀致。log4cpp 性能很低,可以在客户端使用,不适合服务器端使用。Log4cpp不是批量写入模式,而是直接调用write() 进行实时写入,这样的性能就不会非常高。

Log4cpp中最重要概念有Category(种类)、Appender(附加器)、Layout(布局)、Priorty(优先级)、NDC(嵌套的诊断上下⽂)。
在这里插入图片描述

2.1 下载和编译

下载地址:
https://sourceforge.net/projects/log4cpp/files/latest/download

解压

tar zxf log4cpp-1.1.3.tar.gz

编译

cd log4cpp
./configure
make
make check
sudo make install
sudo ldconfig

默认安装路径:
/usr/local/include/log4cpp # 头文件
/usr/local/lib/liblog4cpp # 库文件

2.2 日志级别

log4cpp为例,具体的级别看具体的日志库
◼ EMERG
◼ FATAL
◼ ALERT
◼ CRIT
◼ ERROR
◼ WARN
◼ NOTICE
◼ INFO
◼ DEBUG
有些日志库 DEBUG和INFO的级别是反过来的。
日志输出级别应当在运行时可调,在必要的时候可以临时在线调整日志的输出级别。

muduo 库来说,调整日志的输出级别不需要重新编译,也不需要重启进程,只需要调用 muduo::Logger::setLoglevel()就能即时生效。

2.3 日志格式

一般日志输出时会携带一些关注的信息,比如

20210410 14:18:15.299684Z 30836 INFO NO.1506710 Root Error Message! - log_test.cpp:17

格式为:年月日 时分秒 微妙 时区 日志级别 日志内容 文件名 行号
Log4cpp支持的转义定义:
◼ %% - 转义字符’%’
◼ %c - Category
◼ %d - 日期;日期可以进一步设置格式,用花括号包围,例如%d{%H:%M:%S,%l}。日期的格式符号与ANSI C函数strftime中的一致。但增加了一个格式符号%l,表示毫秒,占三个十进制位。
◼ %m - 消息
◼ %n - 换行符;会根据平台的不同而不同,但对用户透明。
◼ %p - 优先级
◼ %r - 自从layout被创建后的毫秒数
◼ %R - 从1970年1月1日开始到目前为止的秒数
◼ %u - 进程开始到目前为止的时钟周期数
◼ %x - NDC
◼ %t - 线程id

muduo 库默认日志的格式是固定的,不需要运行时配置,这样可以节省每条日志解析格式字符串的开销,如果需要调整消息格式,直接修改代码重新编译即可

log4cpp 可以支持多种格式 Layout,如 SimpleLayout(简单布局), BasicLayout(基本布局)、 PatternLayout(格式化布局)。
1)BasicLayout::format
基本布局。它会为你添加“时间”、“优先级”、“种类”、“NDC”。相当于PatternLayout格式化为:“%R %p %c %x: %m%n”。

2) PassThroughLayout::format
直通布局。顾名思义,这个就是没有布局的“布局”,你让它写什么它就写什么,它不会为你添加任何东西,连换行符都懒得为你加。但是,它支持自定义的布局,我们可以继承他实现自定义的日志格式。

3) PatternLayout::format log4cpp
格式化布局,支持用户配置日志格式。它的使用方式类似C语言中的printf,使用格式化它符串来描述输出格式;目前支持的转义定义。

4) SimpleLayout::format
简单布局。比BasicLayout还简单的日志格式输出,它只会为你添加“优先级”的输出。相当于PatternLayout格式化为:“%p:%m%n”。

2.4 日志输出

日志有不同的输出方式,以log4cpp为例:
1)日志输出到控制台。ConsoleAppender。
2)日志输出到本地文件。FileAppender,值得注意的是,log4cpp使用write()来输出到文件,这种方式需要频繁的切换用户态和内核态,性能不高。这是一个可以优化的地方。
3)日志通过网络传输到远程服务器。RemoteSyslogAppender。

2.5 日志回滚

日志库一般都具备如下功能:
1)本地日志支持最大文件限制。
2)当本地日志达到最大文件限制的时候新建一个文件。
3)每天至少一个文件。

log4cpp回滚日志时,采用文件改名的方式,以文件名数字的大小表示创建的先后,数字越大文件越旧。比如若支持5个日志备份文件,有新的日志文件到来时,先删除最旧的log.5,然后把log.4改名为log.5,log.3改名为log.2,log.2改名为log.3,log.1改名为log.2,log.改名为log.1,最后新建一个log文件存储新的日志。

muduo 库的日志回滚没有采用文件改名,dmesg.log 始终是最新日志,便于编写及时解析日志的脚本

在性能方面,统计文件大小时,log4cpp 调用 lseek来实现,这导致其性能极低。而 muduo 库在应用层增加当前写入日志的大小参数。

三、muduo 异步日志库

3.1 异步日志机制

异步日志机制,是将日志写入操作放入一个独立的线程或者进程中来提高性能,而其他业务线程只需要往这个线程发送日志消息即可,避免因为日志写入操作导致的主线程阻塞。

通常,异步日志机制包含以下几个核心组件:
1)日志缓冲区(Log Buffer):用于临时存储日志消息的缓冲区。

2)日志队列(Log Queue):用于存放待写入日志文件的日志消息。

3)后台写入线程(Logging Thread):负责从日志队列中取出日志缓冲区,并将其中的日志消息写入到日志文件中。该线程通常是一个专门的线程或者进程,与主线程并发运行。

需要注意的是,这是一个典型的消费者生产者模型。生产者线程不是将日志消息逐条传递给消费者线程,而是积攒日志,将多条日志消息缓存组成一个大的buffer(默认4M·),作为队列的元素。等队列满了,再唤醒消费者线程,将日志批量写入磁盘。
在这里插入图片描述
因此,后端日志线程的唤醒有两个条件
1)buffer 写满唤醒:批量写入写满1个 buffer 后,唤醒后端日志线程,减少线程被唤醒的频率,降低系统开销。
2)超时被唤醒:为了及时将日志消息写入文件,防止系统故障导致内存中的日志消息丢失。超过规定的时间阈值,即使 buffer 未满,也会通过wait_timeout唤醒日志落盘线程,立即将 buffer 中的数据写入。

3.2 双缓冲机制

双缓冲机制,是准备两个缓冲区,bufferA 和 bufferB。前端负责向 A 中写入日志消息,后端负责将 B 中的数据写入文件。
当 bufferA 写满或者达到一定的时间间隔后,交换 A 和 B。此时让后端将 A 的数据写入文件,前端向 B 中写入新的日志消息,如此往复。这样在追加日志消息的时候不必等待磁盘 IO 操作,同时也避免了每条新日志消息都触发唤醒后端日志线程。

在这里插入图片描述

3.3 前端日志写入

在这里插入图片描述

AsyncLogging::append():前端生成一条日志消息。

前端准备一个前台缓冲区队列 buffers_和两个 buffer。前台缓冲队列 buffers_用来存放积攒的日志消息。两个 buffer,一个是当前缓冲区 currentBuffer,追加的日志消息存放于此;另一个作为当前缓冲区的备份,即预备缓冲区 nextBuffer,减少内存的开销。

函数执行逻辑如下:

判断当前缓冲区 currentBuffer_是否已经写满

  • 若当前缓冲区未满,追加日志消息到当前缓冲,这是最常见的情况
  • 若当前缓冲区写满,首先,把它移入前台缓冲队列 buffers_。其次,尝试把预备缓冲区 nextBuffer_移用为当前缓冲,若失败则创建新的缓冲区作为当前缓冲。最后,追加日志消息并唤醒后端日志线程开始写入日志数据。
int append_cnt = 0;
void AsyncLogging::append(const char *logline, int len) {
    // 1. 多线程加锁,线程安全
    std::lock_guard<std::mutex> lock(mutex_);
    // 2.判断是否写满buffer,批量数据的积攒阶段
    if (currentBuffer_->avail() > len) // 判断buffer还有没有空间写入这条日志
    {
        // 2.1 buffer未满直接写入buffer
        currentBuffer_->append(logline, len); // 直接写入
    } 
    // 2、当前缓冲写满,两件事
    // 其一,将写满的当前缓冲的日志消息写入前台缓冲队列 buffers
    // 其二,追加日志消息到当前缓冲,唤醒后台日志落盘线程
    else {
        // 其一,当前缓冲移入(move)前台缓冲队列 buffers
        buffers_.push_back(std::move(currentBuffer_)); // buffers_是vector,把buffer入队列
        // 判断预备缓冲nextBuffer是否写满,如果未满则复用,如果满了则重新分配
        if (nextBuffer_) // 用了双缓存
        {   
            // 未满,复用
            currentBuffer_ = std::move(nextBuffer_); // 如果不为空则将buffer转移到currentBuffer_
        } else {
            // 满了,重新分配buffer
            currentBuffer_.reset(new Buffer); // Rarely happens如果后端写入线程没有及时读取数据,那要再分配buffer
        }
        // 其二,追加日志信息到当前缓冲,唤醒日志落盘线程
        currentBuffer_->append(logline, len);
        cond_.notify_one(); // 唤醒日志落盘线程
    }
}

3.4 后端日志落盘

AsyncLogging::threadFunc():后端日志落盘线程的执行函数。

后端同样也准备了一个后台缓冲区队列 buffersToWrite 和两个备用 buffer。后台缓冲区队列 buffersToWrite 存放待写入磁盘的数据。两个备用 buffer,newBuffer1和newBuffer2,分别用来替换前台的当前缓冲和预备缓冲,而这两个备用 buffer 最后会被buffersToWrite内的两个 buffer 重新填充,减少了内存的开销。

参考 异步日志模块的实现

void AsyncLogging::threadFunc() {
  assert(running_ == true);
  latch_.countDown();
  // logFile 类负责将数据写入磁盘
  LogFile output(basename_, rollSize_, false);

  BufferPtr newBuffer1(new Buffer); // 用于替换前台的当前缓冲 currentbuffer
  BufferPtr newBuffer2(new Buffer); // 用于替换前台的预备缓冲 nextbuffer 
  newBuffer1->bzero();  
  newBuffer2->bzero();

  BufferVector buffersToWrite;	// 后台缓冲队列
  buffersToWrite.reserve(16);   // 两个不同的缓冲队列,涉及到锁的粒度问题
  
  // 异步日志开启,则循环执行
  while (running_) {
    assert(newBuffer1 && newBuffer1->length() == 0);
    assert(newBuffer2 && newBuffer2->length() == 0);
    assert(buffersToWrite.empty());

    // <---------- 交换前台缓冲队列和后台缓冲队列 ---------->
    { // 锁的作用域,放在外面,锁的粒度就大了,日志落盘的时候都会阻塞 append
      // 1、多线程加锁,线程安全,注意锁的作用域
      MutexLockGuard lock(mutex_);

      // 2、判断前台缓冲队列 buffers 是否有数据可读
      // buffers 没有数据可读,休眠
      if (buffers_.empty()) {
        // 触发日志的落盘 (唤醒) 的两个条件:1.超时 or 2.被唤醒,即前台写满 buffer
        cond_.waitForSeconds(flushInterval_); // 内部封装 pthread_cond_timedwait
      }

      // 只要触发日志落盘,不管当前的 buffer 是否写满都必须取出来,写入磁盘
      // 3、将当前缓冲区 currentbuffer 移入前台缓冲队列 buffers。
      // currentbuffer 被锁住 -> currentBuffer 被置空  
      buffers_.push_back(std::move(currentBuffer_)); 
      
      // 4、将空闲的 newbuffer1 移为当前缓冲,复用已经分配的空间
      currentBuffer_ = std::move(newBuffer1); // currentbuffer 需要内存空间

      // 5、核心:把前台缓冲队列的所有buffer交换(互相转移)到后台缓冲队列 
      // 这样在后续的日志落盘过程中不影响前台缓冲队列的插入
      buffersToWrite.swap(buffers_);      

      // 若预备缓冲为空,则将空闲的 newbuffer2 移为预备缓冲,复用已经分配的空间
      // 这样前台始终有一个预备缓冲可供调配
      if (!nextBuffer_) { 
        nextBuffer_ = std::move(newBuffer2);  
      }
    } // 注意这里加锁的粒度,日志落盘的时候不需要加锁了,主要是双队列的功劳

    // <-------- 日志落盘,将buffersToWrite中的所有buffer写入文件 -------->
    assert(!buffersToWrite.empty());

    // 6、异步日志消息堆积的处理。
    // 同步日志,阻塞io,不存在堆积问题;异步日志,直接删除多余的日志,并插入提示信息
    if (buffersToWrite.size() > 25) {
      printf("Dropped\n");
      // 插入提示信息
      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)));
      // 只保留2个buffer(默认4M)
      buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end());   
    }

    // 7、循环写入 buffersToWrite 的所有 buffer
    for (const auto& buffer : buffersToWrite) {
      // 内部封装 fwrite,将 buffer中的一行日志数据,写入用户缓冲区,等待写入文件
      output.append(buffer->data(), buffer->length());  
    }

    // 8、刷新数据到磁盘文件?这里应该保证数据落到磁盘,但事实上并没有,需要改进 fsync
    // 内部调用flush,只能将数据刷新到内核缓冲区,不能保证数据落到磁盘(断电问题)
    output.flush();   
   
    // 9、重新填充 newBuffer1 和 newBuffer2
    // 改变后台缓冲队列的大小,始终只保存两个 buffer,多余的 buffer 被释放
    // 为什么不直接保存到当前和预备缓冲?这是因为加锁的粒度,二者需要加锁操作
    if (buffersToWrite.size() > 2) {
       // 只保留2个buffer,分别用于填充备用缓冲 newBuffer1 和 newBuffer2
      buffersToWrite.resize(2);  
    }
    // 用 buffersToWrite 内的 buffer 重新填充 newBuffer1
    if (!newBuffer1) {
      assert(!buffersToWrite.empty());
      newBuffer1 = std::move(buffersToWrite.back()); // 复用 buffer
      buffersToWrite.pop_back();
      newBuffer1->reset();    // 重置指针,置空
    }
    // 用 buffersToWrite 内的 buffer 重新填充 newBuffer2
    if (!newBuffer2) {
      assert(!buffersToWrite.empty());
      newBuffer2 = std::move(buffersToWrite.back()); // 复用 buffer
      buffersToWrite.pop_back();
      newBuffer2->reset();   // 重置指针,置空
    }

    // 清空 buffersToWrite
    buffersToWrite.clear();  
  }
  
  // 存在问题
  output.flush();
}

3.5 coredump 查找未落盘的日志

muduo异步日志——core dump查找未落盘的日志

3.6 总结

如何实现高性能的日志

1)批量写入:同步方式通过积攒一定的数据(如4M)或者设定超时时间,以此触发写入。如glog日志库。异步方式通过append积攒数据,异步落盘线程负责数据写入磁盘,如moduo日志库。

2)唤醒机制:通知唤醒 notify + 超时唤醒 wait_timeout

3)锁的粒度:为减少锁的粒度,减少刷新磁盘的时候日志接口阻塞,采用双队列方式。前台队列实现日志接口,后台队列实现刷新磁盘。

4)内存分配:通过move语义,避免深拷贝;双缓冲,前台后台都设有。

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

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

相关文章

复习第六课 C语言-排序,初识指针

目录 【1】冒泡排序&#xff08;从小到大&#xff09; 【2】选择排序 【3】二维数组 【4】指针 【5】指针修饰 【6】大小端 【7】初见二级指针 练习&#xff1a; 【1】冒泡排序&#xff08;从小到大&#xff09; #include <stdio.h> //数组哪里的\0?自己和字符串…

论文阅读-2:基于深度学习的大尺度遥感图像建筑物分割研究

一、该网络中采用了上下文信息捕获模块。通过扩大感受野&#xff0c;在保留细节信息的同时&#xff0c;在中心部分进行多尺度特征的融合&#xff0c;缓解了传统算法中细节信息丢失的问题&#xff1b;通过自适应地融合局部语义特征&#xff0c;该网络在空间特征和通道特征之间建…

SSH框架简介篇

文章目录 概述目录结构 strutsSpringHibernate总结 概述 SSH框架&#xff08;Struts Spring Hibernate&#xff09;是一种广泛应用的Java企业级开发框架组合&#xff0c;它将Struts、Spring和Hibernate三个优秀的框架有机地结合在一起&#xff0c;提供了一套完整的解决方案&…

cmake 函数相关

目录 cmake函数和宏基础 demo cmake函数和宏的参数处理 cmake函数和宏的基本使用 demo cmake函数和宏使用变量 demo demo cmake函数和宏需要注意的地方 demo cmake函数和宏的关键字参数 demo 使用第二种形式cmake_parse_arguments() demo 关键字list demo singl…

GDB 调试代码

目录 一、其他调试代码的工具 二、GDB调试 1、调试准备 2、开始调试 3、调试命令 1.运行程序 2.退出gdb 3.传参 4.查看代码 5.设置或删除断点及相关操作 6.继续运行 7.运行中打印某些值及其类型 8.自动的打印某些值和信息及其相关操作 9.单步调试 10.设置变量的…

http-server 的安装与使用

文章目录 问题背景http-server简介安装nodejs安装http-server开启http服务http-server参数 问题背景 打开一个文档默认使用file协议打开&#xff0c;不能发送ajax请求&#xff0c;只能使用http协议才能请求资源&#xff0c;所以此时我们需要在本地建立一个http服务&#xff0c…

基于java的智能停车场管理系统

背景 智能停车场管理系统的主要使用者分为管理员和用户&#xff0c;实现功能包括管理员&#xff1a;个人中心、用户管理、车位信息管理、车位租用管理、车位退租管理、违规举报管理、论坛交流、系统管理&#xff0c;用户&#xff1a;个人中心、车位租用管理、车位退租管理、违…

MySQL每日一练——MySQL多表查询进阶挑战

目录 1、首先创建表 t_dept: t_emp: 2、插入数据 t_dept表&#xff1a; t_tmp表: 3、修改表 4、按条件查找 1、首先创建表 t_dept: CREATE TABLE t_dept (id INT(11) NOT NULL AUTO_INCREMENT,deptName VARCHAR(30) DEFAULT NULL,address VARCHAR(40) DEFAULT NULL,P…

为什么单片机可以直接烧录程序的原因是什么?

单片机&#xff08;Microcontroller&#xff09;可以直接烧录程序的原因主要有以下几点&#xff1a; 集成性&#xff1a;单片机是一种高度集成的芯片&#xff0c;内部包含了处理器核心&#xff08;CPU&#xff09;、存储器&#xff08;如闪存、EEPROM、RAM等&#xff09;、输入…

JavaScript 使用URL跳转传递数组对象数据类型的方法

文章目录 首先了解一下正常传递基本数据类型JavaScript 跳转页面方法JavaScript 路由传递参数JavaScript 路由接收参数传递对象、数组效果&#xff1a; 在前端有的时候会需要用链接进行传递参数&#xff0c;基本数据类型的传递还是比较简单的&#xff0c;但是如果要传递引用数据…

AWS 解决方案架构师「免费考」

周五晚&#xff0c;AWS 推出了的训练营活动&#xff0c;这对于正在准备 Cloud Practitioner 的我来说&#xff0c;简直不要太开心。官方文章原文链接《限定&#xff01;直冲「云」霄训练营开营啦》。 PART-01 训练营简介 看到推送后第一时间点了进去&#xff0c;活动的情况简…

Socket API使用——模拟http协议

Socket API使用——模拟http协议 简单的c/s程序——服务端实例 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.nio.char…

Python np.unique()函数详解

np.unique()函数详解&#xff1a;返回数组的唯一值唯一值默认已进行从小到大的排序 一些重要参数 return_index&#xff1a;bool, optional。如果设置为True,返回数组中唯一值的索引号&#xff1b;否则不返回。 注意&#xff1a;返回的数组和输入的数组的大小不相同&#xf…

第十五章——友元、异常

友元 类并非只能拥有友元函数&#xff0c;也可以将类作为友元。在这种情况下&#xff0c;友元类的所有方法都可以访问原始类的私有成员和保护成员。因此尽管友元被授予从外部访问类的私有部分的权限&#xff0c;但它们并不与面向对象的编程思想相悖&#xff0c;相反提高了共有…

《C++程序设计原理与实践》笔记 第20章 容器和迭代器

本章和下一章将介绍STL&#xff0c;即C标准库的容器和算法部分。关键概念序列和迭代器用于将容器&#xff08;数据&#xff09;和算法&#xff08;处理&#xff09;联系在一起。 20.1 存储和处理数据 首先考虑一个简单的例子&#xff1a;Jack和Jill各自在测量车速&#xff0c…

echarts x轴文字过长 文字换行显示

xAxis: {type: "category",data: [四美休闲娱乐文化场馆, 资讯, 大咖分享],axisLabel: {show: true,fontSize: 10,interval: 0,color: "#CAE8EA",formatter: function (params) {var newParamsName "";var paramsNameNumber params.length;var…

MySQL数据库基础 18

第18章_MySQL8其它新特性 1. MySQL8新特性概述1.1 MySQL8.0 新增特性1.2 MySQL8.0移除的旧特性 2. 新特性1&#xff1a;窗口函数2.1 使用窗口函数前后对比2.2 窗口函数分类2.3 语法结构2.4 分类讲解1. 序号函数2. 分布函数3. 前后函数4. 首尾函数5. 其他函数 2.5 小 结 3. 新特…

【讲座笔记】Continual Learning and Memory Augmentation with Deep Neural Networks

20230607【开放世界的感知&#xff1a;探索可迁移与可持续学习之路】巩东&#xff1a;Continual Learning and Memory Augmentation……_哔哩哔哩_bilibili 游荡……游荡……找个talk看一下 讲的是continuous learning&#xff08;好家伙缩写也是CL&#xff09; 1.continual l…

error: ‘CV_LOAD_IMAGE_UNCHANGED’ was not declared in this scope

1-错误 2-错误原因 opencv4.x以上&#xff0c;有些宏&#xff0c;API名字改了&#xff0c;需要改为新的 3-解决方案 CV_LOAD_IMAGE_UNCHANGED 改为 cv::IMREAD_UNCHANGEDCV_LOAD_IMAGE_GRAYSCALE 改为 cv::IMREAD_GRAYSCALECV_LOAD_IMAGE_COLOR 改为 cv::IMREAD_COLORCV_LO…

Win10,WinServer16,DNS,Web ,域 环境配置 周总结 (温故而知新 可以为师矣 第十五课)

Win10,WinServer16,DNS,Web ,域 环境安装 (第十五课) 创建虚拟机安装windowserver2016服务器(NETBASE第二课)_星辰镜的博客-CSDN博客 创建台虚拟机并安装上window10系统&#xff08;NETBASE 第一课&#xff09;_window 虚拟机_星辰镜的博客-CSDN博客配置通过域名访问网站(NET…