rabbitmq,stomp.js,rabbitmq-auth-backend-http,消息调研,消息设计

调研背景

公司消息推送服务以RabbitMQ为基础,pc端消息推送存在消息客户端接口不统一等诸多问题,访问权限不够灵活统一等问题。

为了将公司各个应用之间进行消息解耦,对业务的透明化处理及技术架构的统一管理,降低对各业务模块对消息模块开发难度,保障消息推送服务器与业务系统的稳定性,也方便各应用的消息中间件的快速搭建,尤其对pc端直接操作消息服务器,提供可行性解决方案

一、Web Stomp

消息投递与消费遇到的问题

  1. 统一web端消息接口
  2. 提高消息投递效率,让生产端不在维护交换机和关注队列
  1. 消息服务器与业务解耦

什么是RabbitMQ STOMP

即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。

stomp代理中会将stomp消息的处理委托给一个真正的消息代理(RabbitMQ,activeMQ)进行处理

什么是RabbitMQ Web STOMP

可以理解为 HTML5 WebSocketSTOMP 协议间的桥接,目的也是为了让浏览器能够使用 RabbitMQ。当 RabbitMQ 消息服务器开启了 STOMP 和 Web STOMP 插件后,浏览器端就可以轻松地使用 WebSocket 或者 SockerJS 客户端实现与 RabbitMQ 服务器进行通信。
RabbitMQ Web STOMP 是对 STOMP 协议的桥接,因此其语法也完全遵循 STOMP 协议。STOMP 是基于 frame 的协议,与 HTTPframe 相似。一个 Frame 包含一个 command,一系列可选的 headers 和一个 body。STOMP client 的用户代理可以充当两个角色,当然也可能同时充当:作为生产者,通过 SEND frame 发送消息到服务器;作为消费者,发送 SUBCRIBE frame 到目的地并且通过 MESSAGE frame 从服务器获取消息。
在Web页面中利用WebSocket使用STOMP协议只需要下载stomp.js即可,考虑到老版本的浏览器不支持 WebSocket,SockJS 则提供了 WebSocket 的模拟支持。

STOMP常用命令

CONNECT  与服务器建立连接,校验秘钥
CONNECTED 连接成功
SEND 发送消息
SUBSCRIBE 订阅消息
UNSUBSRIBE 取消订阅
ACK 消息确认,默认为ture,自动确认,手动确认改为false
NACK  NACK是ACK的反向
BEGIN 事务开始
COMMIT 事务提交
ABORT事务回滚
DISCONNECT 断开连接
MESSAGE   用于传输从服务端订阅的消息到客户端,MESSAGE中必须包含destionation头,用以表示这个消息应该发送的目标。如果这个消息被使用STOMP发 送,那么这个destionation应该与相应的SEND帧中的目标一样
ERROR如果连接过程中出现什么错误,服务端就会发送ERROR,并断开连接

header参数

  • durable (aliased as persistent) 持久化 (默认持久化)
  • auto-delete 自动删除 (默认非自动删除)
  • exclusive 独占 (默认非独占)
  • x-message-ttl  消息过期时间 (ms毫秒)
  • x-dead-letter-exchange (死信队列交换机)

服务端docker 暴露15674端口

docker run -it -d --name=rabbit-3.8 -v /d/docker/rabbitmq-stomp/conf:/etc/rabbitmq  -p 5617:5617 -p 5672:5672 -p 4369:4369 -p 15671:15671 -p 15672:15672 -p 25672:25672 -p 15670:15670 -p 15674:15674 rabbitmq:3.8.27

开启插件

如果在docker 环境下  进入docker-rabbitmq容器内 执行

rabbitmq-plugins enable rabbitmq_web_stomp
rabbitmq-plugins enable  rabbitmq_stomp

查看插件是否开启

rabbitmq-plugins list

官方自带demo

rabbitmq-plugins enable rabbitmq_web_stomp_examples

stomp相关配置

如果不想用户名和密码暴露到前端,可以在配置文件中设置默认密码

stomp.default_user = admin
stomp.default_pass = a123456

#更改虚拟vhost
stomp.default_vhost = /


#web stomp 端口修改
web_stomp.tcp.port = 12345

#是否传输压缩
web_stomp.ws_opts.compress = true

#WebSocket 超时时间
web_stomp.ws_opts.idle_timeout = 60000


#无缓存模式
#auth_backends.1 = http

#混合模式 使用模块名称而不是短别名,“http”
#auth_backends.1 = rabbit_auth_backend_http
#auth_backends.2 = internal

#内部身份验证,http授权
#auth_backends.1.authn = internal
#auth_backends.1.authz = rabbit_auth_backend_http

#http授权,内部身份验证
#auth_backends.1.authn = rabbit_auth_backend_http
#auth_backends.1.authz = internal

#stomp.default_user = James
#stomp.default_pass = a123456

stomp 规范

stomp规范没有规定broker必须支持的destination地类型,而是在sendmessage帧中的destination头的值是特定broker。RabbitMQ STOMP适配器支持许多不同的destination类型

有效destination  /temp-queue, /exchange, /topic, /queue, /amq/queue, /reply-queue/

  • /exchange --SEND到任意路由键并SUBSCRIBE任意绑定模式;
    • 1,ex: /exchange/交换机名
    • 2,交换机无法自动创建,可以在管理图形界面创建交换机名或使用rabbitmq默认的交换机
      例如: 如果使用 fanout类型 则后面跟的队列名 无效,会自动给客户端一个临时队列  /exchange/amq.fanout/keyroute
      如果使用direct类型,  /exchange/log/error

amq.direct

direct

D

amq.fanout

fanout

D

amq.headers

headers

D

amq.match

headers

D

amq.topic

  • /queue --向 STOMP 网关管理的队列发送和订阅;
  • /amq/queue --发送和订阅在 STOMP 网关之外创建的队列;备注①
  • /topic -- 发送和订阅临时和持久主题;
    /topic/news.*.*

① stomp网关:就是在stomp协议基础上,创建队列,发布或订阅消息,如果非stomp网关,则无法创建队列,只能订阅已存在的队列

交换机-模式

对应模式在stomp协议下的调用方式

简单队列

//只有一个订阅者
client.subscribe("/queue/task_queue", function(data) {
	var msg = data.body;
	console.log("收到数据:");
	console.log(msg);
	},{ack:'client'});

工作队列,简单队列

//过个订阅者轮询获取
client.subscribe("/queue/task_queue", function(data) {
	var msg = data.body;
	console.log("收到数据:");
	console.log(msg);
	data.ack(); //如果后面带了参数 ack 就是指定要手动确认消息,没带就是自动确认
	},{ack:'client'});

扇形交换机 fanout

扇形交换机效率最好的,它不需要路由绑定等复杂操作

var headers = {ack: 'client'};
	交换机/交换机名
	client.subscribe("/exchange/logs", function(data) {
	var msg = data.body;
	console.log("收到数据:");
	console.log(msg);
	data.ack(); //如果后面带了参数 ack 就是指定要手动确认消息,没带就是自动确认
	},headers);

直连交换机direct

var headers = {ack: 'client'};
	交换机/交换机名/routingkey
	client.subscribe("/exchange/amq.direct/www", function(data) {
	var msg = data.body;
	console.log("收到数据:");
	console.log(msg);
	data.ack(); //如果后面带了参数 ack 就是指定要手动确认消息,没带就是自动确认
	},headers);

主题交换机 topic

function on_connect(x) {
	var headers = {ack: 'client'};
	client.subscribe("/exchange/xxp/fast.*.*", function(data) {
	var msg = data.body;
	console.log("收到数据:");
	console.log(msg);
	data.ack(); //如果后面带了参数 ack 就是指定要手动确认消息,没带就是自动确认
	},headers);
 
};

发送消息

client.send("/queue/user1", {}, content);

前端配置

提示:前端代码演示 实现连接,接受消息,发送消息等具体功能,仅对JS代码做了简单分装以及断开自动重连

引入stomp.js相关文件

<script src="/js/websocket.js"></script>
<script src="/js/jquery.min.js"></script>
<script src="/js/stomp.min.js"></script>

通过stomp建立对话

stompClient.connect('shop','eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ4eHAiLCJpYXQiOiIyMDIyLTAxLTI4IDAwOjAwOjAwIiwiZXhwIjoiMjAyMi0wMS0zMSAwMDowMDowMCIsImF1ZCI6ImZyb20iLCJzdWIiOiJ0byIsInVzZXJuYW1lIjoieHhwIn0.J4fENLsLdHcTEGm6nr5AeQrsEnflVFRGItuBGqgZs1w', function (frame) {
		writeToScreen("connected: " + frame);
  		console.log("连接成功")
  
		}, function (error) {
				console.log("连接失败");
		},
    '/'
	)

成功回调

 var on_connect = function(x) {
 			//subscribe 订阅消息,名为test的队列
       console.log(d.body);
    };

失败回调

 var on_error =  function(f) {
        console.log('error'+f);
};

订阅

var subscribeHeader = {
    	"ack" : "client-individual",
    	'auto-delete': true
    	
    }
	stompClient.subscribe('/queue/15029149799', function (response) {
			writeToScreen(response.body);
			response.ack();

		},subscribeHeader);

消费确认 ACK

  1. 订阅subscribe,需要在header定义ack手动确认
  2. ack 有三种方式,auto(自动), client(批量手动确认), client-individual (逐条手动确认)

 var subscribeHeader = {
    	"ack" : "client-individual",
    }

  1. 需要在subscribe 回调函数中加入

response.ack();

一对一通信

需要提前创建 交换机为 chat2,direct类型

js代码

var stompClient = null;
var wsCreateHandler = null;
var userId = null;
var ipPort = null;
function connect() {
	userId =  GetQueryString("userId");
	if(userId.replace(/(^s*)|(s*$)/g, "").length ==0){
		alert("请在URL带上你的参数");
		return;
	}
	var socket = new WebSocket(ipPort);
	stompClient = Stomp.over(socket);
	stompClient.connect({}, function (frame) {
	writeToScreen("connected: " + frame);

	stompClient.subscribe('/exchange/chat2/'+userId, function (response) {
			writeToScreen('>'+response.body);
		});

		}, function (error) {
			wsCreateHandler && clearTimeout(wsCreateHandler);
			wsCreateHandler = setTimeout(function () {
				console.log("重连...");
				connect();
			}, 10000);
		}
	)
}

function disconnect() {
	if (stompClient != null) {
		stompClient.disconnect();
	}
	writeToScreen("disconnected");
}

function writeToScreen(message) {
	if(DEBUG_FLAG)
	{
		$("#debuggerInfo").val($("#debuggerInfo").val() + "\n" + message);
	}
}

function GetQueryString(name) {
	var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
	var r = window.location.search.substr(1).match(reg); 
	var context = "";
	if (r != null)
		context = r[2];
	reg = null;
	r = null;
	return context == null || context == "" || context == "undefined" ? "" : context;
}

html代码

<head>
<meta charset="UTF-8">
<title>websocket View</title>
<script src="/js/websocket-one-to-one.js"></script>
<script src="/js/jquery.min.js"></script>
<script src="/js/sockjs.min.js"></script>
<script src="/js/stomp.min.js"></script>
<script id="code">

console.log(userId)
    var DEBUG_FLAG = true;
    $(function()
    {
        //启动websocket
        ipPort = "ws://127.0.0.1:15674/ws";
        connect();
    });

    //通过这种方式发送,需要提前创建chat dis交换机

      function send() {
      var toUserId = $("#toUserId").val();

      if(toUserId.replace(/(^s*)|(s*$)/g, "").length ==0){
          alert('请输入用户ID');
          return;
      }
      var msg = $("#msg").val();
      msg = '来自'+userId+'会员的消息:'+msg
      stompClient.send("/exchange/chat2/"+toUserId, {}, msg);
    }
</script>
</head>
    

<body style="margin: 0px;padding: 0px;overflow: hidden; ">
  <div>
    <div>URL参数:userId=xxx UserId我的会员ID</div>
  </div>
  <!-- 显示消息-->
  <textarea id="debuggerInfo" style="width:100%;height:200px;"></textarea>
  <!-- 发送消息-->
  <div>用户:<input type="text" id="toUserId" placeholder="用户ID"></input></div>
  <div>消息:<input type="text" id="msg"></input></div>
    <div><input type="button" value="发送消息" οnclick="send()"></input></div>
</body>
</html>

发布订阅代码演示

发布订阅模式,chat交换机必须提前定义,交换机类型为fanout

js代码

var stompClient = null;
var wsCreateHandler = null;
var userId = null;
var ipPort = null;

function connect() {
	userId =  GetQueryString("userId");
	var socket = new WebSocket(ipPort);
	stompClient = Stomp.over(socket);
	stompClient.connect({}, function (frame) {
		writeToScreen("connected: " + frame);


	stompClient.subscribe('/exchange/chat', function (response) {
			writeToScreen(response.body);
		});

		}, function (error) {
			wsCreateHandler && clearTimeout(wsCreateHandler);
			wsCreateHandler = setTimeout(function () {
				console.log("重连...");
				connect();
			}, 10000);
		}
	)
}

function disconnect() {
	if (stompClient != null) {
		stompClient.disconnect();
	}
	writeToScreen("disconnected");
}

function writeToScreen(message) {
	if(DEBUG_FLAG)
	{
		$("#debuggerInfo").val($("#debuggerInfo").val() + "\n" + message);
	}
}

function GetQueryString(name) {
	var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
	var r = window.location.search.substr(1).match(reg); 
	var context = "";
	if (r != null)
		context = r[2];
	reg = null;
	r = null;
	return context == null || context == "" || context == "undefined" ? "" : context;
}

html代码

<head>
<meta charset="UTF-8">
<title>websocket View</title>
<script src="/js/websocket-subscribe.js"></script>
<script src="/js/jquery.min.js"></script>
<script src="/js/sockjs.min.js"></script>
<script src="/js/stomp.min.js"></script>
<script id="code">

console.log(userId)
    var DEBUG_FLAG = true;
    $(function()
    {
        //启动websocket
        ipPort = "ws://127.0.0.1:15674/ws";
        connect();
    });

    //通过这种方式发送,需要提前创建chat交换机
   function send() {
      var msg = $("#msg").val();
      stompClient.send("/exchange/chat", {}, msg);
    }


</script>
</head>

<body style="margin: 0px;padding: 0px;overflow: hidden; ">
  <!-- 显示消息-->
  <textarea id="debuggerInfo" style="width:100%;height:200px;"></textarea>
  <!-- 发送消息-->
  <div>用户:<input type="text" id="userId"></input></div>
  <div>消息:<input type="text" id="msg"></input></div>
  <div><input type="button" value="群发送消息fanout" οnclick="send()"></input></div>
</body>
</html>

二、认证授权

一. rabbitmq 身份验证和授权是可插拔的,通过启用插件可实现授权,例如  LDAP, HTTP ,内部授权

二. 在rabbitmq中,认证和授权是分开的

  1. 认证是检查消息中心的用户是否有使用虚拟主机的权限,如果没有拒绝链接,当RabbitMQ客户端建立与服务器的连接并进行身份验证时,会指定一个虚拟主机,以便在其中运行。此时将实施一级访问控制,服务器将检查用户是否具有访问该虚拟主机的权限,否则将拒绝连接尝试。
  2. 授权检查是否有权限操作虚拟主机中的资源(交换机和队列),资源,即交换机和队列,是虚拟主机内的实体;相同的名称表示不同虚拟主机中的不同资源。当对资源执行某些操作时,将强制执行第二级访问控制。

开启http安全认证插件

rabbitmq-plugins enable rabbitmq_auth_backend_http

Rabbitmq有两种鉴权方式:

一种是利用内置数据库鉴权。

另一种是rabbitmq-auth-backend-http鉴权插件来实现后端鉴权。
Rabbitmq中,Authentication 和 authorisation是有区别的,

authentication 是“identifying who the user is”, 识别用户是谁

authorisation   是指“determining what the user is and isn’t allowed to do.” 该用户允许你做什么

Rabbitmq有一个默认的vhost和user Rabbitmq的内置数据库会在初始化的时候存入一个名为“/”的vhost,一个名为guest的用户名和密码,并且在“/”的vhost是被授权的。 但是这个默认的用户名和密码只允许连接localhost的rabbitmq 如果想要连接远程的,需要在rabbitmq的config文件配置。

配置文件rabbitmq.conf

rabbitmq版本3.7+

auth_backends.1 = http
auth_http.http_method   = post
auth_http.user_path     = http://some-server/auth/user
auth_http.vhost_path    = http://some-server/auth/vhost
auth_http.resource_path = http://some-server/auth/resource
auth_http.topic_path    = http://some-server/auth/topic

rabbitmq版本<3.7

[
  {rabbit, [{auth_backends, [rabbit_auth_backend_http]}]},
  {rabbitmq_auth_backend_http,
   [{http_method,   post},
    {user_path,     "http(s)://some-server/auth/user"},
    {vhost_path,    "http(s)://some-server/auth/vhost"},
    {resource_path, "http(s)://some-server/auth/resource"},
    {topic_path,    "http(s)://some-server/auth/topic"}]}
].

授权原理

权限如何工作

当 RabbitMQ 客户端与服务器建立连接并进行[身份验证]时,客户端打算操作的虚拟主机。此时执行第一级访问控制,服务器检查用户是否具有访问虚拟主机的任何权限,否则拒绝连接尝试。

资源,即交换和队列,是特定虚拟主机内的命名实体;相同的名称表示每个虚拟主机中的不同资源。当对资源执行某些操作时,会实施第二级访问控制。

RabbitMQ 区分资源上的配置写入读取操作。配置操作创建或销毁资源,或改变它们的行为。操作将 消息写入到资源中。读取操作从资源中取出消息。

为了对资源操作,用户必须已被授予相应的权限。下表显示了执行权限检查的所有 AMQP 命令需要什么类型的资源的权限

权限配置

#该命令仅对rabbitmq 自带内部授权(默认授权)起作用,不对 LDAP, HTTP

#新增一个用户
rabbitmqctl add_user root root

#查看`/`虚拟机权限
rabbitmqctl list_permissions --vhost /

#给admin分配权限
rabbitmqctl  set_permissions -p / admin '.*' '.*' '.*'

设置用户角色
rabbitmqctl set_user_tags admin administrator

MQ向HTTP提交参数

rabbitmq 服务器授权模块 向http验证服务器提交参数

注意! : http 权限认证, 如果websocket端 开启了心跳检测,则建立长链接后,临时更变该链接用户权限,则不会及时检测出权限变更,会继续接受消息,如果关闭了了心跳检测,则长链接过期后会自动重新建立长链接,如果此时临时更不用户权限,则会检测出权限更变,拒绝没有权限的请求,但这存在这个效率问题,因为长链接,建立链接比较耗费宽带资源

user_path

  • username - 用户名
  • password - 密码(ssl开启后密码会被忽略)

vhost_path

  • username - 用户名
  • vhost - 正在访问的资源虚拟主机的名
  • ip - 客户端 IP

resource_path

  • username - 用户名
  • vhost - 包含资源的虚拟主机的名称
  • resource - 资源类型 (exchange,queue,topic)
  • name - 资源的名称
  • permission - 资源的访问级别 ( configure, write, read)

topic_path

  • username - 用户名
  • vhost - 包含资源的虚拟主机的名称
  • resource - 资源的类型(topic在这种情况下)
  • name - 交换机名
  • permission - 资源的访问级别(writeread
  • routing_key - 发布消息时路由键(权限为 时write),订阅时路由(read

用户、角色、权限

php 代码

用户以及角色

权限:rabbitmq 默认给出的四个权限

  • administrator 超管,可以做任何事
  • monitoring  监控者 可登陆管理控制台(启用management plugin的情况下),同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
  • policymaker 策略制定者 可登陆管理控制台(启用management plugin的情况下), 同时可以对policy进行管理。但无法查看节点的相关信息
  • management   普通管理者 仅可登陆管理控制台(启用management plugin的情况下),无法看到节点信息,也无法对策略进行管理
  • none 无法登陆管理控制台,通常就是普通的生产者和消费者。

格式

'用户名' => array(
'password' => '密码',
'roles' => array(
'角色',
),
)

array(
    //Admin user
    'Anthony' => array(
        'password' => 'a123456',
        'roles' => array(
            'administrator',
            // 'impersonator', // report to https://www.rabbitmq.com/validated-user-id.html
        ),
    ),
    'James' => array(
        'password' => 'a123456'
    ),
    'Roger' => array(
        'password' => 'a123456',
        'roles' => array(
            'monitoring',
        ),
    ),
    'Bunny' => array(
        'password' => 'a123456',
        'roles' => array(
            'policymaker',
        ),
    ),
    'admin' => array(
        'password' => 'a123456',
        'roles' => array(
            'administrator',
        ),
    ),
)

权限

格式

'用户名' => array(
'rabbitmq虚拟host' => array(
'rabbitmq ip' => '',
'读' => '.*',
'写' => '.*',
'配置' => '.*',
),
),

$permissions = array(

    'Anthony' => array(
        'isAdmin' => true,
        '/' => array(
            'ip' => '.*',
            'read' => '.*',
            'write' => '.*',
            'configure' => '.*',
        ),
    ),
    'James' => array(
        '/' => array(
            'ip' => '.*',
            'read' => '.*',
            'write' => '.*',
            'configure' => '.*',
        ),
    ),
    'admin' => array(
        '/' => array(
            'ip' => '.*',
            'read' => '.*',
            'write' => '.*',
            'configure' => '.*',
        ),
    ),
);

返回结果

Web 服务器应始终返回 HTTP 200 OK

  • deny - 拒绝访问用户/虚拟主机/资源
  • allow - 允许访问用户/虚拟主机/资源
  • allow [list of tags]- (仅会显示在 user_path时候) - 允许访问,并将用户标记为具有列出的标签

使用TLS/HTTPS 开启

[
  {rabbit, [{auth_backends, [rabbit_auth_backend_http]}]},
  {rabbitmq_auth_backend_http,
   [{http_method,   post},
    {user_path,     "https://some-server/auth/user"},
    {vhost_path,    "https://some-server/auth/vhost"},
    {resource_path, "https://some-server/auth/resource"},
    {topic_path,    "https://some-server/auth/topic"},
    {ssl_options,
     [{cacertfile, "/path/to/cacert.pem"},
      {certfile,   "/path/to/client/cert.pem"},
      {keyfile,    "/path/to/client/key.pem"},
      {verify,     verify_peer},
      {fail_if_no_peer_cert, true}]}]}
].

开启http验证缓存功能

描述

该插件为RabbitMQ 节点执行的[后台http权限验证]提供了一个缓存层。

该插件提供了一种用配置文件,设置缓存时间,缓存身份验证和授权结果的方法。它不是独立的身份验证后端,而是现有后端(例如内置(internal)LDAPHTTP后端)的缓存层 。

缓存过期当前是基于时间的。它对于内置((internal))不是很有用,但对于 LDAP、HTTP 或其他使用网络请求的后端非常有用

开启插件

rabbitmq-plugins enable rabbitmq_auth_backend_cache

配置文件

以http后端验证为例,cache 必须置顶,cache_ttl 为毫秒单位

auth_backends.1 = cache
auth_cache.cached_backend = http
auth_cache.cache_ttl = 5000

auth_http.http_method   = post
auth_http.user_path     = http://localhost:8080/auth/user
auth_http.vhost_path    = http://localhost:8080/auth/vhost
auth_http.resource_path = http://localhost:8080/auth/resource
auth_http.topic_path    = http://localhost:8080/auth/topic

设置缓存模型

提供4种类型

  • rabbit_auth_cache_dict 将缓存条目存储在内部进程字典中。此模块仅用于演示,不应在生产中使用
  • rabbit_auth_cache_ets 将缓存条目存储在ETS表中,并使用计时器进行缓存失效。这是默认实现
  • rabbit_auth_cache_ets_segmented将缓存条目存储在多个 ETS 表中,并且不会删除单个缓存项,而是使用单独的进程进行垃圾收集。
  • rabbit_auth_cache_ets_segmented_stateless与之前相同,但使用最少的gen_server状态,使用 ets 表来存储有关段的信息。

缓存默认实现

auth_cache.cache_module = rabbit_auth_backend_ets

RabbitMQ 权限组合后端

为了解决rabbitmq 不同的权限验证需求,可以开启混合模式

组合:使用多个后端。当使用多个身份验证后端时,配置中后端返回的第一个肯定结果被视为最终结果。

混合:这不应与混合后端混淆(例如,使用LDAP进行身份验证,使用内部后端进行授权)

1.组合身份验证&授权

#rabbitmq.conf 
# internal为RabbitMQ内部鉴权
# 这将首先检查 HTTP,如果用户无法通过 HTTP 进行身份验证,则回退到内部数据库进行权限验证:
auth_backends.1 = rabbit_auth_backend_http
# 使用模块名称而不是短别名,“http” 
auth_backends.2 = internal
#...

2.混合验证

#rabbitmq.conf 
# internal为RabbitMQ内部身份验证
# 通过HTTP进行权限验证
auth_backends.1.authn = internal
auth_backends.1.authz = rabbit_auth_backend_http

常用命令

#显示用户列表
rabbitmqctl list_users

#格式化json格式
rabbitmqctl list_users --formatter=json

#删除用户
rabbitmqctl delete_user 'username'

# 分配权限
# 第一个 ".*" 用于配置每个实体的权限
# 第二个 ".*" 用于每个实体的写权限
# 第三个 ".*" 对于每个实体的阅读许可
rabbitmqctl set_permissions -p "custom-vhost" "username" ".*" ".*" ".*"

#清楚权限
rabbitmqctl clear_permissions -p "custom-vhost" "username"

#授予所有虚拟主机的用户权限。
for v in $(rabbitmqctl list_vhosts --silent); do rabbitmqctl set_permissions -p $v "a-user" ".*" ".*" ".*"; done


# 查看虚拟机
 rabbitmqctl list_vhosts

三、公司业务场景

为了将公司各个应用系统之间进行业务解耦,对业务的透明化处理及技术架构的统一管理,方便对各业务模块对消息模块开发难度,保障消息推送服务器与业务系统的稳定性,也方便各应用的消息中间件的快速搭建,尤其对pc端直接操作消息服务器,提供整体的解决方案

目标

  1. 降低消息与业务耦合度,
  2. 提供统一接口,方便各端调用
  1. 新增更加灵活的访问资源授权模式

方案

服务端

消息可靠性投递流程图

消息确认

消息的确认,是指生产者投递消息后,如果 Broker 收到消息,则会给我们生产者一个应答。生产者进行接收应答,用来确定这条消息是否正常的发送到 Broker ,这种方式也是消息的可靠性投递的核心保障!

Confirm 确认机制流程图

如何实现Confirm确认消息

  • 第一步:在 channel 上开启确认模式: channel.confirmSelect()
  • 第二步:在 channel 上添加监听: channel.addConfirmListener(ConfirmListener listener);, 监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理!

golang代码

//simple 简单路由模式
func confirms(){
	conn, err := amqp.Dial("amqp://shop:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ4eHAiLCJpYXQiOiIyMDIyLTAxLTI4IDAwOjAwOjAwIiwiZXhwIjoiMjAyMi0wMS0zMSAwMDowMDowMCIsImF1ZCI6ImZyb20iLCJzdWIiOiJ0byIsInVzZXJuYW1lIjoieHhwIn0.J4fENLsLdHcTEGm6nr5AeQrsEnflVFRGItuBGqgZs1w@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	ch, err := conn.Channel()
	failOnError(err, "Failed to open an channel")
	defer ch.Close()

	ch.Confirm(false)

	q, err := ch.QueueDeclare(
		"hello", // name
		true,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	failOnError(err, "Failed to declare a queue")
	//处理确认逻辑
	confirms := ch.NotifyPublish(make(chan amqp.Confirmation,1))
	//处理方法
	defer confirmOne(confirms)

	now := time.Now()
	year := now.Year()
	month := now.Month()
	day := now.Day()
	hour := now.Hour()
	minute := now.Minute()
	second := now.Second()
	dateFormat :=  fmt.Sprintf("formart out: %02d-%02d-%02d  %02d:%02d:%02d\n", year, month, day, hour, minute, second)


	body := bodyFrom("hello 我是golang2,我是简单模式1" + dateFormat)

	err = ch.Publish(
		"",            // exchange
		q.Name,
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		})
	failOnError(err, "Failed to publish a message")

	log.Printf(" [x] sent %s", body)
}

// 消息确认
func confirmOne(confirms <-chan amqp.Confirmation) {
	if confirmed := <-confirms; confirmed.Ack {
		fmt.Printf("已送达到RabbitMQ broker tag: %d", confirmed.DeliveryTag)
	} else {
		fmt.Printf("未送达到RabbitMQ brokertag: %d", confirmed.DeliveryTag)
	}
}

正常Broker 接收到消息

D:\go\rabbitmq-server>go run main.go
2022/02/09 13:42:25  [x] sent hello 我是golang2,我是简单模式1formart out: 2022-02-09  13:42:25
已送达到RabbitMQ broker tag: 1

如果不给shop账户权限则

D:\go\rabbitmq-server>go run main.go
2022/02/09 13:44:29  [x] sent hello 我是golang2,我是简单模式1formart out: 2022-02-09  13:44:29
未送达到RabbitMQ brokertag: 0

java代码

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;

public class ConfirmProducer {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();

        String exchangeName = "test_confirm_exchange";
        String routingKey = "item.update";

        //指定消息的投递模式:confirm 确认模式
        channel.confirmSelect();

        //发送
        final long start = System.currentTimeMillis();
        for (int i = 0; i < 5 ; i++) {
            String msg = "this is confirm msg ";
            channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());
            System.out.println("Send message : " + msg);
        }

        //添加一个确认监听, 这里就不关闭连接了,为了能保证能收到监听消息
        channel.addConfirmListener(new ConfirmListener() {
            /**
             * 返回成功的回调函数
             */
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("succuss ack");
                System.out.println(multiple);
                System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
            }
            /**
             * 返回失败的回调函数
             */
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.printf("defeat ack");
                System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
            }
        });
    }
}
Copyimport com.rabbitmq.client.*;
import java.io.IOException;

public class ConfirmConsumer {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setAutomaticRecoveryEnabled(true);
        factory.setNetworkRecoveryInterval(3000);

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();
      
        String exchangeName = "test_confirm_exchange";
        String queueName = "test_confirm_queue";
        String routingKey = "item.#";
        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
        channel.queueDeclare(queueName, false, false, false, null);

        //一般不用代码绑定,在管理界面手动绑定
        channel.queueBind(queueName, exchangeName, routingKey);

        //创建消费者并接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(" [x] Received '" + message + "'");
            }
        };

        //设置 Channel 消费者绑定队列
        channel.basicConsume(queueName, true, consumer);

    }
}

我们此处只关注生产端输出消息

CopySend message : this is confirm msg 
Send message : this is confirm msg 
Send message : this is confirm msg 
Send message : this is confirm msg 
Send message : this is confirm msg 
succuss ack
true
耗时:3ms
succuss ack
true
耗时:4ms

注意事项

  • 我们采用的是异步 confirm 模式:提供一个回调方法,服务端 confirm 了一条或者多条消息后 Client 端会回调这个方法。除此之外还有单条同步 confirm 模式、批量同步 confirm 模式,由于现实场景中很少使用我们在此不做介绍,如有兴趣直接参考官方文档。
  • 我们运行生产端会发现每次运行结果都不一样,会有多种情况出现,因为 Broker 会进行优化,有时会批量一次性 confirm ,有时会分开几条 confirm。

Return 消息机制

  • Return Listener 用于处理一-些不可路 由的消息!
  • 消息生产者,通过指定一个 ExchangeRoutingkey,把消息送达到某一个队列中去,然后我们的消费者监听队列,进行消费处理操作!
  • 但是在某些情况下,如果我们在发送消息的时候,当前的 exchange 不存在或者指定的路由 key 路由不到,这个时候如果我们需要监听这种不可达的消息,就要使用 Return Listener !
  • 在基础API中有一个关键的配置项:Mandatory:如果为 true,则监听器会接收到路由不可达的消息,然后进行后续处理,如果为 false,那么 broker 端自动删除该消息!

Return 消息机制流程图

Return 消息示例

  • 首先我们需要发送三条消息,并且故意将第 0 条消息的 routing Key设置为错误的,让他无法正常路由到消费端。
  • mandatory 设置为 true 路由不可达的消息会被监听到,不会被自动删除.即channel.basicPublish(exchangeName, errRoutingKey, true,null, msg.getBytes());
  • 最后添加监听即可监听到不可路由到消费端的消息channel.addReturnListener(ReturnListener r))

go代码

//Return 消息机制
func ReturnListeningProducer(){
	conn, err := amqp.Dial("amqp://shop:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ4eHAiLCJpYXQiOiIyMDIyLTAxLTI4IDAwOjAwOjAwIiwiZXhwIjoiMjAyMi0wMS0zMSAwMDowMDowMCIsImF1ZCI6ImZyb20iLCJzdWIiOiJ0byIsInVzZXJuYW1lIjoieHhwIn0.J4fENLsLdHcTEGm6nr5AeQrsEnflVFRGItuBGqgZs1w@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	ch, err := conn.Channel()
	failOnError(err, "Failed to open an channel")
	defer ch.Close()


	q, err := ch.QueueDeclare(
		"hello", // name
		true,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	failOnError(err, "Failed to declare a queue")
	//处理确认逻辑
	returns := ch.NotifyReturn(make(chan amqp.Return))
	//处理方法
	go returnsMe(returns)

	now := time.Now()
	year := now.Year()
	month := now.Month()
	day := now.Day()
	hour := now.Hour()
	minute := now.Minute()
	second := now.Second()
	dateFormat :=  fmt.Sprintf("formart out: %02d-%02d-%02d  %02d:%02d:%02d\n", year, month, day, hour, minute, second)


	body := bodyFrom("hello 我是golang2,我是简单模式1,也是return 消息机制" + dateFormat)

	log.Printf("q",q)
	err = ch.Publish(
		"",            // exchange
		"eee",
		true,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(body),
		})
	failOnError(err, "Failed to publish a message")

	log.Printf(" [x] sent %s", body)
}

// 消息确认
func returnsMe(confirms <- chan amqp.Return) {
	confirmed := <-confirms;
	log.Printf("confirmed:",confirmed)
	//_, ok :=confirmed.Headers["x-delay"]
	if string(confirmed.Body)!=""  {
		// 如果生产者消息没有放入队列中,这里需要处理未投递成功的消息,可以放入死信队列还是持久化再次投递
		log.Println("消息没有正确入列:", string(confirmed.Body))
	}else{
	//消息发布成功
	}

}

D:\go\rabbitmq-server>go run main.go
2022/02/09 14:47:39 消息没有正确入列: hello 我是golang2,我是简单模式1,也是return 消息机制formart out: 2022-02-09  14:47:39

java代码

import com.rabbitmq.client.*;
import java.io.IOException;

public class ReturnListeningProducer {
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
      
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        String exchangeName = "test_return_exchange";
        String routingKey = "item.update";
        String errRoutingKey = "error.update";

        //指定消息的投递模式:confirm 确认模式
        channel.confirmSelect();

        //发送
        for (int i = 0; i < 3 ; i++) {
            String msg = "this is return——listening msg ";
            //@param mandatory 设置为 true 路由不可达的消息会被监听到,不会被自动删除
            if (i == 0) {
                channel.basicPublish(exchangeName, errRoutingKey, true,null, msg.getBytes());
            } else {
                channel.basicPublish(exchangeName, routingKey, true, null, msg.getBytes());
            }
            System.out.println("Send message : " + msg);
        }

        //添加一个确认监听, 这里就不关闭连接了,为了能保证能收到监听消息
        channel.addConfirmListener(new ConfirmListener() {
            /**
             * 返回成功的回调函数
             */
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("succuss ack");
            }
            /**
             * 返回失败的回调函数
             */
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.printf("defeat ack");
            }
        });

        //添加一个 return 监听
        channel.addReturnListener(new ReturnListener() {
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("return relyCode: " + replyCode);
                System.out.println("return replyText: " + replyText);
                System.out.println("return exchange: " + exchange);
                System.out.println("return routingKey: " + routingKey);
                System.out.println("return properties: " + properties);
                System.out.println("return body: " + new String(body));
            }
        });

    }
}


Copyimport com.rabbitmq.client.*;
import java.io.IOException;

public class ReturnListeningConsumer {
    public static void main(String[] args) throws Exception {
        //1. 创建一个 ConnectionFactory 并进行设置
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setAutomaticRecoveryEnabled(true);
        factory.setNetworkRecoveryInterval(3000);

        //2. 通过连接工厂来创建连接
        Connection connection = factory.newConnection();

        //3. 通过 Connection 来创建 Channel
        Channel channel = connection.createChannel();

        //4. 声明
        String exchangeName = "test_return_exchange";
        String queueName = "test_return_queue";
        String routingKey = "item.#";

        channel.exchangeDeclare(exchangeName, "topic", true, false, null);
        channel.queueDeclare(queueName, false, false, false, null);

        //一般不用代码绑定,在管理界面手动绑定
        channel.queueBind(queueName, exchangeName, routingKey);

        //5. 创建消费者并接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println(" [x] Received '" + message + "'");
            }
        };

        //6. 设置 Channel 消费者绑定队列
        channel.basicConsume(queueName, true, consumer);
    }
}

我们只关注生产端结果,消费端只收到两条消息。

CopySend message : this is return——listening msg 
Send message : this is return——listening msg 
Send message : this is return——listening msg 
return relyCode: 312
return replyText: NO_ROUTE
return exchange: test_return_exchange
return routingKey: error.update
return properties: #contentHeader<basic>(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
return body: this is return——listening msg 
succuss ack
succuss ack
succuss ack

客户端

客户端接收到消息服务器的消息,需要保存到本地,这里需要做ack手动确认以及幂等性操作

pc客户端流程图

幂等性

由于网络闪断等问题,可能出现重复消息,

  1. 生成唯一ID + 指纹码 ,利用数据库主键去重
    1. 生成唯一ID,利用数据库主键去重
    2. SELECT COUNT(*) FROM msg WHERE id = 唯一ID + 指纹码, 如果查询已存在,则表示已消费
    1. 好处是简单,易操作
    2. 坏处是,高并发下,对数据库的写操作,带来性能瓶颈,解决的办法就是通过分库分表,通过算法(例如hash)把ID路由到不同的字表中,提高数据库读写
  1. 利用redis原子性实现
    1. 好处是利用redis set方法可是轻松实现幂等性
    2. 使用redis做幂等性,如果需要持久化如数据库,带来的问题是,数据库与缓存之间如何做到原子性
    1. 如果不落库,在缓存中存储,如何设置定时同步策略

Ack 和 Nack 机制

消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿!如果由于服务器宕机等严重问题,那我们就需要手工进行ACK保障消费端消费成功!消费端重回队列是为了对没有处理成功的消息,把消息重新会递给Broker!一般我们在实际应用中,都会关闭重回队列,也就是设置为False。

参考 api

订阅subscribe,需要在header定义ack手动确认, ack 有三种方式,auto(自动), client(批量手动确认), client-individual (逐条手动确认)

var subscribeHeader = {
    	"ack" : "client-individual",
}
stompClient.subscribe('/amq/queue/hello', function (response) {
			writeToScreen(response.body);
			response.ack();

},subscribeHeader);

如何设置手动 Ack 、Nack 以及重回队列

  • 首先我们发送五条消息,将每条消息对应的循环下标 i 放入消息的 properties 中作为标记,以便于我们在后面的回调方法中识别。
  • 其次, 我们将消费端的 ·channel.basicConsume(queueName, false, consumer); 中的 autoAck属性设置为 false,如果设置为true的话 将会正常输出五条消息。
  • 我们通过 Thread.sleep(2000)来延时一秒,用以看清结果。我们获取到properties中的num之后,通过channel.basicNack(envelope.getDeliveryTag(), false, true);num为0的消息设置为 nack,即消费失败,并且将 requeue属性设置为true,即消费失败的消息重回队列末端。

消费端限流

默认自动确认模式可能会压倒消费者,因为它们无法像生产者那样快速地处理消息,这可能会导致消费者内存使用 或操作系统CPU暴涨,导致奔溃。

如果RabbitMQ服务器有上万条未处理的消息,我们打开一个消费者客户端,会出现下面情况:
巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据。(导致服务器崩溃,线上故障)
生产端一次推送几百条数据,客户端只接收一两条,在高并发的情况下,不能在生产端做限流,只能在消费端处理

前端代码

var stompClient = null;
var wsCreateHandler = null;
var userId = null;
var ipPort = null;

function connect() {
	userId =  GetQueryString("userId");
	var socket = new WebSocket(ipPort);
	stompClient = Stomp.over(socket);
	stompClient.heartbeat.outgoing = 0; //发送频率
	stompClient.heartbeat.incoming = 0; //接收频率
	stompClient.connect('shop','eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ4eHAiLCJpYXQiOiIyMDIyLTAxLTI4IDAwOjAwOjAwIiwiZXhwIjoiMjAyMi0wMS0zMSAwMDowMDowMCIsImF1ZCI6ImZyb20iLCJzdWIiOiJ0byIsInVzZXJuYW1lIjoieHhwIn0.J4fENLsLdHcTEGm6nr5AeQrsEnflVFRGItuBGqgZs1w', function (frame) {
		writeToScreen("connected: " + frame);

    var subscribeHeader = {
    	"ack" : "client-individual",
    	'auto-delete': true
    	
    }
	stompClient.subscribe('/queue/15029149799', function (response) {
			writeToScreen(response.body);
			response.ack();

		},subscribeHeader);


		}, function (error) {
			wsCreateHandler && clearTimeout(wsCreateHandler);
			wsCreateHandler = setTimeout(function () {
				console.log("重连...");
	
				console.log(stompClient);
				connect();
			}, 10000);
		},'/'
	)
}

function disconnect() {
	if (stompClient != null) {
		stompClient.disconnect();
	}
	writeToScreen("disconnected");
}

function writeToScreen(message) {
	if(DEBUG_FLAG)
	{
		$("#debuggerInfo").val($("#debuggerInfo").val() + "\n" + message);
	}
}

function GetQueryString(name) {
	var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
	var r = window.location.search.substr(1).match(reg); 
	var context = "";
	if (r != null)
		context = r[2];
	reg = null;
	r = null;
	return context == null || context == "" || context == "undefined" ? "" : context;
}

定义消息体

msg_id : 消息id,唯一,可用于消息体

user_id : 用户ID

cat_id : 消息分类 ,1系统通知,2 登录消息,3支付消息,

client_id :客户端分类,0全部客户端,1网络货运,2车后,3财务,4 OA, 5 商城,6统一用户

model: 消息模式 ,0点对点推送,1广播模式

timestamp : 时间戳

data :内容

status :消息状态,默认为0,0未消费,1已消费

{
"msg_id":"xxxxx",
"user_id" : "12345",
"cat_id" : 1,
"client_id" : 0,
"model" : 0,
"level" : 2,
"durable" : 1,
"compensate" : 0,
"timestamp" : "1644463941",
"data" : "您刚在西安登录了",
"status" : 0
}

实例化数据库表

DROP TABLE IF EXISTS `rabbitmq_msg_log`;
CREATE TABLE `rabbitmq_msg_log` (
  `msg_id` varchar(100) NOT NULL AUTO_INCREMENT COMMENT '消息id,唯一,可用于消息体',
  `user_id` varchar(100) NOT NULL DEFAULT '' COMMENT '用户ID',
  `cat_id` varchar(100) NOT NULL DEFAULT '' COMMENT '消息分类 ,1系统通知,2 登录消息,3支付消息',
  `client_id` varchar(20) NOT NULL DEFAULT '' COMMENT '客户端分类,0全部客户端,1网络货运,2车后,3财务,4 OA, 5 商城,6统一用户',
  `model` int(8) NOT NULL DEFAULT 0 COMMENT '消息模式 ,0点对点推送,1广播模式',
  `data` text NOT NULL COMMENT '内容',
  `status` varchar(20) NOT NULL DEFAULT '' COMMENT '消息状态,默认为0,0未消费,1已消费,如果为0,当用户上线,就继续推送',
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '时间',
  PRIMARY KEY (`msg_id`),
  KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='rabbitmq消息';

js封装目标

精简、安全、

一、简单,业务端使用时0学习成本,引入一段js代码即可

二、自动连接到服务器

三、仅提供订阅地址,即可实现点对点推送

四、仅提供订阅地址,即可实现直推模式

五、重连,如果连接断开,则发起自动重连,每20秒重连一次,如果重连5次,依然无法连接,则每1分钟重连一次

六、拉取的数据具有存储本地缓存的功能

七、具有自动去重的功能,如果拉取的数据相同,则丢弃,不再重复存储

八、拉取数据后,需要回调函数返回拉取的消息

九、具有新消息提示功能

十、具有自定义配置功能

  1. 封装连接服务器方法connect (host,username,token,outgoing,incoming)
    参数:
    host 服务器地址(string)
    username:用户名(string)
    token:秘钥(string)
    outgoing:发送心跳 (int)(ms毫秒)
    incoming:接收心跳 (int)(ms毫秒)
  2. 封装订阅方法 subscribe(destination,header,ask)
    参数:
    destination:订阅地址(string)例如:/queue/15029149794_ios ,则路由到用户为15029149794的ios端
    header:头信息 {}  json格式
    ask 消息应答模式 默认为false,自动应答,为true则手动应答,需要和header参数结合使用,subscribeHeader = {
    "ack" : "client-individual"}
  1. 封装自动重连方法reconnect(second)
    second 自动重连秒数(int)
  2. 封装断开连接方法disconnect()
  1. 封装发送消息方法send()

各端定义

考虑到各端消息安全性,各端将以随机数方式展示

destination  /queue/用户名-dtybyWnjPwDIQNDsAE6M

dtybyWnjPwDIQNDsAE6M 网络货运
4nhO0hglWmkAw2sCDn0a 车逅
IPXpOnmCnO9fFbdlB4kZ AO
YJvu4HxWDyMj5hm4Q8GK 财务
58acsSg3jMPs39lOo4NP 统一平台
MqjOMAwJWbo5aDJV9L3S 商城

点对点流程图

生产者

1.双方生产端和消费端共同约束 队列名,
2.生产端只负责推送消息,不负责队列的维护
3.开启消息确认生产端消息确认模式,主要作用是否有权限投递以及是否投递到交换机,
4.Return 消息机制,确保消息被推送到指定队列,如果队列存在则,则投递成功,如果队列不存在,则触发renturn 机制,rabbitmq向生产者返回消息被拒绝提示,这样就可以做后续处理,这样保障了,只有客户端在线才能被消费,不在线则不发送到队列,节省了rabbitmq服务器资源

  1. 生产者发消息的同时,需要持久化消息到mysql数据库,如果触发reteurn 则 ,修改数据库,标记该用户未在线,投递被打回

消息服务器

  1. 用户以及token的验证验证,通过则继续,未通过则直接拒绝
  2. 登录验证通过后,校验该用户时候有读写权限,
  1. 路由到具体的交换机和队列,对数据开始读写操作

消费者

  1. rabbitmq 与客户端 连接 需要建立在 websocket +stomp协议
  2. 客户端 用户,token必须合法,才能建立连接,实现长链接,负责会被拒绝,无法访问
  1. stomp 通过subscribe 订阅地址 ,接受消息消费
  2. 客户端可以把消息存储在本地缓存,展示给消费者,展示方式需产品经理设计

总结

web-stomp插件

优点:

  1. 解决了web 应用,http协议,无法保持长连接困境
  2. 使得html页面与rabbitmq通信成为可能
  1. 长连接节约了频繁握手带来的资源损耗
  2. 提供了简单便捷的订阅,和发布功能
  1. rabbitmq 通过 stomp插件, 实现了 websocket 与rabbitmq数据交换,开发成本较低

问题:

  1. stomp 利用websocket通讯协议,实现了服务器与浏览器之间的全双工通讯,但是大量的websocket 链接,对rabbitmq服务器造成压力
  2. 由于stomp在与rabbitmq通讯是,需要添加用户名与token,这样把账号曝露给用户。造成安全隐患

HTTP后台权限验证

优点

  1. 统一授权验证服务,降低维护成本,实现了业务与消息的解耦
  2. 提高灵活度,降低对rabbtimq开发难度,开发者只负责推送或订阅消息,不再维护或交换机和队列
  1. cache 插件,rabbitmq 与http授权服务器之间搭建缓存中间件,有效解决对授权服务器的压力
  2. 真正做到一个账号一个只能看到自身的消息,解决了传统的一个rabbitmq账号负责所有交换机,队列,消息的创建,维护,一旦该账号泄露,后果严重
  1. 账号,权限更加细分变的方便,简单,灵活

缺点

  1. 频繁访问http后台服务,给授权服务器带来很大的压力,可以使用backend-cache插件来弥补
  2. 只能用于http后台授权与校验,不负责数据是否送达,存储或日志打印等权限之外的工作

附件

  1. web-stomp demo,见该文档随带压缩包
  2. http 授权源码 spring_boot

https://github.com/rabbitmq/rabbitmq-auth-backend-http/tree/master/examples/rabbitmq_auth_backend_spring_boot

参考资料

以上文档文献来源原文连接

  1. rabbitmq配置
    Configuration — RabbitMQ
  2. stomp 文档
    STOMP Plugin — RabbitMQ
    STOMP
    http://stomp.github.io/stomp-specification-1.1.html
  1. web stomp
    RabbitMQ Web STOMP Plugin — RabbitMQ
  2. webstomp简单demo, rabbitmq-web-stomp-examples
    https://github.com/rabbitmq/rabbitmq-web-stomp-examples
  1. 权限控制 access-control
    Authentication, Authorisation, Access Control — RabbitMQ
    Authentication, Authorisation, Access Control — RabbitMQ
    Authentication, Authorisation, Access Control — RabbitMQ
  2. http后台认证 rabbitmq-auth-backend-http
    GitHub - rabbitmq/rabbitmq-auth-backend-http: HTTP-based authorisation and authentication for RabbitMQ
  1. 后台认证缓存 rabbitmq-auth-backend-cache

https://github.com/rabbitmq/rabbitmq-auth-backend-cache

  1. 生产者publishers
    Publishers — RabbitMQ
  2. 队列queues
    Queues — RabbitMQ
  1. 消费者consumers
    Consumers — RabbitMQ
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值