一、前言
在第五部分我们讲解了项目的功能,接下来从服务端的视角,去设计相应模块,以此助力服务端达成项目的三项功能:
● rpc调用
● 服务的注册、发现以及服务的上线、下线通知
● 消息的发布与订阅
二、正文
1. 服务端的功能需求
在对服务端进行具体的模块划分之前,先明确服务端的功能需求
● 通过网络通信接收客户端的请求,提供rpc服务
● 通过网络通信接收客户端的请求,提供服务的注册、发现以及上线、下线通知
● 通过网络通信接收客户端的请求,提供主题的操作(创建、删除、订阅、取消)以及消息发布
2. 服务端的模块划分
基于上述功能,可将服务端划分出以下几个模块
● Network :网络通信模块
●Protocol : 应用层通信协议模块
● Dispatcher :消息分发处理模块
● RpcRouter: 远端调用路由功能模块
● Publish-Subcriber: 发布订阅模块
● Registry-Discovery: 服务注册、发现、上线、下线功能模块
● Server: 由以上模块整合而成的服务端模块
3. Network模块
此模块为网络通信模块,实现底层的网络通信功能,该模块本质上较为复杂庞大,由于项目重点在于Rpc,所以此模块借助陈硕大佬的Muduo库来搭建
4. Protocol
有了Network模块后,双方能够进行通信,但由于采用TCP协议,数据传输时可能出现粘包问题,因此需要应用层通信协议模块:对数据进行解析,解决通信中可能出现的粘包问题,从而获取完整的消息
在之前对muduo库的基本使用中可知,要让服务端或客户端处理消息,需设置onMessage回调函数,在该函数中对收到的数据进行应用层协议处理
而Protocol模块就是网络通信协议模块的设计,即在网络通信中,必须设计应用层网络通信协议来解决粘包问题,解决粘包问题有三种方式:特殊字符间隔、定长、LV格式
本项目采用LV格式来定义应用层通信协议格式。不采用特殊字符间隔是因为消息正文中可能含有特殊字符,需要转义处理,较为麻烦。不采用定长格式是因为传输消息长度不确定,过长会浪费空间,过短无法满足通信需求。而采用LV格式既能区分两条消息,又能适配不同长度的消息,所以采用LV格式定义应用层通信协议

Length:该字段固定4字节长度,用于表示后续本条消息数据长度
MType:该字段为Value中的固定字段,固定4字节长度,用于表示该条消息的类型
● Rpc调用请求/响应类型消息
● 发布/订阅/取消订阅/消息推送类型消息
● 主题创建/删除类型消息
● 服务注册/发现/上线/下线类型消息
IDLength:为消息中的固定字段,固定4字节长度,用于描述后续ID字段的实际长度
MID:每条消息中都有一个固定字段ID字段,用于唯一标识消息,ID字段长度不固定
Body:消息主题正文数据字段,是请求或响应的实际内容字段
5. Dispatcher
Dispatcher存在的意义:区分消息类型,依据不同类型,调用不同业务处理函数来处理消息,更好遵循开闭原则,若有新业务处理函数,无需修改原有代码
当muduo库底层通信收到数据后,在onMessage回调函数中对数据进行应用层协议解析,得到实际消息载荷后,需确定该消息代表客户端的何种请求及如何处理
因此设计出Dispatcher模块作为分发模块,该模块内部会保存一个hash_map<消息,回调函数>,由使用者决定哪条消息用哪个业务函数处理,收到消息后,在该模块找到对应处理回调函数并调用C++零基础构建Json-Rpc框架:第六部分服务端模块解构
消息类型:
● rpc请求&响应
● 服务注册/发现/上线/下线请求&响应
● 主题创建/删除/订阅/取消订阅&响应,消息发布的请求&响应
有了Dispatcher模块,就能将不同消息类型分发给不同模块:RpcRouter,Publish-Subcriber,Registry-Discovery
6. RpcRouter模块
RpcRouter存在的意义:提供rpc请求的处理回调函数,内部需实现的功能是分辨客户端请求的服务并处理以得到结果进行响应
rpc请求中,关键的两点是:
● 请求方法名称
● 请求对应要处理的参数信息
在Rpc远端调用中,先打通客户端到服务端的通信链路,再将所需调用的服务名称及参数信息传递给服务端,由服务端接收处理并返回结果。而无论是客户端传递给服务端的服务名称和参数信息,还是服务端返回的结果,都在上边Protocol定义的Body字段中,所以Body字段中存在另一层正文序列化/反序列化过程
序列化方式有多种,鉴于是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,是整形,
● 返回值类型:整形
有了此描述,回调函数中可先校验传入参数,没问题后取出指定字段数据处理并返回结果。基于此理解,实现该模块时应有以下设计:
该模块需具备Rpc路由管理,包含每个服务的参数校验功能
该模块需具备方法名称和方法业务回调的映射
该模块需向外提供Rpc请求的业务处理函数。

7. 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,
}
功能思想不复杂,需将精力放在实现设计上:
- 该模块需具备主题管理,且主题中需保存订阅该主题的客户端连接
a. 主题收到消息,需将消息推送给订阅该主题的所有客户端- 该模块需具备订阅者管理,且每个订阅者描述中需保存自己订阅的主题名称
a. 目的是订阅客户端断开连接时,能找到订阅信息关联关系并删除- 该模块需向外提供主题创建/销毁,主题订阅/取消订阅,消息发布处理的业务处理函数
8. 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}
]
}
该模块设计如下:
- 需具备服务发现者的管理:
a. 方法与发现者:客户端进行服务发现时记录谁发现过该服务,新提供者上线时可通知该发现者
b. 连接与发现者:发现者断开连接时,删除关联关系,后续不再通知- 需具备服务提供者的管理:
a. 连接与提供者:提供者断开连接时,通知该提供者提供的服务对应的发现者该主机的该服务下线
b. 方法与提供者:知晓谁的哪些方法下线,然后通知发现过该方法的客户端- 需向Dispatcher模块提供服务注册/发现的业务处理回调函数
如此,rpc-provider登记服务后被管理,rpc-caller进行服务发现时,将保存的对应服务的主机信息响应给rpc-caller。rpc-provider上线登记服务时,给进行对应服务发现的rpc-caller发送服务上线通知,告知多了一个对应服务的rpc-provider。rpc-provider下线时,找到进行该服务发现的rpc-caller发送服务下线通知

9. Server
当以上所有功能模块完成后,可将各功能整合实现服务端程序
• RpcServer:rpc功能模块与网络通信部分结合。
• RegistryServer:服务发现注册功能模块与网络通信部分结合
• TopicServer:发布订阅功能模块与网络通信部分结合。
RpcServerC++零基础构建Json-Rpc框架:第六部分服务端模块解构
RegistryServerC++零基础构建Json-Rpc框架:第六部分服务端模块解构
TopicServerC++零基础构建Json-Rpc框架:第六部分服务端模块解构
三、结语
至此,本文关于从零构建Json-RPC框架第六部分的内容就结束了,若有不足之处,欢迎小伙伴们指出!
大家的「关注、点赞、收藏」是我创作的最大动力!感谢大家的支持,下期再见!