调研背景
公司消息推送服务以RabbitMQ为基础,pc端消息推送存在消息客户端接口不统一等诸多问题,访问权限不够灵活统一等问题。
为了将公司各个应用之间进行消息解耦,对业务的透明化处理及技术架构的统一管理,降低对各业务模块对消息模块开发难度,保障消息推送服务器与业务系统的稳定性,也方便各应用的消息中间件的快速搭建,尤其对pc端直接操作消息服务器,提供可行性解决方案
一、Web Stomp
消息投递与消费遇到的问题
- 统一web端消息接口
- 提高消息投递效率,让生产端不在维护交换机和关注队列
- 消息服务器与业务解耦
什么是RabbitMQ STOMP
即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
stomp代理中会将stomp消息的处理委托给一个真正的消息代理(RabbitMQ,activeMQ)进行处理
什么是RabbitMQ Web STOMP
可以理解为 HTML5 WebSocket
与 STOMP
协议间的桥接,目的也是为了让浏览器能够使用 RabbitMQ
。当 RabbitMQ
消息服务器开启了 STOMP 和 Web STOMP 插件后,浏览器端就可以轻松地使用 WebSocket 或者 SockerJS 客户端实现与 RabbitMQ 服务器进行通信。RabbitMQ Web STOMP
是对 STOMP
协议的桥接,因此其语法也完全遵循 STOMP
协议。STOMP
是基于 frame
的协议,与 HTTP
的frame
相似。一个 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,自动确认,手动确认改为falseNACK
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
地类型,而是在send
和message
帧中的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
direct | D | |||
fanout | D | |||
headers | D | |||
headers | D | |||
- /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
- 订阅subscribe,需要在header定义ack手动确认
- ack 有三种方式,auto(自动), client(批量手动确认), client-individual (逐条手动确认)
var subscribeHeader = { "ack" : "client-individual", }
- 需要在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中,认证和授权是分开的
- 认证是检查消息中心的用户是否有使用虚拟主机的权限,如果没有拒绝链接,当RabbitMQ客户端建立与服务器的连接并进行身份验证时,会指定一个虚拟主机,以便在其中运行。此时将实施
一级访问控制
,服务器将检查用户是否具有访问该虚拟主机的权限,否则将拒绝连接尝试。 - 授权检查是否有权限操作虚拟主机中的资源(交换机和队列),资源,即交换机和队列,是虚拟主机内的实体;相同的名称表示不同虚拟主机中的不同资源。当对资源执行某些操作时,将强制执行第二级访问控制。
开启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
- 资源的访问级别(write
或read
)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)
、LDAP
或HTTP后端
)的缓存层 。
缓存过期当前是基于时间的。它对于内置((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端直接操作消息服务器,提供整体的解决方案
目标
- 降低消息与业务耦合度,
- 提供统一接口,方便各端调用
- 新增更加灵活的访问资源授权模式
方案
服务端
消息可靠性投递流程图
消息确认
消息的确认,是指生产者投递消息后,如果 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 用于处理一-些不可路 由的消息!
- 消息生产者,通过指定一个
Exchange
和Routingkey
,把消息送达到某一个队列中去,然后我们的消费者监听队列,进行消费处理操作!
- 但是在某些情况下,如果我们在发送消息的时候,当前的 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客户端流程图
幂等性
由于网络闪断等问题,可能出现重复消息,
- 生成唯一ID + 指纹码 ,利用数据库主键去重
-
- 生成唯一ID,利用数据库主键去重
- SELECT COUNT(*) FROM msg WHERE id = 唯一ID + 指纹码, 如果查询已存在,则表示已消费
-
- 好处是简单,易操作
- 坏处是,高并发下,对数据库的写操作,带来性能瓶颈,解决的办法就是通过分库分表,通过算法(例如hash)把ID路由到不同的字表中,提高数据库读写
- 利用redis原子性实现
-
- 好处是利用redis set方法可是轻松实现幂等性
- 使用redis做幂等性,如果需要持久化如数据库,带来的问题是,数据库与缓存之间如何做到原子性
-
- 如果不落库,在缓存中存储,如何设置定时同步策略
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分钟重连一次
六、拉取的数据具有存储本地缓存的功能
七、具有自动去重的功能,如果拉取的数据相同,则丢弃,不再重复存储
八、拉取数据后,需要回调函数返回拉取的消息
九、具有新消息提示功能
十、具有自定义配置功能
- 封装连接服务器方法connect (host,username,token,outgoing,incoming)
参数:
host 服务器地址(string)
username:用户名(string)
token:秘钥(string)
outgoing:发送心跳 (int)(ms毫秒)
incoming:接收心跳 (int)(ms毫秒) - 封装订阅方法 subscribe(destination,header,ask)
参数:
destination:订阅地址(string)例如:/queue/15029149794_ios ,则路由到用户为15029149794的ios端
header:头信息 {} json格式
ask 消息应答模式 默认为false,自动应答,为true则手动应答,需要和header参数结合使用,subscribeHeader = {
"ack" : "client-individual"}
- 封装自动重连方法reconnect(second)
second 自动重连秒数(int) - 封装断开连接方法disconnect()
- 封装发送消息方法send()
各端定义
考虑到各端消息安全性,各端将以随机数方式展示
destination /queue/用户名-dtybyWnjPwDIQNDsAE6M
dtybyWnjPwDIQNDsAE6M 网络货运 4nhO0hglWmkAw2sCDn0a 车逅 IPXpOnmCnO9fFbdlB4kZ AO YJvu4HxWDyMj5hm4Q8GK 财务 58acsSg3jMPs39lOo4NP 统一平台 MqjOMAwJWbo5aDJV9L3S 商城
点对点流程图
生产者
1.双方生产端和消费端共同约束 队列名,
2.生产端只负责推送消息,不负责队列的维护
3.开启消息确认生产端消息确认模式,主要作用是否有权限投递以及是否投递到交换机,
4.Return 消息机制,确保消息被推送到指定队列,如果队列存在则,则投递成功,如果队列不存在,则触发renturn 机制,rabbitmq向生产者返回消息被拒绝提示,这样就可以做后续处理,这样保障了,只有客户端在线才能被消费,不在线则不发送到队列,节省了rabbitmq服务器资源
- 生产者发消息的同时,需要持久化消息到mysql数据库,如果触发reteurn 则 ,修改数据库,标记该用户未在线,投递被打回
消息服务器
- 用户以及token的验证验证,通过则继续,未通过则直接拒绝
- 登录验证通过后,校验该用户时候有读写权限,
- 路由到具体的交换机和队列,对数据开始读写操作
消费者
- rabbitmq 与客户端 连接 需要建立在 websocket +stomp协议
- 客户端 用户,token必须合法,才能建立连接,实现长链接,负责会被拒绝,无法访问
- stomp 通过subscribe 订阅地址 ,接受消息消费
- 客户端可以把消息存储在本地缓存,展示给消费者,展示方式需产品经理设计
总结
web-stomp插件
优点:
- 解决了web 应用,http协议,无法保持长连接困境
- 使得html页面与rabbitmq通信成为可能
- 长连接节约了频繁握手带来的资源损耗
- 提供了简单便捷的订阅,和发布功能
- rabbitmq 通过 stomp插件, 实现了 websocket 与rabbitmq数据交换,开发成本较低
问题:
- stomp 利用websocket通讯协议,实现了服务器与浏览器之间的全双工通讯,但是大量的websocket 链接,对rabbitmq服务器造成压力
- 由于stomp在与rabbitmq通讯是,需要添加用户名与token,这样把账号曝露给用户。造成安全隐患
HTTP后台权限验证
优点
- 统一授权验证服务,降低维护成本,实现了业务与消息的解耦
- 提高灵活度,降低对rabbtimq开发难度,开发者只负责推送或订阅消息,不再维护或交换机和队列
- cache 插件,rabbitmq 与http授权服务器之间搭建缓存中间件,有效解决对授权服务器的压力
- 真正做到一个账号一个只能看到自身的消息,解决了传统的一个rabbitmq账号负责所有交换机,队列,消息的创建,维护,一旦该账号泄露,后果严重
- 账号,权限更加细分变的方便,简单,灵活
缺点
- 频繁访问http后台服务,给授权服务器带来很大的压力,可以使用backend-cache插件来弥补
- 只能用于http后台授权与校验,不负责数据是否送达,存储或日志打印等权限之外的工作
附件
- web-stomp demo,见该文档随带压缩包
- http 授权源码 spring_boot
参考资料
以上文档文献来源原文连接
- rabbitmq配置
Configuration — RabbitMQ - stomp 文档
STOMP Plugin — RabbitMQ
STOMP
http://stomp.github.io/stomp-specification-1.1.html
- web stomp
RabbitMQ Web STOMP Plugin — RabbitMQ - webstomp简单demo, rabbitmq-web-stomp-examples
https://github.com/rabbitmq/rabbitmq-web-stomp-examples
- 权限控制 access-control
Authentication, Authorisation, Access Control — RabbitMQ
Authentication, Authorisation, Access Control — RabbitMQ
Authentication, Authorisation, Access Control — RabbitMQ - http后台认证 rabbitmq-auth-backend-http
GitHub - rabbitmq/rabbitmq-auth-backend-http: HTTP-based authorisation and authentication for RabbitMQ
- 后台认证缓存 rabbitmq-auth-backend-cache
https://github.com/rabbitmq/rabbitmq-auth-backend-cache
- 生产者publishers
Publishers — RabbitMQ - 队列queues
Queues — RabbitMQ
- 消费者consumers
Consumers — RabbitMQ