梦应逍遥 发表于 2025-2-17 11:36:53

[实现Rpc] 客户端划分 | 框架设计 | common类的实现

目录
3. 客户端模块划分
3.1 Network模块
3.2 Protocol模块
3.3 Dispatcher模块
3.4 Requestor模块
3.5 RpcCaller模块
3.6 Publish-Subscribe模块
3.7 Registry-Discovery模块
3.8 Client模块
4. 框架设计
4.1 抽象层
4.2 具象层
4.3 业务层
⭕4.4 团体设计框架
5. common 类的实现
5.1 常用的零碎功能接口类实现
5.1.1 简单日记宏实现
5.1.2 Json序列化/反序列化的封装
5.1.3 UUID的天生
5.2 fields 消息类型定义
5.2.1 请求字段宏的定义:
5.2.2 消息类型定义:
5.2.3 相应码类型定义:
5.2.4 RPC请求类型定义:
5.2.5 主题操作类型定义:
5.2.6 服务操作类型定义:

前文:[实现Rpc] 项目设计 | 服务端模块划分 | rpc | topic | server
3. 客户端模块划分

在客户端的模块划分中,基于以上明白的功能,可以划分出这么级个模块:

[*]Protocol:应用层通讯协议模块。
[*]Network:网络通讯模块。
[*]Dispatcher:消息分发处理模块。
[*]Requestor:请求管理模块。
[*]RpcCaller:远端调勤劳能模块。
[*]Publish-Subscribe:发布订阅功能模块。
[*]Registry-Discovery:服务注册/发现/上线/下线功能模块。
[*]Client:基于以上模块整合⽽出的客户端模块。
3.1 Network模块

网络通讯基于muduo库实现网络通讯客户端。
3.2 Protocol模块

应用层通讯协议处理,与服务端保持⼀致。
3.3 Dispatcher模块

IO数据分发处理,逻辑与服务端⼀致。
3.4 Requestor模块

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


[*]首先,对于客户端来说,差异的地方在于更多时候客户端是请求方,是主动发起请求服务的⼀方
[*]而在多线程的网络通讯中,多线程下,针对 多个请求举行相应可能会存在时序的问题,这种环境下,我们则无法包管⼀个线程发送⼀个请求后,接下来接收到的相应就是针对自己这条请求的相应,这种环境黑白常危险的⼀种环境。
[*]其次,类似于Muduo库这种异步IO网络通讯库,通常 IO操作都是异步操作,即发送数据就是把数据放入发送缓冲区,但是什么时候会发送由底层的网络库来举行和谐,并且也并不会提供recv接口,而是在连接触发可读变乱后,IO读取数据完成后调用处理回调举行数据处理,因此也无法直接在发送请求后去期待该条请求的相应。(对 于网络通讯前文有提到~ port | netstat | udp缓冲区 | stm32)
针对以上问题,我们则创建出当前的请求管理模块来解决:


[*]它的思想也非常简单,就是给每⼀个请求都设定⼀个请求ID,服务端举行相应的时候标识相应针对的是哪个请求(也就是 相应信息中会包含请求ID)
[*]因此客户端这边我们不管收到哪条请求的相应,将数据存储入⼀则hash_map中,以请求ID作为映射,并向外提供获取指定 请求ID相应的壅闭接口,这样只要在发送请求的时候知道自己的请求ID,那么就能获取到自己想要的相应,而不会出现异常。
针对这个思想,我们再进⼀步,可以将每个请求进⼀步封装描述,添加⼊异步的future控制,或者设置回调函数的方式,在不仅可以壅闭获取相应,也可以实现 异步获取相应以及回调处理相应。

https://i-blog.csdnimg.cn/img_convert/41051a478c75ab72db577125dc336736.jpeg
3.5 RpcCaller模块

(1)RpcCaller模块存在的意义:向用户提供举行rpc调⽤的模块。
Rpc服务调用模块,这个模块相对简单,只需要向外提供几个rpc调用的接口,内部实现向服务端发送请求,期待获取结果即可,稍微麻烦⼀些的是Rpc调用我们需要提供多种差异方式的调用:

[*]同步调用:发起调用后,等收到相应结果后返回。
[*]异步调用:发起调用后立刻返回,在想获取结果的时候举行获取。
[*]回调调用:发起调用的同时设置结果的处理回调,收到相应后⾃动对结果举行回调处理。

https://i-blog.csdnimg.cn/img_convert/2a46195d21750c3cc9749a409c32ce06.png
3.6 Publish-Subscribe模块

(1)Publish-Subscribe模块存在意义:向用户提供发布订阅所需的接口,针对推送过来的消息举行处理。


[*]发布订阅稍微复杂⼀点,因为在发布订阅中有两种角色,⼀个客户端可能是消息的发布者,也可能是消息的订阅者。
[*]而且不管是哪个角色都是 对主题举行操作,因此其中也包含了主题的干系操作,比如要发布⼀条消息需要先创建主题。
[*]且⼀个订阅者可能会订阅多个主题,每个主题的消息可能都会有差异的处理方式,因此需要有订阅者主题回调的管理。

https://i-blog.csdnimg.cn/img_convert/a6a798f9619a2789b2a4b1fd8b66f59a.png
3.7 Registry-Discovery模块

(1)服务注册和发现模块需要实现的功能会复杂⼀些,因为分为两个角色来完成其功能:


[*]注册者:作为Rpc服务的提供者,需要向注册中心注册服务,因此需要实现向服务器注册服务的功能。
[*]发现者:作为Rpc服务的调用者,需要先辈行服务发现,也就是向服务器发送请求获取能够提供指定服务的主机地址,获取地址后需要管理起来留用,且作为发现者,需要关注注册中心发送过来的服务上线/下线消息,以及时对已经下线的服务和主机举行管理。

https://i-blog.csdnimg.cn/img_convert/60beaba63f7f8f14c6e712b6644f9db5.png
3.8 Client模块

将以上模块举行整合就可以实现各个功能的客户端了。


[*]RegistryClient:服务注册功能模块与⽹络通讯客户端结合。
[*]DiscoveryClient:服务发现功能模块与⽹络通讯客户端结合。
[*]RpcClient:DiscoveryClient & RPC功能模块与网络通讯客户端结合。
[*]TopicClient:发布订阅功能模块与⽹络通讯客户端结合。

https://i-blog.csdnimg.cn/img_convert/7797bb092f04232c0bd91eaa1298eb9d.png

https://i-blog.csdnimg.cn/img_convert/b2651c09875c21f573cee50006231938.png

https://i-blog.csdnimg.cn/img_convert/5648a329e0b69bec96b61453ff7143a7.png
https://i-blog.csdnimg.cn/img_convert/3e242b26471981966efab36c9073c40a.png

4. 框架设计

在当前项目标实现中,我们将整个项目标实现划分为三层来举行实现:

[*]抽象层:将底层的网络通讯以及应用层通讯协议以及请求相应举行抽象,使项目更具扩展性和机动性。
[*]具象层:针对抽象的功能举行具体的实现。
[*]业务层:基于抽象的框架在上层实现项目所需功能。
4.1 抽象层

在本项目标实现当中,网络通讯部分采用了第三方库Muduo库,以及通讯协议使用了LV格式的通讯协议解决粘包问题,数据正文中采用了Json格式举行序列化和反序列化,而这几方面我们都可能会存在继续优化的可能,甚至在序列化⽅方面不⼀定非要采用Json
因此在设计项目框架的时候,我们对于底层通讯部分干系功能先辈行抽象,形成⼀层抽象层
而上层业务部分根据抽象层来完成功能,这样的好处是在具体的底层功能实现部分,我们可以实现插拔式的模块化更换,以此来进步项目标机动性和扩展性。

https://i-blog.csdnimg.cn/img_convert/5d4cbd3e134f05cc9ce8514edfb9225e.png

https://i-blog.csdnimg.cn/img_convert/7bc02ea9e5f401fd32c113953cae8ec7.png

https://i-blog.csdnimg.cn/img_convert/43cca3e65597c60a301fa2d454233d78.png
4.2 具象层

具象层就是针对抽象的具体实现。
而具体的实现也比力简单,从抽象类派生出具体功能的派生类,然后在内部实现各个接口功能即可。


[*]基于Muduo库实现网络通讯部分抽象。
[*]基于LV通讯协议实现Protocol部分抽象。
不过这⼀层中比力特殊的是,我们需要 针对差异的请求,从BaseMessage中派生出差异的请求和相应类型,以便于在针对指定消息处理时,能够更加轻松的获取或设置请求及相应中的各项数据元素。

https://i-blog.csdnimg.cn/img_convert/762ae433c66b97881406e3e251211387.png
4.3 业务层

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

https://i-blog.csdnimg.cn/img_convert/59aa2f75ba2ab83d54670db932fd7237.png
(2)发布订阅:

https://i-blog.csdnimg.cn/img_convert/519a0a75ec4e1f26a942874b65187307.png
(3)服务注册&发现:

https://i-blog.csdnimg.cn/img_convert/9a77ea35bd8ab9c47c35179931fd828b.png
⭕4.4 团体设计框架


https://i-blog.csdnimg.cn/img_convert/41f25fd40ff95866c4b07cc435be47ea.png
项目当中会有server和client共同使用的类,也有单独使用的类,所以我们分别实现对应的功能
5. common 类的实现

5.1 常用的零碎功能接口类实现

5.1.1 简单日记宏实现

日记宏意义:快速定位程序运行逻辑出错的位置。
参考前文:简单日记宏实现(C++)
项目在运行中可能会出现各种问题,出问题不可怕,关键的是要能找到问题,并解决问题。解决问题的方式:


[*]gdb调试:逐步调试过于繁琐,缓慢。主要⽤于程序瓦解后的定位。
[*]体系运行日记分析:在任何程序运行有可能逻辑错误的位置举行输出提示,快速定位逻辑问题的位置。
#pragma once
#include <cstdio>
#include <ctime>

#define LDBG 0
#define LINF 1
#define LERR 2

#define LDEFAULT LDBG

#define LOG(level, format, ...)                                                                     \
    {                                                                                                 \
      if (level >= LDEFAULT)                                                                        \
      {                                                                                             \
            time_t t = time(NULL);                                                                  \
            struct tm *lt = localtime(&t);                                                            \
            char time_tmp = {0};                                                                  \
            strftime(time_tmp, 31, "%m-%d %T", lt);                                                   \
            fprintf(stdout, "[%s][%s:%d] " format "\n", time_tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
      }                                                                                             \
    }

#define DLOG(format, ...) LOG(LDBG, format, ##__VA_ARGS__);
#define ILOG(format, ...) LOG(LINF, format, ##__VA_ARGS__);
#define ELOG(format, ...) LOG(LERR, format, ##__VA_ARGS__); 5.1.2 Json序列化/反序列化的封装

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

class JSON
{
public:
    static bool serialize(const Json::Value &val, std::string &body)
    {
      std::stringstream ss;
      // 先实例化一个工厂类对象
      Json::StreamWriterBuilder swb;
      // 通过工厂类对象来生产派生类对象
      std::unique_ptr<Json::StreamWriter> w(swb.newStreamWriter());
      bool ret = w->write(val, &ss);
      if (ret != 0)
      {
            ELOG("json serialize failed!");
            return false;
      }

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

    static bool deserialize(const std::string &body, Json::Value &val)
    {
      Json::CharReaderBuilder crb;
      std::unique_ptr<Json::CharReader> r(crb.newCharReader());

      std::string errs;
      bool ret = r->parse(body.c_str(), body.c_str() + body.size(), &val, &errs);
      if (ret == false)
      {
            ELOG("json deserialize failed : %s", errs.c_str());
            return false;
      }
      return true;
    }
}; 5.1.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进制字符的组合情势来 确保全局唯⼀的同时能够根据序号来分辨数据(随机数肉眼分辨起来真是太难了…)。
#include <iostream>
#include <chrono>
#include <random>
#include <string>
#include <sstream>
#include <atomic>
#include <iomanip>

class UUID
{
public:
    static std::string uuid()
    {
      std::stringstream ss;
      //1. 构造一个机器随机数对象
      std::random_device rd;
      //2. 以机器随机数为种子构造伪随机数对象
      std::mt19937 generator (rd());
      //3. 构造限定数据范围的对象
      std::uniform_int_distribution<int> distribution(0, 255);
      // 4. 生成8个随机数,按照特定格式组织成为16进制数字字符的字符串
      for (int i = 0; i < 8; i++)
      {
            if (i == 4 || i == 6)
            {
                ss << "-";
            }

            ss << std::setw(2) << std::setfill('0') << std::hex << distribution(generator);
      }

      ss << "-";
      //5. 定义一个8字节序号,逐字节组织成为16进制数字字符的字符串
      static std::atomic<size_t> seq(1);
      size_t cur = seq.fetch_add(1);
      for (int i = 7; i >= 0; i--)
      {
            if (i == 5)
            {
                ss << "-";
            }

            ss << std::setw(2) << std::setfill('0') << std::hex << ((cur >> (i * 8)) & 0xFF);
      }

      return ss.str();
    }
};
定义一个天生 UUID(通用唯一标识符) 的工具类 UUID,其核心功能是通过结合随机数和计数器天生一个类似于尺度 UUID 格式的字符串。

[*]使用硬件随机数天生器和梅森旋转算法天生随机数。
[*]将天生的随机数按十六进制格式构造,并在特定位置插入 "-"。
[*]使用静态原子计数器确保 UUID 的唯一性,并将计数器值按字节格式化为十六进制。
[*]最终,返回一个形如 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 的字符串,表示一个天生的 UUID。
这种方式天生的 UUID 包含了随机性和递增序列,确保了其在大多数环境下的唯一性。
5.2 fields 消息类型定义

5.2.1 请求字段宏的定义:

定义如下:
#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"   进步代码的可读性:通过给常量字符串定义宏名称,可以使代码更加易读和易于明白。例如,KEY_METHOD 比直接使用 "method" 更直观地表达了该字符串在代码中的作用。
5.2.2 消息类型定义:

(1)主要功能:


[*]Rpc请求 & 相应。
[*]主题操作请求 & 相应。
[*]消息发布请求 & 相应。
[*]服务操作请求 & 相应。
(2)具体定义如下:
enum class MType
{
/*
Request
Response
*/

REQ_RPC = 0,
RSP_RPC,
REQ_TOPIC,
RSP_TOPIC,
REQ_SERVICE,
RSP_SERVICE
}; 5.2.3 相应码类型定义:

(1)主要功能:


[*]成功处理。
[*]解析失败。
[*]消息中字段缺失或错误导致无效消息。
[*]连接断开。
[*]无效的Rpc调用参数。
[*]Rpc服务不存在。
[*]无效的Topic操作类型。
[*]主题不存在。
[*]无效的服务操作类型。
(2)具体定义如下:
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)
{
    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 iter = err_map.find(code);
    if (iter == err_map.end())
    {
      return "未知错误";
    }

    return iter->second;
} 5.2.4 RPC请求类型定义:

(1)主要功能:


[*]同步请求:期待收到相应后返回。
[*]异步请求:返回异步对象,在需要的时候通过异步对象获取相应结果(还未收到结果会壅闭)。
[*]回调请求:设置回调函数,通过回调函数对相应举行处理。
(2)具体定义如下:
enum class RType
{
    REQ_ASYNC = 0,
    REQ_CALLBACK
}; 5.2.5 主题操作类型定义:

(1)主要功能:


[*]主题创建。
[*]主题删除。
[*]主题订阅。
[*]主题取消订阅。
[*]主题消息发布。
(2)具体定义如下:
enum class TopicOptype
{
    TOPIC_CREATE = 0,
    TOPIC_REMOVE,
    TOPIC_SUBSCRIBE,
    TOPIC_CANCEL,
    TOPIC_PUBLISH
}; 5.2.6 服务操作类型定义:

(1)主要功能:


[*]服务注册。
[*]服务发现。
[*]服务上线。
[*]服务下线。
(2)具体定义如下:
enum class ServiceOptype
{
    SERVICE_REGISTRY = 0,
    SERVICE_DISCOVERY,
    SERVICE_ONLINE,
    SERVICE_OFFLINE,
    SERVICE_UNKNOW
}; ⭕


https://i-blog.csdnimg.cn/img_convert/c009256cdc24c9f8b44b82516a99a487.jpeg

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: [实现Rpc] 客户端划分 | 框架设计 | common类的实现