RPC分布式网络通信框架项目

news2024/11/20 14:05:32

文章目录

      • 对比单机聊天服务器、集群聊天服务器以及分布式聊天服务器
      • RPC通信原理
      • 使用Protobuf做数据的序列化,相比较于json,有哪些优点?
      • 环境配置使用
        • 项目代码工程目录
        • vscode远程开发Linux项目
        • muduo网络库编程示例
        • CMake构建项目集成编译环境
        • Linux环境下搭建muduo网络库
        • 网络I/O模型介绍
      • Protobuf安装配置
        • ubuntu protobuf环境搭建
      • protobuf 实践讲解
      • 如何将本地服务发布成rpc服务?

对比单机聊天服务器、集群聊天服务器以及分布式聊天服务器

(1) 单机聊天服务器
在这里插入图片描述
首先让我们来看单机聊天服务器会遇到的一些性能瓶颈的问题:

  1. 聊天服务器所能承受的用户的并发量受限于硬件资源;
  2. 任何模块的修改,都会导致整个项目代码重新进行编译和部署;
  3. 系统中,有些模块是属于CPU密集型的,有些模块是属于I/O密集型的,造成各模块对于硬件资源的需求是不相同的。所以,将各模块部署在一台服务器上时,没办法按照各模块对资源的需求部署在特定的硬件资源上。

(2)集群聊天服务器
在这里插入图片描述

集群聊天服务器相对于单机聊天服务器,做了哪些优化,还存在哪些性能瓶颈的问题?
优点:
用户的并发量提升、部署集群服务器比较简单。
缺点
1、项目代码还是需要整体重新编译,而且需要进行多次部署;
2、对于某些模块根本不需要高并发(比如上方服务器中的后台管理模块),而使用集群聊天服务器后,在每一台服务器上都部署了相同的模块,可能会造成服务器资源的浪费。

集群:每一台服务器独立运行一个工程的所有模块。
分布式:一个工程拆分了很多模块,每一个模块独立部署运行在一个或多台服务器主机上,所有服务器协同工作共同提供服务,每一台服务器称作分布式的一个节点,根据节点的并发要求,对一个节点可以再做节点模块集群部署。
在这里插入图片描述
当引入了分布式部署聊天服务器的概念后,我们把每个模块按需部署在一台或多台服务器上,使得服务器的资源利用更加充分。同时,如果只是改动了某一个模块的代码,不用将所有的服务器代码都重新编译和部署,只需要重新编译相对应的模块代码。

但是,引入分布式服务器后,会遇到哪些难题需要我们解决呢?

  1. 我们应该如何对大系统的软件模块进行划分呢?怎么划分才能使得各模块的重复代码更少呢?
  2. 当模块被部署在不同的服务器上时,各模块之间如何进行访问呢?比如下图的机器1上的模块如何去调用机器2上的模块的一个业务方法呢?机器1上的模块进程1怎么调用机器1上的模块进程2里面的一个业务方法呢?

在这里插入图片描述
第1个问题通过软件设计师的经验来进行模块的划分,而第2个问题则是通过我们设计的RPC分布式网络框架来实现远程方法的调用,使得客户端基于此框架,能让不同网络结点之间的服务调用像调用本地服务一样简单。


RPC通信原理

RPC(Remote Procedure Call Protocol)远程过程调用协议
在这里插入图片描述
黄色部分:设计rpc方法参数的打包和解析,也就是数据的序列化和反序列化,使用Protobuf
绿色部分:网络部分,包括寻找rpc服务主机,发起rpc调用请求和响应rpc调用结果,使用muduo网络库和zookeeper服务配置中心(专门做服务发现)。
mprpc框架主要包含以上两个部分的内容。


使用Protobuf做数据的序列化,相比较于json,有哪些优点?

  1. Protobuf 是使用二进制存储数据的,而xmljson都是文本存储的;
  2. Protobuf 不需要存储额外的信息,而json是以键值对的方式来存储数据。

环境配置使用

项目代码工程目录

bin:可执行文件
build:项目编译文件
lib:项目库文件
src:源文件
test:测试代码
example:框架代码使用范例
CMakeLists.txt:顶层的cmake文件
README.md:项目自述文件
autobuild.sh:一键编译脚本

vscode远程开发Linux项目
muduo网络库编程示例
CMake构建项目集成编译环境
Linux环境下搭建muduo网络库

muduo库的安装需要依赖boost库,具体安装配置步骤参考博客:
Linux环境下搭建muduo网络库

网络I/O模型介绍
  • accept + read/write
    不是并发服务器
  • accept + fork - process-pre-connection
    适合并发连接数不大,计算任务工作量大于fork的开销
  • accept + thread thread-pre-connection
    比方案2的开销小了一点,但是并发造成线程堆积过多
  • muduo的设计:reactors in threads - one loop per thread
    方案的特点是one loop per thread,有一个main reactor(I/O)负载accept连接,然后把连接分发到某个sub reactor(Worker),该连接的所用操作都在那个sub reactor所处的线程中完成,多个连接可能被分派到多个线程中,以充分利用CPU。
    如果有过多的耗费CPU I/O的计算任务,可以创建新的线程专门处理耗时的计算任务。
  • reactors in process - one loop pre process
    nginx服务器的网络模块设计,基于进程设计,采用多个Reactors充当I/O进程和工作进程,通过一把accept锁,完美解决多个Reactors的“惊群现象”。

Protobuf安装配置

protobuf(protocol buffer)是google 的一种数据交换的格式,它独立于平台语言。
google 提供了protobuf多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。
由于它是一种二进制的格式,比使用 xml(20倍) 、json(10倍)进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

ubuntu protobuf环境搭建

见项目资料下载地址或者在github源代码下载地址:https://github.com/google/protobuf
源码包中的src/README.md,有详细的安装说明,安装过程如下:
1、解压压缩包:unzip protobuf-master.zip
2、进入解压后的文件夹:cd protobuf-master
3、安装所需工具:sudo apt-get install autoconf automake libtool curl make g++ unzip
4、自动生成configure配置文件:./autogen.sh
5、配置环境:./configure
6、编译源代码(时间比较长):make
7、安装:sudo make install
8、刷新动态库:sudo ldconfig


protobuf 实践讲解

首先在服务端编写proto配置文件,比如测试代码中的test.proto,配置文件中包含需要需要进行序列化的请求消息和响应消息,以及在protobuf中定义了描述rpc方法的服务类型,传入相应的rpc请求方法名以及相应的参数,返回参数。

syntax = "proto3"; // 声明了protobuf的版本

package fixbug; // 声明了代码所在的包(对于C++来说是namespace)

// 定义下面的选项,表示生成service服务类和rpc方法描述,默认不生成
option cc_generic_services = true;

message ResultCode
{
    int32 errcode = 1;
    bytes errmsg = 2;
}

// 数据   列表   映射表
// 定义登录请求消息类型  name   pwd
message LoginRequest
{
    bytes name = 1;
    bytes pwd = 2;
}

// 定义登录响应消息类型
message LoginResponse
{
    ResultCode result = 1;
    bool success = 2;
}

message GetFriendListsRequest
{
    uint32 userid = 1;
}

message User
{
    bytes name = 1;
    uint32 age = 2;
    enum Sex
    {
        MAN = 0;
        WOMAN = 1;
    }
    Sex sex = 3;
}

message GetFriendListsResponse
{
    ResultCode result = 1;
    repeated User friend_list = 2;  // 定义了一个列表类型
}

// 在protobuf里面怎么定义描述rpc方法的类型 - service
service UserServiceRpc
{
    rpc Login(LoginRequest) returns(LoginResponse);
    rpc GetFriendLists(GetFriendListsRequest) returns(GetFriendListsResponse);
}

通过在终端中执行protoc test.proto --cpp_out=./即可在当前文件夹中生成test.pb.cctest.pb.h,其中massage以及service生成了相应的类,且都是通过继承得到的,class LoginRequest: public::google::protobuf::Message,而UserServiceRpc服务生成了两个类:class UserServiceRpc: public google::protobuf::Service提供给服务提供者进行调用、class UserService_Stub: public UserServiceRpc提供给服务消费者进行调用。如下所示:
在这里插入图片描述!在这里插入图片描述
在服务提供者方,由于生成的UerServiceRpc类从public google::protobuf::Service继承而来,而类中的Login()方法和GetFriendLists()方法都是虚函数,因此服务提供者需要对相应的方法进行重写。
在服务消费者方,我们可以发现生成的UserServiceRpc_Stub类没有提供默认的构造函数,而只提供了一个含有google::protobuf::RpcChannel* channe参数的构造函数,而且类中的Login()方法和GetFriendLists方法也都是虚函数,在两个方法的底层都调用了RpcChannel类的CallMethod方法。由于RpcChannel 类中的CallMethod方法是一个纯虚函数,因此我们需要重新定义一个类MyRpcChannel继承RpcChannel,在派生类MyRpcChannel中对继承而来的CallMethod方法进行重写。在构造UserServiceRpc_Stub类时,直接向其传递一个MyRpcChannel对象,即可调用派生类中重写函数。


如何将本地服务发布成rpc服务?

用户要将本地服务发布成一个可分布式部署的rpc服务,首先需要通过在user.proto配置文件中写出描述这个rpc方法的方法名字、参数类型以及返回值的响应类型。然后通过在终端执行protoc user.proto --cpp_out=./生成user.pb.ccuser.pb.h,这样就相当于rpc调用方和rpc提供方就生成了一个协议。在rpc方法的提供方,我们可以将本地服务UserServiceUerServiceRpc继承而来,然后重写Login()方法,重写好之后由框架对Login()方法进行直接调用。最后在服务发布方初始化调用的框架MprpcApplication::Init(argc, argv);,定义一个rpc网络服务对象RpcProvider provider;,把UserService对象发布到rpc节点上provider.NotifyService(new UserService());,启动一个rpc服务发布节点provider.Run();Run以后,进程进入阻塞状态,等待远程的rpc调用请求,这样就完成了将本地服务发布成rpc远程服务。

example/user.proto

syntax = "proto3";

package fixbug;

option cc_generic_services = true;

message ResultCode
{
    int32 errcode = 1; 
    bytes errmsg = 2;
}

message LoginRequest
{
    bytes name = 1;
    bytes pwd = 2;
}

message LoginResponse
{
    ResultCode result = 1;
    bool sucess = 2;
}

message RegisterRequest
{
    uint32 id = 1;
    bytes name = 2;
    bytes pwd = 3;
}

message RegisterResponse
{
    ResultCode result = 1;
    bool sucess = 2;
}

service UserServiceRpc
{
    rpc Login(LoginRequest) returns(LoginResponse);
    rpc Register(RegisterRequest) returns(RegisterResponse);
}

example/callee/usrservice.cc

#include <iostream>
#include <string>
#include "user.pb.h"
#include "mprpcapplication.h"
#include "rpcprovider.h"

/*
UserService原来是一个本地服务,提供了两个进程内的本地方法,Login和GetFriendLists
*/
class UserService : public fixbug::UserServiceRpc // 使用在rpc服务发布端(rpc服务提供者)
{
public:
    bool Login(std::string name, std::string pwd)
    {
        std::cout << "doing local service: Login" << std::endl;
        std::cout << "name:" << name << " pwd:" << pwd << std::endl;  
        return false;
    }

    bool Register(uint32_t id, std::string name, std::string pwd)
    {
        std::cout << "doing local service: Register" << std::endl;
        std::cout << "id:" << id << "name:" << name << " pwd:" << pwd << std::endl;
        return true;
    }

    /*
    重写基类UserServiceRpc的虚函数 下面这些方法都是框架直接调用的
    1. caller   ===>   Login(LoginRequest)  => muduo =>   callee 
    2. callee   ===>    Login(LoginRequest)  => 交到下面重写的这个Login方法上了
    */
    void Login(::google::protobuf::RpcController* controller,
                       const ::fixbug::LoginRequest* request,
                       ::fixbug::LoginResponse* response,
                       ::google::protobuf::Closure* done)
    {
        // 框架给业务上报了请求参数LoginRequest,应用获取相应数据做本地业务
        std::string name = request->name();
        std::string pwd = request->pwd();

        // 做本地业务
        bool login_result = Login(name, pwd); 

        // 把响应写入  包括错误码、错误消息、返回值
        fixbug::ResultCode *code = response->mutable_result();
        code->set_errcode(0);
        code->set_errmsg("");
        response->set_sucess(login_result);

        // 执行回调操作   执行响应对象数据的序列化和网络发送(都是由框架来完成的)
        done->Run();
    }

    void Register(::google::protobuf::RpcController* controller,
                       const ::fixbug::RegisterRequest* request,
                       ::fixbug::RegisterResponse* response,
                       ::google::protobuf::Closure* done)
    {
        uint32_t id = request->id();
        std::string name = request->name();
        std::string pwd = request->pwd();

        bool ret = Register(id, name, pwd);

        response->mutable_result()->set_errcode(0);
        response->mutable_result()->set_errmsg("");
        response->set_sucess(ret);

        done->Run();
    }
};

int main(int argc, char **argv)
{
    // 调用框架的初始化操作
    MprpcApplication::Init(argc, argv);

    // provider是一个rpc网络服务对象。把UserService对象发布到rpc节点上
    RpcProvider provider;
    provider.NotifyService(new UserService());

    // 启动一个rpc服务发布节点   Run以后,进程进入阻塞状态,等待远程的rpc调用请求
    provider.Run();

    return 0;
}

当完成了rpc服务提供方的代码后,接下来我们就来看看如何设计和编写的底层框架的代码的,首先关注框架的初始化操作MprpcApplication::Init(argc, argv);,我们在src目录下创建mprpcapplication.hmprpcapplication.cc
mprpcapplication.h

#pragma once

#include "mprpcconfig.h"
#include "mprpcchannel.h"
#include "mprpccontroller.h"

// mprpc框架的基础类,负责框架的一些初始化操作
class MprpcApplication
{
public:
    static void Init(int argc, char **argv);
    static MprpcApplication& GetInstance();
    static MprpcConfig& GetConfig();
private:
    static MprpcConfig m_config;

    MprpcApplication(){}
    MprpcApplication(const MprpcApplication&) = delete;
    MprpcApplication(MprpcApplication&&) = delete;
};

mprpcapplication.cc

#include "mprpcapplication.h"
#include <iostream>
#include <unistd.h>
#include <string>

MprpcConfig MprpcApplication::m_config;

void ShowArgsHelp()
{
    std::cout<<"format: command -i <configfile>" << std::endl;
}

void MprpcApplication::Init(int argc, char **argv)
{
    if (argc < 2)
    {
        ShowArgsHelp();
        exit(EXIT_FAILURE);
    }

    int c = 0;
    std::string config_file;
    while((c = getopt(argc, argv, "i:")) != -1)
    {
        switch (c)
        {
        case 'i':
            config_file = optarg;
            break;
        case '?':
            ShowArgsHelp();
            exit(EXIT_FAILURE);
        case ':':
            ShowArgsHelp();
            exit(EXIT_FAILURE);
        default:
            break;
        }
    }

    // 开始加载配置文件了 rpcserver_ip=  rpcserver_port   zookeeper_ip=  zookepper_port=
    m_config.LoadConfigFile(config_file.c_str());

    // std::cout << "rpcserverip:" << m_config.Load("rpcserverip") << std::endl;
    // std::cout << "rpcserverport:" << m_config.Load("rpcserverport") << std::endl;
    // std::cout << "zookeeperip:" << m_config.Load("zookeeperip") << std::endl;
    // std::cout << "zookeeperport:" << m_config.Load("zookeeperport") << std::endl;
}

MprpcApplication& MprpcApplication::GetInstance()
{
    static MprpcApplication app;
    return app;
}

MprpcConfig& MprpcApplication::GetConfig()
{
    return m_config;
}

当获取到配置文件以后,加载配置文件的函数LoadConfigFile()以及查询配置项信息的函数Load()mprpcconfig.hmprpcconfig.cc中定义和实现。
mprpcconfig.h

#pragma once

#include <unordered_map>
#include <string>

// rpcserverip   rpcserverport    zookeeperip   zookeeperport
// 框架读取配置文件类
class MprpcConfig
{
public:
    // 负责解析加载配置文件
    void LoadConfigFile(const char *config_file);
    // 查询配置项信息
    std::string Load(const std::string &key);
private:
    std::unordered_map<std::string, std::string> m_configMap;
    // 去掉字符串前后的空格
    void Trim(std::string &src_buf);
};

mprpcconfig.cc

#include "mprpcconfig.h"

#include <iostream>
#include <string>

// 负责解析加载配置文件
void MprpcConfig::LoadConfigFile(const char *config_file)
{
    FILE *pf = fopen(config_file, "r");
    if (nullptr == pf)
    {
        std::cout << config_file << " is note exist!" << std::endl;
        exit(EXIT_FAILURE);
    }

    // 1.注释   2.正确的配置项 =    3.去掉开头的多余的空格 
    while(!feof(pf))
    {
        char buf[512] = {0};
        fgets(buf, 512, pf);

        // 去掉字符串前面多余的空格
        std::string read_buf(buf);
        Trim(read_buf);

        // 判断#的注释
        if (read_buf[0] == '#' || read_buf.empty())
        {
            continue;
        }

        // 解析配置项
        int idx = read_buf.find('=');
        if (idx == -1)
        {
            // 配置项不合法
            continue;
        }

        std::string key;
        std::string value;
        key = read_buf.substr(0, idx);
        Trim(key);
        // rpcserverip=127.0.0.1\n
        int endidx = read_buf.find('\n', idx);
        value = read_buf.substr(idx+1, endidx-idx-1);
        Trim(value);
        m_configMap.insert({key, value});
    }

    fclose(pf);
}

// 查询配置项信息
std::string MprpcConfig::Load(const std::string &key)
{
    auto it = m_configMap.find(key);
    if (it == m_configMap.end())
    {
        return "";
    }
    return it->second;
}

// 去掉字符串前后的空格
void MprpcConfig::Trim(std::string &src_buf)
{
    int idx = src_buf.find_first_not_of(' ');
    if (idx != -1)
    {
        // 说明字符串前面有空格
        src_buf = src_buf.substr(idx, src_buf.size()-idx);
    }
    // 去掉字符串后面多余的空格
    idx = src_buf.find_last_not_of(' ');
    if (idx != -1)
    {
        // 说明字符串后面有空格
        src_buf = src_buf.substr(0, idx+1);
    }
}

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

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

相关文章

【RabbitMQ 实战】08 集群原理剖析

上一节&#xff0c;我们用docker-compose搭建了一个RabbitMQ集群&#xff0c;这一节我们来分析一下集群的原理 一、基础概念 1.1 元数据 前面我们有介绍到 RabbitMQ 内部有各种基础构件&#xff0c;包括队列、交换器、绑定、虚拟主机等&#xff0c;他们组成了 AMQP 协议消息…

次方计数的拆贡献法(考虑组合意义)+限定类问题善用值域与位置进行ds:1006T3

对于多次方的计数问题可以考虑拆贡献。 题目问 ∣ S ∣ 3 |S|^3 ∣S∣3&#xff0c; ∣ S ∣ |S| ∣S∣ 表示选的点数。相当于在 ∣ S ∣ |S| ∣S∣ 中选了3次&#xff0c;也就是选了3个可相同的点。 先考虑3个不相同点的贡献&#xff0c;对应任意3个点&#xff0c;必然会对…

Go Gin Gorm Casbin权限管理实现 - 2. 使用Gorm存储Casbin权限配置以及`增删改查`

文章目录 0. 背景1. 准备工作2. 权限配置以及增删改查2.1 策略和组使用规范2.2 用户以及组关系的增删改查2.2.1 获取所有用户以及关联的角色2.2.2 角色组中添加用户2.2.3 角色组中删除用户 2.3 角色组权限的增删改查2.3.1 获取所有角色组权限2.3.2 创建角色组权限2.3.3 修改角色…

uni-app 经验分享,从入门到离职(实战篇)——模拟从后台获取图片路径数据后授权相册以及保存图片

文章目录 &#x1f4cb;前言⏬关于专栏 &#x1f3af;需求描述&#x1f3af;前置知识点&#x1f9e9;uni.showLoading()&#x1f9e9;uni.authorize()&#x1f9e9;uni.downloadFile()&#x1f9e9;uni.saveImageToPhotosAlbum() &#x1f3af;演示代码&#x1f9e9;关于图片接…

不能一棍子敲死刚诞生不久的USB-C,虽然它有时确实很惹人厌

今年iPhone机型最大的预期之一是从苹果专有的Lightning端口过渡到USB-C标准。一些人担心过渡需要他们更换所有配件&#xff0c;而另一些人&#xff08;包括你&#xff09;则期待着未来能够为iPad、MacBook和iPhone使用一根电缆。 然而&#xff0c;现在新机型已经问世&#xff…

微信小程序使用路由传参和传对象的方法

近期在做微信小程序开发&#xff0c;在页面跳转时&#xff0c;需要携带参数到下一个页面&#xff0c;尤其是将对象传入页面。为了方便重温&#xff0c;特此记录。 路由传字符串参数 原始页面 传递字符串参数比较简单。路由跳转有两种方式&#xff0c;一种是通过navigator组件…

【AI视野·今日CV 计算机视觉论文速览 第262期】Fri, 6 Oct 2023

AI视野今日CS.CV 计算机视觉论文速览 Fri, 6 Oct 2023 Totally 73 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers Improved Baselines with Visual Instruction Tuning Authors Haotian Liu, Chunyuan Li, Yuheng Li, Yong Jae Lee大型多模…

java Spring Boot在配置文件中关闭热部署

之前更大家一起搭建了一个热部署的开发环境 但是 大家要清楚一个情况 我们线上程序运行突然内部发生变化这是不可能的。 所以 他就只会对我们开发环境有效 是否开启 我们可以通过 application配置文件来完成 我这里是yml格式的 参考代码如下 spring:devtools:restart:enabled…

雷达天线、信号链路、大气衰减基本概念

一、 有效各向同性辐射功率 有效各向同性辐射功率是各向同性天线必须辐射的假设功率,以在天线最强波束的方向上提供与实际源天线相同(等效)的信号强度。 二、电磁波传播 图1 从发射机到接收机的链路路径 信号与环境传播模型远离地面的高频和/或链路路径和/或窄天线Line of …

基础算法之——【动态规划之路径问题】1

今天更新动态规划路径问题1&#xff0c;后续会继续更新其他有关动态规划的问题&#xff01;动态规划的路径问题&#xff0c;顾名思义&#xff0c;就是和路径相关的问题。当然&#xff0c;我们是从最简单的找路径开始&#xff01; 动态规划的使用方法&#xff1a; 1.确定状态并…

《视觉 SLAM 十四讲》V2 第 6 讲 非线性优化 【高斯牛顿法、列文伯格-马夸尔特方法 、Ceres 库 和 g2o库 】

文章目录 6.1.2 最小二乘 寻找下降增量 Δ x k \Delta\bm{x}_k Δxk​的 4 种方法6.2.1 一阶和二阶梯度法(最速下降法、牛顿法)6.2.2 高斯牛顿法6.2.3 列文伯格-马夸尔特方法 【阻尼牛顿法】【信赖区域法】 6.3 实践6.3.1 手写高斯牛顿法 【Code】6.3.2 谷歌的优化库 Ceres 【最…

自动驾驶学习笔记(一)——Apollo平台

#Apollo开发者社区# 学习课程的传送门如下&#xff0c;当您也准备学习自动驾驶时&#xff0c;可以和我一同前往&#xff1a; 《自动驾驶新人之旅》免费课程—> 传送门 《2023星火培训【感知专项营】》免费课程—>传送门 文章目录 前言 Apollo框架 开发平台 总结 前…

【TMS320F28374之PWM】

简介 增强型脉宽调制器(ePWM)外设是控制商业和工业设备中许多电力电子系统的关键元件。这些系统包括数字电机控制、开关模式电源控制、不间断电源(UPS)和其他形式的电源转换。ePWM外设还可以执行数模转换(DAC)功能&#xff0c;其中占空比相当于DAC模拟值;它有时被称为电源DAC。…

[机缘参悟-109] :接纳生活中的无完美是一种修行,过度追求完美是一种“我执”,接纳污秽、肮脏、邪恶、小人是一种高度

目录 前言&#xff1a; 一、天地以万物为刍狗&#xff0c;站在大自然的视角没有善恶 1.1 天地以万物为刍狗&#xff0c;在自然面前&#xff0c;一切生命都平等 1.2 食物链是一个闭环系统&#xff0c;无所谓善恶 1.3 在大自然中&#xff0c;人类眼中的“美丽”都诞生于“污…

Python(八十八)函数的参数传递

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

C++设计模式-单件(Singleton)

目录 C设计模式-单件&#xff08;Singleton&#xff09; 一、意图 二、适用性 三、结构 四、参与者 五、代码 C设计模式-单件&#xff08;Singleton&#xff09; 一、意图 保证一个类仅有一个实例&#xff0c;并提供一个访问它的全局访问点。 二、适用性 当类只能有一…

RK3588开发笔记(一):基于方案商提供的宿主机交叉编译Qt5.12.10

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/133618273 红胖子网络科技博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬…

最强中间件!Kafka快速入门(Kafka理论+SpringBoot集成Kafka实践)

自媒体文章上下架 需求分析 媒体端下架文章同时app端也下架文章的实现可以通过feign去调用&#xff0c;但这种实现耦合度太高&#xff0c;这里使用MQ进行解耦 自媒体端一旦上下架文章就发送消息给MQ&#xff0c;文章微服务在去读取消息根据消息内容上下架文章 MQ还可以流量削…

SSM - Springboot - MyBatis-Plus 全栈体系(二十二)

第四章 SpringMVC 五、SpringMVC 其他扩展 1. 全局异常处理机制 1.1 异常处理两种方式 开发过程中是不可避免地会出现各种异常情况的&#xff0c;例如网络连接异常、数据格式异常、空指针异常等等。异常的出现可能导致程序的运行出现问题&#xff0c;甚至直接导致程序崩溃。…

嵌入式学习笔记(47)Nand的常见操作及流程分析

9.3.1坏块检查 (1)Flash使用之前一定要先统一擦除&#xff08;擦除的单元是块&#xff09;。Flash类设备擦除后里边全是1&#xff0c;所以擦干净之后读出来的值是0xFF (2)检查坏块的思路就是&#xff1a;先擦除&#xff0c;然后将整个块读出来&#xff0c;依次检测各字节是否…