IBM WebSphere開発者向け技術ジャーナル (US)より。
IBM® WebSphere® Application Server V6.1のSIP(Sessions Initiation Protocol)サポートを利用する場合、いったん確立されたSIPダイアログを操作するためのSIPサービスを提供するアプリケーションが必要になることがあります。例えば、以下のような場合です。
- ダイアログが確立された後に、該当するプリペイド・サービス(テレフォン・カード、公衆電話、プリペイド式携帯電話など)が使い果たされた、あるいはユーザーがWebを介して呼を終了したなどの理由で、アプリケーションが呼を終了できるようにしたい場合。
- 電話会議アプリケーションなどのような別のアプリケーションがWebを介して他のユーザーを同じメディア・ストリームに招待できるようにしたい場合。
このような動作を実現するには、呼の中間でバックツーバックのユーザー・エージェントとして機能し、該当サービスへのアクセスを提供するアプリケーションが必要となります。
この記事では、WebSphere Application Server V6.1を使って、このようなSIPサービスを作成し、SIPサービスの可用性を向上させ、そしてこのサービスを他のアプリケーションに公開する方法をわかりやすく説明します。
SIPサービスがEJBあるいはWebサービスとして公開されている場合、高可用性(HA)環境では、そのEJBまたはWebサービスがSIPセッションの存在するサーバー上で実行されることを保証する簡単な方法はありません。問題は、ステートレス・ルーティング層はコンテキストがなければSIPダイアログで使用されているサーバーを見つけられないという点です。単一サーバーの環境(図1)では、WebサービスはSIPダイアログで使用されているサーバーに簡単にアクセスできます。もちろんこれは、サーバーが1つしかないためです。一方HA環境(図2)となると、Webサービスにはステートレス・プロキシー・サーバーがWebサービスを正しいマシンにルーティングするためのコンテキストが一切ありません。
図1は、単一サーバーがSIP機能を処理する場合です。単一であるが故にWebサービスはSIPサーバーのSIP機能を利用することができます。図2に示す高可用性環境の場合、Webサービスには コンテキストがないためSIPダイアログを処理する正しいサーバーにルーティングされない可能性があります。
図1 単一サーバー環境

図2 高可用性環境

SIP対応サービスを作成するのに最も簡単な方法の1つは、HTTPとSIPの融合型アプリケーションで、HTTPを使用したREST(Representation State Transfer)デザイン・パターンを用いることです。このタイプのアプリケーションでは、SIPサーブレットはプロキシーまたはバックツーバック・ユーザー・エージェント(B2BUA)のいずれかとして機能します。その役割がプロキシーかB2BUAかによって、SIPサービスの機能は以下のように変わってきます。
- B2BUAはユーザー・エージェント・サーバー(UAS)およびユーザー・エージェント・クライアント(UAC)として機能し、ダイアログ内でリクエストを生成することが可能です。
- プロキシー・アプリケーションはメッセージを生成することはできませんが、ダイアログ自体に関する情報にアクセスできるようにします。
これはつまり、例えばB2BUAの場合はBYEメッセージを生成して接続をクローズすることは可能ですが、プロキシーの場合には不可能だということです。
図3は、SIPサーブレット、RESTサービスとして機能するHTTPサーブレット、そしてEJBサービスやWebサービスなどの他のアプリケーション要素で構成されるアプリケーションを図解したものです。
図3 アプリケーションの図解

図3では、まずSIPサーブレット(SIP Servlets)がメインHTTPサービスのエンコードされたURIを取得します。SIPサーブレットが エンコードされたURIを取得するには、まずSipApplicationSessionを取得してこれをIBMApplicationSessionにキャストします。このIBMApplicationSessionを使用してURIをエンコードします。このエンコードされたURIを用いると、HTTPリクエストはSIPダイアログ確立時に作成されたSipApplicationSessionにアクセスし利用できるようになります。HTTPサービスはSipApplicationSessionからSipSessionにアクセスし、呼の終了などを実行できるようになります。
エンコードされたURIをSIPダイアログの確立中に取得した後、アプリケーションはそのエンコードされたURIをクライアントがアクセスできる方法でパブリッシュします。クライアントがそのURIフェッチした後、RESTサービスへアクセスする方法に関してはいくつか選択肢があります。
例えばWebサービスを公開することが要求されているのであれば、SIPアプリケーションはURIをデータベースやJMS、もしくはその他の手段でパブリッシュすれば、呼び出されたWebサービスはそのURIを取得できます。このURIにリクエストを送信することで、WebサービスはRESTサービスを駆動し、要求されるアクションを提供します。この流れを図4に示します。ここでは、外部マシンに呼び出されたWebサービスがプロキシー・サーバーにRESTサービスへのリクエストを送信します。プロキシー・サーバーではRESTサービスへのリクエストのロード・バランスとルーティングが行われます。この図に示されている一連のステップは、以下のとおりです。
- SIPダイアログが確立され、サーバーにSIPダイアログのSipSessionが作成されます。
- アプリケーションがIBMApplicationSessionを使ってHTTP RESTサービスのURIをエンコードし、これをデータベースに挿入します。
- 別のクライアントがWebサービスに特定ユーザーの呼を終了するなどの処理を実行させます。
- アフィニティー・ルーティングを行うためのコンテキストを持たないプロキシーは、Webサービスへのリクエストの送信先としてバックエンド・サーバーの内任意の一つを選択します。
- WebサービスがデータベースからエンコードされたURIをフェッチします。
- Webサービスが該当するRESTサービスを呼び出します。このリクエストは、アフィニティー・ベースのルーティングのためにプロキシー・サーバーに送信されます。
- プロキシーはURIに含まれるアフィニティーを使用して、セッションを所有するサーバーにリクエストを送信します。
図4 外部マシンによって呼び出されるWebサービス

図5に2つ目の例を示します。ここでのクライアントは、単純なWebブラウザーです。この例では、クライアントがユーザーにアクセスが許可されているすべてのセッションを表示するようにリクエストします。このリクエストに対するレスポンスとして、ユーザーにエンコードされた URIのリストが表示され、ユーザーはこれらのURIを使用して、アクセス可能なセッションと確立済みのダイアログを操作できるようになります。図5では、ユーザーがWebから利用可能なすべてのセッションのリストをリクエストし、これらのセッションに対する処理を起動しています。(この例では、エンコードされたURIをパブリッシュする手段としてJMSを使用しています)。この図で概説しているステップは以下のとおりです。
- SIPダイアログが確立され、サーバーにSIPダイアログのSipSessionが作成されます。
- アプリケーションがIBMApplicationSessionを使ってHTTP RESTサービスのURIをエンコードし、これをJMSキューにパブリッシュします。
- 続いてブラウザーがアクセス可能なセッションのリストをリクエストします。このリクエストは、認証済みのユーザー名でフィルタリングされる場合があります。
- アフィニティー・ルーティングを行うためのコンテキストを持たないプロキシーは、Webリクエストの送信先としてバックエンド・サーバーの内任意の一つを選択します。
- HTTPサーブレットは、SIPアプリケーションがパブリッシュ先としているJMSキューへのサブスクリプションを使用してエンコードされたURIのリストと情報をブラウザーに返し、クライアントがエンコードされたURIに対する処理を起動できるようにします。
- クライアントがエンコードされたURIに対する処理 を選択すると、ブラウザーがRESTサービスを呼び出します。このリクエストは、プロキシー・サーバー経由で行われます。
- プロキシーはURIに含まれるアフィニティーを使用して、セッションが存在するサーバーにリクエストを送信します。
図5 ユーザーによる利用可能な全セッションリストのWebリクエスト

ここからは、サンプル・アプリケーションを使用して2つ目の例について詳しく説明していきます。
この記事のサンプル・アプリケーションでは、2つのSIPエンドポイント(UAC(クライアント)とUAS(サーバー))が確立した呼をWebブラウザーから終了させることができます。このWebアプリケーションでロードする最初のページに表示されるのは、その時点で確立済みの呼のリストです。また、このページは最初にロードされた後に確立された呼も非同期でフェッチし、表示します。このページでは通話・通信中の呼に関する情報にアクセスし、ボタンをクリックするだけで簡単に呼を終了できます。
このアプリケーションのベースとなっているのは、基本的なB2BUAアプリケーションです。このタイプのアプリケーションでは、SIPサーブレットはUACから着信する呼に対してはUASとして機能し、呼の宛先であるUASに対してはUACとして機能します。これにより、SIPサーブレットはメッセージ・フローのなかでリクエストを生成し、呼の終了などのアクションを実行することが可能になります。
アプリケーションには以下の4つの重要な役割があります。それぞれについて、この後、詳細を説明します。
- SIPサーブレットでエンコードされたURIを作成する
- SIPサーブレットからエンコードされたURIをパブリッシュする
- HTTPサーブレットでパブリッシュされているエンコードされたURIをフェッチする
- HTTPサーブレットから呼を終了する
このサンプル・アプリケーションではアプリケーション・サーバーがエンコードされたURIを生成することにより、HTTPサーブレットが呼び出されたときにIBMApplicationSessionにアクセスすることが可能になります。ここで重要となるのは、web.xmlファイルを使ってHTTPサーブレットにマッピングされるベースURIです。使用されるベースURIは以下のパターンに従います。
http://[IPAddress]:[Port#]/sample/calls/
上記のパターンに含まれる [IPAddress] はサンプル・アプリケーションが実行されているサーバーのIPアドレス、[Port#] はサンプル・アプリケーションがリッスンしているポート番号です。
必要となるのは、SIPサーブレットを通過するそれぞれの呼に対してエンコードされたURIを作成することです。そしてSIPサーブレット(この例では、PubMsgServlet)はこのURIをパブリッシュします。SipServletのdoInviteメソッドが呼び出されると、SIPサーブレットはdoInviteメソッドの主要なパラメーターとなっているリクエストメッセージを使用して、SipApplicationSessionにアクセスできるようになります。SIPサーブレットはリクエストメッセージからgetApplicationSessionメソッドでSipApplicationSessionを取得し、これをIBMApplicationSessionにキャストします。この時点で、以下に示すIBMApplicationSessionのencodeURIメソッドを使用すれば、上記のURIをエンコードできます。
InetAddress in = InetAddress.getLocalHost();
IBMApplicationSession iasess = (IBMApplicationSession)req1.getApplicationSession();
String apURL = iasess.encodeURI("http://"+in.getHostAddress()+":9080/sample/calls");
|
2.SIPサーブレットからエンコードされたURIをパブリッシュする
前述したように、アプリケーションはこのエンコードされたURI情報をJMSを使用してパブリッシュします。サンプルではCallInfoというPOJO(Plain Old Java Object)が、WebSphere Application Server V6.1のメッセージング・エンジンを使用してパブリッシュされます。CallInfoに組み込まれるのは、以下のように、ステップ1で作成したエンコードされたURI、そして呼に関する情報(呼の発信者と着信者、呼の開始時間など)です。
public class CallInfo implements Serializable{
private String accessPointURL;
private Date callStartTime;
private String callFrom;
private String callTo;
public CallInfo(String apURL, String callFrom, String callTo, Date callStartTime) {
this.accessPointURL = apURL;
this.callFrom = callFrom;
this.callTo = callTo;
this.callStartTime = callStartTime;
} . .
}
|
サンプルSIPサーブレットのPubMsgServletでは、doInviteメソッド内でこのCallInfoオブジェクトに 情報が埋め込まれます。
CallInfo cInfo = new CallInfo(apURL,req1.getFrom().toString(),req1.getTo().toString(),
new Date());
|
アプリケーションは作成したCallInfoオブジェクトをメッセージング・エンジンにパブリッシュします。
Context ctx = new InitialContext();
callInfoQueue = (javax.jms.Queue)ctx.lookup("java:comp/env/jms/CallInfo/dest");
callInfoQCF = (javax.jms.ConnectionFactory)ctx.lookup("java:comp/env/jms/CallInfo/cf");
javax.jms.Connection conn = null;
MessageProducer msgProducer = null;
Session sess = null;
try{
conn = callInfoQCF.createConnection();
sess = conn.createSession(true, Session.AUTO_ACKNOWLEDGE);
msgProducer = sess.createProducer(callInfoQueue);
ObjectMessage message = sess.createObjectMessage(cInfo);
msgProducer.send(message);
|
3. HTTPサーブレットでパブリッシュされているエンコードされたURIをフェッチする
この例では、コンテナーはWebブラウザーから2つのURLへのHTTP GETリクエストを受信できます。一つは(A)ブラウザーからの初期リクエスト用URLであり、もう一つは(B)それに続くjavascript関数による非同期リクエスト用URLです(このjavascript関数は、ページがロードされた後に定期的に呼び出されます)。いずれの場合も、MngCallsServletという名前のHTTPサーブレットのdoGetメソッドが呼び出されます。
Context ctx = null;
Session session = null;
ArrayList cInfos = new ArrayList();
try{
ctx = new InitialContext();
cf = (ConnectionFactory)ctx.lookup("java:comp/env/jms/CallInfo/cf");
dest = (Destination)ctx.lookup("java:comp/env/jms/CallInfo/dest");
// initializing the flag which is for checking
// if the last message in the queue has been already fetched
boolean lastMsg_already_fetched = false;
while(!lastMsg_already_fetched){
try{
conn = cf.createConnection();
conn.start();
session = conn.createSession(true, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer = null;
Message message = null;
try{
consumer = session.createConsumer(dest);
CallInfo cInfo = null;
message = consumer.receive(1);
if(message!=null){
cInfo = (CallInfo)((ObjectMessage)message).getObject();
cInfos.add(cInfo);
}else{
lastMsg_already_fetched = true;
}
|
(A) リクエストが初期アクセスであれば、そのリクエストは http://[IP address]:[Port#]/sample/calls に対して送信されます。コンテナーではリクエスト受信後MngCallsServletが呼び出され、パブリッシュされた呼に関する情報をフェッチし、その情報をCallsList.jspに転送します。CallsList.jspでは、呼に関する情報を基に以下のようなテーブルを作成します。
<body>
<h2>Restful SIP service Sample Application</h2>
<hr/>
<h3>[Call Manager]</h3>
<form>
<table border=4 id="table">
<thead>
<tr>
<td></td>
<td>CallFrom</td>
<td>CallTo</td>
<td>CallStartTime</td>
</tr>
</thead>
<tbody>
<% Iterator i =((ArrayList)request.getAttribute("CallInfo")).iterator();
while(i.hasNext()){
CallInfo cInfo = (CallInfo)i.next();%>
<tr>
<td>
<input type="checkbox" name="CallTerm" value="<%= cInfo.getAccessPointURL()%>"/>
</td>
<td><%= cInfo.getCallFrom().replaceAll("<","<")%></td>
<td><%= cInfo.getCallTo().replaceAll("<","<")%></td>
<td><%= cInfo.getCallStartTime().toString()%></td>
</tr>
<%} %>
</tbody>
</table>
<input type="button" value="terminate" onClick="cterm(this.form)"/>
</form>
</body>
|
(B) リクエストが後続の非同期アクセスであれば、そのリクエストは http://[IP address]:[Port#]/sample/calls;async に送信されます。コンテナーではこのリクエスト受信した後MngCallsServletが呼び出され、メッセージに含まれる情報を基に以下のようなXML応答を作成します。
PrintWriter out = response.getWriter();
response.setContentType("text/xml");
Iterator i = cInfos.iterator();
CallInfo cInfo = null;
out.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
out.println("<calls>");
while(i.hasNext()){
cInfo = (CallInfo)i.next();
out.println("<call>");
out.println("<from>"+cInfo.getCallFrom().replaceAll("<","<")+"</from>");
out.println("<to>"+cInfo.getCallTo().replaceAll("<","<")+"</to>");
out.println("<time>"+cInfo.getCallStartTime().toString()+"</time>");
out.println("<url>"+cInfo.getAccessPointURL()+"</url>");
out.println("</call>");
}
out.println("</calls>");
out.flush();
|
上記のXML応答はブラウザー内のJavaScript関数によって処理され、HTMLテーブルに変換されます。その結果、ブラウザーには図6のようなページが表示されます。
図6 サンプル・アプリケーションの結果ページ

このページからの非同期リクエストは、以下のように送信されます。
var url="http://<%= in.getHostAddress()%>:9080/sample/calls;diff";
function func(){
var req = false;
if(window.XMLHttpRequest && !(window.ActiveXObject)){
.
req = new XMLHttpRequest;
.
.
.
}
if(req){
req.onreadystatechange = function()
{
if(req.readyState == 4){
if(req.status ==200){
var reXML = req.responseXML;
processReqChange(reXML);
}
}
}
req.open("GET", url+'&t='+new Date(),true);
req.send(null);
}
}
|
上記のリクエストに対してこのページがMngCallsServletから受信する応答は、以下のように処理されHTMLテーブルに変換されます。
function processReqChange(res){
var calls = res.getElementsByTagName("call");
var tbl = document.getElementById("table");
var disp = tbl.getElementsByTagName("tbody")[0];
for(var i=0;i<calls.length;i++)
{
var tr = document.createElement("tr");
.
.
.
disp.appendChild(tr);
}
}
onload = function(){setInterval("func()",5000);}
-->
</script>
</html><input type="button" value="terminate" onClick="cterm(this.form)"/>
</form>
</body>
|
上記のページがロードされたら、ユーザーは終了する呼のチェック・ボックスを選択してTerminateボタンをクリックします。すると以下のJavaScript関数がトリガーされ、ステップ3でフェッチされたエンコードされたURIを使用してMngCallsServletへのDELETEリクエストが作成されます。このリクエストはGETやPOSTなどの別のタイプにすることもできますが、ここでは多様性を示すために以下のDELETEリクエストを使用しています。
function cterm(form){
for(var i=0;i<form.elements.length-1;i++)
{
if(form.elements[i].checked)
{
var req = false;
if(window.XMLHttpRequest && !(window.ActiveXObject)){
.
.
.
}else if(window.ActiveXObject){
.
.
.
}
if(req){
url_for_delete = form.elements[i].value;
req.onreadystatechange = function()
{
}
req.open("DELETE", url_for_delete,true);
req.send(null);
}
}
}
}
|
コンテナーはこれらのリクエストを受信すると、MngCallsServletのdoDeleteメソッドを呼び出します。このメソッド内ではgetSessionメソッドによってリクエストパラメーターからHttpSessionがフェッチされます。フェッチされたHttpSessionはIBMSession (US)にキャストされます。このIBMSessionからgetIBMApplicationSession()メソッドによってフェッチされたIBMApplicationSessionが、SipApplicationSessionにキャストされます。そして最終的には、アプリケーションはSipApplicationSessionのgetSessionsメソッドを"SIP"をパラメーターとして呼び出すことで、SipSessionオブジェクトをフェッチすることができます。このサンプル・アプリケーションでは、それぞれの呼はB2BUAタイプのSIPサーブレットによって確立されるため、対応するSipApplicationSessionインスタンスが持つSipSessionインスタンスは2つとなります。アプリケーションは呼を終了するために、これらのインスタンスそれぞれについてBYEリクエストを作成します。BYEリクエストは、"BYE"をパラメーターとして使用してSipSessionのcreateRequestメソッドを呼び出すと作成されます。
IBMSession isess = (IBMSession)request.getSession();
IBMApplicationSession iasess = isess.getIBMApplicationSession();
Iterator i = ((SipApplicationSession)iasess).getSessions("SIP");
SipSession ss = null;
SipServletRequest bye = null;
while(i.hasNext()){
ss = (SipSession)i.next();
bye = ss.createRequest("BYE");
bye.setAttribute("FromHTTP", true);
bye.send();
ss.invalidate();
}
|
既存のSIPセッションを操作するサービスを公開するのに最適なデザイン・パターンの1つは、RESTベースのサービスを使用することです。WebSphere Application Server V6.1の融合型のコンテナーを使用すれば、SIPを利用するRESTベースのサービスを極めて簡単に開発して実行することができます。また、WebSphere Application Server V6.1のプロキシー・サーバーを使用してRESTベースのサービスの可用性を向上させることで、この環境が抱える多くの問題を解決することができます。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| ダウンロード | ConvergedSample202.sar | 78KB | HTTP |
- developerWorks Japan: WebSphere: WebSphereの日本の技術情報サイトです
- developerWorks: WebSphere(US) : WebSphereの英語の技術情報サイトです
- アプリケーション・サーバー
- WebSphere Application Server V6.1のSIPに関する資料 (US)
- SIPの紹介 (US)
- 集中型アプリケーションの開発について (US)
- SIPおよびIMS(IP Multimedia Subsystem)アプリケーションの開発について (US)
- WebSphere Application Server V6.1のSIPに関する資料 (US)
- IBMApplicationSessionに関するAPI資料 (US)
- IBMSessionに関するAPI資料 (US)
- WebSphere Application Server V6.1インフォメーション・センター (US)
- IBM developerWorksのWebSphere Application Serverゾーン (US)
Erik Burckartは、WebSphere Application Server製品の主任アーキテクトです。University of PittsburghのSchool of Information Scienceを卒業し、在学中に電気通信、ソフトウェア開発、そして人間とコンピューターの相互作用について学びました。WebSphere Application ServerのSIPサーブレットに取り組んだ彼は、SIP Servlet 1.1(JSR 289)Expert Groupの一員となり、最先端のJava EEプラットフォームと最新SIPサーブレット仕様を結合する上で多くの貢献を果しています。