クライアント/サーバー間のやりとり

前章では、クライアントアプリケーションのGUIのビジュアルを通して、XMPPサービスがどのようなデータを扱うサービスなのかを見てきました。ここではさらに、クライアントとサーバーの間でどのようなやり取りが行われているかを詳しく説明し、クライアントのGUIの裏で何が行われているのか、サーバーはどのようにサービスを提供しているのかを明らかにしていきます。

ストリーム

XMPPでは、ログインからログアウトまでの間、一つのTCPコネクションを維持し、そのコネクション上で、前章で紹介したようなメッセージやプレゼンスをはじめとする様々なデータのやりとりを双方向に行います。このような性質から、HTTPとは違ってステートフルなサービスと呼ぶことが出来ます。

例えば、会員限定のWebサービスでは、ログインからログアウトまでの間のクライアントとサーバー間のやり取りは、それぞれ独立しており、Cookieなどを利用してセッションのトラッキングを行うのが一般的です。このような性質からHTTPはステートレスだと言われます。

XMPPプロトコルでは、このようなコネクションの事をストリームと呼び、前章で説明したような、クライアントとサーバー間のストリームについては特にC2Sストリームと呼びます。C2SClient to Serverの略語です。

[図が入る]

クライアントは接続完了すると、まず次のようにstreamをルートエレメントとしたXMLを送信します。

client to server
<?xml version="1.0"?>
<stream:stream to="xmpp.example.org" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">

サーバーはクライアントからstream開始タグを受信すると次のようにXMLをクライアントに向けて送信します。

server to client
<?xml version="1.0"?>
<stream:stream from="xmpp.example.org" id="1f6d1a" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">

接続を閉じるときは次のようにstreamの終了タグを送信しあいます。

client to server
</stream:stream>
server to client
</stream:stream>

実際は、上の例のように行儀よく終了タグを送らずに、いきなりTCPの切断処理をするケースも多いですが、
それで特に何の問題もありません。

また、異なる事業者を利用しているユーザー間でも通信できるよう、サーバー間での通信を行うためのストリームについても定義されています。これをS2Sストリームと呼び、Server to Serverの略になります。

[図が入る]

このS2Sを利用すれば、例えばxmpp.example.orgのユーザーから、xmpp.example2.orgのユーザーに対してメッセージを送信できるわけです。SMTP(e-mail)を彷彿とさせますが、プロキシは使わず、ダイレクトにTLS接続を行い証明書を検証するなどして、サーバー間の認証をしっかり行おうということになっています。e-mailでのプロキシサーバーを踏み台としたスパム問題の鉄を踏まないように、ということですね。

S2Sのコスト

Distributedというのはインターネットの思想ですので、「どんな事業者間でもプロトコルにさえ従えば連携できる」、というのは素晴らしいことのように聞こえるのですが、色々な問題も出てくるのが実情です。

スパムの問題に関しても、上記の対策で解決したかというと必ずしもそうではありません。

スタンザ

当然ですが、実際は、stream開始タグを送信してから、最終的にstream終了タグを送信するまでの間に様々なデータが流れることになります。

client to server
<?xml version="1.0"?>
<stream:stream to="xmpp.example.org" version="1.0" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams">
...
  <!--メッセージの送信-->
  <message type='chat' to='romeo@xmpp.example.org'>
    <body>こんにちは</body>
  </message>

  <!--プレゼンスの変更-->
  <presence>
    <show>away</show>
    <status>買い物にでかけます。</status>
  </presence>
...
</stream:stream>

前章でクライアントアプリケーションのGUIを通して紹介した、メッセージやプレゼンスのようなデータは、実際は裏でこのようにサーバーとやり取りされています。上記コード例のmessagepresenceのように、ストリームの中でやりとりされる第一深度のXMLエレメントをスタンザ ( stanza )と呼びます。直訳すると「節」になります。

まとめると、XMPPサービスでは、クライアントとサーバー間で双方向のTCP通信を行い、そのデータはXMLで表現します。streamをルートエレメントとして、その下にくる第一深度のそれぞれのXMLエレメントをスタンザと呼びます。スタンザをイベントの単位として、クライアントとサーバーの間でメッセージ送信や、プレゼンス変更をはじめとする様々なやりとりをします。

スタンザ受信時のサーバーの振舞い

スタンザを受け取ったときにサーバーがすべき仕事は、大きくわけると二種類です。

リクエスト/レスポンス型

一つは、「サーバーに対する情報のリクエストに対して、レスポンスを返すこと」で、これは例えば、「自分のロスター情報が欲しい」というリクエストに対して、適切にロスターアイテムのリストを返してあげることなどが該当します。要はWebサービスのAPIのようなものだと言えば分かりやすいかもしれません。

[図が入る]

中継/配送

もう一つが、「メッセージやプレゼンスなどの中継、配送」です。この配送の方法は二種類あります。

宛先指定送信

指定された特定の相手のみに対して、スタンザを配送します。一対一のチャットにおけるメッセージの送信などがこれにあたります。

[図が入る]

ブロードキャスト

ある条件に合致する全ての相手に対して、スタンザを配送します。例えば、プレゼンスを変更すると、自分のフォロワー全てに対して、自分のプレゼンス変更が通知されます。

[図が入る]

基本的には、メッセージは宛先指定送信、プレゼンスはブロードキャストになります。ただし例外もあります。

特定の相手にだけ別のプレゼンスを見せるダイレクトプレゼンスという機能もあり、これは宛先指定送信になります。

また、グループチャットの場合は、そのチャットルームにいるメンバー全員にメッセージがブロードキャストされます。

スタンザの種類

スタンザにはいくつか種類があります。主要なものは次の三つです。

  • iq
  • message
  • presence

他にもいくつか存在するのですが、上の三つ以外のものは、基本的にログイン手続きの中でしか利用しません。 次の章「ケーススタディ: ログインからログアウトまで」を読むときに注意してみておくとよいでしょう。

ログイン後は上の三つしか利用しません。

これら三つのスタンザについて詳しく説明していきますが、その前に把握しておかなければならないことが一つあります。

JID - 宛先の特定

上で宛先指定送信の話をしましたが、宛先を特定するには何らかのIDが必要です。

XMPPではユーザーのIDを表現するためにJIDというフォーマットを利用します。 上で説明したロスターアイテムにおけるユーザーIDの表現にもJIDが使われていますし、メッセージ送信時には宛先をJIDで指定します。

JIDJabber IDの略であり、プロトコル名がXMPPになる前、まだJabberだった頃の定義でそのまま残っています。

具体的にどんなフォーマットなのかを確認します。次の例を見てください。

taro@xmpp.example.org

ほとんどの人が気づいたかと思いますが、e-mailアドレスとほぼ一緒ですね。ユーザーIDの後に@が続き、その後ろにサービスドメインが続きます。

このようなIDを利用してメッセージの送受信を行います。まるでSMTP(e-mail)のようだと感じる人も多いでしょう。

さて、JIDにはさらに次のような形態も存在します。

taro@xmpp.example.org/home
taro@xmpp.example.org/work

上で説明したe-mailアドレスと同じようなIDの後に、/ (スラッシュ)が続き、さらにその後にhomeworkが続いているのがわかるでしょうか。このhomeworkのように、ドメインパートの後にスラッシュでつなげられて続く文字列を、リソースと呼びます。リソースというのは言ってしまえばコネクションIDのようなものです。

とにかくtaroさんにメッセージが送りたいのなら、taro@xmpp.example.org宛にメッセージを送ります。サーバーは接続中のtaroさんの全てのコネクション、つまり、taro@xmpp.example.org/hometaro@xmpp.example.org/workの二つのコネクションに対してメッセージを配信します。

taroさんの特定のコネクションに対してメッセージを送りたいのなら、リソースも含めたJIDを指定してメッセージを送ります。例えば taro@xmpp.example.org/home 宛にメッセージを送信すると、サーバーは、taroさんのすべてのコネクションにはメッセージを送らず、リソースがhomeになっているコネクションにだけメッセージを配信します。

これはあくまで一例です。実際はサーバーの実装次第になります。リソースを特定してメッセージを送信しても、相手の全てのコネクションにメッセージを配送するサーバーもあります。

リソースまで含めたJIDをフルJID ( Full JID )、そうでないものをベアJID ( Bare JID )と呼びます

現在はJIDの仕様がひとつのRFCで定められるようになりました。
厳密な仕様が知りたい場合はRFC6122を確認するとよいでしょう。

セマンティックリソースについて

上記の例では、homeworkなど、セマンティックなリソースを使いました。つまり、そのコネクションが自宅からのものであるとか職場からのものであるとかを推測することができるような文字列を利用したわけです。

ところが実際メッセンジャークライアントでは、このようなセマンティックなリソースが有意義に使われている例はほとんどありません。

[図が入る]

上の画像のように、クライアントアプリケーションで、コネクション情報を集約してしまうのがよくあるパターンで、一つ一つのコネクション情報を分類して表示することはあまりありません。

エンドユーザーからしたら、「友人のコネクションがふたつある」とか「そのうちのひとつがhomeで、もうひとつがwork」であるとかはどうでもいい情報で、一番大事なのは「友人がオンラインでチャット可能か、メッセージが届くかどうか」ということであり、クライアントアプリケーションのUIもそのように最適化がされてきたというわけです。

そういう現状を鑑みるに、リソースがセマンティックである必要はなく、コネクションの識別子としてコンピュータが判断できればそれでいいわけです。ハッシュ関数などを利用してランダムな文字列を生成してしまえばよいでしょう。

iqスタンザ

IQはInformation Queryの略です。Queryという名の通り、上で解説したサーバーとクライアントのやりとりの種別のうち、リクエスト/レスポンス型のやりとりにおいてこのスタンザを利用します。

ロスター要求を例に上げて説明しましょう。

クライアントはサーバーに次のようにIQスタンザを送信します。iqには、必ずid属性とtype属性を含めなければなりません。

client to server
<iq id='hu2bac18' type='get'>
  <query xmlns='jabber:iq:roster'/>
</iq>

idの値は何でもよいです。クライアントアプリケーションが、リクエストに対するレスポンスをトラッキングするために、任意の値を生成します。

リクエストする側では、type属性に、getsetのどちらかを利用します。HTTPのGETPOSTのようなメソッドをイメージすればよいでしょう。

iqの子エレメントは、何を要求するかによって変化します。

サーバー側は次のようにレスポンスを返します。リクエストと同様にid属性とtype属性を含んでいなければなりません。

server to client
<iq id='hu2bac18' to='juliet@example.com/balcony' type='result'>
  <query xmlns='jabber:iq:roster'>
    <item jid='romeo@example.net' name='Romeo' subscription='both'><group>Friends</group></item>
    <item jid='mercutio@example.com' name='Mercutio' subscription='from'/>
    <item jid='benvolio@example.net' name='Benvolio' subscription='both'/>
  </query>
</iq>

レスポンスにおいてはtype属性は、resultか、errorのどちらかになります。

id属性の値は、クライアントが送信したリクエストのiqスタンザにつけられていたものを、そのまま利用します。それによって、クライアントは、自分がいままでに送信したリクエストのうち、どのリクエストに対するレスポンスを受信したのかを判断できるようになります。

messageスタンザ

messageスタンザは非常に単純で、メッセージの配信のみがその役割になります。

まずtype属性を指定しなければなりません。次の種類があります。

  • normal
  • chat
  • headline
  • groupchat

一般的には、IMでの一対一のチャットではchatが選択されます。グループチャットでは文字通りgroupchatが選択されます。headlineは、「X月Y日のZ時からメンテナンスします。」というような、サーバーからの警告やお知らせに使えばよいでしょう。normalは、独立したメッセージで、メールのようなイメージを持てばよいかと思います。

type属性を省略されている場合はnormalとして扱うことになっています。

ほかには、宛先を示すto属性や、送信元を示すfrom属性があります。

クライアントがメッセージを送信するときは、fromをつけなくても、サーバー側が自動的に補填するようにします。むしろ、クライアントがfrom属性をつけた状態でメッセージを送信してきたとしても、詐称を防ぐために、サーバー側はそれを信じることなく、from属性を上書きするようにするのがよいでしょう。

messageの小要素として、次のようなものがあります。それぞれ省略可能です。

要素名 概要
body本文です。特に説明はいらないでしょう。たいていのメッセージには本文がつきますが例外もあります。拡張ガイドのチャットステートの章で説明します。
subjectサブジェクト。e-mailでおなじみですね。
threadチャットの履歴などをトラッキングできるようにする目的で、クライアントが任意の値を生成します。

メッセージのやり取りの例を挙げてみましょう。

まず、太郎さんは次郎さんと話したいと思い、次郎さん宛にメッセージを送信します。

taro to server
<message type='normal' to='jiro@xmpp.example.org'>
  <subject>こんにちは</subject>
  <body>チャットしましょう</body>
</message>

サーバーは、それを受け取ると、次郎さんの全てのコネクションに対して、メッセージを配送します。このとき、自動的に太郎さんのJIDをfrom属性に補填しておきます。この例では、次郎さんはhomeworkの二つのリソースで接続していることにしますので、二つのコネクションにメッセージが配送されます。

server to jiro1
<message type='normal' from='taro@xmpp.example.org/home' to='jiro@xmpp.example.org/home'>
  <subject>こんにちは</subject>
  <body>チャットしましょう</body>
</message>
server to jiro2
<message type='normal' from='taro@xmpp.example.org/home' to='jiro@xmpp.example.org/work'>
  <subject>こんにちは</subject>
  <body>チャットしましょう</body>
</message>

次郎さんは、リソースがhomeになっているほうの接続で、チャットを開始するために返信します。この時type属性をchatにし、チャットの履歴をトラッキングできるように、クライアントアプリケーションが任意のthread値を加えます。

jiro1 to server
<message type='chat' to='taro@xmpp.example.org/home'>
  <thread>12345</thread>
  <body>おっけー</body>
</message>

サーバーは太郎さんにメッセージを配送します。

server to taro
<message type='chat' from='jiro@xmpp.example.org/home' to='taro@xmpp.example.org/home'>
  <thread>12345</thread>
  <body>おっけー</body>
</message>

太郎さんは、同じthread値を使って次郎さんとのチャットを開始します。

taro to server
<message type='chat' to='jiro@xmpp.example.org/home'>
  <thread>12345</thread>
  <body>やっほー</body>
</message>

上で挙げた例は、あくまで一例にしか過ぎず、どのtype属性をどう扱うか、body以外のエレメントをどう扱うかは、結局のところ、クライアントアプリケーションの仕様次第です。サーバーはクライアントからクライアントへの配送を中継してあげるだけに過ぎません。XMPPのスペック上ではアバウトな分類があるだけで、厳格な細かいルールはありません。

presenceスタンザ

presenceスタンザには大きくわけて二種類の仕事があります。 一つは、今までのプレゼンスの説明であった通りの、プレゼンス変更情報のブロードキャストです。 もう一つは、サブスクリプションの制御になります。前章で説明したように、Twitterでいうところの、いわゆる「フォロー」「アンフォロー」などがこれにあたり、XMPPでは、これらの処理もpresenceスタンザで行います。 つまり、presenceスタンザというのは、プレゼンス情報のpublish/subscribeを行うスタンザです。

他のスタンザと同じく、いくつかのtype属性の種類がありますが、上記のように、二種類の仕事がありますので、二つに分けて分類してから説明していきます。

プレゼンス情報変更に使われる種別

  • available
  • unavailable

presenceスタンザは次のような子要素を持ちます。

要素名 概要
show状態の種別。chat(チャット可能), away(ちょっと離席中)xa(Extended Awayの略、長時間離席中)dnd(Do not Disturbの略、邪魔しないで)の四つがある。
statusユーザーが編集可能な状態を表すテキストメッセージ。Skypeなどでムードメッセージと呼ばれるもの。
priority優先値を表す数値。-127から128までの範囲。

上であげたような子要素の全てが有効に利用されているかというとそうではありません。
例えばshow要素の値にしても本当に4種類も必要なのでしょうか。オフライン、オンライン(チャット可能)、オンライン(離席中)の三つだけで十分だという意見もありますし、場合によっては、オフラインかオンラインかわかれば十分だという意見もあります。

priorityの値に関しても同様に、大多数のエンドユーザーがこの値を有効に利用できるかどうかは疑問が残ります。多くのユーザーは、「自分が複数のコネクションをつないでいて、こっちのコネクションの優先値を10にして、あっちのコネクションの優先値を1にして」などと意識することはほぼ無いと言えるでしょう。

messageスタンザの時の話と同様に、このあたりの仕様もクライアントアプリケーション次第です。仕様がエンドユーザーにとって複雑すぎると判断したら、UI側でアレンジしてしまいましょう。サーバー側はパッシングするだけです。

サブスクリプション制御に使われる種別

上で説明した通り、サブスクリプションの制御にもpresenceスタンザが使われます。この場合はtype属性として、次のような値が使われます。

  • subscribe
  • subscribed
  • unsubscribe
  • unsubscribed

サブスクリプション制御は、メッセージの送信や、プレゼンス情報変更のブロードキャストに比べると、かなり複雑な処理になります。なぜなら、リクエストやレスポンスの配送だけでなく、サーバー、クライアントでそれぞれロスターの適切な編集や同期も必要になるからです。

Webサービスと連動する場合、「フォローをはじめとする友人リストの管理のような作業は、Webサービス上でやってもらうことを推奨し、XMPP上ではサブスクリプション機能はサポートしない」と割り切ってしまってもいいでしょう。最近のソーシャルネットワーク系のWebサービスの場合は、そういった機能を既に持っていることがほとんどです。


次は、実際にクライアントがログインしてからログアウトするまでの間に、裏でどのような処理がなされるのか、ストリームがどのように状態変化していくのかを見ていくことにします。

ケーススタディ: ログインからログアウトまで »


© Lyo Kato 2012