C++ 从零实现Json-Rpc 框架

一、😀Json-Rpc 简介

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

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

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

本地过程调用:恋爱对象在你的身边, 可以随时约对象吃饭、看电影、约会等等

远端过程调用:好像异地恋⼀样, 隔着千山万水, 如果想约会, 需要先和对象进行约定, 在坐火
车/飞机赶到约定的地点

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

序列化协议

• 通信协议

• 连接复⽤

• 服务注册

• 服务发现

• 服务订阅和通知

• 负载均衡

• 服务监控

• 同步调⽤

• 异步调⽤

该项目是基于C++、JsonCpp、muduo⽹络库实现⼀个简单、易⽤的RPC通信框架,即使是不懂⽹络的开发者也可以很快速的上⼿,它实现了同步调⽤、异步callback调⽤、异步futrue调⽤、服务注册/发现,服务上线/下线以及发布订阅等功能设计。

二、😁技术选型

2-1 🍕目前RPC的实现方案有两种:

1.client和server继承公共接口

◦ 根据IDL(接⼝描述语⾔)定义公共接⼝

◦ 编写代码⽣成器根据IDL语⾔⽣成相关的C++、Java代码

◦ 然后我们的客⼾端和服务器程序共同向上继承公共接⼝即可

◦ ⽐如我们常⽤的Protobuf、json可以定义IDL接⼝,并⽣成RPC相关的代码

◦ 缺点: 使⽤pb因为⽣成⼀部分代码, 所以对理解不够友好; 如果是json定义IDL语⾔ 需要⾃⼰

编写代码⽣成器难度较⼤⼀点, 暂不考虑这种⽅案

2.实现⼀个远程调用接口call, 然后通过传⼊函数名参数来调用RPC接⼝, 我们采⽤这种实现⽅案

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

1.使⽤protobuf的反射机制

2.使⽤C++模板、类型萃取、函数萃取等机制

3.使用更通用的类型, 比如JSON类型, 设计好参数和返回值协议即可

4.前两种技术难度和学习成本较⾼, 我们使⽤第三种⽅式

2-3 🌭网络传输怎么做?

1.原⽣socket - 实现难度较⼤, 暂不考虑

2.Boost asio库的异步通信 - 需要扩展boost库

3.muduo库, 学习开发成本较低

2-4 🍿序列化和反序列化

1.Protobuf: 可选

2.JSON: 因为项目需要使用JSON来定义函数参数和返回值, 所以我们项目中直接采用JSON进行序列化和反序列化

三、😂开发环境

1.Linux(Ubuntu-22.04)

2.VSCode/Vim

3.g++/gdb

4.Makefile

四、😃环境搭建

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

bash 复制代码
[zsc@node ~]$ sudo apt-get install wget

2.更换国内软件源

先备份原来的/etc/apt/source.list⽂件

bash 复制代码
[zsc@node ~]$ sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

添加软件源⽂件内容,新增以下内容
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

新增完毕后,更新源:
[zsc@node ~]$ sudo apt-get update

3.安装lrzsz 传输工具

bash 复制代码
[zsc@node ~]$ sudo apt-get install lrzsz
[zsc@node ~]$ rz --version
rz (lrzsz) 0.12.20

4.安装编译器gcc/g++

bash 复制代码
[zsc@node ~]$ sudo apt-get install gcc g++

5.安装项目构建工具make

bash 复制代码
[zsc@node ~]$ sudo apt-get install make

6.安装调试器gdb

bash 复制代码
[zsc@node ~]$ sudo apt-get install gdb

7.安装git

bash 复制代码
[zsc@node ~]$ sudo apt-get install git
[zsc@node ~]$ git --version

8.安装cmake

bash 复制代码
[zsc@node ~]$ sudo apt-get install cmake
[zsc@node ~]$ cmake --version
cmake version 3.22.1

9.安装jsoncpp

复制代码
[zsc@node ~]$ sudo apt-get install libjsoncpp-dev

10.安装Muduo

bash 复制代码
1.下载源码
# git⽅式
[zsc@node ~]$ git clone https://github.com/chenshuo/muduo.git
2.安装依赖环境
[zsc@node ~]$ sudo apt-get install libz-dev libboost-all-dev
3.运⾏脚本编译安装
[zsc@node muduo-master]$ unzip muduo-master.zip
[zsc@node muduo-master]$ ./build.sh
[zsc@node muduo-master]$ ./build.sh install

五、😄第三方库使用介绍

5-1 🍔JsonCpp库

5-1-1 🚗Json数据格式

Json 是⼀种数据交换格式,它采⽤完全独⽴于编程语⾔的⽂本格式来存储和表⽰数据。

例如: 我们想表⽰⼀个同学的学⽣信息

cpp 复制代码
C 代码表⽰
char* name = "xx";
int age = 18;
float score[3] = {88.5, 99, 58};

Json 表示
{
    "姓名" :"xx",
    "年龄" :18,
    "成绩" : [88.5, 99, 58],
    "爱好" :{
        "书籍" :"西游记"
        "运动" :"打篮球"
    }
}

Json 的数据类型包括对象,数组,字符串,数字等。

• 对象:使⽤花括号 {} 括起来的表⽰⼀个对象

• 数组:使⽤中括号 [] 括起来的表⽰⼀个数组

• 字符串:使⽤常规双引号 "" 括起来的表⽰⼀个字符串

• 数字:包括整形和浮点型,直接使⽤

5-1-2 🚓JsonCpp介绍

Jsoncpp 库主要是⽤于实现 Json 格式数据的序列化和反序列化,它实现了将多个数据对象组织成为 json 格式字符串,以及将 Json 格式字符串解析得到多个数据对象的功能。

Json 数据对象类的表示

1️⃣Json::Value类:中间数据存储类

如果要将数据对象进行序列化,就需要先存储到Json::Value对象中;

如果要将数据进行反序列化,就是解析后,将数据对象放入到Json::Value对象中。

cpp 复制代码
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();
};
2️⃣Json::StreamWrite类

用于进行数据序列化

cpp 复制代码
class JSON_API StreamWriter {        /write()序列化函数
 virtual int write(Value const& root, std::ostream* sout) = 0;
}

/工厂,用于生产Json::StreamWrite 对象
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
 virtual StreamWriter* newStreamWriter() const;
}
3️⃣Json::CharReader类

反序列化类

cpp 复制代码
class JSON_API CharReader {     /parse 反序列化函数
 virtual bool parse(char const* beginDoc, char const* endDoc, 
 Value* root, std::string* errs) = 0;
}

/工厂类,用于生产CharReader对象
class JSON_API CharReaderBuilder : public CharReader::Factory {
 virtual CharReader* newCharReader() const;
}
🔥jsoncpp.cpp🔥

六、😆Muduo库

Muduo由陈硕⼤佬开发,是⼀个基于非阻塞IO事件驱动 的C++高并发TCP网络编程库 。 它是⼀款基于主从Reactor模型的⽹络库,其使⽤的线程模型是one loop per thread , 所谓one loop per thread 指的是:

1.⼀个线程只能有⼀个事件循环(EventLoop), ⽤于响应计时器和IO事件

2.⼀个⽂件描述符只能由⼀个线程进⾏读写,换句话说就是⼀个TCP连接必须归属于某个EventLoop 管理

6-1🧂Muduo库常见接口介绍

6-1-1 🚕TcpServer类介绍
cpp 复制代码
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
typedef std::function<void (const TcpConnectionPtr&,
                           Buffer*,
                           Timestamp)> MessageCallback;
class InetAddress : public muduo::copyable
{
public:
 InetAddress(StringArg ip, uint16_t port, bool ipv6 = false);
};
class TcpServer : noncopyable
{
public:
 enum Option
 {
 kNoReusePort,
 kReusePort,
};
 TcpServer(EventLoop* loop,
 const InetAddress& listenAddr,
 const string& nameArg,
 Option option = kNoReusePort);
 void setThreadNum(int numThreads);
 void start();->启动服务器

 /// 当⼀个新连接建⽴成功的时候被调⽤
 void setConnectionCallback(const ConnectionCallback& cb) -->设置连接建立/关闭时的回调函数
 { connectionCallback_ = cb; }

 /// 消息的业务处理回调函数---这是收到新连接消息的时候被调⽤的函数
 void setMessageCallback(const MessageCallback& cb) -->设置消息处理回调函数
 { messageCallback_ = cb; }
};
6-1-2 🛺EventLoop类介绍
cpp 复制代码
class EventLoop : noncopyable
{
public:
 /// Loops forever.
 /// Must be called in the same thread as creation of the object.
 void loop(); -->开始事件监控循环
 /// Quits loop.
 /// This is not 100% thread safe, if you call through a raw pointer,
 /// better to call through shared_ptr<EventLoop> for 100% safety.
 void quit(); -->停止循环
 TimerId runAt(Timestamp time, TimerCallback cb); -->定时任务
 /// Runs callback after @c delay seconds.
 /// Safe to call from other threads.
 TimerId runAfter(double delay, TimerCallback cb);
 /// Runs callback every @c interval seconds.
 /// Safe to call from other threads.
 TimerId runEvery(double interval, TimerCallback cb);
 /// Cancels the timer.
 /// Safe to call from other threads.
 void cancel(TimerId timerId);
private:
 std::atomic<bool> quit_;
 std::unique_ptr<Poller> poller_;
 mutable MutexLock mutex_;
 std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);
};
6-1-3 🚙TcpConnection类介绍
cpp 复制代码
class TcpConnection : noncopyable,
 public std::enable_shared_from_this<TcpConnection>
{
public:
 /// Constructs a TcpConnection with a connected sockfd
 ///
 /// User should not create this object.
 TcpConnection(EventLoop* loop,
 const string& name,
 int sockfd,
 const InetAddress& localAddr,
 const InetAddress& peerAddr);
 bool connected() const { return state_ == kConnected; } -->判断当前链接是否正常
 bool disconnected() const { return state_ == kDisconnected; }
 
 void send(string&& message); // C++11  -->发送数据
 void send(const void* message, int len);
 void send(const StringPiece& message);
 // void send(Buffer&& message); // C++11
 void send(Buffer* message); // this one will swap data
 void shutdown(); // NOT thread safe, no simultaneous calling -->关闭连接
 void setContext(const boost::any& context)
 { context_ = context; }
 const boost::any& getContext() const
 { return context_; }
 boost::any* getMutableContext()
 { return &context_; }
 void setConnectionCallback(const ConnectionCallback& cb)
 { connectionCallback_ = cb; }
 void setMessageCallback(const MessageCallback& cb)
 { messageCallback_ = cb; }
private:
 enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };
 EventLoop* loop_;
 ConnectionCallback connectionCallback_;
 MessageCallback messageCallback_;
 WriteCompleteCallback writeCompleteCallback_;
 boost::any context_;
};
6-1-4 🛻TcpClient类介绍
cpp 复制代码
因为Client的connect接口是一个非阻塞操作,所以有可能出现另一种意外情况:
connect连接没有建立完成的情况下,调用connection接口获取连接,Send发送数据

class TcpClient : noncopyable
{
public:
 // TcpClient(EventLoop* loop);
 // TcpClient(EventLoop* loop, const string& host, uint16_t port);
 TcpClient(EventLoop* loop,
 const InetAddress& serverAddr,
 const string& nameArg);
 ~TcpClient(); // force out-line dtor, for std::unique_ptr members.

 void connect();//连接服务器 -- 非阻塞接口

 void disconnect();//关闭连接

 void stop();
 //获取客⼾端对应的通信连接Connection对象的接⼝,发起connect后,有可能还没有连接建⽴成
功
 TcpConnectionPtr connection() const
 {
 MutexLockGuard lock(mutex_);
 return connection_;
 }

 Muduo库的客户端也是通过Eventloop进行IO事件监控IO处理的
 / 连接服务器成功时的回调函数
 void setConnectionCallback(ConnectionCallback cb)
 { connectionCallback_ = std::move(cb); }
 
 / 收到服务器发送的消息时的回调函数
 void setMessageCallback(MessageCallback cb)
 { messageCallback_ = std::move(cb); }
private:
 EventLoop* loop_;
 ConnectionCallback connectionCallback_;
 MessageCallback messageCallback_;
 WriteCompleteCallback writeCompleteCallback_;
 TcpConnectionPtr connection_ GUARDED_BY(mutex_);
};
/*
需要注意的是,因为muduo库不管是服务端还是客⼾端都是异步操作,
对于客⼾端来说如果我们在连接还没有完全建⽴成功的时候发送数据,这是不被允许的。
因此我们可以使⽤内置的CountDownLatch类进⾏同步控制
*/
/做计数同步操作的类
class CountDownLatch : noncopyable
{
public:
explicit CountDownLatch(int count);
 
void wait(){ -->计数大于0则阻塞 
 MutexLockGuard lock(mutex_);
 while (count_ > 0)
 {
 condition_.wait();
 }
 }

 void countDown(){ -->计数--,为0时唤醒wait
 MutexLockGuard lock(mutex_);
 --count_;
 if (count_ == 0)
 {
 condition_.notifyAll();
 }
 }
 int getCount() const;
private:
 mutable MutexLock mutex_;
 Condition condition_ GUARDED_BY(mutex_);
 int count_ GUARDED_BY(mutex_);
};
6-1-5 🚌Buffer类介绍
cpp 复制代码
class Buffer : public muduo::copyable
{
public:
 static const size_t kCheapPrepend = 8;
 static const size_t kInitialSize = 1024;
 explicit Buffer(size_t initialSize = kInitialSize)
 : buffer_(kCheapPrepend + initialSize),
 readerIndex_(kCheapPrepend),
 writerIndex_(kCheapPrepend);
 void swap(Buffer& rhs)

 size_t readableBytes() const -->获取缓冲区可读数据大小
 size_t writableBytes() const 

 const char* peek() const -->获取缓冲区中数据的起始地址
 const char* findEOL() const
 const char* findEOL(const char* start) const
 void retrieve(size_t len)
 void retrieveInt64()

 void retrieveInt32() -->数据读取位置向后偏移4字节,本质上就是删除起始位置的4字节数据
 void retrieveInt16()
void retrieveInt8()

 string retrieveAllAsString() -->从缓冲区中读取所有数据,当作string返回,并删除缓冲区中的数据

 string retrieveAsString(size_t len) -->从缓冲区中去除len长度的数据,当作string返回,
                                        并删除缓冲区中的数据
 void append(const StringPiece& str)
 void append(const char* /*restrict*/ data, size_t len)
 void append(const void* /*restrict*/ data, size_t len)
 char* beginWrite()
 const char* beginWrite() const
 void hasWritten(size_t len)
 void appendInt64(int64_t x)
 void appendInt32(int32_t x)
 void appendInt16(int16_t x)
 void appendInt8(int8_t x)
 int64_t readInt64()
 int32_t readInt32()
 int16_t readInt16()
 int8_t readInt8()
 int64_t peekInt64() const

 int32_t peekInt32() const -->尝试从缓冲区获取4字节数据,进行网络字节序转换为整型
                              但是数据并不从缓冲区删除
 int16_t peekInt16() const
 int8_t peekInt8() const
 void prependInt64(int64_t x)
 void prependInt32(int32_t x)
 void prependInt16(int16_t x)
 void prependInt8(int8_t x)
 void prepend(const void* /*restrict*/ data, size_t len)
private:
 std::vector<char> buffer_;
 size_t readerIndex_;
 size_t writerIndex_;
 static const char kCRLF[];
};
🔥server.cpp🔥
🏀_baseloop的作用

在 muduo 网络库中,muduo::net::EventLoop _baseloop; 声明了一个 EventLoop 类型的对象 _baseloop,它是整个事件驱动模型的核心组件之一,主要作用如下:

🔥client.cpp🔥
⚽server.cpp p22中bind的作用
makefile


七、😉C++11 异步操作

7-1 🧂std::future

std::future是C++11标准库中的⼀个模板类,它表⽰⼀个异步操作的结果 。当我们在多线程编程中使⽤异步任务时,std::future可以帮助我们在需要的时候获取任务的执⾏结果。std::future的⼀个重要特性是能够阻塞当前线程 ,直到异步操作完成,从⽽确保我们在获取结果时不会遇到未完成的操作

std::future 本质上不是一个异步任务,而是一个辅助我们获取异步结果的东西

应用场景

1.异步任务 : 当我们需要在后台执⾏⼀些耗时操作时,如⽹络请求或计算密集型任务等,std::future 可以⽤来表⽰这些异步任务的结果。通过将任务与主线程分离,我们可以实现任务的并⾏处理,从⽽提⾼程序的执⾏效率

2.并发控制: 在多线程编程中,我们可能需要等待某些任务完成后才能继续执⾏其他操作。通过使⽤ std::future,我们可以实现线程之间的同步,确保任务完成后再获取结果并继续执⾏后续操作

3.结果获取 :std::future提供了⼀种安全的⽅式来获取异步任务的结果。我们可以使⽤ std::future::get()函数来获取任务的结果,此函数会阻塞当前线程,直到异步操作完成。这样,在

调⽤get()函数时,我们可以确保已经获取到了所需的结果

std::future并不能单独使用,需要搭配一些能够执行异步任务的模板类或函数一起使用


7-2 🥓使用 std::async关联异步任务

异步执行一个函数,内部会创建线程执行异步任务, 返回一个future对象用于获取函数结果

std::async是⼀种将任务与std::future关联的简单⽅法。它创建并运⾏⼀个异步任务,并返回⼀个与该任务结果关联的std::future对象。默认情况下,std::async是否启动⼀个新线程,或者在等待future时,任务是否同步运⾏都取决于你给的 参数。这个参数为std::launch类型:

启动策略 执行时机 线程模型 核心特点
std::launch::deferred 延迟执行(调用 get()/wait() 时) 无新线程,在调用 get()/wait() 的线程执行 本质是「同步延迟」,而非真正的异步
std::launch::async 立即执行(std::async 调用时) 强制创建新线程执行任务 真正的异步,任务与主线程并行执行
`std::launch::deferred std::launch::async`(默认) 系统自动选择(大概率 async 灵活,但不可控,依赖编译器 / 系统实现
策略 适用场景 不适用场景
deferred 任务耗时短、希望延迟执行(如懒加载) 耗时任务、需要并行提升效率的场景
async 耗时任务、需要并行执行、希望立即启动 系统线程资源紧张、任务无需立即执行
deferred async 对执行时机无严格要求、希望适配系统状态 依赖任务执行时机 / 线程模型的场景
7-2-1 🚐async.cpp

7-3 🥚x使用std::packaged_task 和 std::future配合

std:packaged task类模板:是一个任务包,是对一个函数进行二次封装,封装成为一个可调用对象作为任务放到其他线程执行的。为一个函数生成一个异步任务对象(可调用对象),用于在其他线程中执行

std::packaged_task就是将任务和 std::feature 绑定在⼀起的模板,是⼀种对任务的封装。我们可以通过std::packaged_task对象获取任务相关联的std::feature对象,通过调⽤get_future()⽅法获得。std::packaged_task的模板参数是函数签名。

可以把std::future和std::async看成是分开的, ⽽ std::packaged_task则是⼀个整体。

7-3-1 🚎packaged_task.cpp

7-4 🍳使用std::promise 和 std::future配合

std.:.promise类模板:实例化的对象可以返回一个future,在其他线程中向promise对象设置数据,其他线程的关联future就可以获取数据

std::promise提供了⼀种设置值的⽅式,它可以在设置之后通过相关联的std::future对象进⾏读取。换种说法就是之前说过std::future可以读取⼀个异步函数的返回值了, 但是要等待就绪, ⽽std::promise 就提供⼀种 ⽅式⼿动让 std::future就绪

7-4-1 🚑promise.cpp

八、😊项目设计

8-1 🧇理解项目功能

实现rpc(远端调用)思想上并不复杂,甚⾄可以说是简单,其实就是客⼾端想要完成某个任务的处理,但是这个处理的过程并不⾃⼰来完成,⽽是,将请求发送到服务器上,让服务器来帮其完成处理过程,并返回结果,客⼾端拿到结果后返回。

上图的模型中,是⼀种多对⼀或⼀对⼀的关系,⼀旦服务端掉线,则客⼾端⽆法进⾏远端调⽤,

且其服务端的负载也会较⾼,因此在rpc 实现中,我们不仅要实现其基本功能,还要再进⼀步,实现分布式架构的rpc

分布式架构:简单理解就是由多个节点组成的⼀个系统,这些节点通常指的是服务器,将不同的业务或者同⼀个业务拆分分布在不同的节点上,通过协同⼯作解决⾼并发的问题,提⾼系统扩展性和可⽤性。

其实现思想也并不复杂,也就是在原来的模型基础上,增加⼀个注册中心,基于注册中⼼不同的服务提供服务器向注册中⼼进⾏服务注册,相当于告诉注册中⼼⾃⼰能够提供什么服务,⽽客⼾端在进⾏远端调⽤前,先通过注册中⼼进⾏服务发现,找到能够提供服务的服务器,然后发起调⽤。

⽽其次的发布订阅功能,则是依托于多个客⼾端围绕服务端进⾏消息的转发。

不过单纯的消息转发功能,并不能满⾜于⼤部分场景的需要,因此会在其基础上实现基于主题订阅的转发。

基于以上功能的合并,我们可以得到⼀个实现所有功能的结构图

在上图的结构中,我们甚⾄可以让每⼀个Server作为备⽤注册中⼼形成分布式架构,⼀旦⼀个注册中⼼下线,可以向备⽤中⼼进⾏注册以及请求,且在此基础上客⼾端在请求Rpc服务的时候,因为可以有多个rpc-provider可选,因此可以实现简单的负载均衡策略,且基于注册中⼼可以更简便实现发布订阅的功能。

项目的三个主要功能:

1.rpc调用

2.服务的注册与发现以及服务的下线/上线通知

3.消息的发布订阅

8-2 🥞框架设计

8-2-1. 🚒服务端模块划分

服务端的功能请求:

1.基于网络通信接收客户端的请求,提供rpc服务

2.基于网络通信接收客户端的请求,提供服务注册与发现,上线&下线通知

3.基于网络通信接收客户端的请求,提供主题操作(创建/删除/订阅/取消),消息发布

在服务端的模块划分中,基于以上理解的功能,可以划分出这么⼏个模块:

1.Network:网络通信模块

2.Protocol:应用层通信协议模块

3.Dispatcher:消息分发处理模块

4.RpcRouter:远端调用路由功能模块

5.Publish-Subscovery:发布订阅功能模块

6.Registry-Discovery:服务注册/发现/上线/下线功能模块

7.Server:基于以上模块整合而出的服务端模块

1.Network

该模块为⽹络通信模块,实现底层的⽹络通信功能,这个模块本质上也是⼀个⽐较复杂庞⼤的模块,该模块我们将使⽤陈硕⼤佬的Muduo库来进⾏搭建。

2.Protocol

应⽤层通信协议模块的存在意义:解析数据,解决通信中有可能存在的粘包问题,能够获取到⼀条完整的消息。

在前边的muduo库基本使⽤中,我们能够知道想要让⼀个服务端/客⼾端对消息处理,就要设置⼀个onMessage的回调函数,在这个函数中对收到的数据进⾏应⽤层协议处理。

⽽Protocol模块就是是⽹络通信协议模块的设计,也就是在⽹络通信中,我们必须设计⼀个应⽤层的⽹络通信协议出来,以解决⽹络通信中可能存在的粘包问题,⽽解决粘包有三种⽅式:特殊字符间隔,定⻓,LV格式。

⽽项⽬中我们将使⽤LV格式来定义应⽤层的通信协议格式

Length:该字段固定4字节⻓度,⽤于表⽰后续的本条消息数据⻓度。

MType:该字段为Value中的固定字段,固定4字节⻓度,⽤于表⽰该条消息的类型。

◦ Rpc调⽤请求/响应类型消息

◦ 发布/订阅/取消订阅/消息推送类型消息

◦ 主题创建/删除类型消息

◦ 服务注册/发现/上线/下线类型消息

IDLength: 为消息中的固定字段,该字段固定4字节⻓度,⽤于描述后续ID字段的实际⻓度。

MID:在每条消息中都会有⼀个固定字段为ID字段 ,⽤于唯⼀标识消息,ID字段⻓度不固定。

Body:消息主题正⽂数据字段,为请求或响应的实际内容字段。

3.Dispatcher

模块存在的意义:区分消息类型,根据不同的类型,调用不同的业务处理函数进行消息处理。

当muduo库底层通信收到数据后,在onMessage回调函数中对数据进⾏应⽤层协议解析,得到⼀条实际消息载荷后,我们就该决定这条消息代表这客⼾端的什么请求,以及应该如何处理。

因此,我们设计出了Dispatcher模块,作为⼀个分发模块,这个模块内部会保存有⼀个hash_map<消息类型, 回调函数>,以此由使⽤者来决定哪条消息⽤哪个业务函数进⾏处理,当收到消息后,在该模块找到其对应的处理回调函数进⾏调⽤即可。

消息类型:

1.rpc请求&响应

2.服务注册/发现/上线/下线请求&响应

3.主题创建/删除/订阅/取消订阅请求&响应,消息发布的请求&响应

4.RpcRouter

RpcRouter模块存在的意义:提供rpc请求的处理回调函数,内部所要实现的功能,分辨出客⼾端请求的服务进行处理得到结果进行响应。

rpc请求中,最关键的两个点

• 请求⽅法名称

• 请求对应要处理的参数信息

在Rpc远端调⽤中,⾸先将客⼾端到服务端的通信链路打通,然后将⾃⼰所需要调⽤的服务名称,以及参数信息传递给服务端,由服务端进⾏接收处理,并返回结果。

⽽,不管是客⼾端要传递给服务端的服务名称以及参数信息,或者服务端返回的结果,都是在上边

Protocol中定义的Body字段中,因此Body字段中就存在了另⼀层的正⽂序列化/反序列化过程。

序列化⽅式有很多种,鉴于当前我们是json-rpc,因此这个序列化过程我们就初步使⽤json序列化来进⾏,所定义格式如下:

cpp 复制代码
//RPC-request
{
 "method" : "Add",
 "parameters" : {
 "num1" : 11,
 "num2" : 22
 }
}

//RPC-response
{
 "rcode" : OK,
 "result": 33
}

{
 "rcode" : ERROR_INVALID_PARAMETERS
}

需要注意的是,在服务端,当接收到这么⼀条消息后,Dispatcher模块会找到该Rpc请求类型的回调处理函数进⾏业务处理,但是在进⾏业务处理的时候,也是只会将 parameters 参数字段传⼊回调函数中进⾏处理。

然⽽,对服务端来说,应该从传⼊的Json::Value对象中,有什么样的参数,以及参数信息是否符合⾃⼰所提供的服务的要求,都应该有⼀个检测,是否符合要求,符合要求了再取出指定字段的数据进⾏处理。

因此,对服务端来说,在进⾏服务注册的时候,必须有⼀个服务描述,以代码段中的Add请求为例,该服务描述中就应该描述:

◦ 服务名称: Add,

◦ 参数名称: num1,是⼀个整形

◦ 参数名称: num2,是⼀个整形,

◦ 返回值类型:整形

有了这个描述,在回调函数中就可以先对传⼊的参数进⾏校验,没问题了则取出指定字段数据进⾏处理并返回结果

基于以上理解,在实现该模块时,该有以下设计:

1. 该模块必须具备⼀个Rpc路由管理,其中包含对于每个服务的参数校验功能

2. 该模块必须具备⼀个⽅法名称和⽅法业务回调的映射

3. 该模块必须向外提供 Rpc请求的业务处理函数。

5.Publish-Subscovery

Publish-Subscribe模块存在的意义:针对发布订阅请求进⾏处理,提供⼀个回调函数设置给 Dispatcher模块。

发布订阅所包含的请求操作:

• 主题的创建

• 主题的删除

• 主题的订阅

• 主题的取消订阅

• 主题消息的发布

在当前的项⽬中,我们也实现⼀个简单的发布订阅功能,该功能是围绕多个客⼾端与⼀个服务端来展开的。

即,任意⼀个客⼾端在发布或订阅之前先创建⼀个主题,⽐如在新闻发布中我们创建⼀个⾳乐新闻主题,哪些客⼾端希望能够收到⾳乐新闻相关的消息,则就订阅这个主题,服务端会建⽴起该主题与客⼾端之间的联系。

当某个客⼾端向服务端发布消息,且发布消息的⽬标主题是⾳乐新闻主题,则服务端会找出订阅了该主题的客⼾端,将消息推送给这些客⼾端。

既然涉及到⽹络通信,那就先将通信消息的正⽂格式定义出来:

cpp 复制代码
//Topic-request
{
 "key" : "music", //主题名称
 // 主题操作类型
 "optype" : 
TOPIC_CRAETE/TOPIC_REMOVE/TOPIC_SUBSCRIBE/TOPIC_CANCEL/TOPIC_PUBLISH,
 //TOPIC_PUBLISH请求才会包含有message字段
 "message" : "Hello World"
}


//Topic-response
{
 "rcode" : OK,
}

{
 "rcode" : ERROR_INVALID_PARAMETERS,
}

设计实现:

  1. 该模块必须具备⼀个主题管理,且主题中需要保存订阅了该主题的客⼾端连接

a. 主题收到⼀条消息,需要将这条消息推送给订阅了该主题的所有客⼾端

2. 该模块必须具备⼀个订阅者管理,且每个订阅者描述中都必须保存⾃⼰所订阅的主题名称

a. ⽬的是为了当⼀个订阅客⼾端断开连接时,能够找到订阅信息的关联关系,进⾏删除

3. 该模块必须向外提供 主题创建/销毁,主题订阅/取消订阅,消息发布处理的业务处理函数

6.Registry-Discovery

Registry-Discovery模块存在的意义:就是针对服务注册与发现请求的处理。

• 服务注册/发现类型请求中的详细划分

◦ 服务注册:服务provider告诉中转中⼼,⾃⼰能提供哪些服务

◦ 服务发现:服务caller询问中转中⼼,谁能提供指定服务

◦ 服务上线:在⼀个provider上线了指定服务后,通知发现过该服务的客⼾端有个provider可以提供该服务

服务下线:在⼀个provider断开连接,通知发现过该服务的caller,谁下线了哪个服务

服务注册模块,该模块主要是为了实现分布式架构⽽存在,让每⼀个rpc客⼾端能够从不同的节点主机上获取⾃⼰所需的服务,让业务更具扩展性,系统更具健壮性。

⽽为了能够让rpc-caller知道有哪些rpc-provider能提供⾃⼰所需服务,那么就需要有⼀个注册中⼼让这些rpc-provider去注册登记⾃⼰的服务,让rpc-caller来发现这些服务。

因此,在我们的服务端功能中,还需实现服务的注册/发现,以及服务的上线/下线功能。

cpp 复制代码
//RD--request
{
 //SERVICE_REGISTRY-Rpc-provider进⾏服务注册
 //SERVICE_DISCOVERY - Rpc-caller进⾏服务发现
//SERVICE_ONLINE/SERVICE_OFFLINE 在provider下线后对caller进⾏服务上下线通知
 "optype" : 
SERVICE_REGISTRY/SERVICE_DISCOVERY/SERVICE_ONLINE/SERVICE_OFFLINE,
 "method" : "Add",
 //服务注册/上线/下线有host字段,发现则⽆host字段
 "host" : {
 "ip" : "127.0.0.1",
 "port" : 9090
 }
}
//Registry/Online/Offline-response
{
 "rcode" : OK,
}
//error-response
{
 "rcode" : ERROR_INVALID_PARAMETERS,
}
//Discovery-response
{
 "method" : "Add",
 "host" : [
 {"ip" : "127.0.0.1","port" : 9090},
 {"ip" : "127.0.0.2","port" : 8080}
 ]
}

该模块的设计如下:

1️⃣必须具备⼀个服务发现者的管理:

a. ⽅法与发现者:当⼀个客⼾端进⾏服务发现的时候,进⾏记录谁发现过该服务,当有⼀个新的提供者上线的时候,可以通知该发现者

b. 连接与发现者 :当⼀个发现者断开连接了,删除关联关系,往后就不需要通知了

2️⃣必须具备⼀个服务提供者的管理:

a. 连接与提供者:当⼀个提供者断开连接的时候,能够通知该提供者提供的服务对应的发现者,该主机的该服务下线了

b. ⽅法与提供者:能够知道谁的哪些⽅法下线了,然后通知发现过该⽅法的客⼾端

3️⃣必须向Dispatcher模块提供⼀个服务注册/发现的业务处理回调函数

这样,当⼀个rpc-provider登记了服务,则将其管理起来,当rpc-caller进⾏服务发现时,则将保存的对应服务所对应的主机信息,响应给rpc-caller。

⽽,当中途⼀个rpc-provider上线登记服务时,则可以给进⾏了对应服务发现的rpc-caller进⾏服务上线通知,通知rpc-caller当前多了⼀个对应服务的rpc-provider。

同时,当⼀个rpc-provider下线时,则可以找到进⾏了该服务发现的rpc-caller进⾏服务的下线通知。

7.Server

当以上的所有功能模块都完成后,我们就可以将所有功能整合到⼀起来实现服务端程序了

• RpcServer:rpc功能模块与⽹络通信部分结合。

• RegistryServer:服务发现注册功能模块与⽹络通信部分结合

• TopicServer:发布订阅功能模块与⽹络通信部分结合。

8-2-2.🚚客户端模块划分

在客⼾端的模块划分中,基于以上理解的功能,可以划分出这么⼏个模块

1. Protocol:应⽤层通信协议模块

2. Network:⽹络通信模块

3. Dispatcher:消息分发处理模块

4. Requestor:请求管理模块

5. RpcCaller:远端调⽤功能模块

6. Publish-Subscribe:发布订阅功能模块

7. Registry-Discovery:服务注册/发现/上线/下线功能模块

8. Client:基于以上模块整合⽽出的客⼾端模块

4.Requestor

Requestor 模块存在的意义:针对客户端的每⼀条请求进行管理,以便于对请求对应的响应做出合适的操作。

⾸先,对于客⼾端来说,不同的地⽅在于,更多时候客⼾端是请求⽅,是主动发起请求服务的⼀⽅, ⽽在多线程的⽹络通信中,多线程下,针对多个请求进⾏响应可能会存在时序的问题,这种情况下,则我们⽆法保证⼀个线程发送⼀个请求后,接下来接收到的响应就是针对⾃⼰这条请求的响应,这种情况是⾮常危险的⼀种情况。

其次,类似于Muduo库这种异步IO⽹络通信库,通常IO操作都是异步操作,即发送数据就是把数据放⼊发送缓冲区,但是什么时候会发送由底层的⽹络库来进⾏协调,并且也并不会提供recv接⼝,⽽是在连接触发可读事件后,IO读取数据完成后调⽤处理回调进⾏数据处理,因此也⽆法直接在发送请求后去等待该条请求的响应。

针对以上问题,我们则创建出当前的请求管理模块来解决,它的思想也⾮常简单,就是给每⼀个请求都设定⼀个请求ID,服务端进⾏响应的时候标识响应针对的是哪个请求(也就是响应信息中会包含请求ID)因此客⼾端这边我们不管收到哪条请求的响应,将数据存储⼊⼀则hash_map中,以请求ID作为映射,并向外提供获取指定请求ID响应的阻塞接⼝,这样只要在发送请求的时候知道⾃⼰的请求 ID,那么就能获取到⾃⼰想要的响应,⽽不会出现异常。

针对这个思想,我们再进⼀步,可以将每个请求进⼀步封装描述,添加⼊异步的future控制,或者设置回调函数的⽅式,在不仅可以阻塞获取响应,也可以实现异步获取响应以及回调处理响应。

5.RpcCaller

RpcCaller模块存在的意义:向用户提供进行rpc调用的模块。

Rpc服务调⽤模块,这个模块相对简单,只需要向外提供⼏个rpc调⽤的接⼝,内部实现向服务端发送请求,等待获取结果即可 ,稍微⿇烦⼀些的是Rpc调⽤我们需要提供多种不同⽅式的调⽤:

1. 同步调⽤:发起调⽤后,等收到响应结果后返回

2. 异步调⽤:发起调⽤后⽴即返回,在想获取结果的时候进⾏获取

3. 回调调⽤:发起调⽤的同时设置结果的处理回调,收到响应后⾃动对结果进⾏回调处理

6.Publish-Subscribe

Publish-Subscribe模块存在意义:向⽤⼾提供发布订阅所需的接⼝,针对推送过来的消息进⾏处理。

  • 发布订阅稍微能复杂⼀丢丢,因为在发布订阅中有两种⻆⾊,⼀个客⼾端可能是消息的发布者,也可能是消息的订阅者。
  • ⽽且不管是哪个⻆⾊都是对主题进⾏操作,因此其中也包含了主题的相关操作,⽐如,要发布⼀条消息需要先创建主题。
  • 且⼀个订阅者可能会订阅多个主题,每个主题的消息可能都会有不同的处理⽅式,因此需要有订阅者主题回调的管理。
7.Registry-Discovery

服务注册和发现模块需要实现的功能会稍微复杂⼀些,因为分为两个⻆⾊来完成其功能

1️⃣注册者:作为Rpc服务的提供者,需要向注册中⼼注册服务,因此需要实现向服务器注册服务的功能

2️⃣发现者:作为Rpc服务的调⽤者,需要先进⾏服务发现,也就是向服务器发送请求获取能够提供指定服务的主机地址,获取地址后需要管理起来留⽤,且作为发现者,需要已关注注册中⼼发送过来的服务上线/下线消息,以及时对已经下线的服务和主机进⾏管理。

8.Client

将以上模块进⾏整合就可以实现各个功能的客⼾端了。

RegistryClient:服务注册功能模块与⽹络通信客⼾端结合

• DiscoveryClient:服务发现功能模块与⽹络通信客⼾端结合

• RpcClient:DiscoveryClient & RPC功能模块与⽹络通信客⼾端结合

• TopicClient:发布订阅功能模块与⽹络通信客⼾端结合

框架设计:

在当前项⽬的实现中,我们将整个项⽬的实现划分为三层来进⾏实现

1️⃣ 抽象层:将底层的⽹络通信以及应⽤层通信协议以及请求响应进⾏抽象,使项⽬更具扩展性和灵活性。

2️⃣具象层:针对抽象的功能进⾏具体的实现。

3️⃣业务层:基于抽象的框架在上层实现项⽬所需功能。

8-2-3.🚛抽象层

在咱们的项⽬实现中,⽹络通信部分采⽤了第三⽅库Muduo库,以及通信协议使⽤了LV格式的通信协议解决粘包问题,数据正⽂中采⽤了Json格式进⾏序列化和反序列化,⽽这⼏⽅⾯我们都可能会存在继续优化 的可能,甚⾄在序列化⽅⾯不⼀定⾮要采⽤Json,因此在设计项⽬框架的时候,我们对于底层通信部分相关功能先进⾏抽象,形成⼀层抽象层,⽽上层业务部分根据抽象层来完成功能,这样的好处是在具体的底层功能实现部分,我们可以实现插拔式的模块化替换,以此来提⾼项⽬的灵活性和扩展性。

8-2-4.🚜具象层

具象层就是针对抽象的具体实现。

⽽具体的实现也⽐较简单,从抽象类派⽣出具体功能的派⽣类,然后在内部实现各个接⼝功能即可。

• 基于Muduo库实现⽹络通信部分抽象

• 基于LV通信协议实现Protocol部分抽象

不过这⼀层中⽐较特殊的是,我们需要针对不同的请求,从BaseMessage中派⽣出不同的请求和响应类型,以便于在针对指定消息处理时,能够更加轻松的获取或设置请求及响应中的各项数据元素。

8-2-5.🚘业务层

业务层就是基于底层的通信框架,针对项⽬中具体的业务功能的实现了 ,⽐如Rpc请求的处理,发布订阅请求的处理以及服务注册与发现的处理等等。

Rpc
发布订阅
服务注册&发现

🌟整体框架设计🌟

九、😋项目实现

9-1 🧈常用的零碎功能实现

9-1-1.🚔简单日志宏实现

意义:快速定位程序运⾏逻辑出错的位置。

项⽬在运⾏中可能会出现各种问题,出问题不可怕,关键的是要能找到问题,并解决问题。

解决问题的⽅式:

• gdb调试:逐步调试过于繁琐,缓慢。主要⽤于程序崩溃后的定位。

• 系统运⾏⽇志分析:在任何程序运⾏有可能逻辑错误的位置进⾏输出提⽰,快速定位逻辑问题的位置。

9-2 🍞Json序列化/反序列化
9-3 🥐UUID生成

UUID(Universally Unique Identifier), 也叫通⽤唯⼀识别码,通常由32位16进制数字字符组成。 UUID的标准型式包含32个16进制数字字符,以连字号分为五段,形式为8-4-4-4-12的32个字符, 如:550e8400-e29b-41d4-a716-446655440000。 在这⾥,uuid⽣成,我们采⽤⽣成8个随机数字,加上8字节序号,共16字节数组⽣成32位16进制字符的组合形式来确保全局唯⼀的同时能够根据序号来分辨数据。

🔥detail.hpp🔥

核心设计思路

部分 实现方式 作用
随机数部分 硬件随机数(random_device)+ MT19937 保证不同机器 / 进程生成的 UUID 不重复(真随机种子避免伪随机重复)
序列部分 std::atomic<size_t> 原子递增 保证同一进程内生成的 UUID 不重复,且多线程调用时无竞争(线程安全)
格式化 setw(2)+setfill('0')+hex 统一格式为 2 位 16 进制,避免出现 1 位的情况(如 505
位运算解析 (cur >> (i*8)) & 0xFF 将 8 字节的 size_t 拆分为单个字节,保证序列部分的字节顺序统一

9-2 🍞项目消息类型字段信息定义

9-2-1.🚖请求字段宏定义

•消息ID:
•消息类型:
•消息正⽂
① Rpc请求
⽅法名称
⽅法参数
②发布订阅相关请求
主题名称
操作类型
主题消息
③服务操作相关请求
⽅法名称
操作类型
主机信息
IP地址
PORT端⼝
④响应码
⑤Rpc响应

▪ 调⽤结果

cpp 复制代码
#define KEY_METHOD "method"
 #define KEY_PARAMS "parameters"
 #define KEY_TOPIC_KEY "topic_key"
 #define KEY_TOPIC_MSG "topic_msg"
 #define KEY_OPTYPE "optype"
 #define KEY_HOST "host"
 #define KEY_HOST_IP "ip"
 #define KEY_HOST_PORT "port"
 #define KEY_RCODE "rcode"
 #define KEY_RESULT "result"
9-2-2.🚍消息类型定义

• Rpc请求 & 响应

• 主题操作请求 & 响应:

• 消息发布请求 & 响应

• 服务操作请求 & 响应:

cpp 复制代码
enum class MType {
 REQ_RPC = 0,
 RSP_RPC,
 REQ_TOPIC,
 RSP_TOPIC,
 REQ_SERVICE,
 RSP_SERVICE
};
9-2-3.🦽响应码类型定义

• 成功处理

• 解析失败

• 消息中字段缺失或错误导致⽆效消息

• 连接断开

• ⽆效的Rpc调⽤参数

• Rpc服务不存在

• ⽆效的Topic操作类型

• 主题不存在

• ⽆效的服务操作类型

cpp 复制代码
enum class RCode {
 RCODE_OK = 0,
 RCODE_PARSE_FAILED,
 RCODE_ERROR_MSGTYPE,
 RCODE_INVALID_MSG,
 RCODE_DISCONNECTED,
 RCODE_INVALID_PARAMS,
 RCODE_NOT_FOUND_SERVICE,
 RCODE_INVALID_OPTYPE,
 RCODE_NOT_FOUND_TOPIC,
 RCODE_INTERNAL_ERROR
};
static std::string errReason(RCode code) {
 static std::unordered_map<RCode, std::string> err_map = {
 {RCode::RCODE_OK, "成功处理!"},
 {RCode::RCODE_PARSE_FAILED, "消息解析失败!"},
 {RCode::RCODE_ERROR_MSGTYPE, "消息类型错误!"},
 {RCode::RCODE_INVALID_MSG, "⽆效消息"},
 {RCode::RCODE_DISCONNECTED, "连接已断开!"},
 {RCode::RCODE_INVALID_PARAMS, "⽆效的Rpc参数!"},
 {RCode::RCODE_NOT_FOUND_SERVICE, "没有找到对应的服务!"},
 {RCode::RCODE_INVALID_OPTYPE, "⽆效的操作类型"},
 {RCode::RCODE_NOT_FOUND_TOPIC, "没有找到对应的主题!"},
 {RCode::RCODE_INTERNAL_ERROR, "内部错误!"}
 };
 auto it = err_map.find(code);
 if (it == err_map.end()) {
 return "未知错误!";
 }
 return it->second;
}
9-2-4.🦼RPC请求类型定义

• 同步请求:等待收到响应后返回

• 异步请求:返回异步对象,在需要的时候通过异步对象获取响应结果(还未收到结果会阻塞)

• 回调请求:设置回调函数,通过回调函数对响应进⾏处理

cpp 复制代码
enum class RType {
 REQ_ASYNC = 0,
 REQ_CALLBACK
};
9-2-5.🛹主题操作类型定义

• 主题创建

• 主题删除

• 主题订阅

• 主题取消订阅

• 主题消息发布

cpp 复制代码
enum class TopicOptype {
 TOPIC_CREATE = 0,
 TOPIC_REMOVE,
 TOPIC_SUBSCRIBE,
 TOPIC_CANCEL,
 TOPIC_PUBLISH
};
9-2-6.🛼服务操作类型定义

• 服务注册

• 服务发现

• 服务上线

• 服务下线

cpp 复制代码
enum class ServiceOptype {
 SERVICE_REGISTRY = 0,
 SERVICE_DISCOVERY,
 SERVICE_ONLINE,
 SERVICE_OFFLINE,
 SERVICE_UNKNOW
};
🔥fields.hpp🔥

9-3 🥐通信抽象实现

•BaseMessage
BaseBuffer
BaseProtocol
BaseConnection
BaseServer
BaseClient

二、逐个核心抽象类介绍

抽象类名 核心定位 核心接口 / 功能 设计目的
BaseMessage 消息数据模型抽象(RPC 通信的「数据载体」) - set/getMsgId():消息 ID(唯一标识)- set/getMsgType():消息类型(RPC / 主题 / 服务)- set/getBody():消息正文- set/getRCode():响应码 统一封装所有通信消息的通用字段,屏蔽不同业务消息的结构差异,让上层专注业务逻辑
BaseBuffer 字节缓冲区抽象(数据序列化的「内存容器」) - append():向缓冲区追加数据(字符串 / 字节数组)- read():从缓冲区读取数据- clear():清空缓冲区- size():获取缓冲区长度 屏蔽底层内存操作细节,提供安全、高效的字节数据存储,适配「序列化→网络传输」的流程
BaseProtocol 协议编解码抽象(通信协议的「规则定义」) - encode(BaseMessage*):将消息编码为字节缓冲区- decode(BaseBuffer*):将字节缓冲区解码为消息- getProtocolType():获取协议类型(JSON/Protobuf) 解耦「数据格式」与「传输」,支持灵活替换协议(如 JSON 换 Protobuf),核心是实现「消息↔字节」的转换
BaseConnection 连接抽象(网络连接的「操作接口」) - connect():建立连接- disconnect():断开连接- send(BaseMessage*):发送消息(自动编解码)- recv():接收消息(自动解码)- isConnected():判断连接状态 屏蔽底层网络类型(TCP/UDP/UNIX 域套接字)差异,统一连接操作接口,支持「同步 / 异步」收发
BaseServer 服务端抽象(服务端的「核心骨架」) - start():启动服务端(绑定端口、监听)- stop():停止服务端- onAccept(BaseConnection*):处理新连接- onMessage(BaseConnection*, BaseMessage*):处理收到的消息- setProtocol(BaseProtocol*):设置协议编解码器 定义服务端通用行为,屏蔽不同 IO 模型(阻塞 / 非阻塞 / IO 多路复用)差异,支持多连接管理
BaseClient 客户端抽象(客户端的「核心骨架」) - init():初始化客户端(配置服务端地址)- connect():连接服务端- sendRequest(BaseMessage*):发送请求- recvResponse():接收响应- setCallback():设置响应回调(异步场景) 定义客户端通用行为,支持「同步 / 异步」调用,屏蔽不同网络库(如 epoll/libevent)的差异
🔥abstract.hpp🔥

9-4 🥨消息具体实现

•JsonMessage
•JsonRequest & JsonResponse
•RpcRequest & RpcResponse
•TopicRequest & TopicResponse
•ServiceRequest & ServiceResponse

类名 核心定位 核心字段 / 功能 设计目的
JsonMessage JSON 消息基础类(所有 JSON 消息的通用父类) - 封装 JSON 序列化 / 反序列化的通用逻辑(调用之前的 JSON 工具类)- 实现 BaseMessageserialize()/unserialize() 基础接口- 提供 get/set Json::Value 的通用方法 抽离所有 JSON 消息的重复逻辑(如序列化 / 反序列化),避免子类重复编码
JsonRequest JSON 请求基类(所有请求类的父类) - 继承 JsonMessage- 封装请求类的通用属性(如请求 ID、操作类型)- 实现请求类的通用合法性校验(check() 统一所有请求消息的基础属性和校验逻辑(如请求必须有 ID、操作类型)
JsonResponse JSON 响应基类(所有响应类的父类) - 继承 JsonMessage- 封装响应类的通用属性(响应码 RCode、错误描述)- 提供 setResult()/getResult() 通用方法 统一所有响应消息的基础属性(如响应码、错误信息),简化结果返回逻辑
RpcRequest RPC 调用请求消息(继承 JsonRequest - 核心字段:方法名(KEY_METHOD)、参数列表(KEY_PARAMS)、调用类型(RType:同步 / 异步)- 重写 check():校验方法名、参数是否存在- 适配 MType::REQ_RPC 封装 RPC 调用的请求参数,是客户端向服务端发起 RPC 调用的核心数据载体
RpcResponse RPC 调用响应消息(继承 JsonResponse - 核心字段:调用结果(KEY_RESULT)、响应码(RCode)- 重写 check():校验响应码、结果是否合法- 适配 MType::RSP_RPC 封装 RPC 调用的返回结果,是服务端向客户端返回调用结果的核心数据载体
TopicRequest 发布订阅请求消息(继承 JsonRequest - 核心字段:主题名(KEY_TOPIC_KEY)、操作类型(TopicOptype:订阅 / 发布 / 取消)、主题消息(KEY_TOPIC_MSG)- 重写 check():校验主题名、操作类型是否合法- 适配 MType::REQ_TOPIC 封装发布订阅的请求参数,是客户端向服务端发起主题操作的核心数据载体
TopicResponse 发布订阅响应消息(继承 JsonResponse - 核心字段:主题名(KEY_TOPIC_KEY)、操作结果(成功 / 失败)、响应码(RCode)- 重写 check():校验主题名、响应码是否合法- 适配 MType::RSP_TOPIC 封装发布订阅的操作结果,是服务端向客户端返回主题操作结果的核心数据载体
ServiceRequest 服务操作请求消息(继承 JsonRequest - 核心字段:服务名(KEY_METHOD)、操作类型(ServiceOptype:注册 / 发现 / 上下线)、主机信息(KEY_HOST:IP/Port)- 重写 check():校验服务名、主机信息是否合法- 适配 MType::REQ_SERVICE 封装服务管理的请求参数,是服务节点向注册中心发起服务操作的核心数据载体
ServiceResponse 服务操作响应消息(继承 JsonResponse - 核心字段:服务名(KEY_METHOD)、服务列表(发现服务时返回)、响应码(RCode)- 重写 check():校验服务名、响应码是否合法- 适配 MType::RSP_SERVICE 封装服务管理的操作结果,是注册中心向服务节点返回服务操作结果的核心数据载体

🔥message.hpp🔥

9-5 🥯通信-Muduo封装实现

防备一种情况:缓冲区中数据有很多很多。但是因为数据错误,导致数据又不足一条完整消息,也就是一条消息过大,针对这种情况,直接关闭连接

•MuduoBuffer
•MuduoProtocol
•MuduoConnection
•MuduoServer
•MuduoClient

🔥net.hpp🔥

整体调用流程:

9-6 🥖消息-不同消息封装实现

•JsonRequest
◦RpcRequest
◦TopicRequest
◦ServiceRequest
•JsonResponse
◦RpcResponse
◦TopicResponse
◦ServiceResponse

9-7 🫓Dispatcher实现

服务端会收到不同类型的请求,客户端会收到不同类型的响应(因为请求和响应都具有多样性因此在回调函数中,就需要判断消息类型,根据不同类型的消息做出不同的处理;如果单纯使用if语句做分支处理,是一件非常不好的事

程序设计中需要遵守一个原则:开闭原则 --- 对修改关闭,对扩展开放

当后期维护代码或新增功能时:不去修改以前的代码,而是新增当前需要的代码

Dispatcher模块就是基于开闭原则设计的目的就是建立消息类型与业务回调函数的映射关系;如果后期新增功能,不需要修改以前的代码,只需要增加一个映射关系即可

🔥Dispatcher.hpp🔥

•注册消息类型-回调函数映射关系
•提供消息处理接⼝

9-8 🧀服务端-RpcRouter实现

•提供Rpc请求处理回调函数
•内部的服务管理
◦⽅法名称
◦参数信息
◦对外提供参数校验接⼝

在rpc请求中,可能会有大量不同的rpc请求:比如加法,翻译... 作为服务端,首先要对自己所能提供的服务进行管理,以便于收到请求后,能够明确判断自身能否提供客户端所请求的服务。

能提供服务,则调用接口进行处理,返回结果;

不能提供服务,则响应客户端请求的服务不存在。

RpcRouter模块:一共枚举四个类

1.枚举类:枚举出rpc请求参数的类型(布尔,整形,浮点型,浮点数,字符串,数组,对象)

2.服务描述类

1.业务回调函数 --- Add处理回调函数

2.参数信息描述 --- pair<参数字段名称,参数字段类型> {<"num1",int>,<"num2",int>}

3.返回值类型描述 --- int

4.提供参数校验接口 --- 针对请求中的参数,判断是否包含有num1字段,其类型是否是整形;处理逻辑:收到一个rpc请求后,取出方法名称,参数信息;通过方法名称Add,找到Add服务的描述对象,先进行参数校验,校验参数中是否有num1字段,且类型是整形... 判断都没问题则调用回调函数进行处理。

3.服务管理类服务端会提供很多方法服务,需要进行良好的管理

std::hash_map<方法名称,服务描述> 通过这个hash_map就可以很容易判断能否提供服务

4.对外RpcRouter类

1.服务注册接口; 2.提供给dispatcher模块的rpc请求处理回调函数

🔥rpc_router.hpp🔥
步骤拆解与价值
步骤 操作 失败响应码 核心价值
1 查找服务 RCODE_NOT_FOUND_SERVICE 快速判断服务端是否支持该 RPC 方法,避免无效执行
2 参数校验 RCODE_INVALID_PARAMS 提前过滤非法参数,保护业务层,避免业务层处理无效数据
3 执行业务 + 返回值校验 RCODE_INTERNAL_ERROR 执行核心业务逻辑,同时校验返回值类型,保证响应规范
4 构建成功响应 RCODE_OK 标准化响应格式,客户端可根据响应码快速判断请求结果

核心流程拆解(onRpcRequest 方法)

补充:整体架构中的定位

9-9 🥗服务端-Publish&Subscribe实现


•对外提供主题操作处理回调函数
•对外提供消息发布处理回调函数
•内部进⾏主题及订阅者的管理

🔥rpc_topic.hpp🔥

三、核心设计亮点

锁层级 保护对象 适用场景 优势
全局锁_mutex _topics/_subscribers 主题 / 订阅者的查找 / 创建 / 删除 保证全局映射表的并发安全
Topic 锁 subscribers集合 订阅者添加 / 移除、消息推送 仅阻塞当前主题操作,不影响其他主题
Subscriber 锁 topics集合 主题添加 / 移除 仅阻塞当前订阅者操作,不影响

9-10 🥙服务端-Registry&Discovery实现

1.为什么要注册服务,服务注册是要做什么❓

服务注册是要实现分布式系统,让系统更加强壮;

一个节点主机,将自己所能提供的服务,在注册中心进行登记。

2.为什么要服务发现,服务发现要做什么❓

rpc调用者需要知道哪个节点主机能够为自己提供指定服务;

服务发现其实就是询问注册中心,谁能为自己提供指定的服务,将节点信息给保存起来以待后用。

3.服务下线

当前使用长连接进行服务主机是否在线的判断,一旦服务提供方断开连接,查询这个主机提供了哪些服务,分析哪些调用者进行过这些服务发现,则进行服务下线通知。

4.服务上线

因为服务发现是一锤子买卖(调用方不会进行二次服务发现),因此一旦中途有新主机可以提供指定服务,调用方是不知道的;

因此,一旦某个服务上线了,则对发现过这个服务的主机进行一次服务上线通知。

服务端要能够提供服务注册,发现的请求业务处理

  1. 需要将 哪个服务 能够由 哪个主机提供 管理起来 hash<method, vector<provider>>
    实现当由 caller 进行服务发现的时候,告诉 caller 谁能提供指定的服务
  2. 需要将 哪个主机 发现过 哪个服务 管理起来
    当进行服务通知的时候,都是根据谁发现过这个服务,才会给谁通知 <method,vector<discoverer>>
  3. 需要将 哪个连接 对应哪个 服务提供者 管理起来 hash<conn, provider>
    当一个连接断开的时候,能够知道哪个主机的哪些服务下线了,然后才能给发现者通知 xxx 的 xxx 服务下线了
  4. 需要将 哪个连接 对应哪个 服务发现者 管理起来 hash<conn, discoverer>
    当一个连接断开的时候,如果有服务上线下线,就不需要给他进行通知了
🔥rpc_registry.hpp🔥


成员变量 类型 核心作用
_conns unordered_map<连接, Provider> 快速通过连接定位提供者(核心场景:连接断开时查找提供者)。
_providers unordered_map<方法名, 提供者集合> 快速按服务方法名查找所有可用提供者(核心场景:服务发现时返回地址列表)。
_mutex std::mutex 全局锁,保护上述两个映射的多线程安全操作(避免并发修改导致的数据错乱)。

3. 数据结构设计:适配核心场景的高效映射

成员变量 类型 核心作用
_conns unordered_map<连接, Discoverer> 快速通过连接定位发现者(核心场景:客户端断开连接时清理数据)。
_discoverers unordered_map<方法名, 发现者集合> 快速按服务方法名找到所有关注该服务的发现者(核心场景:上下线通知推送)。
_mutex std::mutex 全局锁,保护两个映射的多线程安全操作(避免并发修改导致的通知漏发 / 错发)。

与 ProviderManager 的协同关系

组件 管理对象 核心动作 协同场景
ProviderManager 服务提供者 注册 / 下线 / 地址查询 DiscovererManager 提供「服务 - 地址」数据(服务发现时)、触发上下线事件;
DiscovererManager 服务发现者 记录 / 清理 / 推送通知 基于 ProviderManager 的上下线事件,向客户端推送动态通知;

两者配合实现了「服务注册 - 发现 - 动态感知」的完整闭环:提供者注册 / 下线 → ProviderManager 处理数据 → 触发 DiscovererManager 推送通知 → 客户端感知服务变化。



9-11 🥪服务端-整合封装Server

三大核心组件的协同关系

组件 核心职责 典型场景 与其他组件的协同关系
RegistryServer 服务注册中心服务端 微服务集群的服务注册 / 发现 RpcServer 提供服务注册能力,为 RpcClient 提供服务发现能力,是集群的 "地址中心"
RpcServer RPC 服务提供方核心 同步远程服务调用(如订单 / 库存服务) 可选依赖 RegistryServer 完成服务注册,接收 RpcClient 的 RPC 调用请求并处理
TopicServer 发布 - 订阅主题服务端 异步事件通知 / 消息广播(如配置推送) 独立提供 Pub/Sub 能力,可与 RPC 服务配合(如 RPC 完成下单后,Topic 推送订单事件)
🔥rpc_server.hpp🔥

服务端类 核心能力 核心业务依赖 典型使用场景
RegistryServer 服务注册 / 发现 / 上下线通知 PDManager 微服务注册中心,管理所有服务节点
RpcServer RPC 方法注册 / 远程调用处理 RpcRouter+ 可选RegistryClient 业务服务提供方,对外暴露 RPC 接口
TopicServer 主题管理 / Pub/Sub 消息分发 TopicManager 消息中间件,实现异步事件通知



核心工作流程


9-12 🌮客户端-Requestor实现

•提供发送请求的接⼝
•内部进⾏请求&响应的管理

Requestor 是RPC 客户端的核心请求管理器,核心作用是统一封装 RPC 请求的发送逻辑,解决 "请求 - 响应精准关联" 问题,并支持同步、异步(带回调 / 不带回调)三种调用方式,是客户端实现灵活 RPC 调用的核心组件。

简单来说,它的核心使命是:
让客户端可以用「同步阻塞、异步 Future、异步回调」任意一种方式发送 RPC 请求,并在服务端响应返回时,自动把响应分发到对应的请求处理逻辑中(比如唤醒阻塞的同步调用、执行回调函数、给 Future 赋值)。

层级 核心组件 核心职责 Requestor 的角色
业务层 业务代码 定义 RPC 请求参数、处理响应结果 提供极简调用接口,屏蔽底层复杂度
中间层 Requestor 请求封装、rid 生成、上下文管理、响应分发 核心中间层,承上启下的核心枢纽
分发层 Dispatcher 消息类型路由(响应消息→Requestor) 响应处理的统一入口,依赖 Dispatcher 路由
网络层 BaseClient/Connection 底层 TCP 连接、消息收发、异常处理 依赖网络层发送请求,接收响应

协作组件 交互方式 核心价值
Dispatcher 1. Requestor 将onResponse注册为响应消息(如RSP_RPC)的回调;2. Dispatcher 收到响应后自动调用onResponse 解耦响应接收与处理,无需 Requestor 关心 "如何接收响应",只需关心 "如何处理响应"
BaseClient/Connection Requestor 调用网络层接口发送请求消息;网络层负责底层 TCP 传输,不关心请求内容 屏蔽网络细节(如重连、粘包拆包),Requestor 专注于请求 - 响应逻辑
RequestDescribe Requestor 创建 / 管理 / 删除该结构体,封装单次请求的全量上下文(rid、调用类型、Promise / 回调) 统一管理请求上下文,避免状态分散,保证线程安全
🔥requestor.hpp🔥

9-13 🌮客户端-RpcCaller实现

•提供Rpc请求接⼝

RpcCaller 是RPC 客户端的业务层适配封装类,核心作用是基于底层的 Requestor 封装更易用、更贴合业务的 JSON 级 RPC 调用接口,屏蔽框架层BaseMessage/RpcRequest/RpcResponse 等细节,让业务代码无需关注 RPC 通信的底层实现,只需聚焦 "方法名、JSON 参数、JSON 结果" 的业务逻辑。
简单来说,它是客户端业务代码与底层 RPC 通信框架之间的 "适配层"------ 把框架层的 "消息级调用" 转换为业务层的 "JSON 级调用",大幅降低业务开发的成本。

核心作用拆解

1. 屏蔽底层通信细节,简化业务调用

Requestor 处理的是通用的 BaseMessage 类型(框架层消息),而业务层只关心 "调用哪个方法、传什么 JSON 参数、拿什么 JSON 结果"。RpcCaller 做了以下封装:

  • 请求自动封装 :业务层传入 method(方法名)和 params(JSON 参数),RpcCaller 自动构建 RpcRequest 消息(设置唯一 ID、消息类型、方法名、参数),无需业务代码手动处理消息构造;
  • 响应自动解析 :把 Requestor 返回的 BaseMessage 响应,自动转换为 RpcResponse,提取其中的 JSON 结果(result),并处理类型转换、状态码校验等框架级逻辑;
  • 异常统一处理:集中处理 "消息类型转换失败、响应状态码错误" 等框架级异常,输出标准化日志,业务层只需判断调用是否成功,无需处理底层异常。

2. 统一封装三种 RPC 调用方式(适配业务场景)

基于 Requestor 的同步 / 异步能力,封装出业务层更易理解的三种调用方式,且所有接口都以 JSON 为数据载体:

调用方式 接口签名 业务价值
同步调用 call(conn, method, params, result) 业务层只需传入方法名、JSON 参数,直接获取 JSON 结果,像调用本地函数一样简单;
异步 Future 调用 call(conn, method, params, result)(result为std::futureJson::Value 非阻塞发起请求,业务层可通过 future.get() 在任意时机获取 JSON 结果;
异步回调调用 call(conn, method, params, cb)(cb为std::function<void(const Json::Value&)>) 响应返回时自动执行业务回调,无需主动轮询,适配高性能异步场景;

3. 适配层核心:完成 "框架层↔业务层" 的转换

RpcCaller 的核心逻辑是 "类型转换 + 回调适配",解决框架层与业务层的数据类型不匹配问题:

  • 请求侧 :把业务层的 "方法名 + JSON 参数" → 框架层的 RpcRequest 消息;
  • 响应侧 :把框架层的 BaseMessage 响应 → 业务层的 JSON 结果;
  • 回调侧 :把业务层的 "JSON 回调" → 框架层的 BaseMessage 回调(通过 Callback1),把业务层的 "JSON Future" → 框架层的 BaseMessage 回调(通过 Callback)。

4. 依赖注入设计,保证灵活性

构造函数接收 Requestor::ptr 作为参数(依赖注入),而非内部创建:

  • 解耦 RpcCaller 与 Requestor 的实例化逻辑,便于测试(如注入 Mock 版 Requestor);
  • 复用 Requestor 的核心能力(请求 - 响应关联、多调用方式支持),无需重复开发。

🔥rpc_caller.hpp🔥

9-14. 🌯户端-Publish&Subscribe实现

🔥rpc_topic.hpp🔥

9-15 🫔客户端-Registry&Discovery实现

客户端的功能比较分离,注册端跟发现端根本就不在同一个主机上。
因此客户端的注册与发现功能是完全分离的

  1. 作为服务提供者 --- 需要一个能够进行服务注册的接口
    连接注册中心,进行服务注册,
  2. 作为服务发现者 --- 需要一个能够进行服务发现的接口,需要将获取到的能够提供指定服务的主机信息管理起来hash<method, vector<host>> 一次发现,多次使用,没有的话再次进行发现。
    需要进行服务上线 / 下线通知请求的处理(需要向 dispatcher 提供一个请求处理的回调函数)

因为客户端在一次服务发现中,会一次获取多个能够提供服务的主机地址信息,到底请求谁合理?
负载均衡的思想:RR 轮转(一个一个请求,雨露均沾)

🔥rpc_registry.hpp🔥
4. 业务场景中的定位

在整个服务治理体系中,Provider 类的角色是「客户端侧的注册执行者」:

服务提供者业务代码 → 调用Provider::registryMethod → 发送注册请求 → 服务治理节点(PDManager)→ 返回响应 → Provider校验响应 → 告知业务代码注册结果



9-16 🥫客户端-整合封装Client

🔥rpc_client.hpp🔥



9-17 🍖整合封装RpcServer & RpcClient

十、🤔测试

🔥test_1🔥

test_client.cpp

test_server.cpp

2. 完整运行时序(按时间线)

时间点 服务端 客户端
T0 启动,监听 9090 端口,等待连接 初始化 RpcClient,连接 127.0.0.1:9090
T1 接收同步调用请求 → 解析参数 → 执行 Add (11,22) → 返回 33 发起同步调用 → 阻塞 → 收到 33 → 打印
T2 接收 Future 异步请求 → 解析参数 → 执行 Add (33,44) → 返回 77 发起 Future 调用 → 调用 get () 阻塞 → 收到 77 → 打印
T3 接收回调异步请求 → 解析参数 → 执行 Add (55,66) → 返回 121 发起回调调用 → 立即返回 → 休眠 1 秒
T4 - 收到 121 → 触发 callback → 打印 121
T5 保持监听 休眠结束 → 主线程退出

🔥test_2🔥

rpc_client.cpp

rpc_server.cpp

registry_server.cpp

一、先明确整体架构与核心角色

这三份代码构成了**「微服务 RPC 调用的最小闭环」**,核心角色分工如下:

角色 代码文件 核心职责
注册中心(RegistryServer) registry_server.cpp 1. 存储「方法名 ↔ 服务地址」映射;2. 处理服务端注册 / 心跳;3. 处理客户端发现请求
RPC 服务端(RpcServer) rpc_server.cpp 1. 实现业务方法(Add);2. 向注册中心注册服务;3. 监听端口处理 RPC 调用
RPC 客户端(RpcClient) rpc_client.cpp 1. 从注册中心发现服务地址;2. 基于发现的地址发起 RPC 调用;3. 支持同步 / 异步调用

四、完整运行时序(按时间线)

时间点 注册中心 RPC 服务端 RPC 客户端
T0 启动,监听 8080 端口 - -
T1 - 启动 → 向注册中心注册 Add→9090 → 启动心跳 → 监听 9090 -
T2 接收注册请求 → 存储 Add→9090 → 返回注册成功 - 启动 → 连接注册中心 8080
T3 接收发现请求 → 返回 Add→9090 接收同步请求 → 执行 Add (11,22) → 返回 33 发起同步调用 → 查注册中心 → 调用 9090 → 打印 33
T4 - 接收 Future 请求 → 执行 Add (33,44) → 返回 77 发起 Future 调用 → 用缓存地址 → 调用 9090 → get () 打印 77
T5 - 接收回调请求 → 执行 Add (55,66) → 返回 121 发起回调调用 → 用缓存地址 → 调用 9090 → 休眠 1 秒
T6 - - 触发回调 → 打印 121 → 主线程退出

🔥test_3🔥

publish_client.cpp

subscribe_client.cpp

server.cpp

一、先明确整体架构与核心角色

这三份代码构成了 bitrpc 框架 Pub/Sub 模式的最小闭环,核心角色分工如下:

角色 代码文件 核心职责
发布客户端(Publisher) publish_client.cpp 1. 连接服务端;2. 创建主题;3. 循环发布指定数量的消息;4. 优雅关闭连接
订阅客户端(Subscriber) subscribe_client.cpp 1. 连接服务端;2. 创建主题;3. 订阅主题并绑定回调;4. 阻塞等待接收消息
Pub/Sub 服务端 server.cpp 1. 监听 7070 端口;2. 处理客户端的主题创建 / 发布 / 订阅请求;3. 转发发布消息到订阅端

四、完整运行时序(按时间线)

时间点 服务端 订阅客户端 发布客户端
T0 启动,监听 7070 端口 - -
T1 - 启动 → 连接服务端 → 创建 hello 主题 → 订阅 hello 并绑定回调 → 休眠 10 秒 -
T2 接收订阅请求 → 将订阅端加入 hello 的订阅列表 → 返回订阅成功 等待消息 启动 → 连接服务端 → 创建 hello 主题
T3 接收发布请求(Hello World-0)→ 推送给订阅端 收到消息 → 触发回调 → 打印 发布第 1 条消息
T4-T11 依次接收 9 条发布请求 → 依次推送给订阅端 依次接收 9 条消息 → 依次触发回调打印 依次发布剩余 9 条消息
T12 - 休眠结束 → 调用 shutdown → 断开连接 发布完成 → 调用 shutdown → 断开连接
T13 感知两端断开 → 清理 hello 主题的订阅列表 退出 退出

🌟发布订阅🌟

🌟服务注册与发现🌟

🌟总结🌟

🌟扩展🌟

附录:

1.工厂模式

工厂模式是创建型设计模式 的核心之一,其核心思想是 "封装对象的创建过程 ",通过一个统一的 "工厂" 类或方法来生成目标对象,而非让客户端直接使用 new 关键字创建。这样可以降低客户端与具体产品类的耦合度,提高代码的可扩展性和可维护性。

3. 三种核心形式(对比梳理,重点记适用场景)

类型 核心逻辑 工厂数量 开闭原则兼容性 适用场景
简单工厂模式 一个工厂生产「同一类」所有产品 1 个 不兼容(新增产品要改工厂) 产品类型固定、数量少(如仅生产手机:苹果 / 华为)
工厂方法模式 一个产品对应一个工厂 N 个(1:1) 兼容(新增产品加新工厂) 产品频繁新增(如手机品类不断扩充)
抽象工厂模式 一个工厂生产「一组相关产品」 M 个(1 个产品族对应 1 个) 兼容产品族扩展,不兼容单产品扩展 需要创建「配套产品」(如生产手机 + 充电器 + 耳机的苹果工厂 / 华为工厂)

2. 两种枚举类型(对比梳理,重点记推荐用法)

类型 核心特性 关键问题 推荐度 示例(C++ 为例)
传统枚举(Plain Enum) 成员暴露在全局作用域、支持隐式转 int 易命名冲突、类型不安全 仅兼容旧代码 enum Color { Red, Green }; // Red 可直接用,且 Red == 0
强类型枚举(Scoped Enum) 成员隔离在枚举作用域内、不支持隐式转换 无冲突、类型安全 强烈推荐 enum class Color : int { Red, Green }; // 必须用 Color::Red

2.完美转发回顾

"完美转发(Perfect Forwarding)" 是 C++11 及后续版本中,借助右值引用模板参数推导,实现的一种能让函数模板 "精准传递" 参数值类别(左值 / 右值)的技术。它的核心目的是:让模板函数接收到的参数,能以和原始调用时完全一致的 "值类别",传递给内部调用的其他函数,避免不必要的拷贝或移动,同时保留参数的 "左值 / 右值" 属性。

3.建造者模式

二、建造者模式的核心角色(用「定制电脑」举例,秒懂)

用 "定制电脑" 这个典型场景对应每个角色,你能直观理解各角色的作用:

角色 核心职责 「定制电脑」场景示例
产品(Product) 待构建的复杂对象(包含多个部件) 电脑(包含 CPU、内存、显卡、硬盘等部件)
抽象建造者(Abstract Builder) 定义构建产品的「分步接口」+ 返回产品的接口 电脑建造者接口:buildCPU()buildMemory()buildGPU()getComputer()
具体建造者(Concrete Builder) 实现抽象接口,负责具体部件的构建 游戏本建造者:buildCPU()装 i9、buildGPU()装 RTX4090;办公本建造者:buildCPU()装 i5、buildGPU()装核显
指挥者(Director) 可选角色,封装「标准构建流程」 电脑组装指挥者:定义固定流程「先装 CPU→再装内存→最后装显卡」,客户端只需调用指挥者的construct(),无需手动调分步接口

四、建造者模式 vs 工厂模式(核心差异对比,重点记)

这是最容易混淆的点,用表格清晰区分核心维度:

对比维度 工厂模式 建造者模式
核心目标 快速创建「同家族的不同对象」(如 iPhone、华为手机) 分步构建「同一类产品的不同配置」(如游戏本、办公本)
关注重点 「创建什么」(对象类型) 「怎么创建」(构建步骤、部件组合)
产品特点 产品是「不同类型但同接口」 产品是「同一类型但不同配置」
构建流程 无显式步骤,一次性创建完毕 有明确分步步骤,可控制顺序 / 细节
核心角色 工厂类(负责对象创建逻辑) 建造者(部件构建)+ 指挥者(可选,控流程)
典型场景 生成不同品牌手机、数据库驱动、日志器 定制电脑、汽车组装、复杂文档(如 PDF)生成
一句话总结 我要一个手机(工厂给你 iPhone / 华为) 我要一台电脑,先装 i9 CPU,再装 32G 内存,最后装 4090 显卡

4.异步响应与事件驱动

一、核心概念:一句话讲透

概念 通俗定义 技术本质
同步响应 发起请求后,阻塞等待结果返回,期间啥也干不了 函数调用后,线程挂起,直到获取返回值才继续执行后续逻辑(如 read() 阻塞读文件)
异步响应 发起请求后,不阻塞,直接去做其他事,结果就绪后再处理 函数调用后立即返回,线程不挂起,结果通过其他机制通知(如回调、信号)
事件驱动 通过 "事件" 触发逻辑执行,而非顺序执行代码,是异步响应的 "通知机制" 基于事件循环(Event Loop),监听事件(如 "数据就绪""请求完成"),事件发生时执行对应回调

5.dynamic_pointer_cast

五、dynamic_pointer_cast vs 其他指针转换(对比表)

转换函数 适用对象 运行时类型检查 安全性 性能 适用场景
dynamic_pointer_cast shared_ptr 不确定类型,需要安全转换
static_pointer_cast shared_ptr 确定类型,仅做静态类型转换
const_pointer_cast shared_ptr 去除 / 添加 const 限定符
dynamic_cast(裸指针) 普通指针 / 引用 裸指针的动态类型转换

6.服务端中rpc_topic.hpp中为什么既要用结构体锁,又要用全局锁?

四、两者的职责边界(清晰对比表)

锁类型 核心保护范围 典型使用场景
全局锁 1. 全局映射表 _topics/_subscribers 的结构完整性;2. 跨对象操作的原子性。 创建 / 删除 Topic、查找 Topic/Subscriber、订阅初始化等。
结构体锁 单个 Topic/Subscriber 内部的数据(如订阅者列表、主题列表)。 向 Topic 添加 / 删除订阅者、订阅者添加 / 删除主题、发送消息等。

7.使用lambda解决bind参数不匹配问题/bind的理解

四、Lambda vs std::bind(核心优势对比)

特性 std::bind Lambda
类型匹配灵活性 弱:依赖隐式转换,const / 引用 / 智能指针差异易报错 强:外层严格匹配,内层显式转换,适配任意参数差异
代码可读性 差:需要记忆 placeholders 占位符,参数对应关系不直观 好:参数列表清晰,转换逻辑显式可见,无占位符
维护成本 高:参数个数 / 顺序变化时,占位符需全部调整 低:参数调整仅需改 Lambda 外层 / 内层,逻辑隔离
类型推导 复杂:容易出现 "看似匹配实则不匹配" 的隐式推导错误 简单:参数类型显式声明,推导过程透明
捕获方式 有限:只能绑定预先定义的对象 / 占位符 灵活:支持值捕获、引用捕获、移动捕获,可自定义捕获逻辑
相关推荐
sheji34162 小时前
【开题答辩全过程】以 基于Java的校内美食推荐系统的设计与实现为例,包含答辩的问题和答案
java·开发语言·美食
Mr -老鬼2 小时前
Rust 知识图-谱基础部分
开发语言·后端·rust
三万棵雪松2 小时前
【AI小智硬件程序(八)】
c++·人工智能·嵌入式·esp32·ai小智
码农幻想梦2 小时前
实验7 知识表示与推理
开发语言·人工智能·python
我还可以再学点2 小时前
C语言常见函数
c语言·开发语言
写代码的【黑咖啡】2 小时前
深入理解 Python 中的 SQLAlchemy
开发语言·python·oracle
王老师青少年编程2 小时前
2025年12月GESP真题及题解(C++七级): 学习小组
c++·gesp·csp·信奥赛·七级·csp-s·提高组
特行独立的猫2 小时前
python+Proxifier+mitmproxy实现监听本地网路所有的http请求
开发语言·爬虫·python·http
浩瀚地学2 小时前
【Java】集合-Collection
java·开发语言·经验分享·笔记·学习