まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2014/10

この「まだプログラマーですが何か?」というブログを始めて1年ちょっとになります。

以前は半分業務半分趣味みたいな感じで、会社内の外部向けシステムのブログアプリを使っていたのですが、転職を機に100%趣味で作りなおしました。 で、その際にちょうど「ブログに好きな URL を付けることができる」というキャンペーンをやっていたライブドアブログを選びました(なお、選んだ URL は dotnsf.blog.jp です)。

内容は技術紹介だったり、備忘録だったり、マンホールだったり(?)好き勝手に書いています。でもここ最近になってアクセス数も上がっていて、また色々な所からリンクされていることも分かっています。そういうリンク元を辿るのも最近の楽しみの1つになっています(笑)。


ところで、ライブドアのブログにはアクセス数に応じた5段階の「レベル」制度があります。レベルが高いほどアクセス数が多いグループとなり、同じレベルグループの中でランキングを競ったりしています:
ライブドアブログ レベル別ランキング


 始めた当初はレベル1からスタートしましたが、つい先日「レベル4」にまで到達できました:
 2014103001

おー、ここまで来たか。ここまで来るとやはりレベル5を目指したい所ではありますが、レベル5くらいになると、一度は耳にしたことのあるまとめサイトなども含まれているレベルです。さすがにかなり高い壁だよなあ・・・ でもまあ目指す分にはいい目標になります。

そして、ここまでこのブログが成長できたのも、ひとえに見て&アクセス数増加に貢献いただいている皆様のおかげでございます。見てくださっているだけでも嬉しいのですが、このブログが少しでもお役に立っているのであれば、エンジニア冥利に尽きる、というものです。

というわけで、これからもよろしくお願いします。

MVC 型の PHP アプリケーション開発フレームワークである cakePHP を使ってます。単なるテンプレートエンジンである以上に、日本に利用者が多い(つまり日本語の情報を見つけやすい)こともあって、とても便利です。
cake-logo


そんな cakePHP を使って作っているアプリケーションで、「TSV(CSV) ファイルをダウンロードさせたい」という機能を実装する方法を紹介します。なまじ View/Controller の組み合わせに慣れてしまうと、TSV(CSV) のような View を無視する機能を cakePHP でどう実装するか、という点に迷いますよね。TSV ファイルを作成する前提で調査結果をまとめました。

結論として View(***.ctp) は用意しません。そして Controller 側に以下のような記述を行います:
function download(){
  $this->autoRender = false; //. Viewを使わない
  Configure::write('debug', 0); //. debugコードを出さない

  //. HTTP ヘッダを TSV 用に変更
  header ("Content-disposition: attachment; filename=test.tsv");
  header ("Content-type: application/octet-stream; name=test.tsv");

  //. TSV 本体を生成する部分
  $tsv = "";
  $items = $this->Item->find('all');
  foreach( $items as $item ){
    //. 日本語部分をシフトJISに変換しながら TSV の一行を作成
    $line = $item['Item']['id'] 
      . "\t" . mb_convert_encoding( $item['Item']['name'], "SJIS" )
      . "\t" . mb_convert_encoding( $item['Item']['body'], "SJIS" )
      . "\n";
    $tsv .= $line;
  }

  print($tsv); //. 生成した TSV を出力
  return;
}

肝として、まず View を使わずに処理を終えたいので、autoRender プロパティを false に変更します。また出力結果に debug コードを出したくないこともあるので、debug レベルを 0 に設定しておきます。 更に TSV ファイルをそのまま(選択肢なしで)ダウンロードさせたいので、HTTP ヘッダをダウンロード用のものに変更しています。

TSV 本体部分のコーディングは出力したい内容によって異なりますが、普通に出力したい内容をタブ("\t")区切りにして1行ずつ生成します。これだけでもいいのですが、もし出力した TSV をそのままエクセルで読み込みたい場合などは、文字コードをシフト JIS にしておく必要がある点に注意が必要です。

生成した TSV を最後に出力して処理終了、です。


cakePHP はとても便利なフレームワークですが、今回のような(ビューを使わない)特殊な処理を行う場合は少し特殊な手順が必要になる、というサンプルです。


 

今年も11月2日(イイフタの日)が近づいてきました。「マンホールの蓋好きが集まってキャッキャッする魅惑のイベントマンホールナイトの季節です!


マンホールナイトは今回で6回目を迎えました。今回も「聖地」東京都北区JR王子駅前の「北とぴあ」で、11月2日午後6時半開場です。

第1回目の頃のマンホールナイトは「普通に引く」マニアイベントだったのですが、ここ数年は異文化交流的な内容も加わり、また女性発表者も増えたことで、老若男女誰でも楽しめる集まりになりつつあります、たぶん。


このマンホールナイト、実は第1回目からずっと発表者として参加させていただいております(正確には第1回目は飛び入り)。しかも第2回から第4回まではずっと最終発表者というか、時間調整係というか・・・で参加させていただいてます。ちなみに昨年の5回目はオープニング発表者でした。

第6回目の今回もトリを任され、全力で時間を調整下水道展への出展報告とマンホールマップの便利な使い方の紹介をさせていただく予定です。


なお、今年も同イベント内で写真コンテストが開催されます:
http://manholenight.info/?p=1099

「これぞ!」というマンホール(に関係ありそうな)写真を応募ください。当日の投票でレア中のレアな賞品が当たるかもしれません。なお締切は10月31日です。


当日のプログラムは公式サイトを参照ください(最終発表者が僕です)。まだ若干の空きがあるようなので、11月2日ヒマで、たまには濃い話を聞いてみたいという方がいらっしゃいましたら是非どうぞ:
マンホールナイト6 今年もイイフタの日に開催決定!




 

Apache jMeter を使ったウェブサイトの負荷テストの手順を一通り紹介します。


今回の紹介するシナリオでは、以下の目的および前提でテストを行います:
(1) ウェブサイトは(一応)完成している
(2) ユーザーの想定挙動(「トップページを見て、検索をして、検索結果ページをいくつか閲覧して、・・」という導線)は決まっている
(3) 1つのページあたりのユーザーレスポンスタイム(ユーザーがそのページを開き始めてから反応が返り切るまでの時間)を3秒以内にしたい
(4) この条件で、1秒あたりに何ユーザーのアクセスまで耐えられるシステムなのかを測定したい

アプリケーションのテストには色々な種類がありますが、今回は上記のように「どれだけのユーザーアクセスにまでは耐えうるシステムなのか」を検証するテストを行います。そしてそのようなテストではこの Apache jMeter は便利です。


まずは Apache jMeter をインストールします。jMeter は Pure Java アプリケーションのため、前提として Java の実行環境が必要になります。JRE または JDK を導入して、実行可能な状態にしておいてください。その上で公式サイトから Apache jMeter の最新版バイナリをダウンロードし、展開します。展開後、jMeter の起動ファイル(Windows であれば bin/jmeter.bat)を実行して、jMeter を起動します:
2014102701


次に、このテスト計画にスレッドグループを追加するのですが、その前に今回のテスト目的をおさらいします。一覧のページビューにおいて、そのユーザーレスポンスタイムを3秒(この数字は変更しても構いません)以内にする前提で、1秒あたりに何ユーザーのアクセスまで耐えられるシステムなのかを測定したい、というものでした。 

というわけで、今回のテストでは、あるユーザーシナリオを元に、最初は1ユーザーで実行、次に2ユーザー同時に実行、その次は3ユーザー同時に、、、、と少しずつユーザー数を増やしていきながら、(例えば)20ユーザー同時にアクセスするところまでを実行して、それぞれのページビューアクションでのレスポンスタイムを計測する、という流れになります。そして(今回の場合であれば)3秒以内のレスポンスを期待できるのは何ユーザー程度の同時アクセスまでか、を調べることになります。

つまり今回のテストでは最初は1ユーザーでアクセスし、1分後に1ユーザー増やして2ユーザーでアクセス、更に1分後に1ユーザー増やして3ユーザーでアクセス、、、といった具合に1分ごとにユーザー数(ユーザースレッド)を増やして負荷を加えていきます。そして開始から19分後に20ユーザーでの同時アクセスとなり、ここでのレスポンスタイムを計測して終了、というテストを行います。 なお、1ユーザー増やすタイミングを1分後にする必要はありませんが、後から結果を確認しやすい(開始から何分経過したかで何ユーザーでのアクセスかを推測できる)のでこのようなテストシナリオにしています。

ではこのテストシナリオに従ってスレッドグループを定義します。jMeter の「テスト計画」と書かれた箇所を右クリックして、 追加 > Threads(Users) > スレッドグループ を選択します:
2014102702


テスト契約の下にスレッドグループが追加されます。これをクリックして選択し、以下の内容を入力します:
 スレッド数: 20
 Ramp-Up期間(秒): 1200
 ループ回数: 無限ループ
2014102703


スレッド数はユーザー数です、なので20。Ramp-Up はこの20スレッドをどれだけの時間かけて生成するか、という期間の数字です。今回は1分毎に1スレッドずつ、20スレッドまで増やしていくので 60 x 20 = 1200(秒)を指定します。そして各スレッドは無限に処理を繰り返し実行してほしいので、ループ回数は「無限ループ」を指定しています。

次に、各ユーザースレッドで実行する内容を定義します。「スレッドグループ」を右クリックして、 追加 > サンプラー > HTTPリクエスト を選択します。
2014102704


HTTP リクエストが追加されたら、その中身を実際のテストシナリオに合わせて変更します:
 名前: 何のアクションが分かる名前に変更(どのアクションに時間がかかっているのかを区別できるように)
 サーバー名: テスト先アプリケーションサーバー
 ポート番号: テスト先アプリケーションサーバーのポート番号(80の場合は省略可)
 パス: アクセスする URL のサーバー名以降の部分
2014102705


HTTP リクエスト1つがユーザーの1回のアクセスを定義します。「このページを見て、次にこのページを見て、その次にこのページを見て、・・」のように複数のページを巡回するシナリオで計測する場合は、必要なだけこの HTTP リクエストをスレッドグループに追加していきます。この例では4つのページを巡回するシナリオを定義しています:
2014102706


テストシナリオの定義はこれで終わりです。最後に結果を一覧表で確認できるようにします。スレッドグループを右クリックして 追加 > リスナー > 結果を表で表示 を選択します:
2014102708


これで最小限必要な設定ができたので、メニューから ファイル > テスト計画を保存 を選択して、このテスト計画を保存します。

テストの実行はメニューから 実行 > 開始 を選択するだけです。実行中に「結果を表で表示」をクリックすると、次々に実行されるテストの結果を参照することができます:
2014102707

この "Sample Time(ms)" 列が各アクションのレスポンスタイムになります。スレッドが増えていった時にこの数値がどのように増えていくのかを参照することで(今回の例であれば3秒以内に収まっているかどうかを確認することで「どの程度の同時アクセス数までは期待通りのパフォーマンスを維持できているか」を調査していくことになります。












 

HTML5 で追加された JavaScript の History API を使うと、ブラウザのヒストリ履歴(戻る/進む)の中身を操作できます。これを使って「戻る」を無効にしたページを作ってみます。jQuery を使うので、必要に応じてロードしておきます。

History API では pushState メソッドで履歴を1つ追加、popState メソッドで履歴を1つ(新しいものから)取り出します。この2つを組み合わせて、以下の様なロジックを実装しています:
- ページロード時に強制的にニセの1つ履歴を追加
- そのページ内で「戻る」イベントが発生したら(追加したニセの履歴が取り出されるので)、再度ニセの履歴を1つ追加して処理を終了(return)する

<script>
// History API が使えるブラウザかどうかをチェック
if( window.history && window.history.pushState ){
  //. ブラウザ履歴に1つ追加
  history.pushState( "nohb", null, "" );
  $(window).on( "popstate", function(event){
    //. このページで「戻る」を実行
    if( !event.originalEvent.state ){
      //. もう一度履歴を操作して終了
      history.pushState( "nohb", null, "" );
      return;
    }
  });
}
</script>

これでブラウザの「戻る」ボタンをクリックしても、本来の1つ前の URL ではなく、偽装した履歴が取り出されます。このままですとその偽装した履歴の URL に移動してしまう(今回は "" を指定しているので飛びようもありませんが)のですが、その前に return で強制的に処理を止めているので何も起こりません。また履歴を取り出した直後に再度ニセの履歴を追加しているので、更にもう一度「戻る」をクリックしても同じ処理が繰り返されて、結局戻れない、ということになります。

ちなみにこのブログのエントリの中にもこの JavaScript を埋め込んであります。なので、History API が使えるブラウザでこのページを見ている状態からは「戻る」を押してもどこにも戻れないはずです。


これ、うまく使うと「戻る」ボタンを押した時に本来とは異なるページに強制移動させることもできちゃったりするんじゃないかな。。



このページのトップヘ