更新: | 2013-03-28 |
---|---|
イベント: | SPDY & WS 勉強会 (仮) |
発表日時: | 2013-03-28 |
バージョン: | 0.1.2 |
作者: | @voluntas |
URL: | http://voluntas.github.com/ |
- 自己紹介
- Erlang について
- 概要
- パターンマッチによるパーサー
- 軽量プロセスを使ったサーバ実装
- Erlang で TLS/NPN 実装
- PUSH のデモ
- 質疑応答
お仕事で Erlang/OTP でネットワークサーバーを提案したり設計したり開発したり試験したり。
ag:
$ git clone git://github.com/erlang/otp.git $ cd otp $ ag voluntas .
今回は Erlang で SPDY を実装するとこの位、楽だったよという話をします。
ざっくりと SPDY の実装の流れについて話した後、簡単な PUSH のデモをします。
ただ、PUSH のデモといってもそんなにたいした話では無いので、あまり期待しないでください。
Erlang/OTP を知ってる人がほとんどいないと思いますので、説明させてください。
- エリクソンが開発した言語
- 今もエリクソンがメンテナンスしている
- もともとは交換機のために作った
- 今はルーターが作られたりしてる
- GitHub 上でソースが公開されている
- ライセンスは MPL っぽい EPL という独自ライセンス
- 言語的にはシンプル
- OTP というフレームワークがくっついてる
簡単にいってしまえばネットワーククライアント/サーバ用の DSL 言語です。
それ以外に使うと痛い目にあいます。
最近ではいろいろなところで使われています。SPDY + Erlang だと有名なのは LINE でしょうか。
- Adopting SPDY in Line – Part 1: An Overview « NAVER Engineers' Blog
- http://tech.naver.jp/blog/?p=2381
- Christopher Rogers、Namil Kim「lineもspdyサポート(その1)」 - 以下斜め読んだ内容
- http://d.hatena.ne.jp/vwxyz/20130111/1357894320
LINE ほど大量の処理をさばけているという現状があるようです。
Erlang はバイナリパターンマッチというパターンマッチを持っています。 パターンマッチについての説明は割愛します。
まずはコードを見て頂ければ。ざっと説明していきます。
%% 2.2.2. Data frames
%%
%% +----------------------------------+
%% |C| Stream-ID (31bits) |
%% +----------------------------------+
%% | Flags (8) | Length (24 bits) |
%% +----------------------------------+
%% | Data |
%% +----------------------------------+
-spec parse_frame(binary(), byte(), non_neg_integer(), binary(), zlib:zstream()) -> spdy:frame().
parse_frame(<<?SPDY_DATA_FRAME:1, StreamId:31>>, Flags, _Length, Data, _Z) ->
#spdy_data{stream_id = StreamId,
flags = Flags,
data = Data};
%% 2.2.1. Control frames
%%
%% +----------------------------------+
%% |C| Version(15bits) | Type(16bits) |
%% +----------------------------------+
%% | Flags (8) | Length (24 bits) |
%% +----------------------------------+
%% | Data |
%% +----------------------------------+
parse_frame(<<?SPDY_CONTROL_FRAME:1, Version:15, Type:16>>, Flags, Length, Data, Z) ->
parse_control_frame(Version, Type, Flags, Length, Data, Z);
parse_frame(_T, _Flags, _Length, _Data, _Z) ->
{error, invalid_spdy_protocol}.
%% 2.6.1. SYN_STREAM
%%
%% +------------------------------------+
%% |1| version | 1 |
%% +------------------------------------+
%% | Flags (8) | Length (24 bits) |
%% +------------------------------------+
%% |X| Stream-ID (31bits) |
%% +------------------------------------+
%% |X| Associated-To-Stream-ID (31bits) |
%% +------------------------------------+
%% | Pri|Unused | Slot | |
%% +-------------------+ |
%% | Number of Name/Value pairs (int32) | <+
%% +------------------------------------+ |
%% | Length of name (int32) | | This section is the "Name/Value
%% +------------------------------------+ | Header Block", and is compressed.
%% | Name (string) | |
%% +------------------------------------+ |
%% | Length of value (int32) | |
%% +------------------------------------+ |
%% | Value (string) | |
%% +------------------------------------+ |
%% | (repeats) | <+
parse_control_frame(Version, ?SPDY_SYN_STREAM, Flags, _Length,
<<_X:1, StreamId:31,
_X:1, AssociatedToStreamId:31,
Priority:3, _Unused:5, Slot:8, RawHeaders/binary>>, Z) ->
Headers = parse_headers(Version, RawHeaders, Z),
#spdy_syn_stream{version = Version,
flags = Flags,
stream_id = StreamId,
associated_to_stream_id = AssociatedToStreamId,
priority = Priority,
slot = Slot,
headers = Headers};
ヘッダーのパースは SPDY/2 と SPDY/3 に対応するよう、ちょっとへんな作りになってます。
-spec select_dict(2 | 3) -> binary().
select_dict(2) ->
?HEADERS_ZLIB_DICT_2;
select_dict(3) ->
?HEADERS_ZLIB_DICT_3.
-spec select_length(2 | 3) -> 16 | 32.
select_length(2) ->
16;
select_length(3) ->
32.
-spec parse_headers(spdy:version(), binary(), zlib:zstream()) -> spdy:headers().
parse_headers(Version, Binary, Z) ->
Dict = select_dict(Version),
Length = select_length(Version),
Uncompressed = unpack(Z, Binary, Dict),
<<_Num:Length, Rest/binary>> = Uncompressed,
[ {Name, Value} || <<NameLength:Length, Name:NameLength/binary,
ValueLength:Length, Value:ValueLength/binary>> <= Rest ].
Erlang では「例外をキャッチするのでは無く例外でクラッシュさせる」という書き方をします。そのため例外処理を無視して書くことが出来ます。もちろん細かいロギングをしたかったら必要です。
サーバの簡単な実装ですが、Erlang を使った場合は軽量プロセスを使って大量の処理が出来ます。
軽量プロセスは起動コストが低いスレッドだと思って頂ければ。興味ある方は調べて見てください。
1 プロセス 2 マイクロ秒程度で起動します。メモリに依存しますが数千万プロセス上げても大丈夫です。
SPDY は今のところ前提が TLS だったり TCP なので、コネクションを保持する必要があります。 特に GOAWAY が送られてくるまでは基本的にコネクションは貼りっぱなしの実装になります。
これを Erlang で書くと楽に書けます。というか最近は便利ライブラリが色々出てきてるので大変じゃ無いですね。あまりこの辺にメリットは無いかも知れませんが ... 。
Erlang で書く場合は 1 コネクション 1 プロセスとして扱います。1 プロセスでは「状態管理」をする事が出来ます。
spdy_parser を使って適当にサーバの部分を書いてみることにしました。
以下の部分はすでに独立した軽量プロセスとして動いています。そのため「まっすぐ」書くことが可能です。
SETTINGS とか書いてないし、フレームも色々やっていません。手抜き実装です。
recv(Socket, Zinf, Zdef) ->
case ssl:recv(Socket, 0) of
{ok, Data} when byte_size(Data) =< 0 ->
recv(Socket, Zinf, Zdef);
{ok, Data} ->
case spdy_parser:parse_frames(Data, Zinf) of
[#spdy_syn_stream{headers = Headers} = SynStream] ->
Path = proplists:get_value(<<":path">>, Headers),
PushFrames = push(SynStream),
RawPushFrames = spdy_parser:build_frames(PushFrames, Zdef),
ok = ssl:send(Socket, RawPushFrames),
Frames = dispatch(SynStream, Path),
RawFrames = spdy_parser:build_frames(Frames, Zdef),
ok = ssl:send(Socket, RawFrames),
recv(Socket, Zinf, Zdef);
[#spdy_data{} = D] ->
recv(Socket, Zinf, Zdef);
[#spdy_goaway{} = _Goaway] ->
zlib:close(Zinf),
zlib:close(Zdef),
ssl:close(Socket),
ok;
[#spdy_ping{} = Ping] ->
RawPing = spdy_parser:build_frames([Ping], Zdef),
ok = ssl:send(Socket, RawPing),
recv(Socket, Zinf, Zdef);
_Frames ->
recv(Socket, Zinf, Zdef)
end;
{error, closed} ->
zlib:close(Zinf),
zlib:close(Zdef),
ssl:close(Socket),
ok;
{error, Reason} ->
zlib:close(Zinf),
zlib:close(Zdef),
ssl:close(Socket),
error(Reason)
end.
クライアントから SYN_STREAM が来たら サーバからの SYN_STREAM (PUSH) をガンガン送って最後に SYN_REPLY を送っています。
フレームの管理をしっかりしてないとか、ざっくり実装なのでまだまだ必要な実装は多いですが、適当に動かすにはかなりコード量は少ないと思います。
Erlang/OTP R16B という最近リリースされたバージョンで NPN に対応しました。
next_protocols_advertised でどのプロトコルを指定するかどうか可能です。
Opts = [binary, {ip, {127,0,0,1}}, {active, false}, {certfile, "priv/server_cert.pem"}, {keyfile, "priv/server_key.pem"}, {verify, verify_none}, {reuseaddr, true}, {next_protocols_advertised, [<<"spdy/3">>]}], case ssl:listen(Port, Opts) of {ok, LSocket} -> _Pid = spawn(fun() -> accept(LSocket) end); {error, Reason} -> error(Reason) end.
今までは Erlang/OTP 自体にパッチを当てる必要があったのでめんどくさかったです。
Erlang/OTP の SSL(TLS) 実装は完全に自前です。 OpenSSL は呼び出していますがそれは暗号処理を呼び出しているだけにすぎません。
ということで、簡単に書いてみた Erlang で SDPY 実装としてサーバサイドの PUSH 実装をデモしたいと思います。
並列での PUSH は実装が面倒そうだったので、さぼりました。こんな感じで実装してみたよという話があったら教えてください。
単に画像が表示されるだけですので、あまりたいした事は無いです。
何かあれば