Chienomi

「事前生成戦略」の原点、チャットスクリプト (コードつき)

チャットの開発、そして事前生成戦略に到達する下りはなんども書いているが、それが非常に革命的で、「極めてシンプルにすることができた」という話はそれだけではピンとこないのが普通だろうから、コードを添えて解決することにする。

事前生成戦略

PureBuilder Simplyで採用されている「事前生成戦略」は、私にとってはウェブアプリケーション開発におけるウリのひとつになっている。

その内容は、基本的に「ユーザーが要求するものは静的HTMLであるようにする」ということである。

キッカケはNginxだった。 2005年にはもう取り組み始めていたのだから、かなり感度は高かったといえるだろう。

当時、私のwebサーバーはApache 1.3だったが、結局はこのあとLighttpdへの移行することになる。 そしてその後はまさかのDeleGateへと先祖返りする。

だが、Nginxを検討したのは「静的ファイルの応答速度を重視している」という設計のためだ。

2005年には、ADSLの普及により回線速度がだいぶ向上していた。結果的に、従来では考えられないくらいチャットに対するリクエストが増大するという問題が発生していた。 ちなみに、当時のHTMLチャットといえば、「みんなでアクセスするとエラーになるもの」であり、ログ表示がおかしかったり、発言が消えたり、ページが取得できなかったりということは日常的にあった。

だが、そもそもwebサーバーは静的なページを配信するのが最も基本であり、静的なページにすることで多くの問題が解決するものと思われた。 そもそも、静的なページを取得するのであれば、CGIスクリプトは起動されず、負荷はとても少ない。 また、セキュリティ上のリスクなども(webサーバーが自分の責任で保守しているのではないなら)考えなくて良い。

少なくとも、静的ページを配信することはセキュリティ上もパフォーマンス上も良いことであるのは確かだ。

チャットで事前生成戦略

そもそも、チャットで問題が起きる理由は、「新しい発言を取得しようとしてリロードボタンを連打するから」であり、一種のF5アタックを仕掛けることになるからだ。

仮にF5アタックがさけられないとするならば、更新されるものが静的HTMLであればスクリプトが呼ばれることはなく、随分軽くなる。

私はそもそも、チャットスクリプトは随分長く開発に取り組んでいた。 その目的は既存のフリースクリプトを利用していた状況からの脱却であり、私がプログラミングを再開したときにまず行ったのは、チャットスクリプトを改造すること、そして学習の一環としてそれら(特にPerl4で書かれているもの)をモダンに書き換えることだった。

私のサイトでメインで使われてきたチャットスクリプトは「ゆいちゃっと2000」のち「TeaChat」であるが、サイト全体では非常に多くのチャットスクリプトを動作させていた。 フリースクリプトを比較すると同時に、私の開発テストでもあった。 舞台となったのはfreeweb、そしてロリポップ!である。

プログラミング言語を学習する過程でも、「その言語に適した形でチャットスクリプトを再実装する」という目標があり、 制作した「TeaChatのクローンスクリプト」は、Perl5(オブジェクト指向版), Ruby 1.8, PHP5, Python 2.4となかなか多い。

TeaChatのクローンスクリプトを書く中で感じたのは、 「チャットスクリプトは結局のところ、発言があったときはログを更新するという操作があるが、そうでないときはログを読んでHTMLをビルドしているだけである」ということだ。

この気持ちがより強くなったのは、eRubyを学んだときだった。 チャットのページは、eRubyエンジン、eRubyテンプレート、ログファイルがあれば生成可能であり、Rubyスクリプトは必要としない。 だが、TeaChatに準じると、わざわざRubyスクリプトであれやこれやしてからeRubyエンジンを呼び出すことになる。

そして、何度も何度もリロードによってHTMLをビルドすることになるのだが、更新するのは発言するときだけであり、ビルドする頻度に対して更新する頻度は極めて低い。 更新されていなければ、全く同じ内容のページをビルドしているわけである。これは、この上ないほど無駄だ。

だったら、更新があったときだけHTMLを更新して、ユーザーはHTMLを読めばよいではないか。

結果的には、フレームだけでなく、発言用ページも、チャットページも全て静的HTMLという設計ができあがった。 唯一、発言フォームのsubmit先がCGIであり、これは当初は静的HTMLそのものを編集する、というものだった。 のちに、eRubyを使ってログから生成する設計に変更される。

具体なコード

当時のコードは残っていないが、GitHubにその概要を実装したものを上げた

TeaChatほど高機能なわけではないが、当時のHTMLチャットとして基本的な機能は備えている。 PerlのチャットCGIは小さなものでも300行程度はあったことを考えると、39行というのは驚異的な小ささである。 しかも、このスクリプトはWebrickサーブレットになっており、サーバー機能すら自分で賄う。

本質部分はわずか8行で、

cgi = req.query
chat.unshift({name: esc(cgi["name"]), timestamp: Time.now.strftime("%y-%m-%d %T"), chat: esc(cgi["chat"])&.[](0, 1024)})
chat = chat[0,30]
File.open("chat.log", "w") do |f|
  Marshal.dump chat, f
end
chat_content = ERB.new(CHAT_HTML).result(binding)
File.open("chat.html", "w") {|f| f.puts chat_content}

となっている。ここでは

  • パラメータを読み込み
  • パラメータをオブジェクト化してチャットログに追加し
  • ログを保存して
  • HTMLにビルドして
  • HTMLファイルとして保存する

という手順である。

リロード間隔は5秒と短いが、手動で連打されるよりははるかにマシなので、手動ではやりにくい仕様になっている。

事前生成戦略をとることでコードが短くなり、バグの余地も減った。 そして、設計自体が単純になり見通しもよくなった。基本的に、良いことづくめである。

事前生成戦略の応用

では、このような事前生成戦略が有効なケースを考えてみる。

掲示板システムなどは、より読むことが多く、より適している。 そのように考えていくと非常に多くのケースに使えるということがわかる。

なによりもすごいのが、ブログのような発信型のコンテンツだ。

このようなものは当時、ページマスタリングシステムと呼ばれていた。 その要点としては、テンプレートがあることでページヘッダなど共通の部分を一箇所にまとめ、コンテンツ部分だけが異なるものを書こう、というようなことだ。

しかし、ブログシステムでこれを使うのは明らかに過剰である。 現在でいえば、Pandocですらそれは叶えられるのだ。

このことから、「テンプレート機能のサポート」「PODのようなより簡単にコンテンツを記述できる言語のサポート」の2点がテーマになった。 前者はPureBuilderとして、後者はPureDocとして実装されることになる。これに「記事の前後関係をもたせる」ことを目標としたのがACCSだ。

これらの大きなメリットとして、事前生成戦略ならではの「軽さ」だけでなく、実装が容易であり、なおかつ安全であるということがいえる。

まず、ログイン機能、管理機能などを実装する必要がない。 単純にファイルとして生成するものなので、「ファイルの編集」という概念に吸収される。 これで実装がとても楽になる。

さらに、webに対する攻撃というのはほとんどが定番webアプリケーションの機能を起動しようとしたり、ログインを試みるものであるから、単なる静的HTMLファイルでは攻撃の起点がない。 非常にセキュアである。

「事前生成戦略」という考え方からすれば、そもそも手元で静的ファイルに変換してしまうPureDoc/PureBuilderはより発展的な考え方であった。 これとは別に、検索ページのようなオンデマンドで提供される必要があるものはこの考え方が通用しない。むしろ、負担になる場合すらある。 そのために、「遅延生成戦略」というものも編み出された。これは、「例えページが更新される状況が起きたとしても、最初にアクセスされるまではページを実際には更新しない」というものである。 ただ、これ自体はほとんど「コンテンツキャッシュ機能」と変わりがなく、それほど独特なものではない。生成するべきかどうかを判断するために、リクエストはスクリプトが受け付けなくてはならないからだ。 実際に遅延生成戦略を取ったアプリとしてはMongrelを用いたサーブレットとして実装されたものがいくつかあるが、キャッシュとして以上の効果は発揮できず、この話を大いにすることはなくなった。

郷愁, 探求, 求道

今にしてみれば意外に思われるかもしれないけれど、私は当初から「変わったコード」「きわどいコード」を書いていたわけではない。 そもそも私は完璧主義者だったし、ものすごくきっちりしたコード、学習時に使用したものを正しく踏まえたコードを書いていた。

だが、当時から好奇心は強かったし、真理の探求という傾向も強かった。チャットスクリプトの再実装を繰り返す中で、「これは本当に必要か?」「こう書いたほうが本質的で端的なのでは」という疑問が次々湧き上がり、 1年くらいは耐えていたが、結局は徐々にアレンジを加えるようになっていった。

「別にアプリケーションで応答する必要はないじゃないか」というところに至るのがその流れだ。 サンプルでは本質部分だけを書くことをテーマにしているが、実際にはTeaChatに存在する機能は全て網羅するものを制作した。 TeaChatには電報機能があり、これはprivate messageである。つまり、「ユーザー固有のチャットを事前生成する」という戦略に成功していたことを意味する。

これは2バージョンあり、最初のバージョンではユーザーごとに固有のHTMLファイルを開くようになっていた。 こちらはやや複雑で、

  • フレームページとしてディスパッチャが呼ばれる
  • ディスパッチャはUIDを生成し、UIDのディレクトリを作成してベースとなるHTMLファイルを出力する
  • フォームにはUID値を覚えておく
  • 当該チャンネル上で一定時間内に発言のあったUIDを覚えておき、そのUIDを更新対象にする

可能な限り漏れがない仕様ではあるのだが、見てわかるとおり完全ではない。 電報リストのためにF5しなければならないというのが最も大きい問題だろうか。

後発の仕様はAjax仕様であり、XMLHttpRequestを使う。 HTML内にメッセージIDが埋め込まれており、そのメッセージIDが未知のものであれば、XMLHttpRequestによって追加で取得する。取得したメッセージはJavaScriptによってタイムスタンプでソートし、チャットに混ぜ込まれる。 HeartComではこれを発展した形式になっており、PM用(というよりuser relem用)のアプリケーションが独立して存在する。そしてXMLHttpRequestによってポーリングを行い、メッセージが受信できるようになっている。

結局このチャットにおける事前生成戦略採用が転機になった。 このスクリプトは、常識や、正しいとされていることとは全く異なる。だが、完全に正しく動作し、意図を達成できる。

至って本質的だ。この事実は「本質は端的に表せる」ということを意味しており、その正誤自体は結果1によって証明できる。 また、正しく動作する限りいずれも正しいのであり、世の中で正しいとされている手法や常識はどちらかといえばそれに依存している。つまり、「正しいが、最適であるかどうかは別」である。

以降、私は端的なコードを目指すことになる。 本質がなにかを見極めることができればコードは劇的に短くなる。もちろん、場合によっては泥臭くても今思いつく方法で解決したほうが良い場合もある。 だが、それだけでは成長がない。とにかくタンテ的なコードを目指す。それも、できるだけ短時間で、だ。 私の向上が、求道が始まった。

この求道はなにも「事前生成戦略」というテクニックにとどまらず万事に及んだ。 途方もなく困難に思われたことも、今ならできる。


  1. 結果は現象であり、この「結果」を「売上」や「人気」で測ろうとすると真実にはたどり着けない。↩︎

Wrote on: 2019-09-02T00:00:00+09:00