サービス ステーション
RESTful クライアントの構築
Jon Flanders
RESTful について取り上げるサービス ステーションの今回のテーマは、RESTful サービスに対するクライアントの構築です。RESTful サービスに対するクライアントの構築は、SOAP や WSDL とは違ってメタデータからクライアントを自動生成する機能がないということを主な理由に、難しいと思われがちです。しかし、実際には、他の種類のコードの記述とそれほど違っているわけではありません。最初は、特定のプログラミング パラダイムに慣れるまでの準備期間が必要です。しかし、そのパラダイムのこつさえ掴んでしまえば、コードの記述はどんどん容易になります。この点は RESTful サービスのクライアントを作成する場合にも当てはまることを、私が接してきた多くの開発者が認めています。
クライアントの基本
ここでは、クライアントとサービスとの間で、REST を使用して対話する際の基本について説明します。でもその前に、REST について簡単にまとめておきましょう。REST とはアーキテクチャ スタイルの 1 つで、World Wide Web の基盤を提供する原理と同じ原理に基づいて構築されています。クライアントから HTTP 要求を行うことによってサービスとの対話を開始し、サービスは HTTP 応答を返すことによって対応します。多くの場合、こうした対話の中に、アプリケーションを起動するためにクライアント コードが使用できるリソース表現が含まれます。
Hyper-V (Windows Server 2008 に組み込まれる仮想化テクノロジ) に関連する情報と機能を公開するサービスがあるとしましょう。このサービスの "エントリ ポイント" となる URI が "http://localhost/HyperVServices/VMs.svc/" だとすると、この URI によって特定されるリソース表現を取得するためにクライアントから HTTP GET 要求を行うことになります。この場合、リソースは XML 形式で、特定のコンピューターにインストールされているすべての仮想マシン (VM) の一覧を表現します。
図 1 簡単な HTTP GET 要求
MSDN Magazine の 2009 年 1 月号 (https://msdn.microsoft.com/ja-jp/magazine/dd315413.aspx) の、REST について取り上げたサービス ステーションの初回コラムで説明したように、REST を使用するメリットの 1 つは、ささいなことかもしれませんが、HTTP 要求ツールを使用して、サービスに対する初期テスト要求を行えることです (このようなツールを使用すると、問題点のデバッグにも非常に有効です)。このコラムでは、Fiddler (http://www.fiddler2.com/fiddler2/、英語) という無償ツールを使用して HTTP GET 要求を行い、すべての仮想マシンを表現するリソースを取得します (図 1 参照)。もちろん、Fiddler のようなツールは貴重ですが、アプリケーションを構築するにはこのサービスに対するコードを記述することは必要です。では、まず、HTTP 要求を行う場合の基本について説明してから、実際にリソースを読み取るときに生じる問題点を取り上げます。
.NET Framework では、どのリリースでも、RESTful サービスとの対話に使用できる、基本的な HTTP API を常に提供しています。この API の中心となるのは、HttpWebRequest 型と HttpWebResponse 型です。HTTP 要求を行うには、HttpWebRequest インスタンスを作成および構成してから、HttpWebResponse を要求します。図 2 に、この例を示します。RESTful サービスへの要求を行うには、次の単純な一連の手順を実行します。
- 適切な URI を使用していることを確認する
- 適切なメソッド (HTTP 動詞) を使用していることを確認する
- 状態コードが "200 OK" の場合に応答を処理する
- その他の状態コードの場合は必要に応じて対処する (後述)
手順 1. と手順 2. はごく簡単なので、一般には手順 3. (場合によっては手順 4.) が難しくなります。
手順 3. の作業の大半は、リソース表現を適切な方法で処理することです。XML がリソース応答に使われることが多いため、このコラムでは XML について説明します (他によく使われるメディアの種類といえば JSON でしょう)。リソースが XML のときは、そのリソースを解析して、そのリソースから必要なデータを抽出できるコードを記述する必要があります。.NET には、XML を解析するためのオプションがいくつかあります。たとえば、XmlReader クラスはその有効性が実証されています。XmlDocument クラスにも可能性があります。これらの型のいずれかを使用して、XML を手動で解析したり、XPath を使用して XML 内を移動したりできます。新たに登場した .NET 3.0 では、XDocument クラスも取得できます。LINQ to XML と組み合わせるときは、このクラスが XML を処理する際の事実上の選択肢になります。図 3 に、XDocument を使用して VM の一覧を処理するコード例を示します。
図 2 簡単な HttpWebRequest コード
string uri = "http://localhost/HyperVServices/VMs.svc/";
var webRequest = (HttpWebRequest)WebRequest.Create(uri);
//this is the default method/verb, but it's here for clarity
webRequest.Method = "GET";
var webResponse = (HttpWebResponse)webRequest.GetResponse();
Console.WriteLine("Response returned with status code of 0}",
webResponse.StatusCode);
if (webResponse.StatusCode == HttpStatusCode.OK)
ProcessOKResponse(webResponse);
else
ProcessNotOKResponse(webResponse);
LINQ to XML と匿名型を併せて使用すると、XML をアプリケーションから処理できるオブジェクトに変換するための簡単かつ優れた方法が提供されます。もちろん、匿名型の代わりに定義済みの型を使用することもできます。
SOAP ベースや REST ベースのサービスに対してプログラミングする際によく使われるのもう 1 つの方法は、応答が .NET 型に自動的にシリアル化解除されるようにする方法です。SOAP の場合は、WSDL から生成されるプロキシで行うのが一般的です。Windows Communication Foundation (WCF) と REST では、この処理を実現する方法が複数あります。1 つ目 (あまりお勧めできませんが、完璧を期すために紹介します) は、WCF が対称性を持つという性質を利用して、クライアント側で WCF サービス コントラクトの定義を使用する方法です。実際、REST 向けの WCF サポートには、WebChannelFactory という型があり、この型を使用すると、サービス コントラクト定義に照らして、クライアント チャネルを作成できます。クライアント側でこの種のプログラミングをお勧めしない理由は 2 つあります。1 つは、クライアントの作成に手動の操作が多くなり、間違いを起こしやすくなるためです。もう 1 つは、厳密に型指定されたサービス コントラクトを使用することにより、クライアントとサービスとの間に密接な結び付きが生じるためです。こうした密接な結び付きを避けることが、Web が成功する大きな理由の 1 つです。そのため、Web をプログラムから使用する場合もこの傾向を保つことを考えます。
XML シリアル化を使用するもう 1 つの方法は、HttpWebResponse.GetResponseStream メソッドを使用し、XML からオブジェクトに手動でシリアル化解除する方法です。これは、XmlSerializer シリアライザーか WCF の DataContract シリアライザーのいずれかを使用して実現できます。XmlSerializer では XML ドキュメント内の多くのバリエーション (属性や修飾されていない要素など) を扱えるため、ほとんどの場合、DataContract シリアライザーよりも XmlSerializer の方がよく使用される手法です。
図 3 XDocument を使用した RESTful 応答の処理
var stream = webResponse.GetResponseStream();
var xr = XmlReader.Create(stream);
var xdoc = XDocument.Load(xr);
var vms = from v in xdoc.Root.Elements("VM")
select new { Name = v.Element("Name").Value};
foreach (var item in vms)
{
Console.WriteLine(item.Name);
}
問題が、通常、RESTful サービスではメタデータを公開しないという点に立ち返ったように思えます。メタデータが公開されていれば、このコードの一部またはすべてを自動生成するためにそのメタデータを使用できます。RESTful 関係者の多くはこのことを問題とは考えていません (繰り返しますが、サービス定義に照らして自動生成を行うという行為は、密接な結び付きをもたらす有害因子と見なされる可能性があります)。しかし、このようなツールがないことが、REST の導入を妨げる一因になっていることも事実です。
マイクロソフトが .NET 3.5 において REST プログラミング モデルの機能拡張として提供している WCF REST Starter Kit (asp.net/downloads/starter-kits/wcf-rest/、英語) で採用した興味深い手法の 1 つは、部分的な自動生成機能を提供する方法です。このスタート キットをインストールすると、Visual Studio 2008 の [編集] メニューに新しいメニュー項目が表示されます (図 4 参照)。
図 4 [Paste XML as Types] (XML を型として貼り付け) メニュー項目
このコマンドが使用しているモデルは実に単純です。サービスが公開する判読可能なドキュメントから、または Fiddler のようなツールを使って要求を行うことで、XML リソース表現をクリップボードにコピーします。すると、一連の XmlSerializable 型が作成されるので、これを使用して、ストリームを HttpWebResponse からオブジェクトに変換できます (図 5 参照、見やすくするために生成された型の本体は示していません)。
図 5 [Paste XML as Types] (XML を型として貼り付け) を使用して生成されたコード
var stream = webResponse.GetResponseStream();
//in a real app you'd want to cache this object
var xs = new XmlSerializer(typeof(VMs));
var vms = xs.Deserialize(stream) as VMs;
foreach (VMsVM vm in vms.VM)
{
Console.WriteLine(vm.Name);
}
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "",
IsNullable = false)]
public partial class VMs
{
private VMsVM[] vmField;
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("VM")]
public VMsVM[] VM
{
get
{
return this.vmField;
}
set
{
this.vmField = value;
}
}
}
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "2.0.50727.4918")]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
public partial class VMsVM
{
//omitted
}
REST Starter Kit は、XmlSerializer の使い方を簡略化するだけでなく、HttpWebRequest や WebResponse といった API の上位に、非常に優れた API も提供します。次のコードは、図 1 の単純な GET 要求を REST Starter Kit の HttpClient API を使用して書き直したものです。
string uri = "http://localhost/HyperVServices/VMs.svc/";
var client = new HttpClient();
var message = client.Get(uri);
ProcessOKResponse(message.Content.ReadAsStream());
HttpClient クラスでは、.NET の HTTP API の使用が大幅に簡略化され、使用している動詞が明らかにされます。また、次のコードからわかるように、[Paste XML as Types] (XML を型として貼り付け) で生成された機能の使用も簡略化されます。
var vms = XmlSerializerContent.ReadAsXmlSerializable<VMs>(
message.Content);
REST Starter Kit は、まだ、マイクロソフトからの正式なサポートは受けられませんが、非常に重要な点を 1 つ示しています。つまり、REST クライアントのプログラミングを簡略化でき、RESTful サービスに WSDL ファイルのような完全なメタデータがなくても、プログラミングを部分的に自動化できます。
HTTP の使用
クライアントとサービスで HTTP を活用する 1 つの方法は、状態コードとヘッダーを正しく使用することです。状態コードの詳細については、w3.org/Protocols/rfc2616/rfc2616-sec10.html (英語) を参照してください。
世界中を飛び回って REST について講演してきましたが、そこで頻繁に指摘してきた RESTful サービスのメリットの 1 つは、GET 要求をキャッシュできることです。スケーラビリティや Web の成功は、間違いなく、主として HTTP キャッシュによるものです。
HTTP キャッシュを活用する 1 つの方法として、条件付き GET を使用します。条件付き GET により、クライアント (HTTP 用語では "ユーザー エージェント") は、ユーザー エージェントが既にそのコピーを保持しているリソースに GET 要求を行うことができます。リソースが変更されていなければ、そのリソースとユーザー エージェントが既に保持するリソースのバージョンとが正確に一致していることが、サーバーからユーザー エージェントに通知されます。条件付き GET の効率上のメリットは、サーバーとユーザー エージェントとの間でネットワークが使用する帯域幅が減少し、新しく作成されたリソースや変更されたリソースへの要求に使用する帯域幅が増加することです。また、リソースをシリアル化するのに必要な追加処理時間が削減されます。ただし、リソースの生成や取得にかかる処理時間は削減されません (現在のリソースと、ユーザー エージェントが条件付き GET を使用して送信する情報を比較するために、現在のリソースのコピーが必要になるため)。
図 6 条件付き GET のサーバー側の実装
[OperationContract]
[WebGet(UriTemplate = "/{name}")]
public VMData GetOne(string name)
{
VMManager.Connect();
var v = VMManager.GetVirtualMachine(name);
var newVM = FromVM(v);
string etag = GenerateETag(newVM);
if (CheckETag(etag))
return null;
if (newVM == null)
{
OutgoingWebResponseContext ctx =
WebOperationContext.Current.OutgoingResponse;
ctx.SetStatusAsNotFound();
ctx.SuppressEntityBody = true;
}
SetETag(etag);
return newVM;
}
private bool CheckETag(string currentETag)
{
IncomingWebRequestContext ctx =
WebOperationContext.Current.IncomingRequest;
string incomingEtag =
ctx.Headers[HttpRequestHeader.IfNoneMatch];
if (incomingEtag != null)
{
if (currentETag == incomingEtag)
{
SetNotModified();
return true;
}
}
return false;
}
string GenerateETag(VMData vm)
{
byte[] bytes = Encoding.UTF8.GetBytes(vm.ID +
vm.LastChanged.ToString());
byte[] hash = MD5.Create().ComputeHash(bytes);
string etag = Convert.ToBase64String(hash);
return string.Format("\"{0}\"", etag);
}
void SetETag(string etag)
{
OutgoingWebResponseContext ctx =
WebOperationContext.Current.OutgoingResponse;
ctx.ETag = etag;
}
WCF では HTTP の "高度な" 概念の大半を自動的にはサポートしませんが、条件付き GET も同様です。条件付き GET の実装は、サービスの実装ごとに大きく異なる可能性があるためです。ただし、HTTP の他の "高度な" 概念と同様に、条件付き GET を実装するためのツールが提供されます。条件付き GET を実装する方法は 2 つあります。1 つは、リソースの最終更新日時を使用する方法、もう 1 つは ETag という一意識別子を使用する方法です。
条件付き GET を実装する場合、ETag がよく使われるようになってきています。図 6 に、VM サービスに ETag ベースの条件付き GET を実装するコードが含まれています。この例はサーバー側の実装です。クライアントについてはこの後説明します。
サーバーでの基本的な流れは、クライアントから送信された If-None-Match HTTP ヘッダーに含まれる ETag を検索し、リソース用に生成された現在の ETag と照合します。この例では、各 VM の一意 ID と最終更新タイム スタンプを連結して使用しています。連結したものをバイト列に変換後、MD5 ハッシュに仕立て上げます。これは、ごく一般的な実装です。2 つの値が一致したら、サーバーから "304 Not Modified" HTTP ヘッダーと空の本文を返信して、シリアル化する時間と帯域幅を節約します。クライアントはこのすばやい応答を得て、既に保持しているリソースをそのまま使用できることを認識します。
図 7 簡単な WPF VM クライアント
図 7 に示すようなクライアント アプリケーションを想定します。このアプリケーションでは、各 VM の名前と現在状態を表す画像を表示します。ここで、各 VM の現在状態を照合するようにアプリケーションを更新するとします。これを実現するため、タイマーを使ったコードを記述し、保持するローカル レコードと異なる場合にレコードを更新する必要があります。アプリケーションを簡略化することだけを考え、反復処理の中ですべてのレコードを更新するかもしれませんが、それではリソースの無駄遣いになります。
このような場合はクライアントで条件付き GET を使用します。条件付き GET 要求を送信することで、変更があったかどうかをサービスにポーリングできます。この例では、If-None-Match HTTP ヘッダーを使用して、リソースの ETag を示すことになります。VM のコレクションの場合、サービスは、最近変更された VM を使用して ETag を生成します。クライアントは、1 つ以上の VM の状態が変更されている場合のみ更新を実行します (VM が多数ある場合、VM ごとにこの処理を行うこともできますが、この例では、アプリケーション内でコレクションにデータ バインディングしているため、コレクション全体を更新できます)。
このロジックを実装することはそれほど難しくありませんが、これは REST Starter Kit によって実装される機能の 1 つです。コード サンプルに含まれている PollingAgent.cs というファイルには、ユーザーが定義した間隔で RESTful エンドポイントにポーリングし、条件付き GET を自動的に行うクライアントが含まれています。PollingAgent では、(サービスから 302 を返さなくなっていて) リソースが変更されていると判断すると、コールバックを行います。
したがって、このシンプルな WPF アプリケーションでは、コールバックの結果 (HttpResponseMessage オブジェクト) を受け取って、コントロールに新しいデータをバインドし直すだけです。図 8 に、PollingAgent を使用してアプリケーションを実装するコードを示します。
ハイパーテキストに従う
クライアントの話題を終了する前に、REST のもう 1 つの重要な制約について説明しておきます。それは、アプリケーション状態エンジンとしてのハイパーメディア (HATEOAS とも呼ばれます) を使用する際の制約です。
HATEOAS は、人間が Web を使用する状況を考えると理解しやすいと思います。人間が Web を使用する際、よくアクセスするサイトにブックマークを設定することがあります。しかし、そのサイト内では、通常、その日のニーズに基づいてハイパーリンク先に移動します。たとえば、www.amazon.co.jp にブックマークを設定していても、何かを買いたいときは、それぞれのページ (リソース) 上のリンクをクリックして、商品をカートに追加します。その後、各ページのハイパーリンク先 (フォームの場合もあります) に次々移動して、注文手続きを行います。この注文手続きの各段階で、ページ内のリンクがアプリケーションの現在状態を表します (つまり、ユーザー エージェントとして実行できる処理を表します)。
RESTful クライアントを構築するときの重要な考慮事項の 1 つは、"ブックマーク" を最小限に抑えることです (ただし、この RESTful クライアントの一部は、RESTful サービスの実装者としての役割もあります)。つまり、クライアントでは、実質的に、リソース表現の URI 構造についてできる限り意識しないようにして、リソース内で "ハイパーリンク" を "移動" する方法をできる限り多く認識するようにします。"ハイパーリンク" に引用符を付けたのは、リソース内のすべてのデータが、クライアントの次の "リンク" を構築するための相対 URI になる可能性があるためです。
図 8 REST Starter Kit の PollingAgent を使用した条件付き GET
public Window1()
{
InitializeComponent();
string uri = "http://localhost/HyperVServices/VMs.svc/";
var client = new HttpClient();
var message = client.Get(uri);
var vms = XmlSerializerContent.ReadAsXmlSerializable<VMs>(
message.Content);
_vmList.DataContext = vms.VM;
var pa = new PollingAgent();
pa.HttpClient = client;
pa.ResourceChanged += new EventHandler<ConditionalGetEventArgs>(
pa_ResourceChanged);
pa.PollingInterval = new TimeSpan(0, 0, 5);
pa.StartPolling(new Uri(uri), message.Headers.ETag, null);
}
void pa_ResourceChanged(object sender, ConditionalGetEventArgs e)
{
var vms = XmlSerializerContent.ReadAsXmlSerializable<VMs>(
e.Response.Content);
_vmList.DataContext = vms.VM;
}
この例では、クライアントから VM の名前を URI の一部として要求することにより、特定の VM のデータを要求できるようにするため、実際には各 VM の名前はリンクになっています。したがって、http://localhost/HyperVServices/VMs.svc/MyDesktop は、VM リソース MyDesktop の URI です (MyDesktop は、コレクション内の VM の Name 要素の値です)。
この RESTful サービスを使用して、VM を準備して起動できるものとします。このような場合は、まだ準備が整っていない VM をクライアントが起動しないように、一定の時間内に VM が取り得るさまざまな状態へのハイパーリンクを VM リソース内に埋め込むようにします。
HATEOAS を使用すると、アプリケーションの現在状態を確認できます。また、(クライアントはサービスの URI 構造をほとんど意識する必要がないので) クライアントとの結び付きがより緩やかになります。
想像よりも簡単
RESTful クライアントの構築は、WSDL メタデータを利用できる SOAP ベースのクライアントの構築に比べれば、着手が困難であることは間違いありません。以前のコラムでも触れたように、「REST は単純です、だからといって簡単というわけではありません。SOAP は (WSDL のおかげで) 簡単です、だからといって単純というわけではありません」。適切なコーディングを行い、REST Starter Kit のようなツールがあれば、RESTful クライアントの構築は思ったよりも簡単です。最後に、アーキテクチャ スタイルを使用して得られるメリットは、クライアントの構築での一時的な遅れを補って余りあるということがわかると思います。
Jon Flanders は、フリーのコンサルタント、講演者、Pluralsight のトレーナーという 3 つの肩書きを持っています。彼は、BizTalk Server、Windows Workflow Foundation、および Windows Communication Foundation のスペシャリストです。連絡先は masteringbiztalk.com/blogs/jon です。