最近在做一个公司的日志组件时有一个问题难住了我。今天问题终于解决了。由于在解决问题中,在网上也查了很多资料都没有一个完整的实例可以参考。所以本着无私分享的目的记录一下完整的解决过程和实例。
需求:做一个统一日志系统可以查看日志列表和一个可以订阅最新日志的页面。通过提供一个封装好日志记录方法的sdk文件将日志统一收集。
通过上面的需求进行我们使用RabbitMQ+Mongodb来实现系统。
使用C#封装一个SDK大家都会这里就不说了。C#连接RabbitMQ示例代码也是一堆堆的也没什么好说的。下面重点说一下网页端如何使用JS去订阅RabbitMQ收到的最新日志信息。
后端都是使用RabbitMQ的AMQP协议,而前端要求在网页HTML上显示数据。我们选择了使用MQTT协议从RabbitMQ中订阅数据。
具体步骤:
1、先准备好相关JS库。MQTT有一个叫browserMqtt.js看名字就知道是为浏览器提供的JS库。还有一个封装了操作MQ的JS库 mqfactory.js。最后还要一个jquery.js文件。这样工具就准备好了。JS文件下载
2、HTML端代码。
<script type="text/javascript" src="~/js/MqJs/jquery.js"></script> <script type="text/javascript" src="~/js/MqJs/browserMqtt.min.js"></script> <script type="text/javascript" src="~/js/MqJs/mqfactory.js"></script> <body> <div> <lable>Host: </lable><input id="txtHost" placeholder="192.168.1.88" value="10.1.0.7" /><br /> <lable>Port: </lable><input id="txtPort" placeholder="15675" value="15675" /><br /> <label>UserName: </label><input id="txtUserName" placeholder="username" value="admin" /><br /> <label>Password: </label><input id="txtPassword" placeholder="password" value="admin" /><br /> <label>Protocol: </label><input id="txtProtocol" placeholder="ws" value="ws" /><br /> <input id="btnConnect" type="button" value="Connect RabbitMQ" /> </div> <div> <input id="btnSubscribe" type="button" value="Subscribe" /> <input id="btnPublish" type="button" value="Publish" /><br /> <input id="btnSSHuanjing" type="button" value="Subscribe Huanjing" /> <input id="hdnIsSubscribed" type="hidden" value="" /> <input id="btnPubHuanjing" type="button" value="Publish Huanjing"><br /> 路由:<input id="btnRoutingKey" type="text" value="Dcon/Logs/Client"><br /> <input id="txtMessage" type="text" placeholder="Please enter message" /> </div> <div> <label>log:</label><br /> <ul id="lstLog"></ul> <input id="btnClearLog" type="button" value="Clear Log" /> </div> </body> <script type="text/javascript"> $(function () { var mqclient; //var routingKey = 'Dcon.Logs.ServerWebShow'; var message; $('#btnSubscribe').attr('disabled', 'disabled'); $('#btnPublish').attr('disabled', 'disabled'); $('#btnSSHuanjing').attr('disabled', 'disabled'); $('#btnPubHuanjing').attr('disabled', 'disabled'); $('#btnConnect').click(function () { var mqttOpts = { host: (() => $('#txtHost').val())(), port: (() => $('#txtPort').val())(), username: (() => $('#txtUserName').val())(), password: (() => $('#txtPassword').val())(), //transformWsUrl方法用于在浏览器中使用MQTT的场景,默认情况下,MQTT自动生成的url为ws://ip:port形式, //然而服务器要求的格式是ws://ip:port/ws,所以MQTT提供了此接口用于在生成url时自定义url格式 transformWsUrl: (url, opts, client) => { return opts.protocol && opts.protocol == 'ws' ? url + 'ws' : url; }, clientId: (() => { return 'mqttjs_' + Math.random().toString(16).substr(2, 8); })() }; var biz = { huanjing: function (handler, isOn) { if (isOn !== false) { this.ss(this.topics.huanjing, handler); } else { this.sus(this.topics.huanjing, handler); } }, topics: { huanjing: '/hyj/huanjing/monitor' } }; //系统初始化时注入连接选项 mqfactory.inject(mqttOpts, biz); //创建mqclient单例 mqclient = mqfactory.create(); //注册mqclient的连接成功事件 mqclient.on('connect', mqconnected); }); $('#btnSubscribe').click(function () { if ($(this).val() == 'Subscribe') { //订阅成功后,仅注册一次事件(要考虑每次注册事件时,事件处理器调用的次数,如果仅用一次,就用once方法) //routingKey = $("#btnRoutingKey").val(); mqclient.once('onss', mqSubscribeSuccess); //简单订阅 mqclient.ss($("#btnRoutingKey").val()); } else { mqclient.once('onsus', mqUnsubscribeSuccess) mqclient.sus($("#btnRoutingKey").val()); } }); $('#btnPublish').click(function () { var msg = $('#txtMessage').val().length > 0 ? $('#txtMessage').val() : guid(); if (message === msg) { msg = guid(); } message = msg; $('#txtMessage').val(message); //发送消息 mqclient.pub($("#btnRoutingKey").val(), message); $('#lstLog').append('<li>Send Message: ' + message + '</li>'); }); $('#btnSSHuanjing').click(function () { if ($(this).val() == 'Subscribe Huanjing') { mqclient.once('onss', mqHJSubscribeSuccess); mqclient.huanjing(onHuanjingMessageArrived); } else { mqclient.once('onsus', mqHJUnsubscribeSuccess); mqclient.huanjing(onHuanjingMessageArrived, false); } }); $('#btnPubHuanjing').click(function () { var msg = $('#txtMessage').val().length > 0 ? $('#txtMessage').val() : guid(); if (message === msg) { msg = guid(); } message = msg; $('#txtMessage').val(message); //发送消息 mqclient.pub(mqclient.topics.huanjing, message); $('#lstLog').append('<li>Send Huanjing Message: ' + message + '</li>'); }); $('#btnClearLog').click(function () { $('#lstLog').empty(); }); function mqconnected() { //alert("mqconnected"); $('#btnSubscribe').removeAttr('disabled'); $('#btnPublish').removeAttr('disabled'); $('#btnSSHuanjing').removeAttr('disabled'); $('#btnPubHuanjing').removeAttr('disabled'); $('#lstLog').append('<li>mqclient connected</li>'); } function mqSubscribeSuccess() { //订阅成功,就注册接受消息的方法,此处要接收多次,因此使用了on mqclient.on($("#btnRoutingKey").val(), onMessageArrived); $('#btnSubscribe').val('Unsubscribe'); $('#lstLog').append('<li>Subscribe successful.' + $("#btnRoutingKey").val()+'</li>'); } function mqUnsubscribeSuccess() { //注销订阅,所以将事件处理器解除绑定 mqclient.off($("#btnRoutingKey").val(), onMessageArrived); $('#btnSubscribe').val('Subscribe'); $('#lstLog').append('<li>Unsubscribe successful</li>'); } function mqHJSubscribeSuccess() { $('#btnSSHuanjing').val('Unsubscribe Huanjing'); $('#lstLog').append('<li>Hanjing Subscribe successful</li>'); } function mqHJUnsubscribeSuccess() { $('#btnSSHuanjing').val('Subscribe Huanjing'); $('#lstLog').append('<li>Huanjing Unsubscribe successful</li>'); } function onMessageArrived(message) { $('#lstLog').append('<li>Receive message: ' + new Date().toString() + ' ' + message.toString() + '</li>'); } function onHuanjingMessageArrived(message) { $('#lstLog').append('<li>Receive Huanjing message: ' + new Date().toString() + ' ' + message.toString() + '</li>'); } function guid() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } }); </script>
3.后端代码:
3.1客户端sdk代码
/// <summary> /// 写日志 /// </summary> /// <param name="model"></param> public static void Write(LogModel model) { //判断写入的日志级别 if (model != null && model.LogLevel >= LogLevel) { try { var mqMsg = new MqMessage() { MessageBody = JSON.Serialize(model), MessageRouter = SystemConst.RoutingKeyTopic.LogTopic_Producer }; //MQHelper.Instance.ProducerMessage_Fanout(mqMsg); MQHelper.Instance.ProducerMessage_Topic(mqMsg); } catch (Exception ex) { var errorLog = string.Format("Ip:{0},LogHelper.Write方法异常,{1}", IpHelper.LocalHostIp, ex.Message); //MQHelper.Instance.ProducerMessage_Fanout(new MqMessage() { MessageBody = errorLog }); MQHelper.Instance.ProducerMessage_Topic(new MqMessage() { MessageBody = errorLog }); } } }
3.2后端MQ代码:
#region 主题 交换机 /// <summary> /// 生产者 客户端调用 /// </summary> /// <param name="msg"></param> public void ProducerMessage_Topic(MqMessage msg) { try { using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { var body = Encoding.UTF8.GetBytes(msg.MessageBody); channel.BasicPublish(exchange: SystemConst.MqName_LogMq_TopicDefault, routingKey: msg.MessageRouter, basicProperties: null, body: body); Console.WriteLine(" [x] Sent {0}", msg.MessageBody); } } } catch (Exception ex) { var exMsg = ex.Message; } } /// <summary> /// 消费者 服务器接收并写入数据库 /// 消费方法无法通过参数传入 /// EventHandler<BasicDeliverEventArgs> received /// </summary> public void ConsumeMessage_Topic(params string[] routingKeys) { if (routingKeys == null || routingKeys.Length == 0) { throw new Exception("请指定接收路由"); } using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { var queueName = channel.QueueDeclare().QueueName;//获得已经生成的随机队列名 //对列与交换机绑定 foreach (var rKey in routingKeys) { channel.QueueBind(queue: queueName, exchange: SystemConst.MqName_LogMq_TopicDefault, routingKey: rKey); } var consumer = new EventingBasicConsumer(channel); //绑定消费方法 consumer.Received += consomer_Received_Topic; //绑定消费者 channel.BasicConsume(queue: queueName, autoAck: true, consumer: consumer); Console.WriteLine("日志订阅服务启动成功."); Console.WriteLine(" Press [enter] to exit."); Console.ReadLine(); } } } /// <summary> /// 接收通知服务异步的推送 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void consomer_Received_Topic(object sender, BasicDeliverEventArgs e) { var body = e.Body; var message = Encoding.UTF8.GetString(body); Console.WriteLine(" [x] {0}", message); //这里可以增加写入数据库的代码 } #endregion
3.3路由
/// <summary> /// 主题路由 /// </summary> public class RoutingKeyTopic { /// <summary> /// 生产者 /// </summary> public const string LogTopic_Producer = "Dcon.Logs.Client"; /// <summary> /// 消息者_日志服务_保存日志 /// </summary> public const string LogTopic_Consume_Server_SaveDB = "Dcon.Logs.*"; /// <summary> /// 消息者_日志服务_Web显示日志 /// </summary> public const string LogTopic_Consume_Server_WebShow = "Dcon.Logs#";//".Logs.Client"; /// <summary> /// 消息者_日志服务_Web显示日志 /// </summary> public const string LogTopic_Consume_Server_WebShow_T = "*.Logs.Client";//".Logs.Client"; /// <summary> /// 消息者_日志服务_ # 接收所有 /// </summary> public const string LogTopic_Consume_Server_All = "#";//".Logs.Client"; } }
注意点:
1、MQTT的路由是以 / 来分割的。在RabbitMQ中会被转义成 . 如示例中的路由Dcon/Logs/Client会被转换成 Dcon.Logs.Client
2、网页端接收时的路由要和发送端的路由一至。也就是说 后端用 Dcon.Logs.Client 来推数据前端就要使用 Dcon/Logs/Client来接收数据。
3、MQTT路由不支持通配符.
4、由于MQTT的JS库没有提供Topic交换机与路由绑定功能。所以前端接收时 不能设置订阅主题交换机名称。如果要和amqp交互只能使用amqp的默认主题交换机名称 amq.topic
运行效果图: