前言
上篇《.net core實踐系列之短信服務-架構設計》介紹了我對短信服務的架構設計,同時針對場景解析了我的設計理念。本篇繼續講解Api服務的實現過程。
源碼地址:https://github.com/SkyChenSky/Sikiro.SMS
此服務會使用.NET Core WebApi進行搭建,.NET Core WebApi基礎原型就是RESTful風格,然而什么叫RESTful呢。
REST API簡介
REST
Representational State Transfer的縮寫,翻譯為“表現層狀態轉化”,是由Roy Thomas Fieding在他的博士論文《Architectural Styles and the Design of Network-based Software Architectures》中提出的一種架構思想。
而他的論文中提出了一個RESTful應用應該具備的幾點約束。
- 每個資源都應該有一個唯一的標識
- 每一個對象或資源都可以通過一個唯一的URI進行尋址,URI的結構應該是簡單的。
- 使用標准的方法來更改資源的狀態
- GET、POST、PUT、PATCH、DELETE
- Request和Response的自描述
- 資源多重表述
- URI所訪問的每個資源都可以使用不同的形式加以表示(XML或JSON)
- 無狀態的服務
- 不需要保存會話狀態(SESSION),資源本身就是天然的狀態,是需要被保存的。
RESTful
當某Web服務遵守了REST這些約束條件和原則,那么我們可以稱它設計風格就是 RESTful。
三特點
REST有三大特點:
- 資源(名詞)
- 動作(動詞)
- 表述(超文本)
資源
抽象的說他可以是音頻、也可以是視頻,更可以是訂單。更俗講其實就是實體,更接近我們平常說的“類(class)”。另外REST強調資源有唯一的URI。下面有對比
動作
主要動作:
- GET:檢索單個資源;
- POST:主要是創建資源,但是GET的參數長度受限,因此也可以用在復雜參數的檢索資源場景;
- PUT:更新資源所有屬性,也可以稱為替換資源;
- PATCH:更新資源部分屬性;
- DELETE:刪除資源;
表述
對於Request與Response的自描述,而表述方式有多種:XML、JSON等,強調HTTP通信的語義可見性。
對比
RPC
SMSApi.com/api/GetSMS
SMSApi.com/api/CreateSMS
傳統的接口設計面向過程的,每個動作有特定的URI。
REST
SMSApi.com/api/SMS GET
SMSApi.com/api/SMS POST
REST API每個資源只有唯一的URI,而資源可以有不同的動作執行相應的接口
RPC的更加傾向於面向過程,而RESTful則以面向對象的思想進行設計。
接口定義
回到我們的短信服務,以上面的三特點進行出發,SMS不需要由外部服務進行刪除、修改資源因此:
資源:SMS
動作:GET、POST
表述方式:我們約定Request、Response為JSON格式
/// <summary> /// 短信接口 /// </summary> [Route("api/[controller]")] [ApiController] public class SmsController : ControllerBase { private readonly SmsService _smsService; private readonly IBus _bus; public SmsController(SmsService smsService, IBus bus) { _smsService = smsService; _bus = bus; } /// <summary> /// 獲取一條短信記錄 /// </summary> /// <param name="id">主鍵</param> /// <returns></returns> [HttpGet("{id}")] public ActionResult<SmsModel> Get(string id) { if (string.IsNullOrEmpty(id)) return NotFound(); var smsService = _smsService.Get(id); return smsService.Sms; } /// <summary> /// 發送短信 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost] public ActionResult Post([FromBody] List<PostModel> model) { _smsService.Add(model.MapTo<List<PostModel>, List<AddSmsModel>>()); _smsService.SmsList.Where(a => a.TimeSendDateTime == null) .ToList().MapTo<List<SmsModel>, List<SmsQueueModel>>().ForEach(item => { _bus.Publish(item); }); return Ok(); } /// <summary> /// 查詢短信記錄 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost("_search")] public ActionResult<List<SmsModel>> Post([FromBody] SearchModel model) { _smsService.Search(model.MapTo<SearchModel, SearchSmsModel>()); return _smsService.SmsList; } }
功能描述
由上可見一共定義了三個接口
- GET http://localhost:port/api/sms/id 獲取一條短信記錄
- POST http://localhost:port/api/sms 發送短信
- POST http://localhost:port/api/sms/_search 查詢短信記錄
獲取一條短信記錄就不多解析了
查詢短信記錄
動作我使用了POST,有人會問檢索資源不是用GET么?對,但是GET的參數在URL里是受限的,因此在復雜參數的場景下應該選擇POST,然而我是模仿elasticsearch的復雜查詢時定義,添加多一個節點/_search申明此URI是做查詢的。
發送短信
此接口的實現邏輯主要兩件事,持久化到MongoDB,過濾出及時發送的短信記錄發送到RabbitMQ。
在持久化之前我做了一個分頁的動作,我們提供出去的接口,同一條短信內容支持N個手機號,但是不同的短信運營商的所支持一次性發送的手機數量是有限的。
開始實現時,我把分頁發送寫到隊列消費服務的發送短信邏輯里,但是這里有個問題,如果分頁后部分發送成功,部分發送失敗,那么這個聚合究竟以失敗還是成功的狀態標示呢?換句話來說我們無法保證聚合內的數據一致性。
因此我的做法就是優先在分頁成多個文檔存儲,那么就可以避免從數據庫取出后分頁導致部分成功、失敗。
public void Add(List<AddSmsModel> smsModels) { DateTime now = DateTime.Now; var smsModel = new List<SmsModel>(); foreach (var sms in smsModels) { var maxCount = _smsFactory.Create(sms.Type).MaxCount; sms.Mobiles = sms.Mobiles.Distinct().ToList(); var page = GetPageCount(sms.Mobiles.Count, maxCount); var index = 0; do { var toBeSendPhones = sms.Mobiles.Skip(index * maxCount).Take(maxCount).ToList(); smsModel.Add(new SmsModel { Content = sms.Content, CreateDateTime = now, Mobiles = toBeSendPhones, TimeSendDateTime = sms.TimeSendDateTime, Type = sms.Type }); index++; } while (index < page); } SmsList = smsModel; _mongoProxy.BatchAddAsync(SmsList); }
項目相關開源框架
EasyNetQ
EasyNetQ.DI.Microsoft
Sikiro.Nosql.Mongo
log4net
Mapster
EasyNetQ
這個開源框架是針對RabbitMQ.Client的封裝,隱藏了很多實現細節,簡化使用方式。並提供了多種IOC注入方式
源碼地址:https://github.com/EasyNetQ/EasyNetQ
Sikiro.Nosql.Mongo
這個是我自己針對mongo驅動的常用的基礎操作的封裝庫
源碼地址:https://github.com/SkyChenSky/Sikiro.Nosql.Mongo
Mapster
實體映射框架,看評測數據比AutoMapper等之類的效率要高,而且易用性也非常高。
https://github.com/MapsterMapper/Mapster
全局異常日志記錄
public class GolbalExceptionAttribute : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { if (!context.ExceptionHandled) { context.Exception.WriteToFile(); } base.OnException(context); } } public void ConfigureServices(IServiceCollection services) { services.AddMvc(option => { option.Filters.Add<GolbalExceptionAttribute>(); }) .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
LoggerHelper
上面的WriteToFile是我對Exception的擴展方法,使用了Log4Net日志框架對異常進行記錄,如果有需要也可以寫到mongodb或者elasticsearch
/// <summary> /// 日志幫助類 /// </summary> public static class LoggerHelper { private static readonly ILoggerRepository Repository = LogManager.CreateRepository("NETCoreRepository"); public static readonly ILog Log = LogManager.GetLogger(Repository.Name, typeof(LoggerHelper)); static LoggerHelper() { XmlConfigurator.Configure(Repository, new FileInfo("log4net.config")); } #region 文本日志 /// <summary> /// 文本日志 /// </summary> /// <param name="message"></param> /// <param name="ex"></param> public static void WriteToFile(this Exception ex, string message = null) { if (string.IsNullOrEmpty(message)) message = ex.Message; Log.Error(message, ex); } #endregion }
工具庫的封裝
框架與工具庫都是以庫的形式提供我們使用,而且都是可復用,但是他們區別在於:工具庫開箱即用,大多數以靜態方法提供調用,只調用少量甚至一個方法則完成使用。
而框架定義,為了實現某個軟件組件規范時,提供規范所要求之基礎功能的軟件產品,而他具有約束性、可復用性、規范性。他是一個半成品,可重寫。
因此為了簡化框架的使用,對常用設置、構建組合進行封裝,以一個擴展類或者幫助類的形式提供,簡化使用、增加可讀性。
Swagger的使用
Http協議的好處是輕量、跨平台,如此良好的靈活性然而需要接口描述對外暴露。Swagger是一個很好的選擇,不需要自己手寫文檔並提供后台管理界面,還可以測試,簡化不少工作。
我選擇了NSwag.AspNetCore開源組件,他的使用非常簡單。只需要兩步:
1.配置Swagger:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseSwaggerUiWithApiExplorer(settings => { settings.GeneratorSettings.DefaultPropertyNameHandling = PropertyNameHandling.CamelCase; settings.PostProcess = document => { document.Info.Version = "v1"; document.Info.Title = "Sikiro.SMS.API"; document.Info.Description = "短信服務API"; document.Info.TermsOfService = "None"; }; }); app.UseMvc(); }
2.設置站點項目
此設置為了把接口、參數注釋顯示到Swagger頁面
NSwag還有多個版本的UI選擇:
- UseSwaggerReDoc
- UseSwaggerUi
- UseSwaggerUi3
訪問http://localhost:port/swagger就可以見到API文檔了
部署
因為我公司還是使用windows server 2008。因此部署前應准備環境安裝包:
.NET Core 2.1.3 windows-hosting
安裝完成后重啟服務器,再把文件發布到服務器,編輯應用程序池為無托管代碼。就可以訪問了
結尾
本篇介紹Sikiro.SMS.Api的設計與實現,下篇會針對API調用進行封裝SDK。如果有任何建議,請在下方評論反饋給我。