当前位置: 首页 > article >正文

[实现Rpc] 环境搭建 | JsonCpp | Mudou库 | callBack()

目录

1. 项目介绍

2. 技术选型

3. 开发环境和环境搭建

Ubuntu-22.04环境搭建

1. 安装 wget(一般情况下默认会自带)

2. 更换国内软件源

① 备份原始 /etc/apt/sources.list 文件

② 编辑软件源文件

③ 更新软件包列表

3. 安装常用工具

3.1 安装 lrzsz 传输工具

3.2 安装编译器 gcc/g++

3.3 安装项目构建工具 make

3.4 安装调试器 gdb

3.5 安装版本控制系统 git

3.6 安装 cmake

3.7 安装 jsoncpp

4. 安装 Muduo 库

4. 第三方库使用介绍

4.1 JsonCpp库

4.1.1 Json的数据格式

4.1.2 JsonCpp介绍

4.1.3 JsonCpp使用

4.2 Muduo库

4.2.1 Muduo库是什么

4.2.2 Muduo库常见接口介绍

1. TcpServer 类

2. EventLoop 类

3. TcpConnection 类

4. TcpClient 类

5. Buffer 类

4.2.3 Muduo库的使用

1. 实现英译汉 TCP 服务器

2. 实现英译汉 TCP 客户端

3. Makefile 编译指令

运行结果

回调函数

1. 解耦网络事件处理逻辑

2. 事件驱动的核心机制

3. 提高代码复用性与可维护性

4. 非阻塞与多线程安全

5. 动态响应不同需求


1. 项目介绍

(1)

  • RPC(Remote Procedure Call)远程过程调用,是⼀种通过网络从远程计算机上请求服务,而不需要了解底层网络通信细节。
  • RPC可以使用多种网络协议进行通信, 如HTTP、TCP、UDP等, 并且在TCP/IP网络四层模型中跨越了传输层和应用层。
  • 简言之 RPC就是像调用本地方法⼀样调用远程方法。

此过程可以理解为业务处理、计算任务,更直白的说,就是程序/方法/函数等,就是像调用本地放法⼀样调用远程放法。

(2)

举个形象点谈恋爱的例子:

  • 本地过程调用:恋爱对象在你的身边, 可以随时约对象吃饭、看电影、约会等等
  • 远端过程调用:好像异地恋⼀样, 隔着千山万水, 如果想约会, 需要先和对象进行约定,在坐火车/飞机赶到约定的地点

(3)

⼀个完整RPC通信框架,大概包含以下内容:

  • 序列化协议。
  • 通信协议。
  • 连接复用。
  • 服务注册。
  • 服务发现。
  • 服务订阅和通知。
  • 负载均衡。
  • 服务监控。
  • 同步调用。
  • 异步调用。

本项目是基于C++、JsonCpp、muduo网络库实现⼀个简单、易用的RPC通信框架

实现了 同步调用、异步callback调用、异步futrue调用、服务注册/发现/上线/下线,主题发布订阅等功能设计。


2. 技术选型

(1)目前RPC的实现方案有两种:

第一种是 client和server继承公共接口:

  • 根据IDL(接口描述语言)定义公共接口。
  • 编写代码生成器根据IDL语言生成相关的C++、Java代码
  • 然后我们的客户端和服务器程序 共同向上继承公共接口即可

比如我们常用的Protobuf、json可以定义IDL接口,并生成RPC相关的代码

  • 缺点:使用pb因为生成⼀部分代码, 所以对理解不够友好
  • 如果是json定义IDL语言需要自己编写代码⽣成器难度较大一点, 暂不考虑这种方案

第二种实现⼀个远程调用接口call,然后通过传入函数名参数来调用RPC接口。本项目采用这种实现方案。

(2)网络传输的参数和返回值 怎么映射到对应的RPC 接口上?

  1. 使用protobuf的反射机制。
  2. 使用C++模板、类型萃取、函数萃取等机制。
  3. 使用更通用的类型, 比如 JSON类型, 设计好参数和返回值协议即可。

前两种技术难度和学习成本较高, 本项目使用第三种方式。

(3)网络传输怎么做?

  • 原生socket - 实现难度较⼤, 暂不考虑。
  • Boost asio库的异步通信 - 需要扩展boost库。
  • 使用muduo库,学习开发成本较低。

(4)序列化和反序列化?

  • Protobuf:可选。
  • JSON:因为项⽬需要使⽤JSON来定义函数参数和返回值,所以本项目中 直接采用JSON进行序列化和反序列化。

3. 开发环境和环境搭建

  • Linux(Centos-7.6 / Ubuntu-22.04)。
  • VSCode/Vim。
  • g++/gdb。
  • Makefile。

Ubuntu-22.04环境搭建

1. 安装 wget(一般情况下默认会自带)

在Ubuntu 22.04中,wget通常已经预安装。如果需要确保它已安装,可以运行以下命令:

sudo apt-get update
sudo apt-get install -y wget
2. 更换国内软件源

为了加速下载速度和提高稳定性,可以将官方的APT源更换为国内镜像源。

① 备份原始 /etc/apt/sources.list 文件

首先备份原来的软件源文件,以防出现问题时可以恢复:

sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
② 编辑软件源文件

接下来,使用文本编辑器打开并编辑/etc/apt/sources.list文件以添加新的源。推荐使用nano或您喜欢的其他编辑器:

sudo nano /etc/apt/sources.list

然后,在文件中添加或替换以下内容(这里使用了阿里云和清华大学的镜像源):

deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

# 添加清华源
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse

请注意,这里的focal是Ubuntu 20.04 LTS的代号。对于Ubuntu 22.04,请根据实际情况更改为jammy

③ 更新软件包列表

保存更改后,通过以下命令更新APT软件包列表:

sudo apt-get update
3. 安装常用工具

现在可以安装一些常用的开发工具和库。

3.1 安装 lrzsz 传输工具

用于从远程服务器上传和下载文件:

sudo apt-get install lrzsz
3.2 安装编译器 gcc/g++

安装GNU编译器集合,包括C和C++编译器:

sudo apt-get install gcc g++
3.3 安装项目构建工具 make

用于管理和构建项目:

sudo apt-get install make
3.4 安装调试器 gdb

用于调试程序:

sudo apt-get install gdb
3.5 安装版本控制系统 git

用于代码版本管理:

sudo apt-get install git
3.6 安装 cmake

用于跨平台的项目构建系统:

sudo apt-get install cmake
3.7 安装 jsoncpp

用于解析JSON数据格式:

sudo apt-get install libjsoncpp-dev

4. 安装 Muduo 库

1. 下载源码

可以通过 git 命令从 GitHub 克隆 Muduo 的源码到本地:

# 使用 git 克隆源码
git clone https://github.com/chenshuo/muduo.git

2. 安装依赖环境

Muduo 依赖于以下库,需要通过包管理工具安装相关依赖:

# 安装 zlib 和 Boost 库
sudo apt-get install libz-dev libboost-all-dev

3. 编译与安装

执行以下步骤完成编译和安装:

  • 解压源码(如适用):
    如果下载的是压缩包(如 muduo-master.zip),需要先解压。
  • unzip muduo-master.zip
    cd muduo-master
  • 运行编译脚本
    使用 build.sh 脚本编译 Muduo 库。
  • ./build.sh
  • 安装到系统目录
    运行安装脚本将编译生成的库文件安装到系统路径。
./build.sh install

安装成功:


4. 第三方库使用介绍

4.1 JsonCpp库

4.1.1 Json的数据格式

Json简介

Json(JavaScript Object Notation)是一种轻量级的数据交换格式

  • 它采用完全独立于编程语言的文本格式来存储和表示数据。
  • 这种格式易于人阅读和编写,同时也易于机器解析和生成。

示例:学生信息表示

  • C/C++代码表示
char *name = "xx";
int age = 18;
float score[3] = {88.5, 99, 58};
  • Json 表示
{
    "姓名" : "xx",
    "年龄" : 18,
    "成绩" : [88.5, 99, 58],
    "爱好" :
    {
        "书籍" : "西游记",
        "运动" : "打篮球"
    }
}

Json 的数据类型

  • 对象:使用花括号 {} 括起来表示一个对象。
  • 数组:使用中括号 [] 括起来表示一个数组。
  • 字符串:使用常规双引号 "" 括起来表示一个字符串。
  • 数字:包括整形和浮点型,直接使用。

4.1.2 JsonCpp介绍

Jsoncpp库概述

Jsoncpp库主要用于实现Json格式数据的序列化和反序列化,即它可以将多个数据对象组织成Json格式字符串,并且可以从Json格式字符串解析得到多个数据对象。

⭕Json 数据对象类 Value

class Json::Value
{
    Value &operator=(const Value &other); // Value重载了[]和=,因此所有的赋值和获取数据都可以通过简单的方式完成
    Value& operator[](const std::string& key); // val["name"] = "xx";
    Value& operator[](const char* key);
    Value removeMember(const char* key); // 移除元素
    const Value& operator[](ArrayIndex index) const; // val["score"][0]
    Value& append(const Value& value); // 添加数组元素 val["score"].append(88);
    ArrayIndex size() const; // 获取数组元素个数 val["score"].size();
    std::string asString() const; // 转换为 string string name = val["name"].asString();
    const char* asCString() const; // 转换为 char* char *name = val["name"].asCString();
    Int asInt() const; // 转换为 int int age = val["age"].asInt();
    float asFloat() const; // 转换为 float float weight = val["weight"].asFloat();
    bool asBool() const; // 转换为 bool bool ok = val["ok"].asBool();
};

序列化接口

class JSON_API StreamWriter 
{
    virtual int write(Value const& root, std::ostream* sout) = 0;
};

class JSON_API StreamWriterBuilder : public StreamWriter::Factory 
{
    virtual StreamWriter* newStreamWriter() const;
};

反序列化接口

class JSON_API CharReader 
{
    virtual bool parse(char const* beginDoc, char const* endDoc, Value* root, std::string* errs) = 0;
};

class JSON_API CharReaderBuilder : public CharReader::Factory 
{
    virtual CharReader* newCharReader() const;
};

序列化和反序列化都是需要通过工厂类来进行实例化。


4.1.3 JsonCpp使用

代码编写示例

下面是一个简单的例子,展示了如何使用Jsoncpp库进行序列化和反序列化操作。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <sstream>
#include <memory>

// 序列化函数
bool serialize(const Json::Value &val, std::string &body)
{
    std::stringstream ss;
    Json::StreamWriterBuilder swb;
    std::unique_ptr<Json::StreamWriter> w(swb.newStreamWriter());
    bool ret = w->write(val, &ss);
    if (!ret) return false;

    body = ss.str();
    return true;
}

// 反序列化函数
bool deserialize(const std::string &body, Json::Value &val)
{
    Json::CharReaderBuilder crb;
    std::unique_ptr<Json::CharReader> r(crb.newCharReader());

    std::string errs;
    bool ret = r->parse(body.c_str(), body.c_str() + body.size(), &val, &errs);
    if (!ret) 
    {
        std::cout << "json unserialize failed : " << errs << std::endl;
        return false;
    }

    return true;
}

int main()
{
    const char* sex = "男";
    const char* name = "小明";
    int age = 18;
    int score[4] = {70, 80, 90};

    Json::Value stu1;
    stu1["姓名"] = name;
    stu1["性别"] = sex;
    stu1["年龄"] = age;
    for (auto s : score) {
        stu1["分数"].append(s);
    }
    Json::Value fav;
    fav["书籍"] = "西游记";
    fav["运动"] = "打篮球";
    stu1["爱好"] = fav;
    std::string body;

    bool ret1 = serialize(stu1, body);
    std::cout << body << std::endl;

    std::string str = R"({"姓名":"小黑", "年龄": 19, "成绩":[32, 45, 56]})";
    Json::Value stu2;
    bool ret2 = deserialize(str, stu2);
    if (!ret2) return -1;

    std::cout << "姓名: " << stu2["姓名"].asString() << std::endl;
    std::cout << "年龄: " << stu2["年龄"].asInt() << std::endl;
    for (int i = 0; i < static_cast<int>(stu2["成绩"].size()); ++i) 
    {
        std::cout << "成绩: " << stu2["成绩"][i].asFloat() << std::endl;
    }

    return 0;
}

⭕注意:对于头文件的包含调用

运行:


4.2 Muduo库

4.2.1 Muduo库是什么

Muduo是由陈硕老师开发的 基于非阻塞IO和事件驱动的C++高并发TCP网络编程库。它采用 主从Reactor模型one loop per thread 线程模型

⭕one loop per thread 的含义:

  1. 一个线程只能有一个事件循环(EventLoop),用于响应计时器和IO事件。
  2. 一个 文件描述符只能由一个线程进行读写,即一个TCP连接必须归属于某个EventLoop管理。

从而避免了,例如一万个请求就创建一万个线程的等待浪费,主从的结构也将 对于网络连接的接收和对于信息的处理 进行了拆分。


4.2.2 Muduo库常见接口介绍
1. TcpServer 类

TcpServer 用于服务端管理和维护 TCP 连接。核心接口包括:

  • 构造函数
TcpServer(EventLoop* loop, const InetAddress& listenAddr, const string& nameArg, Option option = kNoReusePort);
    • 参数:
      • loop:主EventLoop,用于事件循环。
      • listenAddr:服务器监听地址。
      • nameArg:服务器实例名称。
      • option:是否复用端口。
  • 线程设置与启动
void setThreadNum(int numThreads); // 设置IO线程数量
void start();                      // 启动服务端监听
  • 回调函数设置
void setConnectionCallback(const ConnectionCallback& cb); // 连接建立/断开回调
void setMessageCallback(const MessageCallback& cb);       // 消息接收回调
2. EventLoop 类

EventLoop 是事件循环的核心类。

  • 循环控制
void loop();  // 启动事件循环
void quit();  // 退出事件循环
  • 定时器接口
TimerId runAt(Timestamp time, TimerCallback cb);      // 在指定时间运行回调
TimerId runAfter(double delay, TimerCallback cb);     // 指定延迟后运行回调
TimerId runEvery(double interval, TimerCallback cb);  // 周期性运行回调
void cancel(TimerId timerId);                         // 取消定时器
3. TcpConnection 类

TcpConnection 负责维护单个TCP连接。

  • 核心功能
void send(const void* message, int len);  // 发送数据
void shutdown();                         // 关闭连接
  • 上下文管理
void setContext(const boost::any& context); // 设置上下文
const boost::any& getContext() const;       // 获取上下文
  • 回调函数
void setConnectionCallback(const ConnectionCallback& cb); // 设置连接回调
void setMessageCallback(const MessageCallback& cb);       // 设置消息回调
4. TcpClient 类

TcpClient 用于客户端管理 TCP 连接。

  • 核心方法
void connect();    // 发起连接
void disconnect(); // 断开连接
void stop();       // 停止客户端
  • 回调函数
void setConnectionCallback(ConnectionCallback cb); // 设置连接回调
void setMessageCallback(MessageCallback cb);       // 设置消息回调
  • 注意事项
    由于TcpClient操作是异步的,在连接建立之前尝试发送数据是不被允许的,可通过CountDownLatch进行同步。
5. Buffer 类

Buffer 用于管理数据缓冲区。

  • 核心方法
size_t readableBytes() const;  // 可读数据大小
size_t writableBytes() const;  // 可写数据大小
string retrieveAllAsString();  // 取出所有数据并转为字符串
void append(const char* data, size_t len); // 添加数据
void prepend(const void* data, size_t len); // 在缓冲区头部追加数据
  • 内部实现
    使用std::vector<char>作为缓冲区,支持动态扩容。

4.2.3 Muduo库的使用
1. 实现英译汉 TCP 服务器

代码示例

  • 核心逻辑
    • 使用TcpServer管理服务器。
    • 注册onConnectiononMessage回调函数。
    • onMessage中,通过字典查找翻译单词,并返回给客户端。

代码实现

class DictServer {
public:
    DictServer(int port)
        :_server(&_baseloop, InetAddress("0.0.0.0", port), "DictServer", TcpServer::kReusePort)
    {
        _server.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));
        _server.setMessageCallback(std::bind(&DictServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
    }

    void Start() {
        _server.start();
        _baseloop.loop();
    }

private:
    void onConnection(const TcpConnectionPtr &conn) {
        if (conn->connected()) std::cout << "连接建立!\n";
        else std::cout << "连接断开!\n";
    }

    void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp) {
        std::unordered_map<std::string, std::string> dict = {
  
  {"hello", "你好"}, {"world", "世界"}};
        std::string msg = buf->retrieveAllAsString();
        conn->send(dict.count(msg) ? dict[msg] : "未知单词!");
    }

private:
    EventLoop _baseloop;
    TcpServer _server;
};

int main() {
    DictServer server(8080);
    server.Start();
    return 0;
}
2. 实现英译汉 TCP 客户端

代码示例

  • 核心逻辑
    • 使用TcpClient连接服务器。
    • 注册回调函数onConnectiononMessage
    • 在建立连接后,通过send方法发送单词。

代码实现

class DictClient {
public:
    DictClient(const std::string &ip, int port)
        :_baseloop(_loopthread.startLoop()), _latch(1), _client(_baseloop, InetAddress(ip, port), "DictClient")
    {
        _client.setConnectionCallback(std::bind(&DictClient::onConnection, this, std::placeholders::_1));
        _client.setMessageCallback(std::bind(&DictClient::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _client.connect();
        _latch.wait();
    }

    void send(const std::string &msg) {
        if (_conn->connected()) _conn->send(msg);
        else std::cout << "连接断开,发送失败!\n";
    }

private:
    void onConnection(const TcpConnectionPtr &conn) {
        if (conn->connected()) {
            _conn = conn;
            _latch.countDown();
        } else {
            _conn.reset();
        }
    }

    void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp) {
        std::cout << buf->retrieveAllAsString() << std::endl;
    }

private:
    EventLoop *_baseloop;
    EventLoopThread _loopthread;
    TcpClient _client;
    TcpConnectionPtr _conn;
    CountDownLatch _latch;
};

int main() {
    DictClient client("127.0.0.1", 8080);
    std::string msg;
    while (std::cin >> msg) client.send(msg);
    return 0;
}
3. Makefile 编译指令
CFLAG= -std=c++11 -I ../build/release-install-cpp11/include/
LFLAG= -L../build/release-install-cpp11/lib -lmuduo_net -lmuduo_base -pthread

all: server client

server: server.cpp
    g++ $(CFLAG) $^ -o $@ $(LFLAG)

client: client.cpp
    g++ $(CFLAG) $^ -o $@ $(LFLAG)

clean:
    rm -f server client

运行结果

通过简单输入单词实现实时翻译,服务器返回翻译结果或提示“未知单词”。

客户端

回调函数

在 Muduo 中使用回调函数(如 setConnectionCallbacksetMessageCallback)是为了实现网络库的 事件驱动编程模型,这符合非阻塞 I/O 和高并发网络编程的设计理念。

🔷 为什么要借助回调函数呢


1. 解耦网络事件处理逻辑

Muduo 库本身只负责处理网络事件的底层逻辑,比如连接建立、断开、消息接收等,但具体的业务逻辑是用户的需求(如数据如何处理、何时响应客户端等),而这些需求是 多变且不可预测的

  • 问题:如果将业务逻辑直接嵌入到 Muduo 内部,不仅会导致耦合度过高,还会限制灵活性。
  • 解决通过 回调函数,用户可以将自己的业务逻辑注册给 Muduo,在事件发生时 Muduo 自动调用注册的回调函数。

示例

  • 连接建立或断开时,用户可以通过 setConnectionCallback 注册逻辑来处理事件(如记录日志、初始化资源等)。
  • 消息到达时,通过 setMessageCallback 注册回调处理消息(如解析请求、响应客户端)。

2. 事件驱动的核心机制

Muduo 的设计基于 事件驱动模型,回调函数是这种模型的核心:

  1. 在事件循环(EventLoop)中,Muduo 会监听文件描述符(如 socket)上的事件。
  2. 当检测到事件(如新连接、可读数据等),会触发相应的回调函数。

使用回调函数的好处:

  • 异步非阻塞:Muduo 不会阻塞等待事件完成,而是注册回调函数后,立即返回继续处理其他事件。
  • 灵活性高:用户可以根据业务需要,提供任意逻辑来响应事件,而无需修改框架本身。

示例

void onConnection(const TcpConnectionPtr& conn) {
    if (conn->connected()) {
        std::cout << "新连接建立:" << conn->peerAddress().toIpPort() << std::endl;
    } else {
        std::cout << "连接断开" << std::endl;
    }
}

用户通过 setConnectionCallbackonConnection 注册给 TcpServer,当连接建立或断开时,TcpServer 自动调用用户定义的逻辑。


3. 提高代码复用性与可维护性

回调函数机制使得:

  • 复用性高:Muduo 的底层逻辑和用户的业务逻辑是解耦的,可以分别复用。例如,Muduo 可用于不同类型的服务器(如 HTTP 服务器、聊天服务器),而无需修改核心代码。
  • 易于维护:用户只需关心自己的回调函数,不需要深入理解网络库内部的实现。框架和业务逻辑的分离使得代码更清晰易维护。

4. 非阻塞与多线程安全

在高并发场景中,回调函数的设计可以帮助 Muduo:

  1. 避免阻塞:通过异步回调实现逻辑处理,避免阻塞事件循环线程(EventLoop)。
  2. 线程安全:Muduo 使用 one loop per thread 模型,每个线程一个 EventLoop,回调函数在绑定的线程中执行,不需要担心多线程竞态问题。

5. 动态响应不同需求

回调函数的动态绑定特性允许用户在运行时根据需求改变逻辑:

  • 用户可以在程序启动时或运行时,调用 setConnectionCallbacksetMessageCallback 设置不同的回调函数,动态调整行为。

示例

// 设置新的消息回调函数
server.setMessageCallback([](const TcpConnectionPtr& conn, Buffer* buf, Timestamp) {
    std::string msg = buf->retrieveAllAsString();
    conn->send("Echo: " + msg);
});

通过新的回调函数,程序逻辑可以轻松从英译汉服务器切换为回声服务器,而无需修改其他部分的代码。

总结

使用回调函数的原因总结如下:

  1. 解耦网络库与业务逻辑,增强灵活性。
  2. 事件驱动模型的核心机制,支持异步非阻塞操作。
  3. 提高复用性与可维护性,框架和业务逻辑分离。
  4. 适应高并发需求,避免阻塞并提供线程安全保证。
  5. 支持动态逻辑调整,满足运行时多样化需求。

回调函数是事件驱动编程中不可或缺的设计模式,Muduo 通过它实现了高性能、高并发的网络框架设计理念。


http://www.kler.cn/a/510648.html

相关文章:

  • 【English-Book】Go in Action目录页翻译中文
  • Springboot Redisson 分布式锁、缓存、消息队列、布隆过滤器
  • 解决 MySQL 服务无法启动:failed to restart mysql.service unit not found
  • 【Linux系统】Ext系列磁盘文件系统二:引入文件系统(续篇)
  • 深入内核讲明白Android Binder【二】
  • tlias部门管理-新增部门-接口开发
  • VUE学习笔记(入门)17__小黑记事本综合案例
  • Oracle 深入学习 Part12: Managing Indexes (管理索引)
  • A Dual-Module Denoising Approach 解读
  • ES6是什么
  • Agentic AI 和 AI Agent 之间的区别(ChatGPT回答)
  • 【2024年华为OD机试】(B卷,100分)- 计算最接近的数 (Java JS PythonC/C++)
  • 【专题二 二叉树中的深搜】230. 二叉搜索树中第K小的元素
  • 微软宣布Win11 24H2进入新阶段!设备将自动下载更新
  • pycharm python pyqt5
  • 豪越科技消防一体化安全管控平台:推动消防作训模式智慧转型
  • Liunx搭建KVM私有虚拟主机
  • MarsCode青训营打卡Day6(2025年1月19日)|稀土掘金-360.可被K整除的子数组问题
  • postcss插件-实现vw适配
  • C#,入门教程(02)—— Visual Studio 2022开发环境搭建图文教程
  • 寒假1.19
  • 国产编辑器EverEdit - 合并行
  • 基于STM32单片机火灾安全监测一氧化碳火灾
  • linux制作自定义service服务单元
  • 算法-数组拆分
  • 解锁Web数据存储:浏览器数据库 IndexedDB