preload
5月 08

前回までのエントリ(第1回, 第2回)では、FlashLite コンテンツのサーバでの動的生成/合成処理について、開発ステップの全体的な流れや、swfmill を使用する際の SWF 構造の見方などについて紹介してきました。

今回は、前回のサンプルを使って、実際に swfmill を使用しながら SWF の動的生成をおこなう手順を紹介していきます。

前回の後半で、swfmill swf2xml で得られた XML をテンプレート化しておき、これを操作する方法として、次の2種類を紹介しました。

  • XML 中の置き換えをおこなう部分を場所を一意に特定できる文字列にしておき、プログラムからは文字列置換により書き換える
  • 置き換え対象部分を独自に定義した要素などに編集しておき、適当な XML 操作のための API (僕の場合は、PHP であれば SimpleXML、Ruby であれば REXML や Libxml-Ruby などを使ったりしてます) を介して書き換える

ここでは、手軽な前者の方法により、前回紹介したサンプルの背景画像の入れ替えをやってみます。背景画像は、XML 中:

182       <DefineBitsJPEG2 objectID="4">
183         <data>
184           <data>/9n/2P/Y/+AAEEp..

の data で定義されていますので、これを下のような形に書き換えてみます。

182       <DefineBitsJPEG2 objectID="4">
183         <data>
184           <data>####BACKGROUND_IMAGE####</data>

この XML を sample.xml として保存します。あとは、プログラムから、この XML の ####BACKGROUND_IMAGE#### の部分を文字列置換により JPEG 画像に差し替え => swfmill xml2swf とやれば、背景画像の差し替え完了となります。

せっかくの XML データですから、各種 XML API を介して操作するのが良いのかもしれませんが、システム内外からの不特定な入力の余地がないのであれば、こういった単純な文字列置換の方が、よりシンプルに実装でき、かつ高速な動作が期待できると思います。

今回は、ruby から swfmill を起動、XML の文字列置換をするためのクラスとして、下のような簡単なクラスを書いてみました。

module SwfmillUtil

  SWFMILL = "/Users/tmtysk/bin/swfmill"

  class Swfmill

    def self.xml2swf(xml, option = "-e cp932")
      IO.popen("#{SWFMILL} #{option} xml2swf stdin", "r+") do |io|
        io.write xml
        io.close_write
        io.read
      end
    end 

    def self.swf2xml(swf, option = "-e cp932")
      IO.popen("#{SWFMILL} #{option} swf2xml stdin", "r+") do |io|
        io.write swf
        io.close_write
        io.read
      end
    end

  end 

end

これを使って、以下のように差し替え処理を書いてみます。背景画像は bg.jpg とします。

require '../lib/swfmill_util'

xml = File.open('sample.xml').read
xml.gsub!(Regexp.new('####BACKGROUND_IMAGE####'),
            Base64.encode64([0xff, 0xd9, 0xff, 0xd8].pack("C*") +
            File.open('bg.jpg').read).gsub("\n",""))                                                                                                        

File.open('foo.swf', 'w') do |f|
  f.write SwfmillUtil::Swfmill.xml2swf(xml)
end

上記では、ファイルからオープンした jpeg データの先頭に [0xff, 0xd9, 0xff, 0xd8].pack(”C*”) の4Byteの文字列を付加していますが、これは SWF File Format Specification にも記載されている接頭子(マーカー)になります。このマーカーを忘れたり、また、Base64エンコーディングした後の改行文字を取り除くのを忘れたりすると、SWF 生成に失敗したり、SWF 生成は一見うまく行くのに再生すると画像部分が真っ赤になっていたりします。この「画像が真っ赤になる現象」は、プレイヤーが画像をレンダリングできない <= 画像データがどこかマズいという事のようで、画像の差し替えをやるとよくハマる箇所ですので、もし発生したら、上の画像エンコード周りを確認してみると良いと思います。

上のコードを実行すると、差し替え後の swf が foo.swf として保存されます。生成した swf をそのままブラウザに返すのであれば、適当な Content-Type を設定した上で SwfmillUtil::Swfmill.xml2swf(xml) の値をそのまま送信してやれば OK です。ただし、swfmill の起動コストもありますので、(程度にもよりますが)アクセスが集中するようなサイトで、同期的に SWF を生成してブラウザへ返す、というような事をやるのは、あまりオススメできません。あらかじめ SWF を作り置きしておくとか、非同期処理やバッチ処理で回避できるよう画面遷移を調整するとか、SWF 再生中のボタンアクションで非同期に loadMovie するように SWF の構成を見直すとか考える必要が出てくる事もあります。swfmill だと限界が見えてくるかもしれませんが、この他、負荷軽減のアイデアや実績があれば、コメントなどで教えていただければ幸いです。

さて、今回は、文字列置換により背景画像のみ差し替える、というやり方を紹介しましたが、同様の方法で、前回紹介したような「ムービークリップシンボルを丸ごと差し替える」というのも可能です。具体的には DefineBitsLossless2, DefineShape, DefineSprite 辺りを丸ごと文字列置換や XML 操作で置き換えてやれば OK です。この際、置き換え前後で objectID の参照関係が変わってしまわないように注意する必要があります。この辺の話も書こうと思ってたのですが、似たような話になってきたので、ここは割愛することにします。

今回使用したコードや、もうちょっと便利に使えるようにしたクラス群を github かどこかに置こうと思っています。ご興味の方、適当に遊んでみていただければと思います。

最後に、この手の事を swfmill 経由でやっていて、よく聞かれる事をまとめておきます。

Q. jpeg の部分に gif, png など他の形式の画像を置く事ってできる?

A. もっとも簡単な方法は、swfmill 外部で GD やら ImageMagick やら使って画像形式を相互変換してしまうことです。が、それだと SWF の容量制限上アレだったり、圧縮がかかって見た目上ナニだったりするので嫌、ということになると思います。そういうときは、swf2xml した後で、DefineBitsJPEG2 と DefineBitsLossless2 を置き換えてやれば OK です。jpeg 画像は DefineBitsJPEG2 で定義されていますし、それ以外の画像(gifやらpngやらbmpやら)は、すべて SWF の独自ビットマップ形式として DefineBitsLossless2 で定義されています。

DefineBitsJPEG2 では、今回の上で紹介した通り、比較的簡単な方法で JPEG データを流し込む事ができますが、DefineBitsLossless2 のデータについては SWF の独自形式ですので、既存の画像形式からの変換処理を書いてやる必要があります。この辺りの処理は、後ほど公開するクラス群に入っていたりするので、ご興味のある方はご覧ください。あまりよく確認してないですが、PHP や Perl でも実装例があったようななかったような気がしますので、そちらをお求めの方は適当に検索してみると良いと思います。

Q. 文字(テキスト)の入れ替えをやりたい?フォントを変える事もできる?

A. FlashLite1.1 の場合、テキストデータは DefineEditText(ダイナミックテキスト)要素 の initialText 属性で定義されているのがほとんどです(DefineText でパブリッシュされたのを見た事無いだけで仕様的には存在しうると思うのですが)。なので、この initialText を書き換えて xml2swf してやればテキストの入れ替えが可能です。テキストに日本語を含む場合もありますので、KLab の方が公開されている swfmill への文字エンコーディング指定パッチを適用しておくと便利です。(紹介を忘れてましたが、このパッチ、FlashLite1.1 を swfmill で扱う場合は、ほぼ必須と思います。)

フォントを変えるというのは、不可能ではありませんが、ちょっと手の込んだ事をやる必要があります。というのも、指定されうるテキストのフォント情報(グリフ)を SWF に埋め込むと、たいていの場合、それだけで FlashLite の容量制限をオーバーしてしまうからです。やるのであれば、「テキストではなく画像にしてしまう」、もしくは、「表示するテキスト情報から使用されている文字を抽出し、使用する文字の分だけグリフを埋め込む」という事をやることになるでしょう。グリフ情報はあらかじめ DB などで保持しておけば OK です。なお、フォント種類や文字にもよりますが、幾つか試してみた感じでは、日本語の漢字一文字を埋め込むと、SWF サイズが大体 300〜500Byte くらい増加するようです。

ちなみに、グリフ情報は DefineFont タグなどで定義されています。適当に1,2文字フォント埋め込みをしたSWFをパブリッシュして、構造を解析してみると良いかもしれません。

Q. ActionScript 部分を差し替えたい

A. これは、わりと僕がよくやる方法です。ActionScript のロジックは DoAction タグで定義されていますので、この子要素を SWF File Format Specification の action model 仕様を見ながら書き換えてやれば OK です。具体的には、SWF の再生フローを ActionScript 内の特定の変数値によって分岐するように書いておき、この変数値をプログラムから書き換える、といった感じです。Flashゲームや、ちょっとしたツールなど、アプリケーションライクな SWF を動的生成する際、動的な部分をある程度 SWF 側に持たせてしまうことで、生成プログラムの開発コスト/個別対応コストを下げる事も期待できます。

単に変数の置き換えをするのであれば、変数値を固定長(長さが変わってしまうと、内部で管理されているタグの長さの不一致が発生し、swf 構造が不正なものとなってしまいます)にしておけば、swf をそのまま(バイナリセーフな)文字列置換処理にかける事で、swfmill すら使わずに動的に swf を書き換える事もできます。swf の動的生成(と呼んでいいのかアレですが)としてはかなり高速な方法ですので、ゲームなど多量なアクセスが発生しうる SWF のセッション管理用にユーザ/セッション固有の ID を埋め込む、というのにも有効です。

Q. swf2xml したものを xml2swf しただけなのに、再生できなくなった

A. これはかなり稀なケースなんですが、swfmill で if / else の ActionScript コードを含む SWF を swf2xml したときの ActionScript のバイトコード変換処理に不都合があるらしく、ブロックジャンプのオフセット値が間違って出力されることがあるようです。swfmill のどの辺りかを追えてないので、根本的な解決ができてないのが申し訳ないのですが、こいつも SWF File Format Specification の action model 周りの仕様を睨みながら、出力された XML 中のオフセット値を適当に調整してやる事で、問題が解決できる場合があります。

以上、3回にわたって書いてきましたが、この手の話で、現時点で文章にできるのはこんなところです。機会があれば、今後は、こういう仕組みを使って個人的に作ったものを紹介していければ良いなあと思ってます。

何か参考になれば幸いです。

Tagged with:
5月 04

前回のエントリでは、サーバで生成した FlashLite コンテンツを配信するケータイサイトの開発/制作フローとして、以下のような流れを紹介しました。

  1. SWF への入出力(input/output)に見通しを立てる (全員で)
  2. Flash IDE でプロトタイプの fla ファイルを制作し、SWF でパブリッシュ (デザイナ)
  3. SWF 中の何(What)を、どのように動的にいじりたいかを整理 (全員で) (ここまでが前の項)
  4. 動的にいじる手段を選択 (主に開発者で)
  5. 決定した手段に応じて、SWF への入出力を決定 (主に開発者とデザイナで)
  6. fla の制作ルールを決める (主に開発者とデザイナで)
  7. 上の制作ルールに基づいて Flash IDE で fla ファイルを制作し、本番用の SWF をパブリッシュ (デザイナ)
  8. 制作した SWF を必要に応じてテンプレート化する (開発者もしくはデザイナで)
  9. テンプレート化した SWF をいじるアプリケーションを実装する (開発者)
  10. できあがり。運用時の更新は、7〜8〜9の繰り返し

前回は、このフローの4まで、つまり SWF をサーバ上でどのようにいじりたいかを検討し、そのための手段(ツール)として、swfmill を選択する、というところまで紹介しました。

今回は、具体的なコンテンツを想定しながら、このフローの5以降の流れについてまとめていきたいと思います。

サンプルコンテンツの概要

動的にいじるサンプルコンテンツとして、以下のような背景画像+キャラクタのアニメーションを再生するシンプルなFlashLiteコンテンツの動的生成要件を考えてみます。(上記のフローの 3 までの検討結果イメージについても併記してみます。)

  • ステージサイズは 176×208
  • 背景画像とキャラクタのアニメーションをステージ上に持つ
  • SWF を動的に生成するときの入力(input)
    • 背景画像
    • キャラクタ画像
  • SWF 再生後の出力(output)
    • なし。ただ再生するだけ
  • SWF 中の何(what)をいじるのか
    • 背景画像とキャラクタ画像

文字だけだとどんなコンテンツなのかイメージがわきにくいので、サンプルも貼っておきます。(DeviceCentral で再生しているものをキャプチャしています。)

今回は、このコンテンツをベースに、

  • 背景の jpeg 画像を指定のものに差し替え、
  • 飛んでいるキャラクタの画像とアニメーションを指定のムービークリップシンボルに差し替える

ということをやってみます。具体的には、背景画像を以下のもの:

bg2

に差し替え、さらに、キャラクタのアニメーションを、別途作成した以下のようなムービークリップシンボル:

に差し替えたものを、新しい FlashLite コンテンツとして生成することとします。(余談ですが、FlashLite の動的生成が「FlashLite の(動的)合成」と表現されている事がありますが、その意味するところは、上記のように「ムービー中の画像素材などを指定して、これらを組み合わせて(合成して)FlashLite の生成をおこなう」ということと理解しています。)

それでは、以下、先に紹介した制作フローの続きを紹介していきます。

SWF 生成(合成) 時の入出力を決める

先に検討した入出力を精査し、何を入力パラメタとして SWF をつくり、ムービー再生後にどのような出力パラメタをもってサイト遷移に戻るか、を決定します。今回は、以下のように決定したとします。

  • 入力
    • 背景画像: ステージサイズと同じ大きさの jpeg データ
    • キャラクタ画像: アニメーションを伴ったムービークリップシンボルとして指定される(元のキャラクタ画像は GIF89a)
  • 出力
    • なし

なお、実際の要件や制作/開発の現場では、上のようにまとめられるとは限りません。「背景画像には jpeg と GIF の形式のどちらかが指定されうる」とか「キャラクタ画像は画像のビットマップのみ入力として与えるだけとし、アニメーションは固定で良い」などの要望が出る場合があります。今回は、この後のステップを紹介するのに都合が良いように要件を決めていますので、まずは以下をお読みいただいた上で、上記のような要件変更/要望の取り込みがどの辺りの開発ステップ/難易度/コストに影響を与えてきそうかを捉えていただければと思います。正直なところ、ここ以降の制作フローの詰めが、FlashLite コンテンツの動的生成をおこなう際の一番の難所(業務でやる場合は、リスク要因となりやすいフェーズ)と、個人的には感じています。

.flaの制作ルールを決める

SWF 中の差し替えをおこなう要素と、そのときに与えられる入力が決まったところで、デザイナと開発者との間でベースとなる SWF の制作ルールを決めていきます。なぜ、制作ルールを決める必要があるかと言うと、一番の理由は、プログラムから差し替えをおこなう要素が特定できるようになっていないことには、画一的な処理で要素の差し替え(SWF の動的生成)をすることができず、ベースとなる SWF ごとに処理をつくってやらなければならなくなるからです。

これは逆を言えば、ベースとなる SWF が少なく、運用時の SWF 追加や更新も無い、などの理由で、個別の動的生成処理を実装していくのにコストがかからないのであれば、この時点で厳密な制作ルールの握りをつくる必要は無い、ということにもなります。

今回は、この後の説明を簡単にする都合で、以下のように制作ルールを決める事ができた、と仮定します。

  • 背景画像は、jpeg を貼付けただけのムービークリップシンボルを別途作成しておき、これをステージに配置する際に “bg” というインスタンス名を付与しておく事にする
  • キャラクタ画像は、アニメーションを含める形でムービークリップシンボルを別途作成しておき、これをステージに配置する際に “animation” というインスタンス名を付与しておく事にする

上記のように、今回は、差し替えをおこなう要素をふたつともムービークリップシンボル化しておくことを仮定しています。

ただ設置するだけの背景画像を、わざわざシンボル化して、インスタンス名まで付与するのは、制作側からすれば一見非効率かも知れません。ただし、これらのルールは、先にも述べた通り、プログラム側から差し替えをおこなう要素(ムービークリップ)を特定/識別するためのルールですので、ケースバイケースと考えていただければよろしいかと思います。たとえば、「SWF 中には、背景画像として唯一 jpeg 要素を使用する」という制作ルールがあれば、「背景画像として入力した jpeg を SWF 中の jpeg データと置き換える」という処理を作ってやれば、わざわざインスタンス名で特定する必要がないということにもなります。

本番用 SWF をパブリッシュし、必要に応じてテンプレート化し、差し替え処理を実装する

上記のルールに基づいて .fla を制作し、SWF をパブリッシュしたら、いよいよ、具体的な FlashLite 動的生成処理の実装に入っていきます。

まずは swfmill swf2xml を使って、できあがったベースの SWF の構造を確認してみます。(swfmill の基本的な使い方は、他のサイトでもわかりやすい説明が多いので、ここでは割愛します。)今回のサンプル SWF は以下のようになっていたとします。

  1 <?xml version="1.0" encoding="UTF-8"?>
  2 <swf version="4" compressed="0">
:
 13       <DefineBitsLossless2 objectID="1" format="3" width="30" height="30" n_colormap="7">
 14         <data>
 15           <data>eNq9kEsO...
:
 17       </DefineBitsLossless2>
 18       <DefineShape objectID="2">
:
 30               <ClippedBitmap objectID="1">
 31                 <matrix>
 32                   <Transform scaleX="20.00000000000000" scaleY="20.00000000000000" transX="-300" transY="-300"/>
 33                 </matrix>
 34               </ClippedBitmap>
:
 52       <DefineSprite objectID="3" frames="20">
 53         <tags>
 54           <PlaceObject2 replace="0" depth="1" objectID="2">
 55             <transform>
 56               <Transform transX="299" transY="299"/>
 57             </transform>
 58           </PlaceObject2>
 59           <ShowFrame/>
:
176       </DefineSprite>
177       <PlaceObject2 replace="0" depth="3" objectID="3" name="animation">
178         <transform>
179           <Transform transX="3541" transY="1031"/>
180         </transform>
181       </PlaceObject2>
:
182       <DefineBitsJPEG2 objectID="4">
183         <data>
184           <data>/9n/2P/Y/+AAEEp..
:
186       </DefineBitsJPEG2>
187       <DefineShape objectID="5">
188         <bounds>
189           <Rectangle left="0" right="3520" top="0" bottom="4160"/>
190         </bounds>
191         <styles>
192           <StyleList>
193             <fillStyles>
194               <ClippedBitmap objectID="4">
195                 <matrix>
196                   <Transform scaleX="20.00000000000000" scaleY="20.00000000000000" transX="0" transY="0"/>
197                 </matrix>
198               </ClippedBitmap>
:
216       <DefineSprite objectID="6" frames="1">
217         <tags>
218           <PlaceObject2 replace="0" depth="1" objectID="5">
219             <transform>
220               <Transform transX="0" transY="0"/>
221             </transform>
222           </PlaceObject2>
223           <ShowFrame/>
224           <End/>
225         </tags>
226       </DefineSprite>
227       <PlaceObject2 replace="0" depth="1" objectID="6" name="bg">

swfmill を使った FlashLite の動的生成処理を作る際は、(場合にもよりますが、)

  • ベースの SWF をそのまま保持しておき、動的に swf2xml => 差し替え処理 => xml2swf するよりは、
  • 上記のように XML 化したものを要素を差し替え可能な状態に適宜編集して保持しておき、動的に 差し替え処理 => xml2swf  したほうが、

応答速度的にも効率が良くなると思います。ですので、ここでベースの SWF の構造を XML 化しておき、差し替えをおこなう要素の構造を理解しておくとともに、この XML を差し替え可能な状態に編集し、テンプレートとして保存しておく事にします。

今回の要件では、ステージにムービークリップを配置した際のインスタンス名を “bg” または “animation” で指定してもらっているはずですので、まずはこれらがどこに位置しているかを見ていきます。FlashLite 1.1 でパブリッシュされた SWF では、ムービークリップは PlaceObject2 要素(Tag)で name 属性をともなって配置されているはずです。(このあたりの SWF の構造に関しては、Adobe 社が公開している SWF の仕様 – SWF Technology Center (必要に応じて Open Screen Project )も参考にしてください。)上記の場合では、

177       <PlaceObject2 replace="0" depth="3" objectID="3" name="animation">

の行と、

227       <PlaceObject2 replace="0" depth="1" objectID="6" name="bg">

が該当する事になります。

ここから、さらに PlaceObject2 が参照している object を辿っていきます。objectID 属性を確認すると、たとえば animation インスタンスは objectID=”3″ ですので、objectID=”3″ となっているムービークリップ要素 = DefineSprite 要素を探します。今回の例ですと:

 52       <DefineSprite objectID="3" frames="20">
 53         <tags>
 54           <PlaceObject2 replace="0" depth="1" objectID="2">
 55             <transform>

ここが該当します。このなかで、さらに PlaceObject2 により objectID=”2″ が参照されていますが、ここではムービークリップ内に設置されたキャラクタ画像が参照されているはずです。FlashLite 1.1 でムービークリップ内に画像を定義する方法は何種類かありますが、基本的には、

  • DefineBitsLossless2 もしくは DefineBitsJPEG2 で定義した画像データを、
  • //DefineShape/styles/fillStyles/ClippedBitmap で参照し、このシェイプを PlaceObject2 で配置する

というのが一般的な構造になるようです。今回は、上で指定されていた objectID=”2″ な DefineShape を辿って、さらにそのなかの ClippedBitmap というように、objectID をたどっていきます。最終的には、

 13       <DefineBitsLossless2 objectID="1" format="3" width="30" height="30" n_colormap="7">
 14         <data>
 15           <data>eNq9kEsO...

に行き着きます。ここの data 要素の中身が、アニメーションをおこなっているキャラクタ画像のビットマップデータということになります。今回はアニメーションとキャラクタ画像を丸ごと差し替えたいという要件ですので、このあたりの DefineBitsLossless2, DefineShape, DefineSprite の構造を、objectID の参照関係を維持したまま作り替えてやれば、アニメーション部分の差し替えができるということになります。

同様に、背景画像については上記 227 行目の PlaceObject2 で参照されている objectID=”6″ を追っていってやれば、

182       <DefineBitsJPEG2 objectID="4">
183         <data>
184           <data>/9n/2P/Y/+AAEEp..

に辿り着きますので、この data 要素を差し替えてやれば良い、ということになります。

対象の SWF の構造がわかったところで、この XML をテンプレート化します。差し替え処理の実装方法は幾つか考えられますが、

  • XML 中の置き換えをおこなう部分を場所を一意に特定できる文字列にしておき、プログラムからは文字列置換により書き換える
  • 置き換え対象部分を独自に定義した要素などに編集しておき、適当な XML 操作のための API (僕の場合は、PHP であれば SimpleXML、Ruby であれば REXML や Libxml-Ruby などを使ったりしてます) を介して書き換える

などの方法があります。前者は単純な文字列置換なので、対象の SWF がシンプルであれば非常に簡単に実装できますが、その分、.fla の制作ルールや実行環境によってはバグやセキュリティホールを作り込んでしまう可能性もあるので、注意した方が良いかもしれません。

ちょっと長くなってきたので、実際の入れ替え処理を書いていくところは、次回以降にしたいと思います。

まとめ

以上、2回にわたってケータイサイトで FlashLite コンテンツを動的生成する方法の概要をまとめてきました。全体の流れが見えると、ベースとなる SWF の制作ルールと、システム開発コストがどのように関連してきそうか、が、なんとなく見えてくるんじゃないかな、と思います。

実際のところ、制作ルールの策定は、動的生成処理の開発コストを、システム開発側とSWF制作側とで分担する割合/バランスを探る作業であるようにも感じています。ですので、全体の流れや仕組みの見通しが立たない事には、実サービスにこういった仕組みを入れていくのは大変かもしれません。

FlashLite 動的生成のシステムには何回か関わらせていただいていますが、正直なところ、こういった流れ、とくに制作側にいろんな制作ルールをお願いするようなやり方が本当に良いのかどうかは見え切れていません。今でも、わりと試行錯誤しています。この手のシステムに関わった方と情報交換する機会がほとんど無いので、いろんな方のご意見をうかがってみたいです。

さて、次回以降は、今回紹介したサンプルコンテンツの動的生成の続きから、実際に FlashLite 動的生成をやっていて使用したコードや、ハマったところなどが紹介できればと思います。

長々と読んでいただいてありがとうございました。期待せずにお待ちください。

Tagged with:
12月 27

先日 GitHub というのを初めて使ってみて、テキトーな小物をちょいちょい晒していくにも都合が良さそうだということがわかったので、今回、標記のプログラムも晒してみる事にしました。使用許諾条件は、基本的には the MIT License に基づきますが、「(迷惑メール送信に用いるなど)悪用・乱用を禁止する」という条件を付加したいと思います(っていうほど作り込んでないので、まあ、あんまし意味のない付加条件だと思いますが)。



MbSmtp – GitHub



MbSmtp は、Net::SMTP の拡張クラスで、日本の携帯向けにできるだけ効率よくメールを送る事を目的とした SMTP クライアントです。



MbSmtp は以下の機能を提供しています。

  • SMTP セッションあたりの送信通数が設定値以下になるよう、セッションを自動分割制御します
  • キャリアからの通信ブロックが発生した際、 設定時間待機した後、
    設定回数、失敗したメールのみ再送リトライします
  • メール送信処理中にエラーとなるアドレスが含まれていた場合、同一セッションで未送信のアドレスリストからエラーアドレスを除去します
  • セッション終了時、 送信リポートの構造体を返します
    • リクエストメッセージ数, 送信完了メッセージ数, エラーメールアドレスなど

各種設定値は lib/mb_smtp.rb 内で指定されていますが、必ずしも適正値ではありません。適宜設定を変更してご利用ください。



以下、簡単な使用例です。基本的には Net::SMTP と同じように使えますが、キャリアメールサーバは :docomo, :au, :softbank などといったシンボル指定で open できます。また、以下のようにセッション内である程度まとまった数を送信したとしても、1 セッションでの送信通数が設定値を超えないよう、勝手にセッション分割してくれます。その他、キャリアのメールサーバからの応答に応じて、エラーアドレスの振り分けや送信成功数のカウント、キャリアブロックに対する待機などをおこない、最後に MbSmtp::Result 形式でのレポートを生成する、といった動きをするようになっています。

@mail = TMail::Mail.new # MbMail::DMail とかでもOK
## @mail を適当に組み立てる
@result = MbSmtp.start(:docomo) do |smtp|
50.times do
smtp.send_mail @mail.encoded, @mail['from'].to_s, @mail['to'].to_s
end
end
puts @result # => MbSmtp::Result による送信結果レポート

この他の使い方や、動きの詳細については spec/mb_smtp/*.rb にある spec ファイルや lib/mb_smtp.rb 本体をご覧ください。



ちなみに MbSmtp は、元々は、個人的にメール配信時のキャリアのメールサーバの振る舞いを調べてみようとごにょごにょやっていたときの副産物です。実サービスでの利用実績はありませんし、これにより高速に携帯メール配信ができるという保証も検証結果も一切ありません。もちろん、これを使用した事で、キャリアブロックされまくってマトモにメールが送れなくなった、とかになっても、僕の方で責任はとれませんです。あくまでご参考まで、ということで。。

Tagged with:
12月 25

git とか GitHub とかいうのを使った事がなかったので、このたび初めて使ってみました。というわけで、標記の Rails プラグインを the MIT License にて公開します。


MbMail – GitHub


MbMail は、Rails で日本の携帯向けサービスをつくる際、 メール扱い周りで発生するちょっとした面倒を回避するための小物寄せ集めプラグインです。rails-2.0.2, tmail-1.1.0 環境下での動作を確認しています。また、rails プラグイン形式で配布していますが、MbMail::DMail は rails 環境に関係なく使えます。


MbMail は以下の機能を提供しています。

  • MbMail::DMail
    • キャリア間HTMLメール(デコメール, デコレーションメール, デコレメール)の相互変換
  • MbMail::MbMailer
    • ActionMailer の拡張
      • 標準で使用する charset (エンコーディング)を ISO-2022-JP に変更
      • 文字コード変換器に NKF を使用するように変更 (機種依存文字の送信を可能に)
      • 日本語ヘッダを作成するためのメソッド base64 を追加 (Mailer 内で日本語Subjectや日本語Fromを簡単に記述)
      • RFC違反のドットが連続するアドレスが扱えるよう、TMail::Parser を置き換え

たとえば、以下のように au デコレーションメール形式のファイルがある場合、これを load し to_docomo_format とすれば、docomo のデコメールフォーマットが得られます。

au = MbMail::DMail.load("au_decoration_mail.eml")
docomo = au.to_docomo_format
puts docomo.encoded # => docomo デコメールフォーマット文字列

その他、使用方法サンプルは、git リポジトリの中の spec ファイルたちをご参考ください。


パッケージに含まれるクラスのうち、MbMail::MbMailer は以前、こちらのエントリでも取り上げた「ドット連続アドレス問題」などに対応するものです。ただし、MbMailer を使用する事により TMail::Parser を書き換えてしまいますので、ご注意ください(MbMailer を使用するに当たって、ActionMailer の使い勝手を残しつつ TMail の一部だけ書き換える、ウマい方法が思いつかず。。誰か助けて)。


MbMail::DMail も MbMail::MbMailer も個人的な利用実績がありますので、基本的な部分はそこそこ使えると思います。が、プラグイン形式に切り出したのはこれが初めてですので、何かと不都合があるかもしれません。(絵文字の変換辺りで微妙にオカシイところがあるかも。)あと、git の使い方がよく分かってないうちは、何かと不慣れなことをしでかすかもしれません。諸々ご理解の上、ご利用いただければ幸いです。


なお、再掲を含みますが、上記のプラグイン提供に当たりお世話になった方々をご紹介しておきます。勉強させていただきました。ありがとうございました。

  • 携帯メールを扱うに当たっての base64 メソッドの実装や NKF 利用のアイデアとして、以下のサイト様を参考にさせていただきました
  • DMail の絵文字相互変換に Jpmobile の絵文字変換テーブルを使用させていただいています
  • ActionMailer の拡張時に TMail の幾つかのクラスを書き換え利用させていただいています
  • サンプルで使用している GIF 画像に 素材屋イチゴアポロ の素材を利用させていただいています

ついでに、以前こちらで紹介した「ケータイサイトで機種情報を取得する Rails プラグイン mbterm_db」も GitHub に引っ越しました。ついでに更新しようと思ったけど、更新の仕方を忘れたので、そのうちメンテします。


MbtermDb – GitHub


それでは皆様、よいクリスマス&よいお年をお迎えくださいませ。

Tagged with:
12月 11

以下のように around_filter 内で reset_session するアクションに対し、大量のアクセスを重ねていくと、メモリ使用量ががつがつ増大していく。

class FugaController < ApplicationController
around_filter :foo
def index
end
protected
def foo
reset_session
yield
end
end

こいつを script/server -e production で起動し、ab -n3000 http://localhost:3000/fuga/ などにより多量のリクエストを送ると、メモリ使用量が 80MB を超えるまでに増大し、リクエスト完了後もまったく解放されない。foo 内で yield しなくても同様。セッションストレージにデフォルトの CookieStore を使った場合のほか、:active_record_store, :mem_cache_store を使っても同じ。session_options[:expires] の設定も関係無し。
なお、before_filter や after_filter で reset_session した場合は、問題は起こらないようだ。また、rails-2.0.2 環境で発見したが、rails-2.2.2 でも再現する。

状況解析のため、こちらのエントリ(Memory leak profiling with Rails) を参考にメモリ利用状況をプロファイルしてみたところ、around_filter を使った場合は String のインスタンス数が一気に膨れ上がった後、一向に解放されないことがわかった。(before_filter を使った場合は、リクエスト処理完了後、それなりに解放された。)

次に、この String の正体を見るために、上記の MemoryProfiler の :string_debug オプションを使って ObjectSpace 中の String をダンプしてみたところ、session_id などセッションデータと思しき文字列が大量に入っていた。リクエスト終了後は、CgiRequest や CGI::Session などのリクエストやセッションに絡むオブジェクトはすぐに解放されていた事から、何らかの原因でセッション内文字列のみ GC 対象にならない状態になっていると思われる。

さらに、上記の String がどの時点で生成されているのかを追ってみた。

actionpack-2.0.2/lib/action_controller/cgi_process.rb L.150-

private
# Delete an old session if it exists then create a new one.
def new_session
if @session_options == false
Hash.new
else
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
end
end

どうも上記の CgiRequest#new_session の CGI::Session.new で確保されているようだ。何が原因なのか追いたいが、CGI::Session は ActionPack で拡張されており、どうも切り分けが難しい。いろいろ書き換えながら試してみたところ、オリジナルの CGI::Session 中:

lib/ruby/1.8/cgi/session.rb L.299

ObjectSpace::define_finalizer(self, Session::callback(@dbprot))

ここのファイナライザ定義があることで、around_filter でのみリークするようだ(これを試しに無効にすると、リークしない。謎)。かといって、こんなところを無効にするわけにはいかず.. と、今度は around_filter と before_filter とで、何が変わってきているのか、を追ってみた。

actionpack-2.0.2/lib/action_controller/filters.rb L.709-

def run_before_filters(chain, index, nesting)
while chain[index]
filter, index = skip_excluded_filters(chain, index)
break unless filter # end of call chain reached
case filter.type
when :before
filter.run(self)  # invoke before filter
index = index.next
break if @before_filter_chain_aborted
when :around
yielded = false
filter.call(self) do
yielded = true
# all remaining before and around filters will be run in this call
index = call_filters(chain, index.next, nesting.next)
end
halt_filter_chain(filter, :did_not_yield) unless yielded
break
else
break  # no before or around filters left
end
end
index
end

ソースを追う限り、filter 処理は適用順序に並べた配列形式に変換された後、before_filter も around_filter も上記の ActionController::Filters::InstanceMethods#run_before_filters で実際のフィルタ処理がおこなわれているように見える。で、around_filter では、上記の filter.call(self) へ渡しているブロックで index = call_filters している箇所があるが、いろいろ切り分けていってみるに、どうもここが怪しい。around_filter に入れ子になっている filter を再帰的に処理しているようだが、ここの中で CGI::Session.new するとセッション中の String のみが解放されない状態になってしまう.. ということだろうか。

もうひといきな気がするんだが、ちょっとツラくなってきたので、とりあえずここまでで POST しておく。Ruby の GC の問題なのか、Rails の黒魔術のせいなのか、まだ微妙なところだ。とりあえず before_filter にしとけば問題は起こらないのだが、多数のフィルタ処理をサイトの全域で使うようなサービスの場合、個々のコントローラには around_filter をひとつ書いておいて、その filter 内に多数のフィルタメソッドを並べる、というのがラクチンだとおもう。うーむ、悔しいが before_filter で回避してしまおうか.. フィルタ内で、セッション検査をおこなった上で、不正セッションであれば reset_session して仕切り直す、みたいなのって、わりとあるような気がするんだけどなあ.. ハマってる人が見当たらない。

Continue reading »

Tagged with: