【jsonRpc】项目介绍

目录

一.项目概览

二.服务端模块划分

2.1.Network模块

2.2.Protocol模块(应用层通信协议模块)

2.3.Dispatcher模块

2.4.RpcRouter模块

2.5.Publish-Subscribe(发布订阅)模块

2.6.Registry-Discovery模块

三.客户端模块划分

3.1.Network:⽹络通信模块

[3.2. Protocol:应⽤层通信协议模块](#3.2. Protocol:应⽤层通信协议模块)

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

3.4.Requestor模块

[3.5.RpcCaller 模块](#3.5.RpcCaller 模块)

[3.6.Publish-Subscribe 模块(发布-订阅模块)](#3.6.Publish-Subscribe 模块(发布-订阅模块))

3.7.Registry-Discovery模块


一.项目概览

如果直观上来说的话,我们这个思想其实是很简单的

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

注册中心的引入

在传统的RPC(远程过程调用)模型中,若客户端与服务端采用多对一或一对一的直接连接方式,一旦服务端发生故障或下线,客户端将无法继续发起远程调用,同时服务端也容易因集中处理所有请求而负载过高。

因此,在实际的RPC实现中,我们不仅要完成基本的远程调用功能,还应进一步构建支持分布式架构的RPC系统。

分布式架构可简单理解为由多个节点(通常指服务器)协同组成的系统。通过将不同业务模块,或同一业务的不同部分,拆分并部署到多个节点上,系统能够更有效地应对高并发场景,从而提升整体扩展性和可用性。

实现这一目标的核心思路并不复杂:在原有RPC模型基础上引入一个注册中心 。服务提供者启动时向注册中心注册自身所能提供的服务,即告知注册中心其具备的服务能力;客户端在发起远程调用前,先通过注册中心进行服务发现,查询并获得可提供对应服务的服务器地址,继而完成调用。

这一架构还带来以下优势:

  • 负载均衡:客户端可从多个服务提供者中选择一个,避免单一节点压力过大;

  • 高可用:当某个服务节点不可用时,注册中心可将其剔除,客户端可自动发现其他可用节点;

  • 动态扩展:新增服务节点时,只需向注册中心注册,即可被客户端发现并使用。

通过引入注册中心与服务发现机制,RPC系统得以从单点结构演进为真正意义上的分布式服务架构,显著提升了系统的弹性与可维护性。

基于注册中心的结构,还可以进一步扩展出发布订阅 功能。传统的消息转发模式通常依赖于客户端直接与服务器通信,但这种简单的广播或群发机制往往无法满足复杂业务场景的需求。因此,我们可以在此基础上,实现基于主题的订阅与分发机制:服务提供者或特定客户端可向注册中心发布消息,而其他客户端则根据自己感兴趣的主题进行订阅,仅接收与之相关的消息,从而实现更高效、更解耦的通信。

注册中心挂了怎么办?

如果说我们的注册中心挂了,那岂不是这个服务也挂了。

更进一步,我们可以将注册中心本身设计为高可用的分布式集群。

例如,让多个注册中心节点以对等或主从方式协同工作,形成一个可靠的注册中心集群

这样,即使某个注册中心节点下线,服务提供者与消费者也能自动切换到其他可用节点进行注册与发现,确保整个RPC框架的核心协调功能持续可用。

在此基础上,整个系统将具备更完善的分布式特性:

  • 服务端负载均衡:由于存在多个可用的服务提供者,客户端或注册中心可以实施灵活的负载均衡策略(如轮询、随机、加权等),将请求分发到不同实例,进一步提升系统整体的吞吐与弹性。

  • 服务的动态治理:注册中心集群能够实时感知服务节点的上下线,并通知客户端更新服务列表,实现热部署与无缝扩缩容。

  • 增强的发布订阅能力:借助分布式的注册中心,主题订阅关系可以集中管理并跨节点同步,使得发布订阅功能在分布式环境下也能保持一致与可靠。

综上所述,通过引入注册中心集群基于主题的发布订阅 以及客户端负载均衡等机制,RPC框架便从一个简单的远程调用工具,演进为真正具备高可用、易扩展、松耦合特性的分布式服务基础设施。这不仅提升了系统应对故障的能力,也为微服务架构、事件驱动架构等现代分布式模式提供了坚实的基础支撑。

项目总结

项⽬的三个主要功能:

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

二.服务端模块划分

服务端的功能需求:

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

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

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

2.1.Network模块

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

2.2.Protocol模块(应用层通信协议模块)

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

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

⽽Protocol模块就是是⽹络通信协议模块的设计,也就是在⽹络通信中,我们必须设计⼀个应⽤层通信处理协议模块的。

⽹络通信中可能存在粘包问题,⽽解决粘包有三种⽅式:特殊字符间

隔,定⻓,LV格式。

  1. 特殊字符间隔法 :通过在数据包末尾添加一个双方约定的特殊字符(如换行符\n)作为"终止符",接收方持续读取数据并以此字符作为分割点来拆分成独立数据包。这种方法实现简单,但需确保数据内容本身不包含该特殊字符,否则需额外进行转义处理。
  2. 定长协议法:规定每个数据包具有固定的字节长度。发送方将不足长度的数据填充至指定长度,接收方每次严格读取固定长度的数据作为一个完整包。这种方式处理简单直接,但灵活性差,对于可变长度数据会造成带宽浪费或设计复杂。
  3. LV格式(Length-Value) :**在数据包头部用一个固定长度的字段(例如4字节整数)来明确标识后续数据部分(Value)的实际长度。**接收方先读取固定长度的包头解析出长度L,再精确读取后续L字节的数据,自然避免了粘包。

⽽我们项⽬中将使⽤LV格式来定义应⽤层的通信协议格式。因为它兼具灵活性(支持变长数据)和高效率(无转义开销,内存精准)。

那么我们定义的网络通信模块。

Length字段(4字节固定长度): 这是协议的核心字段,固定占用4个字节(32位),采用大端字节序存储。它表示本条消息中Value部分的总字节长度,为接收方提供精确的数据读取边界。接收方首先读取这4个字节,即可准确知道后续需要接收多少数据才能构成一个完整的消息包。

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

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

IDLength(ID长度字段,4字节固定):

指示后续MID字段的实际字节长度。虽然MID长度可变,但通过这个固定长度的描述字段,接收方能够准确解析MID的边界。

MID(消息标识符,可变长度):

每条消息的唯一标识,用于消息追踪、去重和请求-响应对应。其长度由IDLength字段明确指定,确保灵活性与精确性的平衡。

Body(消息正文,可变长度):

承载消息的实际业务数据,是请求或响应的核心内容。

其长度可通过公式计算得出:
Body长度 = Length值 - (4 + 4 + IDLength值),这种设计既保证了协议的扩展性,又实现了内存的高效利用。

什么叫应用层协议处理?

当有新消息到来,那么框架底层(muduo网络库)就会调用onMessage回调函数,那么在这个回调函数里面,我们需要对接收到的原始字节流完成应用层协议解析,那么这个应用层协议处理的过程,就是存在于Protocol模块,经过应用层协议处理解析后,我们得到的是一个结构化的消息对象。

那么应用层协议处理包含什么呢?

应用层协议处理是将网络传输的二进制数据转换为应用程序可识别业务数据的过程。

这一过程包含三个核心步骤:

  • 提取(从二进制流中获取完整数据包):网络数据以连续字节流形式到达。首先需要从接收缓冲区中提取出一个完整协议单元。使用LV格式时,先读取前4字节得到数据长度,再读取对应长度的字节,确保获得完整数据包。
  • **分割(按协议格式拆分数据包):**获得完整数据包后,按照协议定义的字段结构进行拆分。在LV格式中,按顺序解析出:消息类型(4字节)、ID长度(4字节)、ID(变长)和消息正文(剩余部分)。每个字段都有明确的位置和长度。
  • **赋值(将二进制数据转换为业务对象):**将分割后的二进制数据转换为应用程序可处理的业务对象。例如:将消息类型字节转换为枚举值,将ID字节转换为字符串,将消息正文字节反序列化为具体的数据结构。最终得到完整的业务消息对象,供后续逻辑处理。

简而言之,应用层协议处理就是从原始字节流中提取、分割和转换数据,使应用程序能够理解网络传输的信息。

2.3.Dispatcher模块

Dispatcher模块在RPC通信框架中扮演着消息路由中枢的角色。

它的核心职责是:根据消息的类型,将其精准分派到对应的业务处理函数中执行,从而实现网络数据包与上层应用逻辑的解耦。

当有新消息到来,那么框架底层(muduo网络库)就会调用onMessage回调函数,那么在这个回调函数里面,我们需要对接收到的原始字节流完成应用层协议解析,那么这个应用层协议处理的过程,就是存在于上面的Protocol模块,经过应用层协议处理解析后,我们得到的是一个结构化的消息对象。

这个结构化的消息对象里面有很多数据:比如说这条消息代表客户端的何种请求。

但是我们现在仅仅只是获取到了客户端发来的这个数据,我们还需要对这个消息进行处理。

我们需要根据消息类型来作出对应的处理,去选择调用不同的消息处理回调函数。

Dispatcher类正是为此而设计。

工作原理与设计

  • Dispatcher内部维护一个 "消息类型-处理函数"映射表 (通常实现为std::unordered_map或类似哈希结构)。
  • 框架的使用者可以提前向该模块注册不同消息类型对应的业务回调函数的注册。事实上,下面的RpcRouter模块就会完成这个业务处理回调函数的注册
  • 当一条消息抵达时,Dispatcher通过其类型标识快速查找映射表,获取对应的回调函数并执行,完成消息的处理闭环。

核心处理的消息类型

Dispatcher需要区分并处理以下几类核心消息,它们共同支撑了RPC框架的基础功能与高级特性:

  1. RPC调用与响应

    • RPC请求:客户端发起的远程方法调用。

    • RPC响应:服务端返回的方法执行结果或错误。

  2. 服务治理相关

    • 服务注册/发现请求与响应:服务提供者向注册中心注册服务,消费者查询可用服务。

    • 服务上线/下线通知:注册中心主动推送服务节点状态变更。

  3. 发布/订阅相关

    • 主题管理请求:创建、删除主题。

    • 订阅管理请求:订阅或取消订阅特定主题。

    • 消息发布请求与投递:向指定主题发布消息,并由系统分发给所有订阅者。

通过这种设计,Dispatcher模块将复杂的网络消息处理流程标准化、模块化。它不仅简化了业务逻辑的接入流程------新功能只需注册新的消息类型与回调即可接入系统,同时也提高了框架的可扩展性与可维护性,为整个RPC系统的清晰分层与高效协作奠定了坚实基础。

2.4.RpcRouter模块

首先,我们先回答一个问题

还记得Dispatcher类是怎么进行设计的吗

  • Dispatcher内部维护一个 "消息类型-处理函数"映射表 (通常实现为std::unordered_map或类似哈希结构)。
  • 框架的使用者 可以提前向Dispatcher模块注册不同消息类型 对应的业务回调函数的注册。事实上,我们的RpcRouter模块就会写好消息类型是RPC请求 的业务处理回调函数,然后在Dispatcher模块里面统一进行注册的。
  • 当一条消息抵达时,Dispatcher通过其类型标识快速查找映射表,获取对应的回调函数并执行,完成消息的处理闭环。

注意注册的这个过程,其实不是在我们这个模块里面实现的,而是在Dispatcher模块里面统一进行注册的。

RPC请求里面其实有很多种RPC服务,其实也就是有很多方法,我们还需要进行更细分的进行管理,每个RPC请求到底是想要调用哪个方法,我们必须搞清楚。

那么RpcRouter模块就是专门来管理这么多不同种RPC请求的。

RpcRouter模块

  • 存在的意义:向Dispatcher模块里面注册 Rpc请求类型对应的业务回调函数。
  • 这个业务处理回调函数主要负责解析传入的RPC请求,根据请求的方法名称找到对应的业务处理函数,执行并返回结果。

具体来说,这个业务处理回调函数内部需要实现的关键功能包括:

  1. 准确识别客户端请求的服务
  2. 验证参数有效性
  3. 触发相应的业务逻辑
  4. 并最终组织响应数据

写好这个业务处理回调函数之后,我们后续在编写这个Dispatcher模块时就会进行对这个业务处理回调函数注册。


现在我们就来研究一下RPC请求的核心数据:

一次完整的RPC调用,其请求中必须包含两个最关键的要素:

  • 请求方法名称:指明需要调用的具体服务。

  • 请求参数信息:提供执行该方法所需的输入数据。

我们从哪里获取RCP请求,这些数据存在于哪里?

无论是客户端发送的请求数据(方法名和参数),还是服务端返回的结果,都承载于我们在通信协议(Protocol)中定义的Body字段内。

在RPC流程中,客户端首先建立到服务端的通信连接,随后将方法名和参数信息封装并发送。

服务端的RpcRouter模块在接收到请求后,负责解析、校验并处理,最后将执行结果返回给客户端。

无论是客户端发送的请求数据(方法名和参数),还是服务端返回的结果,都承载于我们在通信协议(Protocol)中定义的Body字段内。

这句话什么意思呢?我们上面不是已经定义了一个协议了吗?为什么这个所有发布订阅相关的请求与响应均采用JSON格式在协议Body字段中承载?

在整个通信过程中,无论是客户端发送的请求(包含方法名和参数),还是服务端返回的响应(包含结果或状态),其核心的业务数据都统一承载于我们自定义应用层协议的 Body 字段中。

理解这一点,需要明确我们设计中的两个关键层次:传输协议层与业务数据层。

  • 我们首先定义的传输协议,其核心目的是解决网络通信中的基础问题:如何确保一个完整的数据包能被准确、无误地发送与接收。它规定了数据包的通用结构(例如包含标识、长度、序列号、Body 等固定字段),类似于为运输货物设计了一个标准的、带有标签和尺寸说明的"集装箱"。这个"集装箱"保证了数据在网络传输层面的可靠性与完整性。
  • 然而,这个"集装箱"(即协议)内部的"货物"如何摆放、如何解读,则由业务数据层来决定。在我们的系统中,所有与发布订阅相关的具体信息,都采用结构化的JSON格式进行组织,并最终作为一串字符序列(字符串)放入Body字段。

因此,一个完整的通信流程包含了两个紧密衔接的解析阶段:

  1. 协议解析阶段: 接收方首先根据自定义协议的规则(也就是我们上面Protocol模块里面讲的那个通信协议),**从网络字节流中识别出一个完整的数据包,并准确地提取出其中的 Body 字段内容。**此时,Body 被视为一个不透明的二进制字符串或字节数组。
  2. 业务数据解析(反序列化)阶段: 在此之后,我们需要对这个 Body 字符串进行进一步的"拆封"和解读。具体而言,就是按照 JSON 的语法规则对Body 字符串进行解析 ,从中分离出结构化的键值对,例如 "method": "subscribe"、"topic": "news" 等。随后,**将这些解析出的数据赋值到程序内部易于操作的数据结构(例如一个 Json::Value 对象)**中,从而将原始的字符串转换为内存中有意义的数据对象,供业务逻辑代码使用。

**因为我们的数据都是以JSON格式进行封装传递的。**无论是客户端发送的请求数据(方法名和参数),还是服务端返回的结果,都承载于我们在通信协议(Protocol)中定义的Body字段内。

基于当前实现JSON-RPC的考虑,我们初步采用JSON格式进行封装,定义如下:

请求与响应格式示例

javascript 复制代码
// RPC 请求(Request)
{
  "method": "Add",
  "parameters": {
    "num1": 11,
    "num2": 22
  }
}

// RPC 成功响应(Response)
{
  "rcode": "OK",
  "result": 33
}

// RPC 错误响应(Response)
{
  "rcode": "ERROR_INVALID_PARAMETERS"
  "reason": "无效的参数信息!"
}

参数是如何传递给回调函数的?

参数是通过 Json::Value 对象直接传递给回调函数的。

为什么这样子设定呢?

其实根本原因就是我们传输的内容就是JSON格式。那么使用json::Value,我们就能更加方便的去传递给我们的回调函数。

让我详细解释这个传递过程:

回调函数的定义

首先,定义回调函数的类型:

javascript 复制代码
using ServiceCallback = std::function<void(const Json::Value&, Json::Value &)>;

这是一个接受两个参数的函数:

  • const Json::Value&:输入参数(只读)
  • Json::Value &:输出结果(可修改)

我们就一个作为输入参数,另外一个就作为函数返回值,

参数检验

RpcRouter模块 向Dispatcher模块里面注册好了 Rpc请求类型对应的业务回调函数。

服务端的Dispatcher模块会将 RPC请求类型的消息 路由至RpcRouter进行处理。

RpcRouter模块在调用具体的业务回调函数时,仅会将parameters字段的数据传入。

然而,在RpcRouter模块在调用具体的业务回调函数之前,我们必须对传入的参数进行事前校验,以确认其名称、类型、数量是否符合该服务的要求,只有校验通过后才能提取数据执行业务逻辑。

需要注意的是,

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

然⽽,对服务端来说,应该从传⼊的Json::Value对象中 ,有什么样的参数,以及参数信息是否符合⾃⼰所提供的服务的要求(其实就是想要调用的那个存在于服务器的一个函数的参数类型,返回值 匹不匹配的问题),都应该有⼀个检测,是否符合要求,符合要求了再取出指定字段的数据进⾏处理。

因此,服务端在注册服务 时,必须提供一份清晰的服务描述(其实就是将服务器上存在那些作为一个服务的函数的参数类型,返回值这些信息保存起来)

Add方法为例,其描述应包含:

  • 服务名称Add

  • 参数列表 :参数num1(整型)、参数num2(整型)

  • 返回值类型:整型

这份描述是RpcRouter实现强类型校验安全调用的基础。借助它,RpcRouter可以在执行业务逻辑前,确保调用方传递的数据是合法且完整的,从而大幅提升系统的健壮性。

总结

基于以上分析,RpcRouter模块的设计应包含以下核心部分:

  1. 路由与校验管理器:维护所有已注册的服务,并具备对每个服务的参数进行结构化校验的能力。

  2. 方法映射表 :维护一个从"方法名称 "到"业务处理回调函数"的高效映射关系,确保快速路由。注意这里的方法其实就是我们在服务器提供的那个函数。

  3. 统一的请求处理入口:对外提供一个统一的RPC请求处理函数,作为Dispatcher回调的目标。该函数内部将串联请求解析、服务查找、参数校验、业务执行及响应组装的完整流程。

通过这样的设计,RpcRouter模块不仅能实现基本的RPC调用分发,更能通过规范的服务描述和严格的参数校验,为整个RPC框架提供可靠、安全的业务处理基石。

2.5.Publish-Subscribe(发布订阅)模块

其实这个模块和上面的RpcRouter模块是一样的,就是针对发布订阅请求进⾏处理,提供⼀个回调函数设置给 Dispatcher模块。

Publish-Subscribe(发布订阅)模块的核心职责是处理所有与发布订阅模式相关的网络请求,并向Dispatcher模块提供统一的处理回调函数,从而将主题式的消息分发功能无缝集成到系统中。

还记得Dispatcher类是怎么进行设计的吗

  • Dispatcher内部维护一个 "消息类型-处理函数"映射表 (通常实现为std::unordered_map或类似哈希结构)。
  • 框架的使用者 可以提前向Dispatcher模块注册不同消息类型 对应的业务回调函数的注册。事实上,我们的Publish-Subscribe模块就会写好这个消息类型是发布订阅请求 的业务处理回调函数,后续我们就在这个Dispatcher模块里面对 这个写好的发布订阅请求的业务处理回调函数统一进行注册的。
  • 当一条消息抵达时,Dispatcher通过其类型标识快速查找映射表,获取对应的回调函数并执行,完成消息的处理闭环。

同样的,对于这个发布订阅请求 ,它里面其实也可以细分成很多请求操作,那么针对不同的请求操作,我们必须作出不同的响应。

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

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

我们先在这个Publish-Subscribe(发布订阅)模块内部实现这5个请求操作的具体实现过程,然后我们再写一个大一统的事件分发函数,自动分析请求的是哪个操作,我们再去调用对应的处理函数。最后我们再在Dispatcher模块里面去注册这个事件分发函数。

注意注册的这个过程,其实不是在我们这个模块里面实现的,而是在Dispatcher模块里面统一进行注册的。

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

即,

  • 任意⼀个客⼾端在发布或订阅之前先创建⼀个主题,⽐如在新闻发布中我们创建⼀个⾳乐新闻主题
  • 哪些客⼾端希望能够收到⾳乐新闻相关的消息,则就订阅这个主题,服务端会建⽴起该主题与客⼾端之间的联系。
  • 当某个客⼾端向服务端发布消息,且发布消息的⽬标主题是⾳乐新闻主题,则服务端会找出订阅了该主题的客⼾端,将消息推送给这些客⼾端。

消息协议定义

由于涉及网络通信,我们首先定义应用层的消息格式。所有发布订阅相关的请求与响应均采用JSON格式在协议Body字段中承载。至于具体过程,我在上面就已经说过了。

请求格式示例 (Topic-request):

javascript 复制代码
{
    "key": "music", // 主题名称
    "optype": "TOPIC_CREATE", // 主题操作类型:TOPIC_CREATE/TOPIC_REMOVE/TOPIC_SUBSCRIBE/TOPIC_CANCEL/TOPIC_PUBLISH
    "message": "Hello World" // 仅在 TOPIC_PUBLISH 请求中包含此字段
}

响应格式示例 (Topic-response):

javascript 复制代码
{
    "rcode": "OK"
}
{
    "rcode": "ERROR_INVALID_PARAMETERS"
}

模块核心设计要点

尽管基础思想直观,但一个健壮的发布订阅实现需要精心的设计,重点关注状态管理和分布式协作:

  1. 主题管理器

    • 该模块必须具备⼀个主题管理,且主题中需要保存订阅了该主题的客⼾端连接,主题收到⼀条消息,需要将这条消息推送给订阅了该主题的所有客⼾端

    • 一个主题可能被多个客户端订阅。

    • 内部需维护一个全局的主题-订阅者集合映射表。

    • 每个主题条目需要保存所有订阅了它的客户端连接标识(如连接句柄或ID)。

    • 核心功能是:当收到一条针对某主题的发布消息时,能立即定位到所有订阅者,并高效地将消息推送出去。

  2. 订阅关系管理器

    • 该模块必须具备⼀个订阅者管理,且每个订阅者描述中都必须保存⾃⼰所订阅的主题名称,⽬的是为了当⼀个订阅客⼾端断开连接时,能够找到订阅信息的关联关系,进⾏删除

    • 一个客户端可能订阅多个主题。

    • 需要维护一个客户端-已订阅主题集合的逆向映射。

    • 此举至关重要:当一个客户端连接异常断开时,系统能快速查找该客户端订阅的所有主题,并清理各主题下的订阅者列表,避免内存泄露和无效推送。

  3. 对外业务接口

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

    • 模块必须对外提供一组完整的业务处理函数,供Dispatcher调用。这些函数将具体实现:

      • HandleTopicCreate:主题创建

      • HandleTopicRemove:主题删除

      • HandleTopicSubscribe:主题订阅

      • HandleTopicCancel:取消订阅

      • HandleTopicPublish:消息发布

通过以上设计,Publish-Subscribe模块不仅实现了基本的消息广播,更通过双向关系的精细管理,确保了系统在动态变化(客户端上下线、主题热更新)时的一致性与资源清洁

这使得发布订阅功能成为构建实时通知、事件广播、配置下发等高级特性的可靠基础设施。

2.6.Registry-Discovery模块

其实这个模块和上面的RpcRouter模块,Publish-Subscribe(发布订阅)模块 是一样的,也是提供⼀个回调函数设置给 Dispatcher模块。

它的核心意义在于处理所有与服务治理相关的请求实现服务的动态注册、发现与状态同步 ,提供⼀个回调函数设置给 Dispatcher模块。

注意:注册的这个过程,其实不是在我们这个模块里面实现的,而是在Dispatcher模块里面统一进行注册的。

Publish-Subscribe(发布订阅)模块的核心职责是处理所有与发布订阅模式相关的网络请求,并向Dispatcher模块提供统一的处理回调函数,从而将主题式的消息分发功能无缝集成到系统中。

还记得Dispatcher类是怎么进行设计的吗?

  • Dispatcher内部维护一个 "消息类型-处理函数"映射表 (通常实现为std::unordered_map或类似哈希结构)。
  • 框架的使用者 可以提前向Dispatcher模块注册不同消息类型 对应的业务回调函数的注册。事实上,我们的Publish-Subscribe模块就会写好这个消息类型是服务治理相关的请求 的业务处理回调函数,后续我们就在这个Dispatcher模块里面对 这个写好的 服务治理相关的请求的业务处理回调函数统一进行注册的。
  • 当一条消息抵达时,Dispatcher通过其类型标识快速查找映射表,获取对应的回调函数并执行,完成消息的处理闭环。

当然与服务治理相关的请求,这个其实还是能细分成很多操作 ,那么针对不同的请求操作,我们必须作出不同的响应。

该模块主要处理以下四类核心操作,共同维护一个动态、可靠的服务目录:

  1. 服务注册:服务提供者(Provider)启动时,向注册中心宣告自己能够提供的服务列表。

  2. 服务发现:服务消费者(Caller)调用服务前,向注册中心查询能够提供目标服务的所有可用节点。

  3. 服务上线通知:当新的提供者注册某个服务后,主动通知曾发现过该服务的消费者,以便其更新本地服务列表。

  4. 服务下线通知:当某个提供者异常断开连接时,主动通知相关消费者,使其及时剔除失效节点,避免调用失败。

我们就先在这个Publish-Subscribe(发布订阅)模块里面实现这4个处理函数,并且我们再写一个事件分发函数,根据不同的事件我们就去调用不同的处理函数。

最后,我们在这个 Dispatcher类 里面统一对这个事件分发函数进行注册。

这个模块的功能就很清晰了。


为了支撑上述功能,需要定义清晰的服务治理通信协议。

请求与响应格式定义

服务注册/发现请求 (RD-request):

javascript 复制代码
{
    "optype": "SERVICE_REGISTRY", // 操作类型: SERVICE_REGISTRY, SERVICE_DISCOVERY, SERVICE_ONLINE, SERVICE_OFFLINE
    "method": "Add", // 服务方法名
    "host": { // 主机信息(注册、上线、下线时包含,服务发现时无此字段)
        "ip": "127.0.0.1",
        "port": 9090
    }
}

响应格式:

  • 注册/上线/下线通用响应:

    javascript 复制代码
    {
        "rcode": "OK"
    }
    
    {
        "rcode": "ERROR_INVALID_PARAMETERS"
    }
  • 服务发现响应:

    javascript 复制代码
    {
        "method": "Add",
        "host": [
            {"ip": "127.0.0.1", "port": 9090},
            {"ip": "127.0.0.2", "port": 8080}
        ]
    }

模块核心设计

一个健壮的服务注册与发现模块,其内部设计关键在于维护两组核心映射关系,并确保它们之间联动的正确性与高效性:

  1. 服务消费者(发现者)管理

    • 维护 "服务方法 -> 发现者集合" 的映射。当一个消费者查询某项服务时,记录该消费者,以便在未来该服务有新的提供者上线时,能主动推送通知。

    • ⽅法与发现者:当⼀个客户端进行服务发现的时候,我们可以对这个服务进行记录谁发现过该服务,当这个服务有一个新的提供者上线的时候,可以通知曾经发现过这个服务的发现者。

    • 同时,需维护 "消费者连接 -> 其发现的服务集合" 的逆向映射。当消费者断开连接时,能快速清理其在所有服务下的订阅记录,避免资源泄漏和无效通知。

    • 连接与发现者:当⼀个发现者断开连接了,删除这个发现者和它曾经发现过的所有的服务的关联关系,往后这些曾经发现过的所有的服务 有一个新的提供者上线的时候, 就不需要通知这个发现者了。

  2. 服务提供者管理

    • 维护 "提供者连接 -> 其提供的服务集合" 的映射。当提供者断开连接时,能立即知晓其下线的所有服务。

    • 连接与提供者:当⼀个提供者断开连接的时候,能够通知曾经发现过这个 提供者提供的所有服务 的 发现者, 该主机的该服务下线了

    • 更重要的是维护 "服务方法 -> 提供者集合" 的核心映射。这是服务发现的直接依据,并能用于当某个提供者下线时,精准定位到需要被通知的消费者列表。

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

  3. 对外业务接口

    • 模块必须向Dispatcher模块提供一个统一的业务处理回调函数,用于处理上述所有类型的服务治理请求,至于这个统一的业务处理回调函数,他是在这个Dispatcher模块里面进行统一注册的。

运作流程

基于上述设计,模块的运作形成一个完整的状态管理与通知闭环:

  1. 服务注册 :当服务提供者发起注册请求时,模块更新**"服务方法->提供者集合"**映射,并记录该提供者连接与其提供服务的关系。

  2. 服务发现 :当消费者查询某服务时,模块首先从**"服务方法->提供者集合"** 映射中返回当前可用的提供者列表,随后在**"服务方法->发现者集合"** 映射中记录该消费者的订阅关系,并在**"消费者连接->其发现的服务集合"**中建立反向记录。

  3. 提供者上线通知 :当新的提供者为某服务完成注册后,模块通过查询**"服务方法->发现者集合"**映射,获取所有曾订阅该服务的消费者,并主动推送新增的提供者节点信息。

  4. 提供者下线处理 :当提供者断开连接时,模块首先通过**"提供者连接->其提供的服务集合"** 映射获取其负责的所有服务方法。随后,针对每一个受影响的服务方法 ,模块通过**"服务方法->发现者集合"** 映射获取需要通知的消费者列表,推送该提供者下线信息。最后,清理该提供者在**"服务方法->提供者集合"**映射中的记录,并移除其连接相关的所有映射条目。

通过这样精细化的状态管理,Registry-Discovery模块确保了分布式环境下服务视图的最终一致性,为上层RPC调用提供了可靠、动态的服务路由基础,是微服务架构中不可或缺的核心组件。

对于这个提供者下线这里,我还是想多说几句:

具体处理流程

  1. 系统检测到"提供者A"连接断开

  2. 查询"连接 -> 服务集合"映射,得知提供者A负责 [Service1, Service2, Service3] 这三个服务

  3. 对于每个受影响的服务

    • 从"服务方法 -> 提供者集合"映射中,将提供者A从各服务的提供者集合中移除

    • 查询"服务方法 -> 发现者集合"映射,获取所有订阅了该服务的消费者

    • 向这些消费者发送通知:"Service1的提供者A已下线"

三.客户端模块划分

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

    1. Protocol:应⽤层通信协议模块
    1. Network:⽹络通信模块
    1. Dispatcher:消息分发处理模块
    1. Requestor:请求管理模块
    1. RpcCaller:远端调⽤功能模块
    1. Publish-Subscribe:发布订阅功能模块
    1. Registry-Discovery:服务注册/发现/上线/下线功能模块
    1. Client:基于以上模块整合⽽出的客⼾端模块

3.1.Network:⽹络通信模块

⽹络通信基于muduo库实现⽹络通信客⼾端

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

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

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

IO数据分发处理,逻辑与服务端⼀致

3.4.Requestor模块

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

Requestor模块在客户端网络通信架构中扮演着核心管理者的角色。

其根本意义在于建立一套严谨的请求与响应匹配机制,以应对高并发、异步网络环境下的核心挑战:确保每一条响应都能被安全、准确地递送给对应的请求发起方,从而保障业务逻辑的正确性。

以下是对其设计背景、核心原理及高级功能的详细阐述:

一、 应对的核心挑战

  1. 多线程并发下的时序错乱

    在一个多线程的客户端程序中,多个线程可能同时向服务器发起请求 。由于网络延迟、服务器处理速度差异、以及可能的负载均衡等因素,服务器返回响应的顺序与客户端发送请求的顺序极有可能不一致。 如果没有一个有效的匹配机制,**客户端线程在收到一个响应时,将无法判断这个响应是属于本线程的请求,还是属于其他线程的请求。**这种不确定性将直接导致数据混乱、状态错误,是系统稳定性的严重威胁。

  2. 异步I/O模型的非阻塞特性

    在现代高性能网络库(如Muduo、Netty)的异步I/O模型中,I/O操作是事件驱动和非阻塞的。

    • 发送:调用发送接口仅是将数据放入操作系统的发送缓冲区或库的内部队列,真正的网络传输由系统在后台异步完成。调用后函数立即返回,不代表数据已送达对端。

    • 接收 :没有传统的、主动调用的recvread函数来等待数据。相反,当套接字变得可读时,网络库会自动读取数据,并调用预先注册的全局消息到达回调函数。

    • 这种模型带来了高性能,但也彻底改变了编程范式。客户端在"发出请求"的动作和"收到响应"的事件之间是断开的,无法通过简单的同步等待来获取特定请求的结果。

二、 核心解决方案:请求ID与中央注册表

Requestor模块的核心思想是引入一个请求唯一标识符(Request ID) 和一块中央共享状态,以解耦请求的发送与响应的处理。

  1. 请求标识化:客户端在构造每一条待发送的请求时,为其分配一个全局唯一的ID(如递增序号、UUID等)。这个ID作为请求的一部分被发送至服务器。

  2. 响应关联 :协议约定,服务器在处理完请求后,必须在返回的响应数据包中原样携带该请求的ID。

  3. 建立中央注册表 :客户端内部维护一个并发安全的数据结构(通常是一个哈希表) ,作为请求-响应的中央注册表。该表以 请求ID为键。

  4. 请求注册与响应派发

    • 发送一条请求 时,模块会先在中央注册表中以该请求的ID为键,创建一个**"槽位"** 或"承诺"(Promise)。这个**"槽位"**用于暂存未来到达的响应数据。

    • 随后,请求被异步发送出去。

    • 当网络库的全局消息回调函数被触发时,它从数据包中解析出响应和其中包含的请求ID

    • 回调函数并不关心这个响应具体对应哪个业务逻辑,它只执行一个标准操作:根据 请求ID,找到中央注册表中对应的那个"槽位" ,并将响应数据存入其中。

  5. 提供可控的获取接口:模块对外提供getResponse(requestId)这样的接口。对于发起请求的线程或业务逻辑而言:

    • 它可以阻塞等待,直到指定ID的响应数据被填入槽位后返回。这模拟了同步调用,但底层仍是异步I/O。

    • 关键在于,无论网络响应多么混乱,因为有了唯一的ID和中央注册表的精准映射,每个请求者都能准确无误地取回自己的响应,彻底解决了时序错乱问题。

三、 高级扩展:异步编程范式的支持

在基础模型之上,Requestor模块可以进一步封装,提供更现代化、更灵活的异步处理能力:

  1. Future/Promise模式

    • 在创建请求槽位时,直接返回一个与这个槽位关联的std::future或类似的对象。

    • 业务逻辑可以在任何需要的时候,通过这个future来尝试获取(get)或等待(wait_for)结果。

    • 这使得"获取响应"这一操作本身也变成了一个可调度、可组合的异步任务,可以方便地集成到更复杂的异步链或并发流程中。

  2. 回调函数(Callback)机制

    • 在发送请求时,允许调用者注册一个专属的响应到达处理回调函数。

    • 响应到达并被存入中央注册表后,模块可以自动调度执行该回调函数,将响应结果作为参数传入。

    • 这实现了真正的"事件驱动"编程风格,业务逻辑在请求发出后即可继续执行,结果将通过回调异步通知,非常适合UI事件处理或高吞吐量的服务器场景。

3.5.RpcCaller 模块

RpcCaller模块存在的意义:向⽤⼾提供进⾏rpc调⽤的模块。

RpcCaller 模块作为 RPC(远程过程调用)框架的核心组件之一,其主要作用是向用户提供简洁、统一的远程服务调用能力。

该模块封装了底层网络通信、序列化及服务寻址等复杂细节,使得开发者在进行跨进程或跨网络的服务调用时,如同调用本地方法一样简单直观。

模块内部负责构建请求、向服务端发送数据,并接收和处理响应。

其中较为关键的是,为适应不同业务场景与性能要求,RpcCaller 需要支持多种调用模式:

  1. 同步调用:客户端发起调用后,当前线程将阻塞等待,直到收到服务端返回的响应结果后才继续执行。该方式逻辑直观,便于控制流程,适用于对实时性要求较高、且调用线程可以暂时等待的场景。

  2. 异步调用:客户端发起调用后立即返回,不阻塞当前线程,后续在需要获取结果时再通过 Future 或 Promise 等机制主动获取。这种方式能有效提升调用方的并发性能和资源利用率,特别适合高并发或不允许阻塞的业务逻辑。

  3. 回调调用:在发起调用的同时,传入一个结果处理回调函数(或监听器)。当收到服务端响应后,系统自动触发回调执行相应处理逻辑。该模式将结果处理与调用发起解耦,适用于事件驱动或响应式编程模型,有利于构建松耦合、易扩展的系统结构。

综上所述,RpcCaller 模块通过提供上述三种调用方式,兼顾了开发的便利性、系统的灵活性以及执行的高效性,从而满足不同业务场景下的远程服务调用需求。

3.6.Publish-Subscribe 模块(发布-订阅模块)

Publish-Subscribe 模块(发布-订阅模块)是系统中实现异步、解耦通信的核心组件。

其存在意义在于:为用户提供一套完整且易于使用的接口,用于发布消息、订阅关注的主题,并对接收到的消息进行相应的业务处理。

封装了下面5个核心功能接口

  • createTopic(创建主题)
  • removeTopic(删除主题)
  • subscribe(订阅主题)
  • cancel(取消订阅)
  • publish(向主题发布消息)

我们用户在使用上面这5个接口的时候,内部都是去发送一个请求,这个请求都会被放到这个Requetor模块里面进行等待响应。

回调函数的引入

我们仔细观察一下就会发现

  • createTopic(创建主题)
  • removeTopic(删除主题)
  • subscribe(订阅主题)
  • cancel(取消订阅)

都是客户端主动的请求。

但是这个publish(向主题发布消息)操作却有一点问题

有一个客户端A在某个主题里面发布了一条消息,发布了消息之后,我们的服务器会将这个消息推送给订阅了该主题的客户端B。

  • 对于向主题里面发消息的那个客户端A是主动的请求
  • 但是对于这个订阅了该主题的客户端B就是一个被动的请求

A是发布消息出去,但B是被动接受了一条服务器推送过来的消息。嗯?那么我们的客户端B怎么去处理这个服务器推送过来的消息???

解决方法:一个客户端订阅主题的时候,就必须设置一个主题消息的处理回调函数进去。

等到收到这个服务器推送过来的消息时,客户端自动调用这个处理回调函数来处理这个消息。

也就是说,我们将这个服务器推送过来的这个主题的消息视作一个请求,客户端需要对这个请求作出处理

Dispatcher模块的引入

还记得Dispatcher类是怎么进行设计的吗

  • Dispatcher内部维护一个 "消息类型-处理函数"映射表 (通常实现为std::unordered_map或类似哈希结构)。
  • 框架的使用者 可以提前向Dispatcher模块注册不同消息类型 对应的业务回调函数的注册。事实上,我们的Publish-Subscribe 模块(发布-订阅模块)就会写好消息类型是 发布/订阅操作请求 的业务处理回调函数,然后在Dispatcher模块里面统一进行注册的。
  • 当一条消息抵达时,Dispatcher通过其类型标识快速查找映射表,获取对应的回调函数并执行,完成消息的处理闭环。

Publish-Subscribe 模块(发布-订阅模块)就会写好消息类型是 发布/订阅操作请求的业务处理回调函数就是onPublish接口。

我们向外提供一个onPublish接口,他就是专门告诉Dispatcher模块收到 服务端推送过来的我们这个客户端订阅的主题的消息 时该如何做,它的内部处理逻辑:调用订阅主题时设置的回调函数进行处理。

此外,一个客户端可能会订阅多个主题,每次订阅一个主题的时候,都会设置一个回调函数,去处理收到的该主题的消息,因此模块中应该将不同主题的处理回调给管理起来。因此我们引入了<主题,回调函数>这个数据结构来存储,每个主题对应的回调函数。

它提取消息中的主题标识,在<主题,回调函数>映射表中查找对应的回调函数,并随即调用该函数,将消息内容作为参数传入,从而驱动用户业务的执行。

这样子,我们的客户端 收到了 服务端推送过来的我们这个客户端订阅的主题的消息 就调用onPublish接口进行处理

3.7.Registry-Discovery模块

作为服务操作的客户端:

也就是要实现的客户端既能用于provider进行服务注册,也能用于caller进行服务发现

  • 1.向外提供服务注册的功能接口:连接注册中心,发送服务注册请求,等待响应判断是否注册成功
  • 2.向外提供服务发现的功能接口:连接注册中心,发送服务发现请求,等待响应获取到提供服务的主机信息

对于服务发现来说,这是个一锤子买卖

进行服务发现的时候,注册中心只能将当前注册了该服务的主机地址进行返回如果后续又有其他的主机注册了该服务,之前进行了服务发现的主机是得不到这个主机信息的

因此还得有服务上线/下线通知。


服务注册与发现模块的功能设计较为复杂,需要由两个不同角色协同完成:

  1. 服务注册者(Provider)

    作为 RPC 服务的提供方,**需向注册中心注册自身提供的服务。**因此需要实现服务注册功能,通常包括注册服务名、服务版本、服务实例地址、元数据等信息,并支持服务心跳上报、健康状态同步等机制,以确保服务信息的实时有效。

  2. 服务发现者(Consumer)

    作为 RPC 服务的调用方,**需通过服务发现机制获取可用的服务实例地址。**具体包括向注册中心查询指定服务的实例列表,并将获取的地址信息进行本地缓存与管理。同时,作为发现者,还需监听注册中心推送的服务变更事件(如服务上线/下线、实例状态变化等),及时更新本地服务路由信息,确保调用流量的正确转发与故障隔离。

通过以上两个角色的协作,该模块能够实现服务信息的统一管理、动态更新与高效发现,从而支撑分布式系统中服务的稳定运行与弹性伸缩。

相关推荐
工业甲酰苯胺2 小时前
C#中的多级缓存架构设计与实现深度解析
缓存·c#·wpf
cjp56020 小时前
020.WPF MVVM数据绑定底层原理类封装
wpf
黑夜中的潜行者21 小时前
构建高性能 WPF 大图浏览器:TiledViewer 技术解密
性能优化·c#·.net·wpf·图形渲染
yy7634966681 天前
WPF样式入门:5分钟学会自定义Button样式
wpf
她说彩礼65万1 天前
WPF路由事件作用
wpf
LcVong1 天前
WPF DataGrid 全属性详解(分类整理+实用说明)
wpf
Greyscarf1 天前
WPF使用MxDraw云图插件入门
wpf·mxdraw云图·mxdraw
执笔论英雄2 天前
【大模型推理】VLLM 引擎使用
wpf·vllm
LateFrames2 天前
动画性能比对:WPF / WinUI3 / WebView2
wpf·webview·用户体验·winui3