一、前言

        在第五弹中我们对项目的功能进行了讲解,接下来我们就服务端的角度,来设计对应模块,帮助服务端实现项目的三个功能:

●  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<消息,回调函数>,以此由使用者来决定哪条消息用哪个业务函数来进行处理,当收到消息后,在该模块找到对应的处理回调函数进行调用即可

消息类型:

 ● 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,是⼀个整形,

● 返回值类型:整形

有了这个描述,在回调函数中就可以先对传⼊的参数进⾏校验,没问题了则取出指定字段数据进⾏处 理并返回结果 基于以上理解,在实现该模块时,该有以下设计:

1. 该模块必须具备⼀个Rpc路由管理,其中包含对于每个服务的参数校验功能

2. 该模块必须具备⼀个⽅法名称和⽅法业务回调的映射

3. 该模块必须向外提供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,
}

功能思想并不复杂,因此我们需要把更多的精⼒放到其实现设计上:

1. 该模块必须具备⼀个主题管理,且主题中需要保存订阅了该主题的客⼾端连接

         a. 主题收到⼀条消息,需要将这条消息推送给订阅了该主题的所有客⼾端

2. 该模块必须具备⼀个订阅者管理,且每个订阅者描述中都必须保存⾃⼰所订阅的主题名称         a. ⽬的是为了当⼀个订阅客⼾端断开连接时,能够找到订阅信息的关联关系,进⾏删除 3. 该模块必须向外提供主题创建/销毁,主题订阅/取消订阅,消息发布处理的业务处理函数

 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}
 ]
}

 该模块的设计如下:

1. 必须具备⼀个服务发现者的管理

         a. ⽅法与发现者:当⼀个客⼾端进⾏服务发现的时候,进⾏记录谁发现过该服务,当有⼀个新的提供者上线的时候,可以通知该发现者

         b. 连接与发现者:当⼀个发现者断开连接了,删除关联关系,往后就不需要通知了

2. 必须具备⼀个服务提供者的管理

         a. 连接与提供者:当⼀个提供者断开连接的时候,能够通知该提供者提供的服务对应的发现者, 该主机的该服务下线了

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

3. 必须向Dispatcher模块提供⼀个服务注册/发现的业务处理回调函数

这样,当⼀个rpc-provider登记了服务,则将其管理起来,当rpc-caller进⾏服务发现时,则将保存的对应服务所对应的主机信息,响应给rpc-caller。⽽,当中途⼀个rpc-provider上线登记服务时,则可以给进⾏了对应服务发现的rpc-caller进⾏服务上 线通知,通知rpc-caller当前多了⼀个对应服务的rpc-provider。同时,当⼀个rpc-provider下线时,则可以找到进⾏了该服务发现的rpc-caller进⾏服务的下线通知。

 9. Server

当以上的所有功能模块都完成后,我们就可以将所有功能整合到⼀起来实现服务端程序了

 • RpcServer:rpc功能模块与⽹络通信部分结合。

 • RegistryServer:服务发现注册功能模块与网络通信部分结合

 • TopicServer:发布订阅功能模块与网络通信部分结合。

RpcServer 

RegistryServer

 TopicServer

三、结语

        到此为止,本文关于从零实现Json-RPC框架第六弹的内容到此结束了,如有不足之处,欢迎小伙伴们指出呀!

         关注我 _麦麦_分享更多干货:_麦麦_-CSDN博客

         大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐