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/
是非、試してみてください。