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

【从零实现Json-Rpc框架】- 项目设计篇

📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 📢前言
  • 🏳️‍🌈1 理解项目功能
  • 🏳️‍🌈2 框架设计
    • 2.1 服务端模块划分
      • 2.1.2 NetWork:网络通信模块
      • 2.1.3 Protocol: 应用层通信协议模块
      • 2.1.4 Dispatcher: 消息分发处理模块
      • 2.1.5 RpcRouter: 远端调用路由功能模块
      • 2.1.6 Publish-Subscribe: 发布订阅功能模块
      • 2.1.7 Registry-Discovery: 服务注册/发现/上线/下线功能模块
      • 2.1.8 Server: 基于以上模块整合而出的服务端模块
    • 2.2 客户端模型划分
      • 2.2.1 Protocol: 应用层通信协议模块
      • 2.2.2 Network: 网络通信模块
      • 2.2.3 Dispatcher: 消息分发处理模块
      • 2.2.4 Requestor: 请求管理模块
      • 2.2.5 RpcCaller: 远端调用功能模块
      • 2.2.6 Publish-Subscribe: 发布订阅功能模块
      • 2.2.7 Registry-Discovery: 服务注册/发现/上线/下线功能模块
      • 2.2.8 Client: 基于以上模块整合而出的客户端模块
    • 2.3 框架设计
      • 2.3.1 抽象层
      • 2.3.2 具象层
      • 2.3.3 业务层
      • 2.3.4 整体框架设计
  • 👥总结


📢前言

截至当前,笔者已经将所有基础要用的插件等都装好了,也较为详细地介绍了下几个第三方库

那么这篇,笔者就来介绍一下我们该如何设计一个 Json-Rpc框架


🏳️‍🌈1 理解项目功能

本质上来讲,我们要实现的rpc(远端调用)思想上并不复杂,甚至可以说是简单,其实就是客户端想要完成某个任务的处理,但是这个处理的过程并不自己来完成,而是,将请求发送到服务器上,让服务器来帮其完成处理过程,并返回结果,客户端拿到结果后返回。
在这里插入图片描述
然而上图的模型中,是一种多对一一对一的关系,一旦服务端掉线,则客户端无法进行远端调用,目其服务端的负载也会较高,因此在rpc实现中,我们不仅要实现其基本功能,还要再进一步,实现分布式架构的rpc

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

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

在这里插入图片描述
在这里插入图片描述
而其次的发布订阅功能,则是依托于多个客户端围绕服务端进行消息的转发。不过单纯的消息转发功能,并不能满足于大部分场景的需要,因此会在其基础上实现基于主题订阅的转发。
在这里插入图片描述
基于以上功能的合并,我们可以得到一个实现所有功能的结构图
在这里插入图片描述
在上图的结构中,我们甚至可以让每一个Server作为备用注册中心形成分布式架构,一旦一个注册中心下线,可以向备用中心进行注册以及请求,且在此基础上客户端在请求Rpc服务的时候,因为可以有多个rpc-provider可选,因此可以实现简单的负载均衡策略,且基于注册中心可以更简便实现发布订阅的功能。

项目的三个主要功能:

  • rpc调用
  • 服务的注册与发现以及服务的下线/上线通知
  • 消息的发布订阅

🏳️‍🌈2 框架设计

2.1 服务端模块划分

服务端的功能需求:

  • 基于网络通信接收客户端的请求,提供rpc服务
  • 基于网络通信接收客户端的请求,提供服务注册与发现,上线&下线通知
  • 基于网络通信接收客户端的请求,提供主题操作(创建/删除/订阅/取消),消息发布

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

  1. Network:网络通信模块
  2. Protocol:应用层通信协议模块
  3. Dispatcher:消息分发处理模块
  4. RpcRouter:远端调用路由功能模块
  5. Publish-Subscribe:发布订阅功能模块
  6. Registry-Discovery:服务注册/发现/上线/下线功能模块
  7. Server:基于以上模块整合而出的服务端模块

2.1.2 NetWork:网络通信模块

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

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

Protocol模块就是是网络通信协议模块的设计,也就是在网络通信中,我们必须设计一个应用层的网络通信协议出来,以解决网络通信中可能存在的粘包问题,而解决粘包有三种方式: 特殊字符间隔,定长,LV格式

而我们项目中将使用LV格式来定义应用层的通信协议格式

Length: 该字段固定4字节长度,用于表示后续的本条消息数据长度。
MType: 该字段为Value中的固定字段,固定4字节长度,用于表示该条消息的类型。

  • Rpc调用请求/响应类型消息。
  • 发布/订阅/取消订阅/消息推送类型消息
  • 主题创建/删除类型消息。
  • 服务注册/发现/上线/下线类型消息。

IDLength: 为消息中的固定字段,该字段固定4字节长度,用于描述后续ID字段的实际长度。
MID: 在每条消息中都会有一个固定字段为ID字段,用于唯一标识消息,ID字段长度不固定。
Body: 消息主题正文数据字段,为请求或响应的实际内容字段。

2.1.3 Protocol: 应用层通信协议模块

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

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

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

在这里插入图片描述

2.1.4 Dispatcher: 消息分发处理模块

消息类型:

  • rpc请求&响应
  • 服务注册/发现/上线/下线请求&响应
  • 主题创建/删除/订阅/取消订阅请求&响应,消息发布的请求&响应

在这里插入图片描述

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

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

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

  • 请求方法名称
  • 请求对应要处理的参数信息。

在Rpc远端调用中,首先将客户端到服务端的通信链路打通,然后将自己所需要调用的服务名称,以及参数信息传递给服务端,由服务端进行接收处理,并返回结果

而,不管是客户端要传递给服务端的服务名称以及参数信息,或者服务端返回的结果,都是在上边Protocol中定义的Bodv字段中,因此Bodv字段中就存在了另一层的正文序列化/反席列化过程。

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

// 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请求的业务处理函数。

在这里插入图片描述

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

Publish-Subscribe模块存在的意义: 针对发布订阅请求进行处理,提供一个回调函数设置给Dispatcher模块。
发布订阅所包含的请求操作:

  • 主题的创建
  • 主题的删除
  • 主题的订阅
  • 主题的取消订阅
  • 主题消息的发布

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

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

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

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

// 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. 该模块必须具备一个主题管理,且主题中需要保存订阅了该主题的客户端连接。主题收到一条消息,需要将这条消息推送给订阅了该主题的所有客户端
  2. 该模块必须具备一个订阅者管理,且每个订阅者描述中都必须保存自己所订阅的主题名称’。目的是为了当一个订阅客户端断开连接时,能够找到订阅信息的关联关系,进行删除
  3. 该模块必须向外提供 主题创建/销毁,主题订阅/取消订阅,消息发布处理的业务处理函数

在这里插入图片描述

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

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

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

  • 服务注册: 服务provider告诉中转中心,自己能提供哪些服务。
  • 服务发现: 服务caller询问中转中心,谁能提供指定服务。
  • 服务上线: 在一个provider上线了指定服务后,通知发现过该服务的客户端有个provider可以。提供该服务
  • 服务下线: 在一个provider断开连接,通知发现过该服务的caller,谁下线了哪个服务

服务注册模块,该模块主要是为了实现分布式架构而存在,让每一个rpc客户端能够从不同的节点主机上获取自己所需的服务,让业务更具扩展性,系统更具健壮性。而为了能够让rpc-caller知道有哪些rpc-provider能提供自己所需服务,那么就需要有一个注册中心让这些rpc-provider去注册登记自己的服务,让rpc-caller来发现这些服务:

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

// 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. 必须具备一个服务发现者的管理:
    • 方法与发现者: 当一个客户端进行服务发现的时候,进行记录谁发现过该服务,当有一个新的提供者上线的时候,可以通知该发现者
    • 连接与发现者: 当一个发现者断开连接了,删除关联关系,往后就不需要通知了
  2. 必须具备一个服务提供者的管理:
    • 连接与提供者: 当一个提供者断开连接的时候,能够通知该提供者提供的服务对应的发现者,
      该主机的该服务下线了
    • 方法与提供者: 能够知道谁的哪些方法下线了,然后通知发现过该方法的客户端
  3. 必须向Dispatcher模块提供一个服务注册/发现的业务处理回调函数

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

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

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

在这里插入图片描述

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

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

  • RpcServer:rpc: 功能模块与网络通信部分结合。
  • RegistryServer: 服务发现注册功能模块与网络通信部分结合
  • TopicServer: 发布订阅功能模块与网络通信部分结合。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2 客户端模型划分

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

  1. Protocol: 应用层通信协议模块
  2. Network: 网络通信模块
  3. Dispatcher: 消息分发处理模块
  4. Requestor: 请求管理模块
  5. RpcCaller: 远端调用功能模块
  6. Publish-Subscribe: 发布订阅功能模块
  7. Registry-Discovery: 服务注册/发现/上线/下线功能模块
  8. Client: 基于以上模块整合而出的客户端模块

2.2.1 Protocol: 应用层通信协议模块

网络通信基于muduo库实现网络通信客户端

2.2.2 Network: 网络通信模块

应用层通信协议处理,与服务端保持一致。

2.2.3 Dispatcher: 消息分发处理模块

I0数据分发处理,逻辑与服务端一致

2.2.4 Requestor: 请求管理模块

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

首先,对于客户端来说,不同的地方在于,更多时候客户端是请求方,是主动发起请求服务的一方,而在多线程的网络通信中,多线程下,针对多个请求进行响应可能会存在时序的问题,这种情况下,则我们无法保证一个线程发送一个请求后,接下来接收到的响应就是针对自己这条请求的响应,这种情况是非常危险的一种情况。

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

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

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

2.2.5 RpcCaller: 远端调用功能模块

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

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

  1. 同步调用: 发起调用后,等收到响应结果后返回
  2. 异步调用: 发起调用后立即返回,在想获取结果的时候进行获取
  3. 回调调用: 发起调用的同时设置结果的处理回调,收到响应后自动对结果进行回调处理

在这里插入图片描述

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

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

发布订阅稍微能复杂一丢丢,因为在发布订阅中有两种角色一个客户端可能是消息的发布者,也可能是消息的订阅者

而且不管是哪个角色都是对主题进行操作,因此其中也包含了主题的相关操作,比如,要发布一条消息需要先创建主题

且一个订阅者可能会订阅多个主题,每个主题的消息可能都会有不同的处理方式,因此需要有订阅者主题回调的管理。

在这里插入图片描述

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

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

  1. 注册者: 作为Rpc服务的提供者,需要向注册中心注册服务,因此需要实现向服务器注册服务的功能
  2. 发现者: 作为Rpc服务的调用者,需要先进行服务发现,也就是向服务器发送请求获取能够提供指定服务的主机地址,获取地址后需要管理起来留用,且作为发现者,需要关注注册中心发送过来的服务上线/下线消息,以及时对已经下线的服务和主机进行管理。
    在这里插入图片描述

2.2.8 Client: 基于以上模块整合而出的客户端模块

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

  • RegistryClient: 服务注册功能模块与网络通信客户端结合
  • DiscoveryClient: 服务发现功能模块与网络通信客户端结合
  • RpcClient: DiscoveryClient&RPC功能模块与网络通信客户端结合
  • TopicClient: 发布订阅功能模块与网络通信客户端结合

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3 框架设计

2.3.1 抽象层

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

  1. 抽象层:将底层的网络通信以及应用层通信协议以及请求响应进行抽象,使项目更具扩展性和灵活性。
  2. 具象层:针对抽象的功能进行具体的实现。
  3. 业务层:基于抽象的框架在上层实现项目所需功能。

在咱们的项目实现中,网络通信部分采用了第三方库Muduo库,以及通信协议使用了LV格式的通信协议解决粘包问题,数据正文中采用了Json格式进行序列化和反序列化,而这几方面我们都可能会存在继续优化的可能,甚至在序列化方面不一定非要采用Json,因此在设计项目框架的时候,我们对于底层通信部分相关功能先进行抽象,形成一层抽象层,而上层业务部分根据抽象层来完成功能,这样的好处是在具体的底层功能实现部分,我们可以实现插拔式的模块化替换,以此来提高项目的灵活性和扩展性。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.3.2 具象层

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

  • 基于Muduo库实现网络通信部分抽象.
  • 基于LV通信协议实现Protocol部分抽象。

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

2.3.3 业务层

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

RPC
在这里插入图片描述
发布订阅
在这里插入图片描述
服务注册&发现
在这里插入图片描述

2.3.4 整体框架设计

在这里插入图片描述


👥总结

本篇博文对 【从零实现Json-Rpc框架】- 项目设计篇 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

请添加图片描述


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

相关文章:

  • ngx_conf_parse - location块
  • C# MemoryStream 中 ToArray 和 GetBuffer 的区别
  • 唯品会 unidbg 补环境 分析
  • 菜鸟的程序编程理解
  • 《大语言模型》学习笔记(四)--Transformer 模型
  • 大模型思维链COT:Chain-of-Thought Prompting Elicits Reasoningin Large Language Models
  • k8s存储介绍(二)Secret
  • 爬虫豆瓣电影
  • 国内常用各类证件照的尺寸,证件照尺寸大小汇总【免费改图网站】
  • 《Python实战进阶》第33集:PyTorch 入门-动态计算图的优势
  • 微软纳德拉最新一期访谈
  • 基于Java,SpringBoot和Vue高考志愿填报辅助系统设计
  • aab 转 apk
  • 前端安全加密方式
  • mknod命令与device_create函数的关系
  • 类和对象—封装
  • KNN算法+鸢尾花分类+手写数字识别案例
  • Swift实现嵌套json字典重排序并输出string
  • 树状数组模板
  • leetcode 之(移除元素)