项目-- Json-Rpc框架

目录

项目简介

RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,它允许一个计算机程序通过网络调用另一个计算机程序中的子程序(也就是远程过程),并获取返回值。客户端调用远程服务端的方法就像调用本地方法一样,客户端将参数传递给远程方法,远程方法执行后将结果返回给客户端。在这个过程中,RPC抽象了网络通信的复杂性,开发者只需关注于调用函数或方法。这样客户端就可以使用到远程服务器的资源,从而完成一些复杂的计算。

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

  • 序列化协议:将对象转换成二进制数据的过程称为序列化,反序列则是将二进制转换为对象的过程。因为网络传输的数据必须是二进制数据,所以在RPC调用中,对参数对象与返回值对象进行序列化与反序列化是一个必须的过程
  • 通信协议:负责在客户端和服务器之间传输请求和响应。协议需要定义如何封装参数、如何传输数据以及如何处理异常等。
  • 连接复用:在多次通信过程中,使用同一个连接进行数据传输,避免频繁地建立和释放连接。这可以减少连接建立和释放的开销,提高通信性能。
  • 服务注册:将服务的信息(如服务名、地址、端口等)注册到一个中心化的服务注册表中,使得其他服务可以通过查询服务注册表来发现和调用这些服务。
  • 服务发现:在动态的服务环境中,帮助客户端发现可用的服务实例,并维持服务注册信息,以支持高可用性和故障转移。
  • 服务订阅和通知:当服务状态发生变化时(如上线、下线、故障等),能够及时通知订阅了该服务的客户端,客户端据此更新本地的服务地址缓存,确保后续的服务调用能够顺利进行。
  • 负载均衡:通过智能地将请求分散到不同的节点上,以提高系统的可扩展性和性能。
  • 服务监控:记录调用日志,提供监控和追踪功能,便于故障排查和性能分析。
  • 同步调用:客户端发起请求后阻塞等待服务器端处理完请求并返回结果。这种方式实现简单,易于理解和调试,但在高并发场景下可能导致系统资源利用率低下和响应时间延长。
  • 异步调用:客户端发送请求后立即继续执行其他任务,而不必等待服务器端的响应。这种方式可以提高系统的吞吐量和响应速度,适用于高并发场景和长耗时操作。

这里的项目是基于C++、JsonCpp、muduo网络库实现⼀个简单、易用的RPC通信框架,它实现了同步调用、异步callback调用、异步futrue调用、服务注册/发现,服务上线/下线以及发布订阅等功能设计。

环境搭建

Ubuntu-22.04

  • 安装wget

    wget是一个功能强大的网络下载工具,适用于各种操作系统平台

    cpp 复制代码
    sudo apt-get install wget
    sudo apt-get update
  • 安装make

    make是一个构建工具,用于自动化软件项目的构建过程

    cpp 复制代码
    sudo apt-get install make
  • 安装编译器gcc/g++

    cpp 复制代码
    sudo apt-get install gcc g++
  • 安装调试器gdb

    cpp 复制代码
    sudo apt-get install gdb
  • 安装git

    cpp 复制代码
    sudo apt-get install git
    git --version
  • 安装jsoncpp

    python 复制代码
    方法一:直接使用软件包安装到系统中
    sudo apt-get install libjsoncpp-dev
    
    方法二:使用源代码安装到指定路径下
    git clone https://github.com/open-source-parsers/jsoncpp.git	#下载源代码
    cd jsoncpp	
    mkdir build	#用于构建需要,避免污染源代码目录
    cd build
    cmake .. -DCMAKE_INSTALL_PREFIX=myPath	#依赖上级目录jsoncpp的配置文件生成构建文件并配置安装路径为myPath
    make	#依据构建文件生成对应的库
    sudo make install #将编译好的库和头文件安装到前面指定的目录,如果该目录不是系统路径,sudo可以省略
  • 安装Muduo

    python 复制代码
    git clone https://github.com/chenshuo/muduo.git	#下载源代码
    
    sudo apt-get install libz-dev libboost-all-dev	#安装依赖环境
    
    ./build.sh install	#生成对应的库和头文件,可以在build/release-install-cpp11下查看到include和lib两个文件

第三方库使用

JsonCpp

Json 是⼀种数据交换格式,它采⽤完全独立于编程语言的⽂本格式来存储和表⽰数据,它的数据类型包括对象,数组,字符串,数字等。

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

使用时需要包含头文件 #include <jsoncpp/json/json.h>,同时在编译时需要手动链接该库。

具体使用可以参考博客的Jsoncpp部分

Muduo

在网络编程过程中,我们很容易想到的一个服务端框架为:让主线程进行 accept 获取客户端链接,接着为每一个客户端链接分配一个线程处理客户端请求,为了减少线程创建删除开销我们或许会使用线程池管理线程。这种方法实现简单,但由于操作系统默认最大线程数是通常几千到几万不等,因此在面对百万级别的高并发请求时,服务端立马就崩溃了,而且维护大量线程十分消耗资源,因此这种方式只适用于低并发场景。

于是我们引入了IO多路复用来解决这些问题。考虑到客户端连接到来后,大部分时间连接都是处于空闲状态,只有少部分时间才会请求服务,因此我们没有必要为每一个连接都分配一个线程,我们可以利用一个I/O 多路复用模型如 epoll ,一开始先将监听套接字 listen_socket 加入 epoll 中以监控客户端的连接到来,这就是主reactor,同时我们还会根据CPU核数创建一定数量的子reactor,主 reactor 检测到客户端连接到来后就将该客户端套接字 socket 负载均衡地加入到子 reactor 进行事件监听,一旦客户端 socket 事件触发,说明对应客户端需要请求服务,我们就为该连接分配一个线程进行请求处理,处理完成之后就回收该线程,这里一般使用线程池。这种模式称为多线程 reactor 模式或主从Reactor模式,简单来说就是:主 Reactor 接受连接,子 Reactor 监听 I/O 事件,业务线程池处理任务。这种模式下服务端仅处理正在活跃的连接,可以较为轻松地支持高并发操作,例如在100 万连接中,可能只有 1 万连接 同时活跃,而 8 个子 Reactor 线程 + 200 业务线程就足够处理这些连接了。

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

  • ⼀个线程只能有⼀个事件循环(EventLoop),用于响应计时器和IO事件
  • ⼀个文件描述符只能由⼀个线程进行读写,换句话说就是⼀个TCP连接必须归属于某个EventLoop管理

基础类

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_);
};
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,主动发起 TCP 半关闭(SHUT_WR),即 关闭写端
	 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_;
};
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()
	 void retrieveInt16()
	 void retrieveInt8()
	 string retrieveAllAsString()
	 string retrieveAsString(size_t len)
	 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
	 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[];
};
TcpClient类

用于搭建客户端

cpp 复制代码
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_;
	 }
	 
	 /// 连接服务器成功时的回调函数
	 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(){
		 MutexLockGuard lock(mutex_);
		 while (count_ > 0)
		 {
		 	condition_.wait();
		 }
	 }
	 
	 void countDown(){
	 	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_);
};
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; }
};

服务端基本搭建

1.初始化一个TcpServer对象

cpp 复制代码
TcpServer(EventLoop* loop,
 const InetAddress& listenAddr,
 const string& nameArg,
 Option option = kNoReusePort);
  • loop:指向EventLoop对象的指针,指定了TcpServer将要使用的事件循环
  • listenAddr:指定了服务器将要监听的IP地址和端口号
  • nameArg:为服务器实例提供了一个名称
  • kNoReusePort:是否设置端口复用

2.使用TcpServer对象设置连接事件的回调

cpp 复制代码
void setConnectionCallback(const ConnectionCallback& cb);

当连接建立或者断开时调用该函数

  • cb:用户自定义的回调函数,其定义如下
cpp 复制代码
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;

即用户传入的回调函数的返回值为空,参数为TcpConnection对象的引用,以对连接进行控制,同时该对象提供了消息读写接口

3.使用TcpServer对象设置连接消息的回调

cpp 复制代码
 void setMessageCallback(const MessageCallback& cb);

当消息到达时执行该函数

  • cb:用户自定义的回调函数,其定义如下
cpp 复制代码
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&,
 Buffer*,
 Timestamp)> MessageCallback;

即用户传入的回调函数的返回值为空,参数为:

  • TcpConnection对象的引用,以对连接进行控制,该对象提供了消息读写接口
  • Buffer指针用于存储接收到的数据
  • Timestamp通常是用于记录消息到达的时间戳

4.使用TcpServer对象设置开始监听

cpp 复制代码
 void start();

5.使用第1步初始化中的EventLoop对象开始事件监控

cpp 复制代码
void loop();

客户端基本搭建

  1. 初始化TcpClient对象
cpp 复制代码
 TcpClient(EventLoop* loop,
 const InetAddress& serverAddr,
 const string& nameArg);
  • loop:指向EventLoop对象的指针,指定了TcpServer将要使用的事件循环,为了避免阻塞主线程或进行异步I/O操作,通常需要用 EventLoopThread 类的startLoop()构造loop对象,这样就创建了一个线程让它去进行事件监控,从而避免主线程阻塞,后面我们也不需要显示调用 void loop(); 函数进行事件监控
  • serverAddr:服务端的IP地址和端口号
  • nameArg:为客户端实例提供了一个名称

2.使用TcpClient对象设置连接事件的回调

cpp 复制代码
 void setConnectionCallback(ConnectionCallback cb)

当连接建立或者断开时调用该函数

  • cb:用户自定义的回调函数,其定义如下
cpp 复制代码
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;

即用户传入的回调函数的返回值为空,参数为TcpConnection对象的引用,以对连接进行控制,同时该对象提供了消息读写接口

3.使用TcpClient对象设置连接消息的回调

cpp 复制代码
void setMessageCallback(MessageCallback cb)

当消息到来时执行该函数

  • cb:用户自定义的回调函数,其定义如下
cpp 复制代码
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&,
 Buffer*,
 Timestamp)> MessageCallback;

即用户传入的回调函数的返回值为空,参数为:

  • TcpConnection对象的引用,以对连接进行控制,该对象提供了消息读写接口
  • Buffer指针用于存储接收到的数据
  • Timestamp通常是用于记录消息到达的时间戳

4.使用TcpClient对象连接服务端

cpp 复制代码
 void connect();

5.使用CountDownLatch对象进行同步控制

cpp 复制代码
void wait()

在客户端连接上服务端后,客户端不会同步等待连接建立,如果此时发送消息就会出错,因此需要使用CountDownLatch对象进行同步控制,创建CountDownLatch对象时传参为数值1.

cpp 复制代码
void countDown();//执行--操作

当连接建立成功之后唤醒进程,唤醒的操作可以放到连接消息的回调函数中。

6.使用TcpClient对象关闭连接

cpp 复制代码
 void disconnect();

future

用于获取异步任务的结果

std::future是C++11标准库中的⼀个模板类,它用于获取一个异步线程的执行结果。std::future的⼀个重要特性是能够阻塞当前线程,直到异步操作完成,从而确保我们在获取结果时不会遇到未完成的操作。

应用场景:

  • 异步任务: 当我们需要在后台执行⼀些耗时操作时,如网络请求或计算密集型任务等,可以将任务与主线程分离,实现任务的并行处理,并用std::future获取这些异步任务的结果,从而提高程序的执行效率

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

  • 结果获取:std::future提供了⼀种安全的方式来获取异步任务的结果。我们可以使用std::future::get()函数来获取任务的结果,此函数会阻塞当前线程,直到异步操作完成。这样,在调用get()函数时,我们可以确保已经获取到了所需的结果

future只是辅助获取异步任务的结果,要执行具体的异步操作还需要其它模板类的配合,如std::async、std::packaged_task或者std::promise

一. std::future搭配std::async

std::async(asynchronous,异步的)内部会自动创建线程去执行指定任务。

  1. 关联异步任务aysnc_task 和 futrue
cpp 复制代码
future<type> async(policy,fun,args);

type:需要获取的结果的类型
policy:执行策略,有

  • std::launch::deferred:该函数会被延迟调用,直到在future上调用get()或者wait()时线程才会开始执行函数

  • std::launch::async:函数现在就可以在线程上运行

  • std::launch::deferred | std::launch::async 内部通过系统等条件自动选择策略

fun:线程需要执行的函数
args:fun函数的参数

举例:

cpp 复制代码
std::future<int> res = std::async(std::launch::deferred, Add, 11, 22);
  1. 使用future对象获取结果
cpp 复制代码
res.get()

使用举例:

cpp 复制代码
#include<iostream>
#include<future>
#include<unistd.h>

int Add(int num1,int num2){
    sleep(1);
    std::cout<<"hello"<<std::endl;
    return num1+num2;
}

int main(){
    std::future<int> result=std::async(std::launch::async,Add,1,2);
    std::cout<<"--------"<<std::endl;
    std::cout<<result.get()<<std::endl;
    return 0;
}

二. std::future搭配std::packaged_task

std::packaged_task用于将一个函数进行二次封装使其成为一个可调用对象,这样就可以放到线程中执行并用std::future获取结果。

  1. 将函数封装成任务包
cpp 复制代码
std::packaged_task<retType(argsType)>  (fun);

retType:函数fun返回值类型
argsType:函数fun的参数类型列表
fun:需要执行的函数

例如:

cpp 复制代码
std::packaged_task<int(int, int)> task(Add);
  1. 获取任务包关联的future对象
cpp 复制代码
std::future<int> result = task.get_future();
  1. 创建线程执行该任务
cpp 复制代码
std::thread thr([task](){
       task(11, 22);
   });
thr.join();

因为get()方法只是阻塞主线程直到获取结果,如果子线程执行获得结果之后还有其它任务执行但主线程已经获取结果退出就会出错,因此主线程需要调用join()等待子线程执行结束

  1. 使用std::future对象获取结果
cpp 复制代码
result.get()

三. std::future搭配std::promise

std::promise是对线程执行的结果进行封装以方便我们获取

  1. 实例化一个promise对象
cpp 复制代码
std::promise<int> pro;
  1. 通过promise对象获取关联的future对象
cpp 复制代码
std::future<int> res = pro.get_future();
  1. 创建线程执行函数并设置结果
cpp 复制代码
std::thread thr([&pro](){
    int sum = Add(11 , 22);
    pro.set_value(sum);
});
thr.join();

通过promise对象设置结果时要注意该对象的作用域范围,如果不在同一个作用域可以考虑new一个对象使用指针(不允许拷贝),这里主线程也要调用join()等待子线程执行结束

  1. 利用std::future对象获取结果
cpp 复制代码
res.get() 

项目设计

通用模块设计

我们主要实现俩个功能,一个是Rpc注册与调用,另一个则是主题订阅与发布:

由于我们有不同的请求和响应,我们希望所有流动的数据都可以用一种统一的形式描述,于是我们用以下结构描述它们:

其中JsonBody是一个Json对象,根据消息的不同我们就可以往里面添加不同的字段,接着我们就可以派生出6种需要的消息类型

这样我们就可以统一使用BaseMsg指针指向任意消息类型。

由于TCP协议是面向字节流动,我们需要自己解决粘包问题,考虑解决办法较多,这里先提供抽象接口,由底层进行具体的实现即选择一种具体的解决办法。

同理,我们还有客户端、服务端、缓冲区、连接的抽象:

这样我们就可以利用这些抽象类接口进行上层业务设计,以后就不关心底层具体实现,底层可以较为简单的进行更换而不影响上层业务代码。

考虑到我们有六种不同的消息类型,每一种消息到来时的回调函数是不一样的,而且客户端和服务端也只会收到其中某几种类型的消息,因此设计了一个Dispatcher模块,可以向里面注册不同类型消息到来时的回调,这个模块向外提供一个接口,这个接口是消息到来时的回调函数,是直接注册到客户端或服务端的,由内部自动根据消息类型调用对应注册的回调函数。

发送端针对某一种特定类型的消息,有以同步、异步、回调三种不同方式进行发送的需求,于是我们又设计了Requestor模块,以允许发送端可以以三种方式之一进行消息发送,里面依旧向外提供一个接口注册到Dispatcher模块,由Requestor模块对响应结果进行处理。

Rpc功能模块设计

发现者设计

发现者包含两个客户端,一个是服务发现客户端,另一个则是Rpc调用客户端。设计rpcDiscover模块,用于进行服务发现,里面管理着已经发现的服务提供者,这样就不用每一次进行服务发现时都请求一次服务注册中心,利用rpcDiscover,实现出了服务发现客户端discovererClient。

接着这是RpcCaller模块,以向服务提供者发起Rpc调用,这个模块只是简单的对Requestor模块进行封装,以满足Rpc调用需求,最后利用discovererClient和RpcCaller两个模块设计出了Rpc客户端rpcClient。

提供者设计

提供者也包含两个部分,一个是Rpc注册客户端,另一个是Rpc请求服务端。rpcRegistry模块用于向注册中心进行服务注册,利用该模块设计出Rpc注册客户端registryClient。rpcRouter模块管理着注册好的Rpc调用函数,并对接收到的Rpc请求进行处理,最后利用registryClient和rpcRouter设计出Rpc服务端rpcServer。

服务注册中心设计

主要设计了一个rpcRegistry模块,里面记录了所有连接到注册中心的发现者和提供者,以处理服务发现、上线、下线、掉线。据此设计出服务服务注册中心,registryServer。

Topic功夫模块设计

主题管理中心设计

这里只是用topicManage模块简单的记录所有创建的主题,包括改主题的订阅者,一旦该有主题有消息发布,则对该主题的所有的订阅者进行消息推送,利用topicManage模块就简单得到了主题管理中心topicServer。

主题客户端设计

这里也只是简单用topicManage模块向注册中心发起主题创建、删除、订阅、取消订阅、发布5种操作,利用topicManage模块就简单得到了主题客户端topicClient。

实现细节

1.宏定义不受命名空间约束,同宏定义语句后面禁止添加分号:;

2.... 是声明可变参数的语法,但在宏的展开部分,必须使用__VA_ARGS__或者##__VA_ARGS__,当可变参数为空时,##__VA_ARGS__移除它前面的逗号,避免语法错误,而__VA_ARGS__保留逗号,可能导致编译错误

cpp 复制代码
//正确
#define DLOG(format,...) LOG(Universe::DEBUG,format,##__VA_ARGS__)
//错误
#define DLOG(format,...) LOG(Universe::DEBUG,format,...)

3.例如像我们实现通用功能的头文件时,应该用一个命名空间保护里面的所有实现,同时按照功能把他们放到不同的类里面进行二次保护,并定义成静态函数以便类外访问,如序列化反序列化放到一个类里面,生成唯一ID的通用函数放到另一个类里面,如:

cpp 复制代码
namespace UniversalUtils {

// 序列化模块
class Serializer {
public:
    static std::string toJson(const std::vector<int>& data);
    static std::vector<int> fromJson(const std::string& json);
};

// ID 生成模块
class IDGenerator {
public:
    static std::string generateHexUUID();
};

4.使用原子操作atomic时,一般使用无符号整型,这样在溢出之后会重新回到0。

5.std::stringstream(字符串流,来自 )用于将各种数据类型(如 int、float 等)格式化为字符串,或从字符串中解析数据,如

cpp 复制代码
#include <sstream>
#include <string>

// 将数字转为字符串
std::stringstream ss;
int num = 42;
ss << "The answer is: " << num;
std::string result = ss.str(); //需要用str()成员函数将其转为string类型

// 从字符串解析数据
std::string input = "100 200";
std::stringstream ss2(input);
int a, b;
ss2 >> a >> b; // a=100, b=200

同时std::stringstream还可以配合C++提供的流控制符将字符串进行格式化

6.在序列化和反序列化Json串时,我们或许会像下面这样写:

cpp 复制代码
root["name"] = "Roman";

这种代码的拓展性较差,因为我一旦想将 "name" 改为 "NAME",在大型项目中这是十分繁琐的事情,因此我们可以这样写:

cpp 复制代码
#define KEY_NAME "nane"
root[KEY_NAME] = "Roman";

7.代码实现进行分层:业务层、抽象层、实现层。抽象层是对业务层要调用的接口的抽象,实现层是抽象层抽象接口的实现,这样业务层直接调用抽象层接口实现自身业务逻辑,不关心底层实现层的实现,当我们想更换底层使用的代码和库时,只需要修改实现层就可以了,不必要修改业务层和抽象层,从而增强代码的拓展性。

8.通过一个基类衍生出多个派生类对象后,我们应该定义一个生产工厂统一生产这些派生类对象,以免以后对象创建方式修改之后到处寻找修改已经创建的对象,增强代码可拓展性:

cpp 复制代码
//实现⼀个消息对象的⽣产⼯⼚
class MessageFactory {
public:
    static BaseMessage::ptr create(MType mtype) {
        switch (mtype) {
        case MType::REQ_RPC:
            return std::make_shared<RpcRequest>();
        case MType::RSP_RPC:
            return std::make_shared<RpcResponse>();
        case MType::REQ_TOPIC:
            return std::make_shared<TopicRequest>();
        case MType::RSP_TOPIC:
            return std::make_shared<TopicResponse>();
        case MType::REQ_SERVICE:
            return std::make_shared<ServiceRequest>();
        case MType::RSP_SERVICE:
            return std::make_shared<ServiceResponse>();
        }
        return BaseMessage::ptr();
    }
	
	//使用有参数的构造方法
    template <typename T, typename... Args>
    static std::shared_ptr<T> create(Args&&... args) {
        return std::make_shared<T>(std::forward(args)...);
    }
};

以上只是实现了一个简单的生产工厂,还有其它的更加复杂的生产方式。

9.如果有两个智能指针pb,pd,一个指向基类B,一个指向派生类D,指向派生类的智能指针是可以直接转为指向基类类型的智能指针的,而dynamic_cast只能直接将基类指针转换成派生类指针,如果想将基类智能指针转换成派生类智能指针可以使用dynamic_pointer_cast,由智能指针库<memory>提供,类似的还有static_pointer_cast、const_pointer_cast、const_pointer_cast。

cpp 复制代码
class Base{};
class Derive : public base{};

//直接转换
share_ptr<Base> ptr1=make_shared<Derive>;

//这里不能用dynamic_cast
share_ptr<Derive> ptr2=dynamic_pointer_cast<Derive>(ptr1);

10.如果Json库版本比较旧,不支持一些新的c++特性,可以考虑升级Json库,或者指定编译时使用c++11:-std=c++11。如果我们没有将库安装到系统目录下,需要-L指明库路径,-l指明需要的库名,-I指明头文件路径,如果采用动态链接,还需要配置好运行时路径。

11.可变参数模板有三种用法:

  • ①一次匹配完所有参数
    这时我们可以重载函数让编译器选择匹配的版本
cpp 复制代码
void fun();
void fun(double a);
void fun(int a,string b);

template<typename... Args>
void call(Args... args){
	fun(arg...);//展开参数包自行匹配fun函数
}
  • ②一次匹配多个参数
    这时只能递归展开展开,而且要提供递归终止条件。
cpp 复制代码
fun(int a,char b){};

// 递归终止条件
void call() { 
    cout << "递归终止" << endl; 
}

template<typename... Args>
void call(int a,char b,Args... args){
	fun(a,b);
	call(args...);
}

③一次匹配一个参数

这时可以选择使用递归展开,也可以使用逗号表达式展开参数

cpp 复制代码
fun(int a){};

template<typename... Args>
void call(Args... args){
	int arr[]={(fun(args),0)...};
}

...的位置总结:

cpp 复制代码
template <typename... Args>
void call(Args... args) {
	sizeof...(args);//求参数个数
    fun(args...);  // 展开参数包:args... → arg1, arg2, arg3, ...
}

//使用完美转发:
template <typename... Args>
void wrapper(Args&&... args) {
    target(std::forward<Args>(args)...);  // 展开为 std::forward<T1>(arg1), std::forward<T2>(arg2), ...
}

12.如果基类中没有声明fun(),即使派生类有定义,也无法通过基类指针动态调用,编译器只检查该函数是否在基类中声明(即是否属于基类的接口)。

13.muduo不会对发送的数据进行网络字节序转换,但从缓冲区拿取数据时自动将网络字节序转为主机字节序。

14.std::string类型的字符串需要调用成员函数c_str()转换格式才可以被printf()函数正确打印,这里的LOG日志打印底层用的是printf()函数。

15.设计时应该遵循开闭原则,即对拓展开放,对修改关闭,例如项目实现了 Dispatcher 模块,注册好相应的回调函数之后就可以根据消息类型进行函数回调,当有新的消息类型和回调函数时只需要进行注册即可,无需对代码进行大量修改。

16.如果fun函数有一个参数是一个模板参数,可以这样做:

cpp 复制代码
template<class T>
class MyTye{
	using NewType=function(void(int,T));
}

template<class T>
void fun(int,typename MyType::NewType t);

注意需要加上 typename 关键字

17.允许基类的派生类是一个模板类

cpp 复制代码
class Base{};

template<class T>
class Derive : public Base;

18.std::bind 默认按值存储参数,导致引用丢失,如果要保留引用语义,可以使用 std::ref 或 std::cref

cpp 复制代码
void sum(int& num1,int& num2);

//像下面这种绑定引用丢失
int num1=1;
std::bind(fun,num1,std::placeholders::_1);

//可以这样保留引用语义
std::bind(fun,std::ref(num1),
	std::ref(std::placeholders::_1));

同时std::placeholders::_1的含义是调用时的第一个参数num放到std::placeholders::_1的位置

19.如果某一行的代码长度过程,要进行分行处理,以免影响代码阅读

20.使用锁时,一般使用智能指针对对锁进行管理,确保锁被正确释放,同要注意加锁粒度,尽量避免锁冲突(如锁lock1要对A、B操作加锁,但B操作内部已经有锁lock2保护了,lock1可以根据具体情况不对B加锁)

cpp 复制代码
std::mutex::_mutex;
fun(){
	{
		std::unique_lock(std::mutex) mutex1(_mutex);
		//操作A
	}
	//操作B
}

21.如果某个函数fun处理后的值直接是另一个函数的一个参数,建议fun函数通过return返回处理值,如果处理值返回之后,还要进行额外加工才可以成为另一个函数的参数,建议fun函数通过引用参数返回处理值。同时如果确保某个参数不需要修改,在底层就应该开始使用const修饰,否则有时候想在上层用const修饰就得改底层代码。如果某个函数的参数大多数情况都是通过另一个函数的返回值得到的,那么这个参数就不要设置为引用,不然每次使用这个函数时都要先创建一个变量接收另一个函数的返回值。

cpp 复制代码
shared_ptr<Object>() ptr;//得到一个智能指针对象ptr,值为nullptr,不可解引用
make_shared<Object>() ptr();//得到一个智能指针对象ptr,指向一个使用Object的默认构造构造的Object对象

using FUN=function<void(int)>;
auto fun = FUN();//fun时一个FUN对象,不可以调用,可以这样判断:true==fun
相关推荐
非凡的世界20 小时前
PHP 中的动态函数调用
php·动态函数
切糕师学AI1 天前
.NET 对象转Json的方式
json·.net
Okailon1 天前
Debian12上安裝免费开源的CMS Drupal 11 机顶盒实例
开源·php·cms
木辰風1 天前
如何在MySQL中搜索JSON数据,并去除引号
数据库·mysql·json
一个儒雅随和的男子1 天前
Redis连接超时排查与优化指南
redis·bootstrap·php
我叫汪枫1 天前
【刷机分享】解决K20Pro刷入PixelOS后“网络连接”受限问题(附详细ADB命令)
开发语言·adb·php
老程序员刘飞1 天前
hardhat 搭建智能合约
开发语言·php·智能合约
海外住宅ip供应商-luck1 天前
Smartproxy API 代理 IP 提取指南——JSON-first 架构与参数化最佳实践
tcp/ip·架构·json
21号 11 天前
C++ 从零实现Json-Rpc 框架
网络协议·rpc·json
多多*1 天前
Spring Bean的生命周期 第二次思考
java·开发语言·rpc