SlideShare a Scribd company logo
How to Make Own Framework built on OWIN
A Case of LightNode
2014/02/08
Yoshifumi 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の未来を考える
OWIN
Open 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...
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.Web
var 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.Web
var outStream = HttpContext.Current.Response.Outpu
// OWIN (Environment = IDictionary<string, object>
var stream = environment["owin.ResponseBody"] as S
IDictionary<string, object>
辞書 is ダルい
そこでKatanaのOwinContext
EnvironmentをラップしたOwinContext
生でCookie扱うとか超絶ダルい(文字列を気合パース)けどその辺も楽
Katana vs Owin
Owin自体はシンプルな仕様
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 Pipeline
BeginRequest イベントを発生します。
AuthenticateRequest イベントを発生します。
PostAuthenticateRequest イベントを発生します。
AuthorizeRequest イベントを発生します。
PostAuthorizeRequest イベントを発生します。
ResolveRequestCache イベントを発生します。
PostResolveRequestCache イベントを発生します。
MapRequestHandler イベントを発生します。
PostMapRequestHandler イベントを発生します。
AcquireRequestState イベントを発生します。
PostAcquireRequestState イベントを発生します。
PreRequestHandlerExecute イベントを発生します。
要求に対応する IHttpHandler クラスの ProcessRequest メソッド (または非
同期バージョンの IHttpAsyncHandler.BeginProcessRequest) を呼び出します。
PostRequestHandlerExecute イベントを発生します。
ReleaseRequestState イベントを発生します。
PostReleaseRequestState イベントを発生します。
UpdateRequestCache イベントを発生します。
PostUpdateRequestCache イベントを発生します。
LogRequest イベントを発生します。
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 LightNode
ver 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 field
readonly Dictionary<RequestPath, OperationHandler> handlers
= new Dictionary<RequestPath, OperationHandler>();
// --- snip ---
// create handler
var handler = new OperationHandler(options, classType, methodInfo);
lock (handlers)
{
var path = new RequestPath(className, methodName); // dictionary key
handlers.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 prepare
var 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付きは汎用デリゲート
にないので手作りする
ParameterBinder
var 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 filter
return filters[index].Invoke(context, invokeRecursive);
}
else
{
// execute operation
return ExecuteOperation(options, context);
}
};
return invokeRecursive();
}
この中にobject[]が入ってる
フィルタ適応もOwinのパイ
プライン風になっていて再
起で適用していく
Framework Pipeline
SelectHandler
Execute
WriteResponse
結果をResponseStreamに書く
ParameterBinding
Write Stream
var 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引数は動的に決め打たれてる
UseExtensions
namespace 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, pa
{
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/
���非、試してみてください。

More Related Content

How to Make Own Framework built on OWIN