Home日記コラム書評HintsLinks自己紹介
 

フィンローダのあっぱれご意見番 第110回「急がば近道」

← 前のをみる | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →

次世代携帯電話の実験サービスに申し込んだのだが、 あっさりと抽選には漏れたようだ。 実験申し込みの時にサーバがなかなか応答しなかったので、 倍率が結構高そうな気はしていたのだが、 もしかするとネタにしようという邪悪な動機が災いしたのかもしれない。

 

※ 何の実験なのか忘れてしまった。 Foma?

サイズの大きなデータをダウンロードしたい場合、 現在のモバイル環境ではどうしようもない。 もちろん、ダウンロードすればいいという考え方もあるが、 大金持ちならともかく、 128バイトで0.3円というパケット料金では自滅するようなものだし、 i-mode対応機種に変えたら速度が9600bpsになってしまって、まさにやってられない。 かといって、64kbpsで接続できるPHSだと、時間単位で課金されてしまうため、 ちょっとうっかりするとデータが流れていないのにどんどんお金が出て行ってしまうハメになる。 という訳で、とりあえず、速い環境が欲しかっただ。

 

※ Air H" はまだだっけ? DoPa というサービスを使っていたのだが、 i-mode 対応機種に機種変更したら、 速度が 9600bps に落ちてしまったのである。

ところで、自宅ではサーバが稼動していて、外部とはフレッツ・ISDNで接続されている。 64kbpsという速度は今では時代遅れかもしれないが、24時間接続可能なのと、 料金はどれだけ使っても変わらないというのがミソである。 実際、私の環境はおおむね接続したままになっているのだが、 たかが64kbpsとはいっても、実はそのバンド幅の殆どが無駄になっているのが現状である。 もっとも、フレッツ・ISDNのユーザー全員がバンド幅をフルに使ったらどうなるか分かったものではないが。

 

※ 「64kbps? 速いじゃん」という時代だったのだ、当時は。

そこで思いついたのは、 データを転送する指示だけを自宅のサーバに出して、 実際のデータ転送は自宅のサーバから行うというアイデアである。 これなら、ダウンロードの処理はフレッツの課金で済む。 つまり、定額料金だから、データサイズを気にする必要はないのだ。 これは割と簡単な話で、ftpでダウンロードできる対象なら、 自宅のサーバにログインしてftpを実行するだけである。 では、httpでダウンロードするには?

 

※ 自宅のサーバーは Linux か Solaris か、 その種の環境になっていたはず。

思いつきはいいとして、実現するのが結構面倒のような気がしてきたのだが、 とにかくCGIでダウンロードする処理を書いてみた。 指定したURLからデータをgetしてファイルに保存するperlスクリプトである。 ダウンロードの処理自体は問題なく動作したが、 一つ致命的な問題が残ってしまった。 CGIの処理が終了するまで、処理が戻ってこないのである。 つまり、ダウンロードに10分かかるターゲットを指定したら、 それをダ1ウンロードし終わるまで「リクエストを受け付けました」というページが表示されない。 ブラウザが応答を待ったままになるのだ。

 

※ system とか使って、 バックグラウンドで中身を動かすスクリプトを叩けばよかったかな?

もっとも、これは時間単位の課金なら致命的な状況だが、 DoPaのようなデータ量単位の課金であれば、それほどの問題ではない。 データのやりとりがなければ1円も料金はかからないからだ。 ただ、モバイル環境でなかなか厳しいのがバッテリーの持ち時間である。 携帯電話の連続待ち受け時間はかなり長くなったが、 連続データ通信時間というのはそれほど長くなっていないのだ。 無駄な接続は極力避けたい。

できれば、リクエストだけサーバに出しておいて、 処理を受け付けたというページをすぐに表示したい。 例えば、ダウンロードの処理を別のプロセスで並行させておき、 画面に表示するデータはすぐに戻するという手がある。 本格的にやるには、 ダウンロードサーバのためのプロセスを立ち上げておいて、 CGIからそのプロセスにコマンドを送って、リクエスト自体への応答はすぐに表示する、 というような構成が基本か。 Perlを使ってネットワークプロセスを書くのはそう難しい話ではないといっても、 別プロセスのサーバを立ち上げるというのは気が重い。

§

  

そこでで思いついたのが、 もしかしてservletで実現すれば簡単ではないか、ということである。 Javaはマルチスレッドの処理が得意だという噂だから、 そのあたりの実装は簡単ではなかろうかと思ったのだ。 実際、これも最後にはうまく行った。 ところが、以前紹介したように、私にはJavaの知識がないという巨大な壁がある。 Java的には、私のレベルというのはビギナーofビギナーということで、 まさに真の初心者なのだ。 というわけで、思わぬ所でひっかかったのである。

 

※ まあ実際得意であるが、 後述のように至るところに thread safe かどうかという罠があるので注意。

早速、今回の大ボケを紹介するわけだが、それはファイル名の作成に関する処理である。 早い話が、マルチスレッドとかネットワーキングの話とは全然関係ないのだ。 URLを指定してファイルをダウンロードするという発想はそれでいいとして、 作成するファイルの名前は何とすればいいだろう。 ダウンロードしたURLを参考にするのはナイスアイデアである。 構造的には、URLを全て駆使してディレクトリ構造を構築し、 そこにダウンロードするというのが最も美しいと思う。 ただ、それは確かに美しいが、 場合によってはパスが長くなりすぎるし、 世の中にはヘンな名前をつけたがる人もいるという厳しい現実もある。 そこで、処理時刻を使ってファイル名を作成し、 ダウンロードの履歴を記録するログの中に、 作成したファイル名とURLのペアを記録する仕様にした。

  

作成するファイル名は、年月日と時分秒をハイフンで繋ぐことにする。 つまり、"20010701-123456" のような感じである。 この場合は、2001年7月1日12時34分56秒に作ったファイルということになる。 1秒以内にダウンロードを複数回行うと上書きされてしまうという冗談のような制限事項があるが、 フレッツ・ISDNではまず問題ない仕様だ。 ただし、数百Mbpsの速度になった時に、この仕様で大丈夫かどうか、 いまいち自信はない。数十Mbpsの時代になったら、まずアウトだろう。 しかし、とりあえず今のところこれで全く問題ない。

 

※ 今だと怪しい。 ということで、最近は、Time の表現による long の値をそのまま使ったりする。 ms まで表現しているので、まず衝突しないし、 衝突したら1増やした値を使えば問題ない。 ただし将来1msに1つ以上のファイルをダウンロードするようなことになったら、 潜在しているバグが活動し始めるかもしれない。

さて、コケたのはこのファイル名の作り方である。 とりあえず、List 1 のようなコードで処理できると思った。 何か最近全然Cの話題が出てこなくてすみません。 これはJava。 cal.get(cal.MONTH) が、今月を表現する数値を0~11で戻してくれるから、 それに1を加えた値が今月になる。 Stringのsに数値を加えることができるというのがJava的だ。

  
---- List 1 ----

    Calendar cal = new GregorianCalendar();
    String s = "";
    int i;

    i = cal.get(cal.MONTH) + 1;
    s += i;

---- List 1 end ----
 

※ こんなところで new しないで、 現在時刻をその都度取得した方がいいかも。

はて、どこでコケたのか? List 1 を実行したら、 もし今が8月だとすると、 String s の中身は "8" のようになる。 しかし、欲しいのは "8" ではなくて "08" なのだ。 8月ならまだいいが、1月11日と11月1日がどちらも111になっては洒落にならない。 では、数字の8を "08" に変換するにはどうすればよいのか? 言い換えれば、 Cやperlの sprintf に相当する処理をどうすれば実現できるのかという問題だ。 ちなみに、目的のコードを perlで書けば、List 2 のようにたった1行である。

 

※ 当時の Java の標準APIにはprintfがなかった。 printf が使えるようになったのは、 2004年に JRE 5 が登場してからである。

---- List 2 ----

  s = sprintf("%02d", i);

---- List 2 end ----

これをJavaだとどう実現するか、というだけの話なのだが…。 ええ、もちろん Web でも探してみました。 すると、Format だとか NumberFormat のクラスを解説したページがヒットするし、 このあたりのクラスのリファレンスマニュアルは一応読んだつもりだ。 読んだといっても、 ざっと見た程度で天を仰いで挫折したというか、 何となくここを熟読すれば出来そうな気がする、 という所から先に進めなくなってしまった。 いや、諦めて進むことを放棄した。

§

なぜ進まなかったのか? 実は、この問題を全く別の方法で解決してしまったからなのである。 つまり、近道があることに気付いたのだ。 実際に使ったコードは List 3 のような感じになった。

  
---- List 3 -----

    i = cal.get(cal.MONTH) + 1;
    if (i < 10) {
      s += "0";
    }
    s += i;

---- List 3 end ----
 

※ 説明するまでもないと思うが、 1~9月だったら先に文字列に '0' を書いておいてから、 その後に文字列表現化した月を追加している。

超馬鹿馬鹿しいけど、これでも動作上は全く問題ないのである。 基本に戻って考えてみたのである。 私は一体何をしたいのか。 欲しいのは、「数値が1桁ならば頭に"0"を追加する」という処理だ。 だったらそういうコードを書けば解決するのではないか。 安直な発想だが、筋は通っている。

  

マニュアルで数値をフォーマットして文字列化する処理を調べるよりも、 具体的なコードを書いた方が早かったわけだ。 それに、このコードは残念なことに、難しくも何ともない。 1分あれば書けるし、 このロジックを理解できないようなプログラマーはまずこの世には存在しないだろう。 アプローチとしては、これってJava的にはかなりの邪道だと思うのだが、 これでも実際何の問題もなさそうだということを気付いてしまったために、 山を越えてJava的な正道の解法を調べる気など全く起きなくなってしまったのである。

 

※ 1分で書けることは確かだが、 思いつかないと書けないコードでもある。

§

  

あまり読者の皆さんにヘンな影響を与えたらヤバいから正攻法も紹介しておく。 実はこの後 DecimalFormat を使えば目的を達成できることが分かった。 分かってしまえば、そんなに難しい話ではない。 List 4程度のコードで解決する。 最初、"00" というのを "##" にしてしまって訳がわからなくなったというのが余談。 しかし、後から考えてみると、実際のところどちらが正道なのかよく分からないような気もする。 それに、肝心の処理は書き換える意欲がないので、実は元のままで使っていたりする。

 

※ printf とかの先入観があると、#を使いたくなる。 おっと、スレッドが…という話でしたっけ。 DecimalFormat.format がスレッドセーフか、 という巨大な問題が残っている。

---- List 4 ----

  DecimalFormat dm = new DecimalFormat("00");
  s += dm.format(i);

---- List 4 end ----
  

何はともあれ、プライベートサーバでは tomcat が稼動し、 URLを指定すれば、 それをローカルディスクにダウンロードする servlet も動いた。 出来てしまえば、 リモートからダウンロード指示を出したいようなコンテンツがあまり見つからなくなってしまったのがどうも不思議だ。 それはそうとして、 その後は、戻してくる画面がちょっとそっけないので、使いやすい画面にしたいと思って JSPにforwardするような構成を考えてみたのだが、 これがまた罠にハマりまくりという話はまたの機会に。

 

※ サーバーはしばしば停止しているが、 とりあえずこの処理自体は健在だ。

(C MAGAZINE 2001年8月号掲載)
内容は雑誌に掲載されたものと異なることがあります。

修正情報:
2006-03-03 裏ページに転載。

(C) Phinloda 2001-2006, All rights reserved.