.Net Core 实现阿里云MNS + 简单封装
.Net Core 实现阿里云MNS + 简单封装
前言
在实际的开发应用场景会出现很多需要事件通知的场景,例如用户下单30分钟后未付款,这时候需要系统自动关闭订单等操作,这个时候不可能在后台开启一个Job去一直查询监听数据,这个时候就需要事件推送 事件订阅来完成关闭订单的操作,事件相关的中间件有很多例如RabbitMQ,ZeroMQ,RocketMQ等等等,但是相较于小型初创企业自己去搭建一个消息服务在自己去维护,笔者觉的成本过于庞大没有专业人员的运维,实际上线后可能稳定性和性能都不尽如人意,这个时候可以考虑看看各大云厂商的解决方案。
准备
本次采用的环境是 .Net Core 1.1版本
本次消息服务采用的是阿里云MNS
实践
首先根据阿里云的MNS Api 稳定我们要把相关的消息服务接口代码进行实现 如下
1 /// <summary> 2 /// 阿里云MNS消息服务 3 /// </summary> 4 public class MNSService : IMNSService 5 { 6 private static HttpClient client; 7 private static string Url = @"https://xxxxxxx.mns.cn-hangzhou.aliyuncs.com/"; 8 private static string AccessKeyId; 9 private static string AccessKeySecret; 10 private static string Host = Url.StartsWith("http://") ? Url.Substring(7) : Url; 11 private static string Version = "2015-06-06"; 12 13 public MNSService(string url,string accessKeyId,string accessKeySecret) 14 { 15 Url = url; 16 AccessKeyId = accessKeyId; 17 AccessKeySecret = accessKeySecret; 18 } 19 20 /// <summary> 21 /// 删除消息 22 /// </summary> 23 /// <typeparam name="T"></typeparam> 24 /// <param name="receipthandle"></param> 25 /// <returns></returns> 26 public async Task DeleteMessageAsync<T>(string receipthandle) 27 { 28 client = new HttpClient(); 29 var queueName = typeof(T).Name; 30 client.DefaultRequestHeaders.Clear(); 31 Dictionary<string, string> headers = new Dictionary<string, string>(); 32 headers.Add("Host", Host); 33 headers.Add("Date", DateTime.Now.ToUniversalTime().ToString("r")); 34 headers.Add("x-mns-version", Version); 35 string url = string.Format("{0}/{1}", queueName, "messages"); 36 headers.Add("Authorization", this.Authorization("DELETE", headers, string.Format("{0}", "/queues/" + queueName + "/messages?ReceiptHandle=" + receipthandle))); 37 38 foreach (var kv in headers) 39 { 40 if (kv.Key != "Content-Type" && kv.Key != "Host") 41 client.DefaultRequestHeaders.Add(kv.Key, kv.Value); 42 } 43 var res = await client.DeleteAsync(Url + string.Format("queues/{0}/{1}?ReceiptHandle=" + receipthandle, queueName, "messages")); 44 if (res.StatusCode == System.Net.HttpStatusCode.NoContent) 45 { 46 return; 47 } 48 } 49 50 /// <summary> 51 /// 消费消息 52 /// </summary> 53 /// <typeparam name="T">返回消息模型</typeparam> 54 /// <param name="waitseconds">等待时间</param> 55 /// <returns></returns> 56 public async Task<Tuple<T, string>> ReceiveMessageAsync<T>(int waitseconds) 57 { 58 client = new HttpClient(); 59 var queueName = typeof(T).Name; 60 Dictionary<string, string> headers = new Dictionary<string, string>(); 61 headers.Add("Host", Host); 62 headers.Add("Date", DateTime.Now.ToUniversalTime().ToString("r")); 63 headers.Add("x-mns-version", Version); 64 string url = string.Format("{0}/{1}", queueName, "messages"); 65 headers.Add("Authorization", this.Authorization("GET", headers, string.Format("{0}", "/queues/" + queueName + "/messages?waitseconds=" + waitseconds))); 66 67 foreach (var kv in headers) 68 { 69 if (kv.Key != "Content-Type" && kv.Key != "Host") 70 client.DefaultRequestHeaders.Add(kv.Key, kv.Value); 71 } 72 var res = await client.GetAsync(Url + string.Format("queues/{0}/{1}?waitseconds=" + waitseconds, queueName, "messages")); 73 if (res.StatusCode == System.Net.HttpStatusCode.OK) 74 { 75 var contentText = await res.Content.ReadAsStringAsync(); 76 var dic = contentText.GetInfoFromXml(); 77 var ReceiptHandle = dic["ReceiptHandle"]; 78 byte[] bpath = Convert.FromBase64String(dic["MessageBody"]); 79 var bodyjson = Encoding.UTF8.GetString(bpath); 80 var result = JsonConvert.DeserializeObject<T>(bodyjson); 81 return new Tuple<T, string>(result, ReceiptHandle); 82 } 83 else 84 { 85 return new Tuple<T, string>(default(T), ""); 86 } 87 } 88 89 /// <summary> 90 /// 发送消息 91 /// </summary> 92 /// <param name="bodyjson">消息内容</param> 93 /// <returns></returns> 94 public async Task SendMessageAsync<T>(string bodyjson) 95 { 96 client = new HttpClient(); 97 var queueName = typeof(T).Name; 98 Dictionary<string, string> headers = new Dictionary<string, string>(); 99 headers.Add("Host", Host); 100 headers.Add("Date", DateTime.Now.ToUniversalTime().ToString("r")); 101 headers.Add("x-mns-version", Version); 102 headers["Content-Type"] = "text/xml"; 103 string url = string.Format("{0}/{1}", queueName, "messages"); 104 headers.Add("Authorization", this.Authorization("POST", headers, string.Format("{0}", "/queues/" + queueName + "/messages"))); 105 106 foreach (var kv in headers) 107 { 108 if (kv.Key != "Content-Type" && kv.Key != "Host") 109 client.DefaultRequestHeaders.Add(kv.Key, kv.Value); 110 } 111 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); 112 StringBuilder sb = new StringBuilder(); 113 sb.Append("<Message> "); 114 sb.Append("<MessageBody>" + Convert.ToBase64String(Encoding.UTF8.GetBytes(bodyjson)) + "</MessageBody> "); 115 //sb.Append("<DelaySeconds>0</DelaySeconds> "); 116 sb.Append("<Priority>1</Priority>"); 117 sb.Append("</Message>"); 118 HttpContent content = new StringContent(sb.ToString()); 119 content.Headers.ContentType = new MediaTypeHeaderValue("text/xml"); 120 client.DefaultRequestHeaders.Connection.Add("keep-alive"); 121 var res = await client.PostAsync(Url + "/" + string.Format("queues/{0}/{1}", queueName, "messages"), content); 122 if (res.StatusCode == System.Net.HttpStatusCode.Created) 123 return; 124 } 125 126 }
实现了阿里云的Api后 笔者发现需要自己实现消息的监听,根据阿里官方推荐监听的实现方式有两种方式官方采用的Java代码作为案例如下
第一种方式,利用定时器,如quartz, Timer, ScheduledExecutorService..., 每隔一段时间拉取一次消息。
第二种方式 ,使用while循环,如while(true)
笔者考虑第一种方式可能略显笨重,也不想依靠三方库进行实现所以采用了 while 的方案代码如下
1 /// <summary> 2 /// 事件总线实现 3 /// </summary> 4 public class EventBus : IEventBus 5 { 6 public IMNSService IMNSService { get; set; } 7 8 public EventBus(IMNSService MNSService) 9 { 10 IMNSService = MNSService; 11 } 12 13 /// <summary> 14 /// 推送消息 15 /// </summary> 16 /// <typeparam name="T"></typeparam> 17 /// <param name="body">消息体</param> 18 /// <returns></returns> 19 public async Task PushAsync<T>(T body = default(T)) 20 { 21 var json = JsonConvert.SerializeObject(body); 22 await IMNSService.SendMessageAsync<T>(json); 23 } 24 25 /// <summary> 26 /// 订阅消息 27 /// </summary> 28 /// <typeparam name="T"></typeparam> 29 /// <param name="subscribeMethod">订阅的方法</param> 30 /// <returns></returns> 31 public async Task SubscribeAsync<T>(Func<T, Task> subscribeMethod) 32 { 33 while (true) 34 { 35 try 36 { 37 var mq = await IMNSService.ReceiveMessageAsync<T>(10); 38 Task.WaitAll(subscribeMethod.Invoke(mq.Item1)); 39 await IMNSService.DeleteMessageAsync<T>(mq.Item2); 40 } 41 catch (Exception) 42 { 43 //异常情况 不删除消息 44 } 45 } 46 } 47 }
笔者在EventBus 中分别实现了,消息推送和消息订阅这里只是初步简单的封装准备逐步进行优化和扩展,实现了推送和订阅后订阅需要在Startup中进行注册因此还需要实现,订阅的注册 代码如下
1 public static class AliYunMNSCollectionExtensions 2 { 3 /// <summary> 4 /// 自动注册所有阿里云事件监听 5 /// </summary> 6 public static IServiceCollection AutoHandler(this IServiceCollection services, string url, string accessKeyId, string accessKeySecret, params Type[] types) 7 { 8 services.Scan(scan => scan.FromAssembliesOf(types) 9 .AddClasses(classes => classes.AssignableTo(typeof(IMNSNotificationHandler<>))) 10 .AsImplementedInterfaces() 11 .WithTransientLifetime()); 12 13 services.AddTransient<IEventBus, EventBus>(); 14 services.AddSingleton<IMNSService>(new MNSService(url,accessKeyId,accessKeySecret)); 15 return services; 16 } 17 18 /// <summary> 19 /// 启动需要监听的消息 20 /// </summary> 21 /// <param name="app"></param> 22 /// <returns></returns> 23 public static IApplicationBuilder AddHandler<TEvent>(this IApplicationBuilder app) where TEvent : IMNSNotification 24 { 25 var handlers = app.ApplicationServices.GetService<IEventBus>(); 26 Task.Run(async () => 27 { 28 var eventbus = app.ApplicationServices.GetService<IMNSNotificationHandler<TEvent>>(); 29 await handlers.SubscribeAsync<TEvent>(eventbus.Handle); 30 }); 31 return app; 32 } 33 }
通过AddHandler<TEvent> 注册需要监听的消息下面看一下 使用案例
第一步:创建消息模型
1 public class TestMQ : IMNSNotification 2 { 3 public string Name { get; set; } 4 }
第二步:创建订阅消息的实现
1 public class TestMQHendler : IMNSNotificationHandler<TestMQ> 2 { 3 public async Task Handle(TestMQ notification) 4 { 5 var n = notification; 6 } 7 }
第三步:注册订阅
1 public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 2 { 3 loggerFactory.AddConsole(Configuration.GetSection("Logging")); 4 loggerFactory.AddDebug(); 5 app.AddHandler<TestMQ>(); 6 app.UseMvc(); 7 }
这里需要注意的是 如果没新增一个订阅消息 都需要 app.AddHandler<XXX>(); 来注册需要订阅的消息。
思考
通过简单的对于阿里云MNS的封装可以在业务中便捷的使用消息推送和消息订阅,由于时间的关系还有很多代码没有实现比如异常机制,日志的记录,配置的扩展等,笔者将逐步完善并且和大家分享如果大家有什么更好的提意或者建议欢迎留言。