メランジュ-ネットワークプロトコル用のDSL

すべてのプログラマは遅かれ早かれデータを転送する必要があります。 Javaには約9000のシリアル化ライブラリが存在することは秘密ではありません。C++では存在するように見えますが、存在しないようです。 大多数の人にとって幸運なことに、Google Protobufは数年前に登場し、データ構造を定義する非常に便利な方法をもたらし、すぐに人気を博しました。 これは実際、大衆が利用できる最初のライブラリであり、XMLのようなものをいじることなく、ネットワーク上で既製のデータ構造を駆動できます。 庭には2008年でした。



少し戻りましょう。 2006年、シンプルなインドのプログラマー(どんなに疑わしい音でも!)世界で最も有名なOCaml開発者の1人であり、最新の本Real World OCamlの著者であるAnil Madhavapeddiは、ケンブリッジで博士論文を擁護しました。 今日お話しするのは彼女についてです。



アニルはすぐにグーグルより先に進んだ。 彼はすぐに、なぜ人々は通常、正式なデータ構造をネットワーク経由で送信するのかと思いました。 ある種のプロトコルを実装するため。 そして、プロトコルとは何ですか? これはある種の有限状態機械です。 そして、複雑で、適切に設計され、実績のあるプロトコルの良い例はどこで入手できますか? はい、通常のネットワークスタックで! そこで、一連のネットワークデータ構造とプロトコルを取りました:イーサネットフレーム、IPv4、ICMP、TCP、UDP、SSH、DNS、DHCP、および問題の説明:これらのプロトコルの大部分(特にSSHとDNS)は実装され、「手」と呼ばれ、 Cに典型的なバッファオーバーフローがなく、すべての遷移が自動的に行われ、これをすべて検証できるため、通常ではなく迅速に動作するようになります。



誰も論文を読むことはないので、すぐに言います。これは成功した以上のことです。 作業の結果に基づいて、DNSおよびSSHサーバーのリファレンス実装が作成され、BINDおよびOpenSSHとの比較が行われました。 従来のOCaml実装と比較すると、パフォーマンスはわずかなものからほぼ2倍に向上します。 さらに、SSHのRFCでエラーが見つかりました(ワークグループに通知され、RFCが修正されました)。 何が行われたのか、それとどのように生きるのかについては、以下を読んでください。



MPL


まず、OCamlの2つの記述言語とその翻訳者が作成されました。 最初の言語は、パッケージの構造を記述するMeta Packet LanguageまたはMPLです。 一般的には、protobufの類似物ですが、完全ではありません。 まず、MPLは構造にオーバーヘッドを追加しません。 一般的に。 データ型などを示す余分なビットはありません。 一方、protobufのように、新しいフィールドを追加して構造を簡単に拡張することはできません。他方、protobufを使用してTCPヘッダーを読み取ることはありません。 第二に、MPLは、ネットワーク構造に必要なすべてのロジック(パッケージングまたはアライメント)、および構造の他のフィールドに依存するフィールドの変数セットやフィールド値などをすぐに実装します。 例として、IPv4ヘッダーを見てください。

packet ipv4 { version: bit[4] const(4); ihl: bit[4] min(5) value(offset(options) / 4); tos_precedence: bit[3] variant { |0 => Routine |1 -> Priority |2 -> Immediate |3 -> Flash |4 -> Flash_override |5 -> ECP |6 -> Internetwork_control |7 -> Network_control }; tos_delay: bit[1] variant {|0 => Normal |1 -> Low}; tos_throughput: bit[1] variant {|0 => Normal |1 -> Low}; tos_reliability: bit[1] variant {|0 => Normal |1 -> Low}; tos_reserved: bit[2] const(0); length: uint16 value(offset(data)); id: uint16; reserved: bit[1] const(0); dont_fragment: bit[1] default(0); can_fragment: bit[1] default(0); frag_offset: bit[13] default(0); ttl: byte; protocol: byte variant {|1->ICMP |2->IGMP |6->TCP |17->UDP}; checksum: uint16 default(0); src: uint32; dest: uint32; options: byte[(ihl * 4) - offset(dest)] align(32); header_end: label; data: byte[length-(ihl*4)]; }
      
      







ここでは、パケットの内容はデータバイトの配列として記述されます(他のすべての可能なプロトコルを記述しないように)が、代わりにレコードがある可能性があります。

 classify (protocol) { |1: "ICMP"-> data: packet icmp(); |2: "TCP" -> data: packet tcp(); |3: "UDP" -> data: packet udp(); };
      
      







そして、IPv4パケットを読み取ると、その内容をすぐに解析します。 その結果、パッケージの(デ)シリアル化は魅力的なタスクに変わり、データを適切にパックする方法や、どこに配置する必要があるかについて考える必要はありません。 そのため、人気のあるMessagePack形式のほぼ完全な実装には、約40行かかりました。



残念ながら、この言語には欠点もあります。 そのうち2つを数えました。 まず、再帰的なパッケージは禁止されています。 これにより、MessagePackを完全に実装することができなくなりました。MPLは、リストまたはマップがリストまたはマップに含まれているとは言えません。 これは意図的に行われるため、パッケージのすべての読み取りは厳密に最終的なものになりますが、これは簡単ではありません。 2番目の問題:データ型を定義できません。 Anilは、SSHに存在するバイト、ビット、数字、文字列、またはmpintなどのネットワークの標準タイプを実装しましたが、このリストは修正されています。 突然mpint1型を使用するssh-agentプロトコルを実装したい場合は、それをバイトの配列として記述し、コードで解析するだけです。 サポートされているタイプのリストを拡張する唯一の方法は、MPLコンパイラにパッチを書き込むことです。これは最も簡単なタスクではありません。



SPL


2番目の言語は、Statecall Policy LanguageまたはSPLでした。 これは、有限オートマトンを記述するための言語、つまりプロトコルの中心です。 厳密に言えば、すべての言語の有限状態マシンを作成するためのライブラリがきちんと存在しています。 それらとのSPLの違いはわずかです(オートマトンのプログラムされたタスクの代わりに、その記述言語を除いて)。 まず、SPLコンパイラーは、プログラムモデルSPINの検証者のために、PROMELAですぐに記述を生成できます。 正直に言って、私はスピンを理解できなかったので、この場所で、著者はクールだという言葉を使わざるを得ませんでした。 次に、状態名Receive_NAMEおよびTransmit_NAMENAMEはMPLのメッセージタイプ)を使用して、状態マシンをMPLのデータ構造と密接に統合できます。 これについては後で説明しますが、ここでは、SSHでの承認のための状態マシンの説明の例を見てみましょう。

 automaton auth (bool success, bool failed) { Receive_Transport_ServiceAccept_UserAuth; Transmit_Auth_Req_None; Receive_Auth_Failure; do { either { always_allow (Receive_Auth_Banner) { either { Transmit_Auth_Req_Password_Request; auth_decision (success); } or { Transmit_Auth_Req_PublicKey_Request; auth_decision (success); } or { Transmit_Auth_Req_PublicKey_Check; either { Receive_Auth_PublicKeyOK; } or { Receive_Auth_Failure; } } } } or { Notify_Auth_Permanent_Failure; failed = true; } } until (success || failed); }
      
      





ご覧のとおり、SPLのステートマシンを使用すると、かなり分岐したロジックを記述したり、同じSPLで記述された関数( auth_decision )の呼び出しを挿入したり、変数を操作したりできます。



どのように作業するのですか?


残念ながら、プロジェクト(全体はMelangeと呼ばれます)はドキュメントが豊富ではありません。その主な情報源は論文です。 そのため、製品全体の動作を実証すると同時に、このようなクイックスタートガイドのような小さな概念実証を作成することにしました。 これを行うには、小さなネットワークアプリケーションを作成する必要があります。 シンプルで明確なプロトコルを備えたシンプルなアプリケーションの役割については、古き良きゲームである海戦を選択しました。 これが、メッセージ構造の外観です。

 packet message { message_type: byte; message_id: uint16; classify (message_type) { | 0:"Shot" -> row: bit[4]; column: bit[4]; | 1:"ShotResult" -> result: byte variant { |0 -> Missed |1 -> Damaged |2 -> Killed }; | 2:"Disconnect" -> (); }; }
      
      





メッセージには、ショット、ショットの結果、何らかの理由で切断したい情報の3種類があります。 次に、提案されているプロトコルを見てみましょう。

 automaton seawar () { Initialize; during { multiple(1..) { Ready; either { Transmit_Message_Shot; Receive_Message_ShotResult; } or { Receive_Message_Shot; Transmit_Message_ShotResult; } } } handle { either { Transmit_Message_Disconnect; exit; } or { Receive_Message_Disconnect; exit; } } }
      
      





初期化(オートマトンの初期状態)から開始し、各ステップで撮影または撮影を行います。 すべてがシンプルです。 さらに、何らかの理由で切断タイプのメッセージが表示された場合、これはゲームオーバーであり、マシンを停止する必要があることを意味します。



次に、これがコードからどのように使用されるかを見てみましょう。 メッセージを読み取るために、データで満たされる特別なMPLバッファーを使用します。この場合、ネットワークから取得します。

 val mutable env_ = Mpl_stdlib.new_env (String.make 4 '\000'); val mutable tick_ = Protocol.init (); method tick state = tick_ <- Protocol.tick tick_ state; method send_message msg = ( Mpl_stdlib.reset env_; self#tick (msg#xmit_statecall :> Protocol.s); if not (Thread.wait_timed_write sock_ 10.) then self#disconnect ~exc_text:"Timeout"; Mpl_stdlib.flush env_ sock_ ); method receive_message = ( Mpl_stdlib.reset env_; if not (Thread.wait_timed_read sock_ 300.) then self#disconnect ~exc_text:"Timeout"; Mpl_stdlib.fill env_ sock_; let msg = Message.unmarshal env_ in let state = Message.recv_statecall msg in self#tick state; msg );
      
      





送受信された各メッセージは必ず状態マシンを新しい状態に遷移させることに注意してください。 msg#xmit_statecallおよびMessage.recv_statecall msg呼び出しは、メッセージ(ShotResultなど)に基づいて、対応する状態の名前( Transmit_Message_ShotResultおよびReceive_Message_ShotResult )を作成します。 このため、不正なマシン移行によりBad_statecall例外が発生した場合、潜在的なプログラムエラーのほとんどがここで検出されます。 たとえば、AIの場合、すべてが単純である場合-1つのスレッドで完全に同期して動作し、このような単純なタスクで問題が発生することはありません。グラフィカルインターフェイスでは、すべてがより複雑になります。





たとえば、すべてが簡単に「爆発」する方法の簡単な例です。 グラフィカルインターフェイスについては、新しくリリースされたQt 5.2フレームワークを使用しました。これについては、同志のDmitry KosarevがOCamlバインダーを作成しました (興味があれば、別の投稿でお伝えします)。 別のスレッドで敵フィールドのセルをクリックすると、次のコードを実行できます。

 let send_shot col row = ignore(game#send_message (Message.Shot.t ~row:row ~column:col)); let result, state = game#receive_message in (match result with | `ShotResult x -> Board.mark opp_board row col x#result; next_turn state x#result | `Disconnect x -> game#disconnect ~send:false | _ -> game#disconnect ~exc_text:"Unexpected_Message_Type" ~raise_exc:true ~send:true)
      
      





ダブルショットが防止されない場合、このメソッドは2回呼び出されます。この場合、ショットが連続する2つのメッセージを送信するか、ミスメッセージを受信した後に2番目のメッセージを送信できます。



コードの完全なレビューで記事が煩雑にならないように、誰でもダウンロードして表示できるソースへのリンクを提供することをお勧めします。



おわりに


次に何をするか、読者は私に尋ねます。 さて、ある種の限界的なツールは、4年間更新されておらず、理解できない言語で機能しています。 Node.js、MongoDB、イチゴのスムージーを持っているのに、なぜこれがすべて必要なのですか?



読者は正しい。 このツールは時代遅れであり、私が言及したいくつかの重大な欠点があります。 しかし同時に、どの方向に開発する必要があるかを示しています。 したがって、すべての宣言的な説明、グラフィカルインターフェイス、および最も愚かなAIを含むアプリケーションコード全体は850行です。 これはもちろん「javascriptの30行」ではなく、多すぎません。



ほぼ8年前、ネットワークインタラクションの発生方法が正確に示され、ほぼ6年前にGoogleが普及させたのはその半分だけでした。 アイデアにはロケット科学はありません。これは事実であり、個々のコンポーネントはすべて長い間書かれています。 あなた、%USERNAME%は、来年の新年にこのアイデアを実現し、世界的に有名になり、世界を奴隷にする絶好の機会を持っています。 まあまたはそのようなもの。



All Articles