Part 1. WCF に導入されている新たな設計概念
では順番に、WCF について説明していってみようと思います。……が、WCF を理解する上で重要なのは、実装方法というよりも、むしろ WCF がなぜ作られたのかとか、どういう考えやモデルに基づいて作られているか、という点だと思います。そこで、Part 1. では、まず WCF が持っているいくつかのきわめて重要な設計概念についてまとめて説明してみたいと思います。
・WCF の狙い
・エンドポイント (ABC モデル)
・WCF パイプライン(ABC モデル)と OSI 7 階層モデルの関係
・Message オブジェクトとエンコード方式
なお、以下の説明では、ある程度リモート通信に関する基礎知識がある(例えば ASP.NET XML Web サービスを使ったことがある)方を対象として記述しています。もしリモート通信技術を全く触ったことがないという場合には、まず ASP.NET XML Web サービス(*.asmx)などを軽く学習してから本エントリをお読みいただくことをお勧めします。
※ Part 1 は概念的に難しいので、よくわからない...という方は、Part 2 以降を読んでから、最後に戻ってきてもう一回読むとよいかと思います。
[Step. 1] WCF の狙い
WCF は、様々な通信プロトコルを、同一のプログラミングモデルの中で取り扱えるようにするための仕組みです。
もともと世の中に多種多様な通信プロトコルが存在するのは、通信プロトコルに求められる要件が、アプリケーションやシステムの特性によって大きく変化するため、ひとつの通信プロトコルでは多種多様な通信に対応できないからです。
- 信頼性を重視しなければならない場合(一般的な業務システムとか)
- 付加機能を充実しなければならない場合(トランザクション引き継ぎが必要な業務システムとか)
- とにかく性能を重視する必要のある場合(取引システムとかオンラインゲームとか)
このような理由から、世の中には様々な通信プロトコルが存在します(Windows 系に限ってみても、SOAP over HTTP, DCOM, TCP/IP, MSMQ, Service Broker, 名前付きパイプ, etc...)。しかし、一方で次のような問題も存在します。
- 通信プロトコルごとに、プログラミング方式(API)がまるで違う。
- 同一の通信特性を持つ通信であっても、簡単に置き換えることができない。
また、最近では WS-* (WS-Security や WS-ReliableMessaging, WS-AtomicTransaction など)といった高付加価値プロトコルも開発されていますが、こうしたプロトコルは従来の通信ミドルウェアの設計ではなかなかスマートに対応させることができません。こうした問題を解決するために設計されたのが WCF です。WCF を利用すると、以下のようなメリットが得られます。
- 様々な通信プロトコルを扱う際のプログラミングモデルが統合されます。
このため、新しい通信プロトコルを利用したアプリケーションに開発する際の学習コストが低減されます。 - WS-* プロトコルへの対応が非常にスマートにできます。
(※ この点は極めて重要です。詳細は後述します。) - 同一の通信特性を持つ通信プロトコルであれば、簡単にリプレースできます。
例えば、SOAP over HTTP ベースで開発された WCF のアプリケーションの通信方式を、TCP/IP ベースの通信に簡単に切り替えられます。
例えば一例を挙げると、"Hello World" メッセージを返すサーバアプリケーションを作成する場合、WCF を利用すれば、SOAP over HTTP だろうと TCP/IP だろうと、サーバのサービス実装は全く同じになります。構成設定ファイルを書きかえることにより、通信プロトコルを変更することができます。(※ ここでは単純化して解説しています。具体的な通信プロトコルの置換方法については Part 3. で扱います。)
[WCF サービス実装]
1: [ServiceContract]
2: public class WcfService
3: {
4: [OperationContract]
5: public string GetMessage(string name)
6: {
7: return String.Format("Hello World, {0}", name);
8: }
9: }
[SOAP over HTTP の場合の構成設定ファイルの書き方]
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Server.WcfService">
5: <endpoint address="https://localhost:8000/Server/WcfService"
6: binding="basicHttpBinding" contract="Server.WcfService" />
7: </service>
8: </services>
9: </system.serviceModel>
10: </configuration>
[TCP/IP の場合の構成設定ファイルの書き方]
1: <configuration>
2: <system.serviceModel>
3: <services>
4: <service name="Server.WcfService">
5: <endpoint address="net.tcp://localhost:7000/Server/WcfService"
6: binding="netTcpBinding" contract="Server.WcfService" />
7: </service>
8: </services>
9: </system.serviceModel>
10: </configuration>
※ 2 つ目のポイントについては結構勘違いしやすいので注意してください。当たり前のことですが、WCF は、通信プロトコルを任意に切り替えられるようにするものではありません。WCF を利用しても通信特性の異なるプロトコルへの切り替えはできません。たとえば、上記の「リクエスト/レスポンス」型のアプリケーションの通信プロトコルを、「一方向」型の通信プロトコルである MSMQ などに切り替えることはできません。
※ なお、WCF サービスを実装する場合、通常は上記のサンプルのように、サーバ側のコードに [ServiceContract], [OperationContract] 属性をつけたコードを記述しますが、昔ながらのリモート通信プログラミングモデルのように、インタフェース定義を分離することもできるようになっています。(下記参照) ただし、業務アプリケーションではこのようにインタフェース定義を分離する必要は通常ないので、本資料ではインタフェース分離しない形でサンプルコードを示していきます。(理由は SI 上にビジネスロジックを実装することはないためなのですが、これはまた別の機会に。)
1: [ServiceContract]
2: interface IWcfService {
3:
4: [OperationContract]
5: string GetMessage(string name);
6:
7: [OperationContract]
8: PubsDataSet GetAuthorsDataByState(string state);
9: }
10:
11: public class WcfServiceImpl : IWcfService {
12: public string GetMessage(string name) {
13: return "Hello World, " + name;
14: }
15: public PubsDataSet GetAuthorsDataByState(string state) {
16: authorsTableAdapter ta = new authorsTableAdapter();
17: PubsDataSet pds = new PubsDataSet();
18: ta.FillByState(pds.authors, state);
19: return pds;
20: }
21: }
さて WCF では、
- サーバ側のサービスを、[ServiceContract] や [OperationContract] の属性をつけた形で記述する。
- 構成設定ファイルで通信プロトコルを指定する。
という形で実装していくのですが、このような通信プロトコル切り替えを実現する源泉となっているのが、エンドポイントと呼ばれる概念です。
[Step. 2] エンドポイント (ABC モデル)
エンドポイントとは、サーバとクライアントの両側にできる「通信の出入り口」のことで、以下の 3 つの要素から構成されています。
- A (Address) : URL (例えば https://server:8000/WcfService など。)
- B (Binding) : 通信制御に利用するパイプラインの種類
- C (Contract) : SOAP メッセージの仕様 (すなわち I/F 仕様)
この A/B/C に関する構成をサーバとクライアントの両方で合致させることにより、適切な通信ができるようになる...と WCF の本には書かれているのですが、最初にこれを読んだときにはぶっちゃけ何のことだかさっっっぱりわかりませんでした;。たぶん、開発者の方々の場合には、以下のようなエンドポイント内部の構造を具体的に示した方がわかりやすいでしょう。
WCF によるクライアントからサーバへの通信は、以下の流れで進みます。
平文 SOAP メッセージの組み立て
プロキシクラスのメソッドを呼び出すと、プロキシクラスは平文の SOAP メッセージを組み立てます。
クライアント側エンドポイントによるパイプライン処理
クライアント側エンドポイントによって、平文の SOAP メッセージに対する適切な加工処理(認証情報付与やデータ暗号化など)が行われます。
サーバ側リスナへのデータ送信
例えば WS-Security を使っている場合には、クライアント側のエンドポイントパイプラインにより加工された(すなわち暗号化された)SOAP メッセージが、通信チャネル経由で送信されることになります。
サーバ側エンドポイントによるパイプライン処理
2. の逆の処理(解読・復元処理)が行われます。
サービス実装クラスへのディスパッチ
平文となった SOAP メッセージを元に、実際のサービスが呼び出されて処理されます。
そしてサーバ側で処理された結果については、逆の流れをたどってクライアントに届けられます。このように、WCF サービスを正しく動作させるためには、構成設定ファイル(前掲)でこの A/B/C を正しく設定しなければならないのですが、上図から、A/B/C が具体的に何に紐づいているのかがわかると思います。
- A (Address) : リスナまたはトランスポートの種類およびその URL
- B (Binding) : SOAP メッセージに対する前加工・後加工処理を施すためのパイプラインの種類
- C (Contract) : SOAP メッセージの仕様
そして、WCF のマルチプロトコル対応は、この Binding (及びその下側にある A)の取り換えによって実現されています。例えば、Step 1. で示した SOAP over HTTP 及び TCP/IP 通信の切り換えの例の場合は、Binding を basicHttpBinding から netTcpBinding に切り替えていますが、これは下図のように、WCF パイプラインを入れ替えることに相当します。
つまり、端的にいえば、
Binding の変更 = WCF パイプラインの交換 = 通信ランタイムの全とっかえ
を意味します。(このためバインディングを変更すると、通信特性や利用可能な機能がまるごと変わる) これが、同一のプログラミングモデルで異なる通信プロトコルを取り扱うことができる仕組みの源泉になっています。
※ 繰り返しになりますが、通信特性の異なる Binding へ取り替えても、アプリケーションは動作しない(するはずがない) ので注意が必要です。
なお、WCF には様々な Binding が用意されていますが、まず真っ先に覚えるべき Binding は以下の 4 つです。(覚えてしまうことを推奨)
[Step. 3] WCF パイプライン(ABC モデル)と OSI 7 階層モデルの関係
さて、上で「Binding の変更は、通信ランタイムの全とっかえに相当する」と書いたのですが、そもそもそんなことが許されていいのか?! ……という人も多いと思います(私もそう思います;)。ですが、この ABC モデルこそが WCF の設計上の一番の肝であり、さらにこれが実は OSI 7 階層モデルの上に WCF 3 階層モデルとでも呼ぶべき新しい階層をもたらすものである、ということに気づくと、WCF の設計の見事さがわかってくると思います。
※ WCF 3 階層モデルというのは nakama が勝手に呼んでるものなので、正しいかどうかはわかりませんが、私はこういうふうに理解しました。
この WCF 3 階層モデル(nakama 命名)を説明する前に、まず SOAP と WS-* の仕様の関係を改めて復習してみたいと思います。
ご存じのとおり、SOAP (Simple Object Access Protocol) は、XML 形式にエンコードされた、リモートメソッド呼び出し要求のためのメッセージです。
SOAP メッセージは通信プロトコルとは独立して設計されているのが大きな特徴でした(つまり、この SOAP メッセージは HTTP 通信、TCP/IP 通信、極端なことを言えばメールの添付ファイルとしてサーバに送りつけることも可能)。従来の様々な RPC (リモートメソッド呼び出し)技術は、通信プロトコルと一体化して設計されてきたわけですが、SOAP はこの前提を覆し、メッセージと通信プロトコルを分離し、「通信プロトコルにはお好きなものを使ってね」というスタンスを取った、というのが大きな特徴でした。
でもって、SOAP メッセージを HTTP 通信で運んだり(SOAP over HTTP)、TCP/IP 通信で運んだり(SOAP over TCP/IP)している分にはたいして問題がなかったのですが、しばらくすると、この SOAP メッセージに WS-* と呼ばれる様々な拡張機能が登場します。代表格が WS-Security で、これは SOAP メッセージを暗号化するための仕様でした。(雰囲気つかむために SOAP メッセージが暗号化された様子を下に示します。詳細は理解しなくてよいです。)
ところがここで「???」となってしまう疑問が生じます。それは、レイヤの逆転現象です。例えば、HTTPS という通信プロトコルを OSI 7 階層モデルに基づいて考えた場合、レイヤの重なり方は
- アプリケーション層(HTTP)
- 暗号化処理を行う層(SSL)
- 通信制御を行う層(TCP/IP)
となっているのですが、WS-Security を使った SOAP メッセージを HTTP で搬送する場合、
- WS-Security による暗号化処理
- SOAP メッセージ
- HTTP 通信
- SSL
- TCP/IP
という形のスタックになっている、つまり HTTP 通信というアプリケーション層の上に、改めて暗号処理を行うレイヤを積み上げている形になってしまっています。設計上の是非はともかく、WS-Security をはじめとした WS-* の仕様を見たときに、
「これって SOAP 上にもう一度プロトコルを全部作り直してるってこと??」
という疑問を抱いたのは(きっと)私だけではないと思います(というか思いたい...)が、通信プロトコル中立性を保ちながら、その上でメッセージを暗号化したりするためには、確かにこうした仕様は必要になります。
ところがここで新たに問題になるのは、では通信インフラを WS-Security などに対応させようと思った場合に、どのような形で対応させればよいのか?(誰がどこで WS-Security の暗号化や復号処理を行うのが設計上スマートなのか?)という点です。例えば、ASP.NET XML Web サービスは、ASP.NET ランタイムが持つ HTTP パイプライン上で動作していますが、その動作モデルは下図のように描くことができます。
ASP.NET ランタイムの HTTP パイプラインは、文字通り HTTP 通信に紐づいた形で設計され、HTTP プロトコルを意識しながら動作するものです。もちろんこのパイプラインの中で SOAP メッセージの暗号化や復号処理を行うようなモジュールを作ることもできるでしょうが、それでは SOAP 及び WS-* が持つ基本設計概念である、通信プロトコル独立性に反します。
そこで現れたのが、WCF の ABC 3 階層モデル(nakama 命名(しつこい;))です。
このように描くとわかりやすいのですが、要するに A/B/C とは、OSI 7 階層モデルの上に、SOAP 及び WS-* 仕様に基づく制御レイヤを導入しようというものです。このレイヤが導入されることにより、クライアントコードや WCF サービスのコードは、以下のようなメリットを享受することができるようになります。
- WS-* 関連の制御から解放される。(WS-* の制御を抽象化できる)
- 通信プロトコルから独立したメッセージのやり取りが可能になる。
このうち前者は極めて重要な点で、WS-* を利用する際には、Binding レイヤで複数回のパケット(SOAP メッセージ)の交換が自動的に行われることがあります。もうちょっと突っ込んで説明するため、例えば、HTTPS (HTTP over SSL)と、WS-Security の 2 つを比較してみます。まず、HTTPS を考えてみてください。
- 一般的に、HTTPS によるサーバとの通信は、アプリケーションレベルからみると「単発のリクエスト/レスポンス」に見えます。
- しかし実は TCP/IP レベルでは、SSL セッション鍵の交換のために、サーバ/クライアント間で複数回のパケット交換(ネゴシエーション処理)が行われています。
つまり HTTPS では、SSL セッションを張るために複数回のパケット交換が行われているものの、アプリケーションレベルではこのパケット交換を意識しなくて済むようになっている、ということになります。
実は WS-Security でも、Binding レイヤにより同様のことが行われます。つまり、
- クライアントコードから Binding (WCF パイプライン)に SOAP の平文メッセージを投入します。
- WCF パイプラインでは WS-Security の暗号化に必要なセッション鍵を入手するため、サーバ側の WCF パイプラインとの間で、SOAP メッセージを何度か交換します。(WS-SecureConversation プロトコルによるメッセージ交換が行われる)
- セッション鍵が交換されると、実際の SOAP メッセージが暗号化されてサーバに送信されます。
という挙動が行われます。見掛け上、クライアントコードからプロキシクラスの呼び出しが一度であっても、実は HTTP 通信レベルでみると、複数回の SOAP メッセージのやり取りが行われていることがある、ということになります。しかし、WCF の Binding レイヤがこれを抽象化してくれているため、アプリケーションレベルではこのネゴシエーションのためのパケット(SOAP メッセージ)交換を意識しなくて済むようになっている、ということになります。
以上を踏まえれば、WCF のエンドポイントの ABC モデルとは、OSI 7 階層モデルの上に、SOAP 及び WS-* による 3 階層の抽象化レイヤを作るものである、と説明できるのではないかと思います。(← という説明がたぶん最も的確じゃないかと思うんですが、こういう説明がなされている資料を見たことがない....)
[Step. 4] Message オブジェクトとエンコード方式
さて、上記において、WCF パイプラインが SOAP 及び WS-* の制御を抽象化してくれるものである、という説明をしたのですが、この動作は、HTTP 通信だけでなく、すべての通信プロトコルにおいて共通の挙動になります。つまり、
- WCF では、TCP/IP 通信だろうと名前付きパイプ通信だろうと、必ずSOAP が使われる。
- ただし、通信チャネル(トランスポート)によっては、SOAP メッセージが XML 形式以外にエンコードされることがある。
という挙動をします。このことを、HTTP 通信を使う場合と、TCP/IP 通信を使う場合とで比較しながら説明します。
上図では、HTTP 通信を使うために basicHttpBinding パイプラインを、TCP/IP 通信を使うために netTcpBinding パイプラインを使っていますが、どちらの場合であっても、
- パイプラインを通過するデータは、SOAP メッセージ。(正確には、SOAP メッセージのオブジェクト形式である System.ServiceModel.Channels.Message オブジェクト)
- データを暗号化する場合には、どちらの場合でも WS-Security 仕様に基づく暗号化処理が行われる。
となります。しかし、トランスポートレイヤでは、
- basicHttpBinding を使った場合には、SOAP メッセージ(のオブジェクト形式である Message オブジェクト)が、XML 形式にエンコードされて転送される。
- netTcpBinding を使った場合には、SOAP メッセージ(のオブジェクト形式である Message オブジェクト)が、バイナリ形式にエンコードされて転送される。
となり、TCP/IP 通信を使う場合にはバイナリフォーマットでのデータ転送が行われることになります。ちなみにご参考までに、Message オブジェクトが XML 形式にエンコードされた場合と、バイナリ形式にエンコードされた場合を以下に示します。バイナリ形式のデータをよく見ると、タグではなく "." 区切りでデータがエンコードされていることがわかります。
[XML 形式にエンコードされた Message オブジェクト]
[バイナリ形式にエンコードされた Message オブジェクト]
つまり、TCP/IP 通信(netTcpBinding)を使った場合には、バイナリエンコード+TCP/IP 通信によるデータ転送が行われるのですが、そこで運ばれているデータは SOAP データそのものです。
※ WCF の開発チームによると、RPC におけるリモート呼び出しデータを持つときに最も優れたフォーマットを追及したら、結局 SOAP と同じデータ項目になったため、WCF パイプラインを通す「RPC 呼び出しデータ」の構造として、SOAP XML データのオブジェクト表現を使うことにしたそうです。
そして、WCF ランタイムでは、リモートのサービス呼び出しの際のメッセージデータ構造として SOAP データを使っているが故に、WS-* 仕様を、すべての通信プロトコルに対して使うことができるようになっています。例えば、
SOAP over HTTP 通信において、通信データを暗号化したい場合には、WS-Security を使うことができます。
ですが、TCP/IP 通信においても、通信データを暗号化したい場合には、WS-Security を使うことができます。
(※ ワイヤを流れるデータはちゃんとバイナリエンコードされることに注意。WS-Security により暗号化された Message オブジェクトが、バイナリエンコードされてワイヤに流れる形になります。)
もちろん、実質的に WS-* 仕様を使う必要・意味がないケースもあります。例えば、名前付きパイプ通信はもともと信頼性のある通信であるため、WS-ReliableMessaging 仕様を適用しても意味がないがありません(このため、netNamedPipeBinding では WS-ReliableMessaging 仕様は使えないようになっています)。ですが、考え方としては、Binding パイプラインが基本的に通信プロトコルに対して中立である、というのが非常に興味深い設計であるといえるでしょう。
※ なお、上記のように、WS-* と通信プロトコルの間には組み合わせ上の適否があるため、組み込みの Binding では利用できる WS-* やエンコード方式に制限が加わっていることがほとんどです。ですが、CustomBinding を使って自分でパイプラインを組み立てると、ある程度はこの組み合わせを自由にすることができます。(例えば、HTTP 通信でバイナリエンコードをしたい場合には、CustomBinding を利用し、httpTransportChannel と binaryEncodingChannel を組み合わせる、など。)
[ここまでのまとめ]
というわけで、とんでもなく説明が長くなりましたが、ここまでの解説をまとめると以下のようになります。
- WCF を使うと、様々な通信プロトコルに対して、似たようなプログラミングモデルでの開発を行うことができるようになる。
- WCF を使うと、同一の通信特性を持つ別の通信プロトコルへの切り替えが簡単にできるようになる。
- WCF では、OSI 7 階層モデルの上に、WS-* などの処理を行うための 3 階層モデルを構築している。これにより、通信プロトコルや WS-* などの付加処理が高度に抽象化されている。
- WCF では、すべての通信手法において、リモート呼び出しのデータが SOAP メッセージ(のオブジェクト表現である Message オブジェクト)として管理される。
次回の Part 2. では、実際に WCF のサーバアプリケーションを開発してみていくことにしたいと思います。
……と、概念ばっかり説明してるとわかりにくいですよね;。これだけだとわからない方も多いと思いますが、Part 2 以降で徐々にわかっていただけると思いますので、しばらくお付き合いいただけると幸いです。
Comments
- Anonymous
January 23, 2009
Nobuyuki Akama has started a series of articles that give a very good introduction to WCF. The first