目录
一、Network
二、Protocol
三、Dispatcher
四、RpcRouter
五、Publish-Subscribe
六、Registry-Discovery
七、Server
服务端的功能需求:
基于网络通信接收客户端的请求,提供rpc服务
基于网络通信接收客户端的请求,提供服务注册与发现,上线&下线通知
基于网络通信接收客户端的请求,提供主题操作(创建/删除/订阅/取消),消息发布
在服务端的模块划分中,基于以上理解的功能,可以划分出这么几个模块:
Network:网络通信模块
Protocol:应用层通信协议模块
Dispatcher:消息分发处理模块
RpcRouter:远端调⽤路由功能模块
Publish-Subscribe:发布订阅功能模块
Registry-Discovery:服务注册/发现/上线/下线功能模块
Server:基于以上模块整合而出的服务端模块
一、Network
该模块为网络通信模块,实现底层的网络通信功能,这个模块本质上也是⼀个比较复杂的模块,因此鉴于项⽬的庞大,该模块我们将使⽤陈硕大佬的Muduo库来进行搭建。
二、Protocol
应用层通信协议模块的存在意义:解析数据,解决通信中有可能存在的粘包问题,能够获取到⼀条完整的消息。
在muduo库基本使用中,我们能够知道想要让⼀个服务端/客⼾端对消息处理,就要设置⼀个onMessage的回调函数,在这个函数中对收到的数据进⾏应⽤层协议处理。
而Protocol模块就是对网络通信协议模块的设计,也就是在网络通信中,我们必须设计⼀个应用层的网络通信协议出来,以解决网络通信中可能存在的粘包问题,而解决粘包有三种方式:特殊字符间隔,定长,LV格式。
LV格式在处理不定长
数据时,提供了更好的灵活性和效率,减少了解析的复杂性,在项目中,将使用LV格式来定义应用层的通信协议格式
Length:该字段固定4字节长度,⽤于表示后续的本条消息数据⻓度。
MyType:该字段为Value中的固定字段,固定4字节长度,用于表示该条消息的类型:
Rpc调用请求/响应类型消息
发布/订阅/取消订阅/消息推送类型消息
主题创建/删除类型消息
服务注册/发现/上线/下线类型消息
IDLength:为消息中的固定字段,该字段固定4字节长度,⽤于描述后续ID字段的实际长度。
MID:在每条消息中都会有⼀个固定字段为ID字段,用于唯⼀标识消息,ID字段长度不固定。
Body:消息主题正文数据字段,为请求或响应的实际内容字段。
三、Dispatcher
Dispathcher存在的意义:区分不同的消息类型,并根据消息类型调用不同的业务处理函数进行消息处理。
当muduo库底层通信收到数据后,在onMessage回调函数中对数据进⾏应用层协议解析,得到⼀条实际消息载荷后,我们就该决定这条消息代表这客户端的什么请求,以及应该如何处理。
因为Dispatcher需要根据不同的消息类型进行对应的函数处理,所以它应该存在一个哈希表hash_map<消息类型, 回调函数>,由使用者来决定哪条消息用哪个业务函数进行处理,当收到消息后,在该模块找到其对应的处理回调函数进行调用即可。
下面的Router、PS、RD模块都要向Dispatcher提供它们各自的回调函数。
四、RpcRouter
RpcRouter存在的意义:提供rpc请求的处理回调函数(在Dispatcher中注册自己模块的回调函数),内部所要实现的功能,分辨出客户端请求的服务进行处理得到结果进行响应。
rpc请求中,最关键的两个点:
请求方法名称
请求对应要处理的参数信息
在Rpc远端调用中,首先将客户端到服务端的通信链路打通,然后将自己所需要调用的服务名称,以及参数信息传递给服务端,由服务端进行接收处理,并返回结果。
而不管是客户端要传递给服务端的服务名称以及参数信息,或者服务端返回的结果,都是在上边Protocol
中定义的Body
字段中,因此Body
字段中就存在了另⼀层的正文序列化/反序列化过程。
序列化方式有很多种,鉴于当前我们是json-rpc,因此这个序列化过程我们就初步使⽤json序列化来进行,所定义格式如下:
//RPC-request
{
"method" : "func", // 请求调用函数
"parameters" : { // 调用函数传入参数
"传参" : x
}
}
//RPC-response
// 处理成功
{
"rcode" : OK,
"result": y // 函数返回值
}
// 处理失败
{
"rcode" : ERROR_INVALID_METHOD
}
除此之外,因为RpcRouter模块要body中传入的Json::Value对象进行解析并拿取其中的方法名称与传入参数,那么为了保证服务端有对应的方法存在,这里还要额外设置一下服务描述,假如需要函数 int func(int num1, double num2) :
服务名称:func
参数名称:num1,int
参数名称:num2,double
返回值类型:int
有了这个描述,在回调函数中就可以先对传⼊的参数进行校验,没问题了则取出指定字段数据进行处理并返回结果
基于以上理解,在实现该模块时,该有以下设计:
该模块必须具备⼀个Rpc路由管理,其中包含对于每个服务的参数校验功能
该模块必须具备⼀个方法名称和方法业务回调的映射
该模块必须向外提供Rpc请求的业务处理函数(提供给dispatcher模块)。
-
hash_map:RpcRouter内部的数据结构,可以进行相关校验,如果校验通过则可以调用对应的回调函数。
注意:RpcRouter的hash_map中的回调函数和Dispatcher中的回调函数不是一回事
Dispatcher是从网络服务器收到数据后的调度,有可能是发布订阅请求,有可能是rpc请求,根据不同的请求进行不同的分发处理
RpcRouter中的调度是针对收到的rpc请求的进一步调度,看要调用哪个rpc注册的回调函数进行rpc调用的处理
五、Publish-Subscribe
Publish-Subscribe存在的意义:针对发布订阅请求进⾏处理,提供⼀个回调函数设置给Dispatcher模块。
发布订阅所包含的请求操作:
-
主题的创建
-
主题消息的发布
-
主题的删除
-
主题的订阅/取消订阅
-
任意⼀个客户端在发布或订阅之前先创建⼀个主题,比如在新闻发布中我们创建⼀个音乐新闻主题,哪些客户端希望能够收到音乐新闻相关的消息,那么就订阅这个主题,服务端会建立起该主题与客户端之间的联系。
-
当某个客户端向服务端发布消息,且发布消息的目标主题是⾳乐新闻主题,则服务端会找出订阅了该主题的客户端,将消息推送给这些客户端。
从第2点可以看出来,推送消息涉及到客户端与服务端的交互,那么又是网络层的消息传输,所以又涉及到了序列化与反序列化,以下先定义出来格式:
//Topic-request
{
//主题名称
"key" : "music",
// 主题操作类型
"optype" : TOPIC_CRAETE/TOPIC_REMOVE/TOPIC_SUBSCRIBE/TOPIC_CANCEL/TOPIC_PUBLISH,
//TOPIC_PUBLISH请求才会包含有message字段
"message" : "Hello World"
}
//Topic-response
//successed
{
"rcode" : OK,
}
//failed
{
"rcode" : ERROR_INVALID_PARAMETERS,
}
功能思想并不复杂,因此我们需要把更多的精⼒放到其实现设计上:
-
主题管理——主题中需要保存订阅了该主题的客户端连接,
-
主题收到⼀条消息,需要将这条消息推送给订阅了该主题的所有客户端
-
数据结构上使用 hash_map<主题名称, topic>,其中
topic
中包含topic_name
与set<connection>
-
-
订阅者管理——每个订阅者都需保存自己所订阅的主题名称
-
目的是为了当⼀个订阅客户端断开连接时,能够找到订阅信息的关联关系,进行删除
-
数据结构上使用
hash_map<connection, subscriber>
,其中subscriber
中包含connection
与vector<topc_name>
-
-
如何理解上下两个hash_map?
-
主题删除,先删除主题信息,然后删除该主题的订阅者,去这些订阅者中删除对应的主题名称信息
-
订阅者退出,删除订阅者信息,然后删除这个订阅者订阅的主题信息中包含的对应订阅者信息
-
-
该模块需要向外提供主题创建/销毁,主题订阅/取消订阅,消息发布处理的业务处理函数
六、Registry-Discovery
Registry-Discovery存在的意义:针对服务注册与发现请求的处理。
服务注册/发现类型请求中的详细划分
-
服务注册:服务提供者告诉中转中心,自己能提供的服务
-
服务发现:服务调用者询问中转中心,谁能提供指定服务
-
服务上线:在⼀个提供者上线了指定服务后,通知发现过该服务的客户端有个provider可以提供该服务
-
服务下线:在⼀个提供者断开连接,通知发现过该服务的caller,谁下线了哪个服务
服务注册模块,该模块主要是为了实现分布式架构而存在,让每⼀个rpc客户端能够从不同的节点主机上获取自己所需的服务,让业务更具扩展性,系统更具健壮性。而为了能够让服务调用者知道有哪些服务提供者能提供自己所需服务,那么就需要有⼀个注册中心让这些服务提供者去注册登记自己的服务,让服务调用者来发现这些服务。
因此,在我们的服务端功能中,还需实现服务的注册/发现,以及服务的上线/下线功能。这也涉及到了网络层的消息传输,所以需要用到Json,下面来看一下定义的格式:
//Registry-Discovery request
{
"optype" :
//provider进行服务注册/caller进行服务发现/在provider上下线后对caller进行服务上下线通知
SERVICE_REGISTRY/SERVICE_DISCOVERY/SERVICE_ONLINE/SERVICE_OFFLINE,
"method" : "func",
//服务注册/上线/下线有host字段,发现则无host字段
"host" : {
"ip" : "127.0.0.1",
"port" : 9090
}
}
//Registry/Online/Offline-response
//成功
{
"rcode" : OK,
}
//失败
{
"rcode" : ERROR_INVALID_PARAMETERS,
}
//Discovery-response
{
"method" : "func",
"host" : [
{"ip" : "127.0.0.1","port" : 9090},
{"ip" : "127.0.0.2","port" : 8080}
]
}
该模块的设计如下:
-
必须具备⼀个服务发现者的管理:
-
方法与发现者:当⼀个客户端端进行服务发现的时候,记录谁发现过该服务,当有⼀个新的提供者上线的时候,可以通知该发现者
-
连接与发现者:当⼀个发现者断开连接了,删除关联关系,往后就不需要通知了
-
-
必须具备⼀个服务提供者的管理:
-
连接与提供者:当⼀个提供者断开连接的时候,能够通知该提供者提供的服务对应的发现者,该主机的该服务下线了
-
方法与提供者:能够知道谁的哪些方法下线了,然后通知发现过该方法的客户端
-
-
必须向Dispatcher模块提供⼀个服务注册/发现的业务处理回调函数
当⼀个rpc-provider登记了服务,则将其管理起来,当rpc-caller进行服务发现时,则将保存的对应服务所对应的主机信息,响应给rpc-caller。当中途⼀个rpc-provider上线登记服务时,则可以给进行了对应服务发现的rpc-caller进行服务上线通知,通知rpc-caller当前多了⼀个对应服务的rpc-provider。同时,当⼀个rpc-provider下线时,则可以找到进行了该服务发现的rpc-caller进行服务的下线通知。
七、Server
当以上的所有功能模块都完成后,我们就可以将所有功能整合到⼀起来实现服务端程序了。
-
RpcServer:rpc功能模块与网络通信部分结合。
-
RegistryServer:服务发现注册功能模块与网络通信部分结合
-
TopicServer:发布订阅功能模块与网络通信部分结合。
RpcServer,只需要对内提供好rpc服务的路由关系管理,以及rpc请求消息的分发处理函数
针对接收到的请求根据路由关系,调用对应的回调函数进行参数检查,完成处理即可
在基于服务注册功能的RpcServer中需要包含有一个服务注册客户端实现向注册中心进行服务注册的功能
RegistryServer,需要对内做好服务提供者与服务发现者的数据管理,且提供服务操作请求的分发处理函数。这样,才能在收到服务注册请求后,将信息管理起来,收到服务发现请求后,向其返回可用的服务提供者信息。
发布订阅服务器,对内做好主题信息与订阅者信息的管理,以便于能够在收到主题操作请求后完成其功能