项目第八弹:网络通信协议模块
一、为何需要有网络通信协议模块
1.项目实现情况
目前为止,我们已经实现了:
交换机管理模块,队列管理模块,绑定信息管理模块,消息管理模块,虚拟机管理模块,路由匹配模块和消费者管理模块
2.网络通信协议的选择
我们知道,要想实现基于网络的服务器,就必须要自定义应用层协议,进行跨网络通信
服务器最常用的应用层协议:HTTP/HTTPS
因为任何一个服务器想要让浏览器能够作为客户端来进行访问就得支持HTTP/HTTPS
因此我们首先考虑,我们的消息队列服务器能否用HTTP/HTTPS协议呢,那样不就能够直接用浏览器充当客户端进行访问了吗
1.能否使用HTTP/HTTPS?
1.技术角度
从技术角度来讲,直接使用http协议的话,编程复杂度会大大增高,如果使用其他HTTP网络库,比如cpphttplib,性能则会有所损耗
因为又要开多线程,而线程太多则会拖垮效率,浪费资源
而且用HTTP协议的话,就算使用cpphttplib这种网络库,也依然需要写前端代码(JavaScript等等),对单独的后端工程师不友好,学习和开发成本太大
当然,这是相对于我们这些不熟悉前端的后端学习者来说的
不足以成为消息中间件是否使用HTTP协议的最大因素,我们要从使用HTTP/自定义协议的优劣点进行分析
2.优劣点
1.HTTP的好处
- 能够支持浏览器,允许通过浏览器的图形化界面调用对应服务,方便用户使用
- 跨平台,跨语言
2.HTTP的不足
-
协议复杂:
报头字段较为复杂:状态码,响应码,Cookie… 有时这些字段没有必要,反而增加了消息传递和解析的开销 -
性能瓶颈:
消息中间件需要处理大量的消息数据,对性能要求较高。
而HTTP协议在处理大量并发请求时,可能会遇到性能瓶颈,比如:HTTP头部解析开销,TCP连接的建立与断开开销 -
状态管理
HTTP是无连接,无状态的,只能通过Cookie,Session等等字段来实现状态管理,因此不够灵活
而消息中间件需要长期、稳定的获取和传输消息,因此HTTP对于消息中间件的可靠性和稳健性是不利的
- HTTP协议不支持RabbitMQ的信道,浏览器想要使用多线程只能建立单独的新TCP连接,浪费资源
浏览器使用多线程,比如:
【我可以一边访问某个视频网站,一边使用浏览器下载其视频】
3.自定义协议的好处
- 简化协议,提高性能:无需复杂,冗余字段,没有HTTP头部解析开销
- 能够很灵活的支持RabbitMQ的一切特性【包括信道等等】
4.自定义协议的坏处
- 无法直接支持浏览器,必须要在中间加一层进行HTTP请求,响应与自定义协议的转化
- 需要自己设计并实现,增加开发成本,但是方便扩展,优化,且无需前端知识
5.具体理解
因为在实际应用当中存在浏览器开多线程来操作访问RabbitMQ,所以为了降低TCP频繁建立与断开的开销,并提高并发处理能力,RabbitMQ将连接细分为信道
而这个操作需要客户端进行配合,而浏览器必须遵守HTTP协议,所以RabbitMQ就不能仅仅只支持HTTP协议了,而需要进行自定义协议支持自己的信道
所以RabbitMQ进行了自定义协议。这一过程也有其他诸多好处
而为了跟浏览器进行兼容,RabbitMQ在中间加了一层【HTTP适配层】将HTTP请求响应与自定义协议的请求响应进行转化,从而既可以支持浏览器提高用户体验,又可以支持自定义协议提高性能
3.总结
项目/RabbitMQ不直接支持HTTP/HTTPS的原因:
- HTTP不支持RabbitMQ当中的信道,因而无法发挥出信道的优势
【对TCP连接的复用,更好的支持多线程,提高并发效率】 - HTTP协议报头字段较为复杂,状态码,响应码,Cookie… 有时这些字段没有必要,反而增加了消息传递和解析的开销
- HTTP是无连接,无状态的,只能通过Cookie,Session等等字段来实现状态管理,因此不够灵活
二、网络通信协议的设计
1.怎么设计?---- 请求响应模型
经过网络编程的学习,我们知道,网络通信协议其实就是规定了:
其实,说白了,就是Client和Server俩人在这里玩,一个人发Request,另一个人拿到Request进行处理,然后返回Response,那个人收到Response进行处理
2.开始设计
1.Request
因此,我们给用户提供的所有接口都要有自己的Request
接口:
- 声明/删除信道
- 声明/删除虚拟机
- 声明/删除交换机
- 声明/删除队列
- 绑定/解绑队列
- 订阅/取消订阅队列【创建/删除消费者】
- 发布/确认消息
因此我们要实现14个Request
2.Response
- 作为客户端,我们想要知道的是对应的请求,被处理的结果如何,有没有处理成功,其他的,具体这个响应是针对声明交换机的,还是声明队列的,我们到底要不要考虑呢?
可以考虑,这样的话效果更好,因为得到的响应更加明确
但是为了简化代码,并且遵循YAGNI原则(You Aren’t Gonna Need It)我们选择搞成同一个响应
这是在代码明确性和代码简洁性上权衡后倾向于代码间接性
==================================================
2.但是有一种响应,需要客户端去自行处理:
它叫做消息推送响应,客户端收到这个响应之后需要调用其事先注册好的消息处理函数
注意:这个消息处理函数必须要由客户端消费者去执行,而不能由我们的消息队列服务器去执行【因此客户端也要有消费者!】
为何?
举个生活中的例子:
你收到了一条微信语音,你需要处理这条消息,你需要在你的客户端上点开这个消息进行播放,只有这样,你才能听到那个消息
【这是在你的微信客户端上处理消息】
如果你在微信服务器上处理这个消息,播放这个消息,假设微信服务器足够优秀,能够处理所有用户的所有客户端的所有消息,
那这个语音就算播放了,你也听不到啊
因此,关于客户端和服务器的消费者处理回调函数是这样的:
- 客户端的消费处理回调回调函数:
- 就是用户真正处理消息的函数
- 服务器的消费处理函数:
- 就是:组织响应,发送给对应的消费者客户端
三、网络通信协议的编写
下面有一个字段:req_id(请求ID)
每一个请求都要用一个UUID作为唯一标识,至于为什么,我们要在介绍客户端模块编写的时候才能让大家有更好的理解
这里就先不赘述了,大家目前先知道他必须要有即可
1.怎么编写?
我们知道,网络传输的结构化字段都必须要进行序列和反序列化,还要处理TCP的粘包问题
可是我们有14个请求,2个响应,如果纯靠我们写,写是肯定可以的,只不过代码非常不优雅,非常不简洁,非常没有可扩展性和灵活性
因此我们要不然用JSON,要不然Protobuf,我们之前也说了,我们的项目采用protobuf和muduo库
而陈硕大佬在写muduo库的时候,还贴心的给了muduo库与protobuf的处理类:ProtobufCodec处理粘包问题
ProtobufDispatcher处理消息的注册机制,贴合muduo库事件注册与响应机制
而且使用起来非常优雅
因此为了代码的简洁性,健壮性,我们直接用muduo库的这两个类来将protobuf和muduo库结合起来
当然,我们不是瞎用,我们之前也分析并介绍过ProtobufDispatcher,ProtobufCodec这两个类,了解了每一步为何要这么做
而且也写了具体的代码,搭建的服务器和客户端
2.开始编写
因此,我们只需要编写proto文件即可
写的过程就遵循一个原则:
完成这些操作,我们服务器需要客户端提供什么参数
那么Request当中就要有什么参数
1.声明/删除信道
信道模块我们还未实现(下一个模块就是实现它),但是声明和删除信道需要的其实就是一个信道ID即可
//1. 信道的声明与删除
message OpenChannelRequest
{
string req_id = 1;
string channel_id = 2;
}
message CloseChannelRequest
{
string req_id = 1;
string channel_id = 2;
}
2.声明/删除虚拟机
虚拟机的成员:
虚拟机名称
虚拟机资源所在数据库
虚拟机消息所在目录
//2. 虚拟机的声明与删除
message DeclareVirtualHostRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string dbfile = 4;
string basedir = 5;
}
message EraseVirtualHostRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
}
3.声明/删除交换机
交换机:
- 交换机名称
- 交换机类型
- 持久化标志
- 自动删除标志
- 其他参数
不要忘了指定虚拟机名称!!
//3. 交换机的声明与删除
message DeclareExchangeRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4;
ExchangeType type = 5;
bool durable = 6;
bool auto_delete = 7;
map<string,string> args = 8;
}
message EraseExchangeRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4;
}
4.声明/删除队列
队列:
- 队列名称
- 持久化标志
- 是否独占标志
- 自动删除标志
- 其他参数
//4. 队列的声明与删除
message DeclareMsgQueueRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string queue_name = 4;
bool durable = 5;
bool exclusive = 6;
bool auto_delete = 7;
map<string,string> args = 8;
}
message EraseMsgQueueRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string queue_name = 4;
}
5.队列的绑定/解除绑定
队列名,交换机名称,binding_key
//5. 队列的绑定与解除绑定
message BindRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4;
string queue_name = 5;
string binding_key = 6;
}
message UnbindRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4;
string queue_name = 5;
}
6.队列的订阅与取消订阅
订阅就是创建消费者,取消订阅就是删除消费者
消费者ID,订阅队列名称,自动删除标志
但是无需设置回调函数 因为服务器需要使用自己内部实现并绑定的消费处理回调函数
//6. 队列的订阅与取消订阅
message BasicConsumeRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string consumer_tag = 4;
string queue_name = 5;
bool auto_ack = 6; //注意:我们无需将消费处理回调函数传给服务器,因为服务器需要使用自己内部实现并绑定的消费处理回调函数
}
message BasicCancelRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string consumer_tag = 4;
string queue_name = 5;
}
7.消息的发布与确认
注意:需要用户指定:消息发布到哪个交换机上,然后我们给他进行路由匹配,放到对应队列当中!!
消息本身需要用户传入的是:
消息内容,消息基本属性
//7. 消息的发布与确认
message BasicPublishRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4; //需要用户指定:消息发布到哪个交换机上,然后我们给他进行路由匹配,放到对应队列当中
string body = 5;
BasicProperities properities = 6;
}
message BasicAckRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string queue_name = 4;
string msg_id = 5;
}
8.两个响应
//通用响应
message BasicCommonResponse
{
string req_id = 1;//这个req_id就是该响应所对应的请求的req_id,原因我们在后面写客户端的时候会介绍
string channel_id = 2;
bool ok = 3;//处理是否成功
}
//消费响应
message BasicConsumeResponse
{
string channel_id = 1;//这里无需req_id,原因我们在后面写客户端的时候会介绍
//下面这三个正式消费处理函数的参数
string consumer_tag = 2;
BasicProperities properities = 3;
string body = 4;
}
3.完整proto文件
syntax = "proto3";
package ns_proto;
import "mq_msg.proto";
/*
请求:
1. 信道的声明与删除
2. 虚拟机的声明与删除
3. 交换机的声明与删除
4. 队列的声明与删除
5. 队列的绑定与解除绑定
6. 队列的订阅与取消订阅
7. 消息的发布与确认
*/
//1. 信道的声明与删除
message OpenChannelRequest
{
string req_id = 1;
string channel_id = 2;
}
message CloseChannelRequest
{
string req_id = 1;
string channel_id = 2;
}
//2. 虚拟机的声明与删除
message DeclareVirtualHostRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string dbfile = 4;
string basedir = 5;
}
message EraseVirtualHostRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
}
//3. 交换机的声明与删除
message DeclareExchangeRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4;
ExchangeType type = 5;
bool durable = 6;
bool auto_delete = 7;
map<string,string> args = 8;
}
message EraseExchangeRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4;
}
//4. 队列的声明与删除
message DeclareMsgQueueRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string queue_name = 4;
bool durable = 5;
bool exclusive = 6;
bool auto_delete = 7;
map<string,string> args = 8;
}
message EraseMsgQueueRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string queue_name = 4;
}
//5. 队列的绑定与解除绑定
message BindRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4;
string queue_name = 5;
string binding_key = 6;
}
message UnbindRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4;
string queue_name = 5;
}
//6. 队列的订阅与取消订阅
message BasicConsumeRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string consumer_tag = 4;
string queue_name = 5;
bool auto_ack = 6; //注意:我们无需将消费处理回调函数传给服务器,因为服务器需要使用自己内部实现并绑定的消费处理回调函数
}
message BasicCancelRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string consumer_tag = 4;
string queue_name = 5;
}
//7. 消息的发布与确认
message BasicPublishRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4; //需要用户指定:消息发布到哪个交换机上,然后我们给他进行路由匹配,放到对应队列当中
string body = 5;
BasicProperities properities = 6;
}
message BasicAckRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string queue_name = 4;
string msg_id = 5;
}
//通用响应
message BasicCommonResponse
{
string req_id = 1;//这个req_id就是该响应所对应的请求的req_id,原因我们在后面写客户端的时候会介绍
string channel_id = 2;
bool ok = 3;//处理是否成功
}
//消费响应
message BasicConsumeResponse
{
string channel_id = 1;//这里无需req_id,原因我们在后面写客户端的时候会介绍
//下面这三个正式消费处理函数的参数
string consumer_tag = 2;
BasicProperities properities = 3;
string body = 4;
}
执行:
protoc --cpp_out=. mq_proto.proto
成功编译
以上就是项目第八弹:网络通信协议模块的全部内容