Rackについて調べても、「ではまず簡単なRackアプリケーションを作ってみましょう。config.ru
を開いて……」みたいな資料しか出てこなくて、サーバー側からみたRackインターフェースがどうなってるかという記事が全然見当たらなかった。んだけど、何やら外国の人のブログでちょうど望んでる内容のものがあったのでざっくり翻訳と紹介します(許可とった)。言い回しとかで解釈わかんないところがあって飛ばしたりしているので本文とニュアンスだいぶ変わってしまっているかもなのですが、まあ最低限の内容が伝われば……ご了承ください。
元記事:http://www.blrice.net/blog/2015/05/31/make-your-own-rack-server/
自分のRackサーバーを作ってみよう
Ruby触ってたら必然Rackについて学ぶよね。RailsとかSinatraとかRubyのWAFの心臓部分。Rackアプリケーションの作成の資料は山ほどあるんだけど、逆側についての資料を見かけたことがない。webサーバーはどうやってRackAppと会話してるんだろ。というわけでSinatraのために最小限のサーバーを書いてみた。
# my_server is the server I want to write
set :server, :my_server
get '/' do
'Hello world!'
end
これをそのまま走らせると当然エラーになる。なぜかというとSinatraがRackにmy_serverを使わせてくれと頼んでも、Rackはそんなサーバーを知らないからだ。なので、まずRackにこのmy_serverを教えてあげる必要がある。
require 'rack'
# Stub out the server we're making
class MyServer
def initialize(app)
@app = app
end
def start
# Handle requests
end
end
module Rack
module Handler
class MyServer
def self.run(app, options = {})
server = ::MyServer.new(app)
server.start
end
end
end
end
Rack::Handler.register('my_server', 'Rack::Handler::MyServer')
Rackにサーバーについて教えるには、Handlerにサーバーの開始方法を書いたクラスを登録してあげれば良い。それはrun
というRackアプリケーションのオブジェクトとサーバー設定値のハッシュを受け取る一つのメソッドを持つ。
さあ、あとはServerの中身の実装(これが一番大変)。
class MyServer
STATUS_CODES = {200 => 'OK', 500 => 'Internal Server Error'}
attr_reader :app, :tcp_server
def initialize(app)
@app = app
end
def start
@tcp_server = TCPServer.new('localhost', 8080)
loop do
socket = tcp_server.accept
request = socket.gets
response = ''
env = new_env(*request.split)
status, headers, body = app.call(env)
response << "HTTP/1.1 #{status} #{STATUS_CODES[status]}\r\n"
headers.each do |k, v|
response << "#{k}: #{v}\r\n"
end
response << "Connection: close\r\n"
socket.print response
socket.print "\r\n"
if body.is_a?(String)
socket.print body
else
body.each do |chunk|
socket.print chunk
end
end
socket.close
end
end
def new_env(method, location, *args)
{
'REQUEST_METHOD' => method,
'SCRIPT_NAME' => '',
'PATH_INFO' => location,
'QUERY_STRING' => location.split('?').last,
'SERVER_NAME' => 'localhost',
'SERVER_POST' => '8080',
'rack.version' => Rack.version.split('.'),
'rack.url_scheme' => 'http',
'rack.input' => StringIO.new(''),
'rack.errors' => StringIO.new(''),
'rack.multithread' => false,
'rack.run_once' => false
}
end
end
もしあなたが普通のHTTPサーバーを書いたことがあれば上のコードの大体は同じようなものだとわかるだろう。ループしながらTCP接続を待って、接続が始まったら諸々必要な環境設定と受け取ったRequestとRackアプリケーションに投げて、アプリケーションから帰ってくるResponseをクライアントに返して接続を閉じる。もちろんこのサーバーは色々と簡素で実際の使用に耐えるものではない。
上のコードの中でRackに対応するためのコードはhashを返すnew_env
メソッドだ。Rackアプリケーションはcallメソッドに返事をするただのオブジェクトにすぎない。このメソッドは現在の環境を表すハッシュの引数を一つ取る。この例では最低限の情報しかこのハッシュ煮詰めていないが、Rackの仕様を見ればより詳細なオプションがあることがわかる。キーポイントはRackアプリケーションは環境(を表現する)ハッシュを期待しているということと、それを提供するのがサーバー側の仕事ということ。
このサーバーは明らかに簡素で機能は足りてないが、Rackを学ぶ足がかりには十分だろう。ちょっとRackのソースを読んだ後、なんて簡単なんだ、と思ったものだ。サーバー側から見ても、RackはRubyのwebアプリケーションの世界に対して本当にシンプルなインターフェースを提供してくれている。