日志系统——日志器模块

news2024/11/16 5:41:07

一,日志器模块主体实现

该模块主要是对前边所以模块的整合(日志等级模块,日志消息模块,日志格式化模块,日志落地模块),向外提供接口完成不同等级日志的输出。当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志

那么该模块需要哪些成员呢?

1.默认的日志输出等级(等级大于默认日志输出等级的日志才会输出)

2.格式化模块对象:在日志输出之前,对输出信息进行格式化处理是必须要有的

3.落地模块对象数组:输出日志时,日志输出方向是必不可少的,那么为什么我们这里是用数组呢,因为一个日志器可能会同时多个方向输出日志,例如标准输出,文件输出,滚动文件输出同时进行

4.互斥锁:由于我们的日志系统也支持异步日志器,因此可能会多线程访问,因此我们需要锁保证日志器是线程安全的

5.日志器的名字:日志器的名字是唯一标识,方便查找

 在实现方面,我们主要分为同步日志器(直接对⽇志消息进⾏输出)和异步日志器(将⽇志消息放⼊缓冲区,由异步线程进⾏输出),两个日志器只是落地方式不同,因此我们可以设计一个Logger基类,该基类完成大部分接口,然后抽象出落地方式接口,派生出同步日志器和异步日志器,由它们自己实现落地方式接口

 1.1 同步日志器

同步日志器非常简单,直接将内容按照落地方向输出即可,重难点在于异步日志器

 1.2 异步日志器

异步日志器指的是将格式化后的日志信息放入到一个缓冲区中,由专门的线程将缓冲区中的信息按照落地方式进行输出,这样业务线程就不会因为日志输出的原因阻塞,能够有效提高运行效率。

因此我们的异步日志器主要分为两大块:1.缓冲区 2.异步工作线程处理缓冲区的信息,同时为了进行解耦,所以我们将缓冲区和异步工作线程都单独封装一个类,最后由异步日志器类进行调度实现

1.2.1 缓冲区

那么这个缓冲区应该怎么设计呢?通常情况缓冲区主要是利用队列先进先出的优点的来实现,那么我们这个队列里面要用那种数据结构呢?首先我们要明确,我们用来充当缓冲区的队列尽量不要涉及到频繁的插入和删除操作,毕竟如果写入一个数据就进行插入,读取一个数据就进行删除那效率岂不是太低了

因此我们这里就产生了方案一:环形队列,确定队列的大小然后对空间循环利用,这样就尽量减少了队列的插入和删除操作

但是我们的缓冲区可能会涉及到多线程输入和读取此时输入线程和输入线程互斥,输入线程和读取线程互斥,读取线程和读取线程互斥,那么就产生了一个问题,我们保证线程安全需要加锁,各个线程直接都要抢这一把锁,那么锁冲突是否过于严重呢?答案是肯定的,那么怎么才能尽量减少锁的冲突?这里我们对缓冲区进行了一个双缓冲区的设计

 双缓冲区如上图所示,我们创建两个缓冲区,一个任务写入缓冲区(专门用来写入信息),一个任务处理缓冲区(专门用于输出信息),当任务写入缓冲区满,任务处理缓冲区空的时候,两个缓冲区进行交换,此时输入线程和读取线程的锁冲突的频率就有效的降低了

那么这个缓冲区类应该如何设计?我们对其成员变量的设计如下:

1. 设计一个存放字符串的缓冲区(这里我们选用vector的数据结构,如果用string,我们每条日志消息都是用'\0'结尾的,而string的大部分接口都是遇到该符号就结束了,因此这里不推荐同string来作为缓冲区)

2. 设计一个writer指针(实际上reader是整形,指向的是vector的下标),用来指向可写区域的初始位置,避免数据写入覆盖

3. 设计一个reader指针(实际上reader是整形,指向的是vector的下标),用来指向读取区域的初始位置,当reader指针和writer指向同一位置时,说明缓冲区没有可以读取的数据

而这个缓冲类主要提供以下接口:

1. 向缓冲区写入数据的接口

2. 获取可读数据地址的接口

3. 获取可读数据长度可写数据长度的接口

4. 移动读写指针的接口

5. 重置读写指针的接口(当缓冲区数据处理完毕时,缓冲区的读写指针需要重置)

6. 提供交换缓冲区的接口(直接交换空间地址)

7. 提供缓冲区的判空接口

1.2.2 异步工作线程

讲完缓冲区,就来到了异步日志器的第二个核心部分异步工作线程,根据双缓冲区的设计,异步工作线程的主要功能有两个:1.将任务添加到输入缓冲区 2. 从输出缓冲区中读取任务并且处理,而这个任务处理方式则由上层决定,也就是异步日志器决定

其实由此来看,异步工作线程其实就是一个生产消费模型生产者将任务添加到输入缓冲区,消费者从输出缓冲区中读出任务。

因此,异步工作线程类需要有以下成员变量

1. 双缓冲区(输入缓冲区,输出缓冲区)

2. 互斥锁,用于保证线程安全

3. 条件变量,实现输入缓冲区和输出缓冲区的同步(当输入缓冲区为空时,输出缓冲区会进入休眠状态等待输入缓冲区的唤醒)

4. 回调函数,用于处理输出缓冲区中的任务

5. 异步工作线程类的工作线程

而该类对外提供的接口有下面两个

1. 停止异步工作线程类

2. 添加数据到输入缓冲区

看到这,大家可能在向输出缓冲区的处理呢?先别急,下面就有了,输出缓冲区的处理没必要对外开放,因此和其他一些小接口放到了private中

1. 创建线程

2. 线程入口函数:先交换缓冲区,然后对输出缓冲区数据进行处理,处理后再次交换

1.2.3 异步日志器完成

在完成缓冲区和异步工作线程后,我们正式在完成异步日志器类,由于上两个模块的完成,异步日志器类仅需重写log,将其数据push入异步工作线程中,然后在写一个处理函数交给异步工作线程即可。

 完成代码后,我们发现创建一个logger类相当费劲,因为里面的成员变量需要我们一个一个创建

 

 mjwlog::Formatter::ptr _fptr(new mjwlog::Formatter());

    mjwlog::Sink::ptr stdout_ptr = mjwlog::SinkFactory::LogSink<mjwlog::StdoutSink>();
    mjwlog::Sink::ptr file_ptr = mjwlog::SinkFactory::LogSink<mjwlog::FileSink>("./logfile/test.log");
    // 以1m为分界线,进行滚动文件输出
    mjwlog::Sink::ptr rollfile_ptr = mjwlog::SinkFactory::LogSink<mjwlog::RollFileSink>("./logfile/test", 1024 * 1024);
    mjwlog::Sink::ptr timefile_ptr=mjwlog::SinkFactory::LogSink<mjwlog::TimeFileSink>("./logfile/test",mjwlog::time_seg::SECOND);
    std::vector<mjwlog::Sink::ptr> Sinks={file_ptr};
    std::string loggername="root";

    mjwlog::Logger::ptr logger(new mjwlog::SyncLogger(mjwlog::LogLevel::level::WARN,_fptr,Sinks,loggername));

这样做太麻烦了,因此我们需要完成一个建造者模式,用于logger的创建

 二,建造者模式

建造者模式用来构造日志器类,这样用户就不用一个一个创建日志器的各个模块,这样能够有效简化用户的构造流程以及日志器使用门槛。

建造者模式设计

日志器建造者模式分两个部分,日志器基类和日志器派生类

(用于构建局部日志器和全局日志器)

而日志器基类需要完成下面的三个需求:1.设置日志器类型(同步日志器,异步日志器) 。2.需要设计接口完成日志器各个模块的构造。 3.抽象一个建造接口,由子类完成日志器的建造

日志器派生类的有两个局部日志器和全局日志器,各自完成各自日志器的建造即可

 三,代码如下

 日志器头文件

#ifndef _M_LOGGER_H_
#define _M_LOGGER_H_
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "format.hpp"
#include "level.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "buffer.hpp"
#include "athread.hpp"
#include <atomic>
#include <mutex>
#include <stdarg.h>
#include <sstream>
#include <stdlib.h>

namespace mjwlog
{
    // 日志器基类
    class Logger
    {
    public:
        using ptr = std::shared_ptr<Logger>;
        Logger(const LogLevel::level &DefaultLevel,
               Formatter::ptr &Format,
               std::vector<Sink::ptr> &Sinks,
               std::string &LoggerName)
            : _DefaultLevel(DefaultLevel),
              _Format(Format),
              _Sinks(Sinks.begin(), Sinks.end()),
              _LoggerName(LoggerName)
        {
        }

        void Debug(const std::string &filename, const size_t &line, const std::string &fmt, ...)
        {
            // 判断是否需要输出,然后构造日志消息对象,将对象进行格式化,然后log输出

            // 1.判断是否大于_DefaultLevel
            if (LogLevel::level::DEBUG < _DefaultLevel)
                return;

            // 2.获取日志信息主体
            va_list vl;
            va_start(vl, fmt);
            char *res;
            size_t ret = vasprintf(&res, fmt.c_str(), vl);
            if (ret == -1)
            {
                std::cout << "vasprintf fail!" << std::endl;
                return;
            }
            va_end(vl);

            // 3.构建日志信息对象
            message msg(line, LogLevel::level::DEBUG, filename, _LoggerName, res);

            // 4.用格式化对象对日志信息进行格式化
            std::stringstream sti;
            _Format->format(sti, msg);

            // 5.调用log进行输出
            Log(sti.str().c_str(), sti.str().size());

            // 调用vasprintf后,系统会在堆上面开辟空间,并且使res指向该空间,因此到最后需要释放
            free(res);
        }
        void Info(const std::string &filename, const size_t &line, const std::string &fmt, ...)
        {
            if (LogLevel::level::INFO < _DefaultLevel)
                return;

            va_list vl;
            va_start(vl, fmt);
            char *res;
            size_t ret = vasprintf(&res, fmt.c_str(), vl);
            if (ret == -1)
            {
                std::cout << "vasprintf fail!" << std::endl;
                return;
            }
            va_end(vl);

            message msg(line, LogLevel::level::INFO, filename, _LoggerName, res);

            std::stringstream sti;
            _Format->format(sti, msg);

            Log(sti.str().c_str(), sti.str().size());

            free(res);
        }
        void Warn(const std::string &filename, const size_t &line, const std::string &fmt, ...)
        {
            if (LogLevel::level::WARN < _DefaultLevel)
                return;

            va_list vl;
            va_start(vl, fmt);
            char *res;
            size_t ret = vasprintf(&res, fmt.c_str(), vl);
            if (ret == -1)
            {
                std::cout << "vasprintf fail!" << std::endl;
                return;
            }
            va_end(vl);

            message msg(line, LogLevel::level::WARN, filename, _LoggerName, res);

            std::stringstream sti;
            _Format->format(sti, msg);

            Log(sti.str().c_str(), sti.str().size());

            free(res);
        }
        void Error(const std::string &filename, const size_t &line, const std::string &fmt, ...)
        {
            if (LogLevel::level::ERROR < _DefaultLevel)
                return;

            va_list vl;
            va_start(vl, fmt);
            char *res;
            size_t ret = vasprintf(&res, fmt.c_str(), vl);
            if (ret == -1)
            {
                std::cout << "vasprintf fail!" << std::endl;
                return;
            }
            va_end(vl);

            message msg(line, LogLevel::level::ERROR, filename, _LoggerName, res);

            std::stringstream sti;
            _Format->format(sti, msg);

            Log(sti.str().c_str(), sti.str().size());

            free(res);
        }
        void Fatal(const std::string &filename, const size_t &line, const std::string &fmt, ...)
        {
            if (LogLevel::level::FATAL < _DefaultLevel)
                return;

            va_list vl;
            va_start(vl, fmt);
            char *res;
            size_t ret = vasprintf(&res, fmt.c_str(), vl);
            if (ret == -1)
            {
                std::cout << "vasprintf fail!" << std::endl;
                return;
            }
            va_end(vl);

            message msg(line, LogLevel::level::FATAL, filename, _LoggerName, res);

            std::stringstream sti;
            _Format->format(sti, msg);

            Log(sti.str().c_str(), sti.str().size());

            free(res);
        }

    protected:
        // 落地方向抽象接口
        virtual void Log(const char *data, size_t len) = 0;

    protected:
        std::atomic<LogLevel::level> _DefaultLevel; // 默认输出等级,因为level本质为int型,因此可以用atomic构建原子类,保证线程安全
        Formatter::ptr _Format;                     // 日志格式化对象
        std::vector<Sink::ptr> _Sinks;              // 日志落地模块对象数组
        std::mutex _mutex;
        std::string _LoggerName;
    };

    // 同步日志器
    class SyncLogger : public Logger
    {
    public:
        SyncLogger(const LogLevel::level &DefaultLevel,
                   Formatter::ptr &Format,
                   std::vector<Sink::ptr> &Sinks,
                   std::string &LoggerName)
            : Logger(DefaultLevel, Format, Sinks, LoggerName)
        {}

    protected:
        void Log(const char *data, size_t len)
        {
            std::unique_lock<std::mutex> lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
            if (_Sinks.empty())
                return;
            for (auto &sink : _Sinks)
            {
                sink->log(data, len);
            }
        }
    };

    //异步日志器
    class AsyncLogger : public Logger
    {
    public:
        AsyncLogger(const LogLevel::level &DefaultLevel,
                   Formatter::ptr &Format,
                   std::vector<Sink::ptr> &Sinks,
                   std::string &LoggerName,
                   mjwlog::BufferMode buffermode)
            : Logger(DefaultLevel, Format, Sinks, LoggerName),
            _athread(std::make_shared<Athread>(std::bind(&AsyncLogger::task_deal,this,std::placeholders::_1),buffermode))
        {}

    protected:
        void task_deal(Buffer con_buffer)
        {
            std::unique_lock<std::mutex> lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
            if (_Sinks.empty())
                return;
            for (auto &sink : _Sinks)
            {
                sink->log(con_buffer.AbleReadData(), con_buffer.AbleReadSize());
            }
        }

        void Log(const char *data, size_t len)
        {
            _athread->push(data,len);
        }
        
    private:
        Athread::ptr _athread;
    };

    //同步日志器,异步日志器枚举
    enum class type
    {
        SyncLogger=0,
        AsyncLogger
    };

    //建造者基类
    class LoggerBuild
    {
        public:
        //type和DefaultLevel以及buffermode给个默认值
        LoggerBuild()
            :_LoggerType(type::AsyncLogger),
            _DefaultLevel(LogLevel::level::DEBUG),
            _buffermode(BufferMode::SAFE)
        {}

        void BuildDefaultLevel(const LogLevel::level& DefaultLevel)
        {
            _DefaultLevel=DefaultLevel;
        } 

        void BuildFormat(const std::string& pattern)
        {
            _Format=std::make_shared<Formatter>(pattern);
        }

        void BuileUnsafeBuffer()
        {
            _buffermode=BufferMode::UNSAFE;
        }
        template <typename SinkDirection, typename... Args>
        void BuildSink(Args &&...args)
        {
            Sink::ptr pt=std::make_shared<SinkDirection>(std::forward<Args>(args)...);;
            _Sinks.push_back(pt);
        }
        void BuildLoggerName(const std::string& LoggerName)
        {
            _LoggerName=LoggerName;
        }
        void BuildLoggerType(const type& LoggerType)
        {
            _LoggerType=LoggerType;
        }
        
        virtual Logger::ptr build()=0;
        protected:
        BufferMode _buffermode; 
        type _LoggerType;
        LogLevel::level _DefaultLevel;
        Formatter::ptr _Format;                     
        std::vector<Sink::ptr> _Sinks;              
        std::string _LoggerName;
    };

    //局部日志器派生类
    class LocalLoggerBuild:public LoggerBuild
    {
        public:
        Logger::ptr build() override
        {
            //1.日志器名字不能为空
            assert(!_LoggerName.empty());

            //2._Format如果为空,我们就用格式化输出模块的默认参数构建
            if(!_Format)
            {
                _Format=std::make_shared<Formatter>();
            }

            //3._Sinks为空,我们就默认加入一个标准输出的落地方式
            if(_Sinks.empty())
            {
                Sink::ptr pt=std::make_shared<StdoutSink>();
                _Sinks.push_back(pt);
            }

            //分同步日志器和异步日志期进行构造
            if(_LoggerType==type::AsyncLogger)
            {
                std::make_shared<AsyncLogger>(_DefaultLevel,_Format,_Sinks,_LoggerName,_buffermode);
            }
            
            return std::make_shared<SyncLogger>(_DefaultLevel,_Format,_Sinks,_LoggerName);
            
        }
    };
}

#endif

缓存区头文件

#ifndef _M_BUFFER_H_
#define _M_BUFFER_H_

#include <vector>
#include <iostream>
#include <cassert>
namespace mjwlog
{
    // 异步双缓冲区
    class Buffer
    {
    public:
#define _Default_Buffer_Size (2 * 1024 * 1024)    // 缓冲区默认大小
#define _Threshold_Buffer_Size (10 * 1024 * 1024) // 缓冲区阈值
#define _Growth_Buffer_Size (2 * 1024 * 1024)     // 超过阈值后线性增长

        Buffer()
            : _buffer(_Default_Buffer_Size),
              _writer(0),
              _reader(0)
        {
        }
        // 写入数据
        void push(const char *data, size_t len)
        {
            // 1.判断是否需要扩容
            Expansion(len);

            // 2.copy数据到buffer
            std::copy(data, data + len, _buffer.begin() + _writer);

            // 3.移动writer指针
            MoveWriter(len);
        }
        // 获取可读数据地址
        const char *AbleReadData()
        {
            return &_buffer[_reader];
        }

        // 获取可读数据长度
        size_t AbleReadSize()
        {
            return _writer - _reader;
        }

        // 获取可写数据长度
        size_t AbleWriteSize()
        {
            return _buffer.size() - _writer;
        }

        // 移动读写指针
        void MoveReader(size_t len)
        {
            assert((_reader + len) <= _writer);
            _reader += len;
        }

        // 重置读写指针
        void ResetPos()
        {
            _writer = 0;
            _reader = 0;
        }

        // 缓冲区交换接口
        void BufferSwap(Buffer &buffer)
        {
            _buffer.swap(buffer._buffer);
            std::swap(_writer, buffer._writer);
            std::swap(_reader, buffer._reader);
        }

        // 判断缓冲区是否为空
        bool Empty()
        {
            return _writer == _reader;
        }

    private:
        void MoveWriter(size_t len)
        {
            assert((_writer + len) <= _buffer.size());
            _writer += len;
        }
        void Expansion(size_t len)
        {
            // 这里我们采用循环扩容,因此有可能写入信息很长,扩容一次空间也不够
            while (AbleWriteSize() < len)
            {
                // 扩容策略:当我们buffer的容量小于阈值,双倍扩容;大于时,则线性扩容
                if (_buffer.size() < _Threshold_Buffer_Size)
                {
                    _buffer.resize(_buffer.size() * 2);
                }
                else
                {
                    _buffer.resize(_buffer.size() + _Growth_Buffer_Size);
                }
            }
        }

    private:
        std::vector<char> _buffer; // 缓冲区
        size_t _writer;            // 写入指针
        size_t _reader;            // 读取指针
    };
}

#endif

异步工作线程类头文件

#ifndef _M_ASYNT_H_
#define _M_ASYNT_H_
#include "buffer.hpp"
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>

namespace mjwlog
{
    // 枚举类,用来选择安全模式,和极限模式
    enum class BufferMode
    {
        SAFE = 1, // 安全模式,缓冲区固定大小,缓冲区满了阻塞
        UNSAFE    // 非安全模式,主要用于测试日志系统的工作效率
        // 缓冲区可以无限扩容,不过可能因为资源耗尽导致关闭
    };
    using func_t = std::function<void(Buffer &)>;

    class Athread
    {
    public:
        using ptr = std::shared_ptr<Athread>;
        Athread(func_t deal_task, BufferMode buffermode)
            : _buffermode(buffermode),
              _stop(false),
              _deal_task(deal_task),
              _thread(std::thread(&Athread::threadEntry, this))

        {
        }
        ~Athread()
        {
            Stop();
            _thread.join();
        }
        void Stop()
        {
            _stop = true;
            _con_cond.notify_all(); // 唤醒threadEntry进行业务处理
        }

        void push(const char *data, size_t len)
        {
            // 1.加锁
            std::unique_lock<std::mutex> ul(_mutex);

            // 2.判断输入缓冲区空间是否足够
            // 2.1 安全模式需要条件变量等待,判断空间是否足够
            // 2.2 极限模式无需判断直接写入即可
            if (_buffermode == BufferMode::SAFE)
            {
                _pro_cond.wait(ul, [&](){ return _pro_buffer.AbleWriteSize() >= len; });
            }

            // 3.缓冲区写入数据
            _pro_buffer.push(data, len);

            // 4.唤醒工作线程
            _con_cond.notify_one();
        }

    private:
        void threadEntry() // 线程入口函数,用于处理输出缓冲区
        {
            while (1)
            {
                // 1.交换缓冲区
                // 局部作用域用于交换缓冲区,该局部作用域目的是,交换完出作用域,锁就自动归还
                {
                    std::unique_lock<std::mutex> ul(_mutex);
                    _con_cond.wait(ul, [&](){ return _stop || !_pro_buffer.Empty(); });
                    
                    //当业务线程处于停止状态,并且输出缓冲区清空后则break,不然输出缓冲区可能会有数据没有输出就退出了
                    if(_stop&&_con_buffer.Empty())
                    {
                        break;
                    }
                    // 交换缓冲区
                    _con_buffer.BufferSwap(_pro_buffer);
                    

                    //安全模式下,需要唤醒输入缓冲区
                    if(_buffermode==BufferMode::SAFE)
                    {
                        _pro_cond.notify_all();
                    }
                }

                // 2.任务处理函数处理输入缓冲区的信息
                _deal_task(_con_buffer);

                // 3.重置_con_buffer的读写指针
                _con_buffer.ResetPos();
            }
        }

    private:
        BufferMode _buffermode;
        bool _stop;
        Buffer _pro_buffer; // 输出缓冲区(生产)
        Buffer _con_buffer; // 输入缓冲区(消费)
        std::mutex _mutex;
        std::condition_variable _pro_cond; // 生产的条件变量
        std::condition_variable _con_cond; // 消费的条件变量
        func_t _deal_task;                 // 任务处理
        std::thread _thread;
    };
}

#endif

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

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

相关文章

【JVM基础】JVM入门基础

目录 JVM的位置三种 JVMJVM体系结构类加载器双亲委派机制概念例子作用 沙箱安全机制组成沙箱的基本组件 NativeJNI&#xff1a;Java Native Interface&#xff08;本地方法接口&#xff09;Native Method Stack&#xff08;本地方法栈&#xff09; PC寄存器&#xff08;Program…

使用yarn build 打包vue项目时静态文件或图片未打包成功

解决Vue项目使用yarn build打包时静态文件或图片未打包成功的问题 1. 检查vue.config.js文件 首先&#xff0c;我们需要检查项目根目录下的vue.config.js文件&#xff0c;该文件用于配置Vue项目的打包和构建选项。在这个文件中&#xff0c;我们需要确认是否正确地配置了打包输…

个性定制还是纯粹简约:探寻界面选择背后的心理宇宙

在数码世界中&#xff0c;我们的界面选择成为了一张架起的桥梁&#xff0c;连接着个性的渴望与效率的追求。当我们面对个性化定制界面和极简版原装界面&#xff0c;我们仿佛站在了一座分岔路口&#xff0c;左右各有一片令人心驰神往的风景。究竟是走向五光十色的个性世界&#…

文件文档在线预览转换解决方案和应用

文章目录 Java Word转PDF文件方案评测一、kkFileView应用场景一&#xff1a;官网原始部署与应用二、kkFileView应用场景二&#xff1a;编译、自定义制作docker镜像部署三、kkfileview预览pdf文件以及关键词高亮和定位 Java Word转PDF文件方案评测 Word转PDF网上常见的方案5种&…

【Unity】【Amplify Shader Editor】ASE入门系列教程第一课 遮罩

新建材质 &#xff08;不受光照材质&#xff09; 贴图&#xff1a;快捷键T 命名&#xff1a; UV采样节点&#xff1a;快捷键U 可以调节主纹理的密度与偏移 添加UV流动节点&#xff1a; 创建二维向量&#xff1a;快捷键 2 遮罩&#xff1a;同上 设置shader材质的模板设置 添加主…

当服务器中了勒索病毒以后该怎么办?勒索病毒解密,数据恢复

无论对于什么体量的企业而言&#xff0c;数据都是企业的命。没有了数据就连正常的生产经营都没有办法做到。也正是因为如此&#xff0c;才会有一些黑客专门攻击企业的服务器&#xff0c;并将数据进行加密&#xff0c;以此来达到勒索赎金的目的&#xff0c;这就是很多企业有可能…

大同趋势,龙头股的自我修养-神奇指标网

入市的投资者总能听到一句话&#xff0c;历史会重演但不会简单重演。 技术分析是相同的道理&#xff0c;当我们关注历史上发生过的行情&#xff0c;我们对于技术的理解自然越近的周期我们印象越深&#xff0c;感性认识越强烈&#xff0c;而市场趋势就是由投资者推动的&#xf…

Scratch 之 如何打包 Scratch 成为可执行文件?

各位好&#xff0c;这期文章教学中&#xff0c;我将教大家如何打包Scratch文件(.sb3)为可执行文件(.exe)或网页文件(*.htm; *.html)。 另附&#xff1a;本期文章中有个可以转换的好网站 首先&#xff0c;还是一如既往打开Turbowarp Packager&#xff1a; 网址&#xff1a;htt…

介绍下杭州聚会的小伙伴们

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 2023年8月17日至18日&#xff0c;卢松松在杭州举办了一次创业者小聚会。 这次杭州之行&#xff0c;卢松松不仅见到了阿里巴巴的朋友&#xff0c;也见到了支付宝的朋友&#xff0c;还参观了支付宝合…

Android沉浸式实现(记录)

沉浸式先看效果 直接上代码 Android manifest文件 android:theme="@style/Theme.AppCompat.NoActionBar"布局文件 <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&qu…

FinalShell报错:Swap file “.docker-compose.yml.swp“ already exists

FinalShell中编辑docker-compose.yml文件&#xff0c;保存时报错&#xff1a;Swap file ".docker-compose.yml.swp" already exists&#xff1b;报错信息截图如下&#xff1a; 问题原因&#xff1a;有人正在编辑docker-compose.yml文件或者上次编辑没有保存&#xff…

【Python爬虫】使用代理ip进行网站爬取

前言 使用代理IP进行网站爬取可以有效地隐藏你的真实IP地址&#xff0c;让网站难以追踪你的访问行为。本文将介绍Python如何使用代理IP进行网站爬取的实现&#xff0c;包括代理IP的获取、代理IP的验证、以及如何把代理IP应用到爬虫代码中。 1. 使用代理IP的好处 在进行网站爬…

使用 uniapp 适用于wx小程序 - 实现移动端头部的封装和调用

图例&#xff1a;红框区域&#xff0c;使其标题区与胶囊对齐 一、组件 navigation.vue <template><view class"nav_name"><view class"nav-title" :style"{color : props.color, padding-top : toprpx,background : props.bgColor,he…

嵌入式软件开发工具简化基于STM8的智能装置开发

嵌入式软件开发工具简化基于STM8的智慧装置开发 降低功耗一直是微控器(MCU)市场的主要关注焦点。超低功耗MCU现在可大幅降低主动和深度睡眠的功耗。此种变化的效果是显而易见的&#xff0c;因为它大幅提高了日常嵌入式应用的电池寿命&#xff0c;以及在未来使用能量采集的可能性…

恒运资本股市资讯:IPO、再融资节奏生变

A股近期呈现震荡&#xff0c;商场对IPO、再融资节奏的重视度进步&#xff0c;一些投行人士已经感触到了股权融资商场正在发生改变。某券商投行人士表明&#xff0c;在当时二级商场震荡加大的背景下&#xff0c;IPO和再融资的审阅有放缓趋势&#xff0c;尤其是部分职业的审阅速度…

sap 获取不同币种间汇率 rfc BAPI_EXCHANGERATE_GETDETAIL

不同 sap 日期格式略有不同&#xff0c;可以在 sm37 中查看 参考 https://www.sapcenter.cn/archive/post/428730824257605.html

Compressor For Mac强大视频编辑工具 v4.6.5中文版

Compressor for Mac是苹果公司推出的一款视频压缩工具&#xff0c;可以将高清视频、4K视频、甚至是8K视频压缩成适合网络传输或存储的小文件。Compressor支持多种视频格式&#xff0c;包括H.264、HEVC、ProRes和AVC-Intra等&#xff0c;用户可以根据需要选择不同的压缩格式。 …

Hugo发布网站

你应该先阅读Windows上安装Hugo的环境。 我们使用PowerShell运行下面的Hugo命令。 1 创建网站 我们在文档下面创建一个名为MyHugoSite的目录结构&#xff1a; cd Documents hugo new site MyHugoSite cd MyHugoSite提示告诉我们有关主题的获取方式、文件的添加和站点的构建。…

恒运资本股票分析:跌停!1600亿“中字头”突发

周四上午&#xff0c;三大指数震动上行&#xff0c;到午间收盘&#xff0c;上证指数涨0.47%&#xff0c;深证成指涨1.14%&#xff0c;创业板指涨1.31%。北向资金半日净买入29.12亿元。此前&#xff0c;北向资金现已连续13个交易日减仓。8月以来&#xff0c;北向资金已累计净卖出…

【Leetcode】118.杨辉三角

一、题目 1、题目描述 给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中,每个数是它左上方和右上方的数的和。 示例1: 输入: numRows = 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]示例2: 输入: numRows = 1 输出: [[1]]提示: …