<![CDATA[深追い Fukaoi.org]]>http://github.com/dylan/node-rssNodeJS RSS ModuleMon, 21 May 2012 00:52:37 GMT<![CDATA[amazon]]>amazon content

amazon

]]>
http://blog.fukaoi.org/2012/05/17/amazonhttp://blog.fukaoi.org/2012/05/17/amazonThu, 17 May 2012 12:10:37 GMT
<![CDATA[DotCloudをちょっと便利に使うツールをCoffeeScriptでつくってみた]]> CoffeescriptでDotCloudをハードに利用する上での便利ツールを作ってみました。最近の人気ではHerokuに押されている感がありますが多種多様な環境を扱えるという意味ではdotcloudも、またまだ捨てがたいです。無料アカウント(Free plan)では、2つのサービス(exp. Web、DB)を作成できます。ですが、

無料だから限界も・・・

下記画像はDotCloudの僕のアカウントページから抜粋したものです。Usageを見るとわかるようにグリーンのラインが一杯になっていて、上限に達しています。このときはnode.js、mongodbをサービスとして登録しました。アプリケーションを作成して運用しはじめると、他の言語、DBを追加したいと思っても、無料アカウントではできません(すでに、2サービスを利用しているので)。無料でつかっているくせに文句はあまり言うなという声もありますが。。。

dotcloud

アイデアで欲求を満たす

それを解決するには基本的には、お金を払って有料アカウントにアップグレードするしかないです。貧乏なので極力お金をかけたくないので考えた結果、無料アカウントを複数作成して複数の実行環境を用意することになります(DotCloudの規約的にOKかどうかは調べていないので、怒られるまで利用させてもらおうかと)。実際にどんな感じかというと、

dotcloud

とうぜん問題点も

このように4つの環境でアプリケーションや、実験テストなどを行っており、DotCloudにはすごく感謝しています(本心から)。ところがアカウント数を複数管理しはじめて、3つ以上アカウントが増えたときに問題点もでてきました。dotcloudのアカウントを別のアカウントに切り替える毎に$HOME/.dotcloud/dotcloud.confのなかにJSON形式で記述されている、apikeyの値を都度書き換えなければなりません。

dotcloud

図で書くとこんな感じなのですが、最初は、エディタで、dotcloud.confを開いて書き換えてましたが、すごく面倒だし、apikeyの値を間違え、DotCloudサーバーにアクセスできないエラーが発生することもしばしばありました。4つの環境を利用できるが、この方法は現実的ではないです。そこで自分が楽にするために、1台のPC上で複数のアカウントを楽に切り替えられる(apikeyを裏側で書き換える)ツール「dotSwitch」をCoffeeScript、node.jsで作成してみました。もし同じような悩みを持たれいるかたはつかってみたください!(大勢いるとは思えませんが・・・)ちょっと便利になり、DotCloudがより好きになるかもしれません!

そこでdotSwitchで小さな幸せを感じてみる

確認した動作環境
  • node.js:v0.4.12 、v0.6.8、v0.6.11
  • npm:1.1.0-2、1.0.105、1.1.1
  • CoffeeScript:1.2.0
  • その他の依存モジュール名:commander
インストール方法
  1. GitHubよりソースを落としてくる
  2. 
    
  3. coffeeコマンドの指定
    • 方法1:coffeeコマンドをPATHにセットする
    • $bash export PATH=dotSwitch/node_modules/coffee-script/bin/coffee
          
    • 方法2:coffeeコマンドを直接指定する
    • dotSwitch/node_modules/coffee-script/bin/coffee dotSwitch.coffee	
          
動作概要

簡単に動作内容を説明すると、dotcloudコマンドがインストールされたPCのホームディレクトり配下にアカウント名のフォルダを作成しその中に、紐づくdotcloud.confを格納していきます。それで、使用するアカウントディレクトリのdotcloud.confから、本来おかれるべき、dotcloud.confの場所にシンボリックリンクを張り、そのシンボリックを張り替えることで、複数のアカウントを管理しています。

  • 機能1:アカウントディレクトリの作成
  • これは一番最初にやる作業で$HOME/.dotcloud配下にアカウント名のディレクトリを作成して、シンボリックを作成します。アカウント名に使えるワードは[a-zA-Z0-9._-]となります。

    [コマンド]

    dotSwitch.coffee -c "アカウント名"

    [出力内容]

    Enter dotcloud api key:xxxxxxxxxxxxxxxxxxxxxxxx
    
    OK api key.
    Add alias of dotcloud file
    

    appleの箇所に管理しやすい命名(DotCloudのアカウント名、Applications名などがおすすめ)を引数として渡して下さい。プロンプトでapikeyの入力をもとめてくるので、dotcloudコマンドで作成したapikeyをはりつける。OK api key. Add alias of dotcloud fileが出力されればアカウントディレクトリの作成成功。ls $HOME/.dotcloudしてみてください、新規に作成されたディレクトリから、dotcloud.confにシンボリックリンクが張られているのが確認できます。

  • 機能2:アカウントを切り替える
  • アカウントディレクトリを作成したら、よく使うのがこの機能です。ここの例では、appleアカウントからorangeアカウントに切り替えています

    [コマンド]

    dotSwitch.coffee -s "アカウント名"

    [出力内容]

    orange switch ok
    
    ↑※orangeというアカウント名に切り替えた場合の出力
    
  • 機能3:現在セットされているアカウント名を調べる
  • 複数のアカウントを持っていると今どのアカウントがアクティブになっているから、忘れてしまいがちなので、現在セットされているアカウント名を調べるときに使います

    [コマンド]

    dotSwitch.coffee -n

    [出力内容]

    Now used in: /home/fukaoi/.dotcloud/orange
    
  • 機能4:不要なアカウントを削除する
  • 作成機能があるので、当然削除機能もあります。不要になったアカウントをこの機能を使ってまるごと削除します。(※ただしカレントアカウントとして指定されている場合は、一旦別のアカウントに切り替えないと削除はできないです。dotcloud.confも削除しますので、シンボリックされているdotcloud.conf先がなくなってしまうと都合がわるいので)

    [コマンド]

    dotSwitch.coffee -d ”アカウント名”

    [出力内容]

    Delete alias orange of directory
    
    ↑※orangeというアカウント名削除した場合の出力
    
  • 機能5:管理してあるすべてのアカウントのリスト出力する
  • アカウント数を多くあると、アカウント名をいちいち覚えてられないので、リスト出力して確認できます

    [コマンド]

    dotSwitch.coffee -l

    [出力内容]

    apple
    banana
    orange
    pine
    
  • 機能6:カレントアカウントを出力する
  • 現在セットされいるカレントアカウント名を出力します

    [コマンド]

    dotSwitch.coffee -n

    [出力内容]

    Now used in: apple
    
    ↑※appleというアカウントが現在カレントとしてセットされている場合
    
  • 機能7:dotSwitchのバージョンを出力する
  • 正直どうでもいいオプションです、、、commanderというプロンプトモジュールを使っているのですが、デフォルト機能として存在していたのでそのまま利用しています

    [コマンド]

    dotSwitch.coffee -V

    [出力内容]

    0.1.0
    
  • 機能8:ヘルプ機能でオプション一覧の出力
  • これもコマンドツールならついてくるオプションを忘れたときのヘルプ機能です

    [コマンド]

    dotSwitch.coffee -h

    [出力内容]

      Usage: dotSwitch.coffee [option] [alias name] or [option]
    
      Options:
    
        -h, --help     output usage information
        -V, --version  output the version number
        -s, --switch   change account of dotcloud file
        -c, --create   create a new account or add account
        -d, --delete   delete a account of directory
        -l, --list     show all account(option only)
        -n, --now      alias in use(option only)
    


テストプログラムなど、作成していないのでレガシーコードですので、上手くうごかなったらごめんなさい。ソースをGitHubに上げてあります、興味のあるかたはどうぞ。

]]>
http://blog.fukaoi.org/2012/05/11/dotswitchhttp://blog.fukaoi.org/2012/05/11/dotswitchFri, 11 May 2012 04:29:30 GMT
<![CDATA[PHPの非同期クエリで並行処理をやってみる]]> node.jsやnginxに代表されるように世間では非同期処理がアツイです(たぶん)。だけどPHPはそんな非同期の話題に上る事もなく、いまや過去の人のような扱いです。かわいそうすぎる・・・PHPでも非同期処理的な事ができないかなーと思ったのが調査するに至ったきっかけでした。TwitterでPHP界隈の人たちに教えてもらった、EioやLibevnetといったライブラリを見つけたのですが、segmentation faultを吐いて安定していなかったり、動作が求めている感じではなかったです。そもそもnode.jsやnginxのように非同期を前提にデザインされているアーキテクチャではないので、無理な発想なわけで半ばあきらめムードでした。そのときに、今はなきSun Microsystems社のmysqlndの記事(2008年と古め)の中にPHPによるMySQL非同期APIの紹介(New asynchrobnous API)という内容を発見しました。 このSun社の記事によると、PHPからMySQLにクエリを投げる際に非同期クエリを発行できるようです。PHP全体の処理を非同期にできるわけではないのですが、久々に心ときめきました。ではさっそくサンプルプログラムを作成してみようと思ったのですが、その前にそもそもSun社の記事の主題であるmysqlndとは何なんでしょうかね???正直、名前位は聞いたことがあったのですが、詳しくは知りませんでした。調べてみて分かったことなのですが、非同期APIはmysqlndに含まれているよる関数とのことなので、まずはmysqlndについて説明します。

mysqlndとは

  • mysqlndはMySQL Native Driverの略であり、MySQL Client Library (libmysql) の後継にあたるもの

  • PHP 5.3.0 以降の公式ソースにその一部として組み込まれているので、PHP5.3以降をつかっているのなら、ライブラリのインストールは不要。Configure Optionで指定するだけでOK

  • C 言語で書かれた PHP 用拡張モジュールでPHPに特化している

  • MySQL Client LibraryI(Libmysql)はMySQLライセンスだけど、MySQL Native Driver(mysqlnd)はPHPライセンスなので、以前起きてたライセンス問題について気にせず利用できる

  • PHP5.4以降はmysql、mysqli、PDO_MYSQLすべてで裏側でMySQL Native Driver(mysqlnd)が動くようになる

  • パフォーマンス統計用の関数 mysqli_get_cache_stats()、 mysqli_get_client_stats()、 mysqli_get_connection_stats()で詳細なサーバー、クライアント間の接続情報が取得できる。MySQL Client LibraryI(Libmysql)には無い関数

  • MySQL Client LibraryI(Libmysql)に比べて0%-5%高速
  • benchmark

    このベンチーマークの結果図を見ると、微妙な数値ですが、mysqlindのほうが安定して高速なのがみてとれます


  • MySQL Client LibraryI(Libmysql)に比べてメモリの使用率が0%-40%低い
  • Copying zVal

    $row = mysqli_fetch_assocの事例を見て分かるようにMySQL Client LibraryI(Libmysql)では結果値$rowに渡す上で、LibmysqlからzValにコピーしおなさないといけなくて、その余計な処理が無い分、MySQL Native Driver(mysqlnd)のほうがメモリ使用量が少なくてすむということみたいです


      
  • 非同期APIが追加された
  • API

    この3つの関数が非同期クエリを実現してれくれる、新しく追加された(※2008年当時)非同期APIです

     

※ここで掲載した画像について著作物に対する権利はOracle社ならびに、その関連会社に帰属します

というようにmysqlndを使った場合のメリットはたくさんあるので、ガンガン使っていったほうがよさそうです。今まであんまり使っていなかったのがもったいない気分です。mysqlndの話題はこれぐらいで。目的である非同期クエリをどうやって実装するのか?

非同期クエリの実装

上記に登場したmysqli_query()、mysqli_poll()、mysqli_reap_async_query()という3つの関数をPHPの公式マニュアルから調べてみたとこ、mysqli_query()に関しては、引数にMYSQLI_ASYNCという定数を設定する必要があるというのは、分かりました。ところが、残りの2つの関数については[警告]この関数は、 現在のところ詳細な情報はありません。引数のリストのみが 記述されています。。。。と冷たい感じの説明だけで詳細な情報を入手できませんでした。結局たよりになるのが、Sun社の資料で、そこに簡単なサンプルがあったので、それをベースにコードを書いてみました

非同期サンプルプログラム
 <?php

      $start = microtime(true);
      $m1 = mysqli_connect('localhost', 'test', '', 'test');
      $m2 = mysqli_connect('localhost', 'test', '', 'test');
      
      mysqli_query($m1, 'SELECT SLEEP(10)', MYSQLI_ASYNC);
      mysqli_query($m2, 'SELECT SLEEP(2)', MYSQLI_ASYNC);
      
      printf("Query   : %2.2fs\n", microtime(true) - $stabrt);
      
      $links = $error = $reject = array();
      while ($processed < 2) {
        $links = array($m1, $m2);
        
        if (mysqli_poll($links, $error, $reject, 0, 50000)) {
           foreach ($links as $k => $link) {
             
             if ($res = mysqli_reap_async_query($link)) {
               mysqli_free_result($res);
               printf("Fetch %d : %2.2fs\n", ++$processed,
                   microtime(true) - $start);
              }
           }
        }
      }
      printf("End    : %2.2fs\n", microtime(true) - $start);

10秒、2秒とSLEEPするサンプルプログラムです。論より証拠ということで、とりあえず実行してみます



[実行結果]

   Query   :  0.01s
   Fetch 1 :  2.02s
   Fetch 2 :10.02s
   End      :10.02s   
 

こんな結果が返ってきました。当たり前というか、いまいちピンとこないですよね?やはり比較する上で、同期クエリのサンプルプログラムも実行してみたほうがよさそうです。

同期サンプルプログラム
   <?php

         $start = microtime(true);
         $m = mysqli_connect('localhost', 'test', '', 'test');
         
         printf("Query   : %2.2fs\n", microtime(true) - $start);

         $result1 = mysqli_query($m, 'SELECT SLEEP(10)');
         if ($result1) {
           printf("Fetch 1 : %2.2fs\n", microtime(true) - $start);
         }

         $result2 = mysqli_query($m, 'SELECT SLEEP(2)');
         if ($result2) {
           printf("Fetch 2 : %2.2fs\n", microtime(true) - $start);
         }
         
         printf("End    : %2.2fs\n", microtime(true) - $start);
 

なんのへんてつもない、普通のクエリ文を実行するサンプルプログラムです

[実行結果]

     Query    : 0.01s
     Fetch 1 :10.03s
     Fetch 2 :12.05s
     End      :12.05s 

おおおっと、ここで結果値に違いがでました。非同期サンプルプログラム(End : 10.02s)と同期サンプルプログラム(End : 12.05s)でEndの時間を比較すると、非同期サンプルプログラムのほうが約2秒ほど処理が早くおわっています。これはまさしく、非同期クエリによって並行処理を行った結果です。非同期サンプルプログラムのほうは"SELECT SLEEP(2)"と"SELECT SLEEP(10)"が並行で実行されているので、互いに処理待ちが発生していません。なのでFetch1、Fetch 2の値が純粋にSLEEPしていた時間とほぼ同じ値です。同期サンプルプログラムは"SELECT SLEEP(10)"の次に"SELECT SLEEP(2)"と順列にしか処理ができないので、"SELECT SLEEP(2)"には処理待ちが発生して、処理時間が長くなっているのです。可視化したほうがわかりやすいので2つのサンプルプログラムの動作を画像で表現してみました

処理の可視化

async

非同期クエリだと並行でクエリを実行できるので処理時間が短いクエリから結果がかえってくる。非同期サンプルプログラムのFetch 1の値を見てもらえればわかるように、(Fetch 1 : 2.02s)なので、"SELECT SLEEP(2)"が先に処理が終わっている



sync

非同期クエリを使っていない、大方のPHPプログラムでは、こんな感じになっているんだと思う

非同期サンプルプログラムのポイントの説明

  $m1 = mysqli_connect('localhost', 'test', '', 'test');
  $m2 = mysqli_connect('localhost', 'test', '', 'test');

コネクションリソースの生成ですが、並行で走らせるクエリ数と同数が必要になります。100個並行でクエリを走らせようと思うと、100個のコネクションリースを消費します。そんなことする人はいないと思いますが。。。



   mysqli_query($m1, 'SELECT SLEEP(10)', MYSQLI_ASYNC);
   mysqli_query($m2, 'SELECT SLEEP(2)', MYSQLI_ASYNC);

MYSQLI_ASYNCを指定して、クエリの非同期宣言を行います。つけ忘れると、mysqli_poll()の箇所でWarning吐きまくりで、非同期にはなりません



   mysqli_poll($links, $error, $reject, 0, 50000)

非同期クエリの肝となる箇所です。名前のとおり、pollingを行いながら、クエリの結果を待っている箇所です。そのためループ処理内(forとかwhileとかで)で実行しなければなりません。引数の内容ですが、下記にピックアップしておきました。第2、第3引数は例のごとくマニュアルで空欄なので目的が分かっていません。とりあえず配列を引数にとることがわかったので、空の配列をセットしてあります。サンプルプログラムでは50000マイクロ秒なので、0,05秒毎にポーリングすることになります

  • 第1引数:※公式マニュアルによると不明(調べたところ、コネクションリーソースの配列をセットする)
  • 第2引数:※公式マニュアルによると不明
  • 第3引数:※公式マニュアルによると不明
  • 第4引数:ポーリングする際の間隔の秒数
  • 第5引数:ポーリングする際の間隔のマイクロ秒数
 

$res = mysqli_reap_async_query($link)

参照系クエリ(SELECT)の場合はmysqli_reap_async_query()という専用の関数を使い結果オブジェクトを取得する必要があります。更新系(INSERT、DELETE、、、)の場合は必要ないです


まとめ

このようにPHPでも標準関数を使うことで、難なく並行処理を実装できる事が分かりました。ネットで調べてもmysqli_poll()を使った非同期クエリを導入した事例というのがほとんどなくて、調査に苦労しました。ですが、この非同期クエリは処理の効率化や、クエリ処理時間の短縮につながるような可能性を秘めていると思いす。引き続き調査を継続していき、可能ならば業務でも投入してみたいです(人柱になる気満々)

]]>
http://blog.fukaoi.org/2012/03/31/php_mysqlihttp://blog.fukaoi.org/2012/03/31/php_mysqliFri, 30 Mar 2012 15:18:36 GMT
<![CDATA[動画アップロードシステムをNode.js、MongoDB、Expressで作ってみた]]> Node.jsの勉強の為にNode.js、MongoDB、Expressで作った「動画アップロードシステム」のソースを晒してみます。もともとは、このサイトVideo Upload With Node.js, Express And Mongodb step-by-stepでソースが公開されていたので、写経のつもりで書き始めたんだけど、写経した段階ではBugってたりして動かなかった。いろいろと修正をして、せっかくので日本語版に置き換え、ちょっとした機能追加をしたのが下記ソースになる。

動画環境

次の環境で確認しています

  • Node.js:v0.4.12、 v0.6.8
  • MongoDB:v1.4.4

修正箇所

  • 原文ではcssのテンプレートエンジンstylusを使うようになっていたが、通常のCSSでコーディングした
  • テンプレートエンジンをejsにするための指定
  • style.cssのデザイン変更
  • 日本語へのローカライズ
  • RegProviderのrequire方式の変更
  • app.post('/videos')でのネスト構造が深かったので、リファクタリング
  • app.post('/videos')でそもそもPOSTデータを受け取れなかったので、正しい形式に修正
  • 動画アップロード時のプログレス状況を、req.form.on('progress')で取得できるようにした
  • その他細かいBug修正など

動画アップロードシステムのソースコード

app.js(Webのエントリポイント)
    /**
     * Module dependencies.
     */
    var express = require('express'),
        form = require('connect-form'),
        mongoose = require('mongoose'),
        util = require('util'),
        sys = require('sys'),
        fs = require('fs'),
        routes = require('./routes'),
        ejs = require('ejs'),
        Post;

    var app = module.exports = express.createServer(
            form({keepExtensions: true}));

    //====== Configuration =======//
    app.configure(function() {
        app.set('views', __dirname + '/views');
        app.set('view engine', 'ejs');
        app.use(express.bodyParser());
        app.use(express.logger());
        app.use(express.methodOverride());
        app.use(app.router);
        app.use(express.static(__dirname + '/public'));
        app.use(express.errorHandler({
            dumpExceptions: true,
            showStack: true
        }));
    });

    app.configure('development', function() {
        app.use(express.logger());
        app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
    });

    app.configure('production', function() {
        app.use(express.errorHandler());
    });

    //====== Mongoose object =======//
    var RegProvider = require('./regprovider').RegProvider;

    //====== Index page =======//
    app.get('/', function(req, res) {
        RegProvider.findAll(function(req, posts) {
            res.render('index',
                {locals: {title: 'Mongo Node.js 動画アップロード',
                    posts: posts}});
        })
    });

    //====== Create video file =======//
    app.get('/videos/new', function(req, res) {
        res.render('reg_new',
            {locals: {title: '新規アップロード'}}
        );
    });

    //====== Update video =======//
    app.post('/videos', function(req, res, next) {
        req.form.complete(function(err, fields, files) {
            console.log('here i go');
            if (err) return next(err);

            ins = fs.createReadStream(files.file.path);
            console.log('filename:' + files.file.filename);
            ous = fs.createWriteStream(__dirname + '/public/uploads/videos/' + files.file.filename);
            util.pump(ins, ous, function(err) {
                if (err) return next(new Error(err));

                RegProvider.save({
                    filename: fields.filetitle,
                    file: files.file.filename
                }, function(error, docs) {
                    res.redirect('/')
                });
            });
        });

        req.form.on('progress', function(bytesReceived, bytesExpected) {
            var percent = (bytesReceived / bytesExpected * 100) | 0;
            console.log('Uploading: ' + percent + '%');
        });
    });



    //====== Show uploaded video =======//
    app.get('/posts/:id', function(req, res) {
        RegProvider.findById(req.param('id'), function(error, post) {
            res.render('reg_show', {
                locals: {
                    title: '詳細ページ',
                    post:post
                }
            });
        });
    });

    app.listen(3000);
    console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
regprovider.js(Mongooseモジュールを使ってMongoDBにアクセス)
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

var Schema = mongoose.Schema,
    ObjectId = Schema.Objectid;

var Post = new Schema({
    filename: String,
    file: String,
    create_at: Date
});

mongoose.model('Post', Post);
var Post = mongoose.model('Post');

RegProvider = function(){};

RegProvider.prototype.findAll = function(callback) {
  Post.find({}, function(err, posts) {
          callback(null, posts)
      });
};

RegProvider.prototype.findById = function(id, callback) {
  Post.findById(id, function(err, posts) {
          callback(null, posts)
      });
};

RegProvider.prototype.save = function(params, callback) {
    var post = new Post(
        {filename: params['filename'],
         file: params['file'],
         create_at: new Date().toLocaleString().toString()});
        post.save(function(err) {
           callback();
        });
};
exports.RegProvider = new RegProvider();
views/layout.ejs(レイアウト)
    <html>
     <head>
      <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css">
     </head>
     <body>
      <h1><%= title %> ページ </h1>
      <h2 class="navi"><a href="/">Home</a></h2>
      <h2 class="navi"><a href="/videos/new">新規アップロード</a></h2>
     <%- body %>
     </body>
    </html>
views/index.ejs(TOPページデザイン)
        <div id="articles">
         <%- partial('reg', posts) %>
        </div>

NodeJS
TOPページイメージ図


views/reg.ejs(TOPページ用ループ表示デザイン)
 <div class="post">
        <div class="columHead">■<%=reg.filename %></div>
        <div class="colum"><a href="/posts/<%=reg.id %>"><%=reg.file %></a></div>
        <div class="colum"><%=reg.create_at %></div>
    </div>

NodeJS
TOPページ用ループイメージ図


views/reg_new.ejs(動画登録ページデザイン)
    <form method="post" action="/videos" enctype="multipart/form-data">
        <div class="post">
            <p>
                <span>ファイル名: </span>
                <input type="filename" name="filetitle">
            </p>
            <p>
            <span>アップロードファイル: </span>
            <input type="file" name="file">
            </p>
            <p>
                <input type="submit" value="Upload">
            </p>
    </div>
    </form>

NodeJS
動画登録ページイメージ図


views/reg_show.ejs(動画再生ページデザイン)
  <div class="post">
        <div class="columHead">■<%=reg.filename %></div>
        <div class="colum"><a href="/posts/<%=reg.id %>"><%=reg.file %></a></div>
        <div class="colum"><%=reg.create_at %></div>
    </div>

NodeJS
動画再生ページイメージ図


public/stylesheets/style.css(CSS)
 body {
    padding: 50px;
    font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}

a {
    color: #00b7ff;
}

.post {
    float: left;
    width: 800px;
    margin: 20px;
    border-top: dashed 2px;
}

.navi {
    margin: 1em 1em 1em 3em;
    float: left;
}

.navi a {
    color: #808080;
}

.columHead {
    font-size: 17px;
    padding: 5px;
}

.colum {
    padding: 5px;
}
プログレス状況とはこんな感じの出力がコンソールに出力される
Uploading: 0%
Uploading: 0%
Uploading: 1%
  .
  .
  .
Uploading: 99%
Uploading: 99%
Uploading: 99%
Uploading: 100%
here i go
filename:animal.mp4

ソースを追いながら、書き写して動作させるだけなら、3時間ほどでできる、、、と思う。ソースコード一式をGitHubにUPしてあるので、Node.js、MongoDBがインストールされている環境ならすぐに動作すると思うので、ご自由にどうぞ

]]>
http://blog.fukaoi.org/2012/02/01/node-video-uploadhttp://blog.fukaoi.org/2012/02/01/node-video-uploadWed, 01 Feb 2012 12:58:40 GMT
<![CDATA[サーバーサイドJavaScriptで簡単Webアプリ開発 - バッチ処理開発編]]> 「Webフロント開発編(3)」に続き、最後の開発工程になるGoogle Calendar APIをたたいてmemberテーブルとマッチする検索キーワードが存在した場合にメール通知を行うバッチ処理プログラムを開発します

バッチ処理の作成
Google Calendarに接続するための各種jarファイルの取得

ダウンロード&解答して以下のjarファイルを$RINGO_HOME/libにおきます

  • gdata-calendar-2.0.jar
  • gdata-client-1.0.jar
  • gdata-core-1.0.jar
  • guava-r09.jar
http://code.google.com/p/gdata-java-client/downloads/からgdata-src.java-1.45.0.zip 
http://code.google.com/p/guava-libraries/downloads/listからguava-r09.zip 
よりダウンロードして解凍する
バックコマンドプログラムの雛型

バッチ処理を行うbatch.jsの雛型を作成します。RingoJSのルール的にコマンドで動作させるバッチ用のプログラムは、if (require.main === module) { でくくるようです。

[batch.js]

/**
 * メイン処理
 */
var main = function() {
  //ここにメイン処理を記述する
};

/**
 * mainファンクションを呼び出す
 */
if (require.main === module) {
  try {
      main();      
    }
  } catch (error) {
    print(error);
  }
}
MemberオブジェクトにgetBatchInfo()メソッドを作成

status=activeの対象データーをオブジェクトで返します。これも前に実装したようにO/Rマッパを使って実装します

[model.js]

  /**
   * バッチで使用する対象データーをDBから取得する
   */
  getBatchInfo: function() {
    var result = this.loadObject.query()
        .equals('status', 'active').select();
    return result;
  }
mainファンクションの処理フロー

memberテーブルより登録されている勉強会キーワードをgetBatchInfo()を使用して、取得してループ毎に処理したいと思います。mainファンクションに必要なデーターとして、「メールアドレス」、「ユニークID」、「勉強会キーワード」を渡したいと思います

[batch.js]

/**
 * mainファンクションを呼び出す
 */
if (require.main === module) {
  try {
    var results = Member.getBatchInfo();
    for each(var result in results) {
      main(result.email, result.uid, result.keyword);      
    }
  } catch (error) {
    print(error);
  }
}
config.jsに対象となるカレンダーのURLを追記する

IT勉強会のURLをconfig.jsで定義しておきます

[config.js]

exports.calendarUrl = 'http://www.google.com/calendar/feeds/fvijvohm91uifvd9hratehf65k%40group.calendar.google.com/public/basic';
必要なファイルのrequireとインスタンスの生成

GDATA関連のクラスはファイル先頭でインスタンスを生成してしまいます。mainファンクション内で生成しても問題ないと思います

[batch.js]

var {Response} = require('ringo/webapp/response');
var {Member} = require('./model.js'); 
var config = require('./config.js');
var {Service} = require('./service.js');

//// {}くくって、DateTimeオブジェクトをセット
var {DateTime} = com.google.gdata.data;

//// カレンダー検索クエリオブジェクトをセット
var cservice = com.google.gdata.client.calendar.CalendarService("");

//// config.jsに定義されたIT勉強会のURLを取得して、URLオブジェクトでラップ
var eventFeedUrl = java.net.URL(config.calendarUrl);
var query = com.google.gdata.client.calendar.CalendarQuery(eventFeedUrl);
var events = com.google.gdata.data.calendar.CalendarEventFeed().class;
検索対象期間を定義する

とりあえず、現在~3か月後を対象にするため、60日間を定義したいと思います

[config.js]

exports.durationDays = 60;
検索期間をコンバートするメソッドの追加

JavaのSimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")クラスを使って、gdataのフォーマットに指定します。いったんミリ秒にしてからフォーマットかけています

[service.js]

var simpleFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

/**
 * 検索対象期間のDateTimeを取得する
 */
exports.Service.searchTerm = {
  sd: simpleFormat, 
  start: function() {
    return this.sd.format(new Date());
  },
  end: function() {
    var duration = config.durationDays * 24 * 60 * 60 * 1000;
    var now = new Date().getTime();
    return this.sd.format(new Date(now + duration));
  }
};
カレンダーへ検索ワードを投げて、結果取得

ここは特に説明のしようがなくて、GDATAのAPI仕様どおりにパラメーターをセットするだけです

[batch.js]

/**
 * メイン処理
 */
var main = function(email, uid, keyword) {
  var entry = "", content = "", text = "";
  query.setFullTextQuery(keyword);
  query.setMinimumStartTime(
    DateTime.parseDateTime(Service.searchTerm.start()));
  query.setMaximumStartTime(
    DateTime.parseDateTime(Service.searchTerm.end()));

  var resultFeed = cservice.query(query, events);
検索結果が空だった場合

resultFeedのエントリー数を調べて0だった場合は処理を中断します

[batch.js]

  var count = resultFeed.getEntries().size();
  if(count === 0) {return;}
バッチ用メールテンプレート作成

いままで作成してきたメールテンプレートと同じように作成するのですが、今回はループして同じテンプレートを何回もよみだしますので、batch.tplとは別にbatchPart.tplという繰り返し用のテンプレートにわけています

[skins/email/batchPart.tpl]

      
----------------------------------------
タイトル:<% studyTitle %>

<% plainText %>

[skins/email/batch.tpl]

バッチで使用します、メールフォーマットの全体テンプレートになります。textBodyにbatchPart.tplを使って生成されたデータが渡されてきます


IT勉強会サーチメール

このキーワードにマッチした勉強会は以下のとおりです

[<% keyword %>]

<% textBody %>


====================================================
今後、このメールが不要な方は、下記URLをクリックして下さい
<% withdrawUrl %>
====================================================

バッチ用メールフォーマットメソッド追加

skins/email/batchPart.tpl用メソッドになります

[service.js]

   batchPart : function(studyTitle, plainText) {
    return Response.skin(module.resolve
                         ('skins/email/batchPart.tpl'),
                         { studyTitle: studyTitle,
                           plainText: plainText}).body;

skins/email/batch.tpl用メソッドになります。退会用のリンクもメールに添付するため、withdrawというアクションを用意します。withdrawについては、後ほど作成します

[service.js]

  batch : function(uid, keyword, text) {
    return Response.skin(module.resolve
                         ('skins/email/batch.tpl'),
                         { keyword: keyword,
                           withdrawUrl: config.fqdn 
                           + "/withdraw?uid=" + uid,
                           textBody: text}).body;
  },
メール用整形メソッドの追加

GData calendarのSummaryデーターを人間の目でみて読みやすい形に変換します。

[service.js]

  /**
   * GData calendarのSummary的な値を読みやすい形に加工する
   */
  modifySummary : function(content) {
    var CR = "\r\n";
    return content.replace(' JST ', CR)
        .replace('予定のステータス: 確定', '')
        .replace('予定の説明", "詳細内容') + CR;       
  }
};
検索でマッチしたデータをループして、Service.modifySummary()に渡して、メールで読みやすい形式に変換します

resultFeed.getEntries().get(i)でマッチしたentryオブジェクトの中から1個取得します。entry.getTextContent().getContent().getPlainText()で取得した検索結果をテキスト形式にコンバートします。

[batch.js]

for (var i = 0; i < count; i++) {
    entry = resultFeed.getEntries().get(i);
    text += Service.textFormat.batchPart(
        entry.getTitle().getPlainText(),
        Service.modifySummary(entry.getTextContent()
                              .getContent()
                              .getPlainText())
    );
}
メール送信

上記で作成した Service.textFormat.batch()に必要な値を渡してメールを送信します

[batch.js]

  var textFormat = Service.textFormat.batch(uid, keyword, text);
  Service.sendMail(email, textFormat);
退会処理
退会処理メソッドの追加

uidをキーにmemberテーブルにマッチするデータがあれば、statusカラムをactiveからinactiveにupdateします。inactiveになることで次回のバッチ処理の対象外とします。

[model.js]

  /**
   * 退会処理
   */
  withdraw: function(uid) {
    var result = this.loadObject.query()
        .equals('uid', uid).select();
    result[0].status = 'inactive';
    result[0].save();  
  },
withdrawアクションの追加

Member.withdraw()をよびだしてステータスをアップデートし、メールを送信するだけのシンプルな処理になります。特に退会用のhtmlテンプレートは用意せず、登録完了時に使用した、skins/complete.htmlを再利用し、退会用メッセージを引数に渡すだけにしたいと思います。

[action.js]

/**
 * withdraw page
 */
exports.withdraw = {};
exports.withdraw.GET = function(req) {
  var uid = req.params['uid'];
  if (uid === "") {
    return Response.skin(module.resolve('skins/complete.html'), 
    { title: 'エラー',
      errorMessage: 'URLに問題があります'});
  }

 Member.withdraw(uid);
  return Response.skin(module.resolve('skins/complete.html'), {
    title: '退会が完了しました',
    headMessage: '今後、IT 勉強会サーチメールは送信されなくなります'
  });

退会処理ページが表示されます

簡単Webアプリ開発完了

以上がバッチ処理機能の開発になります。簡略化された処理もありますが、「サーバーサイドJavaScriptで簡単Webアプリ開発」ひととりやりたいことは網羅できたと思いますので、完了としたいと思います。このブログで使用したソースコード一式はGitHubにUPしてありますので、もしよろしければ見てみてください。

]]>
http://blog.fukaoi.org/2011/06/17/server-side-javascript-batchhttp://blog.fukaoi.org/2011/06/17/server-side-javascript-batchFri, 17 Jun 2011 12:54:13 GMT
<![CDATA[サーバーサイドJavaScriptで簡単Webアプリ開発 - Webフロント開発編(3)]]> 「Webフロント開発編(2)」に続き、本編ではメールに添付された登録完了(complete)機能の開発を行います。メール内にリンクされたURLが登録完了処理のURLになります。リンクをクリックして、GETでuidを取得して会員ステータスを保留(waiting)から有効(active)に変更します

登録完了処理の作成(http://localhost:8080/completeの処理)

action.jsにcompleteメソッドを追加する。GETプロパティを追加する

[action.js]

/**
 * complete page
 */
exports.complete = {};
exports.complete.GET = function(req) {

  //処理を記述する


リクエストパラメータのチェック

リクエストオブジェクトからuidの存在の有無をチェックします。ここもチェック処理自体は最小限にとめておきます。

[action.js]

  var uid = req.params['uid'];
  if (uid === "") {
    return Response.skin(module.resolve('skins/complete.html'), {
      title: 'エラー',
      errorMessage: 'URLに問題があります'});
  }
memberテーブルのstatusをactiveに更新

GETで渡ってきたuidにマッチするuidのレコードをmemberテーブルよりselect()メソッドでひっぱってきて、'active'という文字列をセットしてsave()メソッドで保存します。selectした結果オブジェクトを使って、save()メソッドを実行すると、データーの更新が可能になります

[model.js]

  /**
   * 登録完了処理
   */
  complete: function(uid) {
    var result = this.loadObject.query()
        .equals('uid', uid).select();
    result[0].status = 'active';
    result[0].save();  
  },
登録完了HTMLテンプレート作成

messageを表示するだけのシンプルなものを用意します

[skins/email/complete.html]

<% extends ./base.html %>

Member.complete()メソッドを呼び出す

特に目新しい処理はなく、上記で作成した、Member.complete()メソッド、complete.htmlテンプレートをセットします。

[action.js]

  Member.complete(uid);
  return Response.skin(module.resolve('skins/complete.html'), {
    title: '登録が完了しました',
    headMessage: '今後、検索ワードにマッチした勉強会をメールでお知らせ致します'
  });
};
メールに記述されたリンクをクリック

「Webフロント開発編(2)」で飛ばしたメールのリンクをクリックしてみると内部でaction.jsのcompleteメソッドが呼びだされ、memberテーブルの更新、登録完了ページの表示を行います。更新後の処理は以下の画像で確認できます

メールに記述されている内容のピックアップ
[登録完了URL]
http://localhost:8080/complete?uid=cb18972392a103ae3903a1b70692613b

[uid]
cb18972392a103ae3903a1b70692613b

登録完了ページが表示され、メッセージが表示されます

以上が登録完了機能の開発になります。次編からバッチ処理の開発に入ります。



次のステップへ
バッチ処理開発編へ
]]>
http://blog.fukaoi.org/2011/06/16/server-side-javascript-webdev3http://blog.fukaoi.org/2011/06/16/server-side-javascript-webdev3Thu, 16 Jun 2011 12:42:56 GMT
<![CDATA[node.JS VS RingoJS 比較サイトまとめ]]> node.JSとRingoJSという2つのサーバーサイドJavaScriptが有名(node.JSの方が遥かに知名度は上)ですが、「どっちを使うべきなのか?」という話をtwitterで見かけました。実際、周りでも同じような意見を聞きます。僕自身は、RingoJSについて「サーバーサイドJavaScriptで簡単Webアプリ開発」でも書き始めたように、最近ちょくちょく触りだしているのでなじみがあります。node.JSについては、「hello world」どまりで、比較しようにも比較できない状況でした。そこでネットで調べてた所、海外のブログサイトでnode.JSとRingoJSの比較内容が書かれたブログが2つあったので、自分なりに訳してまとめてみました。

参考にしたブログ

どちらも2010年7月、9月に書かれているので、内容は若干古くとなっているところもあると思います。(特にnode.JSに関してはupdateスピードが早いので)

ブログ名から察するにnode.JSラブな人が書いているんだと思いますが、node.JSだけに偏った意見ではなく、RingoJSについてもわりと公平に比較されていたと思います。


こちらはブログ件名とは異なり、RingoJSの長所を中心に書かれおり、比較はあまりされていませんでした。そのせいか、コメント欄では、色々と反論されており、その投稿した人の反論からnode.JSのメリットをひっぱりあげました

まとめた比較情報の多くはnodejsbotを参考にさせてもらいました

node.JSについて

短所
  • C++のアドオンをコンパイルするのに管理者権限が必要になる

    レンタルサーバとかで一般ユーザ権限しか貸与されてないとコンパイルできない可能性があるということだと思います

  • Windows上で動作させるには注意が必要である

    たぶんブログが書かれた当時はnode.JSのWindows対応がいまいちだったのではないかと推測しました。LinuxとWindowsにnode.jsをインストールしてみたによると最新のnode.JSとWindows7だと、簡単にインストールできるみたいです。なので以前の話かと思います

  • ノンブロッキングシステム(非同期)のコードは、スレッド、同期ベースのコードを書いてきた開発者にとって、制御のフローはなじみが薄い

    JavaScriptをやり始めたころ、コールバックという処理に違和感を感じとことがあります。まぁこれも慣れれば便利なものに

  • 過去の遺産からなる、様々なモジュールが存在していない

    RingoJSでいう、Javaでできたライブラリの存在をさしていると思います。当然、V8エンジンという新しいミドルゥエアであるため 、専用モジュール以外は存在していないですね

  • バージョンUPが速いので、利用者は古いバージョンに取り残されていくかもしれない

    0.0.1がリリースされてから約10カ月の間に、0.4まであがっているのですごい勢いで更新されていますね。これはまだBetaバージョンに近い位置づけだから、現状はある意味しょうがないのではないでしょうか

長所
  • 非同期APIによってクライアントサイド開発のようにイベント駆動型の開発が可能になる

    短所でもでてきた内容の反対の理由ですね。node.JSの長所中の長所かと

  • シングルスレッドは多くの同期問題を回避できることを意味する

    当たり前ですが、マルチスレッドならではの同期問題は発生しなくなりますよね

  • V8エンジンは高速でAPIは綺麗(google v8.hファイルを見る事で、悩んだ際の答えがほとんどのっている)

    V8エンジンのソースを見た事はないのですが、Googleのスーパーエンジニアが設計コーディングしているのだから、いけている実装なんでしょうね

  • C++のアドオンで、何でもやりたいことは実現できる

    C++ベースのV8環境なので、足りない機能があれば、C++で自分でアドオンを作成すればOKということでしょうね

  • APIはJavaScriptのクロージャを使用しており、楽をすることができる

    クロージャ自身はJavaScriptの言語機能だから、node.JS固有のものでもないと思うのですが・・・

  • ローレベルのAPIは多くのAPIと比べて、簡単に複雑な実装が可能である

    ローレベルAPIがどれをさすのか曖昧なので、よくわかりませんが、、、processやstreamのあたりのことでしょうか

  • イベント駆動型システムはスレッド型モデル(コンテキストの変更、メモリのオーバヘッド、ロッキングがある)と比較してすばらしい

    コンテキストの変更はJ2EEモデルでの開発をさしているかと思います。確かにアプリケーションで修正するたびに、コンテキストの変更が発生するようなモデルだと、発狂しそうです。ちなみに、RingoJSではコンテキストを変更したりする事は基本的に無いです

  • ファイルディスクリプタの入出力は素晴らしい

    たぶん、プロセス間でストリーム通信を行う際のファイルディスクリプタの扱いの効率性を指摘しているんじゃないのかなと思いました。badmathさんのnode.js とは何か (2)の記事を見て頂ければnode.JSの内部アーキテクチャの概要が理解できメリットが分かると思います。

  • メモリの消費量が少ない

    シングルスレッド万歳!

  • コミュティは大きく、非常に活発で、フレンドリーである

    これは意外に重要だと思いました。オープンソースアプリケーションの多くは利用するユーザによって、発展している歴史があるので

RingoJSについて

短所
  • JVM上でJavaScriptを実行しているので、node.JSの実行環境であるC++で書かれたV8エンジンより遅い

    これも当たり前の事実だと思っていましたが、RingoJSとnode.JSでベンチマークをとったブログRingoJS vs. Node.js: Runtime Valuesによると、遅いどころか、node.JSよりパフォーマンスが良い結果もありました。node.JSが2010年9月のスナップショットで比較しているので、最新版だとどういう結果になるかは不明ですね

  • APIでJavaScriptらしいコーディングが使われていない

    node.JSのコードと比べるとコールバックなど使わなくても実装できるので、そうかもしれないです。これはアーキテクチャが非同期モデルとスレッドモデルの違いもあると思います

  • ハイレベルAPIのコードがクライアントJavaScriptの記述と似ていない

    ハイレベルAPIがどこを指すのか分からないのですが、この理由も上記に近いのではないでしょうか

  • アドオンについての情報が記載されていない。

    node.JSではアドオン、RingoJSではパッケージの事を指しているんだと思うのですが、確かにnode.JSみたいにアドオン(パッケージ)の作成方法やルールなどが記載されたページはなかったです。

  • JVMを使用しているので、メモリを大量に消費しやすい

    これはVMの弁慶の泣き所ですね。

長所
  • Javaを使ってアドオンの開発ができる、過去のJavaのライブラリなどを再利用可能

    node.JSの長所でもあったように、RingoJSではベースがJavaなので、Javaを使っての機能拡張が可能です

  • JavaScript1.8の機能を使うことにより、より良いスクリプトがかける

    RingoJSは内部でRhinoエンジンを使用しているので、Mozilla Foundationで開発されているJavaScriptの新機能の恩恵を受ける事ができるということですね

  • シングルプロセスの中で、複数のインスタンスを動作させることができる(V8エンジン上でも同じ事はやろうと思えばできるば、難しいだろう)

    たぶん、マルチスレッドだといいたいんだと思います。マルチスレッド全盛の時代で、いまさら長所ポイントとしては弱い気が・・・

  • 別スレッドで動作しているJavaScriptをsync()関数を使って、ブロック(同期化)することができる

    sync()関数はRhinoエンジンが提供する関数で、Javaでの同期処理より簡単に書けるみたいです

  • JVM上で実行されるので、Google App Engine、自宅のPC、携帯電話などで動作する

    自分もRingoJSを使用しようと思った際の決め手となったポイントです。多くのデバイスで動作する可能性というのは魅力的です

  • データーベースはテスト、サポートされたJavaライブラリを経由して行われる

    実績があるJDBCを使用できることのメリットを言いたいんだと思います

  • デスクトップアプリケーションを作成する場合、GUI、マルチメディア、入出力のJavaライブラリを利用できる

    これも上と同じように豊富なJavaライブラリの恩恵ですね

  • Javaのアドオンは素晴らしく、JVM上で動作するので安全である。JNIを使う事で、C++のライブラリも使うことができる

    RingoJSでもやろとう思えば、なんでもできると思います

  • CommonJSに限りなく準拠している

    node.JSよりRingoJSのほうが、CommonJSに準拠しているそうです

じゃあ、どっちをつかえばいいの?

正直それは僕にもよくわかりません(笑)。短所と長所をまとめていく過程で思った事は、node.JS、RingoJSはサーバーサイドJavaScriptという"枠組み"は同じではありますが、比較する対象としては相違点が多いというか、2つのミドルウェアの前提となる仕様の方向性が大きく違っており、比較対象にすべきではないように思いました。なのでプログラマがnode.JS、RingoJSの好きな方のスタイルを選択すればよいと思います。それでも、もしどちらかを選択しないといけなくなった場合、主観になってしまうですが「少ないリソースで大量のトラフィック(アクセス)をさばく必要がある場合はnode.JS」、「それほどトラフィックやリソースを気にする必要がない普通のWebアプリケーション開発ではRingoJS」という位でしょうか。後は、PaaSサービスやレンタルサーバなどの実行環境の制限も基準になりますよね。今の所、判断基準はこれくらいしか思いつかないです(汗)

]]>
http://blog.fukaoi.org/2011/06/11/nodejs-vs-ringojshttp://blog.fukaoi.org/2011/06/11/nodejs-vs-ringojsSat, 11 Jun 2011 05:59:23 GMT
<![CDATA[サーバーサイドJavaScriptで簡単Webアプリ開発 - Webフロント開発編(2)]]> 「Webフロント開発編(1)」に続き、本編では登録画面からのデーターを処理する仮登録(confirm)機能の開発を行います。この開発では、ビジネスロジック、DBのエンティティ周りを別のプログラムファイル(service.js、model.js)に記述します。action.js内にすべての処理をまとめると可読性、保守性、再利用性が悪くなったりしますので。そのため「Webフロント開発編(1)」に比べ、作成するプログラムファイル、ソースコード数も多くなります。

テーブルスキーマ作成

登録ページから渡ってきたリクエストデータを保存するための、テーブルスキーマを設計します。Webアプリに登録するデーターを管理するということで、テーブル名はmemberにします

CREATE TABLE `member` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `email` char(40) NOT NULL 
        COMMENT 'メールアドレス', 
  `status` enum('waiting','active','inactive') NOT NULL 
        COMMENT '会員ステータス',
  `keyword` text NOT NULL 
        COMMENT '勉強会検索キーワード',
  `uid` char(40) NOT NULL 
        COMMENT 'email + keywordでユニーク性を持たせるためのID',
  `timestamp` timestamp NOT NULL DEFAULT 
        CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
※statusカラムの状態の説明
  • waiting・・・仮登録状態のユーザ
  • active・・・登録が正常完了している有効会員
  • inactive・・・本サービスを退会、または無効となった会員
ビジネスロジックを担当するservice.jsを作成

POSTで渡ってくるリクエストデータの中から、email(Eメールアドレス)、keyword(勉強会を探すためのキーワード)をオブジェクトのプロパティにセットするため、下記のように定義します。trim()メソッドは入力時に全角半角スペースがkeywordの前後に含まれていた場合のトリミングするためのメソッドです。commpaReplace()メソッドは入力されたkeywordが複数あった場合に、AND検索にするため、Google Calendar APIに値を投げる前にカンマ区切りにデータを変更するためのメソッドです。

[service.js]

/**
 * action.js、batch.jsのビジネスロジックの
 * 定義 
 */

var config = require('./config.js');

exports.Service = {
  email: "",
  keyword: "",

  /**
   * トリミング(全角、半角のスペース削除)
   */
  trim : function(key) {
    return key.replace(/^\s+|\s+$/g, '');
  },

  /**
   * カンマ(,)でreplace
   */
   commaReplace: function(key) {
    return key.replace(/\s+/g, ',');
  }

};
DB Entityを担当するmodel.jsを作成

model.jsではmemberテーブルを操作するDB関係のメソッドを定義していきます。コーディング前に次の依存ライブラリが必要になりますのでダウンロードしてセットアップしてください

MySQLに接続するためのJava用JDBCドライバー

Javaのjarファイルは$RINGO_HOME/libに置くことでRingoJS内で呼び出すことができます。新しくjarファイルを追加する場合は忘れずにRingoJSの再起動を行っておきます

http://www-jp.mysql.com/downloads/connector/j/ より 
mysql-connector-java-5.1.16.tar.gzをダウンロードして解凍する

$ cp mysql-connector-java-5.1.16/mysql-connector-java-5.1.16-bin.jar $RINGO_HOME/lib
RingoJS用O/Rマッパライブラリ

RingoJSのパッケージインストラーを使い、ringo-sqlstore、ringo-storableをインストールします。ringo-storableはringo-sqlstoreの依存パッケージです。インストールが完了しますと$RINGO_HOME/packages/に置かれる

$ $RINGO_HOME/bin/ringo-admin install oberhamsi/ringo-sqlstore
$ $RINGO_HOME/bin/ringo-admin install hns/ringo-storable 

MAPPING_MEMBER.propertiesでテーブルカラム情報を定義しています。Member.loadObjectプロパティを実行することでO/Rマッピングが行われます

[model.js]

/*
 * Databaseを操作する
 */

var config = require('./config.js');
var Store = require("ringo/storage/sql/store").Store;

/**
 * MySQLの接続情報
 */
var store = new Store({
  "url": config.db.url,
  "driver": config.db.driver,
  "username": config.db.username,
  "password": config.db.password
});

/**
 * Member Entity defined
 */
var MAPPING_MEMBER = {
  "table": "member",
  "properties": {
    "email": {"type": "string", "column": "email", "nullable": false},
    "status": {"type": "string", "column": "status", "nullable": false},
    "keyword": {"type": "string", "column": "keyword","nullable": false},
    "uid": {"type": "string", "column": "uid", "nullable": false}
  }    
};

/**
 * Memberオブジェクトの生成、各種メソッド定義
 */
exports.Member = {
  loadObject: store.defineEntity("Member", MAPPING_MEMBER),
};

config.jsにMySQLの接続情報を追加します

[config.js]

exports.db = {
  url: 'jdbc:mysql://localhost/test',
  driver: 'com.mysql.jdbc.Driver',
  username: 'hogehoge',
  password: 'fugafuga'
};
登録確認処理の作成(http://localhost:8080/confirmの処理)

action.jsにPOSTで値を受け取れる関数を作成するために、index処理の下にconfirmという名前でスケルトンを作成します。リクエストをGETで受けるときは、exports.confirm.GETと変更します

[action.js]

/**
 * confirm page (※POST Only)
 */
exports.confirm = {};
exports.confirm.POST = function (req) {

  //処理を記述する

}

exports.confirm = {};でオブジェクトを生成しています。この初期化がないと、以下のようなエラーメッセージがでて怒られます。

TypeError: Cannot set property "POST" of undefined to 
"org.mozilla.javascript.gen.actions_js_97@252a17" (actions.js#19)
model.js、service.jsのrequire

作成した、model.jsとservice.jsをコールすために、以下のrequire文をaction.jsの先頭に追加します。{}でくくることによって、model.js、service.jsの中にある指定したオブジェクトだけを呼び出すことができます。今回のmodel.js、service.jsともに1ファイルに1オブジェクトしかないので、あまりメリットが感じませんが、1ファイルに複数のオブジェクトが存在する場合は、使用しないオブジェクトはCallされないので、メモリ消費を抑えるメリットがでてくるんだと思います。

[action.js]

var {Member} = require('./model.js'); 
var {Service} = require('./service.js');
リクエストパラメータのチェック

action.jsに次のコードを追加します。Serviceオブジェクトのプロパティ(email, keyword)にリクエストパラメーターをセットします。その下で値が空かどうかのチェックを行っています。どちらかが空であった場合は、skins/index.htmlを呼び出し、テンプレートファイルのerrorMessage変数が赤色で出力されます。

本格的にサービスを運営する場合は、このチェックだけでは不十分ですので、Validationチェックを行う必要があると思います。ここでは時間の都合で最低限の値チェックしか行わないでおきます。

[action.js]

  Service.email = req.params['email'];
  Service.keyword = req.params['keyword'];
  if (Service.email === "" || Service.keyword === "") {
    return Response.skin(module.resolve('skins/index.html'), {
      title: 'エラー',
      errorMessage: '入力した値に問題があります、再度入力してください'
    });
  }

email、keywordを空にして、subimitを行い、エラー画面を表示してみます。赤丸で囲ったtitleの個所が「エラー」という文字に切り替わっており、errorMessageも赤色で「入力した値に問題があります、再度入力してください」表示されているのが確認できました

RingoJS

多重登録防止

多重登録が可能になってしまいますと、悪意ある第3者に登録確認のメールを利用したSpam装置に利用される危険性もあるので、ここでは多重登録防止機能を作成します。仕様的には、memberテーブルのstatus=waitingが同じメールアドレスで存在した場合は、エラーにしたいと思います。

model.jsに次のメソッドを追加します。equals('email',service.email).select()でservice.emailと同じemailアドレスがないかどうか探しにいっています。result変数は配列で中身がかえってきますので、配列が1件以上存在していて、中にwaiting状態であれば、falseとしてエラーにしています

[model.js]

  /**
   * メールアドレスをキーにして多重登録の有無を確認する
   */
  check: function() {
    var result = 
    this.loadObject.query().
        equals('email',service.email).select();

    if (result.length !== 0) {
      for each(res in result) {
        if (res.status === 'waiting') return false;
      }
    }
    return true;
  },

action.jsにMember.check()メソッドを追加してエラー処理を記述します。

[action.js]

  if (Member.check() === false) {
    return Response.skin(module.resolve('skins/index.html'), {
      title: 'エラー',
      errorMessage: '既に申し込み済みのメールが存在しております、' +
                    'お手元のメールを確認の上そちらを先に登録完了ください'
    });    
  }
DBへの仮登録処理

model.jsに次のconfirmメソッドを追加します。new this.loadObject()でオブジェクトを呼び出して、テーブルにinsertする値を代入していきます。keywordの代入では、Service.trim()メソッドで余計な空白を取り除き、複数のキーワードに対しては、カンマ区切りをService.commaReplace()メソッドで行っています。最後にinsertを行う、sqlstoreオブジェクトのsave()メソッドを実行しています。sqlstore自身Transactionも実行可能なのですが、今回は簡略化のため実装を省いています。

[model.js]

  /**
   * 仮登録処理
   */
  confirm: function(uid) {
    var member = new this.loadObject();
    member.email = Service.email;
    member.status = 'waiting';
    member.keyword = Service.commaReplace(Service.trim(Service.keyword));
    member.uid = uid;
    member.save();
  },

service.jsにユニークIDを生成するメソッドを追加します。時間をキーにハッシュ化してユニークIDにしたいと思います。ハッシュ化関数として、RingoJSのコアモジュールにデフォルトでバンドルされているstringsモジュールを使用します。このモジュールは名前からしてわかりますように、文字列を操作するライブラリ群になっており、digest()メソッドも存在しています。内部でjava.security.MessageDigestクラスをラップしており、MessageDigestクラスで対応しているハッシュアルゴリズム(MD5、SHA-256など)が使用可能です。MD5は解読される可能性が高いのでセキュリティ的にはSHA-256などを使用すべきだと思いますが、簡略させるためMD5を使用しています。特にハッシュアルゴリズムを指定しない場合、自動的にMD5になります。

[service.js]

var strings = require('ringo/utils/strings');

  /**
   * ユニークIDの作成
   */ 
  createUniqId: function() {
    return strings.digest(new Date().toString());
  },

action.jsに作成したMember.confirm()メソッドを実装したいと思います。uidを生成して、Member.confirm()メソッドに渡します

[action.js]

  var uid = Service.createUniqId();
  Member.confirm(uid);
メール送信処理

RingoJSのパッケージインストラーを使い、ringo-mailをインストールします。内部でjavax.mailをたたいているパッケージです

$ $RINGO_HOME/bin/ringo-admin install emilis/ringo-mail

service.jsにsendMail()メソッドを追加します。値が変わる箇所としては宛先(To)とメール本文(Body)なので、それは外部から値を受けれるようにしておきます。それ以外の宛先(From)と件名(Subject)は、今回は固定値にするため、config.jsに追加したいと思います

[service.js]

var email = require('ringo/mail');

  /**
   * 確認メールを入力されたメールアドレスに送信する
   */
  sendMail: function(to, body) {
    email.send({from: config.email.from, to: to, 
                subject: config.email.subject, text: body});   
  },
メールテンプレート作成

メールテンプレートもHTMLテンプレートの作成時に使用した、Response.skin()メソッドを使って実装しています。completeUrlはメール本文に出力される、登録完了ページ(次編で開発を行います)のURLになります。ユーザーを特定するためにURLのクエリストリングにuidを付属しDBにあるmemberテーブルのuidカラムとのマッチングを行うために使用します

[action.js]

var {Response} = require('ringo/webapp/response');

/**
 * メールフォーマットの作成
 */
exports.Service.textFormat = {
  confirm: function(uid) {
    var sv = exports.Service;
    return Response.skin(module.resolve
                         ('skins/email/confirm.tpl'),
                         { keyword: sv.commaReplace(sv.trim(sv.keyword)),
                           completeUrl : config.fqdn 
                           + "/complete?uid=" + uid}).body;
  },
};

Response.skin()メソッドはhtmlテンプレートだけでなく、どのようなタイプのテンプレートでも対応できるようになっているので、オリジナルのメール専用のテンプレートを作成して変数をセットしてあげるだけでOKです

 (1)HTMLテンプレートをセットする場合
  return Response.skin(module.resolve
                         ('skins/index.html')
  ↓
 (2)メールテンプレートをセットする場合
  return Response.skin(module.resolve
                         ('skins/email/confirm.tpl')

skins/email/confirm.tplを作成します

[skins/email/confirm.tpl]

<IT 勉強会サーチメール>にお申し込み有り難うございます。

ご入力頂きましたメールアドレスに対して登録確認のための
メールを自動送信しています。

----------------------------------
検索キーワード:<% keyword %>
----------------------------------

ご確認がとれましたら、下記URLにアクセスして登録を
完了してください。
(※本メールをお受け取りになりましてから、3日以内に
登録を完了しませんと無効となります)

config.jsには次の値を追加します

[config.js]

exports.fqdn = 'http://localhost:8080';
exports.email = {
  from: 'it_study_search@fukaoi.org',
  subject: 'IT勉強会サーチメール'
};
actions.jsにメール送信する処理を実装

action.jsにメール送信のためにメールテンプレートから本文を作成して、それをService.sendMail()メソッドの第2引数として渡しています。そして仮登録のメッセージをResponse.skin()メソッドで設定しています。

[action.js]

 Service.sendMail(Service.email, Service.textFormat.confirm(uid));

  return Response.skin(module.resolve('skins/index.html'), {
    title: '受付致しました',
    headMessage: '入力いただいたメールアドレスに登録完了のURLを送信しました'
  });

仮登録完了したページは以下のように、テンプレートは共通のskins/index.htmlですが、赤丸で囲んだ個所で、title、headMessageの文言が上記設定したaction.jsのResponse.skin()メソッド内容に置き換わっていることが確認できます

RingoJS

以上が仮登録機能の開発になります。次編から登録完了ページの開発に入ります。



次のステップへ
Webフロント開発編(3)へ
]]>
http://blog.fukaoi.org/2011/05/30/server-side-javascript-webdev2http://blog.fukaoi.org/2011/05/30/server-side-javascript-webdev2Mon, 30 May 2011 05:54:21 GMT
<![CDATA[サーバーサイドJavaScriptで簡単Webアプリ開発 - Webフロント開発編(1)]]> 「スタート編」に続き、本編ではWebアプリの作成を開始します。今回作成するWebアプリのお題としては、エンジニアならよくお世話になっている、IT勉強会サイトを使わせてもらおうと思います。希望する勉強会のキーワードと通知メールアドレスを、今回作成するWebアプリに登録することによって、IT勉強会サイトに、そのキーワードにマッチする勉強会が登録された場合、登録メールアドレスに対して勉強会の内容を連絡してくれるというアプリです。 勉強会に参加しようと思っても、気になる内容のものがなかったり、気がついたときには人数が多くて参加できなかったりという過去の経験から思いつきました。

概要設計

まずなんといっても、サイト名を決定しないといけません。IT勉強会をサーチするということで、「IT勉強会サーチ」、プロジェクトコードを「it_study_search」にします。 登録を行う側をWebフロント側とし、RingoJSを通して、MySQLのDBにメールアドレスと検索キーワード(探したい勉強会のワード)を登録します。検索キーワードは複数条件を指定でき、AND検索とします。DBに登録されているデータを1日1回Cronでチェックして、Google Calendar APIに対して、検索キーワードを投げてマッチする勉強会が無いかどうか調べ、マッチした勉強会があれば、登録されているユーザのメールアドレスに通知を行います。このCronで行う処理側をバッチ処理側と呼ぶことにします。下記にWebフロント側の画面遷移イメージをおこしたので添付します

(画面遷移イメージ)

flow

RingoJSのコマンドラインで雛型生成

下記コマンドで、it_study_searchプロジェクトのアプリケーション雛型を作成します。ここでは特に説明は必要ないかと思います。スタート編に書いた、公式サイト First Stepsを見てもらえばわかると思います。

$RINGO_HOMEはRingoJSを設置したディレクトリをさします。

$ $RINGO_HOME/bin/ringo-admin create it_study_search 

赤色のファイルが今回中身を実装するファイルとなります。雛形を作成した初期はもっとファイル数は少くなく、今後作成していくプログラムファイルが含まれています

it_study_search/  (Webアプリ プロジェクトディレクトリ)
       | 
       |-- README.txt
       |-- actions.js  (コントローラー プログラムファイル)
       |-- batch.js  (バッチ処理 プログラムファイル)
       |-- config
       |   |-- jetty.xml
       |   `-- log4j.properties
       |-- config.js  (設定ファイル)
       |-- main.js    (アプリケーションのエントリポイント)     
       |-- model.js   (DB Entity プログラムファイル) 
       |-- public
       |   `-- stylesheets
       |       `-- page.css  (共通CSSファイル)
       |-- service.js        (ビジネスロジック プログラムファイル)
       `-- skins
           |-- base.html
           |-- complete.html (登録完了ページのhtmlスキン)
           |-- email
           |   |-- batch.tpl     (バッチ処理全体のmailスキン)
           |   |-- batchPart.tpl (バッチ処理の部分mailスキン)
           |   `-- confirm.tpl   (仮登録ページのmailスキン)
           |-- error.html
           |-- index.html        (申し込み、仮登録ページのhtmlスキン)
           `-- notfound.html

コーディング開始

インデント

好みの問題なので、世間のオーソドックスなルールですとJavaScriptファイルのインデントはスペース4つが主流だと思いますが、ここではスペース2つでコーディングしています。

登録ページの作成(http://localhost:8080/indexのページ)

メールアドレス、勉強会検索キーワードを入力するための、登録ページをまず作成します。action.jsのexports.indexを以下のように変更します。title、headMessageがhtmlテンプレートファイルに渡す変数と値になります。テンプレートファイル側では<% %>で値を取得します。

[action.js]

var {Response} = require('ringo/webapp/response');

/**
 * index page
 */
exports.index = function (req) {
  return Response.skin(module.resolve('skins/index.html'), {
    title: 'IT勉強会サーチ',
    headMessage: '検索したい勉強会キーワードと受け取るメールアドレスを入力してください'
  });
};
テンプレートファイルの修正

つぎに、skins/index.htmlを修正してIT勉強会サーチのためのhtmlに変更します。下記のようにhtmlファイルを修正します

[skins/index.html]

<% extends ./base.html %>

<% subskin content %>
<div id="header"><h1><% title %></h1></div>
<div id="body">
<p id="message"><% errorMessage %></p>
<h3><% headMessage %></h3>
<form action="/confirm" method="post">
<p><input type="text" name="email" value="" size="50">  :  <input type="text" name="keyword" value="" size="50"></p>
<p>メールアドレス  :  検索キーワード</p>
<input type="submit" name="submit" value="送信">
</form>
<br>
<br>
<p>検索方法:</p>
<p>
検索キーワードをスペース区切りで入力する事により、<br>
AND検索になります。今のところOR検索はできません。
</p>
</div>

public/stylesheets/page.cssにエラーメッセージを赤色強調表示させるためのCSS記述を追加します

[public/stylesheets/page.css]

#message {
    color: red;
    font-size: 18px;
    font-weight: bold;
}
とりあえず起動

それではringoコマンドを使い、IT勉強会サーチアプリを起動させてます

$ $RINGO_HOME/bin/ringo it_study_search/main.js 

http://localhost:8080/にアクセスすると、以下のような登録ページが表示されます

IT勉強会サーチ

本編は登録ページのloadしかなかったので、特にロジカルな事はありませんでした、次編より、DB接続、メール送信などを使用する仮登録処理の開発に入ります



次のステップへ
Webフロント開発編(2)へ
]]>
http://blog.fukaoi.org/2011/05/27/server-side-javascript-webdev1http://blog.fukaoi.org/2011/05/27/server-side-javascript-webdev1Fri, 27 May 2011 06:09:04 GMT
<![CDATA[サーバーサイドJavaScriptで簡単Webアプリ開発 - スタート編]]> 最近、Node.JSを中心としたサーバーサイドJavaScriptが熱いですよね。Node.JS以外にもRingoJS、Helma、Jack、Aptana Jaxerなど有名なものから、wikiで調べてみると、サーバーサイドJavaScript環境は約40~50もありました!。今回、サーバーサイドJavaScriptを使ってWebアプリ開発を行っていくための構築方法を何回かにわけて記述していきたいと思います。選択したサーバーサイドJavaは、流行りのNode.JSではなく「RingoJS」というかわいらしい名前の環境になります。RingoJSを選択した理由としまして、以下がポイントとなりました。

  • JavaScriptでは実現が面倒な個所は、Javaの既存パッケージを利用することで、容易に補間できる
  • JavaVM上で動作する事で、Javaだけでなく、ScalaやJVM言語との幅広い連携が可能になる柔軟さ
  • Webアプリを開発する上で、RingoJSのパッケージがそれなりに揃っていた
  • 流行りのクラウド環境で動作させる場合に、Node.JSなどのV8環境より、JavaVM環境のほうが選択できるクラウドサービスの数が多い事
  • Webアプリだけでなく、コマンドラインアプリも作成できる
  • 公式サイトが見やすく、チュートリアルなんかが、そこそこ短くてわかりやすかった(笑)

結局、JavaVM上で動作するということが決め手となりました

動作環境

たぶん、JavaVM環境であれば、Linux、Windows、Mac問題なく動くと思いますが・・・動作環境は以下をベースにします。

  • Linux Debian6.0
  • RingoJS 0.7 (最新ver 0.8がそろそろリリースされると思いますが、ディレクトリ構造が結構変更されていますので互換性は不明です)
  • JVM 1.6.0
  • MySQL5.1.49

構築する上で参考にしたRingoJS関連サイト



次のステップへ
Webフロント開発編(1)へ
]]>
http://blog.fukaoi.org/2011/05/26/server-side-javascript-startuphttp://blog.fukaoi.org/2011/05/26/server-side-javascript-startupThu, 26 May 2011 06:05:13 GMT
<![CDATA[JavaScriptにJavaのようなequalsメソッドを作成する]]> JavaScriptでは等価比較を行う際に、== 、=== の2種類のequalによる比較演算子が用意されており、== の方は、型はチェックせず値が正しいかどうかの判断しかせず、 === の方は型をチェックして値の比較演算を行いますよね。=== の方はより厳密な演算子で、JavaScriptのテクニック本でも === の使用を進めています。 (※PHPでも同じ比較演算子が導入されています。)

var data = "";

// 0でもマッチしてしまう! 
if (data == 0) {
  console.log("Match"); 
}

// これだとマッチしないからGood
if (data === 0) {
  console.log("Match"); 
}

このようにequalの数(2個と3個)によって処理がかわるというのは、ミスを誘因しやすく、それによりBugの原因になったりする危険性があるので、不満がありました。構文チェックツール(JSLint)でチェックして、equalのミスコードをチェックするというのも手なのですが、頻繁にチェックするのは面倒です。そもそも間違いやすい比較演算子が問題なんだと思っています。。。(汗)そこでJavaでの等価比較メソッドである、equalsメソッドと同じようなJavaScript用のequalsメソッドを作成することにしました。 === とコーディングする箇所を誤って == としてしまったりするのは人間である以上、起こりえるものなので、別の名称のメソッドを用意して、イージミスを防ぎ素敵なJavaScriptライフを送りたいと思います。

期待する実装形式は下記の感じです

data1の型とdata2型の厳密比較演算を行いたい!

if (data1.equals(data2)) {
  // なんらかの処理
}

equalsメソッドの実装内容

個人的によく使用するStringオブジェクトのprototype chainに実装してみます。まずは、this == paramで値の比較を行います。そこで値が同じであれば、次に同じstring型かどうかを、typeof演算子で型のチェックを行います。特に目新しいコードでもなく、普通です。。。

String.prototype.equals = function(param) {
  if (this == param) {
    return (typeof param === "string" ? true : false);
  }
  return false;
};

Stringオブジェクトのprototype chainの実装部分でthis === paramと行ったほうが単純そうに思うかもしれませんが、thisはobject型ですので、期待通りの動作はしません。

String.prototype.equals = function(param) {
  return (this === param ? true : false);
};
thisとparamがマッチすることはなく、永遠にfalseがかえってしまいます

Rhinoベースでの環境ではちょっと注意が必要

Rhino1.7の環境で確認したのですが、RhinoやRhinoベースのサーバーサイドJavaScriptの環境では、すでにequalsメソッドが存在しているので注意が必要です。動作的には == 比較演算子と同じ、値の比較しか行いません。実際にRhino1.7のソースを解析した結果を晒しますと、下記になります。

src/org/mozilla/javascript/NativeString.javaを参照
このlineの前後に、巨大なswitch case文があり、Rhinoで使用可能なStringオブジェクトのメソッドが定義されています。line:190にequalsメソッドを確認できます
(1) org.mozilla.javascript.NativeScript line: 190
  ・
  ・
case Id_link:  arity=0; s="link";  break;
case Id_anchor:  arity=0; s="anchor";  break;
<b>case Id_equals:  arity=1; s="equals";  break;</b> 
case Id_equalsIgnoreCase:  arity=1; s="equalsIgnoreCase";  break;
case Id_match:  arity=1; s="match";  break;
  ・
  ・

このlineではequalsメソッドの実装内容が記述されています。しかしScriptRuntime.toStringで値を全て、JavaのString型に変換してしまっているので、型の比較は行ってくれません

(2) org.mozilla.javascript.NativeScript line: 367〜374

case Id_equals:
case Id_equalsIgnoreCase: {
  String s1 = ScriptRuntime.toString(thisObj);
  String s2 = ScriptRuntime.toString(args, 0);
  return ScriptRuntime.wrapBoolean(
  (id == Id_equals) ? s1.equals(s2) : s1.equalsIgnoreCase(s2));
}


]]>
http://blog.fukaoi.org/2011/05/02/javascript_equalshttp://blog.fukaoi.org/2011/05/02/javascript_equalsMon, 02 May 2011 06:01:57 GMT
<![CDATA[ScalaとMecabで形態素解析を行いコンソールに自動Tagリストを作る]]> MecabをScalaから呼び出し、あるデーターを形態素解析し、コンソール上にTag Cloudっぽい表示をしたいと思います。自動的に抽出した単語をTagっぽくみせますので、形態素解析した単語の中から「名詞」にターゲットを絞りこんでScalaで抽出します。コンソールへの出力は以前blogで書いた、「ScalaからCライブラリを読み込んで実行する」で利用した、libfont_style.soを使用し、ScalaからMecab APIを呼び出すのにJavaのAPIが提供されている、cmecab-javaを使用したいと思います。

実装環境

OS     :Ubuntu(kernel:2.6.32-24-generic)
JVM    :OpenJDK 1.6.0_20
Scala  :Scala 2.8.1.final
charset:UTF-8

解析対象データ

2011/02/07の共同通信社の報道ニュースを使用

米ナスダックのシステムにハッカー侵入―取引システムには被害なし


 米株式市場ナスダックを運営するナスダックOMXは5日、 同社のコンピューターシステムにハッカーが侵入していたことを認めた。 ただ、取引システムには被害は生じていないとしている。 ハッカー攻撃を受けたシステムの顧客には通知済みだという。 ハッカー攻撃にあったのは、企業役員向け情報サービス 「ディレクターズ・デスク(Directors Desk)」の システム内で、不正ファイルが見つかったという。

 ナスダックへのハッカー攻撃については、 5日付のウォール・ストリート・ジャーナルが報じており、 過去1年間に複数回、侵入されていたことを明らかにしている。 ナスダックOMXは本紙報道の後、リリースを発表し、 「当該ファイルはすぐに除去されており、現時点では、 ハッカーがディレクターズ・デスクの顧客の情報にアクセスしたり、 それらを取り出したりした形跡はない」と述べた。

 取引システムについては、「ディレクターズ・デスクなど ウェブにつながっているサービスとは独立して運営されており、 ナスダックOMXが運営また提供する取引プラットフォームが 影響を受けたことはまったくない」としている。 取引所運営大手のNYSEユーロネクストとダイレクト・エッジは、 7日もナスダックとの接続を続けるとしている。 一方、一部トレーディング会社も、ナスダックへの発注を続ける方針を示した。

動作環境構築

  1. sconsのインストール(cmecab-javaのインストールで必要)

  2. cmecab-javaではmake形式ではなく、sconsをつかってC/C++プログラムをコンパイルします。aptパッケージにsconsがあるのでaptからインストールします

    apt-get install scons
    
  3. antのインストール(cmecab-javaのインストールで必要)

  4. 日々Javaで開発を行っている人であれば、antは既にインストールされていると思いますが、一応書いておきます。aptパッケージにantがあるのでaptからインストールします

    apt-get install ant
    
  5. protocol bufferのインストール(cmecab-javaのインストールで必要)

  6. wget http://protobuf.googlecode.com/files/protobuf-2.3.0.tar.bz2
    
    tar -xjf protobuf-2.3.0.tar.bz2
    cd protobuf-2.3.0
    
    ./configure
    make
    make install
    

    usr/local/lib に作られるので、ldconfigを実行しておきます (もしかすると)/etc/ld.so.confに"/usr/local/lib"というパスの追加が必要かもしれないです

  7. mecabのインストール

  8. wget http://sourceforge.net/projects/mecab/files/mecab/0.98/mecab-0.98.tar.gz/download -O mecab.tar.gz
    
    tar -xvf mecab.tar.gz
    cd mecab-0.98
    
    ./configure --with-charset=utf8
    make
    make install
    

    デフォルトだと/usr/local/lib配下にライブラリはインストールされます

  9. cmecab-javaのインストール

  10. wget http://cmecab-java.googlecode.com/files/cmecab-1.7.tar.gz
    
    tar -xzf cmecab-1.7.tar.gz
    
    cd cmecab-1.7/
    ant
    

    cmecab-1.7/bin/cmecab-1.7.jarが生成されます

    cd cmecab-1.7/jni/
    vi SConstruct
    
    #環境に応じてjavaの動作環境パスに適時変更します
    Line:37 
     javahome = '/usr/lib/jvm/java-6-openjdk'
    
    #環境に応じてjni.hファイルなどJavaヘッダーファイルのパスを適時追加します
    Line:39
     join(javahome, 'include', 'linux', '/usr/lib/jvm/java-6-openjdk/include')]
    
    scons libCMeCab.so
    

    cmecab-1.7/jni/libCMeCab.soが生成されます

  11. ipa辞書のインストール

  12. このままですと、単語の認識ができないお馬鹿な解析になってしまいますので、標準的な「ipa辞書」をインストールします

    wget http://sourceforge.net/projects/mecab/files/mecab-ipadic/2.7.0-20070801/mecab-ipadic-2.7.0-20070801.tar.gz/download  -O mecab-ipadic-2.7.0-20070801.tar.gz
    
    tar -xzf mecab-ipadic-2.7.0-20070801.tar.gz
    cd mecab-ipadic-2.7.0-20070801
    
    ./configure --with-charset=utf8
    make
    make install
    

    "--with-charset=utf8"をつけないと、デフォルトcharsetで"EUC-JP"になってしまいます 「ipa辞書」意外にも「wikipedia」、「はてな」から辞書を追加する方法を明記されている方がいらっしゃるので、そちらを参考にしてより語彙を高めるとよいかと思います。

実行プログラム

  1. Scalaのソース内容(Analyzer.scala)

  2. ソースの可読性を優先するためエラー処理などは省いています。
    (def main()のwhile (node.hasNext) {xxxxx}の中の実装がいけてない感じがします・・・)

    /**
     * Analyzer.scala
     * This class Morphological analysis, occurrence frequency
     * of words does display
     * 形態素解析、単語出現リスト表示クラス
     *
     * @author fukaoi
     * @version 0.1
     */
    package blojsomscala
    
    import net.moraleboost.mecab.{ Node }
    import net.moraleboost.mecab.impl.StandardTagger
    import scala.util.matching.Regex
    import scala.io.Source
    import com.sun.jna._
    
    object Analyzer {
    
     /**
      * Main method
      * メインメソッド
      */
      def main(args: Array[String]) {
        val words = Source.fromFile(args(0)).getLines.mkString
        val tagger = new StandardTagger("UTF-8", "")
        val node = tagger.parse(words)
        val regex = """名詞""".r
    
        var mapWordLists = Map[Int, String]()
        var i = 0
    
        while (node.hasNext) {
          val feature = node.next
          regex findPrefixOf node.feature match {
            case Some(v) = mapWordLists += (i - feature)
            case None =
          }
          i = i + 1
        }
        val sortWordLists = getMostShowWord(mapWordLists)
        display(sortWordLists)
      }
    
     /**
      * Get Occurrence frequency of word in Scala List
      * 単語の出現頻度をリストにして返す
      */
      def getMostShowWord(wordList: Map[Int, String]): Seq[(String, Int)] = {
        wordList.groupBy(x = x._2).
          map(y = (y._1, y._2.size)).
          toSeq.sortWith(_._2  _._2)
      }
    
     /**
      * Setting in JNA for C Library
      * CライブラリをJava JNAにセットする
      */
      def setNativeLibrary(colorName: String) {
        val lib = NativeLibrary.getInstance("libfont_style.so")
        val func = lib.getFunction("font_color")
        func.invokeVoid(Array(colorName))
      }
    
     /**
      * Analytics data to display on console
      * コンソール結果に集計した結果を出力する
      */
      def display(list: Seq[(String, Int)]) {
        val charCode = "Shift-JIS"
        var maxLength = 0
        list.map(x = if (x._1.getBytes(charCode).length  maxLength) maxLength = x._1.getBytes(charCode).length)
        list.map(x = (setNativeLibrary(getColorNumber(x._2)),
          print(x._1),
          print(" " * (maxLength - x._1.getBytes(charCode).length)),
          print("+" * x._2),
          print("\n")))
      }
    
     /**
      * Get color code for C Library(setNativeLibrary)
      * Cライブラリ(setNativeLibrary)に渡す
      * カラーコードを取得する
      */
      def getColorNumber(count: Int): String = {
        val colors = Map(
          (1 - "blue"),
          (2 - "cyan"),
          (3 - "green"),
          (4 - "magenta"),
          (5 - "red"))
        if (count = 6)
          "yellow"
        else
          colors(count)
      }
    }
    

    自動Tagを作成する上で対象となる名詞だけを抽出するので、mecabの解析データから正規表現でマッチさせて抜き出します

    val regex = """名詞""".r
    

    Tagの文字長がばらばらなため、コンソールに出力する際の見た目の体裁を整えるために、一番長いTagに合わせて出力位置を決定します。そのために冗長ながら、Mapの中から最大文字長を調べています。charCode"Shift-JIS"と定義したのは、UTF-8ですと1文字が1~6バイトの可変長になり文字長を求めるのが難解だったため、固定長のChar Codeにしたかったからです

    val charCode = "Shift-JIS"
    
    if (x._1.getBytes(charCode).length  maxLength) maxLength = x._1.getBytes(charCode).length
    

    Analyzer.scalaのソースコードをダウンロードする(GitHub)

  3. Scalaプログラムのコンパイル

  4. cd blog/2011/
    scalac -classpath .:jna.jar:cmecab-1.7.jar blojsomscala/Analyzer.scala
    
  5. コマンド実行

  6. scala -Djava.library.path=

  7. 実行結果

  8. "+"の数が解析対象データの中でのTag(名詞単語)の出現回数になり、回数が多いTagから上位に並んでいます

    mecab

結果考察

抽出したTagを見てみると、上位3位の「ナスダック」、「システム」、「ハッカー」はTagとしては十分意味が通じる単語 ですが、それより下位の「日」、「こと」、「5」、「の」はTagとしては不十分な結果です。やはり辞書の追加であったり、N-gram による単語の解析が必要だと思います。今後の課題結果となりました。


]]>
http://blog.fukaoi.org/2011/02/08/scala_mecab_taghttp://blog.fukaoi.org/2011/02/08/scala_mecab_tagTue, 08 Feb 2011 05:52:14 GMT
<![CDATA[ScalaからCライブラリを読み込んで実行する]]> JavaからCライブラリを読みこんで実行する場合に使われる方式としては、「JNI」と「JNA」があるとおもいます。「JNI」はヘッダーファイルなどを用意したり面倒なので、「JNA」が最近はおすすめのようです。Javaで実装可能であれば、Scalaからも可能ということから、ScalaからCのライブラリを読み込んでみたいと思います。 実際に使うCのライブラリは、font_style.cという、下記プログラムを使用します。font_style.cというライブラリは特に特殊な事をしているわけでなく動作内容的に、実引数に色コード名(red、yellow、cyan、green、blue、magenta、gray、black)を受け取るとエスケープシーケンスを使って、コンソールのカラーコードを変更するというものです。

導入内容

  1. Cライブラリ - fontstyle.cの用意

  2. #include <stdio.h>
    #include <stdlib.h>
    
    void font_color(char* cname) {
      int cnumber = 0;
    
      if (strcmp(cname, "black") == 0) {
        cnumber = 30;
      } else if (strcmp(cname, "red") == 0) {
        cnumber = 31;
      } else if (strcmp(cname, "green") == 0) {
        cnumber = 32;
      } else if (strcmp(cname, "yellow") == 0) {
        cnumber = 33;
      } else if (strcmp(cname, "blue") == 0) {
        cnumber = 34;
      } else if (strcmp(cname, "magenta") == 0) {
        cnumber = 35;
      } else if (strcmp(cname, "cyan") == 0) {
        cnumber = 36;
      } else if (strcmp(cname, "white") == 0) {
        cnumber = 37;
      } else if (strcmp(cname, "default") == 0) {
        cnumber = 39;
      } else {
        return;
      }
    
      fprintf(stderr, "\x1B[%dm", cnumber);
    }
    </stdlib.h></stdio.h>

    font_style.cのソースコードをダウンロードする(GitHub)


  3. 共有ライブラリとしてコンパイルを行う

  4. gcc -fPIC -shared font_style.c  -o libfont_style.so
    

    共有ライブラリにするので、-fPICと-sharedオプションを指定する

  5. JNAをダウンロード

  6. JNAのjarファイルをダウンロードする

  7. Scalaプログラム - JNAを使って、libfont_style.soを読み込むプログラム

  8. import com.sun.jna.*
    // Javaのjnaライブラリをimport設定
    
    object SetFontColor {
    
      /**
       * @param args the command line arguments
       */
      def main(args: Array[String]): Unit = {
        val lib = NativeLibrary.getInstance("libfont_style.so")
        // font_styleライブラリのインスタンスを生成する
    
        val func = lib.getFunction("font_color")
        // font_style.cにある関数 void font_color(char cname)を読み込む
    
        func.invokeVoid(Array(args(0)))
        // invokeVoidメソッドを使って、font_color関数のcnameに色名を渡す。
      }
    }
    

    - func.invokeVoid(Array(args(0)))について -

    JNAのAPI仕様書をみると、最終的にinvokeメソッドを内部で読んでいるので、invoke()メソッドでOKだと思いますが、font_color関数はvoid型なので、invokeVoidのほうが適切かと思いました。

    [invokeVoid 仕様]  
    
    public void invokeVoid(Object[] args) {
      invoke(Void.class, args);
    }
    

    SetFontColor.scalaのソースコードをダウンロードする(GitHub)


  9. Scalaプログラムのコンパイルを行う

  10. scalac -classpath jna.jar SetFontColor.scala
    

    コンパイルに成功しますと、SetFontColor$.class、SetFontColor.classという2つのクラスファイルができます。
    SetFontColor$.class・・・・SetFontColor.scalaの本体
    SetFontColor.class・・・・ラッパー

  11. 実行するためのコマンドパス

    • libfont_style.soをカレントに設置する場合
    • scala -classpath .:jna.jar SetFontColor 
    • -Djna.library.pathを指定してlibfont_style.so呼びだす場合
    • scala  -Djna.library.path 

出力結果一覧

Blackにつきましては、背景と同色のため、実行する際に、ターミナルの背景色を変更してあります

[Red]

red


[Yellow]

yellow


[Cyan]

cyan


[Green]

green


[Blue]

blue


[Magenta]

magenta


[Gray]

gray


[Black]

black


関連リンク
"http://m-ono.info/tech/programming/jna/
]]>
http://blog.fukaoi.org/2010/12/13/ScalaからCライブラリを読み込んで実行するhttp://blog.fukaoi.org/2010/12/13/ScalaからCライブラリを読み込んで実行するMon, 13 Dec 2010 05:47:10 GMT
<![CDATA[Scala、Java、PHPでソースコードの量を比較してみる]]> Scalaを勉強しはじめて、処理をシンプルに表記できる関数型言語の仕様に衝撃をうけている毎日です。あるロジックを記述するにしても、手続き型言語のように、手続き形式で記述する場合に比べて、ソースコード量が少なく記述できます。コード量がその言語の優越を決定するとは思いませんが、単純に同じ処理を実現する上で、50行で記述するよりは10行で記述できれば、10行で記述できる言語のほうを、生産性が高いと言ってもよいかと思います。 実際にどれくらい差がでるのか共通のロジック条件を元に比較したソースコード、処理結果をかいてみました。比較対象言語としては自分がよく利用している言語になってしまうのですが、Java、PHPそしてScalaで比較しています。
(※きっともっとベストなロジックがあるかと思いますが・・・)

各言語共通ロジック条件

  • 同じ年月は同じグループとしてカウントして数を求める
  • 最新の年月を降順でソートする
  • 無理やり1行にまとめず、ある程度の可読性は保つようにする
  • 宣言部分、実装メソッドの定義部分などはカウントせず除外する
  • //---(logic)----で囲んだ箇所を比較対象箇所とする
  • 下記「変更前」データーから「変更後」データになるよう標準出力を行う
Scala


各言語のソースコード例

Javaでのロジック実装 Pattern.1 -降順にComparatorクラス-

処理フロー的にはターゲットとなっている年月データ(exp. 20xx-xx)の入った配列をループで回しながら、同じ年月データがあれば、Mapの値にカウントして増加させています。降順にソートするためにComparatorクラスを実装することにより実現しております。型宣言、Comparatorクラスの定義などにより、他の言語に比べてソースコード量は一番多くなってしまいます。

import java.util.Comparator;
import java.util.TreeMap;

public class Demo {
  public static void main(String[] args) {
        String yearMonths[] = {
            "2009-11", "2009-01",
            "2010-01", "2010-12",
            "2010-01", "2010-04",
            "2010-01", "2010-12",
            "2010-12", "2010-04"};

//------(logic)--------------------

1.       TreeMap<String, Integer> countsSort = 
               new TreeMap<String, Integer>(new DesComparator());
       
2.       for (String yearMonth: yearMonths) {
3.         if (countsSort.get(yearMonth) == null){
4.           countsSort.put(yearMonth, 1);
5.         } else {
6.           countsSort.put(yearMonth, countsSort.get(yearMonth) + 1);
7.         }
8.       }
9.       System.out.println(countsSort);
10.    }
11.  }

12.  class DesComparator implements Comparator<String> {
13.    public int compare(String object1, String object2) {
14.      return ((Comparable<String>) object1).compareTo(object2) * -1;
15.    }
16.  }

 //------(logic)--------------------

[Javaコードの出力結果]
Java


Javaでのロジック実装 Pattern.2 -降順にdescendingMap()メソッド-

pekoさんに教えてもらったのですが、TreeMap.descendingMap()を使う事によって、Java Pattern.1で書いたようなComparatorクラスの降順ロジックが省けます。こっちほうが全然いけてますね!

import java.util.TreeMap;

public class Demo {
  public static void main(String[] args) {
        String yearMonths[] = {
            "2009-11", "2009-01",
            "2010-01", "2010-12",
            "2010-01", "2010-04",
            "2010-01", "2010-12",
            "2010-12", "2010-04"};

//------(logic)--------------------

1.       TreeMap<String, Integer> countsSort = 
               new TreeMap<String, Integer>();
       
2.       for (String yearMonth: yearMonths) {
3.         if (countsSort.get(yearMonth) == null){
4.           countsSort.put(yearMonth, 1);
5.         } else {
6.           countsSort.put(yearMonth, countsSort.get(yearMonth) + 1);
7.         }
8.       }
9.       System.out.println(countsSort.descendingMap());
10.    }
11.  }

 //------(logic)--------------------

[Javaコードの出力結果]
Java


PHPでのロジック実装 Pattern.1 -foreachで普通にループ-

基本的に処理フローはJavaと殆ど同じです、異なる点としては、降順にソートを行う処理を、年月データのカウントの後にkrsort関数を使用しているところでしょうか。Javaに比べて半分近いソースコード量になりました、LL言語としては想定内の結果ですね。

<?php
$yearMonths = array(
                '2009-11', '2009-01',
                '2010-01', '2010-12',
                '2010-01', '2010-04',
                '2010-01', '2010-12',
                '2010-12', '2010-04');

//------(logic)--------------------

1.  foreach ($yearMonths as $yearMonth) {
2.    if (in_array($yearMonth, $yearMonths)) {
3.      $countsSort[$yearMonth] += 1;
4.    }
5.  }
6.  krsort($countsSort);
7.  print_r($countsSort);

//------(logic)--------------------

[PHPコードの出力結果]
PHP


PHPでのロジック実装 Pattern.2 -array_count_values-

ktnyさんに教えてもらったのですが、array_count_valuesというずばりの関数がありました。これを使えば一撃終了ですね。。。こんな便利関数があったとは

<?php
$yearMonths = array(
                '2009-11', '2009-01',
                '2010-01', '2010-12',
                '2010-01', '2010-04',
                '2010-01', '2010-12',
                '2010-12', '2010-04');

//------(logic)--------------------

1.  $countsSort = array_count_values($yearMonths);
2.  krsort($countsSort);
3.  print_r($countsSort);

//------(logic)--------------------

[PHPコードの出力結果]
PHP


Scalaでのロジック実装 Pattern.1 -手続き型っぽく-

Java、PHPと同じような処理フローで記述してみました、ループ=>グループカウント=>降順ソートという流れですね。PHPと大差はないソースコード量となりました。まだScalaらしさを出し切れていないコード記述だと思います。

object Demo {
  def main(args: Array[String]){
    val yearMonths = List(
             "2009-11", "2009-01",
             "2010-01", "2010-12",
             "2010-01", "2010-04",
             "2010-01", "2010-12",
             "2010-12", "2010-04")
//------(logic)--------------------

1.   var countsSort = Map.empty[String, Int]
2.   yearMonths.foreach {
3.     c => var cnt = countsSort.getOrElse(c, 0)
4.     countsSort += (c -> (cnt + 1))
5.   }   
6.   println(countsSort.toSeq.sortWith(_._1 > _._1))

//------(logic)--------------------
Scalaでのロジック実装 Pattern.2 -つなげて、わたす-

yuroyoroさんのblogを参考に記述したのですが、非常に簡略化できました。まさしくScalaらしさであるつなげて、わたす方式です。最初見たときはまだ関数型言語になれてないのもあって、何がなんだかわからず「なんだ、この可読性の悪さは!」と拒否反応がおきました。ですがScalaを知れば知る程、このような記述方法がシンプルで非常に美しく感じるようになりました。要は慣れということですね。

object Demo {
  def main(args: Array[String]){
    val yearMonths = List(
             "2009-11", "2009-01",
             "2010-01", "2010-12",
             "2010-01", "2010-04",
             "2010-01", "2010-12",
             "2010-12", "2010-04")
//------(logic)--------------------

1.  val countsSort = yearMonths.foldLeft(Map.empty[String, Int]){
             (m, c) => m + (c -> countsSort.getOrElse(c, 0) + 1)}
2.  println(countsSort.toSeq.sortWith(_._1 > _._1))

//------(logic)--------------------
Scalaでのロジック実装 Pattern.3 -もっとシンプルに!-

みずしまさんよりアドバイスして頂き、もっとシンプルに記述ができました。ListクラスのgroupByメソッドを使うという方法で、同じ値をまとめてList()にしてくれるのでPattern.2より簡略化でき、直感的に分かりやすいですね。groupByメソッドのAPI仕様と、処理結果を下記にまとめました。


[groupByメソッドAPI仕様]

def groupBy [K] (f: (A) ⇒ K) : Map[K, List[A]]

[groupByメソッドの処理結果]

(2009-11,List(2009-11))
(2010-01,List(2010-01, 2010-01, 2010-01))
(2009-01,List(2009-01))
(2010-12,List(2010-12, 2010-12, 2010-12))
(2010-04,List(2010-04, 2010-04))

※yearMonths.groupBy(x = x).map(e = (println(e)))で
  標準出力した処理結果です。ちゃんとList()にまとまって
 います。
object Demo {
  def main(args: Array[String]){
    val yearMonths = List(
             "2009-11", "2009-01",
             "2010-01", "2010-12",
             "2010-01", "2010-04",
             "2010-01", "2010-12",
             "2010-12", "2010-04")
//------(logic)--------------------

1.  val countsSort = yearMonths.groupBy(x => x).map(e => (e._1, e._2.size))
2.  println(countsSort.toSeq.sortWith(_._1 > _._1))

//------(logic)--------------------

[Scalaコード(Pattern.1, 2, 3)の出力結果]
Scala

ソースコード量の比較結果

ロジック処理ソースコード数

  • Java :約11行~16行
  • PHP :約3行~7行
  • Scala:約2行~6行
]]>
http://blog.fukaoi.org/2010/10/12/Scala、Java、PHPでソースコードの量を比較してみるhttp://blog.fukaoi.org/2010/10/12/Scala、Java、PHPでソースコードの量を比較してみるTue, 12 Oct 2010 02:54:20 GMT
<![CDATA[Jettyのdeploy仕様がおかしいと思う]]> Jetty で運用しているJavaアプリがあるのですが、ある時、「JSPファイル」、「画像ファイル」、「静的HTMLファイル」にアクセスできなくなり404 Error not foundが発生しはじめました。プログラム修正などを行っていない状態で、指定のJSPファイルを呼び出そうとすると以下のようなエラーExceptionが発生するようになってしまいました。 Jettyも再起動することも、特に問題なく(logを見るかぎり)約1カ月程運用後の出来事でした。最初は404 Error not foundがいきなり発生したので怪奇現象かと思ってしまいました(笑)。調べてみたところ、Jettyのdeploy先のフォルダ内が空っぽになって、「JSPファイル」、「画像ファイル」、「静的HTMLファイル」などファイル一式が無くなっていました?!そもそもJettyのdeploy先に問題があるのではないかと思い、TomcatとJettyのdeploy先を比較してみました。

[404 Error not found発生時のlog内容]

org.apache.jasper.JasperException: 
PWC6117: File "/xxxxxxxxxxx.jsp" not found

at org.apache.jasper.compiler.DefaultErrorHandler.jspError
at org.apache.jasper.compiler.ErrorDispatcher.dispatch
at org.apache.jasper.compiler.ErrorDispatcher.jspError
at org.apache.jasper.compiler.JspUtil.getInputStream
at org.apache.jasper.xmlparser.XMLEncodingDetector.getEncoding

JettyとTomcatのdeploy先の比較

仮にfukaoi.warというWebアプリケーションファイルを作成して各Servlet上にdeployすると仮定します

[Tomcatの場合]

mv fukaoi.war $TOMCAT_HOME/webapps/
↓
$TOMCAT_HOME/webapps/Jetty_0_0_0_0_80_fukaoi.war<strong>fukaoixxx
</strong>
[Jettyの場合]

mv fukaoi.war $JETTY_HOME/webapps/
↓
/tmp/Jetty_0_0_0_0_80_fukaoi.warfukaoixxx

/tmpにdeployされている!!!!

今回使用したミドルウェア情報

[ミドルウェア]
OS    :CentOS release 5.4 (Final)
Jetty :6.1.22(STABLE)
Java  :1.6.0_15(OpenJDK)
原因はJettyがdeploy先をデフォルトで/tmpとしていることで、OSのCronによって消されていた可能性が高いと思います。RedHat系(CentOS)ですと/etc/cron.daily/tmpwatch というCronが存在しており、一定期間(240時間、720時間)アクセスが無い場合など/tmp以下のゴミ掃除を行うようです。

対策方法(1): 公式ページに記述されているworkディレクトリ方式

$JETTY_HOME/workディレクトリを作成する事により/tmpから変更します
mkdir $JETTY_HOME/work
chown nobody(exp. daemon) $JETTY_HOME/work

コメント欄からともさんからアドバイスを頂きましたところ、公式ページに/tmpディレクトリからの変更方法が記述されていました。公式ページにはwebapp.getTempDirectory()を使う方法も記述されていましたが、組み込み方式でのJetty起動はここではとりあげてませんので省略したいと思います。気をつけないといけない点があるとすれば$JETTY_HOME/workにJetty実行ユーザー(nobody、daemon)で読み込み実行権限でアクセスできるようにPermissionの変更をしてあげるということです

対策方法(2): /tmpから$JETTY_HOME/webappsにdeploy先の変更(Tomcat方式)

環境変数$TMPでdeploy先を変更できるので、$JETTY_HOME/webappsに変更するように$TMPに値をセットする

export TMP=$JETTY_HOME/webapps
chown nobody(exp. daemon) $JETTY_HOME/webapps

手動でコマンドで実行してもよいですし、さすがに毎回は面倒であれば.bash_profile、.bash_aliasで定義して自動的に読む込むようにしておいてもよいかもしれません。対策方法(1)にも記述しましたようにPermissionの変更をしておかないとJettyから読み込みできないです

どうしてJettyはデフォルトWebアプリケーションを/tmpにdeployするのか?

理由はさっぱり不明です・・・(笑)
/tmpディレクトリは、一時ファイル、pidファイルなど消されてしまっても影響がないファイルやディレクトリを設置する場所だと認識しています。Tomcat だとwebappsにwarファイルを設置するとdeploy先はそのままwebappsなのに、JettyはLinux環境だと何故か/tmpディレクトリにデフォルトでdeployされます。開発・テスト時では問題になることは少ないと思いますが、運用サーバとして使用した際には/tmpという展開先が非常に問題だと思います。僕と同じ問題が発生する可能性もありますし、しかしネットで「どうしてJettyのデフォルトdeploy先は/tmpなのか?」ググってもそれらしき記事はほとんどなくもやもや感一杯です。他の方の環境では発生していないのか?どなたか、理由をご存じの方がいらっしゃれば教えてください。Jettyの七不思議でした!

]]>
http://blog.fukaoi.org/2010/09/09/Jettyのdeploy仕様がおかしいと思うhttp://blog.fukaoi.org/2010/09/09/Jettyのdeploy仕様がおかしいと思うThu, 09 Sep 2010 13:26:02 GMT
<![CDATA[JettyをPort80で起動する方法]]> JettyはTomcatと比べて、ファイルサイズも小さく、起動が早いので最近、僕のお気にいりのWebサーバ兼サーブレットコンテナなんですが、困った点が1つあります。 デフォルトPortは8080なんですが、Portを80に変更して運用するには管理者権限(root)が必要になってしまうようです。一般Shellユーザー(例えばjettyとかユーザーを作成した場合)でPort:80で試してみましたところ、起動はできるんですが途中でExceptionを吐いてコケテしまいます。
本来はApacheのように実行権限のないnologinのWebサーバ用ユーザー(daemon、 nobody...)で起動するのが理想です。というかセキュリティ面を考えると当たり前だと思うんですよね。管理者権限(root)、一般ShellユーザーでJettyを起動してサービスを運用したくない。万が一JettyにBugがあった場合、バッファーオーバーフローをつかれて、権限を奪取されサーバ内に侵入されてしまうわけですから。そこで JettyをPort:80でnologinユーザー(実行権限の無い)で起動する方法をまとめてみました。下記のJettyのFAQページを参考にしました。

"http://docs.codehaus.org/display/JETTY/port80
(※)Jetty7や他のバージョンでは試していないので同じ方法かどうかはご注意

今回使用したミドルウェア情報・補助情報

[ミドルウェア]
OS    :2.6.31-17-generic(Ubuntu、CentOS)
Jetty :6.1.22(STABLE)
Java  :1.6.0_15(OpenJDK) 

[補助用語]
$JETTY_HOME  :Jettyのソースを設置したディレクトリ
$JAVA_HOME   :Java SDKをインストールしたディレクトリ
               (コンパイルが発生するのJava Runtimeだけでは駄目かも)

導入方法は次の2パターンが有ります

(1):一般ShellューザでPort:8080で起動してOSの機能であるipchains、iptablesを使って、Port:80へのリクエストを8080へフォワードする
(2):JNIを使って、Nativeライブラリを起動時に読み込みnologinユーザーにsetuid (and setumask)を行う

(1):iptables、ipchainsを使用して実現する方法

iptables
/sbin/iptables -t nat -I PREROUTING -p tcp --dport 80 \
 -j REDIRECT --to-port 8080
ipchains
/sbin/ipchains -I input --proto TCP --dport 80 \
-j REDIRECT 8080

(2):nologinユーザーにsetuidを使用して実現する方法

  1. setuidのNativeライブラリをJettyにあてるために解凍したJettyのソースに移動する
  2. cd $JETTY_HOME/extras/setuid
    
  3. (a):Mavenを使ってNativeライブラリ(org_mortbay_setuid_SetUID.c)をインストールする場合
    $JETTY_HOME/extras/setuid配下にMavenの設定ファイル
    (pom.xml)があるので、mvn installを実行する
    
    (b):手動でNativeライブラリ(org_mortbay_setuid_SetUID.c)をインストールする場合
    mkdir -p modules/native/target/generated
    
    cp modules/native/src/main/native/ \
     org_mortbay_setuid_SetUID.c<br> modules/native/target/generated<br>
    javah -d modules/native/target/generated \
     -classpath modules/java/target/ \ 
     jetty-setuid-java-6.1.22.jar
     org.mortbay.setuid.SetUID
    
    gcc -I$JAVA_HOME/include/ \
      -I$JAVA_HOME/include/linux/  \
      -shared \
      modules/native/target/generated/ \
      org_mortbay_setuid_SetUID.c -o
      ../../lib/ext/libsetuid.so
    
  4. libsetuid.soファイルができているか確認する
  5. ls $JETTY_HOME/lib/ext/libsetuid.so
    

    ただし、何故かJetty6.1.22のバージョンでは解凍した段階でlibsetuid.soも同梱されていた・・・どの環境によるコンパイルか不明のため、自分でコンパイルは行った方がよいと思います

  6. Jettyを実行させたいnologinユーザのユーザーIDとグループIDを設定ファイルに指定する
  7. vi $JETTY_HOME/etc/jetty-setuid.xml
    

    nobodyユーザーのuid、gidに修正する。uid、gidはお使いの環境によってことなるので/etc/passwdなどで確認して適時値を変更してください

    <configure id="Server" class="org.mortbay.setuid.SetUIDServer">
     <set name="startServerAsPrivileged">false</set>
     <set name="umask">2</set> 
     <set name="uid">jetty</set> (変更)= <set name="uid">6553</set> 
     <set name="gid">jetty</set> (変更)= <set name="gid">6553</set>
    
    
     <!--↑↑uid、gidの値をjettyから6553に変更する↑↑-->
    </configure>

  8. jetty.xmlのPort指定箇所を8080から80に変更する

    <call name="addConnector">
     <arg>
      <new class="org.mortbay.jetty.nio.SelectChannelConnector">
      <set name="host"><systemproperty name="jetty.host"></systemproperty></set>
      <set name="port"><systemproperty name="jetty.port" default="8080"></systemproperty></set>
    
      <!--↑↑defaultの値を8080から80に変更する↑↑-->
    
      <set name="maxIdleTime">30000</set>
      <set name="Acceptors">2</set>
      <set name="statsOn">false</set>
      <set name="confidentialPort">8443</set>
      <set name="lowResourcesConnections">5000</set>
      <set name="lowResourcesMaxIdleTime">5000</set>
      </new>
     </arg>
    </call>
    
  9. jetty-setuid.xmlを指定して、Jettyを起動させる
  10. $JAVA_HOME/bin/java -jar $JETTY_HOME/start.jar \ 
      $JETTY_HOME/etc/jetty-setuid.xml \
      $JETTY_HOME/etc/jetty.xml
    

    この時の注意点として、読み込ませる設定ファイルの中で、jetty-setuid.xmlを一番最初に指定すること。指定順番が大事です

設定、コンパイルが上手くいかない場合

java.net.SocketException: Permission denied
 at sun.nio.ch.Net.bind(Native Method)
 at sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:119)
 at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:59)

Permission deniedで一般ユーザーでは権限がないというメッセージがでて起動途中でとまってしまいます。この場合は設定ファイル、コンパイル内容などを疑ってください

結論

(1)のほうが、Jettyのソースを変更することなく、Port:80で起動を可能にしますし、お手軽だと思います。ですがPortフォワードというOSの機能を使って実現するあたりが、スマートではないように思います。リクエストのたびにOSに多少なりともフォワード負荷が余計にかかる事が、本来ならアプリケーション処理に割り当てられるはずの、サーバのリソースを無駄に消費している感がなんとも損した気持ちになってしまいます。実際Portフォワードでサーバが消費するリソースは微々たるものだと思いますが・・・。 なにより一般Shellユーザーで結局はPort:8080で起動しないといけないので、セキュリティ的な不安は残ってしまいます。
やはり個人的には(2)がベストだと思います、GCCによるコンパイルなど、ちょっと面倒ではありますが、 Mavenがインストールされている環境であれば、ワンステップで処理が完了するので、そこまで苦にはならないだろうし、nologinのWebサーバ用ユーザーで実行するので、通常のApacheとかの起動内容と同じになりますので、実行ユーザーに関する点のセキュリティ的リスクは限りなく低くなると思います。

]]>
http://blog.fukaoi.org/2010/03/11/jetty-port-80http://blog.fukaoi.org/2010/03/11/jetty-port-80Wed, 10 Mar 2010 15:43:27 GMT
<![CDATA[いまさらながらPHP_EOL定数と改行コードについて]]> PHPには、Linux、Mac、Windowsなどの異なるOS環境間で改行コードを内部的に自動で切り替えてくれるPHPEOLという便利な定数が用意されている。Javaでの実装だとSystem.getProperty("line.separator")にあたると思う。PHPEOLがPHPの内部ではどのように実装されているのか、いまさらながらPHP_EOLと改行コードについて書いてみた。

PHP_EOLが定義されているソースファイルをリーディングする

リーディングするソースコードのPHPバージョンはphp-5.3.1を使う。PHP_EOLの定義箇所は以下のヘッダファイルにあり、PHP_EOL以外の定義済み定数の大半もこのファイルで定義されている。

php-5.3.1/main/php.h の Line47 ~ 71あたり
#ifdef PHP_WIN32<br>
  #include "tsrm_win32.h"
    ・
    ・
  #define PHP_EOL "\r\n"
   // (OSがWindowsの場合に改行コードが<b>\r\n</b>になる)

#else
    ・ 
    ・ 
 #if defined(__MacOSX) 
   // ※<strong>MacOSX</strong>このマクロはMacOS10では使われてない
   //   らしい。なのでここの条件分岐にMacOS10は入らない

   #define PHP_EOL "\r" 
    // (OSがMacOS9以前の場合に改行コードが<b>\r</b>になる) 

 #else

   #define PHP_EOL "\n" 
    // (OSがMacOS10、Linux、Unixなどの場合に<b>\n</b>になる) 

 #endif

#endif

このように、OS環境によって、if文で条件分岐し改行コードの文字表現を\r\n\r\nとdefineしているシンプルなソースコードである。わかりやすいように一覧表にまとめてみると以下の関係になり、ネットでも検索すれば同じような一覧内容は確認できる。

改行コード

各OS環境でPHP_EOLが出力する改行コードをASCIIコードで確認してみる

改行コードを標準出力しても分かりずらいので、PHPのord()関数をつかいASCIIコードに変換して出力してみたいと思う。(※Mac環境がないので、Linux、Windows環境のみで)

以下にASCIIコード一覧の抜粋があるが、そこにある太字のASCIIコードが返ってくることが予想できる。ここで問題なのがWindows環境であるCRLF(\r\n)である。そもそもCRLFというASCII文字は存在していない。いったいどのような値がかえるのか?

ASCIIコード

コマンドラインでスクリプトを実行した結果

下記、改行コードを生成するコマンドをOS環境を変えてコマンドラインから実行していってみる。またOS環境における差異を明確化させるために、strlen()関数で出力される改行コードの長さも測った。下記が結果内容である。

php -r 'echo ord(PHP_EOL);'  (改行コードをASCIIコードで出力)

Linux環境で実行した際の結果値:10
Windows環境で実行した際の結果値:13

php -r 'echo strlen(PHP_EOL);'  (改行コードの長さを出力)
Linux環境で実行した際の結果値:1
Windows環境で実行した際の結果値:2

これらの結果から、疑問であったWindows環境における改行コードのASCIIコードの値は13と出力された。ASCIIコード一覧と照らしてみると、13 = CR = \rになる。このような値が返ってきたのはCRLF自身が、ASCIIコードのCR(\r)とLF(\n)を並べて表現しているからである。現にstrlen()関数で長さを取得するとWindows環境(CRLF)では2という値がかえり、2byteの文字列であることがわかる。そのためord()関数にPHP_EOL定数がPHP内部でOS環境を判定して、\r\nをセットしても、先頭1byteの\r = 13が返ってくるというわけである。これはord()関数の異常というわけでなく、正常動作であった。

]]>
http://blog.fukaoi.org/2010/01/04/php_eolhttp://blog.fukaoi.org/2010/01/04/php_eolSun, 03 Jan 2010 15:37:12 GMT
<![CDATA[MySQLのUDF関数を作った - アルファベットの文字列をひらがな順にソートする]]> MySQLのUDF(ユーザ定義関数)機能を使い、アルファベットの文字列をひらがな順にソートするオリジナル関数を作成してみた。このUDF関数はアルファベットの文字列データが含まれるMySQLのデータカラムに対して、日本語固有のひらがな順にソートを可能にする。漢字、ひらがなで登録された2byte文字列データであれば、MySQL自身が内部で持っている辞書データによりひらがなソートが可能なのだが、アルファベットの文字列データをソートしようとした場合は英語固有のアルファベット順ソートになってしまう。 そこで今回作成した関数(alpha_to_hiragana関数と命名)内部において、アルファベットの文字列に対して、ひらがな順にソートできるように、ひらがな順にデータの重みづけの内部変換を行い、ひらがなソートを実現した。alpha_to_hiragana関数の開発ポイントを今回記述してみたいと思う。

そもそものUDF関数の実装方法を知りたい方は、下記の関連リンク先サイトがわかりやすいのでそちらを見てほしい

アルファベット順ソートとひらがな順ソートの違い

まずは、通常のソート(アルファベット順)とalpha_to_hiragana関数(ひらがな順)を 使ったソートの処理結果の違いを見てもらいたい

無秩序にならんだサンプルデータ(テーブル名:actor)

+-------------------+
| name              |
+-------------------+
| Saitou akira      |(さ行)(頭文字:S)
| L7ucky Isono      |(ら行)(頭文字:L)
| aida masao        |(あ行)(頭文字:A)
| YamadaShougo      |(や行)(頭文字:Y)
| AKAGI haruka      |(あ行)(頭文字:A)
| Mike Davis        |(ま行)(頭文字:M)
| Tanaka123         |(た行)(頭文字:T)
| hamadamayui       |(は行)(頭文字:H)
| goto satoru       |(か行)(頭文字:G)
| WANIDA KENJI      |(わ行)(頭文字:W)
| N-A-G-U-R-A-R-Y-O |(な行)(頭文字:N)
+-------------------+

アルファベットソート後(A-B-C-D-E...順に並びかえられた)

使用したクエリ:SELECT name FROM actor ORDER BY name ASC
+-------------------+
| name              |
+-------------------+
| aida masao        |(頭文字:A)
| AKAGI haruka      |(頭文字:A)
| goto satoru       |(頭文字:G)
| hamadamayui       |(頭文字:H)
| L7ucky Isono      |(頭文字:L)
| Mike Davis        |(頭文字:M)
| N-A-G-U-R-A-R-Y-O |(頭文字:N)
| Saitou akira      |(頭文字:S)
| Tanaka123         |(頭文字:T)
| WANIDA KENJI      |(頭文字:W)
| YamadaShougo      |(頭文字:Y)
+-------------------+

ひらがなソート後(あ-か-さ-た-な...順に並びかえられた)

使用したクエリ:SELECT name FROM actor ORDER BY alpha_to_hiragana(name) ASC
+-------------------+
| name              |
+-------------------+
| aida masao        |(あ行)
| AKAGI haruka      |(あ行)
| goto satoru       |(か行)
| Saitou akira      |(さ行)
| Tanaka123         |(た行)
| N-A-G-U-R-A-R-Y-O |(な行)
| hamadamayui       |(は行)
| Mike Davis        |(ま行)
| YamadaShougo      |(や行)
| L7ucky Isono      |(ら行)
| WANIDA KENJI      |(わ行)
+-------------------+

アルファベット文字列をひらがな順への重みづけ方法

YamadaTarou(やまだたろう)、TanakaMasao(たなかまさお)をサンプルデータとして説明する

  1. 文字列を小文字に変換する
  2. YamadaTarou = yamadatarou
    TanakaMasao = tanakamasao
    
  3. ローマ字読みから重みづけるための変換テーブルを作成する
  4. 以下が変換テーブルである、母音のあ行は1文字全てにアルファベットを割り当て、 子音はまとめて1文字のアルファベットを割りあてる

    あ行:あ(a)、い(b)、 う(c)、 え(d)、 お(e)
    か行:(f)
    さ行:(g)
    た行:(h)
    な行:(i)
    は行:(j)
    ま行:(k)
    や行:(l)
    ら行:(m)
    わ行:(n)
    
  5. 変換テーブルを用いて、サンプルデータを変換する
  6. y a m a d a t a r o u
    ↓
    l a k a h a h a m e c
    
    t a n a k a m a s a o
    ↓
    h a i a f a k a g a e
    
  7. 変換後のアルファベット文字列で比較を行う
  8. y a m a d a t a r o u
    
    [昇順]  h a i a f a k a g a e  

    このようにアルファベット文字列であっても、ひらがな順にソートを実現できるようになる。実際のソートに関しては、alpha_to_hiragana関数内で実装するのではなく、MySQLのORDER BY句で利用してひらがなソートを実現している。

興味をもたれた方は

SOURCEFORGE.JPのMySQLibプロジェクト内にUPしているので 下記よりアクセスしていただけると幸いである

  • alpha_to_hiragana関数の詳細な使用方法はこちら
  • alpha_to_hiragana関数のソースコードのダウンロードはこちら
]]>
http://blog.fukaoi.org/2009/06/14/mysql_udf_alpha_to_hiraganahttp://blog.fukaoi.org/2009/06/14/mysql_udf_alpha_to_hiraganaSat, 13 Jun 2009 15:23:49 GMT
<![CDATA[放置したSSHクライアントを切断されないようにしたい(on FreeBSD) ]]> 私は主に開発作業をFreeBSD上で行っている。時にはサーバにログインして作業を行うケースもあり、SSHクライアント(OpenSSH_4.5)でサーバでアクセスする。ちょっと1~2時間休憩のために、席をはずしどってくると接続が切れてることはないだろうか? 原因の多くはルーター、サーバーで接続コネクションを切断しているからだと思もわれる。 Windows専用、SSHクライアントソフトputtyにはkeepalive機能がついていて、指定した時間に定期的に、 nullパケットをログインしているサーバに送信することにより、切断を防げるのだが、 OpenSSHクライアントには標準で同様な機能はついてないようだ。 色々と調べてみたところ、OpenSSH用のpatchを作成されている方がいたので、利用させてもらうことにした。そのときの導入方法である

今回の導入するパッチの作成者は、東北大学の後藤 英昭博士である、感謝!

導入方法

  1. OpenSSHを最新版(ver4.2 = 4.5)にする
  2. cd /usr/ports/security/openssh-portable
    

    (注意)上記OpenSSHのバージョンはこの記事を書いていた際の最新バージョンである

  3. patchのダウンロード
  4. wget <a href="http://www.sc.isc.tohoku.ac.jp/~hgot/sources/">http://www.sc.isc.tohoku.ac.jp/~hgot/sources/</a>
                           openssh-4.4p1-watchdog.patch.tgz
    
  5. ダウンロードしたパッチをあて、パケット送信機能を実装する
  6. make patch
    
  7. インストール
  8. make install
    

    インストールが成功すると、/usr/local/bin/ssh にSSHクライアントが置かれる

  9. パケット送信する間隔を設定ファイルに書き込む
  10. /etc/ssh/ssh_config に Heartbeat 120
    

    上記設定は2分間隔でパケットを送るという設定になる

  11. インストールしたpatch済みのSSHクライアンと置き換えを行う
  12. CShellを使っているので、aliasを設定し置き換える
    /etc/csh.cshrcに alias ssh /usr/local/bin/ssh を追加する
    

    標準のSSHクライアントは/usr/bin/ssh にあるので、上書き置き換えでも可能だと思う

]]>
http://blog.fukaoi.org/2009/06/12/freebsd-sshhttp://blog.fukaoi.org/2009/06/12/freebsd-sshFri, 12 Jun 2009 14:14:10 GMT
<![CDATA[IT業界 プロジェクトマネージャーに求められるスキルとは]]> プロジェクトマネージャーに必要なスキルとは何が思い浮かぶだろうか?たぶんそれは、業界・業種において求められるスキルは異なってくると思うが、ここではIT業界における、プロジェクトマネージャーに必要なスキルについて述べてみよう

予算管理能力

会社に利益を出すことは当たり前!

ビジネスマンである以上、会社から給料という労働の対価をもらっているはずプログラマー・システムエンジニアであっても「このプロジェクト規模は1億円である、我が社にとってビッグプロジェクトである、なんとしても納期に間に合わせるべく一致団結しよう!」という目標をたてれば、普通の人間であればモチベーションが上がり、その目標に邁進するはずである。一方プロジェクトマネージャーの立場であれば、収支・収益を意識することは当然であり最小の投資で最大限の利益をあげるという意識でプロジェクトを実行すれば、必然的に会社に利益をもたらすことができる。エンジニアリングよりのプロジェクトマネージャーなら、コストを抑えるところから、セールスよりのプロジェクトマネージャーなら、売り上げ規模を上げる所から入ると、時間とともに身につけられるスキルである

コミュニケーション能力

ボトムアップも大事

IT業界でなくても、常に言われる能力の一つであり、社会人として生きていく上でのスタンダードな能力の1つである。ここでいうコミュニケーション能力は相手に思いを伝えるだけでなく、相手から意見を吸い上げる能力も指している。自分の意見をゴリ通すことを、コミュニケーション能力が高い人間だと思っている人が居るかもしれないがそうではなく、相手から意見を吸い上げる方が重要視される。プロジェクトマネージャーという最高権限者である人(※プロジェクトマネージャーに任命される人であれば、それなりの業界経験をつんでいるはずなので年齢もそれなりに重ねているであろう)の意見が通りやすいのは至極当然。現場で発生している問題点などが開発メンバより吸い上げる事が不十分であれば、プロジェクトを成功に導く事は難しい。なのでトップダウンで指示を出すフローだけでなく、ボトムアップで自由に意見を言いやすい環境を用意することも、コミュニケーション能力の1つである

スケジュール管理能力

基本中の基本

スケジュールに始まり、スケジュールに終わるというように、プロジェクト遂行上、この能力が欠落しているマネージャがいるとしたら致命的である。要件を満たした成果物を納期どおりに、リリースすることは、プロジェクトとして最低の条件であり、ビジネスであれば、顧客との契約条件であるはず。納期にリリースするためには、工程のプロセス(タスク)が重要であり、いかにそのプロセスをマイルストーンどおりに完了していなければ、納期日にリリースすることが厳しくなる。そもそもこの基本中の基本ができないプロジェクトマネージャーなる人物が存在そもそもしているかというと、以外と存在しているものだ。さすがに納期日は覚えているのだが、各工程プロセスの管理を放置しておいて、いざ納期日に近くなると慌てだし、騒ぎ出す人だ。そもそも自分自身が与えられた役割をこなしていないのに、プロジェクトメンバーに当り散らすのは、言語道断である

技術力

ITリテラシー+テクニカルスキル

IT業界に身を置く以上、ITリテラシーはプロジェクトマネージャーにはなくてはならない必要なスキルである。というか無い人は仕事になってないと思う。難しいところではあるが、ITリテラシー+テクニカルスキルをもったプロジェクトマネージャーは鬼に金棒である。会社内外の人々とコミュニケーションを取る可能性があるプロジェクトマネージャーはいきなり、テクニカル質問をされ、その場で回答を求められるケースは意外に多い。要件定義・概要設計時にテクニカルスキルが高ければ、プロジェクトメンバーに適切なテクニカルアドバイスを行う事ができメンバーからの信頼度も高まり、今後のプロジェクト運営がスムーズになる可能性がある。それではテクニカルスキルが弱いプロジェクトマネージャーはどうすべきなのだろうか?プロジェクト内における、テクニカルスキルが高いプログラマー・システムエンジニアを技術責任者として抜擢し、プロジェクト内の右腕的存在として自分のウィークポイントを補ってもらおう。ここで気をつけなければならないのは、他のメンバーと同じように開発メンバとしてではなく、システム全体の設計であったり、タスクに遅れが生じたメンバーの、スーパーサブとして考える事である。プロジェクト内の技術責任者に引き上げても、タスク開発に100%従事していたのでは、広い視線でのシステムのアーキテクチャ全体を見渡すことができなくなってしまう。テクニカルスキルが高ければ、年齢・地位に関係なく任命することで、ある意味公平なジャッジになり、他のメンバーも納得しやすい。なによりも技術責任者に任命した当人も、モチベーションを高めて従事してくれるだろう

リスク回避能力(リスクヘッジ)

その他の能力が高くても

この能力がプロジェクト成功に導くために1番必要な影の能力であり、1番習得が難しい。仮定のイメージであるが、他の能力が欠けていてどうしようもないプロジェクトマネージャーが存在したとしよう。唯一リスク回避能力が秀でていた場合(※現実的に他の能力が欠如していて、そもそもプロジェクトマネージャーなるもに任命はされないのだが)、この能力だけで無事プロジェクトをリリースする事をやってのけれると思う。プロジェクトが失敗する要因であるスケジュール遅延、メンバの脱落、仕様ミスなど数を上げるときりがないが、これらの要因を事前にキャッチし、避けながらながら進んでいきリリースまではやってのけそうである。実際にそういうプロジェクトマネージャーを見たことがある。ただ、さすがにその後のシステム運用フェーズに入ると、開発フェーズ以上に、予見不可能な事態が発生しやすいので、この能力だけでは切り抜けていることは不可能と言える。リスク回避能力というものは、生まれもっているセンスのようなもので、血液型でいうならばA型の人が多かったりする。A型の人が多いのは、細かい性格であったり、根っからの心配性な性格がリスク回避能力を高めている。心配だから、各タスクに関しては、色々と深く考える、考えるので、不明・不安要素が早期に発見できる。逆に0型の人は楽観主義的な人が多いので深く熟考せずに物事をすすめがちなので、不明・不安要素が後手後手にまわるケースが多かった(※あくまでも自分の経験上であって、血液型で、その人を差別しているわけではない)。ここまで書いてしまうと、この能力自体、身に着けることは諦めるしかないようだが、楽観主義者であった自分が努力して多少なりにも身に着けた方法とは、成功パターンだけでなく失敗パターンも1セットとして、ミクロではタスクレイヤーから、マクロではスケジュール全体に及ぶまで、時間があれば考えるようにした。仕事帰り電車の中とかで「このタイミングでお客さんから、大幅な仕様追加が要望されたらどうしよう?」、「システムエンジニアである○○君が倒れたらどうなるだろう?」などと。これはA型の人に多い不安な状態に自ら陥るようにする、妄想自虐プレイと呼んでいる。いわゆる、プロジェクトリスクシミュレーションで、こうすることにより不安要素が可視化できリスクの早期発見、解決につなげることができる

最後に

5つの能力を列挙したが、書いている途中に思ったことが、自分を含めこれらの能力を満たしているプロジェクトマネージャーに出会ったことがないという事だ。5つは無理だとしても、3つ持っている人でも中々出会う機会がない。逆に実績・能力もないくせに、プロジェクトマネージャーという肩書きを名乗る人が多いということだろう。業界を含め、外見だけでなく、中身もともなっていかなければ、本当に虚無の世界になってしまうだろう

]]>
http://blog.fukaoi.org/2009/06/10/it_manager_skillhttp://blog.fukaoi.org/2009/06/10/it_manager_skillWed, 10 Jun 2009 00:00:00 GMT
<![CDATA[MySQLでDATETIME型のデータを高速に検索する方法]]> MySQLでDATETIME型のカラムを追加して、YYYY-MM-DD HH:MM:SSの形式でデータを保存することはよくあるケースだと 思う。DATETIME型のカラムをWHERE以降の条件句に指定して、年月日時でデータの絞込みを行い目的のデータを抽出 するという時に、より高速にデータを取得できないかと考えてみた。

※注意

ここに記述されたデータ内容については、構造的な問題により算出された可能性があり、再検証が必要ということになりました。折を見て再検証して報告したいと思います。指摘して頂いた方々ありがとうございます。

追記:2011/08/18

前提条件

  • テストはクエリを10回発行した平均値より比較する
  • テストパターンとして、INDEX有り・無しの2パターンで比較を行う
  • テストデータは100万レコード用意する
  • INT型のデータの作成にはMySQLのUNIX_TIMESTAMP()関数を使う
  • Query CacheはOFFにして行う


  • 今回テストで使用したサーバのスペック

    OS :CentOS release 4.5 (Final)
    DB :MySQL5.0.77
    CPU:Core 2 Duo 1.86GHz 
    Mem:1GByte
    

    今回テストで使用したテーブルスキーマ、データ構造

    (DATETIME型のデータ構造 テーブル名:test_datetime)
    
    mysql desc test_datetime;
    +-----------+----------+------+-----+---------+----------------+
    | Field     | Type     | Null | Key | Default | Extra          |
    +-----------+----------+------+-----+---------+----------------+
    | id        | int(11)  | NO   | PRI | NULL    | auto_increment |
    | datetime  | datetime | NO   | MUL | NULL    |                |
    +-----------+----------+------+-----+---------+----------------+
    +----+---------------------+
    | id | datetime            |
    +----+---------------------+
    |  1 | 2008-08-23 19:03:26 |
    |  2 | 2008-08-23 19:03:26 |
    |  3 | 2008-08-23 19:03:26 |
    |  4 | 2008-08-23 19:03:26 |
    |  5 | 2008-08-23 19:03:26 |
    |  6 | 2008-08-23 19:03:26 |
    |  7 | 2008-08-23 19:03:26 |
    |  8 | 2008-08-23 19:03:26 |
    |  9 | 2008-08-23 19:03:26 |
    | 10 | 2008-08-23 19:03:26 | 
    | ・ |         ・         |
    | ・ |         ・         |
    | ・ |         ・         |
    +----+---------------------+
    
    ================================================
    
    (INT型のデータ構造 テーブル名:test_unixtime)
    
    mysql desc test_unixtime;
    +-----------+----------+------+-----+---------+----------------+
    | Field     | Type     | Null | Key | Default | Extra          |
    +-----------+----------+------+-----+---------+----------------+
    | id        | int(11)  | NO   | PRI | NULL    | auto_increment |
    | unixtime  | int(10)  | NO   |     | NULL    |                |
    +-----------+----------+------+-----+---------+----------------+
    +----+------------+
    | id | unixtime   |
    +----+------------+
    |  1 | 1219485806 |
    |  2 | 1219485806 |
    |  3 | 1219485806 |
    |  4 | 1219485806 |
    |  5 | 1219485806 |
    |  6 | 1219485806 |
    |  7 | 1219485806 |
    |  8 | 1219485806 |
    |  9 | 1219485806 |
    | 10 | 1219485806 |
    | ・ |     ・     |
    | ・ |     ・     |
    | ・ |     ・     |
    +----+------------+
    

    それではテスト開始・・・


    INDEX無しの状態でのベンチマークテスト

    比較演算子を使ったクエリ

    (DATETIME型の場合)

    SELECT * FROM test_datetime WHERE datetime = '2008-08-23 19:10:11';
    
    処理結果(2.99 sec)
    

    (INT型の場合)

    SELECT * FROM test_unixtime WHERE unixtime = 1219486211;
    
    処理結果(0.74 sec)
    

    圧倒的にINT型にしたクエリ条件のほうがデータ取得の処理スピードが4倍高速の結果になった。
    (※年月日時を=(イコール、等値)を使った検索条件句を使うケースは実際のシステム設計の中ではあまりないようなケースだと思う。実際によく使われるのはやはり、次に紹介したBETWEENを使った期間指定検索であろう)

    BETWEENを使った期間指定を使ったクエリ

    (DATETIME型の場合)

    SELECT * FROM test_datetime WHERE datetime 
                  BETWEEN '2008-08-23 19:00:00' AND '2008-08-23 19:05:00';
    
    処理結果(5.20 sec)
    

    (INT型の場合)

    SELECT * FROM test_unixtime WHERE unixtime 
                  BETWEEN 1219485600 AND 1219485900;
    
    処理結果(1.64 sec)
    

    BETWEENを使った期間指定クエリでもINT型の方がデータ取得の処理スピードが3倍高速の結果である

    INDEX有りの状態でのベンチマークテスト

    比較演算子を使ったクエリ

    (DATETIME型の場合)

    SELECT * FROM test_datetime WHERE datetime = '2008-08-23 19:10:11';
    
    処理結果(2.99 sec)
    

    (INT型の場合)

    SELECT * FROM test_unixtime WHERE unixtime = 1219486211;
    
    処理結果(0.02 sec)
    

    INT型の方がデータ取得の処理スピードが150倍高速の圧倒的効果である。INT型はINDEXを最適に使い目的の結果を返してくれるためここまでのパフォーマンス結果がでたものと思われる。面白い副産物結果として、DATETIME型ではINDEX有り・無しかかわらず処理結果値が同じということで、DATETIME型はINDEXの恩恵を受ける事があまりできないのである (※ただしINDEX無しの結果内容の所でも記載したが=(イコール、等値)を使った検索条件句を使うケースはあまりないかもしれない・・・)

    期間指定を使ったクエリ

    (DATETIME型の場合)

    SELECT * FROM test_datetime WHERE datetime 
                  BETWEEN '2008-08-23 19:00:00' AND '2008-08-23 19:05:00';
    
    処理結果(5.12 sec)
    

    (INT型の場合)

    SELECT * FROM test_unixtime WHERE unixtime
                  BETWEEN 1219485600 AND 1219485900;
    
    処理結果(1.64 sec)
    

    DATETIME型、INT型もINDEXを設定しても、期間検索というB-tree型のINDEXの恩恵の受けにくい検索条件句であるため、INDEXが無い場合と比較してそこまで処理スピードの向上には影響を及ぼさなかったが、ここでもINT型の方がデータ取得の処理スピードが3倍高速の結果にはなった

    結論

    DATETIME型で保存するのではなく、UNIX_TIMESTAMP関数を使いINT型にコンバートした形式で保存することにより、DATETIME型より3倍~4倍高速化が実現できた。その際に注意しなければならないのが、INT型の年月日時を指定したカラムを生成しても実際にクエリを生成する際に、'1219485900'(シングルクォート)して文字列(STRING型)として指定しないこと。うっかりこのように指定してしまうと、カラムがINT型であっても処理スピードが遅くなってしまう。ただし、このINT型で年月日時データを保存する方法にもデメリットがあって、UNIX_TIMESTAMPなので直感的に見て人間が理解しずらいのである。そのためサーバのコマンドラインよりMySQL Clientを使いダイレクトにアクセスしデータを取得する必要があるケース(exp. 人間の手による緊急的な集計オペレーション)では、YYYY-MM-DD HH:MM:SSの形式にコンバートする必要があるかと思う。その場合は人手間だが以下のようにFROM_UNIXTIME関数を使用して結果を出力してほしい

    SELECT * FROM_UNIXTIME(unixtime) FROM test_unixtime 
    	WHERE unixtime BETWEEN 1219485600 AND 1219485900;
    
    



    ]]>
    http://blog.fukaoi.org/2009/06/10/mysql_datetimehttp://blog.fukaoi.org/2009/06/10/mysql_datetimeTue, 09 Jun 2009 15:00:00 GMT
    <![CDATA[知って損はない! Webアプリケーションのリリース時に気をつける事]]> Webアプリケーションのリリース時は大変緊張するもので、それはWeb(インターネット)に公開することイコール、世界中の誰にでもアクセスが可能になるためである。といってもWeb以外でもお客さんに納品するタイミングというのは平常時の精神状態ではないため、普段ならおきないミスも発生してしまうリスクがある。リリースするまでのテスト手法などは記述するとキリがないため省いて主に、私の経験上いかにリリース時にトラブルなく、リリースできるかリリース前日、当日、後日の心構え的なことが中心にまとめてみた

    3日前迄にやること

    リリース手順書の作成

    リリース時にやるべきこと(作業内容)を、記述したドキュメントを作成するのとしないのでは、雲泥の結果を生み出す。何回もいうようにリリース時にはどんなに肝っ玉が据わった人でも平常時以上の判断・能力を発揮することはできないので、頭をつかわずとも、リリース作業ができるように前もって手順をまとめておくためである。誰(Who)何(What)何時(When)どう(How)すべきなのかを明確化することが大切である。後は、開発環境では問題なく動作しているプログラムも、本番環境だと動作しない(※ミドルウェアのバージョンや、サーバの設定が異なっている事により発生しやすい)ケースがある。

    ケースによっては巻き戻しフローも明記しておくこともある。すでにサービスを開始しており、リニューアルや、機能追加の場合では、サービスを継続すること(※サービスを止めない)が第1だと思う、前のバージョンに巻き戻す事を巻き戻し作業と呼びます。作業内容が決まれば、作業完了時間が決まる。その時間完了時間から1時間以上オーバした場合、リリース作業を急遽取りやめ、巻き戻し作業に入るわけである

    (参考例)
    
      [12/01 11:00~12:00]
      AさんがプログラムをサーバにUPする
      UPするソース(○○.php、□□.php、△△.php)
    
      [12/01 12:00~13:00]
      BさんがApacheの設定を変更する
      変更するファイル(httpd.confのline:412)
      ○注意箇所
        httpd.confのバックアップ(httpd.conf_bak)を必ずとっておく
       変更後、apacheの再起動を行う
    
    
      [12/01 13:00~13:10]
      BさんがIP制限を一部許可する
      変更するファイル(httpd.confのline:102)
      ○注意箇所
       念のため非許可IPの箇所よりアクセスし、制限が
        かかっているか確認を行う
       変更後、apacheの再起動を行う
    
    
      [12/01 13:10~14:00]
      プロジェクト関係者全員で確認を行う
      ○主に確認する箇所
       Aさん(入会登録)
       Bさん(購入、決済)
       Cさん(商品の表示)
    
      [12/01 14:00~]
      リリース無事完了
    
      [12/01 15:00を超えた場合]
      ●巻き戻しフロー
        Bさんがバックアップより前のバージョンのXXプログラムを
        上書きし元に戻す
        ・
        ・
    
    リリース手順書のレビュー

    作成したリリース手順書を関係者でレビューすることによって、作業漏れの確認になる。当日の気持ちで確認しあうことでシミレーションにもなる。ここでのポイントはプロジェクトリーダの人はいかに、本番当日と同じ緊張感をメンバに伝えられるかどうかである

    1日前迄にやること

    プログラム修正の締切り

    スケジュールがタイトなプロジェクトだと、修正作業をギリギリまでおこなわざるをえない。だが大小あがってくる修正箇所を対応しているとキリがなく、いつまでたってもリリースができない状態になってしまう(※要は、キリがないということである)。それはお客さん自社ともに困るはずなので、あらかじめお客さんに修正受付けの時間を決めておき、了承をもらっておく必要がある。

     
    (参考例)
    
    『○月○日の△時△分までに確認できた、
      修正箇所については優先的に直しますが、
      それ以外のものに関しては、リリース後対応させてください』
    

    前もって出勤時間をリリース時に合わせる

    Webアプリケーションのリリース時間は、世の中の人のアクセスが少ない時間帯を指定される場合が多い。(※新規オープンするサイトでは時間の指定はないが、既存のサイトをリニューアル、機能追加の場合)例えば、リリース時間がAM3:00~の場合、前の日に早く寝て、早く起きるという、平常時とは異なるサイクルをとらなければならない。そうなると体調を崩しやすい寝不足になりやすいです。体調の状態はリリース作業に大きな影響を及ぼすので、以下のように前日から体内リズムを整える手法もある

    (参考例)
    
      [12/02 03:00~08:00にリリースとした場合]
     
     通常は09:00に出勤していた時間帯を
      前日(12/01)の出勤時間を02:00~に変更し
     深夜時間帯に体内リズムを整える
    

    例のように03:00~のリリースだと前日より寝ずにリリースを迎えるという方法も可能。だが私の過去経験上、通常は22時位に仕事を終えていたリズムからさらに時間を延長することにより、集中力がもたない。尚、途中で仮眠を取るという方法もありだが後、数時間でリリースを迎えるという気持ちが高ぶってあまり寝れない経験があった。

    前日から体内リズムをリリース時間に合わせて調整するという方法は、あくまでも私にとってベストであっただけだが、必ずしても万人に当てはまる手法かどうかはわからない。要は睡眠時間をきっちりとれ集中力が高められるかどうかがポイントだと思われる

    リリース当日にやること

    いかに平常心を保てれるか

    前半にも書いたが、リリース時は非常に緊張するものだ。いつもとは違う自分がそこに存在しているといってよいだろう。特にネガティブシンキング傾向にある人がとくに緊張すると思われる。そこで心がけていただきたいのは、無事リリースしたときの成功イメージを頭の中で妄想してみる。それさえも緊張でイメージできない場合は、≪ 絶対に無事リリースする ≫≪ 自分ならできる ≫と何回も頭の中でつぶやき、自己暗示にかけることも効果がある。後は、緊張状態にあると呼吸が浅くなりがちなので、深く深呼吸を3回やるだけでも、緊張がほぐれる。 可能ならばリリース作業前に、プロジェクトメンバ全員で実践できればベストである

    リリース完了後1週間以内にやること

    特別監視状態におく

    無事リリースが完了した場合、これで終わりなのだが、Webアプリケーションの場合、まだ気を抜いてはいけない。ここまではあくまでもインターネットの大海原に1隻の船で港から出向したにすぎない。出向中で一番怖いのが、大波だ。大波=アクセス集中が発生した場合、テスト環境で負荷テストを行って、サーバ、Webアプリケーションのアクセス処理スペックを把握していたとしていても、それ以上のアクセスが集中砲火した場合、サーバ、Webアプリケーションはあっという間にサービス不能に陥いる。そのために、この段階では、監視ツール(※MRTG、nagios...)などでアクセス処理状況に問題がないか毎日、定点監視を行うべきである。

    また開発環境では発見できなかった、Bugが潜んでいる可能性もあるので、時間の経過とともに大きな問題になるまえに、DB、ログファイルに吐き出されたデータも定点確認することによりBugの早期発見が可能になる

    リリース完了後2週間以内にやること

    反省会を開く

    この作業までたどりつけば、リリース作業も無事完了し、安定してサービスが展開されていると判断してよいだろう。ただし、反省すべき点というのは必ずなんらかあるはずである。それはあくまでもたまたま成功しただけかもしれない。次回のリリース作業では2度を起こさない、他の作業者が起こさない意味をこめて、反省会で情報を共有し、リリース作業の品質を高めるべきである

    ]]>
    http://blog.fukaoi.org/2009/02/23/web-releasehttp://blog.fukaoi.org/2009/02/23/web-releaseMon, 23 Feb 2009 01:00:00 GMT
    <![CDATA[Windows Vistaのリモートデスクトップをマルチセッションに対応させる]]> Windows Vista Business、Ultimate、Enterpriseにはリモートデスクトップサーバ機能を実装しているので、別のPC(リモートデスクトップクライアント)から遠隔ログインでき、Vista上で作業を行うことができる。でも、マルチセッションにVistaのどのバージョンも対応していないため、直接ログインしている状態で、遠隔よりリモートデスクトップログインを行うとすると、直接ログインしている人はログアウトしないといけない。Vistaはデフォルト状態だと、シングルセッションにしか対応していない。せっかくVista用にハイスペックマシンを購入したのに、同時に1人しか利用できないのは寂しい話だ。そうであれば、自宅のVistaをマルチセッションサーバにし試してみよう!

    対象はService Pack 1 があたっているVista Business、Ultimate、EnterpriseのOS

    (※そもそもリモートデスクトップは何ぞや?という方はネットで調べてみよう)

    作業前に準備する事

    バイナリエディタの用意

    システムファイルのバイナリデータを書き換えるために、バイナリエディタが必要。お勧めは無料で使いやすいStirling である

    リモートデスクトップのシステムファイルをバックアップを取る

    C:\Windows\System32\termsrv.dllファイルをバックアップ

    (※termsrv.dllがリモートデスクトップサーバ機能)

    実作業

    リモートデスクトップのシステムファイルをデスクトップにCOPYする

    termsrv.dllはシステムファイルの為、C:\Windows\System32\termsrv.dllを直接バイナリエディタで開こうとしても権限をあたえられていないため、読み込めない。そこでデスクトップなどにCOPYして移動しておく

    バイナリエディタで指定の箇所を書き換える

    Stirlingを起動させファイル(F) = 開く(O)でtermsrv.dllを読み込む

    stirling


    検索・移動(S) = 指定アドレスへ移動(J)を選すると、16進数でアドレスを入力するボックスがでるので以下のアドレスを入力して置換していく

    [検索するアドレス]: [置換前の値] = [置換する値]
    0005FDF2: 74 = EB
    00064FD7: 8B = B8
    00064FD8: 81 = 00
    00064FD9: 38 = 01
    00064FDA: 06 = 00
    00064FDC: 00 = 90
    00064FDD: 39 = 89
    00064FDF: 3C = 38
    00064FE3: 75 = EB
    000701BA: 01 = 00
    

    修正したリモートデスクトップのシステムファイルを元のフォルダに上書きする

    指定箇所のデータを修正した、termsrv.dllを元のフォルダに上書きしようとすると、権限がない警告により上書きができない場合がある。そのときは、Administratorでログインしtermsrv.dllにプロパティ = セキュリティ = 編集よりアクセス許可を追加してやると上書きができるようになる

    確認してみよう

    リモートデスクトップクライアントよりアクセスしてみる

    Windows XP Profesionalなどのクライアントからモートデスクトップ接続してみると、今までシングルセッションでしかアクセスできなかった、Vistaマシンに対して、マルチセッションでログインできるようにあり画期的である

    ]]>
    http://blog.fukaoi.org/2008/06/02/windows-vista-remotedesktophttp://blog.fukaoi.org/2008/06/02/windows-vista-remotedesktopMon, 02 Jun 2008 01:00:00 GMT
    <![CDATA[以外と知らないCookieの仕様]]> 何げなく仕様しているCookieだが、以外と仕様を正しく理解している人は少かったりする (わたしの周りだけだったりするかもしれないが..) 良い機会なので、RFCを読みかえしてみる、以下がRFCより抜粋した内容である

    • クライアントが一度に溜めておくことができるCookieの数には限界が存在するこれは、クライアントが受け取り保存するために用意されるべきのCookie最小数の仕様である
    • Cookieの数は、トータルで300個まで
    • 1つのCookieにつき4KBまでで、name と OPAQUE_STRING は、4KBまでの形式に結合される
    • サーバもしくはドメインごとに、クッキーの数は20個まで

    RFCではCookieの最大サイズは4Kbyteという制約を設けているが、世の中の万物限度というものが存在しているようにCookieのサイズ指定があるのも例外ではないわけである。今回は4Kbyteという制限に注目し、本当に4Kbyteという制約は守られているのか、クライアント、サーバ側でテストをおこなってみた。

    テスト内容

    [クライアント側テスト環境]

    OSパターン1 - Linux(CentOS4.4)
    OSパターン2 - Windows(XP)
    ブラウザパターン1 - Firefox2.0(Linux版)
    ブラウザパターン2 - Firefox2.0(Windows版)
    

    [サーバ側テスト環境]

    Cookie発行プログラム使用言語 - PHP5.2
    Cookieの発行条件 - ブラウザ経由でアクセスした際に無条件でCookieを発行する
    

    [Cookie発行PHPプログラム内容]

    <?php
    
     // 最大4092byte(4K)のデータを生成する
    
    for ($i = 0; $i 

    テスト結果

    Firefoxでは最大4092byteのCookieを取得する亊が確認できた。Cookieは名前も4Kの制限に含めるため

    名前=値
    4092=11111111111111111111111111111111111........1111
    ↑         ↑ 
    (4byte)    (4092byte)
    

    4 + 4092 = 4096 すなわち 4Kbyte となります。きっちり1024 X 4された4096Kbyteを閾値としているようだ。 サーバ側のCookie発行プログラムのfor文のループ階数を4092から4093に 変更した場合はFirefox側で受け取ってくれなかった。 最大値を超えたという判断をされるようだ。 4092byte以下のデータサイズであれば、当然問題なくCookie領域にセットができる。 このテスト結果よりFirefoxはLinux版、Windows版ともにRFCに準拠しているいえるのではないだろうか。流石だ!

    ]]>
    http://blog.fukaoi.org/2007/06/20/cookiehttp://blog.fukaoi.org/2007/06/20/cookieWed, 20 Jun 2007 01:00:00 GMT