前言
WebAPi作為接口請求的一種服務,當我們請求該服務時我們目标是需要快速擷取該服務的資料響應,這種情況在大型項目中尤為常見,此時迫切需要提高WebAPi的響應機制,當然也少不了前端需要作出的努力,這裡我們隻講述在大小型項目中如何利用背景邏輯盡可能最大限度提高WebAPi性能,我們從以下幾個方面來進行闡述。
性能提升一:JSON序列化器(Jil)
在.NET裡面預設的序列化器是JavaScriptSrializer,都懂的,性能實在是差,後來出現了Json.NET,以至于在目前建立項目時預設用的序列化器是Json.NET,它被.NET開發者所廣泛使用,它的強大和性能毋庸置疑,以至于現在Json.NET版本已經更新到9.0版本,但是在大型項目中一旦資料量巨大時,此時用Json.NET來序列化資料會略慢,這時我們就可以嘗試用Jil,它裡面的APi也足夠我們用,我們講述幾個常用的APi并一起對比Json.NET來看看:
序列化對比
在Json.NET中是這樣序列化的
JsonConvert.SerializeObject(obj)
而在Jil中序列化資料是這樣的
JSON.Serialize(obj)
此時對于Jil序列化資料傳回的字元串形式有兩種
(1)直接接收
object obj = new { Foo = 123, Bar = "abc" }; string s = Jil.JSON.Serialize(obj)
(2)傳遞給StringWriter來接收
var obj = new { Foo = 123, Bar = "abc" }; var t = new StringWriter(); JSON.SerializeDynamic(obj, t);
上述說到對于資料量巨大時用Jil其效率高于Json.NET,下來我們來驗證序列化10000條資料
序列化類:
public class Person { public int Id { get; set; } public string Name { get; set; } }
測試資料:
var list = new List<Person>(); for (int i = 0; i < 10000; i++) { list.Add(new Person(){ Id = i }); } var stop = new Stopwatch(); stop.Start(); var jil = SerializeList(list); Console.WriteLine(stop.ElapsedMilliseconds); stop.Stop(); var stop1 = new Stopwatch(); stop1.Start(); var json = JsonConvert.SerializeObject(list); Console.WriteLine(stop1.ElapsedMilliseconds); stop1.Stop();
Jil序列化封裝:
private static string SerializeList(List<Person> list) { using (var output = new StringWriter()) { JSON.Serialize( list, output ); return output.ToString(); } }
我們來看看測試用例:

此時利用Json.NET序列化資料明顯優于Jil,但序列化資料為10萬條數,Jil所耗時間會接近于Json.NET,當資料高于100萬條數時這個時候就可以看出明顯的效果,如下:
此時Jil序列化資料不到1秒,而利用Json.NET則需要足足接近3秒。
測試用例更新:
當将代碼進行如下修改時,少量資料也是優于Json.NET,資料量越大性能越明顯,感謝園友【calvinK】提醒:
var list = new List<int>(); for (int i = 0; i < 10000; i++) { list.Add(i); } var stop = new Stopwatch(); stop.Start(); for (var i = 0; i < 1000; i++) { var jil = SerializeList(list); } Console.WriteLine(stop.ElapsedMilliseconds); stop.Stop(); var stop1 = new Stopwatch(); stop1.Start(); for (var i = 0; i < 1000; i++) { var json = JsonConvert.SerializeObject(list); } Console.WriteLine(stop1.ElapsedMilliseconds); stop1.Stop();
結果如下:
關于Jil的序列化還有一種則是利用JSON.SerializeDynamic來序列化那些在編譯時期無法預測的類型。 至于反序列化也是和其序列化一一對應。
下面我們繼續來看看Jil的其他特性。若在視圖上渲染那些我們需要的資料,而對于實體中不必要用到的字段我們就需要進行過濾,此時我們用到Jil中的忽略屬性。
[JilDirective(Ignore = true)]
我們來看看:
public class Person { [JilDirective(Ignore = true)] public int Id { get; set; } public int Name { get; set; } }
var jil = SerializeList(new Person() { Id = 1, Name = 123 } ); Console.WriteLine(jil);
另外在Jil中最重要的屬性則是Options,該屬性用來配置傳回的日期格式以及其他配置,若未用其屬性預設利用Json.NET傳回如【\/Date(143546676)\/】,我們來看下:
var jil = SerializeList(new Person() { Id = 1, Name = "123", Time = DateTime.Now });
進行如下設定:
JSON.Serialize( p, output, new Options(dateFormat: DateTimeFormat.ISO8601) );
有關序列化繼承類時我們同樣需要進行如下設定,否則無法進行序列化
new Options(dateFormat: DateTimeFormat.ISO8601, includeInherited: true)
Jil的性能絕對優于Json.NET,Jil一直在追求序列化的速度是以在更多可用的APi可能少于Json.NET或者說沒有Json.NET靈活,但是足以滿足我們的要求。
性能提升二:壓縮(Compress)
壓縮方式(1) 【IIS設定】
啟動IIS動态内容壓縮
壓縮方式(2)【DotNetZip】
利用現成的輪子,下載下傳程式包【DotNetZip】即可,此時我們則需要在執行方法完畢後來進行内容的壓縮即可,是以我們需要重寫【 ActionFilterAttribute 】過濾器,在此基礎上進行我們的壓縮操作。如下:
public class DeflateCompressionAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext actionContext) { var content = actionContext.Response.Content; var bytes = content == null ? null : content.ReadAsByteArrayAsync().Result; var compressContent = bytes == null ? new byte[0] : CompressionHelper.DeflateByte(bytes); actionContext.Response.Content = new ByteArrayContent(compressContent); actionContext.Response.Content.Headers.Remove("Content-Type"); if (string.Equals(actionContext.Request.Headers.AcceptEncoding.First().Value, "deflate")) actionContext.Response.Content.Headers.Add("Content-encoding", "deflate"); else actionContext.Response.Content.Headers.Add("Content-encoding", "gzip"); actionContext.Response.Content.Headers.Add("Content-Type", "application/json;charset=utf-8"); base.OnActionExecuted(actionContext); } }
利用DotNetZip進行快速壓縮:
public class CompressionHelper { public static byte[] DeflateByte(byte[] str) { if (str == null) { return null; } using (var output = new MemoryStream()) { using (var compressor = new Ionic.Zlib.GZipStream( output, Ionic.Zlib.CompressionMode.Compress, Ionic.Zlib.CompressionLevel.BestSpeed)) { compressor.Write(str, 0, str.Length); } return output.ToArray(); } } }
我們來對比看一下未進行内容壓縮前後結果響應的時間以及内容長度,給出如下測試類:
[HttpGet] [DeflateCompression] public async Task<IHttpActionResult> GetZipData() { Dictionary<object, object> dict = new Dictionary<object, object>(); List<Employee> li = new List<Employee>(); li.Add(new Employee { Id = "2", Name = "xpy0928", Email = "[email protected]" }); li.Add(new Employee { Id = "3", Name = "tom", Email = "[email protected]" }); li.Add(new Employee { Id = "4", Name = "jim", Email = "[email protected]" }); li.Add(new Employee { Id = "5", Name = "tony",Email = "[email protected]" }); dict.Add("Details", li);return Ok(dict); }
結果運作錯誤:
這裡應該是序列化出現問題,在有些浏覽器傳回的XML資料,我用的是搜狗浏覽器,之前學習WebAPi時其傳回的就是XML資料,我們試着将其傳回為Json資料看看。
var formatters = config.Formatters.Where(formatter => formatter.SupportedMediaTypes.Where(media => media.MediaType.ToString() == "application/xml" || media.MediaType.ToString() == "text/html").Count() > 0) //找到請求頭資訊中的媒體類型 .ToList(); foreach (var match in formatters) { config.Formatters.Remove(match); }
我們未将其壓縮後響應的長度如下所示:
壓縮後結果明顯得到提升
接下來我們自定義用.NET内置的壓縮模式來實作看看
壓縮方式(3)【自定義實作】
既然響應的内容是通過HttpContent,我們則需要在重寫過濾器ActionFilterAttribute的基礎上來實作重寫HttpContent,最終根據擷取到浏覽器支援的壓縮格式對資料進行壓縮并寫入到響應流中即可。
public class CompressContent : HttpContent { private readonly string _encodingType; private readonly HttpContent _originalContent; public CompressContent(HttpContent content, string encodingType = "gzip") { _originalContent = content; _encodingType = encodingType.ToLowerInvariant(); Headers.ContentEncoding.Add(encodingType); } protected override bool TryComputeLength(out long length) { length = -1; return false; } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { Stream compressStream = null; switch (_encodingType) { case "gzip": compressStream = new GZipStream(stream, CompressionMode.Compress, true); break; case "deflate": compressStream = new DeflateStream(stream, CompressionMode.Compress, true); break; default: compressStream = stream; break; } return _originalContent.CopyToAsync(compressStream).ContinueWith(tsk => { if (compressStream != null) { compressStream.Dispose(); } }); } }
重寫過濾器特性
public class CompressContentAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext context) { var acceptedEncoding = context.Response.RequestMessage.Headers.AcceptEncoding.First().Value; if (!acceptedEncoding.Equals("gzip", StringComparison.InvariantCultureIgnoreCase) && !acceptedEncoding.Equals("deflate", StringComparison.InvariantCultureIgnoreCase)) { return; } context.Response.Content = new CompressContent(context.Response.Content, acceptedEncoding); } }
關于其響應結果對比則不再叙述,和上述利用DotNetZip結果一緻。
當寫壓縮内容時,我發現一個問題,産生了疑問, context.Response.Content.Headers 和 context.Response.Headers 為何響應中有兩個頭Headers呢?,沒有去細究這個問題,大概說說個人想法。
context.Response.Content.Headers和context.Response.Headers有什麼不同呢?
我們看看context.Response.Headers中的定義,其摘要如下:
// 摘要: // Gets a value that indicates if the HTTP response was successful. // // 傳回結果: // Returns System.Boolean.A value that indicates if the HTTP response was successful. // true if System.Net.Http.HttpResponseMessage.StatusCode was in the range 200-299; // otherwise false.
而context.Response.Content.Headers中的定義,其摘要如下:
// 摘要: // Gets the HTTP content headers as defined in RFC 2616. // // 傳回結果: // Returns System.Net.Http.Headers.HttpContentHeaders.The content headers as // defined in RFC 2616.
對于Content.Headers中的Headers的定義是基于RFC 2616即Http規範,想必這麼做的目的是将Http規範隔離開來,我們能夠友善我們實作自定義代碼或者設定有關響應頭資訊最終直接寫入到Http的響應流中。我們更多的是操作Content.Headers是以将其差別開來,或許是出于此目的吧,有知道的園友可以給出合理的解釋,這裡隻是我的個人揣測。
性能提升三:緩存(Cache:粒度比較大)
緩存大概是談的最多的話題,當然也有大量的緩存元件供我們使用,這裡隻是就比較大的粒度來談論這個問題,對于一些小的項目還是有一點作用,大的則另當别論。
當我們進行請求可以檢視響應頭中會有這樣一個字段【Cache-Control】,如果我們未做任何處理當然則是其值為【no-cache】。在任何時期都不會進行緩存,都會重新進行請求資料。這個屬性裡面對應的值還有private/public、must-revalidate,當我們未指定max-age的值時且設定值為private、no-cache、must-revalidate此時的請求都會去伺服器擷取資料。這裡我們首先了解下關于Http協定的基本知識。
【1】若設定為private,則其不能共享緩存意思則是不會在本地緩存頁面即對于代理伺服器而言不會複制一份,而如果對于使用者而言其緩存更加是私有的,隻是對于個人而言,使用者之間的緩存互相獨立,互不共享。若為public則說明每個使用者都可以共享這一塊緩存。對于這二者打個比方對于部落格園的推送的新聞是公開的,則可以設定為public共享緩存,充分利用緩存。
【2】max-age則是緩存的過期時間,在某一段時間内不會去重新請求從伺服器擷取資料,直接在本地浏覽器緩存中擷取。
【3】must-revalidate從字面意思來看則是必須重新驗證,也就是對于過期的資料進行重新擷取新的資料,那麼它到底什麼時候用呢?歸根結底一句話:must-revalidate主要與max-age有關,當設定了max-age時,同時也設定了must-revalidate,等緩存過期後,此時must-revalidate則會告訴伺服器來擷取最新的資料。也就是說當設定max-age = 0,must-revalidate = true時可以說是與no-cache = true等同。
下面我們來進行緩存控制:
public class CacheFilterAttribute : ActionFilterAttribute { public int CacheTimeDuration { get; set; } public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue { MaxAge = TimeSpan.FromSeconds(CacheTimeDuration), MustRevalidate = true, Public = true }; } }
添加緩存過濾特性:
[HttpGet] [CompressContent] [CacheFilter(CacheTimeDuration = 100)] public async Task<IHttpActionResult> GetZipData() { var sw = new Stopwatch(); sw.Start(); Dictionary<object, object> dict = new Dictionary<object, object>(); List<Employee> li = new List<Employee>(); li.Add(new Employee { Id = "2", Name = "xpy0928", Email = "[email protected]" }); li.Add(new Employee { Id = "3", Name = "tom", Email = "[email protected]" }); li.Add(new Employee { Id = "4", Name = "jim", Email = "[email protected]" }); li.Add(new Employee { Id = "5", Name = "tony", Email = "[email protected]" }); sw.Stop(); dict.Add("Details", li); dict.Add("Time", sw.Elapsed.Milliseconds); return Ok(dict); }
性能提升四:async/await(異步方法)
當在大型項目中會出現并發現象,常見的情況例如注冊,此時有若幹個使用者同時在注冊時,則會導緻目前請求阻塞并且頁面一直無響應最終導緻伺服器崩潰,為了解決這樣的問題我們需要用到異步方法,讓多個請求過來時,線程池配置設定足夠的線程來處理多個請求,提高線程池的使用率 !如下:
public async Task<IHttpActionResult> Register(Employee model) { var result = await UserManager.CreateAsync(model); return Ok(result); }
總結
本節我們從以上幾方面講述了在大小項目中如何盡可能最大限度來提高WebAPi的性能,使資料響應更加迅速,或許還有其他更好的解決方案,至少以上所述也可以作為一種參考,WebAPi一個很輕量的架構,你值得擁有,see u。
所有的選擇不過是為了下一次選擇做準備