How to Make Own Framework built on OWIN

Post on 15-Jan-2015

21.819 views 7 download

description

2014/2/8 CLR/H

Transcript of How to Make Own Framework built on OWIN

How to Make Own Framework built on OWINA Case of LightNode

2014/02/08Yoshifumi Kawai - @neuecc

Self Introduction

@仕事

株式会社グラニ取締役CTO

C# 5.0 + .NET Framework 4.5 + ASP.NET MVC 5

最先端C#によるハイパフォーマンスWebアプリケーション

@個人活動

Microsoft MVP for Visual C#

Web http://neue.cc/

Twitter @neuecc

LINQがひじょーに好き、趣味はライブラリ作成

Public Libraries

NuGet公開パッケージ数30突破

https://www.nuget.org/profiles/neuecc/

linq.js

JavaScriptにLINQ to Objectsを移植したライブラリ

http://linqjs.codeplex.com/

その他色々

Chaining Assertion(ユニットテスト補助), DynamicJson(dynamicなJSON),

AsyncOAuth(PCLのOAuthライブラリ), CloudStructures(Object/Redisマッパー),

ReactiveProperty(Rx + MVVM), AnonymousComparer(LINQ補助), etc...

最近はOwinにムチューなのでMiddlewareいろいろ作ってます

Grani

C#を使ってソーシャルゲームを開発している会社

C# 5.0 + ASP.NET MVC 5 + TypeScript + ...

かなり規模は大きめ

Over 100 Servers, 10000req/s

サーバーはクラウド(AWS)に展開

AWS上でWindows Server 2012

データベースはcr1.8xlarge(32コア244GBメモリ)を複数台

KVSとしてRedisを多用

Agenda

Agenda

OWINについて

OWINとは何か、どういう仕組みなのかを理解する

OWINフレームワーク(LightNode)の実装解説

Webフレームワークはどう動いているのかを、LightNodeとい

う非常に小さな実装を通して把握する

同時に実装のしかた、性能の出しかたを理解する

OWINの意義

それらを通してASP.NETの未来を考える

OWINOpen Web Interface for .NET

いままでのASP.NET

System.WebはIISと密接に結びついてた

IIS以外で動かすことができない

で、それの何が悪いの?何も悪くない!!!

時は流れて現代

SignalRをセルフホスト!軽量ユニットテスト!など用途の広がり

そろそろアプリケーションとサーバーを引っぺがす時が来た

そこで現れたのが、OWIN

ついでにSystem.Webのレガシー設計をぶち壊す一世一代のチャンス

OWIN is...

仕様

詳しくは後で。

Perl/PSGI, Python/WSGI, Ruby/Rack, Node.js/Connectなどの影響

よってOwinについて知りたい時はPerlのPlack Handbook見るのが最も良い

https://github.com/miyagawa/plack-handbook/tree/master/ja

二つの利点

バックエンドを自由に選べる(IIS,HttpListner,etc...)

実質的にIISしか使わない?ユニットテストとかにも便利ですよ

自由に組み合わせられるミドルウェアパイプライン

シンプルながらも非常に強力、そして単純だから分かりやすく作りやすい

Host:Server

Owin Image "Onion"

Request

Response

Web Framework

Middleware

Web Application

Server

Host

Owin Image "Onion"

Request

Response

Web Framework

Middleware

Web Application

Server

Host

HostとServerは等しいことがある

FramworkとMiddlewareの間に、実装上は特に違いはない。アプリケーションにとって直接のレイヤーを指す(よってPipelineの中央になる)

Owin:Server

Request

Response

IIS

HttpListner

UnitTestMock

etc...

Owin:Server

Request

Response

IIS

HttpListner

UnitTestMock

etc...

BlackJumboDog?

Owin:Server

Request

Response

IIS

HttpListner

UnitTestMock

etc...

Helios(Microsoft.Owin.Host.IIS)従来のIISパイプラインを通さず、直接叩くことにより超性能を叩きだした実装(まだα)

System.Web:HttpContext

Request

Response

HttpContext

今まではHttpContextに情報が詰め込まれてやってきてたのが大前提だった

この状態はIISに激しく依存している

OWIN:Environment

Request

Response

IDictionary<string, object>

Environment(環境変数)として完全に動的に情報が送られるので依存がないただし必須のKeyは幾つか存在する

Context vs Environment

Request

Response

IIS

HttpListner

UnitTestMock

etc...

// System.Webvar ua1 = HttpContext.Current.Request.UserAgent;

// OWIN (Environment = IDictionary<string, object>)var headers = environment["owin.RequestHeaders"] as IDictionary<string, string[]>;var ua2 = headers["User-Agent"];

// System.Webvar outStream = HttpContext.Current.Response.OutputStream;

// OWIN (Environment = IDictionary<string, object>)var stream = environment["owin.ResponseBody"] as Stream

IDictionary<string, object>

辞書 is ダルい

そこでKatanaのOwinContext

EnvironmentをラップしたOwinContext

生でCookie扱うとか超絶ダルい(文字列を気合パース)けどその辺も楽

Katana vs OwinOwin自体はシンプルな仕様

Environmentと後述するAppFuncの定義だけ

KatanaはMicrosoftによるリファレンス実装

実装は複数あってもよく、そのうちの一つに過ぎない

(というのが建前、実質的には一択になると思います)

OwinContextのほか、便利MiddlewareやMiddlewareベースなど色々

使っても使わなくてもいい、けれど、使ったほうが楽(でも依存性が?)

// Katana(Microsoft.Owin)/IOwinContext 色々生えてて便利

var body = context.Request.Body;var cookie = context.Request.Cookies["hogehoeg"];

Middleware Pipeline

Before:HttpApplication PipelineBeginRequestイベントを発生します。AuthenticateRequestイベントを発生します。PostAuthenticateRequestイベントを発生します。AuthorizeRequestイベントを発生します。PostAuthorizeRequestイベントを発生します。ResolveRequestCacheイベントを発生します。PostResolveRequestCacheイベントを発生します。MapRequestHandlerイベントを発生します。PostMapRequestHandlerイベントを発生します。AcquireRequestStateイベントを発生します。PostAcquireRequestStateイベントを発生します。PreRequestHandlerExecuteイベントを発生します。要求に対応する IHttpHandlerクラスの ProcessRequestメソッド (または非同期バージョンの IHttpAsyncHandler.BeginProcessRequest) を呼び出します。PostRequestHandlerExecuteイベントを発生します。ReleaseRequestStateイベントを発生します。PostReleaseRequestStateイベントを発生します。UpdateRequestCacheイベントを発生します。PostUpdateRequestCacheイベントを発生します。LogRequestイベントを発生します。PostLogRequestイベントを発生します。

After:Owin Middleware Pipeline

Request

Response

Framework

Middleware

AppFunc

Request

Response

Framework

Middleware

Func<IDictionary<string, object>, Task>により次のミドルウェアに進む

環境変数を受け取り、実行完了を非同期で返す。このFuncをAppFuncと定義する

using AppFunc = System.Func<IDictionary<string, object>, Task>;

Pipeline - Code Image

Framework

Middleware

// Middlewareのコードイメージ

try{

// 実行前アクション(外側から円の中央へ向かう)

// next = AppFunc = Func<IDictionary<string, object>, Task>

await next(env); // 一つ円の内側へ

// 実行後アクション(円の中央から外側へ向かう)

}catch{

// 例外時アクション

}finally{

// Middleware終了時アクション

}

Stack the Middleware!

Framework

Middleware

// 未指定の場合はStartupクラスを自動的に探す

[assembly: OwinStartup(typeof(Hello.Startup))]

namespace Hello{

// アプリケーション立ち上げ時に一度だけ呼ばれる// (Global.asaxのApplication_Startみたいなもの)

public class Startup{

public void Configuration(IAppBuilder app){

// 利用するMiddlewareを重ねていく

app.UseRequestScopeContext().UseRedisSession().UseTwitterAuthentication().UseLightNode();

}}

}

LightNode.NET’s Fastest REST Framework

LightNodeとは

Micro RPC/REST Framework

http://neuecc.github.io/LightNode/

ASP.NET Web APIとかと同種の代物

超絶Lightweight、超簡単にサーバーAPIが作れる

パフォーマンスも超絶凄い、.NET最速

クライアントサイドのコード自動生成機能付き

OWIN上で構築されている

次世代!

Why use LightNode

クライアントサイド作る苦行

Web APIでRESTサーバー実装した、ではクライアント側は?

URLを、リソースを、手作業で特化クライアントSDK作ることに

なる。超絶ダルいし、開発途中はサーバー側のAPIもどんどん変

わっていく

自動生成すればいい

自動生成といってもSOAP/WCFのようなヘヴィなものは嫌

けれどクライアントSDKを前提に置くならRPC風は具合がいい

軽量なサーバー実装、軽量で変更耐性の高い自動生成

Demo...Create API Server and Upload to Azure Web Sites in 10 Minutes

Lightweight Server and Client// http://hogehoge/Hello/GetMC?name={0}&x={1}&y={2}&e={3}// と、呼べるサーバーAPIが公開される

public class Hello : LightNode.Server.LightNodeContract{

public MyClass GetMC(string name, int x, int y, MyEnum e){

return new MyClass { Name = name, Sum = (x + y) * (int)e };}

}

// こう呼べるクライアントが自動生成される

var client = new LightNodeClient("http://hogehoge/")var mc = await client.Hello.GetMCAsync("hoge", 20, 20, MyEnum.B);

メソッドを作れば、それがそのままAPIとして公開され、戻り値は

(JSONなどに)シリアライズされる

T4による自動生成、APIのDLLを指定することにより、それを解析してHttpClient

ベースの専用クライアントを生成する

Fastest!

フレームワークでは最速!これより速いのは生ハンドラのみ

Inside LightNodever 0.2.0~0.3.0

Framework Pipeline

SelectHandler

Execute

WriteResponse

どのメソッドを実行するか探す

メソッドにパラメータを割り当てる

結果をResponseStreamに書く

ParameterBinding

メソッドを実行する

Framework Pipeline

SelectHandler

Execute

WriteResponse

どのメソッドを実行するか探す

ParameterBinding

RegsiterHandler

Select...の前に実行ハンドラを予め登録

アセンブリ中にあるLightNodeContractを継承したクラスの全パ

ブリックメソッドが実行ハンドラというルール

最初に一回だけ登録すればいいのでOwinのStartup時に行う

先にできるものは先に全部やったほうが実行性能的にもいい

public class Hello : LightNodeContract{

public string Say(){

return "Hello LightNode";}

}

RegsiterHandler

LightNodeServer.cs | LightNodeServer.RegisterHandler

// Assembly[] hostAssemblies// デフォルトはAppDomain.CurrentDomain.GetAssemblies()から

var contractTypes = hostAssemblies.SelectMany(x => x.GetTypes()).Where(x => typeof(LightNodeContract).IsAssignableFrom(x)).Where(x => !x.IsAbstract);

// cache fieldreadonly Dictionary<RequestPath, OperationHandler> handlers

= new Dictionary<RequestPath, OperationHandler>();// --- snip ---// create handlervar handler = new OperationHandler(options, classType, methodInfo);lock (handlers){

var path = new RequestPath(className, methodName); // dictionary keyhandlers.Add(path, handler); // add handler cache

}

アセンブリを舐めて対象クラスを探しだす

ハンドラを生成して辞書にキャッシュ

Create Delegate

MethodInfoのInvokeは遅い

最も簡単な動的な実行だけど結果は今ひとつ

動的実行を高速化するにはDelegateを作ってキャッシュする

キャッシュのための共通化

Dictionaryに入れるため全メソッドを一つの型に統一する

全てのメソッドはobject[]を受け取りobjectを返すと表現可能

つまりFunc<object[], object> (実際はT,void,Task,Task<T>の4つの分別はあり)

OperationHandler.cs | OperationHandler.ctor

Expression Compile// common preparevar args = Expression.Parameter(typeof(object[]), "args");var parameters = methodInfo.GetParameters()

.Select((x, i) => Expression.Convert(Expression.ArrayIndex(args, Expression.Constant(i)), x.ParameterType))

.ToArray();

//--- snip ---

// (object[] args) => (object)new X().M((T1)args[0], (T2)args[1])...var lambda = Expression.Lambda<Func<IDictionary<string, object>, object[], object>>(

Expression.Convert(Expression.Call(

Expression.MemberInit(Expression.New(classType), envBind),methodInfo,parameters)

, typeof(object)),envArg, args);

this.handlerBodyType = HandlerBodyType.Func;this.methodFuncBody = lambda.Compile(); // Compile!

実際はOwinのEnvironment

を引数に受け取る仕様

このデリゲートをキャッシュするんだよもん その他、Task対応の処理は更に面倒

だったりするので詳しくはブログで:)

http://neue.cc/2014/01/27_446.html

Select Handler

ルーティングのない素敵

URLのルールは /ClassName/MethodName で固定

なので単純にSplitして辞書から持ってくるだけでOK

最速

これより高速は無理

LightNodeServer.cs | LightNodeServer.SelectHandler

var path = environment["owin.RequestPath"] as string;var keyBase = path.Trim('/').Split('/');// {ClassName, MethodName}var key = new RequestPath(keyBase[0], keyBase[1]);

// キャッシュ済みハンドラを辞書からルックアップ

OperationHandler handler;if (handlers.TryGetValue(key, out handler)){

return handler;}

Framework Pipeline

SelectHandler

Execute

WriteResponse

メソッドにパラメータを割り当てるParameterBinding

ParameterBinding is...

ValueProvider

(Parameter -> string)

public int Add(int x, int y = 0)

ParameterBinder

(-> object[])

TypeBinder

(string -> T)hoge?x=10&y=20

ValueProvider

リクエストからModelBinderに渡す文字列を作る

?x=10&y=20だったら{x:”10”, y:”20”}を作る

ValueProvider.cs | ValueProvider.ctor

var queryString = environment["owin.RequestQueryString"] as string;AppendValues(queryString);

if (verb != AcceptVerbs.Get){

using (var sr = new StreamReader((environment["owin.RequestBody"] as Stream))){

var formUrlEncoded = sr.ReadToEnd();AppendValues(formUrlEncoded);

}}

QueryStringとFormUrlEncodedのみ対応

TypeBinder

単純にTryParseするだけ

クソ単純 = わかりやすい = 速い = 正義

TypeBinder.cs | TypeBinder.cctor

public delegate bool TryParse(string x, out object result);

static readonly Dictionary<Type, TryParse> convertTypeDictionary= new Dictionary<Type, TryParse>(33)

{{typeof(Boolean) ,(string x, out object result) => { Boolean @out; var success = Boolean.TryParse(x, out @out); result = (object)@out; return success; }},{typeof(Nullable<Boolean>),(string x, out object result) => { Boolean @out; result = Boolean.TryParse(x, out @out) ? (object)@out : null; return true; }},{typeof(Int32) ,(string x, out object result) => { Int32 @out; var success = Int32.TryParse(x, out @out); result = (object)@out; return success; }},{typeof(Nullable<Int32>),(string x, out object result) => { Int32 @out; result = Int32.TryParse(x, out @out)? (object)@out:null; return true; }},// snip....

}

out付きは汎用デリゲートにないので手作りする

ParameterBindervar conv = TypeBinder.GetConverter(item.ParameterType,

!options.ParameterEnumAllowsFieldNameParse);

object pValue;if (conv(value ?? values[0], out pValue)){

methodParameters[i] = pValue;continue;

}else if (item.IsOptional){

methodParameters[i] = item.DefaultValue;continue;

}else if ((!item.ParameterTypeIsString || options.ParameterStringImplicitNullAsDefault) && (item.ParameterTypeIsClass || item.ParameterTypeIsNullable)){

methodParameters[i] = null;continue;

}ParameterBinder.cs | ParameterBinder.BindParameter

TypeBinderの結果からパースできたら採用。できなかったらオプション引数やnullが可能かチェックしてobject[] を作る

Framework Pipeline

SelectHandler

Execute

WriteResponse

ParameterBinding

メソッドを実行する

with Filter

OperationHandler.cs | OperationHandler.Execute

public Task Execute(LightNodeOptions options, OperationContext context){

int index = -1;Func<Task> invokeRecursive = null;invokeRecursive = () =>{

index += 1;if (filters.Length != index){

// chain next filterreturn filters[index].Invoke(context, invokeRecursive);

}else{

// execute operationreturn ExecuteOperation(options, context);

}};return invokeRecursive();

}

この中にobject[]が入ってる

フィルタ適応もOwinのパイプライン風になっていて再起で適用していく

Framework Pipeline

SelectHandler

Execute

WriteResponse

結果をResponseStreamに書く

ParameterBinding

Write Streamvar responseStream = environment["owin.ResponseBody"] as Stream;if (options.StreamWriteOption == StreamWriteOption.DirectWrite){

context.ContentFormatter.Serialize(new UnclosableStream(responseStream), result);}else{

using (var buffer = new MemoryStream()){

context.ContentFormatter.Serialize(new UnclosableStream(buffer), result);responseHeader["Content-Length"] = new[] { buffer.Position.ToString() };buffer.Position = 0;if (options.StreamWriteOption == StreamWriteOption.BufferAndWrite){

buffer.CopyTo(responseStream); // not CopyToAsync}else{

await buffer.CopyToAsync(responseStream).ConfigureAwait(false);}

}}

UnclosableStreamにラップ(Close/Dispose

が呼ばれても何もしないStream)

OperationHandler.cs | OperationHandler.ExecuteOperation

MemoryStreamに書き出してから出力(デフォルト)

理由:1. Content-Lengthの取得2. Streamにレスポンスを書き出している途中にシリアライザが落ちたりすると、500に変更したくてもできない (Bodyが1byteでも吐かれた後はStatusCodeは変更できない)

Integrated with OWIN

using AppFunc = Func<IDictionary<string, object>, Task>;

public class LightNodeServerMiddleware{

readonly LightNodeServer engine;readonly bool useOtherMiddleware;readonly AppFunc next;

public LightNodeServerMiddleware(AppFunc next, LightNodeOptions options, Assembly[] hostAssemblies){

this.next = next;this.useOtherMiddleware = options.UseOtherMiddleware;this.engine = new LightNodeServer(options);this.engine.RegisterHandler(hostAssemblies);

}

public async Task Invoke(IDictionary<string, object> environment){

if (useOtherMiddleware){

await engine.ProcessRequest(environment).ConfigureAwait(true);await next(environment).ConfigureAwait(false);

}else{

await engine.ProcessRequest(environment).ConfigureAwait(false);}

}}

Middleware

何も継承しない(Katanaを使わない場合)

フレームワークとして作る/使う場合、nextのInvokeはしない(終点)

LightNodeもデフォルトはこちらのパスに来るnextはユーザーが明示的に後続をUseしなかった時、Katanaの場合はStatusCode = 404を埋めるAppFuncが詰まっている(よって不要なら呼

んではならない)

nextはコンストラクタに乗ってくる(決め打ち)next/environment引数は動的に決め打たれてる

UseExtensionsnamespace Owin{

public static class AppBuilderLightNodeMiddlewareExtensions{

public static IAppBuilder UseLightNode(this IAppBuilder app){

return UseLightNode(app, new LightNodeOptions(AcceptVerbs.Get | AcceptVerbs.Post,new LightNode.Formatter.JavaScriptContentFormatter()));

}

public static IAppBuilder UseLightNode(this IAppBuilder app, LightNodeOptions options){

return app.Use(typeof(LightNodeServerMiddleware), options);}

public static IAppBuilder UseLightNode(this IAppBuilder app, LightNodeOptions options, params{

return app.Use(typeof(LightNodeServerMiddleware), options, hostAssemblies);}

}}

StartupのConfigurationで

app.UseLightNode()とだけ書いて有効にできるようにnamespace Owin

Conclusion

Create Your Own Framework

自分達に最適なものを作り、組み立てる

小さなユーティリティから大きいフレームワークまで

既成品だけでは全てのシチュエーションに合うわけではない

Middleware Stack

ルーティング,キャッシュ,認証などは他のMiddlewareに任せる

俺々フレームワークの欠点である、何でも自作する必要がある、

他の良いものが使えない、という欠点がなくなる

Close the world

OWINはYOU!を作り出す環境

使うだけの時代にさよなら、作り出す時代の幕開け

YOU!

YOU!

Open the nExt

Microsoftだけがリードする時代ではない

Owin自体もコミュニティ発祥

最先端は自分達が、共に築き上げていく

新しい時代の、新しいフレームワーク

LightNodeはそれを具現化する

http://neuecc.github.io/LightNode/

是非、試してみてください。