【#3】项目设计

一、 理解项目功能

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

然而上图的模型中,是一种多对一或一对一的关系,一旦服务端掉线,则客户端无法进行远端调用,且其服务端的负载也会较高,因此在rpc 实现中,我们不仅要实现其基本功能,还要再进一步,实现 分布式架构 的 rpc

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

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

image-20250316201605766

而其次的发布订阅功能,则是依托于多个客户端围绕服务端进行消息的转发。

  • 不过单纯的消息转发功能,并不能满足于大部分场景的需要,因此会在其基础上实现基于主题订阅的转发。
image-20250316201736306

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

image-20250316201811362

在上图的结构中,我们甚至可以让每一个 Server 作为备用注册中心形成分布式架构,一旦一个注册中心下线,可以向备用中心进行注册以及请求,且在此基础上客户端在请求Rpc服务的时候,因为可以有多个 rpc-provider 可选,因此可以实现简单的负载均衡策略,且基于注册中心可以更简便实现发布订阅的功能。

项目的三个主要功能:

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

二、服务端模块划分

服务端的功能需求:

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

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

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

♐️ 该模块为网络通信模块,实现底层的网络通信功能,这个模块本质上也是一个比较复杂庞大的模块,因此鉴于项目的庞大,该模块我们将使用陈硕大佬的 Muduo 库来进行搭建。

2. Protocol

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

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

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


① 特殊字符间隔

原理

  • 在每条消息之间插入一个特殊的分隔符(例如换行符 \n 或其他不可见字符),用于标记消息的结束。
  • 接收方通过解析分隔符来区分不同的消息。

优点

  • 实现简单,适合文本协议(如 HTTP、SMTP 等)。
  • 不需要额外的长度字段或复杂逻辑。

缺点

  • 分隔符不能出现在消息内容中,否则会导致解析错误(需要转义机制)。
  • 对二进制数据不友好,因为二进制数据可能包含与分隔符相同的字节。

② 定长数据

原理

  • 每条消息固定为特定长度(例如 100 字节)。如果消息不足指定长度,则用填充字符(如空格或零字节)补齐。
  • 接收方每次读取固定长度的数据即可。

优点

  • 实现简单,接收方无需复杂的解析逻辑。
  • 适合固定大小的消息场景。

缺点

  • 浪费带宽:短消息需要填充字符。
  • 消息长度受限于固定值,灵活性较差。
  • 需要事先约定消息的最大长度。

③ LV(Length-Value)格式

原理

  • 在每条消息前附加一个固定长度的字段(通常是 2 字节或 4 字节),表示消息体的长度。
  • 接收方先读取长度字段,然后根据长度读取消息体。

优点

  • 高效:没有多余的填充字符,节省带宽。
  • 灵活:支持任意长度的消息。
  • 适合二进制协议和复杂场景。

缺点

  • 实现稍复杂:需要分别处理长度字段和消息体。
  • 长度字段本身需要固定长度,可能会限制消息的最大长度。

三者区别

  • 特殊字符间隔 :适合简单的文本协议,但不适合二进制数据。
  • 定长数据 :实现简单,但浪费带宽且灵活性差。
  • LV 格式 :高效且灵活,是解决粘包问题的最佳实践,尤其适合复杂场景。

在实际开发中,LV 格式 是最常用的方式,因为它既能高效传输数据,又能灵活适应不同长度的消息。


比如:当前的这个项目中将使用 LV格式 来定义应用层的通信协议格式

image-20250316220044392
  • Length:该字段固定4字节长度,用于表示后续的本条消息数据长度。
  • MType:该字段为Value中的固定字段,固定4字节长度,用于表示该条消息的类型。
  • Rpc:调用请求/响应类型消息。
    • 发布/订阅/取消订阅/消息推送类型消息0
    • 主题创建/删除类型消息。
    • 服务注册/发现/上线/下线类型消息。
  • IDLength:为消息中的固定字段,该字段固定4字节长度,用于描述后续ID字段的实际长度。
  • MID:在每条消息中都会有一个固定字段为ID字段,用于唯一标识消息,ID字段长度不固定。
  • Body:消息主题正文数据字段,为请求或响应的实际内容字段。
3. Dispatcher

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

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

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

image-20250316220938086

消息类型:

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