More Related Content Similar to How to Make Own Framework built on OWIN Similar to How to Make Own Framework built on OWIN (20) More from Yoshifumi Kawai More from Yoshifumi Kawai (20) How to Make Own Framework built on OWIN1. How to Make Own Framework built on OWIN
A Case of LightNode
2014/02/08
Yoshifumi Kawai - @neuecc
2. 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がひじょーに好き、趣味はライブラリ作成
4. 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を多用
9. 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しか使わない?ユニットテストとかにも便利ですよ
自由に組み合わせられるミドルウェアパイプライン
シンプルながらも非常に強力、そして単純だから分かりやすく作りやすい
17. 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>
20. 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 イベントを発生します。
23. Pipeline - Code Image
Framework
Middleware
// Middlewareのコードイメージ
try
{
// 実行前アクション(外側から円の中央へ向かう)
// next = AppFunc = Func<IDictionary<string, object>, Task>
await next(env); // 一つ円の内側へ
// 実行後アクション(円の中央から外側へ向かう)
}
catch
{
// 例外時アクション
}
finally
{
// Middleware終了時アクション
}
24. 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();
}
}
}
27. Why use LightNode
クライアントサイド作る苦行
Web APIでRESTサーバー実装した、ではクライアント側は?
URLを、リソースを、手作業で特化クライアントSDK作ることに
なる。超絶ダルいし、開発途中はサーバー側のAPIもどんどん変
わっていく
自動生成すればいい
自動生成といってもSOAP/WCFのようなヘヴィなものは嫌
けれどクライアントSDKを前提に置くならRPC風は具合がいい
軽量なサーバー実装、軽量で変更耐性の高い自動生成
29. 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
ベースの専用クライアントを生成する
35. 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
}
アセンブリを舐めて対
象クラスを探しだす
ハンドラを生成して
辞書にキャッシュ
37. 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
38. 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;
}
42. 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付きは汎用デリゲート
にないので手作りする
43. 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[] を
作る
45. 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のパイ
プライン風になっていて再
起で適用していく
47. 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は変更
できない)
49. 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引数は動的に決め打たれてる
50. 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
52. Create Your Own Framework
自分達に最適なものを作り、組み立てる
小さなユーティリティから大きいフレームワークまで
既成品だけでは全てのシチュエーションに合うわけではない
Middleware Stack
ルーティング,キャッシュ,認証などは他のMiddlewareに任せる
俺々フレームワークの欠点である、何でも自作する必要がある、
他の良いものが使えない、という欠点がなくなる