【#3】项目设计
一、 理解项目功能
⚗️本质上来讲,我们要实现的 rpc(远端调用) 思想上并不复杂,甚至可以说是简单,其实就是客户端想要完成某个任务的处理,但是这个处理的过程并不自己来完成,而是,将请求发送到服务器上,让服务器来帮其完成处理过程,并返回结果,客户端拿到结果后返回。
然而上图的模型中,是一种多对一或一对一的关系,一旦服务端掉线,则客户端无法进行远端调用,且其服务端的负载也会较高,因此在rpc 实现中,我们不仅要实现其基本功能,还要再进一步,实现 分布式架构 的 rpc
- 分布式架构:简单理解就是由多个节点组成的一个系统,这些节点通常指的是服务器,将不同的业务或者同一个业务拆分分布在不同的节点上,通过 协同 工作解决高并发的问题,提高系统扩展性和可用性。
实现思想也并不复杂,也就是在原来的模型基础上,增加一个注册中心,基于注册中心不同的服务提供服务器向注册中心进行服务注册,相当于告诉注册中心自己能够提供什么服务,而客户端在进行远端调用前,先通过注册中心进行服务发现,找到能够提供服务的服务器,然后发起调用。
而其次的发布订阅功能,则是依托于多个客户端围绕服务端进行消息的转发。
- 不过单纯的消息转发功能,并不能满足于大部分场景的需要,因此会在其基础上实现基于主题订阅的转发。
基于以上功能的合并,我们可以得到⼀个实现所有功能的结构图
在上图的结构中,我们甚至可以让每一个 Server 作为备用注册中心形成分布式架构,一旦一个注册中心下线,可以向备用中心进行注册以及请求,且在此基础上客户端在请求Rpc服务的时候,因为可以有多个 rpc-provider
可选,因此可以实现简单的负载均衡策略,且基于注册中心可以更简便实现发布订阅的功能。
项目的三个主要功能:
- rpc调用
- 服务的注册与发现以及服务的下线/上线通知
- 消息的发布订阅
二、服务端模块划分
服务端的功能需求:
- 基于网络通信接收客户端的请求,提供rpc服务
- 基于网络通信接收客户端的请求,提供服务注册与发现,上线&下线通知
- 基于网络通信接收客户端的请求,提供主题操作(创建/删除/订阅/取消),消息发布
在服务端的模块划分中,基于以上理解的功能,可以划分出这么几个模块
Network
:网络通信模块Protocol
:应用层通信协议模块Dispatcher
:消息分发处理模块RpcRouter
:远端调用路由功能模块Publish-Subscribe
:发布订阅功能模块Registry-Discovery
:服务注册/发现/上线/下线功能模块Server
:基于以上模块整合而出的服务端模块
1. Network
♐️ 该模块为网络通信模块,实现底层的网络通信功能,这个模块本质上也是一个比较复杂庞大的模块,因此鉴于项目的庞大,该模块我们将使用陈硕大佬的 Muduo
库来进行搭建。
2. Protocol
应用层通信协议模块的 存在意义:解析数据,解决通信中有可能存在的粘包问题,能够获取到一条完整的消息
- 在前边的
muduo
库基本使用中,我们能够知道想要让一个服务端/客户端对消息处理,就要设置一个onMessage
的回调函数,在这个函数中对收到的数据进行应用层协议处理。
而Protocol模块就是是网络通信协议模块的设计,也就是在网络通信中,我们必须设计一个应用层的网络通信协议出来,以解决网络通信中可能存在的粘包问题,而 解决粘包 有三种方式:特殊字符间隔,定长,LV格式。
① 特殊字符间隔
原理
- 在每条消息之间插入一个特殊的分隔符(例如换行符
\n
或其他不可见字符),用于标记消息的结束。 - 接收方通过解析分隔符来区分不同的消息。
优点
- 实现简单,适合文本协议(如 HTTP、SMTP 等)。
- 不需要额外的长度字段或复杂逻辑。
缺点
- 分隔符不能出现在消息内容中,否则会导致解析错误(需要转义机制)。
- 对二进制数据不友好,因为二进制数据可能包含与分隔符相同的字节。
② 定长数据
原理
- 每条消息固定为特定长度(例如 100 字节)。如果消息不足指定长度,则用填充字符(如空格或零字节)补齐。
- 接收方每次读取固定长度的数据即可。
优点
- 实现简单,接收方无需复杂的解析逻辑。
- 适合固定大小的消息场景。
缺点
- 浪费带宽:短消息需要填充字符。
- 消息长度受限于固定值,灵活性较差。
- 需要事先约定消息的最大长度。
③ LV(Length-Value)格式
原理
- 在每条消息前附加一个固定长度的字段(通常是 2 字节或 4 字节),表示消息体的长度。
- 接收方先读取长度字段,然后根据长度读取消息体。
优点
- 高效:没有多余的填充字符,节省带宽。
- 灵活:支持任意长度的消息。
- 适合二进制协议和复杂场景。
缺点
- 实现稍复杂:需要分别处理长度字段和消息体。
- 长度字段本身需要固定长度,可能会限制消息的最大长度。
三者区别
- 特殊字符间隔 :适合简单的文本协议,但不适合二进制数据。
- 定长数据 :实现简单,但浪费带宽且灵活性差。
- LV 格式 :高效且灵活,是解决粘包问题的最佳实践,尤其适合复杂场景。
在实际开发中,LV 格式 是最常用的方式,因为它既能高效传输数据,又能灵活适应不同长度的消息。
比如:当前的这个项目中将使用 LV格式 来定义应用层的通信协议格式
- Length:该字段固定4字节长度,用于表示后续的本条消息数据长度。
- MType:该字段为Value中的固定字段,固定4字节长度,用于表示该条消息的类型。
- Rpc:调用请求/响应类型消息。
- 发布/订阅/取消订阅/消息推送类型消息0
- 主题创建/删除类型消息。
- 服务注册/发现/上线/下线类型消息。
- IDLength:为消息中的固定字段,该字段固定4字节长度,用于描述后续ID字段的实际长度。
- MID:在每条消息中都会有一个固定字段为ID字段,用于唯一标识消息,ID字段长度不固定。
- Body:消息主题正文数据字段,为请求或响应的实际内容字段。
3. Dispatcher
模块存在的意义:区分消息类型,根据不同的类型,调用不同的业务处理函数进行消息处理。
- 当
muduo
库底层通信收到数据后,在onMessage
回调函数中对数据进行应用层协议解析,得到一条实际消息载荷后,我们就该决定这条消息代表这客户端的什么请求,以及应该如何处理。
因此,我们设计出了Dispatcher模块,作为一个分发模块,这个模块内部会保存有一个hash_map<消息类型,回调函数>,以此由使用者来决定哪条消息用哪个业务函数进行处理,当收到消息后,在该模块找到其对应的处理回调函数进行调用即可。
消息类型:
- rpc请求&响应
- 服务注册/发现/上线/下线请求&响应
- 主题创建/删除/订阅/取消订阅请求&响应,消息发布的请求&响应
发布评论