feel free to skip straight to the second course when CS50 (the first course) moves away from C.
という但し書きの通り、 CS50 の Week 5 以降の講義では主題が C から HTML や CSS, JavaScript, Python に変わってしまい、ざっと見た感じ内容もそれまでと比べて易しくなる印象だったので、 Week 5 以降は受講していません。
受講した結論としては、「C 言語を使ってメモリ管理やアルゴリズム、データ構造などを意識しながら小規模のプログラムを書く」という良い経験ができ、そのへんの素養がなかった私にとってはとても良い勉強になりました。あと、 Week 4 の演習内の最後の課題である “speller” というプログラムの実行速度を競う以下のランキングで、いろいろチューニングを頑張った結果現状 3 位にまでランクインすることができた (2018 年 3 月 14 日時点) のも嬉しい事柄でした。
この位置につけるまで結構試行錯誤したので、嬉しくて思わずスクリーンショットを取ってしまいました (でもまだ上がいるんですよね……)。
受けたかった部分の受講は完了し、それなりに満足できる結果も出せて一区切りついたので、 CS50 の Week 1 ~ 4 で学んだ内容をまとめてみようと思います。
これは edX というオンラインコースプラットフォーム (いわゆる MOOC) 上で公開されている講座で、 Harvard 大学の CS50 というコンピュータサイエンス入門用の授業がオンラインで公開されているものです。受講するだけならアカウント登録をおこなえば無料で受けられます (これに限らず、 OSSU で紹介されている講座はすべて基本的に無料です。)
この講座に限らずオンライン講座一般のいいところとしては、多くの場合レクチャーのビデオと演習用の課題がセットで用意されており、スムーズに独学できるところだと感じています。
レクチャーはすべて英語ですが、字幕も表示できるのでそれで補足しながらであれば説明されていることは概ね把握できますし、スライドや PC の画面も表示されるので理解の助けになります。かつ、この CS50 の担当教授である David J. Malan 先生の熱量がすごく、たとえば生徒も巻き込んでステージ上でアルゴリズムのイメージを実演してくれるなどしながら、毎回非常にわかりやすく講義内容を説明してくれます。ただし私のように非ネイティブの人にとっては、この人の喋りは勢いがすごくかなり早口に感じると思うので、最初それについていくのが大変かもしれません。
各講義回ごとに演習用の課題が用意されており、実際に課題の要件を満たすプログラムを実装して提出する必要があるのもやっていて楽しい点です。 CS50 では Cloud9 をベースにした CS50 IDE という Web IDE が提供されていて、ブラウザ上でコーディングから課題の提出までおこなえるので、ローカルに開発環境を構築する手間がまったく必要ないのもいいところです。実装結果を提出するとリモートでテストが実行され、要件を満たしているかどうか自動的に評価されるようになっていて、これも素晴らしい点だと思います。
ただし Cloud9 のサーバーがおそらく米国にあるため、日本から使うと IDE のターミナル上でのコマンドの入力に微妙に遅延を感じます (遠い国にあるサーバーに SSH して操作するときに感じるラグと同じ感じ)。なお IDE エディタ上のコーティング作業自体はまったく問題ありません。
コンピュータサイエンスの入門講座のため、とても基礎的な内容ですが、私にとっては良い勉強になりました。特に探索やソートといったアルゴリズムや、 C 言語のメモリ管理、そしてデータ構造の実装について実際に手を動かして学ぶことができました。
演習課題も背景知識や実装方針などが非常に丁寧に説明されていて (動画での解説もある)、初学者でもつまづかないようかなり配慮されているように感じられました。課題の内容も結構おもしろいものが多かったです。
何かしらプログラムを書いたことがある立場からすると、 Week 1 の内容は簡単でした。プリミティブなデータ型、演算子、ループ、条件分岐といった、 C 言語の入門として最初に習うものです。
ただし演習課題の1つである Credit という問題で、「クレジットカード番号の単純な認証には Luhn アルゴリズムによるチェックサムが使われている」という背景知識が知れたことは勉強になりました。
int
, double
, …)+
, -
, …)for
, while
)if
, switch
)演習課題は “less comfortable” と “more comfortable” のうちどちらか 1 つずつをやればいいようになっていましたが、せっかくなので両方やりました。
Week 1 の続きの講義もまだまだ初歩的な内容でした。主に文字列と配列に焦点が当たっていました。 C の文字列は非常に原始的で「NUL 文字 \0
で終端された単なる char
の配列である」という点は、日頃現代的な言語を利用していると不便さを感じるときもありますよね。
おもしろかったのは Crack という演習課題で、 unistd.h
の crypt
関数で暗号化されたパスワードを、ブルートフォースアタックにより解読するという内容でした。
crypt
関数で暗号化されたパスワードのハッシュ文字列から、もとのパスワード文字列を見つけ出すプログラムを実装する上記 3 つ中 2 つを提出すればよいことになっていましたが、せっかくなので 3 つ全部やりました。 Crack の課題では、パスワードはすべてアルファベットで最長でも 5 文字という前提になっており、総当りで解読できるような条件設定になっています。 C で 1 ~ 5 文字の文字列の全組み合わせを生成する処理の実装方法に少し悩んだ記憶があります。
Week 2 から私にとってはだんだんおもしろくなってきました。アルゴリズムの勉強の最初に習う探索、ソート、計算量の話です。マージソートの計算量が になる理由がよくわかっていなかったので、ちゃんと理解することができて勉強になりました。ただソートではクイックソートやヒープソートといった、現在主に使われているであろう高速ソートの説明がなかったのが少し残念です。
今回の Music という課題もなかなかおもしろい演習で、「楽譜データを読み込んでその曲の WAV ファイルを生成するプログラムの一部を実装する」というものでした。
この課題は一部のヘルパー関数を実装するだけで、実装ボリューム自体は全然大したことないのですが、音楽の知識がなかったので背景知識をきちんと把握するまでに手間取りました。あと日本人は音階の表記に「ドレミファソラシ」を使うので、 “CDEFGAB” を使う欧米の表記法との間を脳内で相互変換するのも最初苦労しました。
まずこのプログラムの概要は、楽譜データのテキストファイルを読み込んで、記載された各音符の音階と長さの系列データをもとに、波形データを生成して WAV ファイルとして出力する、というものになっています。
WAV ファイルの生成処理などはライブラリが同梱されており、大部分の処理についてはもともと実装されています。演習で実装するのは、楽譜のテキストデータの各音符の情報を入力として
is_rest
duration
frequency
という3つのヘルパー関数だけなので、実装ボリューム自体は大して多くありません。
ですが個人的に難しかったのが frequency
の実装です。背景知識を読んでへーと思ったのが、黒鍵の音符には2通りの表記法があるという点でした。たとえばド
とレ
の白鍵の間の黒鍵の音は、ド#
ともレb
とも表せるということです。なのでどちらの音符であっても、同じ周波数を出力しないといけません。
また、各音符の高さの周波数は「4オクターブめのラ
(A4
)」の 440 Hz を中心に、そこからの黒鍵を含めた音階の差分をもとに計算します。しかしながら「ミ
とファ
」や「シ
とド
」の間には黒鍵がありません。また、オクターブは「ドレミファソラシド」で一周すると1つ上下します。なので、たとえば「6オクターブめのミ
(E6
) は基準となる4オクターブめのラ
(A4
) から何音階分離れているか」といったことをどのように計算すればよいのか結構考えました。
最終的に正しく実装できて、同梱されている楽譜データの「きらきら星」や「ロンドン橋落ちた」、「Jeopardy! のテーマソング」などがちゃんと再生されたときは達成感があり、嬉しかったです。
演習課題としてはおもしろかったのですが、でもこれ探索もソートも計算量も全然関係ない問題なんですよね。なんでこの回の課題がこれなのか不思議です。
Week 3 ではメモリ管理やポインタが主題でした。私がよく使う Go 言語だとありがたいことに GC とエスケープ解析があるので、通常各ポインタのオブジェクトがスタックとヒープのどちらに置かれているか意識しないで済んでしまいます。一方 C 言語ではプログラマがちゃんと考えて、スタックにアロケートするかヒープにアロケートするか判断する必要があります。このように自力でメモリ管理をするプログラムを書く経験もやはり大事だなと感じました。
Week 3 の演習課題はすべて Bitmap (BMP) の画像ファイルを扱う内容で、「BMP ファイルのヘッダ情報や画像データのレイアウトはどうなっているのか」といったことを実装を通してよく理解することができるようになっており、これもかなり勉強になる演習でした。 BMP ファイルは他の画像形式と比較すると中身が単純なため、画像ファイルのデータ構造の入門に適していると思います。
malloc
とヒープどれも BMP ファイルのヘッダ情報の詳細や画像データのレイアウトをきちんと理解した上で取り組まないとできない課題になっているため、 Bitmap の仕様のドキュメントをかなり読みました。また当然 malloc
を使うところもあるので、レクチャーの内容に関連した応用課題にもなっています。
個人的に上記 3 つの中で一番難しかったのが Resize の実装で、入力 BMP 画像をリサイズした結果として、ヘッダ情報と画像データレイアウト (特にパディング) が両方とも適切に変更された画像を生成しないといけません。どこで嵌っていたのか忘れてしまったのですが、これがなかなか正しく実装できず、たとえばニコちゃんマーク画像を拡大しようとしてもグチャグチャな出力画像になってしまったりして、正しい挙動をする実装にたどり着くまでにだいぶ苦労した記憶があります。最終的にちゃんと大きなニコちゃんマーク画像が出力されたときはやったーと思いました。
Week 4 は一番勉強になった講義回でした。データ構造はコンピュータサイエンスの基礎知識として非常に重要ですし、特にハッシュテーブルは数あるデータ構造の中でももっとも便利で利用頻度の高いものだと思うので、その実装方法を理解しておくことは大切だと感じました。言語組み込みや標準ライブラリにハッシュテーブルがない C 言語で自力で実装してみると、「ハッシュテーブルはリンクリストの配列により実装できる」ということがよく理解できますし、バケット数とエントリ数の比率であるロードファクターという指標がなぜ重要なのかも具体的にわかります。また、検索が確実に でおこなえるトライ木も、その有用さがわかりました。
今回の演習課題がこれまでで一番やりがいのあるものでした。プログラムの実行速度を競えるランキングがあるので、いろいろ試行錯誤しながらタイムを短縮していく楽しみがありました。
これがこの記事の冒頭で述べていた演習課題です。プログラムの大枠はあらかじめ実装されており、演習として実装する部分は以下の4つの関数です。
load
check
size
unload
プログラムの流れとしては、 load
関数で辞書ファイルを読み込んで内部で適切なデータ構造 (ハッシュテーブルやトライ木など) として保持したあと、文章ファイルを単語単位で読み込んでいき、それが辞書データに含まれている単語か否かを check
関数で判別していくという流れです。最後の unload
関数は malloc
したデータを適切に解放する処理をおこなうステップで、ヒープにアロケートしたオブジェクトをここですべてきちんと free
しないと、 Valgrind によるメモリリークチェックで怒られてしまいます。
そしてプログラム内でそれぞれの関数の処理にかかった時間が計測されるようになっており、それを如何に縮められるかが頭の使いどころになっています。辞書データの保持方法として「ハッシュテーブルを使った実装方法」と「トライ木を使った実装方法」がそれぞれ説明されている非常に丁寧な解説動画が用意されているため、要件を満たす各関数を実装すること自体はそれほど苦労せずにおこなえます。しかしながら、単にそれに沿った実装をしただけだとあまり速度は出ません。
この演習課題のおもしろいところは、以下のように実行速度のランキングが集計されて公開されており (誰でも見られます)、生徒同士 (とコーススタッフ) で自分が実装したプログラムのタイムを競えるようになっているところです。2018 年 3 月 14 日時点で 330 名以上がエントリしています。
私の場合、一番最初の実装ではたしか 20 ~ 30 位の間ぐらいだったと思います。その後いろいろと試行錯誤しながら実行速度の改善を重ねていき、運の良さもありましたが最終的には現時点で 3 位にランクインするタイムを出すことができました。これを通じてパフォーマンスチューニングという観点での学びがたくさんあり、非常に勉強になりました。今回はもう記事が長くなってしまったので細かい話は書きませんが、今後また別の記事でこの演習において自分がやったことの詳細を書くかもしれません (ただしこの演習課題に特化した話をしても誰のためにもならないと思うので、一般的な話に汎化したいとは思います)。
実際のところ私よりも上位のランカーがまだいるわけなので、まだ最適化の余地は残されているんだろうとは思っていますが、今の私の知識や技術ではこれ以上の改善は難しい状況なので、いつかまた修行を積んでから再度挑戦できたらいいなと思います。このランキングの参加者は基本的に初学者のはずで、演習課題の実装内容自体も簡素なものなので、おそらく速度に気を使って C / C++ 言語を長い間書いてきたプログラマの方であれば、私よりも全然速いプログラムを書ける可能性がかなり高いと思っています。なのでもしこの記事を読んで興味が沸いた方は、この演習だけでもやってみるとおもしろいかもしれません。
何かを勉強する方法として、良質なオンラインコースがあるようであれば、それをやってみるのは 1 つの方法としてありだなーと感じました。もちろん書籍を読んだりするほうが勉強効率としては桁違いに良いはずで、それで進められるのが理想的だと思います。オンラインコースは時間がかかる割にはあまり多くのことを学ぶことはできないでしょう。しかしながら演習課題があって自分で手を動かし、しかもその結果がすぐに評価される仕組みの上で取り組むと、ゲーミフィケーション的な効果で自分が積極的に参加している感じになり、モチベーションを高く維持することができるなと感じました。特に基礎的・入門的な内容の勉強にはオンラインコースは 1 つの選択肢としてありなのではないかと思います。
あと個人的に良かった副作用として、 C 言語が結構好きになりました。日頃現代的な言語を使っていると古臭い言語に感じることは確かで、実際不便な点やバグを生みやすい点も数多くありますが、この素朴さは特に低レイヤのプログラムを書くのにはやっぱり使いやすいんだろうなと思います。今となっては C でも Go でも問題なくポインタを扱うことができるようになっていますが、実は昔 C のポインタで一度挫折した経験があったので、それを無事克服することができてよかったです。
OSSU のカリキュラムに従い、次は Introduction to Computer Science and Programming Using Python という MIT のオンラインコースを受講しています。これまで Python はほとんど書いたことがありませんでしたが、書いてみるとこれも非常に使いやすい言語で、人気の理由がよくわかります。また一区切りついたらレポートを書くかもしれません。
引き続き日々精進していこうと思います。
]]>今回は第4章「ブロック暗号のモード」です。そもそも、ブロック暗号の「モード」とは何なのでしょうか。
前回の終わりにも書いたように、僕らが暗号化したい平文の長さは、たいていの場合ブロック暗号のブロック長 (AES なら 128 ビット) より長いことが多いです。そのため、ブロック暗号を繰り返し使って、長い文章全体を暗号化する必要があります。このブロック暗号を繰り返す方法のことをブロック暗号の「モード」と呼びます。
ブロック暗号のモードにはいくつもの種類がありますが、今回と次回でその中でも比較的使用が推奨されている CBC モードとCTR モードについて説明します。今回は CBC モードについてです。
なお、この記事中の Go のソースコードのバージョンはすべて 1.6 です。
Go ではブロック暗号のモードはすべて crypto/cipher
パッケージに定義されている BlockMode
という統一のインターフェースを介して利用するようになっています。 CryptoBlocks()
で src
のデータを dst
に暗号化または復号します。
1 | type BlockMode interface { |
Block
インターフェースと違い、 BlockMode
インターフェースには CryptBlocks()
メソッド1つしかありません。これは、そもそも暗号化と復号で別のオブジェクトを用いる必要があるからです。たとえば、あとで見るように、 CBC モードでの暗号化には NewCBCEncrypter()
によるオブジェクトを使って、復号には NewCBCDecrypter()
によるオブジェクトを使って CryptBlocks()
を呼ぶ必要があります。
CBC (Cipher Block Chaining) モードは、1つ前の暗号文ブロックと平文ブロックの内容を混ぜ合わせてから暗号化をおこなう方法です。暗号文ブロックをチェーンのように連鎖させることが名前の由来になっています。
では CBC モードの具体的なアルゴリズムを見てみましょう。
以下の図のように、 CBC モードでは1つ前の暗号文ブロックと平文ブロックの XOR をとってから暗号化をおこないます。 “block cipher encryption” の中身には、たとえば AES を用いるのであればその処理が入ります。
ただし、最初の平文ブロックを暗号化するときには「1つ前の暗号文ブロック」が存在しません。そこで、その代わりに「初期化ベクトル (initialization vector; IV)」というランダムなビット列を用意し、それを用います。
最終的に、生成される暗号文ブロックをつなげたものが暗号化されたデータになります。
逆に、復号化は以下の図のようになります。 XOR の対称的な性質により、復号化したブロックと1つ前の暗号文ブロックの XOR を再びとれば、元の平文が得られます。
Go 言語を使い、実際に CBC モードで長い平文を暗号化してみます。ブロック暗号としては AES を使います。
http://play.golang.org/p/_q54T5eDwe
1 | package main |
標準ライブラリのみを用いて直接暗号化をおこなおうとすると、結構面倒なことがわかります。実際にはアプリケーションが使いやすいようなラッパー処理を書き、それを用いることになると思います。
では標準ライブラリの crypto/cipher
において、 CBC モードがどのように実装されているのか見てみましょう。
CBC モードを表すのは以下の cbc
構造体です。
1 | type cbc struct { |
cbc
構造体のコンストラクタは newCBC()
です。書き換えの影響を受けないように、 cbc.iv
には引数の iv
を完全にコピーしたものが入れられます。
1 | func newCBC(b Block, iv []byte) *cbc { |
cbc
構造体は直接 BlockMode
インターフェースを実装してわけではありません。実際はそのエイリアスの cbcEncrypter
/ cbcDecrypter
型が暗号化 / 復号処理を実装しています。同じ構造体に複数の別名型をつけ、それぞれで違うメソッドを実装するというのは、参考になる実装方法ですね。
暗号化用オブジェクトは NewCBCEncrypter()
で生成します。この戻り値が BlockMode
インターフェースを実装した cbcEncrypter
オブジェクトになります。
1 | type cbcEncrypter cbc |
では、 cbcEncrypter
の CryptBlocks()
メソッドの実装を見てみましょう。やっているのは先に説明したアルゴリズム通り、平文ブロック (src[:x.BlockSize]
) と1つ前の暗号文ブロック (iv
) の XOR をとり (xorBytes()
)、それを暗号化する (x.b.Encrypt()
) ことです。
1 | func (x *cbcEncrypter) CryptBlocks(dst, src []byte) { |
一方、復号用オブジェクトは NewCBCDecrypter()
で生成します。この戻り値が BlockMode
インターフェースを実装した cbcDecrypter
オブジェクトになります。
1 | type cbcDecrypter cbc |
続いて cbcDecrypter
の CryptBlocks()
メソッドの実装を見てみます。 To avoid making a copy each time, we loop over the blocks BACKWARDS.
というコメントがある通り、復号処理は暗号文の末尾から逆順でおこなっているようです。しかしながら、「コピーを防ぐため」ということの意図はよくわかりませんでした。
1 | func (x *cbcDecrypter) CryptBlocks(dst, src []byte) { |
CBC モードについては以上です。次回はブロック暗号でよく使われるもう1つのモード、 CTR モードについて書く予定です。
]]>今回は引き続き第3章「対称暗号(共通鍵暗号)」より、新しい標準暗号アルゴリズムである AES (Advanced Encryption Standard) について解説します。
AES (Advanced Encryption Standard) はアメリカ合衆国の標準化機関である NIST (National Institute of Standards and Technology) が2000年に選定した、DES に代わる新たな対称暗号アルゴリズムです。世界中から応募を受け付け、最終的に Rijndael (ラインダール) というアルゴリズムが AES として選ばれました。
現在では、対称暗号を使う場合には AES を用いることが推奨されています。 TLS においても、本文のデータそのものの暗号化には主に AES が使われています。有名な公開鍵暗号は、この対称暗号の鍵を安全に通信相手に送るために使われており、本文のデータを公開鍵暗号で直接暗号化することはありません。
Rijndael (ラインダール) はブロック長 128 ビットの対称暗号アルゴリズムです。鍵のビット長は AES の規格では 128, 192, 256 ビットの3種類あり、それぞれ AES-128, AES-192, AES-256 と呼ばれています。
Go で実際に AES による暗号化/復号をおこなってみます。 AES 用のオブジェクトは crypto/aes
パッケージの NewCipher()
で生成します。戻り値は cipher.Block
インターフェースで、この Encrypt()
/Decrypt()
メソッドはそれぞれ AES による暗号化と復号の処理を実装しています。
http://play.golang.org/p/jUAHbYacE4
1 | package main |
Rindael も DES と同じく1回の暗号化処理単位を「ラウンド」と呼びます。1ラウンドは以下の図のような4つの処理から成り立っています。
以上のラウンドを10〜14回繰り返します。
復号処理はそれぞれの処理の逆処理を、上記手順と逆順でおこないます。すなわち、
という手順を暗号化時と同じラウンド回数分おこなうことで復号します。
AES を実装した crypto.Block
インターフェースの実装は以下の aesCipher
構造体です。フィールドには暗号化用のラウンド鍵群 enc
と復号化用のラウンド鍵群 dec
を持っています。これらは AddRoundKey の処理で使われ、1ラウンドで4バイト使うのでスライスの要素は uint32
になっています。
1 | // A cipher is an instance of AES encryption using a particular key. |
NewCipher()
コンストラクタは以下のようになっています。鍵の長さは 16, 24, 32 バイトのどれかでないとエラーになります。また、 expandKey()
で与えられた鍵からラウンド鍵を生成しています。
1 | // NewCipher creates and returns a new cipher.Block. |
ラウンド鍵を生成する expandKey()
の処理を見てみましょう。実は Go における AES の処理は CPU アーキテクチャが AMD64 か否かで分岐しています。 AMD64 の場合には以下のように、アセンブリコードがある場合にはそちらを使用するようになっており、ソースコードには asm_amd64.s
というアセンブリも一緒に含まれています。高速に AES の処理をおこなうための工夫でしょう。
1 | // +build amd64 |
先に「ラウンドの回数は10〜14回」と書きましたが、上記を見ると、鍵長が 128, 192, 256 ビットだとそれぞれ 10, 12, 14 回のラウンドをおこなうことがわかります。
一方 AMD64 以外の CPU アーキテクチャの場合には、以下のように通常の Go で記述された処理をおこないます。
1 | // +build !amd64 |
また、暗号化/復号をおこなう Encrypt()
/Decrypt()
メソッドの実装は以下のようになっています。
1 | func (c *aesCipher) Encrypt(dst, src []byte) { |
encryptBlock()
, decryptBlock()
も expandKey()
と同様に、 AMD 64 の場合はアセンブリコードを、そうでなければ Go のコードを使用するようになっています。
1 | // +build amd64 |
AMD64 以外はそのまま Go のコードを呼びます。
1 | // +build !amd64 |
encryptBlockGo()
, decryptBlockGo()
の実装の詳細はここでは割愛します。
これまで見てきた対称暗号はすべて「固定長」の平文(ブロック)を暗号化するアルゴリズムでした。 AES の場合なら1回の処理で暗号化するのは 128 ビット (16 バイト) です。
しかし僕たちが普段暗号化したい文章は当然もっと長いので、このブロック暗号を繰り返し使って、長い文章全体を暗号化する必要があります。ブロック暗号を繰り返す方法のことをブロック暗号の「モード」と呼びます。ブロック暗号のモードにはいろいろな種類があります。
ということで次回は第4章「ブロック暗号のモード」の CBC モードと CTR モードについて紹介したいと思います。
]]>そこで『暗号技術入門』を参考に、 Go 言語のライブラリを使い、各種暗号技術の実装や使い方について学んでみたいと思います。以下の Go のソースコードのバージョンはすべて 1.5.2 です。
まずは第3章「対称暗号(共通鍵暗号)」です。対称暗号は、「共通の鍵で暗号化と復号をおこなう暗号アルゴリズム」です。
DES (Data Encryption Standard) は、1977年にアメリカ合衆国の連邦情報処理標準規格 (FIPS) に採用された対称暗号です。しかし現在ではブルートフォースアタックにより短時間で解読されてしまうため、暗号化に用いるべきではありません。
ただ後述する Triple DES は DES による処理を3回おこなう方式であり、これは TLS にも使われている未だ現役な暗号方式であるため、その基礎となる DES の処理を知っておくことは有益です。
DES は 64 ビットの平文を、 64 ビット(実際は 56 ビット) の鍵を使って 64 ビットの暗号文に暗号化するアルゴリズムです。あるまとまり単位で暗号化をおこなうアルゴリズムを「ブロック暗号」と呼び、 DES はブロック暗号の一種です。
Go ではブロック暗号はすべて crypto/cipher
パッケージに定義されている cipher.Block
という統一のインターフェースを介して利用するようになっています。 Encrypt()
で暗号化、 Decrypt()
で復号をおこないます。
1 | package cipher |
このインターフェースにより、ライブラリを使う側は具体的なブロック暗号アルゴリズムの違いを意識する必要なく、統一的に扱えるようになっています。
ではまず実際に DES で暗号化/復号をおこなってみます。 DES 用のオブジェクトは crypto/des
パッケージの NewCipher()
で生成します。戻り値は cipher.Block
インターフェースで、この Encrypt()
/Decrypt()
メソッドはそれぞれ DES による暗号化と復号の処理を実装しています。
http://play.golang.org/p/F7QBes1BFK
1 | package main |
DES のアルゴリズムの概略は以下の図のように表せます。まず 64 ビット (実質使われるのはそのうちの 56 ビット) の鍵から、16 個の「サブ鍵」という鍵を生成します。そしてそのサブ鍵を使い、 64 ビットの平文を「ラウンド」という処理に 16 回かけます。
各ラウンドでは以下の図のようなことをおこないます。まず暗号化対象の 64 ビットデータを 左右 32 ビットずつに分割します。続いて、サブ鍵と右の 32 ビットをラウンド関数 $f$ にかけたものと左の 32 ビットの XOR を取ります。最後にその左右を交換します。ラウンド関数 $f$ には任意の関数を用いることが可能です。
この処理をサブ鍵 16 個を順に使って繰り返して、最終的に出力されるバイト列が暗号化データになります。
復号処理はサブ鍵を逆順で使って同じ処理を繰り返せばよいだけになります。なぜなら XOR を 2 回かけると元に戻るという性質があるためです。
DES の鍵の長さは 64 ビット、すなわち取りうる鍵の数は高々 264 ≒ 1.8 × 1019 個なので、現代のコンピュータであればブルートフォースアタックにより短時間で解読することが可能になってしまっています(ラウンド関数は暗号解読者に知られているという前提で考えるべきなので)。
DES を実装した cipher.Block
インターフェースの実体は以下の desCipher
構造体です。フィールドには subkeys [16]uint64
を持っています。1つの uint64
が1回のラウンドで使用する 64 ビット (8 バイト) 鍵で、 DES はそれを 16 回繰り返すため 16 個のサブ鍵が必要になります。
1 | // desCipher is an instance of DES encryption. |
NewCipher()
コンストラクタは以下のようになっています。鍵の長さは 8 バイト (64 ビット) でないとエラーになります。また c.generateSubkeys(key)
でメインの鍵からサブ鍵の配列を生成しています。
1 | // NewCipher creates and returns a new cipher.Block. |
暗号化/復号をおこなう Encrypt()
/Decrypt()
メソッドの実装は以下のようになっています。 DES の暗号化/復号処理はサブ鍵を使う順番が違うだけなので、内部では cryptBlock()
という共通の関数を用いていることがわかります。
1 | func (c *desCipher) Encrypt(dst, src []byte) { encryptBlock(c.subkeys[:], dst, src) } |
暗号化/復号のメインの処理は以下の cryptBlock()
でおこなわれます。 decrypt
フラグが false
であればサブ鍵が昇順で使われて暗号化され、 true
であれば逆の降順で使われて復号されます。
1 | func cryptBlock(subkeys []uint64, dst, src []byte, decrypt bool) { |
ラウンド関数 $f$ の実装は feistel()
という関数ですが今回は割愛します。
Triple DES (3DES) は、 DES よりも強力になるよう、 DES を3段重ねにした暗号アルゴリズムです。 Triple DES は TLS でも使われています。ただし、 AES (Advanced Encryption Standard) がある今、あえて Triple DES を使う必然性は薄いです。
Triple DES の特徴は、暗号化を3回おこなうのではなく【暗号化 → 復号化 → 暗号化】という重ね方をするところです。これは、すべての鍵を等しくすれば単なる DES と同じように使えるように互換性が確保されているためです。
3つの鍵のうち鍵1と鍵3を同じにしたものを DES-EDE2 と呼び、 3つの鍵すべてを違うものにしたものを DES-EDE3 と呼びます。 EDE は Encryption → Decryption → Encryption の略です。
DES の鍵長は 64 ビットなので、 DES-EDE2 であれば鍵の長さは 128 ビット、 DES-EDE3 なら 192 ビットになります。したがって暗号強度的には DES-EDE3 を用いるほうがよいです。
Triple DES も DES と同じ crypto/des
パッケージに入っています。 Triple DES 用のオブジェクトは NewTripleDESCipher()
で生成します。渡す鍵の長さは 24 バイト (192 ビット) になります。この戻り値も cipher.Block
インターフェースですので、 DES の場合と使い方は同じです。
http://play.golang.org/p/UfXv32bO8e
1 | package main |
Triple DES を実装した cipher.Block
インターフェースの実体は以下の tripleDESCipher
構造体です。フィールドには desCipher
を3つ持っています。
1 | // A tripleDESCipher is an instance of TripleDES encryption. |
NewTripleDESCipher()
コンストラクタは以下のようになっています。鍵の長さは 24 バイト (192 ビット) でないとエラーになります。 3 つの desCipher
はそれぞれ、元の鍵の 8 バイト目まで、9 から 16 バイト目まで、 17 から 24 バイト目までを鍵にしてサブ鍵を生成します。
1 | // NewTripleDESCipher creates and returns a new cipher.Block. |
暗号化/復号をおこなう Encrypt()
/Decrypt()
メソッドの実装は以下のようになっています。 Triple DES のアルゴリズムそのままに、 Encrypt()
では各 desCipher
が順に【暗号化 → 復号化 → 暗号化】の処理をおこないます。一方 Decrypt()
では逆に【復号化 → 暗号化 → 復号化】という順で処理をおこなっていますね。
1 | func (c *tripleDESCipher) Encrypt(dst, src []byte) { |
次回は AES (Advanced Encryption Standard) についての記事を書く予定です。
]]>Go 言語はある種のオブジェクト指向プログラミング (OOP) 言語であり、 OOP 言語の慣例通り、メソッドを呼び出される対象のことを「レシーバ」と呼びます。
1 | p := Person{Name: "Taro"} |
ちなみになぜ「レシーバ」と呼ぶのかというと、昔の OOP 言語の文脈ではメソッド呼び出しのことを「メッセージの送信」と言い、メソッドを呼び出される側は「メッセージの受信側」だからです。
Go 言語では「値」と「ポインタ」が明示的に区別されているため、たとえばある構造体に対してメソッドを定義する場合でも、「値型」に対する定義なのか「ポインタ型」に対する定義なのかはっきりと区別しなければなりません。それぞれについて簡単に説明します。
「値型」に対してメソッド定義されたものが「値レシーバ」です。 Go 言語では構造体は値なので、以下の例では Person
という値型に対して Greet()
というメソッドを定義しています。
1 | type Person struct{ Name string } |
「ポインタ型」に対してメソッド定義されたものが「ポインタレシーバ」です。以下の例では *Person
というポインタ型に対して Shout()
というメソッドを定義しています。
1 | type Person struct{ Name string } |
しかし実際には、メソッド呼び出し時にこのあたりのことを意識しなくてもすみます。それは Go 言語仕様の「呼び出し」のセクションにある通り、レシーバの【値型 ⇔ ポインタ型】間の変換はコンパイラが暗黙的におこなってくれるからです。それぞれ例を見てみましょう。
値型に対してあるメソッドが定義されているときに、ポインタ型変数からそのメソッドを呼び出そうとすると、コンパイラが暗黙的に値型のメソッド呼び出しに変換してくれます。以下の例では、 値型 Person
に Greet()
が定義されていますが、 *Person
型変数 pp
からでも問題なく呼び出せます。
1 | type Person struct{ Name string } |
したがって、以下のように nil
ポインタ変数から呼び出そうとすると panic
を起こします。 *nilp
が存在しないからです。
1 | var nilp *Person // nil ポインタ変数だと... |
これは Greet()
の中で p.Name
を使っていることとは関係なく発生します。
上記コードの Playground: http://play.golang.org/p/SZOv0hTicF
一方、ポインタ型に対してあるメソッドが定義されているときに、値型変数からそのメソッドを呼び出そうとすると、コンパイラが暗黙的にポインタ型のメソッド呼び出しに変換してくれます。以下の例では、 ポインタ型 *Person
に Shout()
が定義されていますが、 Person
型変数 p
からでも問題なく呼び出せます。
1 | type Person struct{ Name string } |
上記コードの Playground: http://play.golang.org/p/Vs-LOJq_1d
さらに、ポインタレシーバのメソッドは nil
ポインタ変数からでも呼び出しが可能です。
1 | var nilp *Person // nil ポインタ変数でも... |
ただし当然のことながら、メソッド内でフィールドを使っていたら参照先がないので panic
になります。以下の例では、 p.Name
を使っているメソッド ShoutName
を nil
ポインタ変数から呼び出すと、メソッド呼び出しそのものは正常におこなわれるものの、フィールド呼び出しがあるせいで panic
になります。
1 | type Person struct{ Name string } |
上記コードの Playground: http://play.golang.org/p/UzB32jpi2q
メソッド定義は本質的には関数定義と等価です。 Go 言語の場合
1 | func (p Person) Greet(msg string) { |
というメソッド定義は、内部的には
1 | func Person.Greet(p Person, msg string) { |
という「メソッド式 (method expression)」と呼ばれる関数として定義されます。これは値レシーバの場合で、同様にポインタレシーバの場合
1 | func (pp *Person) Shout(msg string) { |
は
1 | func (*Person).Shout(pp *Person, msg string) { |
という関数として定義されます。
Go 1.5.2 のソースコードを基にこのことを確かめてみましょう。該当箇所は以下になります (コメントはすべて僕が追記したものです)。
src/cmd/compile/internal/gc/dcl.go#L1325-L1350
1 | // n: メソッドを表す AST ノード |
上記コードの変数 p
を見ていただくとわかる通り、メソッド定義においてメソッド名は (*Type).Method
または Type.Method
という名前をもつ、関数の AST ノードに変換されています。
メソッド定義と同様に、メソッド呼び出しもメソッド式という関数の呼び出しの糖衣構文にすぎません。たとえば、以下の例では p.Greet(...)
と Person.Greet(p, ...)
、 pp.Shout(...)
と (*Person).Shout(pp, ...)
はそれぞれ等価です。
1 | type Person struct{ Name string } |
上記コードの Playground: http://play.golang.org/p/3ENNQWghCX
ちなみに、メソッド式やメソッドは値としても扱えるので、以下のように変数に代入して利用することもできます。この場合、メソッド式 f
にはレシーバ引数が必要で、メソッド値 g
には必要ないことに注意してください。
1 | type Person struct{ Name string } |
上記コードの Playground: http://play.golang.org/p/kpaGwzPu1u
Go は関数が第一級オブジェクトなので便利ですね。
改めて強調すると、メソッド定義において意識すべきなのは、レシーバが「第0引数」であるということです。たとえば、以下のメソッド定義
1 | type Person struct{ Name string } |
は、内部的には
1 | func Person.Greet(p Person, msg string) { |
というメソッド式(関数)として定義されます。ただし直接このような関数名での関数定義はできません(コンパイラが .
を含むシンボル名を許していないので)。
そして、メソッド呼び出しがメソッド式呼び出しと等価ということは、メソッド定義をする際には以下のことに気をつける必要があります。
関数呼び出しごとに引数の値はスタックにコピーされますので、当然メソッド呼び出し時にはレシーバの値もコピーされます。
1 | p := Person{Name: "Taro"} |
したがって、特にデータ量の大きな構造体に値レシーバのメソッドを定義すると、メソッド呼び出しごとにコピーが発生するので非常に非効率であることがわかります。このことから、構造体におけるメソッド定義は原則ポインタレシーバに対しておこなったほうがよいです。
値レシーバの場合、値そのものがまるっとコピーされるので、メソッド内でいくら値を書き換えても元のレシーバの値にはまったく影響がありません。
以下の例の場合、 UnchangeName()
の処理は一見 setter っぽくレシーバの name
を書き換えているように見えますが、 p
が値レシーバなので p.Greet()
で表示される名前は Jiro
ではなく Taro
のままになります。
1 | type Person struct{ name string } |
上記コードの Playground: http://play.golang.org/p/5kJVlx_XY7
このことを利用すると、不変オブジェクトをつくることができます。その典型例が time
パッケージの Time
です。
time.Time
は不変オブジェクトtime.Time
の API 一覧を見てもらうとわかる通り、 Time
オブジェクトは
Time
型(値型)Time
型(値型)となっており、 *Time
型(ポインタ型)は基本的に現れません。
フィールドがプライベートなので直接の値の変更ができず、またレシーバも戻り値もほぼすべてポインタ型ではなく値型なので、一度生成された Time
オブジェクトは一切変更する手段がないということになります。
Java では java.util.Date
クラスや java.util.Calendar
クラスが可変でスレッドセーフではないことが長年問題となっており、やっと Java 8 で不変オブジェクトを基本とする java.time
パッケージが導入されました(Java SE 8 Date and Time -新しい日付/時間ライブラリが必要になる理由-)。 Go では少なくともこの種の問題は避けられているわけですね。
それでも Go ではエクスポートされたフィールドの直接書き換えや、オブジェクトの内部状態を書き換える副作用のあるメソッドが気軽に用いられる傾向にあります。並行処理に耐える不変オブジェクトを設計したい場合は、 Time
を参考にした構造体設計にするとよいかもしれません。
一方、レシーバをポインタレシーバにした場合、メソッド呼び出し時にコピーされるのもポインタになるので、そのポインタを使うことでレシーバの実際の値を書き換えることができます(ただし map
や chan
などの参照型をベースとする型は半分ポインタみたいなものなので、値レシーバでも可能ですが)。
たとえば以下の例の場合、 ChangeName()
メソッドのレシーバは *Person
なので、 p.name
フィールドを書き換えることが可能であり、 p.Greet()
で表示される名前は Jiro
に変化します。
1 | type Person struct{ name string } |
上記コードの Playground: http://play.golang.org/p/asH9DZRmxG
逆に言うと、レシーバの内部状態を変更したいメソッドは、(参照型を除き)必ずポインタレシーバで定義しなければなりません。
nil
チェックをすべきポインタレシーバのメソッドは nil
ポインタ変数からでも呼び出しが可能なため、ポインタレシーバのメソッド内でフィールドを呼び出している場合、常に nil pointer dereference
で panic
になる危険性を孕んでいます。
以下の例の場合、 ChangeNameUnsafe()
には nil
チェックがないため panic
になりますが、 ChangeNameSafe()
のほうは nil
チェックをしているため安全です。
1 | type Person struct{ name string } |
レシーバは他の言語でいう this
や self
などに相当するため、メソッド内で nil
チェックをすることには違和感があるかもしれません。しかし「レシーバは第0引数」であることを思い出せば、他のポインタ引数の nil
チェックと同様に考えて、安全なプログラムを書くためにきちんとやったほうがよいです(僕もよく忘れますが)。
逆に考えると、 Java などと違いメソッド内でレシーバが nil
か否かに応じたハンドリングができるので、それがきちんとなされていれば、メソッド呼び出し側が事前の nil
チェックをする必要がないありがたさもあります。
上記のことに関連して、実はメソッド定義におけるレシーバ変数は書かなくても問題なくコンパイルできるため、メソッド内でレシーバの値を使わない場合は書かないのも1つの手です。
1 | type Person struct{ name string } |
そのような処理は本質的にはメソッドにする必要がなく、単なる関数のままでもよいため、このようなケースはあまり多くないとは思います。ただ、パッとシグニチャを見ただけでレシーバの値が使われていないことがわかるので、ドキュメントとしての価値もあると思います。
最後に、値レシーバとポインタレシーバの使い分けについて整理しておきたいと思います。
レシーバの値を変更したい場合は(参照型を除き)必ずポインタレシーバにしなければなりません。構造体も基本的にはポインタレシーバにしたほうがよいでしょう。
また、統一性の観点からも、ある型のメソッドのレシーバを1つでもポインタレシーバにした場合には、値の変更の有無にかかわらずすべてポインタレシーバにしたほうがよいと思います。
レシーバの値を変更する必要がなく、かつ以下のどれかに該当するときは値レシーバにしたほうがよいです。ただし参照型の場合は値の変更の有無は関係ありません。
int
, string
などのプリミティブ型をベースとする型プリミティブ型はコピーコストが小さいため、通常は値レシーバにします。
map
, chan
といった参照型をベースとする型参照型は半分ポインタみたいなものなので、値レシーバのままでも保持する要素の値を変更できます。コピーコストも大きくないため、通常は値レシーバにします。
先に例として出した time.Time
のような不変型を定義したい場合には、すべて値レシーバとして定義するのも良い方法です。
コピーコストの小さい構造体の場合には、値レシーバでメソッドを定義すると処理がスタックで完結するので、ヒープへのアロケート回数や GC の回数が減ることが期待できます。
ただしこの使い分けは感覚的におこなうのではなく、本当に効率的になるのかきちんと計測した結果に基いておこなうべきです。 Go 言語ではベンチマークの計測や pprof などによる解析が容易におこなえるので、これらを活用するとよいでしょう。
]]>Docker Machine とは、 Docker ホストの管理ツールです。たとえば以下のようにして使います。
1 | # VirtualBox VM でローカル環境に demo という名前のホストをつくる |
このように簡単に Docker ホストを作成することができます。
Docker Machine は複数のホストを一括して管理できます。以下のように環境変数を切り替えることで、 Docker コマンドでの接続先を簡単に切り替えることができます。
1 | # ホスト一覧を表示する |
もう1つの大きな特長は、クラウドサービスのホストも簡単に作成できることです。たとえば、 Amazon EC2 上にホストを立てるには、以下のようにします。
1 | # Amazon EC2 にホストをつくる。各環境変数には適切な値を設定しておく |
コマンド1発で EC2 のホストをプロビジョニングできるなんて、とても便利じゃないでしょうか。
2015年11月3日に v0.5 がリリースされました。その中での最大の変更点が、 Driver Plugins という仕組みです。
Docker Machine のプロビジョニングの処理では、ホストのタイプ (VirtualBox なのか EC2 なのかなど) によって、それぞれに対応したドライバが実際の処理をおこないます。 v0.4 までは、その各ドライバはすべて docker-machine
バイナリに組み込まれていました。しかし v0.5 から、各ドライバが独立したバイナリとして配布されるようになりました。このことは、以下のようにすると確認できます。
1 | # docker-machine 関連のバイナリ一覧 |
この仕組みが Driver Plugins です。このような仕組みになったことで、ドライバの追加や削除が簡単におこなえるというプラガブル性や、メインバイナリとドライババイナリの開発・配布が独立しておこなえるというモジュール性が高まりました。
しかし一方でバイナリ自体が独立したことで、内部の仕組みは大きく変化しました。 v0.5 からは、このドライバとの通信方法に RPC (Remote Procesure Call) を用いるようになっています。ちなみに本家 Docker のプラグイン機構では一足先にこれが採用されています。
このことを確かめてみましょう。以下は OS X でおこなった場合ですので、 Docker デーモンは VirtualBox 上で動いているという前提です。
lsof
コマンドを使って、ポートを使用しているプロセスを見てみます。まず通常状態では、 TCP ポートを使用している “docker” の名前がつくプロセスはいません。
1 | # 通常はポートを使用している "docker" の名前がつくプロセスはいない |
続いて docker-machine ls
コマンドを実行してみます。すると、これまでなかったプロセスが立ち上がっていることがわかります。
1 | # docker-machine ls コマンドを実行すると、各ホストに対応するプラグインが RPC サーバとして立ち上がる |
名前が切れていて docker-ma
までしか表示されていませんが、実は3つの docker-machine-driver-virtualbox
プロセスが起動しています。
これらは RPC サーバとして、メインプロセスからの RPC リクエストを受け付け、ホストのプロビジョニング処理をおこなっています。
ではプラグインを直接起動してみたらどうなるのでしょうか? やってみると以下のようなエラーメッセージが表示され、起動に失敗してしまいます。
1 | $ docker-machine-driver-virtualbox |
実は以下のようにすると起動できますが、このプロセスもすぐ終了してしまいます。
1 | $ MACHINE_PLUGIN_TOKEN=42 docker-machine-driver-virtualbox |
さすがにこんな早さではメインプロセスと通信するのに十分な時間があるとは思えません。ということでこのことも含めて、 Docker Machine v0.5 の Driver Plugins の内部実装を追ってみることにしましょう。
ちなみに以下に記載するコード内に適宜日本語によるコメントを書いていますが、これらはすべて僕が追記したものであり、元のソースコードにはありません。本来のソースコードへのリンクも併せて記載しているので、ちゃんと確認したい方はそちらをご覧ください。
まず RPC クライアント側、すなわちメインプロセス側の実装です。まず RPC クライアントの役割を担うのは、以下の RpcClientDriver
という構造体です。
libmachine/drivers/rpc/client_driver.go#L19-L23
1 | type RpcClientDriver struct { |
この構造体がプラグインへの RPC リクエストを担当します。 Heartbeat の送信についてはあとで解説します。
docker-machine
のコマンドを実行すると、もろもろの前処理 (フラグのパースやホスト名の確認など) をおこなったあと、ほとんどの場合以下の NewRpcClientDriver()
が呼ばれ、 RPC クライアントの生成処理とプラグインの起動処理がおこなわれます。
libmachine/drivers/rpc/client_driver.go#L49-L112
1 | // rawDriverData: ドライバに関する設定情報が JSON にシリアライズされたもの |
まず注目してもらいたいのは、 プラグインの起動処理を別の goroutine がおこなっていることです。このようにすることで、時間のかかるプラグインの起動処理を本処理とは別におこなうことができます。
もう1つは、 途中で heartbeat を送信する goroutine を立ち上げていることです。この goroutine は RpcServerDriver.Heartbeat
というリモートメソッド呼び出しにより、プラグイン側に heartbeat を送っています。このことの意味は、あとで RPC サーバ側の解説をするときに説明します。
では、プラグインの起動処理についてもう少し追ってみましょう。
プラグインの起動処理を実際におこなうのは、以下の LocalBinaryExecutor
という構造体です。
libmachine/drivers/plugin/localbinary/plugin.go#L73-L77
1 | type LocalBinaryExecutor struct { |
この構造体は、以下の Start()
メソッドの中で exec.Command()
と cmd.Start()
を実行することで、プラグインのプロセスを起動します。
libmachine/drivers/plugin/localbinary/plugin.go#L105-L132
1 | func (lbe *LocalBinaryExecutor) Start() (*bufio.Scanner, *bufio.Scanner, error) { |
また、途中で os.Setenv()
により、 MACHINE_PLUGIN_TOKEN=42
に設定しているのがわかります。これが先ほどプラグインプロセスを単体で立ち上げてみたときにおこなったことです。
では RPC リクエストの送信処理はどうなっているのでしょうか。 RpcClientDriver
のメソッドの大部分は、単なる RpcServerDriver
へのリモート呼び出しになっています。
libmachine/drivers/rpc/client_driver.go#L256-L278
1 | func (c *RpcClientDriver) Create() error { |
Create()
, Remove()
, Start()
, Stop()
などの代表的な処理は、全部 RPC サーバ側に丸投げしています。ということで、 RPC サーバ側の実装を見てみましょう。
RPC サーバ側、すなわちプラグインプロセス側の実装を見てみます。
まず先にプラグインプロセスが起動されたときに最初に実行される処理を追ってみましょう。例として docker-machine-driver-virtualbox
のエントリポイントは以下のようになっています。
cmd/machine-driver-virtualbox.go#L8-L10
1 | // docker-machine-driver-virtualbox の main 関数 |
これだけです。では RegisterDriver()
の実装はどうなっているのでしょうか。
libmachine/drivers/plugin/register_driver.go#L17-L56
1 | var ( |
まず最初に、 $MACHINE_PLUGIN_TOKEN != 42
の場合はエラーメッセージを表示して os.Exit(1)
しています。これが特定の環境変数をつけないとプラグイン単体では起動できなかった理由です。意図しない起動のされ方がなされないように、このようなセーフティーネットが張られているようです。 42
なのは 『銀河ヒッチハイクガイド』 に由来しているのでしょうか。
続いて net.Listen("tcp", "127.0.0.1:0")
で利用可能な TCP ポートで listen することを宣言します。 net.Listen()
が内部で呼んでいる net.ListenTCP()
の仕様として、ポート番号 0 の場合には利用可能な TCP ポートのどれかが割り当てられることになっています。
最後にリクエストを待ち受ける goroutine を起動するとともに、 for
-select
で無限ループに入ります。ここで先ほどプラグインプロセスが即時終了してしまった理由がわかります。プラグインは rpcd.HeartbeatCh
チャンネルにメッセージが送られ続けている間は起動していますが、 500ms 以上メッセージが送られてこないと強制終了してしまうようになっているのです。これにより、メインプロセスが予期せぬ終了をしてしまった場合でも、プラグインプロセスがゾンビ化することを防いでいます。
では、 RPC リクエストに対する実際の処理を見てみましょう。 RPC リクエストを受け付けるのは以下の RpcServerDriver
という構造体です。この構造体のメソッドが RpcClientDriver
から呼び出されていましたね。
libmachine/drivers/rpc/server_driver.go
1 | type RpcServerDriver struct { |
各メソッドは以下のようになっていて、実際のところは ActualDriver
に処理を委譲していることがわかります。
1 | func (r *RpcServerDriver) Create(_, _ *struct{}) error { |
ではこの ActualDriver
の型である drivers.Driver
について詳しく見てみましょう。
Driver
interfacedrivers.Driver
はインターフェースであり、ホストへの操作を実際におこなうオブジェクトが実装すべきメソッド群を規定しています。
libmachine/drivers/drivers.go#L11-L73
1 | // Driver defines how a host is created and controlled. Different types of |
Create()
, Start()
, Stop()
, Remove()
など、ホストの操作に必要なメソッドが並んでいます。そして、ホストが VirtualBox なのか Amazon EC2 なのかなどに応じて、各ホストに対する具体的な処理の実装が変わってくるわけです。では例として VirtualBox 用の Driver
の実装を見てみることにしましょう。
Driver
の実装drivers/virtualbox/virtualbox.go#L49-L61
1 | type Driver struct { |
ここで重要なのは VBoxManager
という埋め込みフィールドです。 VBoxManager
はインターフェースになっていて、その実装は VBoxCmdManager
に書かれています。
drivers/virtualbox/vbm.go#L51-L80
1 | func (v *VBoxCmdManager) vbmOutErr(args ...string) (string, string, error) { |
vboxManageCmd
は VirtualBox をインストールすると付属してくる VBoxManage
という CLI のパスを指していて、これはコマンドラインから VirtualBox の VM を操作できるツールです。つまり、内部的には VBoxManage
コマンドを呼び出してそちらに処理をおこなわせているということです。以下の Driver.Create()
の実装を見ることで、どのように VBoxManage
を呼び出しているのか見てみましょう。
Driver.Create()
の実装一番泥臭さがわかりやすそうだったので、 VirtualBox の VM を立ち上げる処理をおこなう Driver.Create()
の実装を詳しく追ってみます。
drivers/virtualbox/virtualbox.go#L229-L417
1 | func (d *Driver) Create() error { |
このように VBoxManage createvm
や VBoxManage modifyvm
コマンドなどに必要なフラグをつけて呼び出すことで、 VirtualBox ホストのプロビジョニング処理をおこなっていることがわかります。ここでは解説しませんが、最後に呼び出している Driver.Start()
の中では VM の起動やネットワークの設定などがおこなわれます。なかなか泥臭さが伝わってきますね。
Driver
の実装もう1つだけ、 Amazon EC2 用の Driver
の実装も見てみましょう。
drivers/amazonec2/amazonec2.go#L23-L69
1 | type Driver struct { |
こちらの構造体では、 EC2 に必要な access key や secret key, region, AMI などの情報を保持していることがわかります。具体的なメソッドの実装は解説しませんが、基本的には AWS SDK for Go を介して対応する EC2 の API を呼び出しています。
僕が今回 Driver Plugins の仕組みを追ってみようと思ったのは、「コンパイル型の静的言語でプラグイン機構を実現するにはどうすればよいのか?」ということを考えていたからでした。スクリプト言語であればプラグイン機構は簡単に実現できるのですが、 Go のように単一バイナリになり、 DLL のような仕組みもない言語において、プラグイン機構を実現する仕組みがわからなかったのです。 RPC を使うというのが常套手段なのはのちのちわかったのですが、具体的な実装を追ってみて腑に落ちた感じです。 Docker Machine のコードは比較的読みやすいので、とても勉強になりました。
]]>aql
コマンドでキーそのものも確認したい分散型 KVS である Aerospike では、 aql
という SQL ライクにデータを確認・操作できるコマンドラインインターフェースが提供されています。使える機能はかなり限られているものの、 KVS にもかかわらずまるで RDB であるかのように扱えるので、データの確認には重宝しています。
たとえば、以下のように INSERT
したり SELECT
したりできます。
1 | aql> INSERT INTO test.testset (PK, a, b) VALUES ('xyz', 'abc', 123) |
しかしながらここで注目してもらいたいのが、キーである 'xyz'
が表示されていないということです。
Aerospike は内部的には、レコードを指定するのにキーそのものではなくそのハッシュ値を使用しています。 Aerospikeのデータモデルの説明 から引用してみます。
Key / Digest
In the application, each record will have a key associated with it. This key is what the application will use to read or write the record.
However, when the key is sent to the database, the key (together with the set information) is hashed into a 160-bit digest. Within the database, the digest is used address the record for all operations.
The key is used primarily in the application, while the digest is primarily used for addressing the record in the database.
The key maybe either an Integer, String, or Bytes value.n
キー / ダイジェスト
アプリケーションの中では、各レコードにはそれに対応するキーがあります。このキーはアプリケーションがレコードを読み出したり書き出したりするのに使うものです。
しかしながら、キーがデータベースに送られるときには、そのキー(とセットの情報)は 160 bit のダイジェストにハッシュ化されます。データベースの中では、すべての操作において、レコードを指定するのにはそのダイジェストが使用されます。
キーは主にアプリケーションの中で使用され、ダイジェストは主にデータベースの中でレコードを指定するのに使用されます。
キーは整数か文字列、バイト値のどれかです。
このような事情のため、 aql
ではそのままではキーが表示されないようです。
aql
でキーも表示するようにするためには、まずそもそも Aerospike にキーそのものの情報も格納するようにしなければなりません。つまり、クライアントアプリケーションからデータを送る際に、キーの情報も保存するように明示的に指定する必要があります。どのクライアントライブラリにも WritePolicy.sendKey
のようなフラグがあるはずなので、それを true
に設定します。
たとえば、 Go 言語のサンプルアプリケーションの場合には、以下のようにします。
1 | package main |
このプログラムを実行してから、 aql
で確認してみます。
1 | $ go run main.go |
きちんとキーも表示されるようになりましたね。
]]>たとえば sqlite3 の gem を入れようとしたところ、以下のようなエラーが発生して、 native extension のビルドに失敗しました。
1 | $ gem install sqlite3 -v '1.3.10' |
どうやら Makefile の生成に失敗しているようです。 Check the mkmf.log file for more details.
とあるので、 mkmf.log を探してみます。
1 | $ find ~/.rbenv | grep sqlite | grep mkmf.log |
ありました。こちらを見てみます。
1 | "clang -o conftest -I/Users/skatsuta/.rbenv/versions/2.1.5/include/ruby-2.1.0/x86_64-darwin14.0 -I/Users/skatsuta/.rbenv/versions/2.1.5/include/ruby-2.1.0/ruby/backward -I/Users/skatsuta/.rbenv/versions/2.1.5/include/ruby-2.1.0 -I. -I/Users/skatsuta/.rbenv/versions/2.1.5/include -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT -O3 -Wno-error=shorten-64-to-32 -pipe conftest.c -L. -L/Users/skatsuta/.rbenv/versions/2.1.5/lib -L. -L/Users/skatsuta/.rbenv/versions/2.1.5/lib -fstack-protector -lruby-static -framework CoreFoundation -lpthread -lgmp -ldl -lobjc " |
ld: library not found for -lgmp
とあるので、 gmp
というライブラリをリンカが見つけられていません。 GMP とは GNU Multi-Precision Library のことで、多倍長整数など任意の精度の算術ライブラリのようです (Wikipedia)。
しかしながら、 gmp
は Homebrew ですでにインストールされており、 /usr/local/lib
にも存在します。
1 | $ brew list | grep gmp |
ここから先の詳しい原因はわからないのですが、 El Capitan になってからデフォルトで Clang が認識するライブラリパスに /usr/local/lib
が含まれなくなったのではないかと思っています。このあたりは新しいセキュリティ機能である System Integrity Protection (SIP; 別名 rootless) とも関連がありそうな気がします。
場当たり的でよいのであれば、先の gem install
時のエラーログにもある通り、 --with-opt-lib
オプションで明示的に指定してやればよいです。
1 | $ gem install sqlite3 -v '1.3.10' -- --with-opt-lib=/usr/local/lib |
ただしこの方法だと gmp
を使う gem をインストールするたびに、毎回指定する必要あり面倒です。
永続的に効果のある解決方法は、 $LIBRARY_PATH
環境変数に /usr/local/lib
を追加してやることです。これで以下のようにインストールに成功するようになります。
1 | $ export LIBRARY_PATH=/usr/local/lib:$LIBRARY_PATH |
export LIBRARY_PATH=/usr/local/lib:$LIBRARY_PATH
は .bashrc や .zshrc に書いておくとよいでしょう。
El Capitan では SIP があることで、 /usr/local
以下を使う Homebrew でも
1 | $ sudo chown -R $(whoami):admin /usr/local |
というコマンドを実行して権限を変えないと正常に動作しなかったりするなど、いろいろなトラブルシューティングが必要になりそうですね。
]]>Docker はクライアントサーバモデルも採用しており、サーバ側の API 仕様は Docker Remote API として公開されています。つまり、 Docker クライアント (Docker コマンド) はこの Remote API を叩いて Docker サーバ (Docker デーモン) に操作を依頼しているのであり、設定をすれば普通の HTTP 経由でも API へアクセスすることが可能になっています。
Docker デーモンはデフォルトでは Unix ソケットの unix:///var/run/docker.sock
を listen しますが、 Windows や OS X で Boot2Docker や Docker Machine を使っている場合には、 TCP ソケット (2376 番ポート) を listen するように設定されています。また、接続は TLS で暗号化されています。この辺りのことをクライアントからのアクセスの際に指定するために、以下の環境変数の設定が必要となるわけです。
1 | # Docker Machine の場合 |
たとえばイメージ一覧を取得してみます。現状のイメージ一覧は以下のようになっているとしましょう。
1 | $ docker images |
では Docker Remote API に直接アクセスしてみます。 wget
を使った方法はこちらの記事で紹介されていますが、僕は HTTPie を愛用しているので、今回はそちらを使ってやってみたいと思います。
結論から言えば、以下のようにオプションを指定して API にアクセスします。
1 | # 簡略化のため、あらかじめホスト VM の IP アドレスを取得しておく |
詳細なイメージ情報が JSON で取得できました。この API 情報を使うことで、サードパーティでもさまざまな Docker 用ツールが開発できそうです。
]]>Linux 以外の OS 上で Docker を利用する場合、 VirtualBox などで Linux のホスト VM を立てる必要があります。このとき、ホスト VM からローカルへポートフォワーディングしたいと思うときがあります。これをおこなうには、 VirtualBox の CLI である VBoxManage
の controlvm
コマンドを使います。
1 | VBoxManage controlvm <VM_NAME> natpf<1-N> "<RULE_NAME>,tcp,<LOCAL_IP>,<LOCAL_PORT>,<VM_IP>,<VM_PORT> |
<LOCAL_*>
はローカルマシンのものを、 <VM_*>
はホスト VM のものを表します。<RULE_NAME>
はルール名で、わかりやすいように名付けるとよいです。 また <VM_IP>
は省略可能です。
では実際に試してみましょう。たとえば、 PostgreSQL の Docker コンテナのポート 5432 をローカルの 5432 にポートフォワーディングしたい場合を考えます。以下のように、コンテナからホスト VM へはあらかじめポートフォワーディングされているものとします。
1 | $ docker run -e POSTGRES_PASSWORD=password -d -p 5432:5432 postgres |
このとき、ホスト VM 名が dev
であるとすると、ホスト VM の 5432 から ローカルの 5432 にポートフォワーディングするには、以下のコマンドを実行します。
1 | $ VBoxManage controlvm dev natpf1 "psql_pf,tcp,127.0.0.1,5432,,5432" |
では確かめてみましょう (OS X の場合)。
1 | $ lsof -P -i -n | grep ':5432 ' |
きちんと 5432 番で listen していますね。
]]>Docker Machine は複数のホスト VM を切り替えることができるのが1つの利点ですが、これにより逆に不便になってしまったことがあります。 Boot2Docker を使う場合、ホスト VM 起動後
1 | $ boot2docker shellinit |
により生成される環境変数を設定するために、 .bashrc
や .zshrc
などに
1 | eval "$(boot2docker shellinit)" |
と書いておけばいいのですが、 Docker Machine の場合はホストを複数作れるために、上記のように一筋縄ではいきません。
Docker Machine で同じように環境変数を設定する場合、以下のようにホスト VM の名前の指定が必要です。たとえば dev
というホスト VM を作った場合、
1 | $ docker-machine env dev |
で環境変数が取得できます。このホスト VM 名は存在し、かつ動いているものでなければなりません。
いろいろ模索と妥協をした結果、 docker-machine ls
コマンドと組み合わせることにより、現状動いているホスト VM を1つ取り出して、その環境変数を設定するシェルスクリプトを .zshrc
に書くことにしました。
1 | # check if `docker-machine` command exists |
IP アドレスだけを使いたいときもあるので、同時に DOCKER_IP
という変数も設定しています。
これでちょっとだけ手間が省けます。
]]>構造体をキーとした map のリテラルを少し簡単に書けるようになります。たとえば、これまでは
1 | m := map[Point]string{ |
と書いていたものが、
1 | m := map[Point]string{ |
と書けるようになります。
コンパイラとランタイムがすべて Go で書き換えられ、C のコードは排除されました。
GC 処理のアルゴリズム改善・スケジューリング改善・並列化により、レイテンシが劇的に小さくなります。アプリケーションの動きが完全に停止する “stop the world” の時間はほとんどの場合 10 ミリ秒以下に抑えられるようです。
GOMAXPROCS
のデフォルト値が 1 からコア数になります。シングルスレッド前提で書かれているプログラムにはもしかすると影響が出るかもしれません。
darwin/arm
, darwin/arm64
(iOS) と linux/arm64
がサポートされました。
6g
, 8g
などのコンパイラ・リンカはなくなり、go tool compile
, go tool link
などに置き換えられました。
go vet
, go cover
, go doc
コマンドが標準で使えるようになりました。
vet
はソースコードのバリデーションや静的解析をおこなうツールです。cover
はカバレッジを計算してくれるツールです。vet
や cover
はよく使っているので、標準で同梱されるようになると嬉しいですね。
Go でのアクセス制御はこれまで public か private しか指定できませんでした。 Go 1.4 で internal package という、パッケージプライベートを実現する機能が試験的に導入されましたが、この段階ではまだコアライブラリでしか利用できるようになっていませんでした。 Go 1.5 でこの機能がユーザライブラリにも開放されます。
具体的には、パッケージ名に internal
が含まれている場合、同一のルートパッケージからのみ import できます。たとえば、 $GOPATH/src/mypkg/internal/foo
というパッケージは、 $GOPATH/src/mypkg
からのみ import できます。
細かい仕様はデザインドキュメントに記載されています。go1.4で追加されたinternal packageについて - Qiita の記事なども参考になります。
Verdoring 機能が実験的にサポートされました。これはどういうものかというと、たとえば
1 | $GOPATH |
というリポジトリ構成にしておくと、 github.com/constabulary/example-gsftp/cmd/gsftp/main.go
において
1 | import ( |
と書けば verdor
以下の依存ライブラリを import できるようになります。 これまでは
1 | import ( |
というふうにフルパスを書かなければならなかったので、だいぶ楽になります。
ただしこの機能を有効にするには環境変数で GO15VENDOREXPERIMENT=1
と設定する必要があります。
これまで Go にはライブラリのバージョン指定機能がなかったため、たとえば依存関係解決ツールである Godep では Godep/_workspace/
で同じような機構を実現していました。 Go 1.5 からはこの標準機能を使っていくことになるでしょう。
flag
パッケージflag
パッケージの help の表示が見やすくなりました。たとえば
1 | cpuFlag = flag.Int("cpu", 1, "run `N` processes in parallel") |
というフラグを定義すると、 help
メッセージは
1 | -cpu N |
と表示されるようになります。 Go 1.4 までは
1 | -cpu=1 run N processes in parallel |
という表示だったので、少し見やすくなります。
math/big
パッケージmath/big
パッケージに、任意精度の浮動小数点型である Float
が追加されます。
reflect
パッケージreflect
パッケージに ArrayOf
と FuncOf
関数が追加されます。 SliceOf
と同様に、実行時に配列や関数を表す型をつくることができるようになります。
個人的な見どころはやはり GC の改善と internal package ですね。正式リリースが待ち遠しいです。
]]>最近 Aerospike という超優秀な KVS を利用しているのですが、 Docker ベースになった werkcer 上でこのクライアントライブラリのユニットテストを実行したいと思ったので試してみました。
werker には services という、 複数のコンテナを外部サービスとして利用する仕組みがあります。この機構により、使用するミドルウェアのコンテナイメージを用意すれば、 CI 時にそれを利用したテストを走らせることができます。素晴らしい仕組みです。たとえば、 Redis を使用したければ、wercker.yml
の services
セクションに以下のように redis
を追記します。
1 | services: |
これにより、 CI 時に Docker Hub の Redis イメージが pull されてきて、 アプリケーションから利用できるようになります。 Aerospike を使う場合も同様で、 wercker.yml
に以下のように書きます。
1 | services: |
こうすると、ビルド時に Docker Hub の aerospike リポジトリからイメージを取得して、利用できるようにしてくれます。
では、アプリケーション側からこれらのサービスにアクセスするにはどうすればよいのでしょうか?
wercker では Docker のコンテナ間リンク機構 を利用して、 環境変数が自動で準備されるようになってます。 Aerospike を使用した場合には、以下のような環境変数が用意されることになります。
1 | AEROSPIKE_ENV_AEROSPIKE_SHA256=df810e67d363291f6f40c046564bbc7ab775fcdb45ebfb878368361705063015 |
ドキュメント にある通り、環境変数の命名規則は
1 | <name>_PORT_<port>_<protocol> |
をプレフィックスとして、 prefix_ADDR
なら IP アドレス、 prefix_PORT
ならポート番号、 prefix_PROTO
ならプロトコル名になっています。なおかつ、Aerospike の Dockerfile が実行されるので、 3000 ~ 3003 番のポートに対応する環境変数がそれぞれ用意されていますね。
したがって、あとはアプリケーション側が環境変数から必要な値を取得するような実装になっていればよいことになります。たとえば Go 言語であれば、 以下のように os.Getenv()
を使えばよいでしょう。
1 | addr := os.Getenv("AEROSPIKE_PORT_3000_TCP_ADDR") |
これで、 DB などの外部サービスに依存したテストも wercker 上で実行できるようになりました!
]]>1 | $ curl -L https://s3.amazonaws.com/downloads.wercker.com/cli/stable/darwin_amd64/wercker -o /usr/local/bin/wercker |
この wercker
コマンドを使ってコンテナイメージを pull してくるには、あらかじめ wercker login
で wercker のアカウントでログインしておく必要があります。そうでないと、以下のようなメッセージが表示されてしまいます。
1 | $ wercker pull skatsuta/aerospike-sample --branch master --load |
ところが僕は GitHub のアカウントを使っており、このログイン情報では次のように認証が通りません。
1 | $ wercker login |
どうなってるのかと思い wercker/wercker-cli リポジトリの issues を見てみると、以下のような issue が挙げられていました。
案の定 GitHub アカウントでのログインには未対応のようです。しかしながら、 Issue #15 の @captn3m0 のコメントに興味深いことが書かれていました。
Alternatively, you can add instructions to directly generate a token and putting it in the ~/.werkcer/token file
なんと、トークンファイルを置いておけばいけると!?
ということで、早速試してみました。
wercker のアカウントページの右上のアイコンから Settings > Personal tokens をたどり、 wercker-cli
という名前でトークンを生成し、コピーします。そしてそれを ~/.wercker/token
ファイルに貼り付けて保存します。
さて、もう一度 pull してみましょう。
1 | $ wercker pull skatsuta/aerospike-sample --branch master --load |
本当に成功しました! これで wercker CLI を活用できますね。
]]>最近、 Play Framework を使ったプロジェクトの開発でこの wercker を使用してみたのですが、試しにビルドしてみたら何と2回め以降のビルドでも毎回依存ライブラリをすべてダウンロードしているではないですか!!!
これは困った…どうにかならないもんか…と思ってやってみたのがこの記事です。
僕が今回取り組んだのが Play Framework を使ったプロジェクトだったため、それを題材に書きますが、本質的には Ant だろうと Maven だろうと Gradle だろうと sbt だろうと、もっと言えば Java / Scala に限らずどの言語のプロジェクトでも同じようにできるはずです。ぶっちゃけた話、単に rsync を使って自力でキャッシュを保存/復元しているだけですので。
メジャーな CI サービスにはキャッシュを制御するディレクティブが用意されているものもあります。
たとえば Travis CI では
1 | cache: |
と書いておけば vendor/bundle
がキャッシュされます。
CircleCI は至れり尽くせりで、 Java / Scala のプロジェクトの場合には
~/.m2
~/.ivy2
~/.gradle
~/.play
を自動で(暗黙的に)キャッシュしてくれますし、明示的にキャッシュしたいものは
1 | dependencies: |
のように書いておけば ~/.sbt
がキャッシュされます。
一方 wercker はまだ若いサービスのためか、上記のようなキャッシュ機構がきちんと整っておらず、今のところ用意されているのは環境変数 $WERCKER_CACHE_DIR
のみのようです (少なくとも僕が探した限りは見つかりませんでした)。
ビルドのたびに依存ライブラリを1からダウンロードされてはたまったもんじゃないので、何とかキャッシュの仕組みを作れないかとやってみました。
以下の wercker の公式ブログの記事を参考にさせていただきました。
上記記事では Go のプロジェクトにおいて、 go get
してきた依存ライブラリを rsync を使って $WERCKER_CACHE_DIR
に保存したりそこから復元したりして、 ビルド時間を短縮しています。
Java や Scala のプロジェクトでも考え方は同じなので、上記記事を参考にして自分なりのやり方を組み立てました。
$WERCKER_CACHE_DIR
からキャッシュを復元する$WERCKER_CACHE_DIR
にキャッシュを保存する要はこれだけなのですが、
ということをしたので、ちょっと手間暇をかけました。
キャッシュの保存/復元は cp
コマンドとかでもいいのでしょうが、先の記事でも rsync を使っていましたし、確かに効率を考えると rsync がよさそうです。しかしビルドのたびに rsync をインストールするのも無駄です。そこで、 wercker にはせっかく box という素晴らしい仕組みがあるので、あらかじめ rsync をインストールした box をつくり、それを利用することにしました。
ありがたいことに、 Java 8 と Typesafe Activator をインストールした mitsuse/wercker-box-activator という box がすでにありましたので、 fork させていただき、 rsync をインストールする処理を追記しました。
簡単な処理であれば wercker.yml に直接書いてしまえばよいのですが、キャッシュ処理が長くなりそうだったので、シェルスクリプトにまとめることにしました。
以下は Play Framework の例です。 Play Framework ではキャッシュディレクトリは ~/.ivy2
と ~/.sbt
なので、その2つを指定しています。
1 |
|
Gist を貼り付けるとレイアウトが崩れてしまうので、 Gist 版はこちら
単に rsync で $WERCKER_CACHE_DIR
へ保存したり、そこから復元したりしているだけです。
このシェルスクリプトをプロジェクトのリポジトリに入れておきます (たとえば sh/wercker_cache.sh
など)。
最後に、 wercker.yml
にキャッシュの保存/復元処理を書きます。
以下はサンプルで、 sh/wercker_cache.sh
に先のスクリプトが入っているとします。
1 | box: skatsuta/activator@0.0.3 |
これで wercker 上でビルドしてみると、2回目以降は restore cache というステップで各キャッシュがホームディレクトリに復元され、 store cache というステップで前回のキャッシュディレクトリとの差分が保存されていることが確認できると思います。
僕のプロジェクトではこのおかげで wercker 上でのビルド時間を2分弱短縮することができました。めでたしめでたし。
]]>currency
フィルタや date
フィルタを日本語の表記に合わせるには?AngularJS のフィルタは便利な機能です。その中で currency
フィルタや date
フィルタといった、フォーマットを整えてくれるフィルタがありますが、デフォルトでは US 表記になっています。これを日本語の表記に合わせるには、angular-i18n モジュールを利用します。
以下の方法は grunt-wiredep使用の環境でangular-i18nをbowerでインストールしたら注入できないとメッセージ - Qiita を参考にさせていただきました。
Bower を利用してインストールします。
1 | $ bower install angular-i18n --save |
ん? bower invalid-meta angular-i18n is missing "main" entry in bower.json
という警告が出ていますね。
同時に gulp serve
などを走らせていた場合、以下のような警告が出てしまいます。
1 | angular-i18n was not injected in your file. |
Bower が angular-i18n モジュールのうち、どのファイルをインクルードしていいのかわからないようです。
bower.json で明示的に使用するロケールファイルを指定してあげましょう。
1 | { |
これで index.html に自動的に追記されるようになります。
1 | <!-- bower:js --> |
以下のような HTML と JS を用意し、レンダリング結果を見てみます。
1 |
|
1 | (function() { 'use strict'; |
“2014-09-15T11:38:50.158Z”
2014年9月15日月曜日
1000000
\1,000,000
きちんと日本語の表記にフォーマットされていますね。
]]>Hexo の導入自体は非常に簡単なのですが、 GitHub Pages と連携させる際のやり方が最近ちょっと変わったらしく、検索して出てきた情報がどれも古かったので、覚え書きも兼ねて書いてみます。
以下のサイトを参考にさせていただきました。
ただし注意点として、これらの記事はどれも Github Pages で公開する手順の設定情報が古いです。現在の正しい設定は下記「GitHub Pages で公開する」のセクションに書きました。
まず Git と Node がインストールされていることが前提です。これらの導入方法については省略します。
Hexo の導入自体は公式サイトのトップページに載っている通りにコマンドを実行するだけです。が、 Hexo のリソース自体も GitHub などで管理したほうがいいので、先にリポジトリを作った上で、それをクローンしてきて、その中で Hexo の初期化を実行したほうが良いと思います。
以下は僕の例です。 skatsuta/blog-hexo という GitHub リポジトリを作った上で、以下を実行します。
1 | # Hexo をインストールする |
メッセージの通り http://localhost:4000/ にアクセスしてみて、無事にプレビューが表示されればとりあえずの構築は完了です。
デフォルトでは記事を書き換えてもプレビューには即時反映されず、毎回サーバを再起動しなければなりません。 Markdown エディタで書けばだいたいの仕上がり具合はわかりますが、やはり実際に適用されるテーマとは違ってしまいます。
そこで BrowserSync を使い、プレビューへの即時反映の設定をしてみたいと思います。
まず Get started in 5 minutes の 2. に従い、 BrowserSync をインストールします。
1 | $ npm install -g browser-sync |
続いて、 hexo-browsersync プラグインをインストールします。 package.json
にも加えたいので、 --save
オプションを付けておきます。
1 | $ npm install hexo-browsersync --save |
これで導入は終わりです。再び Hexo サーバを立ち上げてから設定や記事を書き換えて保存すると、そのたびに以下のようなメッセージが出てブラウザがリロードされ、変更点がプレビューに即時に反映されるはずです。
1 | $ hexo server |
快適な執筆環境が整ったところで、早速記事を書いていきます。 Hexo で新しく記事を作るには以下のコマンドを実行します。たとえば Hello, World
というタイトルの記事を書く場合です。
1 | $ hexo new "Hello, World" |
生成された Markdown ファイルの中は次のようになっています。
1 | title: "Hello, World" |
あとは Markdown で好きなように記事を書いていきます。 BrowserSync を入れているとそのありがたみがわかりますよね?
日本語で書く場合たいていタイトルも日本語になると思うので、 hexo new "日本語タイトル"
というようにコマンド発行したいところです。しかしそうするとファイル名も日本語になってしまい、 Git での管理時にちょっと面倒なことが生じる場合が多いです。なので、ファイル名は英語で生成しておいて、 title:
部分を日本語に書き換えるほうが吉です。このあたりがちょっとした不満点ですね。
最後に GitHub Pages で公開します。
Hexo の Deployment のページにある通り、まず hexo-deployer-git プラグインをインストールします。これも package.json
に加えたいので、 --save
オプションを付けておきます。
1 | $ npm install hexo-deployer-git --save |
続いて _config.yml
の deploy
の項目に必要な設定を追記します。以下は僕の例です。
1 | deploy: |
GitHub Pages で公開する場合には、GitHub Pages のドキュメントにもある通り、 <username>.github.io
というリポジトリを作ります。そこにプッシュされたコンテンツが自動的に http://<username>.github.io/
で公開されるようになります。
最後に、 Hexo で以下のコマンドを使ってデプロイします。
1 | $ hexo deploy --generate |
上記のコマンドは以下と同じ意味になります。
1 | # 静的コンテンツを生成する |
実際には、デプロイの際にいったん既存の生成データをすべて削除した上で再生成したほうが良さそうなため、僕は次のようなシェルスクリプトを実行しています。 hexo clean
は生成コンテンツとキャッシュをすべて削除するコマンドです。
1 |
|
以上が完了すると、 http://skatsuta.github.io/ にこのブログが公開されます。
Hexo は記事をすべて Markdown で書けるので簡単にバージョン管理ができますし、他の Markdown 対応静的サイトジェネレータに移行するのもハードルが低いです。
ちなみにこのブログのテーマには Landscape plus を少しカスタムして使っています。デフォルトで結構格好いいので気に入っています。
当分の間は Hexo をいろいろいじりながら使ってみようと思います。 Hugo がもう少し成熟したらそちらも使ってみたいですね。
]]>内容は非常に初歩的なことばかりです。
シェルスクリプトの先頭に書く宣言文。
どのシェルを利用して実行するかを指定する。
1 |
実行しているシェルの内部でのみ有効な変数。
1 | # 変数abcに123を代入する |
シェルから実行されたコマンド内でも有効な変数。
1 | # 環境変数abcを設定する |
read
コマンド標準入力からデータを読み込む。
1 | # abc に標準出力からのデータを代入する |
1 | # 変数 abc に文字列 xyz をセットする |
$0
は実行コマンド名、$#
は引数の数を表す。
1 |
|
1 | $ ./args.sh aaa bbb |
行末にバックスラッシュをつけると、改行コードがエスケープされて文字列を途中で折り返すことができる。
1 | $ echo "Hi! \ |
1 | if [ condition1 ]; then |
演算子 | 比較内容 |
---|---|
a == b | a と b が等しければ真 |
a != b | a と b が等しくなければ真 |
演算子 | 比較内容 |
---|---|
a -eq b | a と b が等しければ真 |
a -ne b | a と b が等しくなければ真 |
a -ge b | a が b 以上なら真 |
a -le b | a が b 以下なら真 |
a -gt b | a が b より大きいなら真 |
a -lt b | a が b より小さいなら真 |
パスがディレクトリであれば真
1 | if [ -d path ]; then ... |
1 | if test 条件節; then ... |
演算子 | 内容 |
---|---|
-f ファイル名 | 通常ファイルなら真 |
-d ファイル名 | ディレクトリなら真 |
-e ファイル名 | ファイルが存在すれば真 |
-L ファイル名 | シンボリックリンクなら真 |
-r ファイル名 | 読み取り可能なら真 |
-w ファイル名 | 書き込み可能なら真 |
-x ファイル名 | ファイルに実行権限があれば真 |
-s ファイル名 | サイズが0より大きければ真 |