第 3 回 : 仕様・設計と実装( IM "improve" の設計・実装とエミュレーター動作)
はじめに
IM "improve" のサーバー側の機能仕様を決定し、システムを設計、実装します。 ezplus/J-PHONE それぞれのエミュレーターで動作させます。
- 第 1 回 : 携帯 Java と IM
- 第 2 回 : 仕様・設計と実装( IM "improve" の機能仕様と J2ME/MIDP のつまずきどころ)
- 第 3 回 : 仕様・設計と実装( IM "improve" の設計・実装とエミュレーター動作)
- 第 4 回 : IM "improve" を実機に載せよう( IM "improve" のダウンサイズ)
- 第 5 回 : インターフェイス構築のヒント
- 第 6 回 : IM "improve" の実機対応と携帯電話の将来展望
improve サーバーの機能仕様
追加機能としてデバッグを容易にするために、
- PC ブラウザからもプロトコル処理できる機能
- PC ブラウザからサーバーのデータが閲覧できる機能
を搭載します。
improve の設計
全体構成
図1 improve の基本構造
参考情報
IM を携帯電話からだけでなく、PC からも使えるようにするときは 図 2 のような構造が考えられます。 これは携帯電話用の「ポーリング型クライアント」と PC 用の「コネクション型クライアント」の違いを吸収するために、「仮想コネクション型クライアント」を設けてメインのサーバーには最終的につながっているクライアントを意識させないようにしたものです。この場合、データベースだけでなく、仮想クライアントサーブレットと本サーバーは別の物理サーバーで動かすことも可能です。
図2 携帯電話と PC をクライアントに持つ IM のシステム構成例
データモデルとプロトコルの設計
ポーリング型システムで実装するときに注意すべきことをまとめます。
サーバーとクライアントのデータの不整合
サーバーとクライアントのデータの不整合が発生したときに、それを検出し復旧させる機構が必要です。improve ではサーバーからクライアントへ全てのデータを送ることでデータの一貫性を保つようにします。
認証
コネクションを常時張っていないので、クライアントが通信する際、自分が誰であるかを毎回サーバーに伝えなければなりません。improve ではアカウントとパスワードをその他のパラメーターと一緒に送ります。
データモデル仕様
図3 データベース improve のテーブル構造
アカウントを主キーとし、他のテーブルでもアカウントをそのまま用いることにします。
users テーブル
ユーザー1人につき1行を割り当て、ユーザーの情報を保持させます。
friends テーブル
「誰が誰を参照しているか」の情報を保持させます。
message テーブル
サーバーが受け取ったメッセージを蓄積し、宛先のユーザーが取得するまで、データーは残ります。このため、オフラインでも相手にメッセージを送信できます。
プロトコル仕様
表1にプロトコル仕様を示します。以下、プロトコル全体を「プロトコル」、プロトコルの構成要素を「オペレーション」と呼びます。
クライアントからの情報の送信はコード単純化のためすべてパスとパラメーターで渡すことにし、HTTP の GET メソッドのみ用います。
返値は 4 バイト ( signed int ) とします。ただしポーリングの時のみ、前半 ( 下位 2 バイト ) を友達の数、後半 ( 上位 2 バイト ) をメッセージの数とします。友達リストの先頭にはユーザー本人が入っていて、クライアントは各行を、
readUTF : account (アカウント) readUTF : nickname (表示名) readUTF : phone (電話番号) readInt : status (状態:チャット許容度) readUTF : description (状態:自由文)
のフォーマットで読み取ることにします。 メッセージリストの各行は、
readUTF : account (送り主) readInt : type(種類) readUTF : content (内容)
のフォーマットで読み取ることにします。
送受信に DataOutputStream と DataInputStream を使い、文字列は writeUTF、readUTF で送受信することにします。
用途 | パス | パラメータ | 返値:成功 返値:失敗 |
その他データ in |
---|---|---|---|---|
アカウント関連 | ||||
アカウント作成 | /create | account=(アカウント) | 0以上 0未満 |
パスワード |
アカウント抹消 | /delete | account=(アカウント) password=(パスワード) |
0以上 0未満 |
N/A |
友達リスト関連 | ||||
友達追加 | /add | account=(アカウント) password=(パスワード) target=(対象アカウント) |
0以上 0未満 |
N/A |
友達削除 | /remove | account=(アカウント) password=(パスワード) target=(対象アカウント) |
0以上 0未満 |
N/A |
情報関連 | ||||
自分の情報設定 | /set | account=(アカウント) password=(パスワード) item=(0〜n) content=(内容) |
0以上 0未満 |
N/A |
メッセージ関連 | ||||
メッセージ送信 | /send | account=(アカウント) password=(パスワード) target=(対象アカウント) type=(タイプ) content=(内容) |
0以上 0未満 |
N/A |
その他 | ||||
ポーリング | /polling | account=(アカウント) password=(パスワード) |
本文参照 0未満 |
友達リストとメッセージリスト |
プロトコルテスト | /test | path=(テストパス) テストパラメーター郡 |
N/A | HTML ページ:図10 参照 |
サーバー監視 | /check | N/A | N/A | HTML ページ:図9 参照 |
プロトコルを使ったフローを簡単に説明しますと:
- 1.初めて起動したときに、アカウントを create で取得し、
- 2.自分の状態を set で設定する。
- 3.友達は add で追加し、
- 4.send でメッセージを送る。
- 5.定期的に polling を行い、最新状態の友達リストと届いているメッセージを取得する。
- 6.remove で友達を削除し、
- 7.退会するときは delete でアカウントを消す。
となります。
サーバーの設計
HttpServlet クラスを継承した ImproveServlet クラスと、プロトコルの各オペレーションごとのクラスを作成します。ImproveServlet は Operation クラスに HTTP リクエストを判断させ、各 Operation インスタンスを取得します。ImproveServlet は以降の処理を各 Operation インスタンスに委譲します。各 Operation はデータベースにアクセスしてデータを返します。
図 4 サーバー側クラス図
クライアントの設計
図5 画面遷移図
Display.callSerially と Timer.schedule
MIDP の UI のイベントループとタイマーに注意して、通信と UI を設計します。
- HTTP 通信は時間がかかり、かつ同時に複数本開けない。
- UI コールバックは同期化されるので、commandAction 等はすぐに返さなければ、UI が停止してしまう。
という事情から、別スレッドで通信を行うわけですが、単にスレッドだけですと、同時に 2 本開いたり、UI 側スレッドがデータを読んでんでいる最中に、通信側スレッドがデータを変更をしてしまう危険があります。synchronized で排他制御しますが、効率よく synchronized を使うのは簡単ではありません。
次の 2 つのメソッドを使います。
Display#callSerially(Runnable runable)
このメソッドで登録した runnable への run 呼び出しは UI コールバックの 1 つで、他のコールバックとは並列実行されません。
Timer#schedule(TimerTask task, …)
タイマーは 1 つのスレッドで実装されていてます。1 つの Timer インスタンスにこのメソッドで登録した task への run 呼び出しは、Timer インスタンスに登録されているほかの run への呼び出しと並列にはおこりません。
これらを利用すると synchronized をかけなくてもスレッドセーフなコードを書けます。
まとめると次のようになります。
- 作業予約メソッドの呼び出しはいつでも可能。
- 作業予約メソッドは即座に返る。
- 予約された作業は同期化される。
- そこで、この2つを対称にならべることによって、大きな意味での 「 CALL−RETURN 」の関係をつくり出します。図5 にシーケンス図を示します。実行するスレッドによって活性化部分が色分けしてあります。
図5 schedule と callSerially のシーケンス図
ダウンロードと描画を例にとると以下のような動きとなります。
- ユーザーのキー操作などにより、commandAction 実行中など(UI コールバック実行中)に通信の要求発生。
- [CALL] 通信を行うような run メソッドを実装した TimerTask (図中 runnable1)を Timer#schedule でスケジュール。
- commandAction など(UI コールバック)は return する。
- タイマースレッドがスケジュールに応じて runnable1.run を実行し通信を開始。
- ダウンロード用一時バッファにダウンロード (この間、UI は応答する)
- [RETURN] TimerTask#run 内で Display#callSerially を呼ぶ。このとき渡す Runnable (図中 runnable2) には、ダウンロードバッファの内容を UI 側の描画用データに反映するような run メソッドを書いておく。
- UIEventScheduler (勝手に命名しました)が描画を行っていないタイミングで runnable2.run を呼び出す。 描画用変数の値が runnable2.run で変更される。
- 通信の結果が画面に反映される。
この方法のデメリットは
- commandAction 等で始まる一連のプロセスが開始と終了の対応がとりにくい。
- ダウンロードに使う場合は、描画に使用するものとは別に一時バッファが必要となる。
となります。Timer#schedule は周期呼び出しのスケジュールも可能ですので、ポーリングにもってこいです。improve のクライアントでは全面的にこの手法を用い、synchronized は一箇所も使用していません。
図6 にクライアントのクラス図を示します。
図6 クライアント側クラス図
上で説明した機構を利用するために、TimerTask を実装し、private で Timer を持っている Task クラスを作成します。このクラスを継承して、種々の通信前&後処理を定義します。
improve の実装
概要とともにクラス一覧を 表 2 に示すにとどめます。詳細はソースコードをご覧ください。
都合により Ticker と MessageBox のソースコードは公開していません。
なお、ソースコード等をまとめたアーカイブをこちらに用意しました。
クラス名 | 機能の概要 |
---|---|
com.s_cradle.improve | |
ImproveServlet improve | サーバーのメインとなるクラスです。 |
com.s_cradle.improve.server.protocol | |
Operation | 各オペレーションのための抽象クラスです。 |
GetOpertaion | HTTP の GET メソッドを使うオペレーションのための抽象クラスです。 |
PostOperation | HTTP の POST メソッドを使うオペレーションのための抽象クラスです。(未使用) |
ErrorOperation | パスが異常でオペレーションが見つからない場合に処理をするクラスです。 |
Create | オペレーション create を処理するクラスです。 |
Delete | オペレーション delete を処理するクラスです。 |
Add | オペレーション add を処理するクラスです。 |
Remove | オペレーション remove を処理するクラスです。 |
Send | オペレーション send を処理するクラスです。 |
Set | オペレーション set を処理するクラスです。 |
Polling | オペレーション polling を処理するクラスです。 |
Test | オペレーション test を処理するクラスです。 |
Check | オペレーション check を処理するクラスです。 |
com.s_cradle.improve.client | |
ImproveMIDlet | MIDlet を継承するメインのクラスです。 |
ImproveAbstractCanvas | Ticker を貼り付けられる Improve 用の Canvas です。 |
(非公開) Ticker | 独自の Ticker です。MIDP 標準のものに比べかなり高機能となっています。 |
BuddyListCanvas | メイン画面の友達リストを描画する Canvas です。 |
Menu | Improve で使うメニューの一般的な機能を提供する抽象クラスです。 |
BuddyListMenu | 友達リストを表現する Menu です。 |
MainMenu | 本人フォーカス時の Menu です。 |
FriendMenu | 友達フォーカス時の Menu です。 |
ChangeStatusMenu | 自分のステータスを変えるための Menu です。 |
TwoChoiceMenu | 2択の Menu です。 |
MessagingCanvas | 会話画面を描画する Canvas です。 |
(非公開) MessageBox | メッセージを表示するスクロール&色付きテキストボックスです。 |
Buddy | 友達(本人も含む)を表現するクラスです。 |
Message | メッセージを表現するクラスです。 |
Task | Timer を持ち、TimerTask を継承するクラスです。 |
CreateTask | オペレーション create を使ったタスクを行うクラスです。 |
DeleteTask | オペレーション delete を使ったタスクを行うクラスです。 |
FollowedByPollingTask | ポーリングが後に続くタスクを行うクラスです。 |
PollingTask | オペレーション polling を使ったタスクを行うクラスです。 |
ResourceManager | RecordStrore や Image を管理するクラスです。 |
未実装&制限事項
上記ソースには次のような未実装&制限事項があります。
サーバー
- SQL の区切り文字などのエスケープ。
- 絵文字対応。
- 無効文字列でのアカウント作成拒否。
- クライアント
- 入力不備などのチェック・細かなエラー報告。
- 「攻撃」を受けたときの振動 (キャリア依存)。
- 「電話をかける」 (ezplus)。
エミュレーターで動かそう
サーバーのコンパイル&配備
tomcat の webapps に improve フォルダをつくって、そこに Servlet を配備します。定義した web.xml を示します。
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE web-app PUBLIC " -//Sun Microsystems, Inc.//DTD Web Application 2.3//EN " " http://java.sun.com/dtd/web-app_2_3.dtd "> <web-app> <servlet> <servlet-name?>improve</servlet-name> <servlet-class>com.s_cradle.improve.server.ImproveServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>improve</servlet-name> <url-pattern>/ * </url-pattern> </servlet-mapping> </web-app>
また、データベース improve を createdb に EUC_JP を指定して作成し、psql に次のような init.sql を渡して初期化しました。
DROP TABLE messages; -- すでにある場合削除 DROP TABLE friends;-- すでにある場合削除 DROP TABLE users;-- すでにある場合削除 CREATE TABLE users ( account VARCHAR(10),-- アカウント passwordVARCHAR(10),-- パスワード nicknameVARCHAR(9), -- 表示名 phoneVARCHAR(11),-- 電話番号 statusINTEGER,-- 状態(0:オフライン 1:ダメ 2:すこしだけ 3:いつでもいいよ) description VARCHAR(20),-- 状態文 PRIMARY KEY(account)-- 主キー ); CREATE TABLE friends ( account VARCHAR(10) REFERENCES users ON DELETE CASCADE, -- アカウント(誰が)。参照先削除時は削除 targetVARCHAR(10) REFERENCES users ON DELETE CASCADE, -- アカウント(誰を)。参照先削除時は削除 PRIMARY KEY(account, target)-- 主キー ); CREATE TABLE messages ( account VARCHAR(10), -- アカウント(誰から) targetVARCHAR(10), -- アカウント(誰へ) typeINTEGER,-- メッセージタイプ(1:通常 2:攻撃) content VARCHAR(64), -- 内容 receiveTime Timestamp, -- 受信時刻 FOREIGN KEY(account, target) REFERENCES friends(account, target) ON DELETE CASCADE -- 外部キー指定。参照先削除時は削除 );
クライアントのコンパイル
ezplus の開発ツール付属の KJX 作成ツールを使用しました。
ezplus では jad ファイルの MIDlet-X-AllowURL-<n> で許可されていない URL にはアクセスできません。上でリンクしてあるコードは MIDlet-X-AllowURL-1 から つなぎにいくべき場所を取得しています。サーブレットの配備場所に応じて、適切に変更してください。なお J-PHONE Java では MIDlet-Network が Y に指定されている必要があります。
また、improve では RecordStore を使用していますので、MIDlet-Data-Size に1024 byte 程度の上限値を与えてください。
画像については以下のものを使用してください。
矢印 | ||||
通常時アイコン | ||||
選択時アイコン |
実行してみよう
無事アーカイブができたら実行してみてください。図7,8 にスクリーンショットを示します。
図7 improve のメイン画面 | |
図8 メッセージング画面 | |
※ ImproveServlet 配備先の /check にアクセスするとサーバーが現在保持している全データが HTML で一覧できます。
参考までに、プロトコルをテストした時のスクリーンショットを 図10 に示します。
まとめ
- データモデル・プロトコルの仕様を決めました。
- サーバーの設計を行いました。
- クライアントの設計を行いました。
- 実装したソースコードを示しました。
- エミュレーターで動作させてみました。