#「VMの起動が遅いなら落とさなければいいじゃない」とjuliaは言った。
前回、juliaをCentOS7にインストールしましたが、
juliaのVMは起動が遅い
せっかく計算が速いのに、起動が遅い。
Webのサービスで使おうと思えば致命的な遅さです。
「…だったら落とさなければいいじゃない。」
天の声が聞こえました。
きっとjuliaの声だと思います。
というわけで、
「juliaでソケットサーバを作って立ち上げっぱなしにしておいて、リクエストがあった時に事前に用意した関数で計算する」
っていうのをやってみます。
起動時に関数もプリコンパイルされるというメリットもあるかもしれません。
#インタラクティブに通信を試す
ソケットサーバの動作を理解するには実際にやり取りを見るのが早いと思いますので、早速やってみましょう。
まず、ターミナルを2個立ち上げます。
それぞれを「julia側」と「クライアント側」とします。
以下を1ステップずつ試してみましょう。
REPLでサーバというのもなんか新鮮です。
[julia側]
$ julia
> server = listen(2000) ↵
> conn = accept(server) ↵# ここで接続を待つ
[クライアント側]
$ telnet localhost 2000 ↵
hi ↵
[julia側]
> readline(conn) ↵
> write(conn, "hello") ↵
[クライアント側]
(「hello」と表示される)
[julia側]
> close(socket) ↵
closeするとクライアント側の接続も切断されます。
イメージがつかめましたでしょうか?
#いちばん簡単なソケットサーバ
今度はこれをプログラムにして、echoサーバを作ってみます。
「julia socket」などで検索するとよく見つかるリファレンス実装です。
#!/usr/local/bin/julia
#=
ソケットサーバのリファレンス実装1
1:1 の接続に対応
複数の接続が来たら待たせる
=#
server = listen(2000)
while true
conn = accept(server)
line = readline(conn)
write(conn, line)
end
上記のプログラムを作成して、起動させると待機状態になります。
別のターミナルから、
telnet localhost 2000
で接続し、何か文字を入力してEnterキーを押すと、入力した文字がそのまま返ってきます。
待機中のターミナルでCtrl+Cを押すと止まります。
いろいろとエラーメッセージが表示されますが…。
ただ、この方法だと
2つ以上のクライアントがある場合、先に接続したクライアントがリクエストを送るまで次の接続は待たされることになります。
これはちょっとまずいです。
#複数のクライアントに対応させる
接続が来るたびにforkしちまえばいいんじゃね?
と一瞬思いましたが、それだと起動時間を節約するという本来の目的を見失っています。
あほでした。
幸いjuliaは並列処理が得意で、「@async」という魔法のマクロがあります。
「@async」を使うと簡単にコルーチンを生成できます。
以下、コルーチンを使ったサンプルです。
#!/usr/local/bin/julia
#=
ソケットサーバのリファレンス実装2
1:n の接続に対応
レスポンスは全員に返る
=#
server = listen(2000)
while true
conn = accept(server)
@async begin
line = readline(conn)
write(conn, line)
end
end
これだと、複数のクライアントが接続しに来ても大丈夫です。
でも、ちょっと待て…。
これだと、サーバがソケットに書き込むとすべてのクライアントに送信されます。
チャットサーバ的な使い方ならこれでバッチリですが、1:1で個別に送信したい場合は困ります。
やっぱり女の子にはそっとささやきたいたらささやき返して欲しいですよね?
大声で返事されたらみんなに筒抜けで大変なことになっちゃいます。
えーと、なんの話でしたっけ?
#1:1で個別にレスポンスを返す
いよいよ現実的な実装に近づけます。
個別にリクエストを受け取って計算した結果を送信元だけに返します。
関数は別の記事で使った、「頭の悪い方法でフィボナッチ数を求める関数」を使いまわします。
#!/usr/local/bin/julia
#=
サンプル用関数
includeさせたほうがすっきりしそう
=#
function fib(n)
if n < 2
return n
else
return fib(n - 2) + fib(n - 1)
end
end
#=
ソケットサーバ
1:1 のコネクションを複数接続可
Todo : シグナル見て停止とかタイムアウトとかサーバらしい実装が必要
=#
# ソケットを準備
server = listen(2000)
# 無限ループさせる
while true
# 接続待ち
conn = accept(server)
# 接続が来たら、@asyncでコルーチン生成
@async begin
# 接続をコピー
peer = conn
# リクエスト読込
line = readline(peer)
# リクエストによって処理を分ける(サンプル:「close」が来たらサーバ終了)
if chomp(line) == "close"
exit()
end
# レスポンスの書き出し
write(peer, string(fib(parse(Int64,line))))
# 接続を切る
close(peer)
end
end
より実践に近くクライアントも作成します。
PHPで書いてみました。
このPHPではsocketを使いますので、インストール時にsocketが有効にされている必要があります。
#!/usr/local/bin/php
<?php
/**
* callSocket 汎用一発ソケットクライアント
* ホスト名とポート番号を指定して一発だけ文字列を投げつけます。
*
* @param string $request サーバに投げる文字列
* @param string $host = "localhost" ホスト名 デフォルトは「localhost」
* @param integer $port = 2000 ポート番号 デフォルトは「2000」
* @return string サーバから返ってきた文字列
* @todo エラーチェックが甘い
*/
function callSocket($request, $host = "localhost", $port = 2000)
{
// 最低限のサニタイジング
$request = trim($request) . "\n";
$port = intval($port);
// ソケットを開く
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$result = @socket_connect($socket, $host, $port);
if (!$result) {
// ソケットを開けなかったらfalseを返して抜ける
return false;
}
// リクエスト送信
socket_write($socket, $request, strlen($request));
// レスポンスを1行取得
// 複数行欲しい場合はwhileで。
// 取得する文字長は4096バイトに設定
$response = socket_read($socket, 4096);
// ソケットクローズ
socket_close($socket);
// レスポンスを返す
return $response;
}
print callSocket("39", "localhost", 2000);
上記2つのプログラムを別のターミナルで動かすと通信ができていることがわかると思います。
JSONパッケージを利用すれば、もっと凝った情報をやり取りできると思います。
これでjuliaのVMの起動時間0.3〜0.4秒が短縮できるようになりました。
計算だけなら、JavaやCに匹敵する速さです。
仮にPHPから普通にjuliaの実行プログラムを呼び出すとこの時間ブラウザが反応しなくなりますので、Webでは効果絶大です。
もし、この時間に貴重さを感じなければ、WEBサイトの作りを考え直すべきです。
今回のサンプルではエラーチェックが甘かったりシグナルの捕捉などを行なっておりませんので、このサービスをそのままWebで公開するのは危険です。
他の方法としては、「HttpServer」パッケージを利用する方法が考えられます。
juliaで簡単にhttpサーバが作れますので、リバースプロキシと組み合わせるといろいろと面白いことができそうです。
その時はまた記事にしたいと思います。