1.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.网络传输的参数和返回值怎么映射到对应的 RPC 接口上?
1.使⽤protobuf的反射机制
2.使⽤C++模板、类型萃取、函数萃取等机制
3.使用更通用的类型, 比如JSON类型, 设计好参数和返回值协议即可
4.前两种技术难度和学习成本较⾼, 我们使⽤第三种⽅式
3.网络传输怎么做?
1.原⽣socket - 实现难度较⼤, 暂不考虑
2.Boost asio库的异步通信 - 需要扩展boost库
3.muduo库, 学习开发成本较低
4.序列化和反序列化
1.Protobuf: 可选
2.JSON: 因为项目需要使用JSON来定义函数参数和返回值, 所以我们项目中直接采用JSON进行序列化和反序列化
3.开发环境
1.Linux(Ubuntu-22.04)
2.VSCode/Vim
3.g++/gdb
4.Makefile
4.环境搭建
1.安装wget(⼀般情况下默认会自带)
[zsc@node ~]$ sudo apt-get install wget
2.更换国内软件源
先备份原来的/etc/apt/source.list⽂件
sql
[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 传输工具
sql
[zsc@node ~]$ sudo apt-get install lrzsz
[zsc@node ~]$ rz --version
rz (lrzsz) 0.12.20
4.安装编译器gcc/g++
sql
[zsc@node ~]$ sudo apt-get install gcc g++
5.安装项目构建工具make
sql
[zsc@node ~]$ sudo apt-get install make
6.安装调试器gdb
sql
[zsc@node ~]$ sudo apt-get install gdb
7.安装git
sql
[zsc@node ~]$ sudo apt-get install git
[zsc@node ~]$ git --version
8.安装cmake
sql
[zsc@node ~]$ sudo apt-get install cmake
[zsc@node ~]$ cmake --version
cmake version 3.22.1
9.安装jsoncpp
sql
[zsc@node ~]$ sudo apt-get install libjsoncpp-dev
10.安装Muduo
sql
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库
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 的数据类型包括对象,数组,字符串,数字等。
• 对象:使⽤花括号 {} 括起来的表⽰⼀个对象
• 数组:使⽤中括号 [] 括起来的表⽰⼀个数组
• 字符串:使⽤常规双引号 "" 括起来的表⽰⼀个字符串
• 数字:包括整形和浮点型,直接使⽤
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


服了是乱码ovo

6.Muduo库

Muduo由陈硕⼤佬开发,是⼀个基于非阻塞IO 和事件驱动 的C++高并发TCP网络编程库 。 它是⼀款基于主从Reactor模型的⽹络库,其使⽤的线程模型是one loop per thread, 所谓one loop per thread 指的是:
1.⼀个线程只能有⼀个事件循环(EventLoop), ⽤于响应计时器和IO事件
2.⼀个⽂件描述符只能由⼀个线程进⾏读写,换句话说就是⼀个TCP连接必须归属于某个EventLoop 管理
Muduo库常见接口介绍
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; }
};
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_);
};
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_;
};
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_);
};
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,它是整个事件驱动模型的核心组件之一,主要作用如下:
-
事件循环的载体 :
EventLoop代表一个事件循环,它不断地从内部的事件队列中获取并处理各种事件(如 I/O 事件、定时器事件、信号事件等),是 muduo 库实现 Reactor 模式的关键。 -
I/O 多路复用的封装 :
EventLoop内部封装了 epoll(Linux 下)等 I/O 多路复用机制,负责监听注册到其上的文件描述符(如 socket)的 I/O 事件,并在事件发生时回调相应的处理函数。 -
事件的调度与处理 :它维护了一个待处理的事件列表,通过
runInLoop()、queueInLoop()等方法可以将任务(回调函数)派发到事件循环中执行,确保线程安全地处理事件。 -
定时器管理 :提供了定时器功能,可以通过
runAfter()、runEvery()等方法注册定时任务,由EventLoop在指定时间触发。 -
线程关联 :每个
EventLoop对象通常与一个线程绑定(称为 I/O 线程),_baseloop常作为主线程的事件循环,在多线程模型中可能还会有其他子线程的EventLoop实例(如EventLoopThreadPool中的子循环)。
在服务器程序中,_baseloop 通常作为基础的事件循环,负责监听服务器的监听 socket,接受新连接,并可能将新连接的 I/O 事件分发给其他子 EventLoop 处理,从而实现高并发处理。
client.cpp

server.cpp p22中bind的作用
-
绑定成员函数
&DictServer::onConnection是要绑定的成员函数指针。由于成员函数必须通过对象(或指针)调用,因此需要传入this作为第一个参数,指定调用该函数的对象实例。 -
占位符
std::placeholders::_1表示该位置的参数将在回调函数被调用时由
_server传入。假设onConnection的原型是:void DictServer::onConnection(const muduo::net::TcpConnectionPtr& conn);这里的
_1就对应参数const muduo::net::TcpConnectionPtr& conn,即当连接事件发生时,_server会自动将连接对象作为参数传入。 -
适配回调类型
setConnectionCallback通常要求传入一个特定签名的函数(例如只接受TcpConnectionPtr参数的函数),而DictServer::onConnection作为成员函数,其实际签名包含隐含的this指针(第一个参数)。通过std::bind绑定this并保留参数占位符后,生成的绑定器可以匹配setConnectionCallback所需的回调类型。
简单说,这段代码的作用是:当 _server 触发连接事件时,自动调用当前 DictServer 对象的 onConnection 方法,并将连接对象作为参数传入 。std::bind 在这里起到了 "适配函数签名" 和 "保存上下文(this)" 的作用。
makefile

7.C++11 异步操作
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并不能单独使用,需要搭配一些能够执行异步任务的模板类或函数一起使用
2.使用 std::async关联异步任务
异步执行一个函数,内部会创建线程执行异步任务, 返回一个future对象用于获取函数结果
std::async是⼀种将任务与std::future关联的简单⽅法。它创建并运⾏⼀个异步任务,并返回⼀个与该任务结果关联的std::future对象。默认情况下,std::async是否启动⼀个新线程,或者在等待future时,任务是否同步运⾏都取决于你给的 参数。这个参数为std::launch类型:
cpp
1.std::launch::deferred 表明该函数会被延迟调⽤,
直到在future上调⽤get()或者wait()才会开始执⾏任务
2.std::launch::async 表明函数会在⾃⼰创建的线程上运⾏
3.std::launch::deferred | std::launch::async 内部通过系统等条件⾃动选择策略
async.cpp


3.使用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则是⼀个整体。
packaged_task.cpp

4.使用std::promise 和 std::future配合
std.:.promise类模板:实例化的对象可以返回一个future,在其他线程中向promise对象设置数据,其他线程的关联future就可以获取数据
std::promise提供了⼀种设置值的⽅式,它可以在设置之后通过相关联的std::future对象进⾏读取。换种说法就是之前说过std::future可以读取⼀个异步函数的返回值了, 但是要等待就绪, ⽽std::promise 就提供⼀种 ⽅式⼿动让 std::future就绪
promise.cpp

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

上图的模型中,是⼀种多对⼀或⼀对⼀的关系,⼀旦服务端掉线,则客⼾端⽆法进⾏远端调⽤,
且其服务端的负载也会较⾼,因此在rpc实现中,我们不仅要实现其基本功能,还要再进⼀步,实现分布式架构的rpc。
分布式架构:简单理解就是由多个节点组成的⼀个系统,这些节点通常指的是服务器,将不同的业务或者同⼀个业务拆分分布在不同的节点上,通过协同⼯作解决⾼并发的问题,提⾼系统扩展性和可⽤性。
其实现思想也并不复杂,也就是在原来的模型基础上,增加⼀个注册中心,基于注册中⼼不同的服务提供服务器向注册中⼼进⾏服务注册,相当于告诉注册中⼼⾃⼰能够提供什么服务,⽽客⼾端在进⾏远端调⽤前,先通过注册中⼼进⾏服务发现,找到能够提供服务的服务器,然后发起调⽤。


⽽其次的发布订阅功能,则是依托于多个客⼾端围绕服务端进⾏消息的转发。
不过单纯的消息转发功能,并不能满⾜于⼤部分场景的需要,因此会在其基础上实现基于主题订阅的转发。

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

在上图的结构中,我们甚⾄可以让每⼀个Server作为备⽤注册中⼼形成分布式架构,⼀旦⼀个注册中⼼下线,可以向备⽤中⼼进⾏注册以及请求,且在此基础上客⼾端在请求Rpc服务的时候,因为可以有多个rpc-provider可选,因此可以实现简单的负载均衡策略,且基于注册中⼼可以更简便实现发布订阅的功能。
项目的三个主要功能:
1.rpc调用
2.服务的注册与发现以及服务的下线/上线通知
3.消息的发布订阅
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,
}
设计实现:
- 该模块必须具备⼀个主题管理,且主题中需要保存订阅了该主题的客⼾端连接
a. 主题收到⼀条消息,需要将这条消息推送给订阅了该主题的所有客⼾端 - 该模块必须具备⼀个订阅者管理,且每个订阅者描述中都必须保存⾃⼰所订阅的主题名称
a. ⽬的是为了当⼀个订阅客⼾端断开连接时,能够找到订阅信息的关联关系,进⾏删除 - 该模块必须向外提供 主题创建/销毁,主题订阅/取消订阅,消息发布处理的业务处理函数

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}
]
}
该模块的设计如下:
- 必须具备⼀个服务发现者的管理:
a. ⽅法与发现者:当⼀个客⼾端进⾏服务发现的时候,进⾏记录谁发现过该服务,当有⼀个新的提供者上线的时候,可以通知该发现者
b. 连接与发现者 :当⼀个发现者断开连接了,删除关联关系,往后就不需要通知了 - 必须具备⼀个服务提供者的管理:
a. 连接与提供者:当⼀个提供者断开连接的时候,能够通知该提供者提供的服务对应的发现者,该主机的该服务下线了
b. ⽅法与提供者:能够知道谁的哪些⽅法下线了,然后通知发现过该⽅法的客⼾端 - 必须向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:发布订阅功能模块与⽹络通信部分结合。

2.客户端模块划分
在客⼾端的模块划分中,基于以上理解的功能,可以划分出这么⼏个模块
- Protocol:应⽤层通信协议模块
- Network:⽹络通信模块
- Dispatcher:消息分发处理模块
- Requestor:请求管理模块
- RpcCaller:远端调⽤功能模块
- Publish-Subscribe:发布订阅功能模块
- Registry-Discovery:服务注册/发现/上线/下线功能模块
- Client:基于以上模块整合⽽出的客⼾端模块
4.Requestor
Requestor模块存在的意义:针对客户端的每⼀条请求进行管理,以便于对请求对应的响应做出合适的操作。
⾸先,对于客⼾端来说,不同的地⽅在于,更多时候客⼾端是请求⽅,是主动发起请求服务的⼀⽅,⽽在多线程的⽹络通信中,多线程下,针对多个请求进⾏响应可能会存在时序的问题,这种情况下,则我们⽆法保证⼀个线程发送⼀个请求后,接下来接收到的响应就是针对⾃⼰这条请求的响应,这种情况是⾮常危险的⼀种情况。
其次,类似于Muduo库这种异步IO⽹络通信库,通常IO操作都是异步操作,即发送数据就是把数据放⼊发送缓冲区,但是什么时候会发送由底层的⽹络库来进⾏协调,并且也并不会提供recv接⼝,⽽是在连接触发可读事件后,IO读取数据完成后调⽤处理回调进⾏数据处理,因此也⽆法直接在发送请求后去等待该条请求的响应。
针对以上问题,我们则创建出当前的请求管理模块来解决,它的思想也⾮常简单,就是给每⼀个请求都设定⼀个请求ID,服务端进⾏响应的时候标识响应针对的是哪个请求(也就是响应信息中会包含请求ID),因此客⼾端这边我们不管收到哪条请求的响应,将数据存储⼊⼀则hash_map中,以请求ID作为映射,并向外提供获取指定请求ID响应的阻塞接⼝,这样只要在发送请求的时候知道⾃⼰的请求 ID,那么就能获取到⾃⼰想要的响应,⽽不会出现异常。
针对这个思想,我们再进⼀步,可以将每个请求进⼀步封装描述,添加⼊异步的future控制,或者设置回调函数的⽅式,在不仅可以阻塞获取响应,也可以实现异步获取响应以及回调处理响应。
5.RpcCaller
RpcCaller模块存在的意义:向用户提供进行rpc调用的模块。
Rpc服务调⽤模块,这个模块相对简单,只需要向外提供⼏个rpc调⽤的接⼝,内部实现向服务端发送请求,等待获取结果即可,稍微⿇烦⼀些的是Rpc调⽤我们需要提供多种不同⽅式的调⽤:
- 同步调⽤:发起调⽤后,等收到响应结果后返回
- 异步调⽤:发起调⽤后⽴即返回,在想获取结果的时候进⾏获取
- 回调调⽤:发起调⽤的同时设置结果的处理回调,收到响应后⾃动对结果进⾏回调处理

6.Publish-Subscribe
Publish-Subscribe模块存在意义:向⽤⼾提供发布订阅所需的接⼝,针对推送过来的消息进⾏处理。
发布订阅稍微能复杂⼀丢丢,因为在发布订阅中有两种⻆⾊,⼀个客⼾端可能是消息的发布者,也可能是消息的订阅者。
⽽且不管是哪个⻆⾊都是对主题进⾏操作,因此其中也包含了主题的相关操作,⽐如,要发布⼀条消息需要先创建主题。
且⼀个订阅者可能会订阅多个主题,每个主题的消息可能都会有不同的处理⽅式,因此需要有订阅者主题回调的管理。

7.Registry-Discovery
服务注册和发现模块需要实现的功能会稍微复杂⼀些,因为分为两个⻆⾊来完成其功能
- 注册者:作为Rpc服务的提供者,需要向注册中⼼注册服务,因此需要实现向服务器注册服务的功能
- 发现者:作为Rpc服务的调⽤者,需要先进⾏服务发现,也就是向服务器发送请求获取能够提供指定服务的主机地址,获取地址后需要管理起来留⽤,且作为发现者,需要关注注册中⼼发送过来的服务上线/下线消息,以及时对已经下线的服务和主机进⾏管理。

8.Client
将以上模块进⾏整合就可以实现各个功能的客⼾端了。
• RegistryClient:服务注册功能模块与⽹络通信客⼾端结合
• DiscoveryClient:服务发现功能模块与⽹络通信客⼾端结合
• RpcClient:DiscoveryClient & RPC功能模块与⽹络通信客⼾端结合
• TopicClient:发布订阅功能模块与⽹络通信客⼾端结合


框架设计:
在当前项⽬的实现中,我们将整个项⽬的实现划分为三层来进⾏实现
- 抽象层:将底层的⽹络通信以及应⽤层通信协议以及请求响应进⾏抽象,使项⽬更具扩展性和灵活性。
- 具象层:针对抽象的功能进⾏具体的实现。
- 业务层:基于抽象的框架在上层实现项⽬所需功能。
3.抽象层
在咱们的项⽬实现中,⽹络通信部分采⽤了第三⽅库Muduo库,以及通信协议使⽤了LV格式的通信协议解决粘包问题,数据正⽂中采⽤了Json格式进⾏序列化和反序列化,⽽这⼏⽅⾯我们都可能会存在继续优化的可能,甚⾄在序列化⽅⾯不⼀定⾮要采⽤Json,因此在设计项⽬框架的时候,我们对于底层通信部分相关功能先进⾏抽象,形成⼀层抽象层,⽽上层业务部分根据抽象层来完成功能,这样的好处是在具体的底层功能实现部分,我们可以实现插拔式的模块化替换,以此来提⾼项⽬的灵活性和扩展性。
4.具象层
具象层就是针对抽象的具体实现。
⽽具体的实现也⽐较简单,从抽象类派⽣出具体功能的派⽣类,然后在内部实现各个接⼝功能即可。
• 基于Muduo库实现⽹络通信部分抽象
• 基于LV通信协议实现Protocol部分抽象
不过这⼀层中⽐较特殊的是,我们需要针对不同的请求,从BaseMessage中派⽣出不同的请求和响应类型,以便于在针对指定消息处理时,能够更加轻松的获取或设置请求及响应中的各项数据元素。
5.业务层
业务层就是基于底层的通信框架,针对项⽬中具体的业务功能的实现了,⽐如Rpc请求的处理,发布订阅请求的处理以及服务注册与发现的处理等等。
Rpc

发布订阅

服务注册&发现

整体框架设计

9.项目实现
1.常用的零碎功能实现
1.简单日志宏实现
意义:快速定位程序运⾏逻辑出错的位置。
项⽬在运⾏中可能会出现各种问题,出问题不可怕,关键的是要能找到问题,并解决问题。
解决问题的⽅式:
• gdb调试:逐步调试过于繁琐,缓慢。主要⽤于程序崩溃后的定位。
• 系统运⾏⽇志分析:在任何程序运⾏有可能逻辑错误的位置进⾏输出提⽰,快速定位逻辑问题的位置。
Json序列化/反序列化
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

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"
2.消息类型定义
• Rpc请求 & 响应
• 主题操作请求 & 响应:
• 消息发布请求 & 响应
• 服务操作请求 & 响应:
cpp
enum class MType {
REQ_RPC = 0,
RSP_RPC,
REQ_TOPIC,
RSP_TOPIC,
REQ_SERVICE,
RSP_SERVICE
};
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;
}
4.RPC请求类型定义
• 同步请求:等待收到响应后返回
• 异步请求:返回异步对象,在需要的时候通过异步对象获取响应结果(还未收到结果会阻塞)
• 回调请求:设置回调函数,通过回调函数对响应进⾏处理
cpp
enum class RType {
REQ_ASYNC = 0,
REQ_CALLBACK
};
5.主题操作类型定义
• 主题创建
• 主题删除
• 主题订阅
• 主题取消订阅
• 主题消息发布
cpp
enum class TopicOptype {
TOPIC_CREATE = 0,
TOPIC_REMOVE,
TOPIC_SUBSCRIBE,
TOPIC_CANCEL,
TOPIC_PUBLISH
};
6.服务操作类型定义
• 服务注册
• 服务发现
• 服务上线
• 服务下线
cpp
enum class ServiceOptype {
SERVICE_REGISTRY = 0,
SERVICE_DISCOVERY,
SERVICE_ONLINE,
SERVICE_OFFLINE,
SERVICE_UNKNOW
};
fields.hpp

3.通信抽象实现
abstract.hpp

4.消息具体实现
message.hpp

5.通信-Muduo封装实现
防备一种情况:缓冲区中数据有很多很多。但是因为数据错误,导致数据又不足一条完整消息,也就是一条消息过大,针对这种情况,直接关闭连接
net.hpp

6.消息-不同消息封装实现
7.Dispatcher实现

服务端会收到不同类型的请求,客户端会收到不同类型的响应(因为请求和响应都具有多样性)
因此在回调函数中,就需要判断消息类型,根据不同类型的消息做出不同的处理;如果单纯使用if语句做分支处理,是一件非常不好的事
程序设计中需要遵守一个原则:开闭原则 --- 对修改关闭,对扩展开放
当后期维护代码或新增功能时:不去修改以前的代码,而是新增当前需要的代码
Dispatcher模块就是基于开闭原则设计的;目的就是建立消息类型与业务回调函数的映射关系;如果后期新增功能,不需要修改以前的代码,只需要增加一个映射关系即可
Dispatcher.hpp

8.服务端-RpcRouter实现
在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

9.服务端-Publish&Subscribe实现
rpc_topic.hpp

10.服务端-Registry&Discovery实现
1.为什么要注册服务,服务注册是要做什么?
服务注册是要实现分布式系统,让系统更加强壮;一个节点主机,将自己所能提供的服务,在注册中心进行登记。
2.为什么要服务发现,服务发现要做什么?
rpc调用者需要知道哪个节点主机能够为自己提供指定服务;服务发现其实就是询问注册中心,谁能为自己提供指定的服务,将节点信息给保存起来以待后用。
3.服务下线
当前使用长连接进行服务主机是否在线的判断,一旦服务提供方断开连接,查询这个主机提供了哪些服务,分析哪些调用者进行过这些服务发现,则进行服务下线通知。
4.服务上线
因为服务发现是一锤子买卖(调用方不会进行二次服务发现),因此一旦中途有新主机可以提供指定服务,调用方是不知道的;因此,一旦某个服务上线了,则对发现过这个服务的主机进行一次服务上线通知。
服务端要能够提供服务注册,发现的请求业务处理
- 需要将 哪个服务 能够由 哪个主机提供 管理起来 hash<method, vector<provider>>
实现当由 caller 进行服务发现的时候,告诉 caller 谁能提供指定的服务 - 需要将 哪个主机 发现过 哪个服务 管理起来
当进行服务通知的时候,都是根据谁发现过这个服务,才会给谁通知 <method,vector<discoverer>> - 需要将 哪个连接 对应哪个 服务提供者 管理起来 hash<conn, provider>
当一个连接断开的时候,能够知道哪个主机的哪些服务下线了,然后才能给发现者通知 xxx 的 xxx 服务下线了 - 需要将 哪个连接 对应哪个 服务发现者 管理起来 hash<conn, discoverer>
当一个连接断开的时候,如果有服务上线下线,就不需要给他进行通知了
rpc_registry.hpp

11.服务端-整合封装Server(有点问题)
rpc_server.hpp

12.客户端-Requestor实现

requestor.hpp

13.客户端-RpcCaller实现
rpc_caller.hpp

14.客户端-Publish&Subscribe实现
rpc_topic.hpp

15.客户端-Registry&Discovery实现
客户端的功能比较分离,注册端跟发现端根本就不在同一个主机上。
因此客户端的注册与发现功能是完全分离的
- 作为服务提供者 --- 需要一个能够进行服务注册的接口
连接注册中心,进行服务注册, - 作为服务发现者 --- 需要一个能够进行服务发现的接口,需要将获取到的能够提供指定服务的主机信息管理起来
hash<method, vector<host>> 一次发现,多次使用,没有的话再次进行发现。
需要进行服务上线 / 下线通知请求的处理(需要向 dispatcher 提供一个请求处理的回调函数)
因为客户端在一次服务发现中,会一次获取多个能够提供服务的主机地址信息,到底请求谁合理?
负载均衡的思想:RR 轮转
rpc_registry.hpp

16.客户端-整合封装Client
rpc_client.hpp

17.整合封装RpcServer & RpcClient
附录:
1.工厂模式
工厂模式是创建型设计模式 的核心之一,其核心思想是 "封装对象的创建过程 ",通过一个统一的 "工厂" 类或方法来生成目标对象,而非让客户端直接使用 new 关键字创建。这样可以降低客户端与具体产品类的耦合度,提高代码的可扩展性和可维护性。
一、为什么需要工厂模式?(解决的问题)
在直接使用 new 创建对象的场景中,客户端需要知道具体产品类的完整名称 (如 new ApplePhone()),且一旦产品类发生变化(如类名修改、构造逻辑调整),所有使用该类的客户端代码都需要修改 ------ 这违反了设计模式的 "开闭原则"(对扩展开放、对修改关闭)。
工厂模式通过以下方式解决问题:
- 客户端只需请求工厂 "生产" 对象,无需知道对象的具体创建细节;
- 新增产品时,只需扩展工厂,无需修改现有客户端代码;
- 统一管理对象创建逻辑(如初始化参数、依赖注入、单例控制等),避免代码重复。
二、工厂模式的三种核心实现形式
工厂模式根据复杂度和适用场景,分为简单工厂模式 、工厂方法模式 和抽象工厂模式,三者层层递进,满足不同规模的需求(详细介绍请查看相关文献)
| 特性 | 简单工厂模式 | 工厂方法模式 | 抽象工厂模式 |
|---|---|---|---|
| 核心能力 | 生产单一产品族的所有产品 | 生产单一产品族的单个产品 | 生产多个相关产品族的所有产品 |
| 工厂数量 | 1 个(单一工厂) | N 个(1 个产品对应 1 个工厂) | M 个(1 个产品族对应 1 个工厂) |
| 开闭原则兼容性 | 不兼容(新增产品需改工厂) | 兼容(新增产品只需加工厂) | 兼容产品族,不兼容产品等级 |
| 适用场景 | 产品固定且少 | 产品频繁新增 | 需统一产品族 |
四、工厂模式的实际应用场景
- 框架底层设计 :如 Spring 框架的
BeanFactory(创建 Bean 对象)、MyBatis 的SqlSessionFactory(创建 SqlSession); - 跨平台组件创建:如游戏引擎的 "图形渲染工厂"(Windows 用 DirectX 工厂,Linux 用 OpenGL 工厂);
- 插件化架构:如 IDE 的 "插件工厂"(不同插件对应不同工厂,动态创建插件实例);
- 复杂对象创建:如对象需要多步初始化(如连接数据库、加载配置),工厂统一管理创建逻辑,避免客户端重复代码
通过工厂模式,代码的 "创建逻辑" 与 "使用逻辑" 分离,更符合 "高内聚、低耦合" 的设计目标,是大型项目中最常用的设计模式之一。
枚举enum
枚举的核心价值是 "用有意义的名字替代无意义的数字"
| 特性 | 传统枚举(Plain Enum) | 强类型枚举(Scoped Enum) |
|---|---|---|
| 作用域 | 成员暴露在当前作用域 | 成员隔离在枚举作用域内 |
| 命名冲突 | 易冲突 | 无冲突 |
| 隐式转换 | 支持(转 int) | 不支持(需显式转换) |
| 底层类型自定义 | C++11 后支持,但语法繁琐 | 直接支持(: 类型) |
| 推荐度 | 不推荐(仅兼容旧代码) | 强烈推荐(类型安全、清晰) |
2.完美转发回顾
"完美转发(Perfect Forwarding)" 是 C++11 及后续版本中,借助右值引用 和模板参数推导,实现的一种能让函数模板 "精准传递" 参数值类别(左值 / 右值)的技术。它的核心目的是:让模板函数接收到的参数,能以和原始调用时完全一致的 "值类别",传递给内部调用的其他函数,避免不必要的拷贝或移动,同时保留参数的 "左值 / 右值" 属性。
一、为什么需要完美转发?
在模板编程中,函数模板的参数往往是万能引用 (T&&,结合模板参数推导时,既可以绑定左值,也可以绑定右值)。但如果直接传递这些参数,可能会丢失原始的 "左值 / 右值" 属性,导致:
- 本该移动(
move)的右值,被当作左值进行拷贝,造成性能浪费。 - 内部函数无法根据参数的 "左值 / 右值" 属性,选择最优的重载版本(如
std::vector::push_back对左值做拷贝,对右值做移动)。
二、完美转发的实现:std::forward
std::forward 是 <utility> 头文件中的模板函数,专门用于 "恢复" 参数的原始值类别。它的核心逻辑是:
- 若模板参数
T推导为左值引用 (如int&),则std::forward<T>(arg)会将参数以左值形式转发。 - 若模板参数
T推导为非引用类型 或右值引用 (如int或int&&),则std::forward<T>(arg)会将参数以右值形式转发。
三、代码示例:理解完美转发
cpp
#include <iostream>
#include <utility>
#include <string>
// 模拟一个需要区分左值/右值的函数
void processValue(int& val) {
std::cout << "处理左值,值为:" << val << std::endl;
}
void processValue(int&& val) {
std::cout << "处理右值,值为:" << val << std::endl;
}
// 模板函数:使用完美转发
template <typename T>
void forwardValue(T&& val) {
// 关键:用 std::forward 转发参数,恢复原始值类别
processValue(std::forward<T>(val));
}
int main() {
int x = 10;
// 情况1:传递左值
forwardValue(x); // 输出:处理左值,值为:10
// 情况2:传递右值(临时对象)
forwardValue(20); // 输出:处理右值,值为:20
// 情况3:传递右值引用(延长临时对象生命周期)
int&& rref = 30;
forwardValue(std::move(rref)); // 输出:处理右值,值为:30
return 0;
}
四、步骤拆解(以 forwardValue(20) 为例)
-
模板参数推导 :
调用
forwardValue(20)时,20是右值,模板参数T会被推导为int(因为T&&结合右值推导为int&&,但T本身是int)。 -
std::forward<T>(val)的作用 :此时
T是int,std::forward<int>(val)会将val(原本是右值20)以右值 形式转发给processValue,因此调用processValue(int&& val)版本。 -
如果不用完美转发 :
若直接写
processValue(val),val在forwardValue内部是左值(因为有名字的变量都是左值),会调用processValue(int& val),无法利用右值的移动语义,也不符合 "传递右值" 的意图。
五、典型应用场景:工厂模式 / 容器插入
完美转发在需要传递参数到其他函数,且需要保留参数值类别的场景中非常有用,比如:
- 工厂类创建对象时,传递构造参数(如前面的
MessageFactory::create<T>(Args&&... args),需要将参数精准传递给T的构造函数)。 - 容器插入元素时(如
std::vector::emplace_back,直接在容器内构造对象,避免拷贝)。
六、总结
完美转发是 C++ 模板编程中 "精准传递参数值类别" 的关键技术,核心依赖:
- 右值引用(实现万能引用,让模板参数能绑定左值或右值)。
std::forward(根据模板参数推导结果,恢复参数的原始左值 / 右值属性)。
它解决了模板中参数值类别丢失的问题,让模板函数能像普通函数一样,根据参数的 "左值 / 右值" 选择最优的处理逻辑,同时避免不必要的性能开销。
3.建造者模式
建造者模式的核心是 **"分步构建复杂对象,通过不同的构建步骤或细节组合,生成不同表现的对象"**。它专注于解决 "如何创建具有多个组成部分、构建步骤复杂的对象"(如 "定制电脑""复杂文档")。
1. 核心思想
将复杂对象的 "构建过程 " 与 "对象表示" 分离:客户端无需关心对象的具体构建步骤,只需通过 "指挥者" 指定构建流程,或直接通过 "建造者" 控制细节。
2. 核心角色
- 产品(Product):待构建的复杂对象(如 "电脑",包含 CPU、内存、显卡等部件)。
- 抽象建造者(Abstract Builder) :定义构建产品的 "分步接口"(如
buildCPU()、buildMemory()),以及返回产品的接口(getProduct())。 - 具体建造者(Concrete Builder):实现抽象建造者,负责具体部件的构建(如 "游戏本建造者""办公本建造者")。
- 指挥者(Director):可选角色,封装 "标准构建流程"(如 "先装 CPU→再装内存→最后装显卡"),避免客户端重复编写流程。
-
优点:
- 解耦构建与表示:同一构建流程可生成不同产品(如 "游戏本""办公本")。
- 灵活控制构建细节:客户端可自定义构建步骤(如先装内存再装 CPU)。
- 便于扩展:新增产品(如 "设计本")只需新增具体建造者,无需修改其他类。
-
缺点:
- 结构复杂:需新增多个建造者类,适合复杂对象;简单对象使用会增加冗余。
- 产品需有共性:建造者针对的是 "同一类产品的不同配置",若产品差异过大则不适用。
-
适用场景:
- 对象包含多个部件,且部件组合不同会产生不同表现(如定制电脑、汽车、文档)。
- 构建步骤复杂或需灵活调整步骤顺序。
- 需隔离对象的构建细节与使用逻辑。
| 维度 | 工厂模式 | 建造者模式 |
|---|---|---|
| 核心目标 | 快速创建 "同家族的不同对象" | 分步构建 "复杂对象的不同配置" |
| 关注重点 | "创建什么"(对象类型) | "怎么创建"(构建步骤与细节) |
| 产品特点 | 产品是 "不同类型但同接口"(如 iPhone、华为) | 产品是 "同一类型但不同配置"(如游戏本、办公本) |
| 构建流程 | 无显式步骤,一次性创建完毕 | 有明确分步步骤,可控制顺序和细节 |
| 角色核心 | 工厂(负责对象创建逻辑) | 建造者(负责部件构建)+ 指挥者(可选,控流程) |
| 典型场景 | 生成不同品牌的手机、数据库驱动 | 定制电脑、汽车组装、复杂文档生成 |
4.异步响应与事件驱动
异步响应就是 "不傻等" 的回应方式------ 发起请求后,不用一直盯着等结果,可以先去做别的事,结果出来了再 "通知" 你
"去餐厅吃饭" 对比 "点外卖",就能秒懂 "同步" 和 "异步" 的区别;去餐厅吃饭点完菜后,什么都做不了,只能盯着服务员 / 后厨,一直等菜端上桌,哈哈哈。而点外卖后,照样该干啥干啥,外卖小哥快到了,APP 会发消息提醒 你 "快取餐"(这就是事件驱动),这时你再去门口拿就行。
异步响应解决了 "不阻塞" 的问题,事件驱动解决了 "怎么知道该处理结果了" 的问题 。
没有事件驱动,异步响应很难高效实现(总不能靠 "一直问" 来等结果);而事件驱动的设计,也往往是为了支撑更灵活的异步交互(否则同步模式下,直接顺序执行就行,不需要 "事件" 来触发)
"异步响应依赖事件驱动,事件驱动服务于异步响应" 的紧密关系"
5.dynamic_pointer_cast
dynamic_pointer_cast 是智能指针库中的一个函数模板,用于在共享指针(std::shared_ptr)之间进行动态类型转换
它的主要作用是:
- 在继承层次结构中安全地将基类类型的
std::shared_ptr转换为派生类类型的std::shared_ptr - 进行运行时类型检查,如果转换失败,会返回一个空的
std::shared_ptr
使用 dynamic_pointer_cast 需要注意:
- 被转换的类型必须具有多态性(即至少有一个虚函数)
- 转换失败时不会抛出异常,而是返回空指针
- 它位于
<memory>头文件中,属于std命名空间 - 相比
static_pointer_cast,dynamic_pointer_cast会进行运行时类型检查,因此更安全但也有一定的性能开销
如果要转换 std::unique_ptr,则没有对应的 dynamic_pointer_cast,因为 unique_ptr 不支持这种转换(由于其独占所有权的特性)。
6.服务端中rpc_topic.hpp中为什么既要用结构体锁,又要用全局锁?
为什么要用结构体锁?
1. 避免 "全局大锁" 导致的性能瓶颈
TopicManager 本身已经有一个全局锁 _mutex,用于保护 _topics 和 _subscribers 这两个核心映射表。但如果仅依赖这个全局锁,会导致:
- 任何涉及主题或订阅者的操作(如订阅、发布、取消订阅)都需要竞争同一把锁,大量并发操作会被阻塞,形成性能瓶颈。
- 例如:两个完全无关的主题(如 "topicA" 和 "topicB")的操作(如分别向它们发布消息),本可以并行执行,却会因为全局锁而串行化,效率低下。
结构体级别的锁(Subscriber::_mutex 和 Topic::_mutex)将锁的范围缩小到单个对象内部,使得不同对象的操作可以并行进行。
2. 保护结构体内部数据的独立修改
Subscriber 和 Topic 都有自己的内部状态需要保护:
Subscriber的topics集合(订阅的主题列表)可能被多个线程同时修改(如同时订阅两个主题、或取消订阅)。Topic的subscribers集合(订阅者列表)也可能被多个线程同时修改(如多个订阅者同时订阅、或一个订阅者取消订阅)。
如果仅依赖 TopicManager 的全局锁,当需要修改这些内部数据时,必须先获取全局锁,这会导致:
- 即使两个操作涉及不同的
Subscriber或Topic,也需要等待全局锁,浪费并发机会。 - 例如:两个不同的订阅者分别取消订阅不同的主题,本可以独立进行,但全局锁会强制它们串行执行。
结构体内部的锁确保:只有修改同一个 Subscriber 或 Topic 时才需要竞争锁,不同对象的修改可以完全并行。
3. 支持更细粒度的 "读 - 写" 分离
在实际场景中,Subscriber 和 Topic 的 "读操作"(如遍历订阅者列表发送消息)可能远多于 "写操作"(如添加 / 删除订阅者)。结构体级别的锁可以:
- 允许对同一对象的 "读操作" 并行执行(如果使用
std::shared_mutex等读写锁)。 - 仅在 "写操作" 时阻塞其他操作,进一步提升并发效率。
例如:Topic::pushMessage(向所有订阅者发送消息)是典型的 "读操作"(遍历 subscribers),如果多个线程同时向不同的 Topic 发送消息,结构体锁可以让这些操作并行执行,而无需等待全局锁。
4. 降低死锁风险
全局锁的过度使用容易导致死锁。例如:
- 线程 1 先持有全局锁,尝试修改
TopicA,此时需要TopicA的内部锁(如果有的话)。 - 线程 2 先持有
TopicA的内部锁,尝试获取全局锁修改_topics。
这种情况下会产生死锁。而结构体级别的锁通过缩小锁的范围,减少了不同锁之间的交叉依赖,从而降低死锁风险。
总结
结构体级别的锁是 "最小权限原则 " 在并发编程中的体现:仅对需要保护的最小范围(单个 Subscriber 或 Topic)加锁,既能保证线程安全,又能最大化并发性能。这对于高并发的发布 - 订阅系统尤为重要,可有效避免全局锁带来的性能瓶颈。
为什么要用全局锁?
在 TopicManager 中,结构体锁(Subscriber::_mutex、Topic::_mutex) 和 全局锁(_mutex) 并非替代关系,而是职责分工不同 的互补设计。全局锁的存在是为了保护跨结构体的全局映射关系,这些关系无法被单个结构体的锁覆盖。具体原因如下:
1. 全局锁保护 "跨对象的映射表"
TopicManager 维护了两个核心的全局映射表:
_topics:std::unordered_map<std::string, Topic::ptr>(主题名 → 主题对象)_subscribers:std::unordered_map<BaseConnection::ptr, Subscriber::ptr>(连接 → 订阅者对象)
这些映射表是跨所有结构体的全局数据,其本身的增、删、查操作需要线程安全保证。例如:
- 创建主题时,需要向
_topics插入新的键值对; - 删除主题时,需要从
_topics中删除对应的键值对; - 订阅主题时,需要先从
_topics中查找主题对象,再从_subscribers中查找或创建订阅者对象。
这些操作涉及对全局映射表的修改或查找,而单个 Topic 或 Subscriber 的结构体锁只能保护自身内部数据(如 Topic::subscribers 或 Subscriber::topics),无法保护全局映射表的结构完整性。如果没有全局锁,多个线程同时对 _topics 或 _subscribers 进行插入 / 删除,可能导致哈希表崩溃(如迭代器失效、数据丢失等)。
2. 全局锁保证 "多对象操作的原子性"
很多业务逻辑需要跨多个结构体的操作,这些操作必须作为一个原子步骤完成,否则会出现数据不一致。例如:
示例 1:订阅主题(topicSubscribe)
订阅操作需要同时操作三个对象:
- 从
_topics中查找Topic对象(全局映射); - 从
_subscribers中查找或创建Subscriber对象(全局映射); - 将
Subscriber添加到Topic的订阅者列表(Topic内部数据); - 将
Topic名称添加到Subscriber的主题列表(Subscriber内部数据)。
如果没有全局锁,步骤 1 和步骤 2 可能被其他线程打断(比如在查找 Topic 后,该 Topic 被另一个线程删除),导致后续操作基于无效数据执行(如向已删除的 Topic 中添加订阅者)。全局锁确保 "查找 Topic 和 Subscriber" 这两个跨映射表的操作是原子的,避免中间状态被其他线程干扰。
示例 2:删除主题(topicRemove)
删除主题需要:
- 从
_topics中查找并删除Topic对象(全局映射); - 遍历该
Topic的所有订阅者,从每个订阅者的topics中移除该主题(涉及多个Subscriber内部数据)。
如果没有全局锁,步骤 1 中 "查找 Topic" 和 "删除 Topic" 之间可能插入其他操作(如新增订阅者),导致删除主题后仍有订阅者引用该主题,造成内存泄漏或无效消息发送。
3. 全局锁避免 "结构体锁的滥用与冲突"
结构体锁的作用范围是单个对象内部,如果试图用结构体锁覆盖全局映射表的操作,会导致两个问题:
- 锁粒度失控 :为了保护全局映射表,可能需要对所有
Topic或Subscriber加锁,相当于退化为全局锁,且逻辑更复杂; - 死锁风险剧增 :例如,线程 A 持有
TopicA的锁,试图获取TopicB的锁;线程 B 持有TopicB的锁,试图获取TopicA的锁,容易形成死锁。
全局锁通过统一保护全局映射表,避免了这种跨对象锁的滥用,降低了死锁概率。
总结:两者的职责边界
| 锁类型 | 保护范围 | 典型场景 |
|---|---|---|
全局锁(_mutex) |
全局映射表 _topics 和 _subscribers 的结构完整性;跨对象操作的原子性。 |
创建 / 删除主题、查找主题 / 订阅者、订阅初始化等。 |
| 结构体锁 | 单个 Topic 或 Subscriber 内部的数据(如 subscribers 列表、topics 列表)。 |
向主题添加 / 删除订阅者、订阅者添加 / 删除主题、向订阅者发送消息等。 |
简言之:全局锁管 "映射关系",结构体锁管 "对象内部数据"。两者分工协作,既保证了全局数据结构的一致性,又通过细粒度锁提升了并发性能,是高并发场景下线程安全设计的典型实践
7.使用lambda解决bind参数不匹配问题/bind的理解



- 通过一个
_dispatcher(调度器)注册回调函数,用于处理某种响应消息(MType::RSP_SERVICE类型) - 当响应到达时,调度器会调用我们注册的回调函数,最终需要执行
client::Requestor类的onResponse成员函数来处理响应
二、核心问题:回调函数类型不匹配
调度器的registerHandler方法对回调函数的类型有严格要求,而我们的onResponse成员函数直接绑定会出现类型不兼容,具体表现为:
1. 调度器要求的回调类型
_dispatcher->registerHandler<T>()需要的回调函数(MessageCallback)通常定义为:
cpp
/ 伪代码:调度器期望的回调类型
using MessageCallback = std::function<void(
BaseConnection::ptr&, / 连接对象(非const引用)
std::shared_ptr<T> / 消息对象(shared_ptr智能指针)
)>;
2. 我们的onResponse成员函数
假设client::Requestor类中的处理函数是:
class client::Requestor {
public:
// 我们的处理函数,参数可能与调度器要求不一致
void onResponse(
const BaseConnection::ptr& conn, // const引用(与调度器的非const冲突)
BaseMessage::ptr& msg // 可能是另一种智能指针类型(与shared_ptr冲突)
) {
// 实际处理响应的逻辑
}
};
3. 直接绑定的问题
当我们用std::bind直接绑定onResponse时:
// 原始绑定方式(会报错)
auto rsp_cb = std::bind(
&client::Requestor::onResponse,
_requestor.get(),
std::placeholders::_1,
std::placeholders::_2
);
会因为参数类型不匹配 (const修饰符、智能指针类型差异)导致编译错误。
三、解决思路:用 Lambda 作为 "适配器"
核心方案是在调度器要求的回调类型和onResponse之间加一层 Lambda 表达式,解决类型不匹配问题:
- Lambda 的角色
- 外层:Lambda 的参数严格匹配调度器要求的
MessageCallback类型(保证能被registerHandler接受) - 内层:在 Lambda 内部调用
onResponse,此时可以灵活处理参数类型转换(适配onResponse的参数要求)
-
具体代码实现
// 1. 创建Lambda表达式作为回调
auto rsp_cb = [requestor = _requestor.get()]( // 捕获Requestor对象的原始指针
BaseConnection::ptr& conn, // 第1个参数:严格匹配调度器要求(非const)
std::shared_ptr<BaseMessage> msg // 第2个参数:严格匹配调度器要求(shared_ptr)
) {
// 2. 在Lambda内部做参数转换,适配onResponse的要求
const BaseConnection::ptr& const_conn = conn; // 转换为const引用
BaseMessage::ptr msg_ref = msg; // 转换智能指针类型(如果需要)// 3. 调用实际的处理函数 requestor->onResponse(const_conn, msg_ref);};
// 3. 注册回调(此时类型完全匹配,编译通过)
_dispatcher->registerHandler<BaseMessage>(MType::RSP_SERVICE, rsp_cb);
四、为什么 Lambda 能解决问题?
- 类型自动匹配 :Lambda 的参数列表可以精确匹配
registerHandler的要求,避免std::bind的复杂类型推导问题 - 显式类型转换 :在 Lambda 内部可以清晰地处理
const修饰符、智能指针类型等转换,逻辑更直观 - 简化代码 :避免
std::placeholders占位符的使用,代码可读性更高 - 灵活性 :如果未来
onResponse或调度器的参数有微小调整,只需修改 Lambda 内部的转换逻辑,无需大面积改动
五、总结
问题本质是回调函数的参数类型不匹配,解决方案是用 Lambda 作为中间适配器:
- 外层满足调度器的类型要求
- 内层适配
onResponse的参数需求
这种方式既解决了编译错误,又保证了代码的可读性和可维护性,是现代 C++ 中处理回调绑定的推荐做法。












