sifue's blog

プログラマな二児の父の日常 ポートフォリオは www.soichiro.org

Scala2.9から2.10への移行作業の健忘録

ただの作業ログです。

build.sbtのscalaVersionを2.10.0に変更

scalaVersion := "2.10.0"

以上のようにする。

Seq#firstをSeq#headに置換

盲目的に置き換え。

Squerylを2.10対応の最新バージョンに。でもリポジトリのURLがおかしいので直接追加

libraryDependencies  ++=  Seq(
  "org.squeryl" %% "squeryl" % "0.9.5-6" from "http://repo1.maven.org/maven2/org/squeryl/squeryl_2.10/0.9.5-6/squeryl_2.10-0.9.5-6.jar",
  "mysql" % "mysql-connector-java" % "5.1.21"
)

actor使えなくなるのでakkaへの乗り換え。実装量大。

object actors is not a member of package scala
[error] import scala.actors.Actor

悲しい...。build.sbtに

resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
 
libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.1.1"

を追加して、あとはakkaのAPIを見ながら実装していきます。
というかActorの作り方も、実装の仕方や例外のハンドリングなんかも基本全然違うのでakkaに合わせて書きなおさないと、変な感じになってしまう。というか書きなおしたほうが早い。

まとめ

結構コード量があったこと、元々Actorを使いたくてScalaを選んでいたところもあるので、2.9からの移行はそれなりに大変でした。というか全部終わらず、無理にバージョンアップさせる必要はないと感じました...。辛すぎる。せめてコードコンバーターとかあれば。


まだ生まれて10年経ってない言語の辛さだなぁ。まだ2.11と3.0でも大きく変わりそうだし。しゃーなし。

ssh上でマウススクロールも使える大規模PHP開発向けvim+tmux環境の構築

全体で数百万行、1ファイル1万行超のPHPファイルも一秒以内で開き、開発していくことのできる大規模開発向けのvim+tmux環境を紹介しようと思います。この設定この環境で半年ぐらい安定して利用できています。(無論そんな1万行あるファイルや数千行で複雑性循環度500超のメソッド作るなよという話もあるんですが、すでに存在する魔獣とは向き合わなければならないのです...)
なので数百万行レベルの大規模開発となると重すぎて使えない設定やプラグインもあります。そのようなものはきちんと外してあります(PHPの関数折りたたみ機能、デフォルトのPHPシンタクスハイライト、neocomplcache.vim、ツリーの構成に時間のかかるproject.vimなど)。
基本的にはターミナルエミュレーターとして、macならばiTerm2、windowsならばTeraTermまたはPuttyを利用することを想定しています。
ただ自分自身は、普段はチームでのペアプロ環境維持のためにSublimeText2を利用しており、ssh上で作業する時にのみvimを利用しています。基本的にはマルチカーソルなど独特なものを除きSublimeText2でできることは大抵vimでもできるようにしてあります。


スクリーンショットはこのような感じです。

以上の例では、上のペインではvimを膻分割で利用して、下にも2つペインを用意して、一つはSymfony2のphpunitテストの実行、もうひとつはtopの監視をしています。


実際に現在自分が利用しているtmux及びvimの設定の全体は、
https://github.com/sifue/dotfiles/blob/master/.tmux.conf
https://github.com/sifue/dotfiles/blob/master/.vimrc
以上のようになっています。


なお、仮想端末になぜscreenでなくtmuxを使うかというと、既存セッションへのアタッチが高速(1秒以内)であること、マウス操作が優秀なこと、単一セッションを複数のマシンからデフォルトで使えることなどがあります。(screenでも設定で複数マシンからというのはできるそうですが...)


最終的にはssh上からの利用で以下のようなことができます。

  • 仮想端末機能の利用(tmuxの持っている機能)
    • セッション維持
    • 複数セッションのタブやペインでの使い分け
    • 単一セッションの複数マシンからの利用
  • マウス操作
    • マウスを使った分割されたtmuxペイン選択
    • マウスを使った分割されたtmuxペインのサイズ変更
    • マウスのホイールを使ったvim上のスクロール
    • ドラッグを利用したvim上の文書選択
    • ダブルクリックを使ったvim上の単語選択
    • vimのタブのマウス選択
  • vimプラグイン管理 (NeoBundle)
  • ファイル、履歴、バッファ、レジスタ等のワイルドカードを利用したリソース選択 (Unite)
  • 軽くてPHP5.4に対応したのシンタクスハイライト (shawncplus/php.vim)
  • 自動補完入力 (デフォルト機能/ctags)
  • シンタクスチェック(make)
  • 関数の定義元にジャンプ(ctags)
  • コードスニペット(snipMates)
  • svn, gitの履歴確認 (vcscommand.vim)
  • クラス内メソッドのアウトライン表示 (unite-outline)
  • ファイル名のファジー検索 (ctrlp.vim)


基本的レンタルサーバーなどでも利用できるようにホームディレクトリ以下にローカルインストールする前提で~/local以下にインストールします。サーバー自体にインストールされる方は、~/localを/usr/localと読み替えて下さい。
そのため、利用しているシェルで~/local/binと~/local/libにパスを通してください。
特に自分の使っているシェルがわからないよという方は、デフォルトシェルのbashでパスを通すように、

$ echo "export PATH=/usr/local/bin/:/usr/local/lib/:\$PATH" >> $HOME/.bashrc
$ source  $HOME/.bashrc

以上を実行して始めて頂ければと思います。


説明の順番は

  1. インストールと使い方
  2. 大規模開発の際に使えるvimテクニック

となっています。

インストールと使い方

tmuxのインストール

まずはtmuxをインストールします。
http://web.sfc.keio.ac.jp/~t10078si/wpx/?p=615
を参考に。libeventの適切なバージョンが無いこともあるので一緒にインストールしていまいます。

$ cd
$ mkdir -p ~/local/source
$ cd ~/local/source
$ wget http://downloads.sourceforge.net/project/tmux/tmux/tmux-1.7/tmux-1.7.tar.gz
$ wget https://github.com/downloads/libevent/libevent/libevent-2.0.17-stable.tar.gz
$ tar zxvf tmux-1.7.tar.gz
$ tar zxvf libevent-2.0.17-stable.tar.gz
$ cd libevent-2.0.17-stable
$ ./configure --prefix=$HOME/local
$ make
$ cd ../tmux-1.7
$ /configure --prefix=$HOME/local CPPFLAGS="-I$HOME/local/include -I$HOME/local/include/ncurses" LDFLAGS="-static -L$HOME/local/include -L$HOME/local/ncurses -L$HOME/local/lib"
$ make
$ make install
$ cd ~
$ wget "https://raw.github.com/sifue/dotfiles/b14f380ad667bc02a1fcad402d2cbf4a3eb0e36a/.tmux.conf"
$ tmux

これでOK。設定はgithubにおいてあるものを取得して早速起動しています。セッションを抜けるのには、ctrl+t d、また引き続き続けるにはtmux aコマンドでOKです。
詳しい設定の内容、各種ショートカットの説明は以下の通り。
~/.tmux.conf

# Prefix
set-option -g prefix C-t
unbind-key C-b
bind-key C-t send-prefix

## disable ESC delay
set -s escape-time 0

# View
set -g status-interval 1
set -g status-left-length 16
set -g status-right-length 50

set -g status-bg black
set -g status-fg white
set -g status-left '#[fg=cyan,bold][#14H:#S]#[default]]'
set -g status-right '|#[fg=magenta,bold]#(load.sh)#[default]| #[fg=blue,bold][%a %m/%d %H:%M:%S]#[default]'
set -g message-attr bold
set -g message-fg white
set -g message-bg red

set  -g pane-active-border-fg cyan
set  -g pane-active-border-bg black
setw -g window-status-current-fg blue

set-window-option -g mode-bg white
set-window-option -g mode-fg black
set-window-option -g window-status-fg white
set-window-option -g window-status-bg black
set-window-option -g window-status-current-bg black
set-window-option -g window-status-current-fg green

# Option
set-window-option -g utf8 on
set-window-option -g mode-keys vi
set-window-option -g automatic-rename off
set-window-option -g mode-mouse on
set-window-option -g mouse-resize-pane on
set-window-option -g mouse-select-pane on
set-option -g base-index 1

# KeyBindings
unbind ^C

bind r source-file ~/.tmux.conf; display-message "Reload Config!!"

bind c new-window
bind p previous-window
bind n next-window
bind t last-window

bind k kill-pane
bind K kill-window
bind y copy-mode
bind P paste-buffer


ショートカットですがキーインはCtrl-tになっています。
Ctrl-tを入力したい際には、Ctrl-t Ctrl-tです。

機能 ショートカット
横分割 Ctrl-t "
縦分割 Ctrl-t %
ペインを除去 Ctrl-t k
ペインを移動 Ctrl-t o
ペインを交換 Ctrl-t Ctrl-o
セッションを抜ける Ctrl-t d
セッション再開 コマンドでtmux a
ウインドウを作成 Ctrl-t c
ウインドウを除去 Ctrl-t K
ウインドウ名を編集 Ctrl-t ,
次のウインドウへ Ctrl-t n
前のウインドウへ Ctrl-t p
好きな番目のウインドウへ Ctrl-t 数字
直前のウインドウへ Ctrl-t t
マウスで選択したバッファを貼り付ける Ctrl-t P
設定を読み込む Ctrl-t r
ショートカット一覧を見る Ctrl-t ?

こんな感じの設定になっています。詳しくはCtrl-t ?で起動するショートカット一覧を見ててみてください。もちろんタイトルや色なども自由に設定することができます。

gitとctagsとvimのインストール

http://d.hatena.ne.jp/sifue/20120406/1333738754
を参考にしています。gitは各種vimプラグインをクローンしてくる際に必要だったりするので、入れておきます。また、ctagsの日本語版も利用するのでインストールしておきます。

$ mkdir -p ~/local/source
$ cd ~/local/source
$ wget http://git-core.googlecode.com/files/git-1.8.1.3.tar.gz
$ tar zxvf git-1.8.1.3.tar.gz
$ cd git-1.8.1.3
$ make prefix=$HOME/local
$ make prefix=$HOME/local install
$ cd ..
$ wget http://hp.vector.co.jp/authors/VA025040/ctags/downloads/ctags-5.8j2.tar.gz
$ tar zxvf ctags-5.8j2.tar.gz
$ cd ctags-5.8j2
$ ./configure --prefix=$HOME/local
$ make
$ make install
$ cd ..
$ wget ftp://ftp.vim.org/pub/vim/unix/vim-7.3.tar.bz2
$ tar -jxf vim-7.3.tar.bz2
$ cd vim73
$ ./configure --enable-multibyte --enable-xim --enable-fontset --with-features=big --prefix=$HOME/local
$ make
$ make install
$ wget "https://raw.github.com/sifue/dotfiles/2e722bb2e186ace5ca8f1587cae86d45a99df83b/essential/.vimrc"
$ mkdir -p ~/.vim/bundle
$ export GIT_SSL_NO_VERIFY=true
$ git clone https://github.com/Shougo/neobundle.vim ~/.vim/bundle/neobundle.vim

最後にある程度の設定がしてある、.vimrcを取得し、neobundleをgitでインストールして完了です。

まず、vimを起動して、:NeoBundleInstallと入力して、必要なプラグインをインストールしましょう。

$ vim
:NeoBundleInstall

無事インストールが終われば、それぞれのプラグインの全機能を使うことができます。
基本的には各プラグインの使い方は、各webサイトの最新や、~/.vim/bundlesの中の各プラグインフォルダ内にあるドキュメントを参照お願いします。ちょっと説明をすると

neobundle

基本的には、.vimrcにインストールしたいgithubおよびvim online上のプラグインプロジェクト名を記述して:NeoBundleInstallを実行する使い方しかしません。プラグインを最新のものにバージョンアップする際は、:NeoBundleInstall!、アンインストールする際は、設定を消した後に:NeoBundleCleanを実行するだけです。
プラグイン管理されてると、簡単にいろんなプラグインの相性を試せるので本当に楽です。

unite

下の.vimrcで設定しているショートカット(ファイルなら;uf、履歴なら;umなど)で、各種リソースを開くことができます。
起動後、文字を入力するとマッチするものに絞りこまれますが、Escしてカーソルを動かし、tabを押して実行したいコマンド選ぶモードにいくこともできます。キャンセルはqとなっています。ちなみに絞込み時には*を利用したワイルドカードも利用できます。大規模プロジェクトでは複数階層を検索する**/を入力すると大抵落ちるのでそれだけは注意です。
詳しくは、
https://github.com/Shougo/unite.vim/blob/master/doc/unite.txt
を参考に。

snipMate

PHPの各種スニペットをtabキーで展開することができます。
たとえばforと入力してtabキーを押すと

for ($i = 0; $i < count; $i++) {
     // code...
}

のように展開されます。
~/.vim/bundles/snipMate/snippets
以上のフォルダ内に利用できるスニペットの一覧が入っています。
自作のスニペットも登録することが可能です。
詳しくは、
http://www.vim.org/scripts/script.php?script_id=2540

ctrlp

Ctrl+pでカレントフォルダ以下のファイルをファジー検索をすることができます。
まさにSublimeText2のCtrl+pと同じ役割となっています。Uniteよりもファジーに検索してくれ巨大なプロジェクトにおいても高速です。

unite-outline

下記に説明してあるctagsコマンドでtagsファイルを作成の後、設定したショートカット(Ctrl+o)でメソッド一覧などを表示してくれます。

vcscommand

:VCSで始まる様々なバージョン管理システムの機能を利用することができます。
詳しくは、
http://www.vim.org/scripts/script.php?script_id=90
特に:VCSLogと:VCSBlame、:VCSDiffはよく使うのではないかと思います。

マウスやスクロールの使い方について

説明は不要かと思いますが、tmuxのペインの区切り、ペインの選択、vimのタブ、vimの文章、ダブルクリックでvimの単語が編集でき、マウスのスクロールも効くのではないかと思います。トラックパッドや慣性スクロールをつかっていると、でかいファイルでもスルーっとなめらかにスクロールできます。

.vimrcについて

なお~/.vimrcの内容は以下のようになっています。それぞれの設定の説明に、どのような意味なのか、どのようなショートカットで読み出せるかコメントで書いてありますので確認ください。
ほとんどのPHP開発で、必要な設定になっているのではないかと思います。なお、でかいファイルを開くのに障害になる設定や、JavaScriptやHTML用のプラグインや設定は省いてあります。

"VimをなるべくVi互換にする
set nocompatible

""""""""""" NeoBundle設定  """""""""""{{{
" https://github.com/Shougo/neobundle.vim
" インストール
" $ mkdir -p ~/.vim/bundle
" $ export GIT_SSL_NO_VERIFY=true
" $ git clone https://github.com/Shougo/neobundle.vim ~/.vim/bundle/neobundle.vim
" :NeoBundleInstall でプラグインインストール :NeoBundleInstall! で更新
filetype plugin indent off     " required!
if has('vim_starting')
  set runtimepath+=~/.vim/bundle/neobundle.vim/
  call neobundle#rc(expand('~/.vim/bundle/'))
endif

" gitを使ったプラグインマネージャ 基本Vundleと一緒
NeoBundle 'Shougo/neobundle.vim'

"""""""" github
" Uniteコマンドによるフィルタ付き読み出し等
NeoBundle 'Shougo/unite.vim'
" Uniteコマンドでアウトラインを表示
NeoBundle 'h1mesuke/unite-outline'
" PHP5.4にも対応している新しいPHPのシンタックスハイライト
NeoBundle 'shawncplus/php.vim'
" ctrl+pで起動するファジー検索に対応したファイラー
NeoBundle 'kien/ctrlp.vim'

" SVNやgitなど http://blog.blueblack.net/item_144 :VCS* で実行
NeoBundle 'vcscommand.vim'
" tabでスニペット補完
NeoBundle 'snipMate'

filetype on
filetype indent on
filetype plugin on
"}}}
""""""""""" プラグインごとの設定 """""""""""{{{
" Unite起動時にインサートモードで開始
let g:unite_enable_start_insert = 1

" Uniteの各種ショートカット設定
" バッファ一覧
nnoremap <silent> ;ub :<C-u>Unite buffer<CR>
" ファイル一覧
nnoremap <silent> ;uf :<C-u>UniteWithBufferDir -buffer-name=files file<CR>
" レジスタ一覧
nnoremap <silent> ;ur :<C-u>Unite -buffer-name=register register<CR>
" 最近使用したファイル一覧
nnoremap <silent> ;um :<C-u>Unite file_mru<CR>
" 全部乗せ
nnoremap <silent> ;ua :<C-u>UniteWithBufferDir -buffer-name=files buffer file_mru bookmark file<CR>

" Ctrl +  o でタグアウトラインを表示
nnoremap <C-o> :<C-u>Unite outline<CR>

"}}}
""""""""""" Vimの基本的な設定  """""""""""{{{
"バックスペースキーの動作を決定する
"2:indent,eol,startと同じ
set backspace=2

"行数表示
set number

"新しい行を開始したときに、新しい行のインデントを現在行と同じ量にする
set autoindent

"検索で小文字なら大文字を無視、大文字なら無視しない設定
set smartcase

"(no)検索をファイルの末尾まで検索したら、ファイルの先頭へループする
set nowrapscan

"インクリメンタルサーチを行う
set incsearch

"highlight matches with last search pattern
set hlsearch

"閉じ括弧が入力されたとき、対応する括弧を表示する
set showmatch

"カーソルが何行目の何列目に置かれているかを表示する
set ruler

"新しい行を作ったときに高度な自動インデントを行う
set smartindent

"保存しないで他のファイルを表示することが出来るようにする
set hidden

"カレントバッファ内のファイルの文字エンコーディングを設定する
set fileencoding=utf-8

"Insertモードで<Tab> を挿入するのに、適切な数の空白を使う
set expandtab
set ts=4

"ファイル内の <Tab> が対応する空白の数
set tabstop=4

"自動インデントの各段階に使われる空白の数
set shiftwidth=4

"行頭の余白内で Tab を打ち込むと、'shiftwidth' の数だけインデントする
"set smarttab

"強調表示(色付け)のON/OFF設定
syntax on

"ステータスラインを表示するウィンドウを設定する
"2:常にステータスラインを表示する
set laststatus=2

"ステータス行の表示内容を設定する
set statusline=%<%f\ %m%r%h%w%{'['.(&fenc!=''?&fenc:&enc).']['.&ff.']'}G8%=%l,%c%V%8P

"vimのバックアップファイルとスワップファイル
set nobackup
set noswapfile

"バッファをクリップボードにコピー(for OSX)
set clipboard=unnamed,autoselect

"自動改行オフ
set tw=0

" マウスモード有効
set mouse=a

" xtermとscreen対応
set ttymouse=xterm2

"MacVimやGVimを利用する際にIMEがモードの切替でオフとなる設定
set imdisable

"UTF-8文字化け対応
set termencoding=utf-8
set encoding=utf-8
set fileencoding=utf-8
set fileencodings=utf-8,cp932


"}}}
""""""""""" 効率化UPのための設定 """""""""""{{{
" <Leader>を\にリマッップ
nnoremap \ <Leader>
vnoremap \ <Leader>

"全角スペースを で表示
highlight JpSpace cterm=underline ctermfg=Blue guifg=Blue
au BufRead,BufNew * match JpSpace / /

"タブを見えるように設定
set list
set listchars=tab:>-,trail:-

" サーチハイライトををESC二回で消す
nnoremap <Esc><Esc> :nohlsearch<CR><Esc>

" 挿入モードとノーマルモードでステータスラインの色を変更する
au InsertEnter * hi StatusLine guifg=DarkBlue guibg=DarkYellow gui=none ctermfg=Blue ctermbg=Yellow cterm=none
au InsertLeave * hi StatusLine guifg=Black guibg=White gui=none ctermfg=Black ctermbg=White cterm=none

"バイナリ編集(xxd)モード(vim -b での起動、もしくは *.bin で発動します)
augroup BinaryXXD
	autocmd!
	autocmd BufReadPre  *.bin let &binary =1
	autocmd BufReadPost * if &binary | silent %!xxd -g 1
	autocmd BufReadPost * set ft=xxd | endif
	autocmd BufWritePre * if &binary | %!xxd -r
	autocmd BufWritePre * endif
	autocmd BufWritePost * if &binary | silent %!xxd -g 1
	autocmd BufWritePost * set nomod | endif
augroup END

" ヴィジュアルモードで選択したテキストをnで検索する(レジスタv使用)
vnoremap <silent> n "vy/\V<C-r>=substitute(escape(@v,'\/'),"\n",'\\n','g')<CR><CR>

" gfでカーソル下のファイル名を新しいタブで開く
nnoremap gf :tabe <cfile><CR>
vnoremap gf :tabe <cfile><CR>

" 検索語が画面中央にくるように
nmap n nzz
nmap N Nzz

" ヤンク、切り取り時にレジスタ"の値をzにもコピーしておく(連続貼付可に使う)
vnoremap <silent> y y:let @z=@"<CR>
vnoremap <silent> d d:let @z=@"<CR>

" ビジュアルモードで選択したテキストを消してレジスタzの内容を貼付ける(連続貼付可)
vnoremap <silent> p x"zP

" vimrcの新しいタブでの編集と読み込みのショートカット設定
nnoremap ;s :source $MYVIMRC<CR>
nnoremap ;v :tabe $MYVIMRC<CR>
nnoremap ;g :tabe $MYGVIMRC<CR>
nnoremap ;l :tabe ~/.vimrc.local<CR>

" :makeや:grepをした際に自動的にquickfixが開くようにする
autocmd QuickfixCmdPost make,grep,grepadd,vimgrep,vimgrepadd if len(getqflist()) != 0 | cw | endif

" テキストファイル専用の設定
augroup ettext
	autocmd!
	autocmd BufRead,BufNewFile *.txt setlocal expandtab nolist nonumber tw=0
augroup END

" ファイルを開いたときに前回の編集箇所に移動
autocmd BufReadPost * if line("'\"") > 0 && line("'\"") <= line("$") | exe "normal g`\"" | endif


"}}}
""""""""""" 言語ごとの設定 """""""""""{{{
"ctagsのファイルをカレントディレクトリから検索して上位にあるもの読み込む
if has('path_extra')
	set tags+=tags;
endif

""""" VIM用設定 """"""""
" vimファイルに関して{と}による折りたたみ設定をする
au FileType vim setlocal foldmethod=marker

""""" PHP用設定 """"""""
" :makeでPHP構文チェック
au FileType php setlocal makeprg=php\ -l\ %
au FileType php setlocal errorformat=%m\ in\ %f\ on\ line\ %l

" PHPの関数やクラスの折りたたみ(非常に重い)
let php_folding = 0

" 文字列の中のSQLをハイライト
let php_sql_query = 1

" Baselibメソッドのハイライト
let php_baselib = 1

" HTMLもハイライト
let php_htmlInStrings = 1

" <? を無効にする→ハイライト除外にする
let php_noShortTags = 1

" ] や ) の対応エラーをハイライト
let php_parent_error_close = 1
let php_parent_error_open = 1


"}}}

以上で環境構築自体は完了となります。変えたい部分は随時設定などしてみてください。
実際に設定を一つ一つ確認しながら、使ってみて頂ければと思います。個人の好みなどがあると思いますので、カラースキームなどの設定は予め外してあります。


なおctagsだけは、必要になった後にタグを付けたいPHPのプロジェクトのフォルダのルートにて、

$ rm tags
$ ctags -R --langmap=PHP:.php.inc --php-types=c+f+d+v+i;

を実行ください。これで、古いtagsファイルが消し、新たにtagsファイルが作られます。
そのフォルダ以下のファイルをvimで開く際に自動的にtagsファイルが読み込まれ、Ctrl+]で関数の定義元にジャンプCtrl+t Ctrl+t(tmuxのキーバインドとかぶっているため)で戻ってくることができます。また、Ctrl+pやCtrl+nで表示する補完候補にもこのctagsの情報が使われるようになります。
後はvimのタグジャンプのヘルプなどを読んで使ってみてください。

大規模開発の際に使えるvimテクニック

最後に大規模開発でよく使うテクニックを紹介します。
一応、vimはvimtutorで説明される使い方は基本的に使えることを前提とします。その上で開発するのに便利なvimのデフォルト機能を紹介します。

1. コピペモード :set paste

これをやっておけば貼りつけた際にインデントがくずれたりすることがありません。
戻すときは、:set nopaste
でもコピペ開発はいけませんよ!
テンプレートやスニペットを利用したい際のみに使いましょう。重複は大規模開発の保守における悪!

2. マクロ qaで覚えてqで記録終了@aで実行

aは記憶領域の名前なだけで、同じキーであれば別にどれでも大丈夫です。
同じような入力操作、たとえばgetterやsetterを書いたりするのはすぐにマクロにして効率化した方がよいですよね。

3. 前回の動作を繰り返す . 10回繰り返す10.

同じ操作の繰り返しは、.で実行。それを10000回実行するのもモニターを見ているだけで十分です。

4. ミニバッファでヤンクした文字列を呼び出す C-r"

ミニバッファにて、レジスタの情報を呼び出したい時、よく選択した単語をヤンクして検索した時などに便利です。

5. 数値のインクリメント、デクリメント C-a と C-x

これとマクロを組み合わせると、数値の羅列やテストデータの作成などが非常に楽になります。

1, 20, 40
2, 21, 41
3, 22, 42
4, 23, 43
5, 24, 44

こういうのはマクロとインクリメント・デクリメントでサクッと作ってしまいましょう。

6. テキストオブジェクトの選択 viw

マウスのダブルクリックと同じなのですが、iwなどで選択できるテキストオブジェクトを意識してマクロなどを組みはじめると、一気にいろんな操作ができるようになります。

7. 今のファイルと比較 :vertical diffsplit [ファイル名]

コマンドが長いので忘れがちですが、2つのファイルのdiffをssh上で調べなきゃいけない時に便利です。環境の設定差異を見たりするのに使います。

8. 複数行同時編集 C-vで矩形選択、IまたはAの後好きな文字列を入力して、Escで反映

これは複数行コメントアウトや、複数行同時編集のマクロ化などいろいろなシーンで役に立つテクニックです。

9. ls | vim - などで標準出力をvimに渡す

結果をvimで編集、調査したいときなどに便利です。
またシンタクスハイライトを目的に利用することもあります。
よく使うのが、diffでの利用

$ svn diff | vim -
$ git diff | vim -

になります。

10. gf でカーソル下のパスを開く

これはvim -で読みだした標準出力と非常に相性の良いテクニックです。

$ git diff | vim -

でdiffを開き、更にそこから、パスの上にカーソルを持って行き、gfで実際のファイルにジャンプして:VCSLogなどで、過去の履歴などを調べたりすることもできます。

11. :makeでシンタックスチェック

まさかシンタックスエラーのあるコードなんてコミットしませんよね。設定によっては保存するたびに:makeが走るようにすることもできます。ただ、:makeが重い場合もあるので、その時に合わせた運用で。


以上が、ssh上でマウススクロールも使える大規模PHP開発向けvim+tmux環境の構築と大規模開発の際に使えるvimテクニックの紹介でした。
お疲れ様でした。

エリック・エヴァンスのドメイン駆動設計に沿ってSymfony2でユーザー管理アプリを作ってみた

あけましておめでとうございます。
去年の暮からエリック・エヴァンスドメイン駆動設計という5200円、500ページもする本を購入して読み始めた自分です。

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

あまりに勿体無かったので試しにこのドメイン駆動設計の設計思想にそって、簡単なアプリをSymfony2で作ってみました。


実際に作られたサイトは、
http://www.soichiro.org/sf
こんな感じです。

  • id: test1@test.com
  • pass: test1

でログインできます。(ユーザー作るだけならadmin/adminpassでもOK)


画面イメージは





こんな感じです。UIにはtwitterのbootstrapを使っています。


ソースコードは、
https://github.com/sifue/UserManagement
からチェックアウトできます。無論MITライセンスです。サンプルとして使えそうな所があったら好きなように改変してご自由にご利用ください。


ドメイン駆動設計はすごく簡単に言うと、システムを作る時って業務モデル(実際のビジネスを抽象化した概念、例えばUMLのクラス図で書いたようなもの)の共有ってすごく重要だよねって話から始まって、そのモデルとシステムをいかに近づけるかって話と、実際のシステムを実装するときには、

に分けて実装すれば良いよっていうところから、更に深く実際のいろいろなプラクティスが書かれています。


かなりじっくり読まなきゃいけないタイプの本で、読めば読むほど考えさせられる所もあって読み進まないタイプの本でした。オブジェクト指向大規模アプリ設計の教科書と言っても過言ではないのでしょうか。


読み進んでビックリしたのは、ここに出てくる概念がほとんどSymfony2で実装されているというところでした。たぶんこの本に相当影響を受けてるんだと思います。例えで言うと、

  • サービス : Symfony2ではサービスコンテナというDIコンテナ
  • エンティティ: Symfony2ではエンティティというデータクラス
  • ファクトリ: Symfony2ではサービスにインスタンスを作るファクトリを登録したり、サービスを作るファクトリを設定できる
  • リポジトリ: Symfony2ではリポジトリというORMとの接続処理実装集約クラス
  • モジュール: Symfony2ではバンドル、OSGiのバンドルのように拡張ポイントもある

とかがありました。
残念ながらなかった概念は、値オブジェクト。Javaenumのように不変でstaticなインスタンスを作る仕組みがPHPにはないのでやむなしというところなのかもしれません。


実際にアプリ作るにあたってエリック・エヴァンスドメイン駆動設計によるとドメイン層はできるだけ集約して一つにして高い凝集にしたほうがよいということだったので、実際に作ったこのユーザー管理システムのバンドル設計、クラス図はこんな風になりました。

Symfony2の思想としては、ドメイン層とアプリ層とUI層を一つのバンドル内で作ってしまうように作られていたのですが、よりドメイン層に全てのビジネスロジックを集約する意図を持たせるために、DomainBundleというものを作りました。
赤がインフラ層、オレンジがドメイン層、緑がアプリ層、青がUI層をあらわしています。UI層のテンプレートファイルやルーティング設定ファイル類は割愛しています。


確かにこの設計で作ってみると非常にアプリの作りがいいように感じます。あと、クラスそれぞれが単一責務になっている。


以下に実際にUserFactoryの実装を書きますが、ここではUserのインスタンスができる際に守らせなければならない、インスタンス作成時は有効になっていることや、ランダムなハッシュ用のsaltの設定などを守らせることができます。

<?php
namespace Sifue\Bundle\DomainBundle\Factory;
use Sifue\Bundle\DomainBundle\Entity\User;

/**
 * Userインスタンスの作成とインスタンスが守らなければいけない整合性を保つ責務を持つクラス
 */
class UserFactory
{
    /**
     * ユーザーのインスタンスを取得する
     * @return Sifue\Bundle\DomainBundle\Entity\User
     */
    public function get()
    {
        $user = new User();
        $user->setIsActive(true);
        $user->setSalt(base_convert(sha1(uniqid(mt_rand(), true)), 16, 36));
        return $user;
    }
}

いい具合に責務が分離されています。
これなら大規模開発で拡張、リファクタリングを続けていっても耐えられる構成なのではないかなという確信を得ることができました。


とは言えまだドメイン駆動設計、ぶっちゃけ業務でいろいろやってみないとわからないことも多そうなので、いろいろなテクニックを取り入れながら少しずつ試して行けたらななんて考えています。まだ初心者な所もあるので、ご指摘などあればコメント下さい。


ちなみにこのアプリ。Symfony2のインストールのためのPHP環境、MySQL環境を揃えた後、

$ git clone git@github.com:sifue/UserManagement.git
$ vim app/config/parameters.yml
parameters:
    database_driver:   pdo_mysql
    database_host:     127.0.0.1
    database_port:     ~
    database_name:     symfony
    database_user:     root
    database_password: password

    mailer_transport:  smtp
    mailer_host:       127.0.0.1
    mailer_user:       ~
    mailer_password:   ~

    locale:            ja
    secret:            ThisTokenIsNotSoSecretChangeIt
$ php app/console doctrine:database:create
$ php app/console doctrine:schema:update --force

とするだけで、UserManagement/webをウェブサーバーに公開すれば動かすことができるようになっています。


テストもひと通り実装してあり、PHPUnitがインストールしてあれば、

$ phpunit -c app

で全てのテストが実施可能です。
Symfony2はPHPUnitをいい具合に拡張してあって、WebのUIをエミュレーションするテストやサービスコンテナのテストができるところが便利です。


ぜひ試してみて下さい。


追伸
ちなみに自分はPHPなんてオブジェクト指向言語の風上にも置けない言語だと思っていたのですが、Symfony2に触れて全然そんなことはないな、普通に戦えるフレームワークだなと思いました。PHPはひどい言語だけど、捨てたもんじゃない!きっとw

恐ろしく高い編集能を持つマルチカーソルエディタとしてのSublimeText2

最近、開発環境の導入コストの低いIDEの導入を広めようと思って、VimからSublimeText2にメインエディタに変更した自分ですが、SublimeText2のすごさはやっぱりマルチカーソルにあるなと思ったのでエントリーを書くことにしました。

Sublime Text2 は

http://www.sublimetext.com/

以上のURLからMac, Windows, Linuxマルチプラットフォームでダウンロード、利用できます。(たまに保存時にダイアログは出ますが無料でずっと使い続けられます)


無論、Vimにもマルチカーソルに近い感じの複数行編集モードは存在しています。
vimで複数行の行頭、行末に一気に文字を挿入する方法 - プログラマ 福重 伸太朗 〜基本へ帰ろう〜
などで紹介されている。Ctrl+vで矩形選択モードで縦1列を選択して、IまたはAで行頭または行末に入力してEscで抜けると全ての行に反映されているというこの動きです。


SublimeText2でも基本的に同じことができるのですが、矩形選択による複数行編集と、複数選択のあとのマルチカーソル選択は根本的に違うものだということがわかりました。



このスクリーンショットのように、SublimeText2では、CommandまたはCtrlを押しながら領域を複数選択し、Command + dを押すことで複数のカーソルを置くことができます。このマルチカーソルのすごいところは、この全てのカーソルを上下左右に動かすことが出来、一度に編集できることです。つまり矩形選択の枠に縛られない。



そして、この次のスクリーンショットでは、通常の複数行選択をCommand + Shift + l で複数の選択に分割してマルチカーソルを設置し、全ての行頭、行末に編集を加えたり、改行を消去したりしています。マルチカーソルはなんと行を超えてカーソルを移動することができるので、改行を消すことも追加することもできるわけです!


すごいよ、すごすぎるよSublimeText2。


このマルチカーソルとしての機能があることで、個人的にはVimを超えた概念を持つエディタが登場したなと感じました。これからも、SublimeText2での編集技術を磨いて行こうと思います。


ぜひ一度は、このすごい編集能を持つSublimeText2でマルチカーソルエディタのすごさを体験して見ることをオススメします。

JavaでノンブロッキングIOを使ったネットワークアプリを学ぶのに最適なNetty 3.5系のGetting Startedを日本語訳しました

Nettyと言えばJavaのノンブロッキングIOのAPIであるNIOをラップしたフレームワークとして、TwitterのFinagleなどで分散ネットワークアプリケーションシステムで使わていて高速で実績のあるライブラリとして有名ですが、ノンブロッキングIOでイベント駆動のサーバークライアントのネットワークアプリケーションを知るのに非常に良い題材ですので、素人翻訳ですがその日本語訳を公開することにしました。


ちなみにNettyがどれぐらいパフォーマンスに優れているのかというと、Herokuの仮想インスタンスを利用した実験の結果が参考になります。Scala(Finagle)がNettyの実装を利用したものになりますが、秒間6000リクエスト時の1dyno(APサーバー)の応答が秒間4000レスポンスで、C(Accept)、Java(Jetty)、Java(Tomcat)、Js(Node)、Python(Bottle)、Ruby(Sinatra)と比べて最も応答性がよく、接続エラーが少ないという結果が出ています。


つまり、Nettyは今ある有名なネットワークアプリを作るフレームワークの中でかなり良いパフォーマンスと安定性を持つ選択肢として考えることができると思います。
Nettyすごい。
NettyはTCP/IPを使った通信のフレームワークを自由に実装できますが、Codec framework、SSL/TLS、HTTP、WebSockets、Google Protocol Bufferなどに関しては予め実装があり、利用することができます。


といってもこういうものを実務で利用しなくては行けない方というのは少ないのではないかと思いますが、TCP/IPの通信ってこんな感じなんだ、Webサーバーの中身の実装ってこうなってるんだとか、リソース管理ってこうするんだというノウハウがこのGetting Startedを通じて学べますので非常に価値があると思います。


元のドキュメントは、
http://static.netty.io/3.5/guide/
となります。素人の意訳なのでご利用は自己責任でお願いします。


なお、ソースコードGitHub
https://github.com/sifue/nettystudy
にて公開しておりますので、動かす場合はチェックアウトして、mavenでビルドしてライブラリを落としてご利用下さい。

The Netty Project 3.x User Guide

高速なネットワークアプリケーション開発のための折り紙つきのアプローチ


はじめに

1. 問題
2. 解決法

1. 問題

今日、わたちたちは一般的に相互通信をさせるためにアプリケーションやライブラリを使います。例えばウェブサーバーから情報を取ったり、ウェブサーバーのリモートプロシージャを実行するのにいつもHTTPクライアントライブラリを利用します。


しかし、一般的なプロトコルやその実装はよくスケールしません。 それは巨大なファイルやeメール、ほぼリアルタイムの金融情報ややマルチプレイヤーのゲームにの通信にHTTPサーバーを使わないというようなことです。特別な目的を果たすための高く最適化されたプロトコルの実装には何が必要なのでしょうか。例えば、AJAXベースのチャットアプリや、メディアストリーミング、大きなファイルの転送にはHTTPサーバーは最適化されたHTTPサーバーの実装が必要になるでしょう。更に正確にあなたの要求を満たすならば、全く新しいプロトコルをデザイン、実装をしたいかもしれません。

2. 解決法

Nettyプロジェクトは、非同期のイベント駆動ネットワークアプリケーションフレームワークの提供に注力し、メンテナンス可能なハイパフォーマンスでハイスケーラビリティのサーバーとクライアントのプロトコルの高速な開発を整備します。


言い換えれば、Nettyは、高速に簡単に、プロトコルサーバークライアントのようなネットワークアプリを開発することを可能にするNIOクライアントサーバーフレームワークです。NettyはTCPUDPソケットサーバー開発のようなネットワークプログラミングを簡単にし、合理化します。


"高速で簡単"はメンテナンス性やパフォーマンスの問題に見舞われることを意味しているのではありません。Nettyは、FTPSMTP、HTTPや様々なバイナリやテキストベースの巨大な古いプロトコルのような多くのプロトコルの実装から得られた経験に基づき、とても注意深く設計されました。結果として、Nettyは簡単な開発、パフォーマンス、安定性、柔軟性を、妥協なしに達成する方法を見つけることに成功しました。


あるユーザーは、既に似たような利点を持つ他のネットワークアプリケーションフレームワークを既に見つけていたり、Nettyがそれらとどう違うのかが聞きたいでしょう。答は哲学的なところです(難訳)。NettyはあなたにAPIの内容とその実装という最も快適な経験をその日から与えるでしょう。ただそれは、あなたがこのガイドを読んで実際にNettyで遊んで得られる哲学のように、触れないものなのです。

Chapter 1. Getting Started

1. はじめる前に
2. ディスカードサーバーを書く
3. 受け取ったデータを見る
4. ECHOサーバーを書く
5. TIMEサーバーを書く
6. TIMEクライアントを書く
7. ストリームベースの通信を扱う
7.1. ソケットバッファーに関する一つの小さな警告
7.2. 第一解決策
7.3. 第二解決策
8. ChannelBufferの代わりのPOJOを使って通信する
9. アプリケーションをシャットダウンする
10. まとめ


この章は、あなたが素早くはじめるための簡単な例を用いたNettyの中心的構造のツアーです。この章の最後についた時、あなたはNetty上でクライアントとサーバーが書けるようになっているでしょう。


もしあなたが何かを学ぶのにトップダウンのアプローチの方が好きなら、Chapter 2, Architectural Overview を先に読んでそれから戻ってきてください。

1. はじめる前に

この章の例を動かすのに最低限のものが2つあります。NettyとJDK1.5以上です。Nettyの最新バージョンはプロジェクトのdownloadのページから利用できます。正しいバージョンのJDKに関しては、好きなJDKのベンダーのサイトを参照して下さい。


読む時に、この章で沢山の導入されたクラスについて沢山の疑問を持つでしょう。もしあなたがもっと知りたいとおもったのであれば、APIリファレンスを参照して下さい。このドキュメントの全てのクラス名はAPIリファレンスにリンクしています。同様に、Nettyプロジェクトのコミュニティに連絡を取るのをためらわないでください。不正確な情報、文法間違い、ドキュメント改善に関する良いアイディがあったら教えて下さい。

2. ディスカードサーバーを書く

最もシンプルなプロトコルはこの世界に置いてはハローワールドではなくDISCARDです。このプロトコルは、何かしらのデータを受け取ったら何かのレスポンスをすることなく、無視します。


DISCARDプロトコルを実装するためにはただ、受け取ったデータを無視することを考えればよいだけです。ではNettyによって生成されたIOイベントを操作をハンドラーの実装を始めましょう。

package org.soichiro.nettystudy.example.discard;

import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

public class DiscardServerHandler extends SimpleChannelHandler {

	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
		e.getCause().printStackTrace();

		Channel ch = e.getChannel();
		ch.close();
	}
}

1. SimpleChannelHandlerを継承したDiscardServerHandlerはChannelHandlerの実装です。SimpleChannelHandlerはあなたがオーバーライドすべき様々なイベントハンドラメソッドを提供します。ここではハンドラを実装しなくても、SimpleChannelHandlerを継承するだけで十分です。
2. messageReceivedイベントハンドラメソッドをここではオーバーライドしています。このメソッドは、 MessageEventと共にコールされ、クライアントから受け取る新しいデータが含まれています。この例では、DISCARDプロトコルの実装のために、受け取ったデータは何もしないことによって無視します。
3. exceptionCaughtは IO例外がイベント処理の中で投げられた時に、ExceptionEventと共に呼ばれます。大抵の場合、キャッチされた例外はログにはかれ、関連するチャンネルをここで閉じます、例外のシチュエーションへの対応実装をどうしたいかは異なるにも関わらずです。例えば、あなたはコネクションが閉じる前にエラーコードとレスポンスメッセージ返したいだろう。


今のところ順調です。もうDISCARDサーバーの半分を実装しました。DiscardServerHandlerを使いサーバーをスタートさせるmainメソッドが残っています。

package org.soichiro.nettystudy.example.discard;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;

public class DiscardServer {

  public static void main(String[] args) throws Exception {
      ChannelFactory factory =
          new NioServerSocketChannelFactory(
                  Executors.newCachedThreadPool(),
                  Executors.newCachedThreadPool());

      ServerBootstrap bootstrap = new ServerBootstrap(factory);

      bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
          public ChannelPipeline getPipeline() {
              return Channels.pipeline(new DiscardServerHandler());
          }
      });

      bootstrap.setOption("child.tcpNoDelay", true);
      bootstrap.setOption("child.keepAlive", true);

      bootstrap.bind(new InetSocketAddress(8080));
  }
}

4. ChannelFactoryは、 Channelとそれに関わるリソースを作るファクトリーです。全てのIOリクエストを処理し、ChannelEventの生成のためのIOを実行します。Nettyは様々なChannelFactoryの実装を提供しています。この例ではサーバー再度のアプリケーションを実装しています。それはつまり、NioServerSocketChannelFactoryが使われます。別なことを書くと、これ自身はIOスレッドを作りません。あなたがコンストラクタで設定したスレッドプールを使ってスレッドを作成します。そしてセキュリティーマネージャの用いられサーバーのようなアプリケーション実行環境でのスレッドがどのように管理されるべきかという制御をあなたに与えます。
5. ServerBootstrap はサーバーをセットアップするヘルパークラスです。あなたは、Channelを直接設定することができます。しかし、それは殆どの場合あなたに不必要な退屈なプロセスです。
6. ここでChannelPipelineFactoryを設定します。新しい接続がサーバーにアクセプトサれるときはいつでも、新しいChannelPipelineがChannelPipelineFactoryに特徴づけられて作成されます。この新しいパイプラインはDiscardServerHandlerを含みます。もし複雑化してきたときは、もっとハンドラをパイプラインに追加して、匿名クラスをトップレベルクラスに随時抽出しましょう。
7. Channelにパラメータを渡すこともできます。私たちはTCP/IPのサーバーを書いています。だから、tcpNoDelayとkeepAliveをソケットオプションとして設定できます。"child."とプレフィックスを付け加えていることについて書きます。これは、ServerSocketChannelのオプションの代わりに、Channelのオプションとして適用されます。ServerSocketChannelのオプションは以下のようにします。

bootstrap.setOption("reuseAddress", true);

8. 今行く準備ができました。あとは、ポートをバインドしてサーバーをスタートさせるだけです。ここにこのマシンのすべてのNICの8080にバインドしました。違うアドレスならば何度でもバインドすることができます。


おめでとうございます!これであなたはあなたの最初のサーバーをNetty上に構築し終えました。

3. 受け取ったデータを見る

丁度いまわたしたちの最初のサーバーを書きました。本当に動いているかどうか確かめなくてはいけません。最もテストする簡単な方法はtelnetコマンドを使うことです。例えば、"telnet localhost 8080"とコマンドラインで入力して、さらに何かタイプしてみてください。(MacLinuxならすぐtelnetが使えます。Windowsはバージョンによって準備が必要かもしれません...)


しかしながら、サーバーはうまく動いているといっていいのでしょうか?ディスカードサーバーであるためわかりません。何のレスポンスも得られないでしょう。本当にうまく行っているか検査するために、何か受け取ったらプリントしてみるように修正しましょう。


私たちは、既にイベントを受け取るとMessageEventが生成され、messageReceivedが呼び出されることを知っています。ちょっとしたコードを、DiscardServerHandlerのmessageReceivedメソッドに書きましょう。

	 @Override
	 public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
	     ChannelBuffer buf = (ChannelBuffer) e.getMessage();
	     while(buf.readable()) {
	         System.out.println((char) buf.readByte());
	         System.out.flush();
	     }
	 }

9. ソケット通信におけるメッセージの型が常にChannelBufferだと推定するのは安全です。ChannelBufferはNettyにおいてバイトシーケンスを保存する基本的なデータ構造です。これは、NIOのByteBufferに似ています。しかし、より簡単で柔軟性があります。例えば、Nettyは不要なメモリコピーを減らしたChannelBufferを複数組み合わせたChannelBufferなどを作ることもできます。
多くの部分がByteBufferに似ているにていますが、APIリファレンスを参照することを強く薦めます。ChannelBufferを正確に学ぶことは難なくNettyを使うために致命的に重要なステップです。


telnetのコマンドをもう一度叩いてみて下さい。サーバーが受け取ったものがprintされるのがわかります。
DISCOARDサーバーの全ソースは、配布物のorg.jboss.netty.example.discardの中にあります。

4. ECHOサーバーを書く

ここまでで、全くレスポンスはせずデータを消費するだけでした。しかしながら、サーバーは通常リクエストに反応します。次は与えられたデータを返すECHOプロトコルを実装して、メッセージをクライアントに返す実装の仕方を学びましょう。
DISCARDサーバーとの唯一の違いは、受け取ったデータをコンソールに出す代わりにクライアントに送り返すところです。つまり、messageReceivedを以下のように修正し直すだけで十分です。

	 @Override
	 public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
	     Channel ch = e.getChannel();
	     ch.write(e.getMessage());
	 }

10. ChannelEventオブジェクトは関連するChannelへの参照をもっています。つまり、Channelは受け取ったMessageEventの接続を表しています。Channelを取得しwriteメソッドでリモートの相手に対して何かを戻すことができます。


telnetコマンドをもう一度実行してみましょう。何を送っても返してくれると思います。
この全体のソースコードは、配布物のorg.jboss.netty.example.echoに含まれています。

5. TIMEサーバーを書く

この項ではTIMEプロトコルを実装します。前の例と違うのは、何も受け取らず32bitの整数を含むメッセージを送り、送った後にすぐに切断するところです。この例では、メッセージの構築の仕方と完全にコネクションを閉じることを学びます。
どんなデータを受け取っても無視するためコネクションが確立した瞬間にメッセージを送ります。この度はmessageReceivedは使えません。代わりに、channelConnectedメソッドを使います。以下のような実装になります。

package org.soichiro.nettystudy.example.time;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

public class TimeServerHandler extends SimpleChannelHandler {

    @Override
    public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
        Channel ch = e.getChannel();
        
        ChannelBuffer time = ChannelBuffers.buffer(4);
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
        
        ChannelFuture f = ch.write(time);
        
        f.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture future) {
                Channel ch = future.getChannel();
                ch.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}

11. 説明すると、channelConnectedメソッドは接続が確立した時に呼ばれます。そして、現在時間の秒数を表す32bitのintegerを書きます。
12. 新しいメッセージを送るため、メッセージを含む新しいバッファが必要です。32bitのintegerを書くので、ChannelBufferのキャパシティは4バイトです(4 x 8bit)。ChannelBuffersは新しいバッファを確保するためのヘルパークラスです。bufferメソッドに加え、多くの便利な関連するメソッドを持っています。もっと知るために、APIリファレンスを参照して下さい。
一方で、ChannelBuffersをstaticインポートすると、以下のように書けます。

 import static org.jboss.netty.buffer.ChannelBuffers.*;
 ...
 ChannelBuffer  dynamicBuf = dynamicBuffer(256);
 ChannelBuffer ordinaryBuf = buffer(1024);

13. 普段は、構築されたメッセージを書きますがちょっとまってください、filpメソッドはどこでしょうか?私たちは、NIOにメッセージを送る際にByteBuffer.flip()を読んでいません。ChannelBufferはそのようなメソッドをもっていません。なぜならそこには2つのポインタしかないからです。一つは読む操作のためのもの、もうひとつは書く操作のためのものです。(ちなみにNIOのByteBufferのposition, limit, capacityの話は、http://kimama2index.info/coolJava/channel/buffer.html の日本語の資料が参考になる) writer indexはChannelBufferに書き込みを行った場合に増加します。その場合reader indexは増加しない。reader indexとwriter indexはそれぞれ、メッセージの最初と終わりをそれぞえ表しています。
対照的に、NIO のバッファはflipメソッドを除いてメッセージの最初と終わりを表す明確な方法を提供していません。flipし忘れて不正なデータを送ったり送れなかったりするトラブルに見舞わるでしょう。Nettyの場合そんなエラーは発生しません。なぜなら、操作の異なる違うポインターを用意しているためです。あなたはflipをしない生活がとても簡単だということにきづくでしょう。
writeメソッドが返すChannelFutureの別な点を記述します。ChannelFutureはまだ起こっていないIOの操作を表すものです。つまり、まだ実行されていないリクエストです。なぜならば、Nettyの全ての操作は非同期だからです。例えば、以下のコードではメッセージを送るまえに 接続を閉じてしまいます。

Channel ch = ...;
ch.write(message);
ch.close();

つまり、closeメソッドは、ChannelFutureでwriteメソッドの結果が返されてその結果が通知された後実行される必要があります。同様にclose処理もすぐに行われるわけではなく、ChannelFutureを返します。
14. どのようにwriteリクエスト終了したかをきづくのでしょうか。これはシンプルでChannelFutureListenerをChannelFutureに追加することで出来ます。私たちは、オペレーションが行われたあとで、Channelを閉じる非同期のChannelFutureListenerを追加しました。
別に言い換えれば、あなたは元々定義されたリスナーを使って以下のようにシンプルにも書けます。

f.addListener(ChannelFutureListener.CLOSE);

このサーバーが正しく動いているかテストするために、UNIXのrdateコマンドを使えます。

$ rdate -o <port> -p <host>

ポートはmainメソッドで設定されたもので、hostはいつもどおりlocalhostです。(Macにはrdateコマンドを入れる方法がありませんでした、残念。次にTIMEクライアントを書くのでそこで検証します。)


なおTimeServerの実装は、例が配布物に含まれていませんが以下のとおり。

package org.soichiro.nettystudy.example.time;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;

public class TimeServer {

  public static void main(String[] args) throws Exception {
      ChannelFactory factory =
          new NioServerSocketChannelFactory(
                  Executors.newCachedThreadPool(),
                  Executors.newCachedThreadPool());

      ServerBootstrap bootstrap = new ServerBootstrap(factory);

      bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
          public ChannelPipeline getPipeline() {
              return Channels.pipeline(new TimeServerHandler());
          }
      });

      bootstrap.setOption("child.tcpNoDelay", true);
      bootstrap.setOption("child.keepAlive", true);

      bootstrap.bind(new InetSocketAddress(8080));
  }
}
6. TIMEクライアントを書く

DISCARDやECHOサーバーと異なり、TIMEプロトコルはクライアントが必要です。なぜなら、32bitのバイナリデータをカレンダー上の日付に人間は訳せないからです。
この項では、どうやってサーバーが正しく動いているか確かめる方法を議論し、Nettyでクライアントを書きます。


Nettyにおけるサーバーとクライアントの最も大きい唯一の違いは、違うBootstrapとChannelFactoryが必要なことです。以下のコードを見て下さい。

package org.soichiro.nettystudy.example.time;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;

public class TimeClient {

    public static void main(String[] args) throws Exception {
        String host = "localhost";
        int port = Integer.parseInt("8080");

        ChannelFactory factory =
            new NioClientSocketChannelFactory(
                    Executors.newCachedThreadPool(),
                    Executors.newCachedThreadPool());

        ClientBootstrap bootstrap = new ClientBootstrap(factory);

        bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            public ChannelPipeline getPipeline() {
                return Channels.pipeline(new TimeClientHandler());
            }
        });
        
        bootstrap.setOption("tcpNoDelay", true);
        bootstrap.setOption("keepAlive", true);

        bootstrap.connect(new InetSocketAddress(host, port));
    }
}

15. NioServerSocketChannelFactoryの代わりにNioClientSocketChannelFactoryをクライアントサイドのChannelを作るために使います
16. ClientBootstrapがServerBootstrapのクライアントサイドに相当するものです。
17. child.というプレフィックスは不要です。SocketChannelはクライアントサイドでは親を持っていません。
18. bindメソッドの代わりにconnectメソッドを使う必要があります。


見てみるとサーバーサイドのスタートアップとの違いは内容に見えます。ChannelHandlerの方はどうでしょうか?32bitのintegerをサーバーから受け取り、人間が読めるフォーマットに変換し、出力して接続を閉じなくてはいけません。

package org.soichiro.nettystudy.example.time;

import java.util.Date;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

public class TimeClientHandler extends SimpleChannelHandler {

    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
        ChannelBuffer buf = (ChannelBuffer) e.getMessage();
        long currentTimeMillis = buf.readInt() * 1000L;
        System.out.println(new Date(currentTimeMillis));
        e.getChannel().close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
}

とてもシンプルでサーバーサイドの例とは違って見えると思います。しかしながら、このハンドラはIndexOutOfBoundsExceptionを生じて動かないでしょう。なぜこのようなことが起きたのかは次の項で議論します。

7. ストリームベースの通信を扱う
7.1. ソケットバッファーに関する一つの小さな警告

TCP/IPのようなストリームベースの通信では、受け取られたデータはソケットのrecive bufferで受け取られる。不幸なことに、ストリームペースの通信のバッファは、パケットのqueueではなく、byteのqueueとなっています。これは、2つのメッセージを異なる独立したパケットとして送った場合、オペレーションシステムはこれらを2つのメッセージとしては扱わず、ただバイトの束として扱うということです。つまり、リモートで書かれたものが正確に読まれるという保証がないのです。例えば、OSのTCP/IPが3つのパケットを受け取ったと想定します。

                                    • +
ABC DEF GHI
                                    • +

ストリームベースのプロトコルの一般的な性質のため、あなたのアプリケーション上では以下の様な断片で受け取る可能性が高い。

                                          • +
AB CDEFG H I
                                          • +

つまり、サーバーサイドかクライアントサイドかによらず受け取りの部分では、受け取った一つまたは複数のフレームを結合し、アプリケーションのロジックが簡単に理解できるようにしなくてはならない。上の例で言うならば、データは以下のようにフレーム分けされないといけません。

                                    • +
ABC DEF GHI
                                    • +
7.2. 第一解決策

TIMEクライアントの例に戻ろう。わたちたちは同じ問題に面しています。32bitのintegerはとても小さなデータです。そして、よく断片化しそうな幹事ではない。しかし、実際には断片化しています。断片化の可能性はトラフィックが増えるほど増す。
最も簡単な解決案は、内部的な累積バッファを作り、全ての4バイトを内部バッファないに受け取るまで待つことです。以下は、そのようにしてTimeClientHandlerを実装して問題を修正した例です。

package org.soichiro.nettystudy.example.time;

import static org.jboss.netty.buffer.ChannelBuffers.dynamicBuffer;

import java.util.Date;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
 
 public class TimeClientHandler extends SimpleChannelHandler {
 
     private final ChannelBuffer buf = dynamicBuffer();
 
     @Override
     public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
         ChannelBuffer m = (ChannelBuffer) e.getMessage();
         buf.writeBytes(m);
         
         if (buf.readableBytes() >= 4) {
             long currentTimeMillis = buf.readInt() * 1000L;
             System.out.println(new Date(currentTimeMillis));
             e.getChannel().close();
         }
     }
 
     @Override
     public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
         e.getCause().printStackTrace();
         e.getChannel().close();
     }
 }

19. 必要に応じて容量を増加させるChannelBufferは動的バッファであります。メッセージの長さがわからない時にはとても便利であります。
20. 最初に、全てのデータがbufの中に蓄積されます。
21. そして、bufの長さが十分かどうかチェックし、この例では4バイトに達した時に実施のビジネスロジックを実行します。一方で、もしもっとデータが到着した場合、NettyはmessageReceivedをもう一度呼び出し、最終的に4バイトずつ累積します。

7.3. 第二解決策

第一解決策でTIMEクライアントの問題を解決したが、修正したハンドラはクリーンには見えない。もっと複雑なプロトコルを想像しよう。様々なフィールドを複数持つようなものです。あなたのChannelHandlerはメンテナンス不可だが速いというものになるだろう。
あなたが多くに気を払う時、あなたは一つ以上のChannelHandlerをChannelPipelineに足すことができる。つまり、あなたのアプリケーションの複雑性を減らすために複数のモジュールに巨大なひとつのChannelHandlerを分割することができる。例えば、TimeClientHandlerは2つのハンドラに分割できる。

  • 断片化問題を扱うTimeDecoder
  • 最初のシンプルなTimeClientHandler

幸運なことに、Nettyは最初の物を箱の外に書くような拡張クラスを提供しています。

package org.soichiro.nettystudy.example.time;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;

public class TimeDecoder extends FrameDecoder{
	 
    @Override
    protected Object decode(
            ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
            
        if (buffer.readableBytes() < 4) {
            return null; 
        }
        
        return buffer.readBytes(4);
    }
}

22. FrameDecoderはおの断片化問題を簡単に扱うChannelHandlerの実装です。
23. FrameDecoderはdecodeメソッドをメンテナンスされた累積バッファを用いて呼ぶ。
24. nullが返った場合、まだデータが十分ではないということです。FrameDecoderは十分の量のデータが来た時にもう一度呼ばれる。
25. もしnullではないデータが帰った場合、decodeメソッドが成功したということであります。FrameDecoderは読まれた部分を内部バッファから破棄します。複数のメッセージをデコードする必要が無いことを思い出して欲しい。FrameDecoderはdecorderメソッドをnullの間呼び続ける。

なお、TimeClientのパイプラインの部分は以下の様な実装となる。

        bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            public ChannelPipeline getPipeline() {
                return Channels.pipeline(
                		new TimeDecoder(),
                		new TimeClientHandler());
            }
        });


もしあなたが一歩進んだ人なら、decorderをもっとシンプルにするReplayingDecoderを試してみたいだろう。もしもっと情報が欲しい場合にはAPIリファレンスをもっと読む必要があります。

package org.soichiro.nettystudy.example.time;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.replay.ReplayingDecoder;
import org.jboss.netty.handler.codec.replay.VoidEnum;

public class TimeDecoder extends ReplayingDecoder<VoidEnum> {
	 
    @Override
    protected Object decode(
            ChannelHandlerContext ctx, Channel channel,
            ChannelBuffer buffer, VoidEnum state) {
            
        return buffer.readBytes(4);
    }
}

加えて、Nettyはもっとプロトコルを簡単にし、巨大なメンテナンスできないハンドラの実装を避けることを助けるout-of-the-boxのデコーダーを提供します。以下のパッケージの例を参照して欲しい。

8. ChannelBufferの代わりのPOJOを使って通信する

いま見てきた全ての例において、プロトコルのメッセージとしてずっとChannelBufferが使われてきました。この項では、TIMEプロトコルクライアント、サーバーをChannelBufferの代わりにPOJO(Plain Old Java Object)を使うように改善します。
POJOをあなたのChannelHandlerで使う利点は、明白性にあります。あなたのハンドラはよりメンテナンスできるようになりChannelBufferから情報を抽出することでコードが分割されて再利用性が高まります。このTIMEクライアント、サーバーの例では、32bitのintegerのみを読むので、ChannelBufferを直接読む問題はありません。しかし、実装と現地つ世界のプロトコルを分割することが重要なことを理解してもらいます。
まず最初に、UnixTypeという型を定義しましょう。

package org.soichiro.nettystudy.example.time;

import java.util.Date;

public class UnixTime {
	private final int value;

	public UnixTime(int value) {
		this.value = value;
	}

	public int getValue() {
		return value;
	}

	@Override
	public String toString() {
		return new Date(value * 1000L).toString();
	}
}

TimeDecoderをUnixTImeをChannelBufferの代わりに返すように改定します。

	 @Override
	 protected Object decode(
	         ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
	     if (buffer.readableBytes() < 4) {
	         return null;
	     }
	 
	     return new UnixTime(buffer.readInt());
	 }

26. FrameDecoderとReplayingDecoderはどんな型のオブジェクトでも返せるようになっています。もしChannelBufferだけを返すように制限したい場合は、ChannelBufferをUnixTimeに変更する別なChannelHandlerを追加しなくてはいけません。


このdecoderの更新で、TimeClientHandlerもうChannelBufferを使えなくなりました。

	 @Override
	 public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
	     UnixTime m = (UnixTime) e.getMessage();
	     System.out.println(m);
	     e.getChannel().close();
	 }

よりシンプルでエレガントではないでしょうか?同じテクニックをサーバーサイドにも適用します。まずはTimeServerHandler。

	 @Override
	 public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
	     UnixTime time = new UnixTime((int)System.currentTimeMillis() / 1000);
	     ChannelFuture f = e.getChannel().write(time);
	     f.addListener(ChannelFutureListener.CLOSE);
	 }

今、かけている要素としてはエンコーダががあるが、それはUnixTimeをChannelBufferに翻訳するChannelHandlerの実装です。パケットの断片化の問題とアッセンブルのに配慮する必要がないためとてもシンプルに書ける。

package org.soichiro.nettystudy.example.time;

import static org.jboss.netty.buffer.ChannelBuffers.buffer;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;

public class TimeEncoder extends SimpleChannelHandler {

    public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) {
        UnixTime time = (UnixTime) e.getMessage();
        
        ChannelBuffer buf = buffer(4);
        buf.writeInt(time.getValue());
        
        Channels.write(ctx, e.getFuture(), buf);
    }
}

27. エンコーダーは書き込みリクエストに割り込むためにwriteRequestedメソッドをオーバーライドします。MessageEventパラメーターに注目してください。messageReceivedで設定されたものと同じタイプですが、異なる割り込みをします。ChannelEventはアップストリームかダウンストリームかのどちらかのイベントで、イベントの流れに依存します。例えば、MessageEventは、messageReceivedで呼ばれるときはアップストリームでり、writeRequestedで呼ばれるときはダウンストリームとなります。アップストリームイベントとダウンストリームイベントの差をもっと知りたい場合は、リファレンスを参照下さい。
28. POJOをChannelBufferに一度変換したら、新しいバッファを前のChannelDownstreamHandlerにChannelPipelineで転送しなくてはいけません。ChannelsはChannelEventを生成したり送ったりする様々なヘルパーメソッドを提供しています。この例では、Channels.write(...) メソッドは新しいMessageEventが作られ、ChannelPipelineの中でそれを前のChannelDownstreamHandlerに送っています。
一方で、Channelsのstaticインポートを使うとこうも書けます。

import static org.jboss.netty.channel.Channels.*;
...
ChannelPipeline pipeline = pipeline();
write(ctx, e.getFuture(), buf);
fireChannelDisconnected(ctx);

最後のタスクはTimeEndocerをサーバーサイドのChannelPipelineに挿入し、あとは取るに足らないエクササイズが残るのみとなります。

   bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
          public ChannelPipeline getPipeline() {
              return Channels.pipeline(new TimeServerHandler(),
            		  new TimeEncoder());
          }
      });
9. アプリケーションをシャットダウンする

TimeClientを起動すると、アプリケーションが何もせずに起動し続けていることにきづくだろう。全てのスタックトレースから見ると、2つのIOのスレッドが動き続けていることわかる。これらのIOスレッドをシャットダウンし、上品にアプリケーションを終了させるには、ChannelFactoryのリソースを開放する必要があります。
ネットワークアプリケーションの形式的なシャットダウンのプロセスは、以下の3つで構成されます。

  1. なんであれ、全部サーバーのソケットを閉じる
  2. なんであれ、サーバーではない全部のソケットを閉じる(クライアントソケットやアクセプトソケットなど)
  3. ChannelFactoryが持つ全てのリソースを開放する


この3つのステップをTimeClientに適用すると、TimeClient.main() は自身を上品にシャットダウンするため、唯一のクライアントコネクションを閉じて、ChannelFactoryの全リソースを開放します。

package org.soichiro.nettystudy.example.time;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;

public class TimeClient {

    public static void main(String[] args) throws Exception {
        String host = "localhost";
        int port = Integer.parseInt("8080");

        ChannelFactory factory =
            new NioClientSocketChannelFactory(
                    Executors.newCachedThreadPool(),
                    Executors.newCachedThreadPool());

        ClientBootstrap bootstrap = new ClientBootstrap(factory);

        bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            public ChannelPipeline getPipeline() {
                return Channels.pipeline(
                		new TimeDecoder(),
                		new TimeClientHandler());
            }
        });
        
        bootstrap.setOption("tcpNoDelay", true);
        bootstrap.setOption("keepAlive", true);

        ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port));
        future.awaitUninterruptibly();
        if (!future.isSuccess()) {
            future.getCause().printStackTrace();
        }
        future.getChannel().getCloseFuture().awaitUninterruptibly();
        factory.releaseExternalResources();

    }
}

29. ClientBootstrapのconnectメソッドはChannelFutureを返す。これは接続が成功または失敗した場合に通知します。同様に接続に関連するChannelへの参照を持つ。
30. ChannelFutureは、接続が成功したかしてないかが判定されるまでを待ちます。
31. もし失敗した場合、なぜ失敗したかをプリントします。ChannelFutureのgetCause()メソッドは接続が成功もキャンセルもサれなかった場合、どのような失敗が起こったのかを返す。
32. 接続への試みが終了した場合、ChannelのcloseFutreで接続が閉じられるのを待つ必要があります。全てのChannelは自身のcloseFutreを持っており、閉じられた時に実行され通知を行う。
たとえ接続への試みが失敗してもcloseFutreは呼ばれる。なぜなら、Channelは接続が失敗したら自動的に閉じられるためであります。
33. 全てのコネクションはこの時点で閉じられています。残ったタスクはChannelFactoryの全てのリソースを開放することであります。やり方は、releaseExternalResources()メソッドをよぶだけであります。NIO Selectorsやスレッドプールを含む全てのリソースがシャットダウンし、自動的に終わらせられる。


クライアントのシャットダウンはとても簡単です。しかし、サーバーはどうだろうか?まずポートをアンバインドし、全部のacceptedコネクションを閉じる。これをやるために、アクティブな接続のリストのトラックを維持するデータ構造が必要になるだろう。そして、これは平凡な仕事ではない。幸運なことにそれには解決策があります。ChannelGroupです。


ChannelGroupはJava Collections APIの特別な拡張です。これは開いているChannelのセットを表す。もしChannelがChannelGroupに加えられた場合、そして、Channelが閉じられた場合、自動的にChannelGroupから取り除かれる。あなたはただ同じグループの全てのチャンネルに一つの操作を実行すれば良い。例えば、サーバーにおいて全てのChannelGroupのChannelを閉じれば良い。


開いたソケットのトラックを維持するために、TimeServerHandlerでChannelが開いた際にグローバルのChannelGroupにChannelを追加しよう。TimeServer.allChannelsだと

 @Override
 public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
     TimeServer.allChannels.add(e.getChannel());
 }

34. もちろんChannelGroupはスレッドセーフです。

今全てのアクティブなチャンネルは自動的にメンテナンスされています。サーバーをシャットダウンするは、クライアントをシャットダウンするのと同じように簡単です。

package org.soichiro.nettystudy.example.time;

import java.net.InetSocketAddress;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.ChannelGroupFuture;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;

public class TimeServer {

  final static ChannelGroup allChannels = new DefaultChannelGroup("time-server");
  
  public static void main(String[] args) throws Exception {
	  
      ChannelFactory factory =
          new NioServerSocketChannelFactory(
                  Executors.newCachedThreadPool(),
                  Executors.newCachedThreadPool());

      ServerBootstrap bootstrap = new ServerBootstrap(factory);

      bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
          public ChannelPipeline getPipeline() {
              return Channels.pipeline(new TimeServerHandler(),
            		  new TimeEncoder());
          }
      });

      bootstrap.setOption("child.tcpNoDelay", true);
      bootstrap.setOption("child.keepAlive", true);

      Channel channel = bootstrap.bind(new InetSocketAddress(8080));
      allChannels.add(channel);
      waitForShutdownCommand();
      ChannelGroupFuture future = allChannels.close();
      future.awaitUninterruptibly();
      factory.releaseExternalResources();
  }
  
  private static final CountDownLatch shutdownSignal = new CountDownLatch(1);
  private static void waitForShutdownCommand() {
	  Thread shutdown = new Thread() {
	      public void run() { 
	    	  shutdownSignal.countDown();
	      }
	  };
	  Runtime.getRuntime().addShutdownHook(shutdown);
	  try {
		shutdownSignal.await();
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
  }
}

35. DefaultChannelGroupはグループ名を引数として必要とします。グループ名は単独で他のグループと区別が付く名前です。
36. ServerBootstrapのbindメソッドは、ローカルアドレスが設定されたChannelを返します。close()メソッドが呼ばれるとChannelはローカルアドレスをアンバインドします。
37. サーバーサイド、クライアントサイド、アクセプテッドに限らずどんなタイプのChannelもChannelGroupに追加することができます。つまりアクセプテッドなチャンネルを一回で、Channel領域まるまるサーバーをしゃっとだうんですることができます。
38. waitForShutdownCommand()はサーバーのシャットダウンを待つ仮想のメソッドです。特権的なクライアントのからのメッセージやJVMのシャットダウンをフックするように実装してください。
39. 同じChannelGroupに置いてすべてのチャンネルが同じオペレーションが実行されます。この場合、全てのチャンネルを閉じます。これは、サーバーサイドのチャンネル領域を意味し、これらはアンバウンドされて全てのアクセプテッドなコネクションは非同期的に閉じられます。全てのコネクションが閉じたれたことを通知するため、ChannelGroupもChannelFutureを返します。

10. まとめ

このチャプターでは、ネットワーク上で動くNetty上のアプリケーションを書くデモン捨てレーションを交えてNettyのクイックツアーを行いました。
Nettyに関する詳細は、今後の章で紹介します。あと、 org.jboss.netty.example package以下にある他の例も見て見ることをおすすめします。
あと、Nettyのコミュニティはいつも、あなたの質問とあなたを助ける質問を待ち、それらのフィードバックを得てNettyを改良し続けていることを知っていて下さい。



以上となります。お疲れ様でした。

ScalaでEclipseデバック可能な実行jarを作るまでの手順 (例:特定のハッシュタグを含むツイートをファイルに保存し続けるアプリ)

最近Scalaでちょっとしたjarアプリを作ることが多いのでその手順をまとめておきます。まだ発展途上とは言え2.0.2になったことでScala IDE for Eclipseも実用に耐えうるところまできているように思います。あとは、リファクタリング機能とクイックフィックス機能がもう少し良くなると良いですが、コンパイルエラーが出た際に、どうするべきかということがポップアップメニューで出るのでその点で助かっています。
ちなみに今回この記事を書こうと思ったのは、今までSbt0.11.2まで利用できた実行jarを作るproguardというプラグインが利用できなくなってしまったこともあり、sbt-assemblyを利用したものを再度書くことにしました。

環境確認

まず、環境情報の確認です。Macのパッケージ管理システムで利用できる2012/07/28現在でできる限り最新安定版を利用しています。


Javaは、Macの場合は

$ java -version

と実行するとインストールされ、バージョンが確認できます。ソースコード付きのJDKを入れたい方は、この記事を参照。
Homebrewはリンク先を参照してインストールの事。XCodeとCommand line toolsのインストールも必要です。
Scalaとsbtのインストールは

$ brew install scala
$ brew install sbt

で完了です。バージョンアップの際には、

$ brew update
$ brew upgrade scala
$ brew upgrade sbt

を実行します。Eclipse 3.6.2とScala IDE for Eclipse 2.0.2のインストールはリンク先のインストール方法に従って下さい。


というわけで、環境は整ったということでお題であるEclipseデバッグ可能な外部jarライブラリを内包する実行可能jarを作ってきます。お題は、


特定ハッシュタグを含むツイートをファイルに保存し続けるアプリ」


です。すでに実装したものが、GitHubの方にMITライセンスでpushしてありますので、ソースコードだけが欲しい場合はこちらをcloneしてお使いください。

プロジェクトの作成

まず、プロジェクトの作成です。ここではtweet_recorderとします。有りそうな名前ですねw

$ mkdir tweet_recorder
$ cd tweet_recorder

次にjarを作るまでに雛形を作成します。なお基本的にこのtweet_recorderディレクトリから操作していきます。

$ mkdir -p src/main/scala
$ echo 'object TweetRecorder { def main(args: Array[String]) = println("Hi!") }' > src/main/scala/TweetRecorder.scala
$ vim build.sbt

build.sbtを編集

name := "tweet_recorder"

version := "1.0"

scalaVersion := "2.9.2"

次に念のためにsbtもバージョン指定しておきます。

$ mkdir project
$ vim project/build.properties

project/build.propertiesを編集

sbt.version=0.11.3

とここまでで、プロジェクトのひな形は完成。



というわけで早速sbtでビルド、実行してみます。

$ sbt
> compile
[success] Total time: 15 s, completed 2012/07/29 9:00:22
> run
Hi!
[success] Total time: 1 s, completed 2012/07/29 9:00:36
> exit

これで大丈夫なことを確かめます。

実行jarの作成

次にjar化できるようにします。他にもjar化させるものはいくつかありますがsbt-assemblyが非常に良い出来だったので、これを使います。

$ vim project/plugins.sbt

project/plugins.sbtを編集

resolvers += Resolver.url("artifactory", url("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns)

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.8.3")
$ vim build.sbt

build.sbtを編集

import AssemblyKeys._ // put this at the top of the file

assemblySettings

name := "tweet_recorder"

version := "1.0"

scalaVersion := "2.9.2"

mainClass in assembly := Some("TweetRecorder")

他にも設定が、github内に書いてありますが、ここではmainメソッドのあるクラスの定義だけ設定します。
そして早速jarを作ってみます。assemblyコマンドでjar作成とのことです。
早速、jarを作成、実行してみましょう。

$ sbt
> assembly
[info] Done packaging.
[success] Total time: 107 s, completed 2012/07/29 9:14:19
> exit
$ java -jar target/tweet_recorder-assembly-1.0.jar
Hi!

これでOKそうですね。

実行jarへの3rdパーティjarライブラリの梱包

ちなみにもうライブラリとしてtwitter4jを使うことがわかっているので、twitter4jの必要なライブラリとソースをプロジェクトにコピーしてビルドしてみましょう。tmpディレクトリに一度ダウンロードし、削除します。

$ mkdir lib
$ mkdir lib-src
$ mkdir tmp
$ cd tmp
$ curl -O http://twitter4j.org/en/twitter4j-2.2.6.zip
$ unzip twitter4j-2.2.6.zip
$ cp lib/twitter4j-core-2.2.6.jar ../lib
$ cp lib/twitter4j-stream-2.2.6.jar ../lib 
$ cp twitter4j-core/twitter4j-core-2.2.6-sources.jar ../lib-src 
$ cp twitter4j-stream/twitter4j-stream-2.2.6-sources.jar ../lib-src
$ cd ..
$ rm -rf tmp
$ sbt
> assembly
[info] Including twitter4j-core-2.2.6.jar
[info] Including twitter4j-stream-2.2.6.jar
[success] Total time: 1 s, completed 2012/07/29 9:32:14
> exit
$ java -jar target/tweet_recorder-assembly-1.0.jar
Hi!

これでライブラリの実行jarへの内包も大丈夫そうです。


Eclipseへのsbtプロジェクトのインポート

次に、Eclipseにインポートできるようにします。sbteclipseを使います。

$ vim project/plugins.sbt

project/plugins.sbtを編集して、末尾に追記します。

resolvers += Resolver.url("artifactory", url("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns)

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.8.3")

addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.1.0")

編集後、eclipseコマンドを実行します。

$ sbt
> eclipse
[info] Successfully created Eclipse project files for project(s):
[info] tweet_recorder
> assembly
[info] Including twitter4j-core-2.2.6.jar
[info] Including twitter4j-stream-2.2.6.jar
[success] Total time: 1 s, completed 2012/07/29 9:32:14
> exit
$ java -jar target/tweet_recorder-assembly-1.0.jar
Hi!

eclipseプロジェクトの作成も、実行jarの作成もOKそうです。


次に、Eclipseへのインポートです。ScalaパースペクティブのPackage Explorerビューの右クリックメニューにて。Import > General > Existing Projects into Workspaceを選択して、Nextを押します。


Select root directoryで、プロジェクトのフォルダを選択して、Finishボタンを押します。この際に、copyをせずにそのままそのフォルダを利用します。これは今後sbtでビルドを利用するためです。

Twitter4jのStreamingAPIを使った実装とビルド

これでプロジェクトのインポートが終わったので、早速ちょっとコードを書いてデバッグで止めていましょう。
早速サンプルコードを書いてみます。

TweetRecorder.scalaを編集して

import twitter4j.conf.ConfigurationBuilder
import twitter4j.TwitterStreamFactory
import twitter4j.FilterQuery
import twitter4j.StatusListener
import twitter4j.Status
import twitter4j.StatusDeletionNotice
import java.io.PrintWriter
import java.io.FileWriter
object TweetRecorder { 
  def main(args: Array[String]) = {
    val conf = new ConfigurationBuilder().setUser(args(0)).setPassword(args(1)).build
    val twitterStream = new TwitterStreamFactory(conf).getInstance()
    twitterStream.addListener(new Listener)
    twitterStream.filter(new FilterQuery().track(args.slice(2, args.length)))
  }
  class Listener extends StatusListener {
    override def onStatus(status: Status) = {
      val tweet = status.getCreatedAt().toLocaleString() +
      	"," + status.getUser().getScreenName() + 
      	"," + status.getText() 
      val writer = new PrintWriter(new FileWriter("tweet_recoder.csv", true))
      writer.println(tweet)
      writer.close
      println(tweet)
    } 
    override def onDeletionNotice(statusDeletionNotice: StatusDeletionNotice) = {}
    override def onTrackLimitationNotice(numberOfLimitedStatuses: Int) = {}
    override def onException(ex: Exception) = {}
    override def onScrubGeo(userId: Long, upToStatusId: Long) = {}
  }
}

これでOK。起動する時に第一引数がtwitterのアカウントのID、第二引数がtwitterのアカウントのパスワード、第三引数以降は追跡したいキーワード(英単語か#ではじまるハッシュタグのみ) を半角スペース区切りで追加という感じになります。右クリックでDebag As > Scala Applicationを選択してデバッグ実行で引数が無いという例外が投げられて失敗したあと。
虫アイコンのドロップダウンメニューのDebug ConfigurationのArgumentsタグでそれぞれの引数を設定して実行します。


今回は、単語をすごい量のツイートが流れていそうなhttpに設定して実行します。コンソールに

2012/07/29 11:06:30,Vasilisa_Anime,http://t.co/nKcvZJw7
2012/07/29 11:06:30,Neylianavera,esteeeen; x_x @Georgejsm. http://t.co/Ap5vLa1R

のように表示されればOK。今回は日付、ユーザーID、ツイートをカンマ区切りで表示してます。


無論ブレークポイントを貼ってデバッグすることもできます。


最後にビルドして実際に動くか確かめて見ましょう。
今回は、2chまとめのタグ#MT2とニコ動の#nicovideoで検索してみます。

$ sbt
> assembly
[info] Including twitter4j-core-2.2.6.jar
[info] Including twitter4j-stream-2.2.6.jar
[success] Total time: 1 s, completed 2012/07/29 9:32:14
> exit
$ java -jar target/tweet_recorder-assembly-1.0.jar sifue password \#MT2 \#nicovideo
2012/07/29 11:35:08,nicorank_bot,毎時ランキング第1位:じょしらく 第三席「無情風呂」「浅草参り」「真田小ZOO」  http://t.co/0U9SZFnQ #nicovideo #so18447313 2012/07/29 11:35 現在
2012/07/29 11:35:09,2chtitle,「あっ、こいつインド人だな」って思う仕草 インド人とのつきあい方?インドの常識とビジネスの奥義 を Amazon でチェック!  http://t.co/Lf2sWYQ5 #MT2 #まとめ
2012/07/29 11:35:12,zyazya11,RT @Mell_Ta: ニコニコ有名歌い手がポケモン割れ発覚 http://t.co/qQh9eCvL #MT2 今ぐるたみんが熱い

以上のように表示され無事利用できるようです。これで好きなハッシュタグのツイートなどを保存して解析したりするのに使えますね。
ちゃんとtweet_recoder.csvというファイルを確かめても保存できているようです。


以上でScala2.9.2でsbt-assemblyを使ってEclipseでデバック可能な3rdパーティjarを含む実行jarを作るまでの手順を終わりとします。お疲れ様でした。

イングリッシュ記法(名前に情報を追加する)は、誰もが行うべき重要な変数命名法

リーダブルコードを読み終えました。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

Java, JavaScript, PHPがメインな自分としては、過去に読んだEffective JavaJavaScript: The Good Parts、Clean Code、リファクタリング、プログラミング作法と基本的に同じ情報ではあったのですが、ここ最近のトレンドを取り込み、薄い本としてまとめているというところに非常に価値のある本でした。


その中でも一番のお気に入りは、「2.4 名前に情報を追加する」の項で書かれているハンガリアン記法と区別してイングリッシュ記法を提唱している以下の部分です。

これってハンガリアン記法なの?

ハンガリアン記法というのは、Microsoft社で広く使われていた命名規則だ。すべての変数名の接頭辞に「型」をつける。例えばこんな感じだ。

変数名 意味
pLast あるデータ構造の最後の要素を指すポインタ(p)
pszBuffer ゼロ終端(z)の文字列(s)バッファを指すポインタ(p)
cch 文字(ch)のカウント(c)
mpcopx カラーポインタ(pco)からX軸のポインタ(px)を指すマップ(m)

これも「名前に属性を追加」しているけれど、もっと厳密で規律のあるシステムだ。
一方、僕達が提唱しているのは、もっと大まかで規律のゆるいシステムだ。必要な時にだけ変数の大切な属性を見つけ出して、それを読みやすくして名前に追加する。これをハンガリアンならぬ「イングリッシュ記法」と呼んでもいいだろう。


この項では、変数名を

var start = (new Date()).getTime();

ではなく

var start_ms = (new Date()).getTime();

と書いたりして、変数名に属性を追加しわかりやすくしようということを言っています。


載っていた例としては

状況 変数名 改善後
passwordはプレインテキストなので、処理をする前に暗号化すべきである。 password plaintext_password
ユーザーが入力したcommentは表示する前にエスケープする必要がある。 comment unescaped_comment
htmlの文字コードUTF-8に変えた。 html html_utf8
入力されたdataをURLエンコードした。 data data_urlenc

といったものです。


このイングリッシュ記法は本当に重要で、変数をわかりやすくする他、変数名にあえて属性を入れることで変数の意図しない使い回しを避けさせる効果もあります。

var start = new Date();
if('ms' === type){
    start = start.getTime();
}else if('array' === type){
    start = [start];
}else if('sec' === type){
    start = start / 1000;
}else if('string' === type){
    start = "" + start;
}

// ... この数百行程度の別なコード

// TODO 1年後、startを使った処理を他人が書く。バグらないだろうか?

こういうコードが本当に多いのも難点ですが...、もしこのstartという変数がstart_msやstart_arrayだとすれば、さすがにミリ秒や配列以外のものを代入しようとは思いませんし、また後でこの変数を利用する人が余計な型チェックや状態チェック、特にsecとmsの判定などをせずに、安心して変数をつかうことができます。


このようにイングリッシュ記法には、余計なコードを排除し、わかりやすくなってバグを減らせるという素晴らしい効果があります。しかもエディタの入力補完があるので、総コーディングタイプ数はそんなに変わりません。


あとこれは前職の先輩に教えてもらったテクニックなのですが、選別に使いたい属性を先にしておくと入力補完機能のあるエディタのあるEclipsevimemacsでは非常に良い副作用があります。

var buttonTitle = null;
var buttonTextArea = null;
var buttonCombobox = null;
var labalTitle = null;
var labalTextArea = null;
var labalCombobox = null;
var titles = null;
var mapCombboxContent = null;

大抵コーディングする場合には、そこに書くべき変数の型というのは決まっていることが多いです。 この場合はtitを押して入力補完を実行するとすぐにタイトルの配列titlesが取れます。
これが、もしボタンやラベルまでtitleが最初に付く変数になっていると、後でボタンなのかラベルなのか配列なのかを候補の中から選ばなくてはいけなくなってしまいます。


実装の内容にもよりますが、属性をつけるのを前にするか後にするかでコーディングスピードを変えることができる、というのもこのイングリッシュ記法のメリットだと思います。この辺も考えながら実装するとより、楽な実装ができるのではないかと思います。
ぜひ、イングリッシュ記法を実践してみてください。では楽しい開発を!

// 2012/07/21 変更 最後の例がシステムハンガリアンを強制するような内容であったのでJavaScriptで書き直し