The Open Web Interface for .NET (OWIN) 原始碼閱讀

風靈使發表於2018-07-08

katana開源許久,網上仍未搜尋到對其原始碼的閱讀總結,本人在工作中正好遇到資料處理流程框架設計,想來跟伺服器處理requestresponse差不多,遂起了閱讀katana原始碼,並借鑑其設計的想法,磕磕碰碰,困難重重,所幸有一些收穫,與大家交流交流。

katana原始碼 https://katanaproject.codeplex.com/

owin官網 http://owin.org/

兩個最重要的資料結構

1. Environment

IDictionary<string, object>

官方解釋:

This data structure is responsible for storing all of the state necessary for processing an HTTP request and response, as well as any relevant server state. An OWIN-compatible Web server is responsible for populating the environment dictionary with data such as the body streams and header collections for an HTTP request and response. It is then the responsibility of the application or framework components to populate or update the dictionary with additional values and write to the response body stream.

Environment是在pipeline中流動的資料,代表著一個具體的requestresponse,後文會介紹在每個pipeline stage中會對這個dictionary中自己關心的資料進行處理,並在進入下一個stage的時候丟棄引用,採用的是原子操作,因而每個Environment只存在一個pipeline stage中。資料舉例:

Required Key Name Value Description
Yes owin.RequestBody A Stream with the request body, if any. Stream.Null MAY be used as a placeholder if there is no request body. See Request Body.
Yes owin.RequestHeaders An IDictionary<string, string[]> of request headers. See Headers.
Yes owin.RequestMethod A string containing the HTTP request method of the request (e.g., “GET”, “POST”).
Yes owin.RequestPath A string containing the request path. The path MUST be relative to the “root” of the application delegate. See Paths.
Yes owin.RequestPathBase A string containing the portion of the request path corresponding to the “root” of the application delegate; see Paths.
Yes owin.RequestProtocol A string containing the protocol name and version (e.g. “HTTP/1.0” or “HTTP/1.1“).
Yes owin.RequestQueryString A string containing the query string component of the HTTP request URI, without the leading “?” (e.g., “foo=bar&amp;baz=quux“). The value may be an empty string.
Yes owin.RequestScheme A string containing the URI scheme used for the request (e.g., “http”, “https”); see URI Scheme.

2.AppFunc

Func<IDictionary<string, object>, Task>;

官方解釋:

The second key element of OWIN is the application delegate. This is a function signature which serves as the primary interface between all components in an OWIN application. The definition for the application delegate is as follows:

The application delegate then is simply an implementation of the Func delegate type where the function accepts the environment dictionary as input and returns a Task. This design has several implications for developers:

  • There are a very small number of type dependencies required in order to write OWIN components. This greatly increases the accessibility of OWIN to developers.
  • The asynchronous design enables the abstraction to be efficient with its handling of computing resources, particularly in more I/O intensive operations.
  • Because the application delegate is an atomic unit of execution and because the environment dictionary is carried as a parameter on the delegate, OWIN components can be easily chained together to create complex HTTP processing pipelines.

這就是middleware,也是每個pipeline stage中具體的處理方法,採用非同步呼叫的方式,由StartUp類進行註冊,並生成一條鏈,實際上就是壓進一個List中。

原始碼閱讀,能學到很多東西,肯定有很多理解有偏差的地方,歡迎指正,我將從一個具體的middleware註冊和StartUp的執行切入,大致勾勒一個pipeline的構造和流動過程。


OWIN中Environment初始化

按照官方文件的解釋,Microsoft.Owin.Host.SystemWeb在啟動的時候會進行一系列的初始化,具體的入口點隱藏太深無法尋找,我們假定現在流程已經到了Microsoft.Owin.Host.SystemWeb.OwinHttpHandler這裡,這裡將進行Environmentpipeline的初始化。本文所涉及的class大都在Microsoft.Owin.Host.SystemWeb名稱空間下。

先看Environment的初始化,原始碼進行了精簡,只留下重要部分,建議參考完整原始碼。

OwinHttpHandler被例項化,參考OwinHttpHandlerTests

var httpHandler = new OwinHttpHandler(string.Empty, OwinBuilder.Build(WasCalledApp));

先不考慮OwinBuilder.Build具體操作,httpHandler將開始處理request,即 public IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object extraData)將被呼叫,用於初始化一個基本的OwinAppContext,並根據httpContext引數初始化一個RequestContext,再將request資訊合併進入OwinAppContext,從上下文開始執行

public IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object extraData) 
{
    …
    try 
    {
        OwinAppContext appContext = _appAccessor.Invoke();
        //初始化基礎OwinAppContext
        Contract.Assert(appContext != null);
        // REVIEW: the httpContext.Request.RequestContext may be used here if public property unassigned?
        RequestContext requestContext = _requestContext ?? new RequestContext(httpContext, new RouteData());
        string requestPathBase = _pathBase;
        string requestPath = _requestPath ?? httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(1) + httpContext.Request.PathInfo;
        OwinCallContext callContext = appContext.CreateCallContext(
        //完善OwinAppContext資訊
        requestContext,
                            requestPathBase,
                            requestPath,
                            callback,
                            extraData);
        try 
        {
            callContext.Execute();
            //   這是本文的重點,一個request進來之後,OWIN的處理流程已經開始
            // 啟動以後的處理的原始碼都是可見的
        }
        …
                        return callContext.AsyncResult;
    }
    …
}

Excecute方法中進行Environment的再次初始化

internal void Execute() 
{
    CreateEnvironment();
    //再次初始化Environment
    …
}

這次初始化主要將上下文資訊中將要進入pipeline流動的資料存入AspNetDictionary中,這其實是一個擴充套件的

IDictionary<string, object>,也就是前文所介紹的第一個最重要資料結構,之後middleware開始Invoke,也就是pipeline的第一個AppFunc開始執行,引數就是初始化完成的Environment

那麼很自然有個問題,pipeline的第一個AppFunc是怎麼來的呢?pipeline又是如何串起來的呢?這很自然就涉及到了前文所提到的第二個重要內容Func<IDictionary<string, object>, Task>Pipeline定義為接收Environment,返回一個TaskFunc,當第一個AppFunc(也就是Func<IDictionary<string, object>, Task>)執行之後返回的其實又是一個AppFunc,而這種連結關係是由OwinBuilder建立的。

OwinBuilder所做的事情主要尋找程式集中的StartUp方法,並根據其中註冊pipeline的順序將各個AppFunc串起來,這將是一個浩大的工程。


OwinBuilder原始碼閱讀

原始碼參見Microsoft.Owin.Host.SystemWeb.OwinBuilder

通過前文知道,Build方法將被呼叫,其做的第一件事兒就是尋找Startup方法

internal static OwinAppContext Build()
{
 Action<IAppBuilder> startup = GetAppStartup();
 return Build(startup);
}

GetAppStartup方法主要完成從當前Assembly中尋找AppStartup方法,這也是為什麼申明Startup,使其被呼叫有兩種方法:

[assembly: OwinStartup(typeof(XX.Startup))]  //利用OwinStartupAtrribute來指導Startup
<appSettings>  
  <add key="owin:appStartup" value="StartupDemo.ProductionStartup" />
</appSettings>

//在webconfig中定義owin:appStartup鍵值對,以下將對負責搜尋Startup方法的DefaultLoader的原始碼進行分析,來了解如何定位Startup方法的
internal static Action<IAppBuilder> GetAppStartup()
        {
            string appStartup = ConfigurationManager.AppSettings[Constants.OwinAppStartup];
            var loader = new DefaultLoader(new ReferencedAssembliesWrapper());
            IList<string> errors = new List<string>();
            Action<IAppBuilder> startup = loader.Load(appStartup ?? string.Empty, errors);

            if (startup == null)
            {
                throw new EntryPointNotFoundException(Resources.Exception_AppLoderFailure
                    + Environment.NewLine + " - " + string.Join(Environment.NewLine + " - ", errors)
                    + (IsAutomaticAppStartupEnabled ? Environment.NewLine + Resources.Exception_HowToDisableAutoAppStartup : string.Empty)
                    + Environment.NewLine + Resources.Exception_HowToSpecifyAppStartup);
            }
            return startup;
        }

上面原始碼展示了呼叫DefaultLoaderLoad方法來搜尋Startup,而Startup是一個Action方法,即接受一個實現了IAppBuilder介面的例項作為引數,返回值為voidAction

    public Action<IAppBuilder> Load(string startupName, IList<string> errorDetails)
        {
            return LoadImplementation(startupName, errorDetails) ?? _next(startupName, errorDetails);
        }

Load方法實際上是對LoadImplementation的一個封裝,如果尋找失敗則使用_next進行尋找(實際上這會返回null,這不是重點)

private Action<IAppBuilder> LoadImplementation(string startupName, IList<string> errorDetails)
        {
            Tuple<Type, string> typeAndMethod = null;
            startupName = startupName ?? string.Empty;
            // Auto-discovery or Friendly name?
            if (!startupName.Contains(','))
            {
                typeAndMethod = GetDefaultConfiguration(startupName, errorDetails);    //通常會進入這一流程,如果startupName中包含逗號,則對應另一種申明方式
            }

            if (typeAndMethod == null && !string.IsNullOrWhiteSpace(startupName))    //這種申明方式為StartupName = “startupName,assemblyName”
            {
                typeAndMethod = GetTypeAndMethodNameForConfigurationString(startupName, errorDetails);    //對startupName和assemblyName進行分離,並找到對應的assembly載入
                                                                //其中的startupName
            }

            if (typeAndMethod == null)
            {
                return null;
            }

            Type type = typeAndMethod.Item1;
            // default to the "Configuration" method if only the type name was provided    //如果只提供了startup的type,則預設呼叫其中的Configuration方法
            string methodName = !string.IsNullOrWhiteSpace(typeAndMethod.Item2) ? typeAndMethod.Item2 : Constants.Configuration;

            Action<IAppBuilder> startup = MakeDelegate(type, methodName, errorDetails);    //直接呼叫startup方法或者做為一個middleware壓入List中,後文會講到具體實現
            if (startup == null)
            {
                return null;
            }

            return builder =>    //再對startup進行一次delegate封裝,傳入引數為builder,供上層呼叫
            {
                if (builder == null)
                {
                    throw new ArgumentNullException("builder");
                }

                object value;
                if (!builder.Properties.TryGetValue(Constants.HostAppName, out value) ||
                    String.IsNullOrWhiteSpace(Convert.ToString(value, CultureInfo.InvariantCulture)))
                {
                    builder.Properties[Constants.HostAppName] = type.FullName;    //獲取並記錄HostAppName
                }
                startup(builder);    //開始構造
            };
        }

由於引數startupName為最初定義的常量,其值為Constants.OwinAppStartup = "owin:AppStartup";所以很明顯會呼叫GetDefaultConfiguration(startupName, errorDetails)方法進一步處理。

    private Tuple<Type, string> GetDefaultConfiguration(string friendlyName, IList<string> errors)
        {
            friendlyName = friendlyName ?? string.Empty;
            bool conflict = false;
            Tuple<Type, string> result = SearchForStartupAttribute(friendlyName, errors, ref conflict);

            if (result == null && !conflict && string.IsNullOrEmpty(friendlyName))
            {
                result = SearchForStartupConvention(errors);
            }

            return result;
        }

這個方法又是對SearchForStartupAttribute的一個封裝

先了解一下OwinStartupAttribute

看上文使用到的建構函式

    public OwinStartupAttribute(Type startupType)
            : this(string.Empty, startupType, string.Empty)
        {
        }
    public OwinStartupAttribute(string friendlyName, Type startupType, string methodName)
        {
            if (friendlyName == null)
            {
                throw new ArgumentNullException("friendlyName");
            }
            if (startupType == null)
            {
                throw new ArgumentNullException("startupType");
            }
            if (methodName == null)
            {
                throw new ArgumentNullException("methodName");
            }

            FriendlyName = friendlyName;
            StartupType = startupType;
            MethodName = methodName;
        }

這裡預設將FriendlyNameMethodName設定為空,即只記錄了Startup類的Type,下面的SearchForStartupAttribute主要也是通過尋找OwinStartupAttribute中的StartupType 來獲取Startup的。

private Tuple<Type, string> SearchForStartupAttribute(string friendlyName, IList<string> errors, ref bool conflict)
        {
            friendlyName = friendlyName ?? string.Empty;
            bool foundAnyInstances = false;
            Tuple<Type, string> fullMatch = null;
            Assembly matchedAssembly = null;
            foreach (var assembly in _referencedAssemblies)    // 遍歷程式集
            {
                object[] attributes;
                try
                {
                    attributes = assembly.GetCustomAttributes(inherit: false);    // 獲取程式集的所有自定義Attribute
                }
                catch (CustomAttributeFormatException)
                {
                    continue;
                }

                foreach (var owinStartupAttribute in attributes.Where(attribute => attribute.GetType().Name.Equals(Constants.OwinStartupAttribute, StringComparison.Ordinal)))    // 對獲取到的Attribute進行過濾,只遍歷OwinStartupAttribute,即是優先會 //對上文所說的第一種 Startup申明進行呼叫
                {
                    Type attributeType = owinStartupAttribute.GetType();    //採用反射機制,先獲取Type
                    foundAnyInstances = true;

                    // Find the StartupType property.
                    PropertyInfo startupTypeProperty = attributeType.GetProperty(Constants.StartupType, typeof(Type));    //尋找屬性名是StartupType,屬性型別是Type的屬性
                    if (startupTypeProperty == null)    //尋找失敗,記錄錯誤
                    {
                        errors.Add(string.Format(CultureInfo.CurrentCulture, LoaderResources.StartupTypePropertyMissing,
                            attributeType.AssemblyQualifiedName, assembly.FullName));
                        continue;
                    }

                    var startupType = startupTypeProperty.GetValue(owinStartupAttribute, null) as Type;    //獲取StartupType屬性的值,並轉換為Type,為反射做準備
                    if (startupType == null)    //獲取或者轉換失敗,記錄錯誤
                    {
                        errors.Add(string.Format(CultureInfo.CurrentCulture, LoaderResources.StartupTypePropertyEmpty, assembly.FullName));
                        continue;
                    }

                    // FriendlyName is an optional property.
                    string friendlyNameValue = string.Empty;    //FriendlyName是可選項,作為對Startup類的別稱,不是重點
                    PropertyInfo friendlyNameProperty = attributeType.GetProperty(Constants.FriendlyName, typeof(string));
                    if (friendlyNameProperty != null)
                    {
                        friendlyNameValue = friendlyNameProperty.GetValue(owinStartupAttribute, null) as string ?? string.Empty;
                    }

                    if (!string.Equals(friendlyName, friendlyNameValue, StringComparison.OrdinalIgnoreCase))    //如果未定義FriendlyName則預設是Empty,否則記錄錯誤
                    {
                        errors.Add(string.Format(CultureInfo.CurrentCulture, LoaderResources.FriendlyNameMismatch,
                            friendlyNameValue, friendlyName, assembly.FullName));
                        continue;
                    }

                    // MethodName is an optional property.
                    string methodName = string.Empty;    同理MethodName也是可選項,如果為定義預設是Empty
                    PropertyInfo methodNameProperty = attributeType.GetProperty(Constants.MethodName, typeof(string));
                    if (methodNameProperty != null)
                    {
                        methodName = methodNameProperty.GetValue(owinStartupAttribute, null) as string ?? string.Empty;
                    }

                    if (fullMatch != null)    //表明已經尋找到一個Startup類,則衝突了,說明有重複申明Startup類
                    {
                        conflict = true;
                        errors.Add(string.Format(CultureInfo.CurrentCulture,
                            LoaderResources.Exception_AttributeNameConflict,
                            matchedAssembly.GetName().Name, fullMatch.Item1, assembly.GetName().Name, startupType, friendlyName));
                    }
                    else    //尚未尋找到Startup類,將StartupType和MethodName存為二元組,記錄程式集
                    {
                        fullMatch = new Tuple<Type, string>(startupType, methodName);
                        matchedAssembly = assembly;
                    }
                }
            }

            if (!foundAnyInstances)    //未尋找到申明Startup的程式集,記錄錯誤
            {
                errors.Add(LoaderResources.NoOwinStartupAttribute);
            }
            if (conflict)    //如果有衝突,返回null
            {
                return null;
            }
            return fullMatch;    //返回結果
        }

前文講到MakeDelegate(Type type, string methodName, IList<string> errors)主要作用是將尋找到的startup方法作為一個middleware壓入List中,看其原始碼

private Action<IAppBuilder> MakeDelegate(Type type, string methodName, IList<string> errors)
        {
            MethodInfo partialMatch = null;
            foreach (var methodInfo in type.GetMethods())
            {
                if (!methodInfo.Name.Equals(methodName))
                {
                    continue;
                }

                // void Configuration(IAppBuilder app)    //檢測Startup類中的Configuration方法的引數和返回值,這種為預設的方法,也是新建MVC時預設的方法
                if (Matches(methodInfo, false, typeof(IAppBuilder)))    //方法無返回值(void),引數為(IAppBuilder)
                {
                    object instance = methodInfo.IsStatic ? null : _activator(type);    //如果為靜態方法,則不需要例項,否則例項化一個Startup物件
                    return builder => methodInfo.Invoke(instance, new[] { builder });    //返回一個Lambda形式的delegate,實際上就是呼叫Startup的Configuration(IAppBuilder)方法
                }

                // object Configuration(IDictionary<string, object> appProperties)    //另一種Configuration方法,引數為Environment,返回object
                if (Matches(methodInfo, true, typeof(IDictionary<string, object>)))
                {
                    object instance = methodInfo.IsStatic ? null : _activator(type);    //由於傳入引數為Dictionary,所以將這個Configuration方法壓入middleware的List中
                    return builder => builder.Use(new Func<object, object>(_ => methodInfo.Invoke(instance, new object[] { builder.Properties })));
                }    //builder.Use傳入引數是一個Func<object,object>的delegate,實際上就是一個middleware,不過因為在初始化階段,所以不需要進入下一個stage

                // object Configuration()    //無引數,返回object
                if (Matches(methodInfo, true))
                {
                    object instance = methodInfo.IsStatic ? null : _activator(type);
                    return builder => builder.Use(new Func<object, object>(_ => methodInfo.Invoke(instance, new object[0])));
                }

                partialMatch = partialMatch ?? methodInfo;    //記錄找到但不符合三種定義的Configuration方法
            }

            if (partialMatch == null)    //未找到的Configuration,記錄錯誤
            {
                errors.Add(string.Format(CultureInfo.CurrentCulture,
                    LoaderResources.MethodNotFoundInClass, methodName, type.AssemblyQualifiedName));
            }
            else    找到Configuration,但不符合三種定義,記錄錯誤
            {
                errors.Add(string.Format(CultureInfo.CurrentCulture, LoaderResources.UnexpectedMethodSignature,
                    methodName, type.AssemblyQualifiedName));
            }
            return null;
        }

總結:OwinBuilder主要完成對Startup類的尋找,並呼叫其中的Configuration方法,Configuration有三種簽名(傳入引數與返回結果),將其封裝成一個方法返回給上層,供上層呼叫。接下來就是最重要的工作,呼叫Startup中的Configuration具體做了什麼,每個middleware是如何注入到pipeline中的,這就是AppBuilder主要做的工作了。

相關文章