一步步完成“迷你版” 的ASP.NET Core框架

来源:https://www.cnblogs.com/qtqs/archive/2019/05/10/10839134.html
-Advertisement-
Play Games

一 前言 Artech 分享了 "200行代碼,7個對象——讓你瞭解ASP.NET Core框架的本質" 。 用一個極簡的模擬框架闡述了ASP.NET Core框架最為核心的部分。 這裡一步步來完成這個迷你框架。 二 先來一段簡單的代碼 這段代碼非常簡單,啟動伺服器並監聽本地5000埠和處理請求。 ...


一 前言

Artech 分享了 200行代碼,7個對象——讓你瞭解ASP.NET Core框架的本質 。 用一個極簡的模擬框架闡述了ASP.NET Core框架最為核心的部分。

這裡一步步來完成這個迷你框架。

二 先來一段簡單的代碼

這段代碼非常簡單,啟動伺服器並監聽本地5000埠和處理請求。

        static async Task Main(string[] args)
        {
            HttpListener httpListener = new HttpListener();
            httpListener.Prefixes.Add("http://localhost:5000/");
            httpListener.Start();
            while (true)
            {
                var context = await httpListener.GetContextAsync();
                await context.Response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("hello world"));
                context.Response.Close();
            }
        }

現在要分離伺服器(Server) 和 請求處理(handle),那麼一個簡單設計架構就出來了 :

Pipeline =Server + HttpHandler

三 處理器的抽象

處理器要從請求(Request)中獲取數據,和定製響應(Response)的數據。
可以想到我們的處理器的處理方法應該是這樣的:

  Task Handle(/*HttpRequest HttpResponse*/);

它可以處理請求和響應,由於處理可以是同步或者非同步的,所以返回Task。

很容易想到要封裝http請求和響應,封裝成一個上下文(Context) 供處理器使用(這樣的好處,處理器需要的其他數據也可以封裝在這裡,統一使用),所以要開始封裝HttpContext。

封裝HttpContext

 public class HttpRequest
    {
        public Uri Url  { get; }
        public NameValueCollection Headers { get; }
        public Stream Body { get; }
    }

    public class HttpResponse
    {
        public NameValueCollection Headers { get; }
        public Stream Body { get; }

        public int StatusCode { get; set; }
    }

    public class HttpContext
    {
        public HttpRequest Request { get; set; }

        public HttpResponse Response { get; set; }
    }

要支持不同的伺服器,則不同的伺服器都要提供HttpContext,這樣有了新的難題:伺服器和HttpContext之間的適配
現階段的HttpContext包含HttpRequest和HttpResponse,請求和響應的數據都是要伺服器(Server)提供的。
可以定義介面,讓不同的伺服器提供實現介面的實例:

    public interface IHttpRequestFeature
    {
        Uri Url { get; }

        NameValueCollection Headers { get; }

        Stream Body { get; }
    }
    public interface IHttpResponseFeature
    {
        int StatusCode { get; set; }

        NameValueCollection Headers { get; }

        Stream Body { get; }
    }

為了方便管理伺服器和HttpContext之間的適配,定義一個功能的集合,通過類型可以找到伺服器提供的實例

   public interface IFeatureCollection:IDictionary<Type,object>
    {
    }

    public static partial class Extensions
    {
        public static T Get<T>(this IFeatureCollection features)
        {
            return features.TryGetValue(typeof(T), out var value) ? (T)value : default;
        }

        public static IFeatureCollection Set<T>(this IFeatureCollection features,T feature)
        {
            features[typeof(T)] = feature;
            return features;
        }
    }

    public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection { }

接下來修改HttpContext,完成適配

    public class HttpContext
    {
       
        public HttpContext(IFeatureCollection features)
        {
            Request = new HttpRequest(features);
            Response = new HttpResponse(features);
        }
        public HttpRequest Request { get; set; }

        public HttpResponse Response { get; set; }

    }
    
    public class HttpRequest
    {
        private readonly IHttpRequestFeature _httpRequestFeature;
        public HttpRequest(IFeatureCollection features)
        {
            _httpRequestFeature = features.Get<IHttpRequestFeature>();
        }
       public Uri Url => _httpRequestFeature.Url;

       public NameValueCollection Headers => _httpRequestFeature.Headers;

       public Stream Body => _httpRequestFeature.Body;
    }

    public class HttpResponse
    {
        private readonly IHttpResponseFeature _httpResponseFeature;
        public HttpResponse(IFeatureCollection features)
        {
            _httpResponseFeature = features.Get<IHttpResponseFeature>();
        }
       public int StatusCode
        {
            get => _httpResponseFeature.StatusCode;
            set => _httpResponseFeature.StatusCode = value;
        }

       public NameValueCollection Headers => _httpResponseFeature.Headers;

       public  Stream Body => _httpResponseFeature.Body;
        
    }
    public static partial class Extensions
    {
        public static Task WriteAsync(this HttpResponse response,string content)
        {
            var buffer = Encoding.UTF8.GetBytes(content);
            return response.Body.WriteAsync(buffer, 0, buffer.Length);
        }
    }

定義處理器

封裝好了HttpContext,終於可以回過頭來看看處理器。
處理器的處理方法現在應該是這樣:

  Task Handle(HttpContext context);

接下來就是怎麼定義這個處理器了。
起碼有兩種方式:
1、定義一個介面:

    public interface IHttpHandler
    {
        Task Handle(HttpContext context);
    }

2、定義一個委托類型

public delegate Task RequestDelegate(HttpContext context);

兩種方式,本質上沒啥區別,委托代碼方式更靈活,不用實現一個介面,還符合鴨子模型。
處理器就選用委托類型。
定義了處理器,接下來看看伺服器

四 伺服器的抽象

伺服器應該有一個開始方法,傳入處理器,並執行。
伺服器抽象如下:

    public interface IServer
    {
        Task StartAsync(RequestDelegate handler);
    }

定義一個HttpListener的伺服器來實現IServer,由於HttpListener的伺服器需要提供HttpContext所需的數據,所以先定義HttpListenerFeature

    public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
    {

        private readonly HttpListenerContext _context;

        public HttpListenerFeature(HttpListenerContext context) => _context = context;
        Uri IHttpRequestFeature.Url => _context.Request.Url;

        NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers;

        NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers;

        Stream IHttpRequestFeature.Body => _context.Request.InputStream;

        Stream IHttpResponseFeature.Body => _context.Response.OutputStream;

        int IHttpResponseFeature.StatusCode
        {
            get => _context.Response.StatusCode;
            set => _context.Response.StatusCode = value;
        }
    }

定義HttpListener伺服器

  public class HttpListenerServer : IServer
    {
        private readonly HttpListener _httpListener;

        private readonly string[] _urls;

        public HttpListenerServer(params string[] urls)
        {
            _httpListener = new HttpListener();
            _urls = urls.Any() ? urls : new string[] { "http://localhost:5000/" };
        }
        public async Task StartAsync(RequestDelegate handler)
        {
            Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));

            _httpListener.Start();
            Console.WriteLine($"伺服器{typeof(HttpListenerServer).Name} 開啟,開始監聽:{string.Join(";", _urls)}");
            while (true)
            {
                var listtenerContext = await _httpListener.GetContextAsync();
                var feature = new HttpListenerFeature(listtenerContext);

                var features = new FeatureCollection()
                    .Set<IHttpRequestFeature>(feature)
                    .Set<IHttpResponseFeature>(feature);
                var httpContext = new HttpContext(features);

                await handler(httpContext);

                listtenerContext.Response.Close();
            }
        }
    }

修改Main方法運行測試

        static async Task Main(string[] args)
        {
            IServer server = new HttpListenerServer();
            async Task FooBar(HttpContext httpContext)
            {
                await httpContext.Response.WriteAsync("fooBar");
            }
            await server.StartAsync(FooBar); 
        }

運行結果如下:

至此,完成了伺服器和處理器的抽象。
接下來單看處理器,所有的處理邏輯都集合在一個方法中,理想的方式是有多個處理器進行處理,比如處理器A處理完,則接著B處理器進行處理……
那麼就要管理多個處理器之間的連接方式。

五 中間件

中間件的定義

假設有三個處理器A,B,C
框架要實現:A處理器開始處理,A處理完成之後,B處理器開始處理,B處理完成之後,C處理器開始處理。

引入中間件來完成處理器的連接。

中間件的要實現的功能很簡單:

  • 傳入下一個要執行的處理器;
  • 在中間件中的處理器里,記住下一個要執行的處理器;
  • 返回中間件中的處理器,供其他中間件使用。
    所以中間件應該是這樣的:
 //偽代碼
 處理器  Middleware(傳入下一個要執行的處理器)
 {
     return 處理器
     {
         //處理器的邏輯
         下一個要執行的處理器在這裡執行
     }
 }

舉個例子,現在有三個中間件FooMiddleware,BarMiddleware,BazMiddleware,分別對應的處理器為A,B,C
要保證 處理器的處理順序為 A->B->C
則先要執行 最後一個BazMiddleware,傳入“完成處理器” 返回 處理器C
然後把處理器C 傳入 BarMiddleware ,返回處理器B,依次類推。

//偽代碼
var middlewares=new []{FooMiddleware,BarMiddleware,BazMiddleware};
middlewares.Reverse();
var  next=完成的處理器;
foreach(var middleware in middlewares)
{
    next=  middleware(next);
}
//最後的next,就是最終要傳入IServer 中的處理器

模擬運行時的偽代碼:

 //傳入完成處理器,返回處理器C
 處理器 BazMiddleware(完成處理器)
 { 
     return 處理器C
            {    
               //處理器C的處理代碼
               完成處理器
            };
 }
 //傳入處理器C,返回處理器B
  處理器  BarMiddleware(處理器C)
 { 
     return 處理器B
            {    
               //處理器B的處理代碼
               執行處理器C
            };
 }
  //傳入處理器B,返回處理器A
  處理器  FooMiddleware(處理器B)
 { 
     return 處理器A
            {    
               //處理器A的處理代碼
               執行處理器B
            };
 }

 

這樣當處理器A執行的時候,會先執行自身的代碼,然後執行處理器B,處理器B執行的時候,先執行自身的代碼,然後執行處理器C,依次類推。

所以,中間件的方法應該是下麵這樣的:

RequestDelegate DoMiddleware(RequestDelegate next);

中間件的管理

要管理中間件,就要提供註冊中間件的方法和最終構建出RequestDelegate的方法。
定義註冊中間件和構建處理器的介面: IApplicationBuilder

    public interface IApplicationBuilder
    {
        IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);

        RequestDelegate Build();
    }

實現:

    public class ApplicationBuilder : IApplicationBuilder
    {
        private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>();
        public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
        {
            _middlewares.Add(middleware);
            return this;
        }
        public RequestDelegate Build()
        {
            _middlewares.Reverse();
            RequestDelegate next = context => { context.Response.StatusCode = 404; return Task.CompletedTask; };
            foreach (var middleware in _middlewares)
            {
                next = middleware(next);
            }
            return next;
        }       
    }

定義中間件測試

在Program 類里定義三個中間件:

        static RequestDelegate FooMiddleware(RequestDelegate next)
        {
            return async context =>
            {
                await context.Response.WriteAsync("foo=>");
                await next(context);
            };
        }
        static RequestDelegate BarMiddleware(RequestDelegate next)
        {
            return async context =>
            {
                await context.Response.WriteAsync("bar=>");
                await next(context);
            };
        }
        static RequestDelegate BazMiddleware(RequestDelegate next)
        {
            return async context =>
            {
                await context.Response.WriteAsync("baz=>");
                await next(context);
            };
        }

修改Main方法測試運行

        static async Task Main(string[] args)
        {
            IServer server = new HttpListenerServer();

            var handler = new ApplicationBuilder()
                .Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware)
                .Build();
            await server.StartAsync(handler); 
        }

運行結果如下:

六 管理伺服器和處理器

為了管理伺服器和處理器之間的關係 抽象出web宿主
如下:

 public interface IWebHost
    {
        Task StartAsync();
    }

    public class WebHost : IWebHost
    {
        private readonly IServer _server;

        private readonly RequestDelegate _handler;

        public WebHost(IServer server,RequestDelegate handler)
        {
            _server = server;
            _handler = handler;

        }
        public Task StartAsync()
        {
            return _server.StartAsync(_handler);
        }
    }

Main方法可以改一下測試

        static async Task Main(string[] args)
        {
            IServer server = new HttpListenerServer();

            var handler = new ApplicationBuilder()
                .Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware)
                .Build();

            IWebHost webHost = new WebHost(server, handler);
            await webHost.StartAsync();
        }

要構建WebHost,需要知道用哪個伺服器,和配置了哪些中間件,最後可以構建出WebHost
代碼如下:

 public interface IWebHostBuilder
    {
        IWebHostBuilder UseServer(IServer server);

        IWebHostBuilder Configure(Action<IApplicationBuilder> configure);

        IWebHost Build();
    }

    public class WebHostBuilder : IWebHostBuilder
    {
        private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>();
        private IServer _server;
        public IWebHost Build()
        {
            //所有的中間件都註冊在builder上
            var builder = new ApplicationBuilder();
            foreach (var config in _configures)
            {
                config(builder);
            }
            return new WebHost(_server, builder.Build());
        }

        public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
        {
            _configures.Add(configure);
            return this;
        }

        public IWebHostBuilder UseServer(IServer server)
        {
            _server = server;
            return this;
        }
    }

給IWebHostBuilder加一個擴展方法,用來使用HttpListenerServer 伺服器

    public static partial class Extensions
    {
        public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)
        {
            return builder.UseServer(new HttpListenerServer(urls));
        }
    }

修改Mian方法

        static async Task Main(string[] args)
        {
            await new WebHostBuilder()
                .UseHttpListener()
                .Configure(app=>
                app.Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware))
                .Build()
                .StartAsync();              

        }

完成。

七 添加一個UseMiddleware 擴展 玩玩

        public static IApplicationBuilder UseMiddleware(this IApplicationBuilder application, Type type)
        {
            //省略實現
        }

        public static IApplicationBuilder UseMiddleware<T>(this IApplicationBuilder application) where T : class
        {
            return application.UseMiddleware(typeof(T));
        }

添加一個中間件

    public class QuxMiddleware
    {
        private readonly RequestDelegate _next;

        public QuxMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {

           await context.Response.WriteAsync("qux=>");

            await _next(context);
        }
    }
    public static partial class Extensions
    {
        public static IApplicationBuilder UseQux(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<QuxMiddleware>();
        }
    }

使用中間件

    class Program
    {
        static async Task Main(string[] args)
        {
            await new WebHostBuilder()
                .UseHttpListener()
                .Configure(app=>
                app.Use(FooMiddleware)
                .Use(BarMiddleware)
                .Use(BazMiddleware)
                .UseQux())
                .Build()
                .StartAsync();              

        }

運行結果

最後,期待Artech 新書。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 簡介 主要是採用identity Server4 和ocelot 加上consul 實現簡單的客戶端模式 開發準備 環境準備 下載並安裝Consul具體請參考前幾篇的內容 項目介紹 創建ocelotServerTest項目 創建IdentityServer4Test項目 創建consulServer ...
  • 什麼是options請求 options請求為發送非簡單跨域請求前的預檢請求,若該請求未正常返回,瀏覽器會阻止後續的請求發送。 一般情況下,有三種方式會導致瀏覽器發起預檢請求 1.請求的方法不是GET/HEAD/POST 2.POST請求的Content Type並非application/x ww ...
  • .NET Core 常用第三方包 作者:高堂 原文地址:https://www.cnblogs.com/gaotang/p/10845370.html 寫在前面 最近在學習.NET Core 中經常用到的一些插件,在此做個整理備忘。 Autofac "Autofac" Alexinea.Autofa ...
  • Split是string中非常有趣的命令,它是用來分隔字元串中子字元串的字元數組,並且不包含分隔符的空數組或null,如下麵的代碼: String value = "This is a short string."; Char delimiter = ' '; String[] substrings ...
  • 業務場景:有主表、子表兩個GridView點擊主表的行,會自動讀取主表對應的子表數據但是如果反覆點擊會導致反覆讀取,其實反覆點擊的時候只需要最後一次執行查詢,前面的幾次點擊都是無意義操作根據這一需求設計了一個函數:private static List Tup = new List();/// //... ...
  • 一、Excel理論知識 最新版NPOI2.4.1鏈接:https://pan.baidu.com/s/1iTgJi2hGsRQHyw2S_4dIUw 提取碼:adnq • 整個Excel表格叫做工作簿:WorkBook • 工作簿由以下幾部分組成 a.頁(Sheet); b.行(Row); c.單元 ...
  • 上一章快速陳述了自定義驗證功能添加的過程,我的第一個netcore2.2 api項目搭建(三) 但是並沒有真正的去實現,這一章將要實現驗證功能的添加。 這一章實現目標三:jwt認證授權添加 在netcore2.2中,只要添加很簡單的配置就能添加jwt功能了。至於jwt本身是啥大家自行去瞭解,這裡不做 ...
  • 用戶可以創建屬於自己的篩選方案用戶可以創建不同的篩選方案適用於不同場景需求●設計篩選條件●欄位排序方式●欄位是否顯示●欄位顯示順序●欄位顯示寬度菜單打開,如果有預設的篩選條件直接篩選不彈出篩選框修複第一次打開篩選彈窗條件的值沒有數據類型的問題直接拖拽列位置,調整列寬度可以自動保存到預設方案中系統預設... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...