ASP.NET Core中的響應(yīng)壓縮的實(shí)現(xiàn)
介紹#
響應(yīng)壓縮技術(shù)是目前Web開發(fā)領(lǐng)域中比較常用的技術(shù),在帶寬資源受限的情況下,使用壓縮技術(shù)是提升帶寬負(fù)載的首選方案。我們熟悉的Web服務(wù)器,比如IIS、Tomcat、Nginx、Apache等都可以使用壓縮技術(shù),常用的壓縮類型包括Brotli、Gzip、Deflate,它們對(duì)CSS、JavaScript、HTML、XML 和 JSON等類型的效果還是比較明顯的,但是也存在一定的限制對(duì)于圖片效果可能沒那么好,因?yàn)閳D片本身就是壓縮格式。其次,對(duì)于小于大約150-1000 字節(jié)的文件(具體取決于文件的內(nèi)容和壓縮的效率,壓縮小文件的開銷可能會(huì)產(chǎn)生比未壓縮文件更大的壓縮文件。在ASP.NET Core中我們可以使用非常簡(jiǎn)單的方式來使用響應(yīng)壓縮。
使用方式#
在ASP.NET Core中使用響應(yīng)壓縮的方式比較簡(jiǎn)單。首先,在ConfigureServices中添加services.AddResponseCompression注入響應(yīng)壓縮相關(guān)的設(shè)置,比如使用的壓縮類型、壓縮級(jí)別、壓縮目標(biāo)類型等。其次,在Configure添加app.UseResponseCompression攔截請(qǐng)求判斷是否需要壓縮,大致使用方式如下
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddResponseCompression(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseResponseCompression(); } }
如果需要自定義一些配置的話還可以手動(dòng)設(shè)置壓縮相關(guān)
public void ConfigureServices(IServiceCollection services) { services.AddResponseCompression(options => { //可以添加多種壓縮類型,程序會(huì)根據(jù)級(jí)別自動(dòng)獲取最優(yōu)方式 options.Providers.Add<BrotliCompressionProvider>(); options.Providers.Add<GzipCompressionProvider>(); //添加自定義壓縮策略 options.Providers.Add<MyCompressionProvider>(); //針對(duì)指定的MimeType來使用壓縮策略 options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( new[] { "application/json" }); }); //針對(duì)不同的壓縮類型,設(shè)置對(duì)應(yīng)的壓縮級(jí)別 services.Configure<GzipCompressionProviderOptions>(options => { //使用最快的方式進(jìn)行壓縮,單不一定是壓縮效果最好的方式 options.Level = CompressionLevel.Fastest; //不進(jìn)行壓縮操作 //options.Level = CompressionLevel.NoCompression; //即使需要耗費(fèi)很長(zhǎng)的時(shí)間,也要使用壓縮效果最好的方式 //options.Level = CompressionLevel.Optimal; }); }
關(guān)于響應(yīng)壓縮大致的工作方式就是,當(dāng)發(fā)起Http請(qǐng)求的時(shí)候在Request Header中添加Accept-Encoding:gzip或者其他你想要的壓縮類型,可以傳遞多個(gè)類型。服務(wù)端接收到請(qǐng)求獲取Accept-Encoding判斷是否支持該種類型的壓縮方式,如果支持則壓縮輸出內(nèi)容相關(guān)并且設(shè)置Content-Encoding為當(dāng)前使用的壓縮方式一起返回??蛻舳说玫巾憫?yīng)之后獲取Content-Encoding判斷服務(wù)端是否采用了壓縮技術(shù),并根據(jù)對(duì)應(yīng)的值判斷使用了哪種壓縮類型,然后使用對(duì)應(yīng)的解壓算法得到原始數(shù)據(jù)。
源碼探究#
通過上面的介紹,相信大家對(duì)ResponseCompression有了一定的了解,接下來我們通過查看源碼的方式了解一下它大致的工作原理。
AddResponseCompression#
首先我們來查看注入相關(guān)的代碼,具體代碼承載在ResponseCompressionServicesExtensions擴(kuò)展類中[點(diǎn)擊查看源碼👈]
public static class ResponseCompressionServicesExtensions { public static IServiceCollection AddResponseCompression(this IServiceCollection services) { services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>(); return services; } public static IServiceCollection AddResponseCompression(this IServiceCollection services, Action<ResponseCompressionOptions> configureOptions) { services.Configure(configureOptions); services.TryAddSingleton<IResponseCompressionProvider, ResponseCompressionProvider>(); return services; } }
主要就是注入ResponseCompressionProvider和ResponseCompressionOptions,首先我們來看關(guān)于ResponseCompressionOptions[點(diǎn)擊查看源碼👈]
public class ResponseCompressionOptions { // 設(shè)置需要壓縮的類型 public IEnumerable<string> MimeTypes { get; set; } // 設(shè)置不需要壓縮的類型 public IEnumerable<string> ExcludedMimeTypes { get; set; } // 是否開啟https支持 public bool EnableForHttps { get; set; } = false; // 壓縮類型集合 public CompressionProviderCollection Providers { get; } = new CompressionProviderCollection(); }
關(guān)于這個(gè)類就不做過多介紹了,比較簡(jiǎn)單。ResponseCompressionProvider是我們提供響應(yīng)壓縮算法的核心類,具體如何自動(dòng)選用壓縮算法都是由它提供的。這個(gè)類中的代碼比較多,我們就不逐個(gè)方法講解了,具體源碼可自行查閱[點(diǎn)擊查看源碼👈],首先我們先看ResponseCompressionProvider的構(gòu)造函數(shù)
public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseCompressionOptions> options) { var responseCompressionOptions = options.Value; _providers = responseCompressionOptions.Providers.ToArray(); //如果沒有設(shè)置壓縮類型默認(rèn)采用Br和Gzip壓縮算法 if (_providers.Length == 0) { _providers = new ICompressionProvider[] { new CompressionProviderFactory(typeof(BrotliCompressionProvider)), new CompressionProviderFactory(typeof(GzipCompressionProvider)), }; } //根據(jù)CompressionProviderFactory創(chuàng)建對(duì)應(yīng)的壓縮算法Provider比如GzipCompressionProvider for (var i = 0; i < _providers.Length; i++) { var factory = _providers[i] as CompressionProviderFactory; if (factory != null) { _providers[i] = factory.CreateInstance(services); } } //設(shè)置默認(rèn)的壓縮目標(biāo)類型默認(rèn)為text/plain、text/css、text/html、application/javascript、application/xml //text/xml、application/json、text/json、application/was var mimeTypes = responseCompressionOptions.MimeTypes; if (mimeTypes == null || !mimeTypes.Any()) { mimeTypes = ResponseCompressionDefaults.MimeTypes; } //將默認(rèn)MimeType放入HashSet _mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase); _excludedMimeTypes = new HashSet<string>( responseCompressionOptions.ExcludedMimeTypes ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase ); _enableForHttps = responseCompressionOptions.EnableForHttps; }
其中BrotliCompressionProvider、GzipCompressionProvider是具體提供壓縮方法的地方,咱們就看比較常用的Gzip的Provider的大致實(shí)現(xiàn)[點(diǎn)擊查看源碼👈]
public class GzipCompressionProvider : ICompressionProvider { public GzipCompressionProvider(IOptions<GzipCompressionProviderOptions> options) { Options = options.Value; } private GzipCompressionProviderOptions Options { get; } // 對(duì)應(yīng)的Encoding名稱 public string EncodingName { get; } = "gzip"; public bool SupportsFlush => true; // 核心代碼就是這句 將原始的輸出流轉(zhuǎn)換為壓縮的GZipStream // 我們?cè)O(shè)置的Level壓縮級(jí)別將決定壓縮的性能和質(zhì)量 public Stream CreateStream(Stream outputStream) => new GZipStream(outputStream, Options.Level, leaveOpen: true); }
關(guān)于ResponseCompressionProvider其他相關(guān)的方法咱們?cè)谥v解UseResponseCompression中間件的時(shí)候在具體看用到的方法,因?yàn)檫@個(gè)類是響應(yīng)壓縮的核心類,現(xiàn)在提前說了,到中間件使用的地方可能會(huì)忘記了。接下來我們就看UseResponseCompression的大致實(shí)現(xiàn)。
UseResponseCompression#
UseResponseCompression具體也就一個(gè)無(wú)參的擴(kuò)展方法,也比較簡(jiǎn)單,因?yàn)榕渲煤凸ぷ鞫加勺⑷氲牡胤酵瓿闪耍晕覀冎苯硬榭粗虚g件里的實(shí)現(xiàn),找到中間件位置ResponseCompressionMiddleware[點(diǎn)擊查看源碼👈]
public class ResponseCompressionMiddleware { private readonly RequestDelegate _next; private readonly IResponseCompressionProvider _provider; public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider) { _next = next; _provider = provider; } public async Task Invoke(HttpContext context) { //判斷是否包含Accept-Encoding頭信息,不包含直接大喊一聲"抬走下一個(gè)" if (!_provider.CheckRequestAcceptsCompression(context)) { await _next(context); return; } //獲取原始輸出Body var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>(); var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>(); //初始化響應(yīng)壓縮Body var compressionBody = new ResponseCompressionBody(context, _provider, originalBodyFeature); //設(shè)置成壓縮Body context.Features.Set<IHttpResponseBodyFeature>(compressionBody); context.Features.Set<IHttpsCompressionFeature>(compressionBody); try { await _next(context); await compressionBody.FinishCompressionAsync(); } finally { //恢復(fù)原始Body context.Features.Set(originalBodyFeature); context.Features.Set(originalCompressionFeature); } } }
這個(gè)中間件非常的簡(jiǎn)單,就是初始化了ResponseCompressionBody。看到這里你也許會(huì)好奇,并沒有觸發(fā)調(diào)用壓縮相關(guān)的任何代碼,ResponseCompressionBody也只是調(diào)用了FinishCompressionAsync都是和釋放相關(guān)的,不要著急我們來看ResponseCompressionBody類的結(jié)構(gòu)
internal class ResponseCompressionBody : Stream, IHttpResponseBodyFeature, IHttpsCompressionFeature { }
這個(gè)類實(shí)現(xiàn)了IHttpResponseBodyFeature,我們使用的Response.Body其實(shí)就是獲取的HttpResponseBodyFeature.Stream屬性。我們使用的Response.WriteAsync相關(guān)的方法,其實(shí)內(nèi)部都是在調(diào)用PipeWriter進(jìn)行寫操作,而PipeWriter就是來自HttpResponseBodyFeature.Writer屬性??梢源笾赂爬?,輸出相關(guān)的操作其核心都是在操作IHttpResponseBodyFeature。有興趣的可以自行查閱HttpResponse相關(guān)的源碼可以了解相關(guān)信息。所以我們的ResponseCompressionBody其實(shí)是重寫了輸出操作相關(guān)方法。也就是說,只要你調(diào)用了Response相關(guān)的Write或Body相關(guān)的,其實(shí)本質(zhì)都是在操作IHttpResponseBodyFeature,由于我們開啟了響應(yīng)輸出相關(guān)的中間件,所以會(huì)調(diào)用IHttpResponseBodyFeature的實(shí)現(xiàn)類ResponseCompressionBody相關(guān)的方法完成輸出。和我們常規(guī)理解的還是有偏差的,一般情況下我們認(rèn)為,其實(shí)只要針對(duì)輸出的Stream做操作就可以了,但是響應(yīng)壓縮中間件竟然重寫了輸出相關(guān)的操作。
了解到這個(gè)之后,相信大家就沒有太多疑問了。由于ResponseCompressionBody重寫了輸出相關(guān)的操作,代碼相對(duì)也比較多,就不逐一粘貼出來了,我們只查看設(shè)計(jì)到響應(yīng)壓縮核心相關(guān)的代碼,關(guān)于ResponseCompressionBody源碼相關(guān)的細(xì)節(jié)有興趣的可以自行查閱[點(diǎn)擊查看源碼👈],輸出的本質(zhì)其實(shí)都是在調(diào)用Write方法,我們就來查看一下Write方法相關(guān)的實(shí)現(xiàn)
public override void Write(byte[] buffer, int offset, int count) { //這是核心方法有關(guān)于壓縮相關(guān)的輸出都在這 OnWrite(); //_compressionStream初始化在OnWrite方法里 if (_compressionStream != null) { _compressionStream.Write(buffer, offset, count); if (_autoFlush) { _compressionStream.Flush(); } } else { _innerStream.Write(buffer, offset, count); } }
通過上面的代碼我們看到OnWrite方法是核心操作,我們直接查看OnWrite方法實(shí)現(xiàn)
private void OnWrite() { if (!_compressionChecked) { _compressionChecked = true; //判斷是否滿足執(zhí)行壓縮相關(guān)的邏輯 if (_provider.ShouldCompressResponse(_context)) { //匹配Vary頭信息對(duì)應(yīng)的值 var varyValues = _context.Response.Headers.GetCommaSeparatedValues(HeaderNames.Vary); var varyByAcceptEncoding = false; //判斷Vary的值是否為Accept-Encoding for (var i = 0; i < varyValues.Length; i++) { if (string.Equals(varyValues[i], HeaderNames.AcceptEncoding, StringComparison.OrdinalIgnoreCase)) { varyByAcceptEncoding = true; break; } } if (!varyByAcceptEncoding) { _context.Response.Headers.Append(HeaderNames.Vary, HeaderNames.AcceptEncoding); } //獲取最佳的ICompressionProvider即最佳的壓縮方式 var compressionProvider = ResolveCompressionProvider(); if (compressionProvider != null) { //設(shè)置選定的壓縮算法,放入Content-Encoding頭的值里 //客戶端可以通過Content-Encoding頭信息判斷服務(wù)端采用的哪種壓縮算法 _context.Response.Headers.Append(HeaderNames.ContentEncoding, compressionProvider.EncodingName); //進(jìn)行壓縮時(shí),將 Content-MD5 刪除該標(biāo)頭,因?yàn)檎膬?nèi)容已更改且哈希不再有效。 _context.Response.Headers.Remove(HeaderNames.ContentMD5); //進(jìn)行壓縮時(shí),將 Content-Length 刪除該標(biāo)頭,因?yàn)樵趯?duì)響應(yīng)進(jìn)行壓縮時(shí),正文內(nèi)容會(huì)發(fā)生更改。 _context.Response.Headers.Remove(HeaderNames.ContentLength); //返回壓縮相關(guān)輸出流 _compressionStream = compressionProvider.CreateStream(_innerStream); } } } } private ICompressionProvider ResolveCompressionProvider() { if (!_providerCreated) { _providerCreated = true; //調(diào)用ResponseCompressionProvider的方法返回最合適的壓縮算法 _compressionProvider = _provider.GetCompressionProvider(_context); } return _compressionProvider; }
從上面的邏輯我們可以看到,在執(zhí)行壓縮相關(guān)邏輯之前需要判斷是否滿足執(zhí)行壓縮相關(guān)的方法ShouldCompressResponse,這個(gè)方法是ResponseCompressionProvider里的方法,這里就不再粘貼代碼了,本來就是判斷邏輯我直接整理出來大致就是一下幾種情況
- 如果請(qǐng)求是Https的情況下,是否設(shè)置了允許Https情況下壓縮的設(shè)置,即ResponseCompressionOptions的EnableForHttps屬性設(shè)置
- Response.Head里不能包含Content-Range頭信息
- Response.Head里之前不能包含Content-Encoding頭信息
- Response.Head里之前必須要包含Content-Type頭信息
- 返回的MimeType里不能包含配置的不需要壓縮的類型,即ResponseCompressionOptions的ExcludedMimeTypes
- 返回的MimeType里需要包含配置的需要壓縮的類型,即ResponseCompressionOptions的MimeTypes
- 如果不滿足上面的兩種情況,返回的MimeType里包含*/*也可以執(zhí)行響應(yīng)壓縮
接下來我們查看ResponseCompressionProvider的GetCompressionProvider方法看它是如何確定返回哪一種壓縮類型的
public virtual ICompressionProvider GetCompressionProvider(HttpContext context) { var accept = context.Request.Headers[HeaderNames.AcceptEncoding]; //判斷請(qǐng)求頭是否包含Accept-Encoding信心 if (StringValues.IsNullOrEmpty(accept)) { Debug.Assert(false, "Duplicate check failed."); return null; } //獲取Accept-Encoding里的值,判斷是否包含gzip、br、identity等,并返回匹配信息 if (!StringWithQualityHeaderValue.TryParseList(accept, out var encodings) || !encodings.Any()) { return null; } //根據(jù)請(qǐng)求信息和設(shè)置信息計(jì)算匹配優(yōu)先級(jí) var candidates = new HashSet<ProviderCandidate>(); foreach (var encoding in encodings) { var encodingName = encoding.Value; //Quality涉及到一個(gè)非常復(fù)雜的算法,有興趣的可以自行查閱 var quality = encoding.Quality.GetValueOrDefault(1); //quality需大于0 if (quality < double.Epsilon) { continue; } //匹配請(qǐng)求頭里encodingName和設(shè)置的providers壓縮算法里EncodingName一致的算法 //從這里可以看出匹配的優(yōu)先級(jí)和注冊(cè)providers里的順序也有關(guān)系 for (int i = 0; i < _providers.Length; i++) { var provider = _providers[i]; if (StringSegment.Equals(provider.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase)) { candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); } } //如果請(qǐng)求頭里EncodingName是*的情況則在所有注冊(cè)的providers里進(jìn)行匹配 if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal)) { for (int i = 0; i < _providers.Length; i++) { var provider = _providers[i]; candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider)); } break; } //如果請(qǐng)求頭里EncodingName是identity的情況,則不對(duì)響應(yīng)進(jìn)行編碼 if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase)) { candidates.Add(new ProviderCandidate(encodingName.Value, quality, priority: int.MaxValue, provider: null)); } } ICompressionProvider selectedProvider = null; //如果匹配的只有一個(gè)則直接返回 if (candidates.Count <= 1) { selectedProvider = candidates.FirstOrDefault().Provider; } else { //如果匹配到多個(gè)則按照Quality倒序和Priority正序的負(fù)責(zé)匹配第一個(gè) selectedProvider = candidates .OrderByDescending(x => x.Quality) .ThenBy(x => x.Priority) .First().Provider; } //如果沒有匹配到selectedProvider或是identity的情況直接返回null if (selectedProvider == null) { return null; } return selectedProvider; }
通過以上的介紹我們可以大致了解到響應(yīng)壓縮的大致工作方式,簡(jiǎn)單總結(jié)一下
- 首先設(shè)置壓縮相關(guān)的算法類型或是壓縮目標(biāo)的MimeType
- 其次我們可以設(shè)置壓縮級(jí)別,這將決定壓縮的質(zhì)量和壓縮性能
- 通過響應(yīng)壓縮中間件,我們可以獲取到一個(gè)優(yōu)先級(jí)最高的壓縮算法進(jìn)行壓縮,這種情況主要是針對(duì)多種壓縮類型的情況。這個(gè)壓縮算法與內(nèi)部機(jī)制和注冊(cè)壓縮算法的順序都有一定的關(guān)系,最終會(huì)選擇權(quán)重最大的返回。
- 響應(yīng)壓縮中間件的核心工作類ResponseCompressionBody通過實(shí)現(xiàn)IHttpResponseBodyFeature,重寫輸出相關(guān)的方法實(shí)現(xiàn)對(duì)響應(yīng)的壓縮,不需要我們手動(dòng)進(jìn)行調(diào)用相關(guān)方法,而是替換掉默認(rèn)的輸出方式。只要設(shè)置了響應(yīng)壓縮,并且請(qǐng)求滿足響應(yīng)壓縮,那么有調(diào)用輸出的地方默認(rèn)都是執(zhí)行ResponseCompressionBody里壓縮相關(guān)的方法,而不是攔截具體的輸出進(jìn)行統(tǒng)一處理。至于為什么這么做,目前我還沒有理解到設(shè)計(jì)者真正的考慮。
總結(jié)#
在查看相關(guān)代碼之前,本來以為關(guān)于響應(yīng)壓縮相關(guān)的邏輯會(huì)非常的簡(jiǎn)單,看過了源碼才知道是自己想的太簡(jiǎn)單了。其中和自己想法出入最大的莫過于在ResponseCompressionMiddleware中間件里,本以為是通過統(tǒng)一攔截輸出流來進(jìn)行壓縮操作,沒想到是對(duì)整體輸出操作進(jìn)行重寫。因?yàn)樵谥拔覀兪褂肁sp.Net相關(guān)框架的時(shí)候是統(tǒng)一寫Filter或者HttpModule進(jìn)行處理的,所以存在思維定式??赡苁茿sp.Net Core設(shè)計(jì)者有更深層次的理解,可能是我理解的還不夠徹底,不能夠體會(huì)這樣做的好處究竟是什么,如果你有更好的理解或則答案歡迎在評(píng)論區(qū)里留言解惑。
到此這篇關(guān)于ASP.NET Core中的響應(yīng)壓縮的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)ASP.NET Core 響應(yīng)壓縮內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- ASP.NET Core 文件響應(yīng)壓縮的常見使用誤區(qū)
- asp.net core為IHttpClientFactory添加動(dòng)態(tài)命名配置
- 如何在ASP.NET Core中使用HttpClientFactory
- 在ASP.NET Core中用HttpClient發(fā)送POST, PUT和DELETE請(qǐng)求
- .NET CORE HttpClient的使用方法
- .NET Core使用HttpClient進(jìn)行表單提交時(shí)遇到的問題
- .Net Core下HTTP請(qǐng)求IHttpClientFactory示例詳解
- Asp.Net Core2.1前后使用HttpClient的兩種方式
- ASP.NET Core針對(duì)一個(gè)使用HttpClient對(duì)象的類編寫單元測(cè)試詳解
- .NET Core中HttpClient的正確打開方式
- .NET Core中使用HttpClient的正確姿勢(shì)
- .NET Core 2.1中HttpClientFactory的最佳實(shí)踐記錄
- .Net Core HttpClient處理響應(yīng)壓縮詳細(xì)
相關(guān)文章
VB.NET調(diào)用MySQL存儲(chǔ)過程并獲得返回值的方法
這篇文章主要介紹了VB.NET調(diào)用MySQL存儲(chǔ)過程并獲得返回值的方法,涉及基于VB.NET操作MySQL數(shù)據(jù)庫(kù)的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07關(guān)于服務(wù)器或虛擬主機(jī)不支持 AjaxPro 的問題終極解決方法
asp.net的網(wǎng)站,訪問時(shí)提示不支持 AjaxPro,那就因?yàn)檎`刪的映射導(dǎo)致,可以通過下面的方法解決2012-03-03.NET?如何使用?OpenTelemetry?metrics?監(jiān)控應(yīng)用程序指標(biāo)
這篇文章主要介紹了.NET?使用?OpenTelemetry?metrics?監(jiān)控應(yīng)用程序指標(biāo),通過代碼演示了如何通過 OpenTelemetry 把 Metrics 的數(shù)據(jù)發(fā)送到 Prometheus 里進(jìn)行查詢與展示,然后又演示了自定義相關(guān)指標(biāo)來滿足業(yè)務(wù)數(shù)據(jù)指標(biāo)的監(jiān)控,需要的朋友可以參考下2024-06-06HttpWebRequest的常見錯(cuò)誤使用TcpClient可避免
有時(shí)使用HttpWebRequest對(duì)象會(huì)出現(xiàn)錯(cuò)誤有三種服務(wù)器提交了協(xié)議沖突/基礎(chǔ)連接已經(jīng)關(guān)閉:連接被意外關(guān)閉/無(wú)法發(fā)送具有此謂詞類型的內(nèi)容正文,感興趣的朋友可以參考下本文2013-02-02Asp.net中將Word文件轉(zhuǎn)換成HTML的方法
這篇文章主要介紹了Asp.net中將Word文件轉(zhuǎn)換成HTML的方法,需要的朋友可以參考下2014-08-08在ASP.NET Core中實(shí)現(xiàn)一個(gè)Token base的身份認(rèn)證實(shí)例
以前在web端的身份認(rèn)證都是基于Cookie | Session的身份認(rèn)證,本篇文章主要介紹了在ASP.NET Core中實(shí)現(xiàn)一個(gè)Token base的身份認(rèn)證實(shí)例,有興趣的可以了解一下。2016-12-12