OWIN輕量型框架介紹
引言
什么是OWIN,我就不介紹了,請自行搜索,這里主要是介紹自行開發的OWIN框架的特點和用法。由於.NET的web框架都比較龐大,導致性能總是不高,所以我才想到要在新出的OWIN協議基礎上,開發一個高性能的框架,追求極限的性能,絕不發展為一個復雜低效的框架,這是這套框架的初衷。目前功能還不是很全,只能提供WebApi的功能,Razor模板要下個版本再考慮。
如果你是一個注重程序運行效率,而不是編程方便的人,這個框架會很適合你。它在提供最高效率的同時,也極大方便了編程開發。
下載地址:https://github.com/qldsrx/OwinFramework
框架的特色
我用過ServiceStack,看過Nancy的代碼風格,這兩個框架廣受好評(但性能不行),特別是編程風格上面,於是我效仿了它們。目前框架中的序列化部分,還有使用ServiceStack_V3的dll,但由於ServiceStack非常不厚道,自從收費后,免費版本的最后一個版本刪了幾個常用方法,收費版本直接命名空間大變樣,升級直接導致項目出錯,因此最終會全部自己實現,不用ServiceStack的類庫。
該框架最低要求.net4.0環境,因為需要Task類的支持。
如何啟動
調試的時候可以用自承載方式,方法和Nancy的一樣,但win7以上系統需要管理員權限啟動,代碼如下:
static void Main(string[] args) { var url = "http://+:8080"; using (WebApp.Start<OwinLight.Startup>(url)) { Console.WriteLine("Running on {0}", url); Console.WriteLine("Press enter to exit"); Console.ReadLine(); } }
項目最終部署推薦使用Linux+Jexus,因為Jexus會優先處理靜態資源,而我的這個框架是不處理靜態資源的,如果用IIS部署,需要設置經典模式並自行配置靜態路徑的處理映射。
* jexus部署方法:
* 1、編譯得到OwinLight.dll(也可以自行改名)。
* 2、將編譯得到的dll連同Owin.dll、Microsoft.Owin.dll等文件
* 一同放置到網站的bin文件夾中
* 3、在對應網站的jws網站配置文件中加入一句,聲明要使用的適配器:
* OwinMain=OwinLight.dll,OwinLight.Startup
* 4、重啟Jexus讓配置生效。
* TinyFox部署方法:
* 1、編譯得到OwinLight.dll(也可以自行改名)。
* 2、將編譯得到的dll連同Owin.dll、Microsoft.Owin.dll等文件
* 一同放置到網站的bin文件夾中(site\wwwroot\bin)
* 3、默認配置文件為TinyFox.exe.config,也可以在site\wwwroot下面放web.config,優先讀取web.config
* 4、運行fox.bat或fox.sh(linux),端口在啟動腳本里。
各項功能
靜態路由的3種寫法
首先來看下這樣的寫法:
public Demo1() { //定義靜態路徑處理函數,Any表示任意請求類型,Get表示只對GET請求處理,Post表示只對POST請求處理 Any["/"] = GetRoot; }
這和Nancy里面里面是類似的,提供了Any、Get、Post三種屬性來定義路徑處理函數,不同的是,我的這個函數必須是Func<IOwinContext, Task>形式的,而不接受任何dynamic的參數。這里IOwinContext是web請求響應上下文,包含了請求和響應的各種信息。
下面就看下GetRoot函數的示例:
public Task GetRoot(IOwinContext context) { var x = new TaskCompletionSource<object>(); x.SetResult(null); //調用SetResult后,這個服務即轉為完成狀態 context.Response.ContentType = "text/html; charset=utf-8"; HttpHelper.WritePart(context, "<h1 style='color:red'>您好,Jexus是全球首款直接支持MS OWIN標准的WEB服務器!</h1>"); return x.Task; }
看到這里,你也許會覺得,這樣寫太麻煩了。是的,但是某些特殊場合會用的到,因此提供了這樣的寫法。下面介紹POCO參數方式的編程,和ServiceStack很像,但是更實用。
public enum ddd : byte { aa = 1, bb = 2, cc = 3, } /// <summary> /// 普通屬性接收來自URL或FORM表單的數據,接口屬性存儲文件,若非POST請求或文件個數為0,則接口屬性為空。 /// </summary> [Route("/api/dog1", 65536)] public class DOG1 : IHasHttpFiles { public int? id { get; set; } public string name { get; set; } public Guid token { get; set; } public ddd dsc { get; set; } /// <summary> /// 實現IHasHttpFiles,接收來自Form表單提交的文件,可能為空 /// </summary> public List<HttpFile> HttpFiles { get; set; } }
這里我們通過Route這個特性,來告訴框架,有這么一個地址/api/dog1的路由請求,請求內容要被自動封裝到這個DOG1里面,這里DOG1還繼承了IHasHttpFiles接口,這樣還可以接受來自Form表單上傳的文件。如果不繼承IHasHttpFiles接口,文件內容則不會被接收。DOG1里面的其它屬性,可以來自Query也可以來自Form,或者來自Xml和Json的序列化(此時不存在HttpFiles數據)。另外這里的Route特性里,定義了一個65536的數字,意思是,如果POST的數據超過了65536字節數,就忽略請求不處理,用來防攻擊的。默認最多接收4M字節。
下面看下這樣一個參數類型定義:
/// <summary> /// 普通屬性接收來自URL的數據,POST的內容存入接口屬性,若非POST請求或POST內容為空,則接口屬性也為空 /// </summary> [Route("/api/dog2", 65536)] public class DOG2 : IHasRequestStream { public int? id { get; set; } public string name { get; set; } public Guid token { get; set; } public ddd dsc { get; set; } /// <summary> /// 實現IHasRequestStream,接收來自POST請求的數據 /// </summary> public Stream RequestStream { get; set; } }
也許我們會想自己處理POST請求的數據,同時又要判斷URL附帶的參數,那么就可以定義這樣的類型,集成IHasRequestStream接口后,所有的POST數據不再處理,而是直接將網絡原始流設置到這個RequestStream屬性上面,注意,它不是緩存的,因此無法訪問Length屬性,也不能修改Position,更不能直接原樣返回,因為Http請求數據不接收完畢,是無法寫入響應的。
另外這個Route不單單可以對自定義類進行設置,還可以設置到某個函數上面,來看下這個例子:
public class Demo2 : BaseService { public object Any(DOG1 request) { return request; } public object Post(DOG2 request) { if (request.RequestStream == null) return null; StreamReader sr = new StreamReader(request.RequestStream); return sr.ReadToEnd(); } [Route("/api/test1", 1024)] public object testString(String request) { return request; } [Route("/api/test2", "POST", int.MaxValue)] public Stream testStream(Stream request) { if (request == null) return null; MemoryStream ms = new MemoryStream(); request.CopyTo(ms); ms.Position = 0; return ms; } }
DOG1和DOG2就是前面提到的POCO類型,這里Any方法匹配任意請求,而Post方法則只處理POST請求(還有Get方法類同)。而testString和testStream函數上面也出現了Route定義,這里是針對非自定義類型參數做的路由處理,String和Stream是系統自帶的,卻往往非常有用。有時候,我們會忽略任何參數,定義一個路徑請求,那么直接用testString函數的形式即可,不處理GET請求的URL參數。如果是POST請求,則會封送為String傳入函數,Stream的場合,則直接傳遞原始POST請求的網絡流,不緩存。響應也可以為一個不緩存的流,例如FileStream。
偽靜態路由的支持
[Rewrite("/api/FileGet/{fileid}/{filename}")] public class FileGet { public int fileid { get; set; } public string filename { get; set; } public bool download { get; set; } }
Rewrite特性代替Route,用來定義偽靜態響應。偽靜態一般用在對url格式有非常高的要求的場合,例如linux下面的wget命令下載文件,只能通過url來識別要保存的文件名。不建議用偽靜態,但一定要用的話,這里提供一個非常高效率的支持,處理速度相當快,因為沒有用正則。很多框架都是爛在偽靜態下面的,正則處理到一些莫名其妙的請求,導致CPU負荷很重,響應變慢。
我所支持的偽靜態,只做完全匹配,不支持使用通配符*,且一旦遇到了{xxx}的結構,那么后面的內容都作為參數處理,而不做匹配要求。例如這樣的寫法:[Rewrite("/api/FileGet/{fileid}/{filename}/Get")],雖然最后還有一個Get的固定路徑,但我也認為它是參數,只是一個不需要傳遞的參數,只有前面的/api/FileGet/才用來區分請求地址用。這樣做的好處是,匹配速度非常快。
偽靜態的實現,通過函數名Rewrite,如果函數名不對,將不加載。
public class Demo2 : BaseService, IDisposable { public string Rewrite(FileGet request) { Response.ContentType = "text/html; charset=utf-8"; return string.Format("<h1 style='color:red'>fileid:{0}<br/>filename:{1}</h1>", request.fileid, request.filename); } }
處理Form表單提交的文件
在前面靜態路由的示例中已經出現過,你定義的POCO類型,繼承IHasHttpFiles接口的情況下,才會自動幫你解析請求進來的文件,否則,你只能用Stream作為參數,自己處理Post請求的原始數據流。IHasHttpFiles接口有個HttpFiles屬性,存放Form表單所提交的全部文件。
流式處理Post請求的數據
在前面靜態路由的示例中已經出現過,你定義的POCO類型,繼承IHasRequestStream接口的情況下,其接口屬性RequestStream將為Post的原始網絡流,這對於大數據的流式接收很有利。POCO類型的其它屬性來自url附屬的參數,這個ServiceStack是不支持的,曾經為了這個網上搜索了下答案,官方給的答案是“沒必要支持”,自己從請求的參數里面抓——垃圾。
如果沒有url的附屬參數,你可以直接用Stream作為函數參數,Route特性定義在函數上面,前面已經給出過示例,也可以到項目代碼里看。
多種請求類型自動識別
該框架能處理的請求類型有:application/x-www-form-urlencoded、multipart/form-data、application/xml、application/json,如果請求不指定Content-Type,則會讀取url的參數format,但format只處理xml、json類型,如果都沒有指定,則視為json數據處理。而相應的內容目前只能是json格式,如果希望更改為xml或其他格式,請設置相應為String或Stream類型,並自行序列化。響應文檔格式的多樣化會在今后的版本考慮添加。
響應處理
對於使用靜態或偽靜態路由特性的返回值,支持任意類型,可以是object。如果是繼承Stream的類型,還會在處理結束時,自動調用Close和Dispose方法,所以不用擔心直接返回FileStream后的文件關閉問題。對於其他非字符串類型,會進行Json序列化后輸出,目前不提供多種輸出,但你可以自行序列化后,以字符串返回,這樣就不會再次為你序列化了。
響應的文檔類型,如果用戶設置,則采用用戶設置的文檔類型,否則自動按照函數返回值類型自動添加。用戶還可以自行設置IService接口的IsHeadersSended屬性為true,來阻止框架最后自行設置文檔長度。默認的文檔長度是計算返回值對象的產生的字節長度,但是如果在返回對象之前,用戶已經有往響應里寫入內容了,那文檔長度就不可預知了,因為這里的響應流是不緩存的,這也是為了提高效率,不然輸出一個大文件也緩存,服務器吃不消。備注:如果是IIS下面跑,響應只能是緩存的,微軟提供的IIS下使用的Owin適配器帶輸出緩存,還無法關閉。
請求響應上下文
框架中定義了這樣一個基類,但你也可以自己重新定義,繼承IService接口即可:
public abstract class BaseService : IService { public IOwinRequest Request { get; set; } IOwinResponse _Response; public IOwinResponse Response { get { return _Response; } set { _Response = value; _Response.OnSendingHeaders(t => { IsHeadersSended = true; }, null); } } public bool IsHeadersSended { get; set; } }
這個基類中的請求、響應上下文屬性,將會在請求時自動賦值,我們可以直接訪問最原始的請求和響應對象,用來控制Headers。而那個IsHeadersSended屬性,也會在響應首次輸出后,變為true,框架在自動處理函數返回值時,會用來判斷是否還需要設置文檔長度了。當然,你也可以強制設置為true,另外IIS下面帶緩存,因此你只能自己設置true,否則永遠不會變為true。
自定義默認處理函數
當沒有路由規則匹配時,默認的處理函數,可以為空,系統在GET請求是自動返回404,而POST請求則直接取消任務,取消任務的場合,看不同的Owin服務器是如何處理的,TinyFox已經可以配合中斷Socket了,而微軟的幾個宿主則沒有這么做。取消任務是非常重要的功能,對於攻擊防護很有意義,如果你不打算處理POST請求了,那么網絡I/O就應該立刻釋放,而不是等到數據接收完畢,再給一個響應,這樣I/O的占用對服務器來說是個不小的損失。通過設置Startup.NotFountFun委托,你可以自己定義匹配不到時的處理方式,后面在框架的擴展里,會給出一個靜態內容的支持示例,用的就是這個默認處理函數支持。
內置各種便捷函數
HttpHelper類里面:
GetMapPath,獲得當前絕對路徑,同時兼容windows和linux,.NET自帶的Path.Combine方法,到了linux下面的表現就和windows不同了,因為linux 的"/"代表了系統的根,和網站的根沖突了,但是我這里是要代表網站的根,所以Owin下面沒有一個可用的函數,只能自己寫。這個函數還支持“../”的寫法,查找上級,這樣我們在使用相對路徑時會很方便。
Escape,Unescape,對url參數進行編碼或解碼,由於Owin不需要System.Web,所以我們沒必要再引用過多的dll進來,於是我封裝了這兩個方法,支持參數為null的情況,這兩個方法本身也設置為了擴展方法,支持null時的調用,調用更方便。補充:對象在調用方法時,如果為空,一般會報“未將對象引用設置到對象實例”的錯誤,但是擴展方法可以避免這樣的報錯。
AddHttpRangeResponseHeaders,設置分段響應頭,當我們要提供斷點續傳時,這個函數可以方便的設置響應頭。
ParseFormData,解析Form表單數據,如果你要以Stream或String接收Post響應,可以調用這個函數來解析Form表單數據。
復合類型的請求處理
除了簡單的POCO類型外,自定義類的屬性可以是某個Model類型,這種就是復合類型,此時用來接收POST請求提交的Json數據,同時,url參數也會在Json數據接收后,被添加到自定義類型的簡單屬性上,確保Query的參數也被自動填充。
你也可以用Dictionary<string,object>類型作為請求的參數,此時將Route特性定義到處理函數上即可,POST傳遞的Json數據,將自動反序列化到這個字典類型上。
框架的擴展
框架是開源的,可以隨便更改。如果希望有后期的支持,請提交需求讓我來修改,或者記住自己的改動部分,以便下次合並修改。
HttpHelper類是框架的核心處理類,其靜態函數里面定義了大量的類型轉換函數,這里可以自己添加不存在的類型,或者修改已經存在的類型處理。類型分為兩類,數組和非數組,其傳遞的參數是不同的,源碼中有示例,這里不展開了。源碼中有段“#if DEBUG”代碼,用來設置是否對參數處理過程發生的異常做記錄,如果項目是在Debug下面編譯的,就會記錄,此時如果POST請求的數據不是xml或json格式,卻要強制進行轉換,就會記錄到異常。應用層的異常自行決定是否要捕獲,前面的是框架處理過程的異常。另外還有一個Debug類,里面就一個方法Write,框架中用來輸入各種異常都會調用它。你可以修改輸出的路徑,也可以對日志輸出進行一些緩存處理,再或者直接不做任何處理,屏蔽掉處理代碼。
靜態內容的支持
下面給一個通過默認處理函數,添加靜態頁響應的示例,僅供測試用

跨域Post的支持
IE8+、谷歌、火狐等瀏覽器均支持,不考慮IE6的話,可以使用這個功能
<appSettings> <!--偽靜態路徑最大深度--> <add key="rewritedepth" value="10"/> <!--自定義響應頭,key-value用冒號隔開,多個頭用封號隔開--> <add key="responseheaders" value="Access-Control-Allow-Origin:*;Access-Control-Allow-Methods:GET,POST;Access-Control-Allow-Headers:Content-Type"/> </appSettings>
在配置文件里添加如下代碼,注釋已經很清楚了。而Owin本身沒要求配置文件,所以,你的宿主可能不提供配置文件,那么你只是在你的網站根目錄下面放一個web.config,我框架會優先查找這個文件的配置,查找不到的情況才會去用應用程序默認的配置文件。HttpHelper.AppSettings用來訪問web.config下面的appSettings節點,HttpHelper.ConnectionStrings用來訪問web.config下面的connectionStrings節點。
基礎類型繼承靈活處理
BaseRoute類一般很少會用,也沒什么好說的。BaseService類用的最多,用它可以極大的提高開發效率,同時你也可以繼承該類實現更多的便利,下面我再給一段示例,演示如何自己繼承BaseService或IService來減少編碼。可以將這個MyService作為基類,再派生出相關處理下面的類。

ORM操作推薦用Dapper,性能最高。這里Db屬性有了,Dapper的使用就非常簡單了,因為Dapper的操作都是基於連接類的擴展方法。
RouteAttribute類也是可以繼承的,當你希望某些路由下用特定的響應頭,你可以派生出自己的RouteAttribute繼承類,將Headers屬性定死。框架在檢測到你設置過Headers屬性后,就不再讀取配置文件里面的響應頭設置了。
尾聲
這是第一版框架,由於工作繁忙,只能先實現這么多功能,其它功能下個版本添加。計划要增加Session處理和Razor引擎。