1. kwatch

    No comment

    kwatch
Changes in body
Source | HTML | Preview
@@ -1,698 +1,721 @@
Rack や WSGI の代わりになる仕様を考えてみました (ライブラリ (rack.rb や wsgiref.py) のほうではなく、プロトコル仕様のほうです)。自分のアイデアを書き連ねただけなので、まとまってないかもしれませんがご了承ください。
なお本稿は、今後何度か改訂すると思います。ご意見があればご自由にコメントしてください。
* 【対象読者】Rack や WSGI に興味のある人
* 【必要な知識】Rack や WSGI の基礎知識
# Rack と WSGI の概要
Ruby の [Rack] や Python の [WSGI] は、HTTP のリクエストとレスポンスを抽象化した仕様です。
[Rack]: http://rack.github.io/
[WSGI]: http://www.wsgi.org/
たとえば Rack では:
* 引数として、リクエストを表す Hash オブジェクトを受け取り、
* 戻り値として、レスポンスのステータスコードとヘッダーとボディを返します。
```ruby
class RackApp
def call(env) # env はリクエストを表す Hash オブジェクト
status = 200 # ステータスコード
headers = {"Content-Type"=>"text/plain"} # ヘッダー
body = "Hello" # ボディ
return status, headers, [body] # この 3 つがレスポンスを表す
end
end
```
このように HTTP のリクエストとレスポンスを抽象化した仕様が、Ruby の Rack や Python の WSGI です。
これにより Web アプリケーションは、アプリケーションサーバ (WEBrick や Unicorn や Puma や UWSGI や waitress) が Rack や WSGI に対応していれば、どれでも使うことができます。たとえば開発中は手軽に使える WEBrick や waitress を使い、本番環境では高速な Unicorn や Puma や UWSGI を使う、という切り替えが簡単にできます。
また Rack や WSGI は、いわゆるデコレータパターンを使うことで、簡単に機能を追加できるように設計されています。たとえば、
* セッション機能を追加する
* リクエスト処理にかかった時間を記録する
* エラーページの表示を開発環境と本番環境で切り替える
といったことが、Web アプリケーションの変更なしに実現できます。
```ruby
## オリジナルの Rack アプリケーション
app = RackApp()
## たとえば、セッション機能を追加する
require 'rack/sesison/cookie'
app = Rack::Session::Cookie.new(app,
:key => 'rack.session', :path=>'/',
:expire_after => 3600,
:secret => '54vYjDUSB0z7NO0ck8ZeylJN0rAX3C')
## たとえば、開発環境のときだけ詳細なエラーを表示する
if ENV['RACK_ENV'] == "development"
require 'rack/showexceptions'
app = Rack::ShowExceptions(app)
end
```
このように、もとの Web アプリケーションに機能を追加するためのラッパーオブジェクトを、Rack や WSGI では「ミドルウェア (Middleware)」といいます。上の例だと、`Rack::Session::Cookie` や `Rack::ShowException` がミドルウェアです。
# WSGI (Python) の問題点
WSGI は、Rack のもとになった仕様です。WSGI がなければ Rack は生まれなかったでしょう。
WSGI が登場した時期は、似たようなものとして Java の Servlet がありました。しかし Servlet の仕様はかなり複雑であり、実装するのが大変でした[^1]。
また仕様が複雑なせいでアプリケーションサーバごとに挙動が微妙に異なることもあり、結局のところみんな仕様書を見ずに、リファレンス実装である Tomcat を動かしてみて仕様を確認するという状態でした。
そういう状態だったので、Servlet の理念には共感するものの、仕様はまったく異なるとてもシンプルなものとして WSGI は登場しました。
[^1]: 物事を必要以上に複雑にするのは、Java と IBM の得意技です。
具体的なコードをみてみましょう。以下は WSGI のサンプルコードです。
```python
class WSGIApp(object):
## environ はリクエストを表すハッシュ (辞書) オブジェクト
def __call__(self, environ, start_response):
status = "200 OK" # 数値ではなく文字列
headers = [ # ハッシュではなくキーと値のリスト
('Content-Type', 'text/plain'),
]
start_response(status, headers) # レスポンスを開始する
return [b"Hello World"] # ボディを返す
```
これを見ると、Rack と結構違うことが分かります。
* レスポンスのステータスは、Rack では数値 (ex: `200`) ですが、WSGI では文字列 (ex: `"200 OK"`) です。
これは独自のステータスコードを使う場合に違いとなります。
たとえば「[509 Bandwidth Limit Exceeded]」という独自ステータスコードを使いたい場合、WSGI では何の問題もありませんが、Rack では「509」は簡単に指定できても「Bandwidth Limit Exceeded」を指定する方法が (仕様上は) ありません。
* レスポンスのヘッダーは、Rack ではハッシュオブジェクトですが、WSGI ではキーと値のリストです。
これは、レスポンスヘッダーでは Set-Cookie ヘッダーが複数個登場する可能性があるからです。
Set-Cookie ヘッダーが複数ある場合、WSGI ではそれを自然に表現できますが、Rack だと値を複数行で表す必要があります。
* WSGI では、レスポンスの開始時に呼ばれるコールバック関数が必要です。これは Rack と比べると初心者にはわかりにくいし、使うのも少し面倒です。
* WSGI での戻り値は、レスポンスボディだけです。Rack では、ステータスコードとヘッダーとボディの 3 つを返します。
* WSGI におけるリクエストヘッダーとレスポンスヘッダーは、キーも値も `str` (つまり Python2 ならバイナリ、Python3 ならユニコード文字列) です。ただしレスポンスボディは、必ずバイナリ (のリスト) です。
[509 Bandwidth Limit Exceeded]: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#509
-さて個人的な意見ですが、WSGI の最大の問題点は、`start_response()` というコールバック関数の存在でしょう。
-これがあるせいで、初心者が WSGI を理解するにはまず「関数を受けとる関数 (高階関数)」を理解しなければならず、敷居が高いです[^2]。
-また WSGI アプリケーションを呼び出すのも、`start_response()` のせいで無駄に手間がかかります。これ、ほんと面倒。
-
+さて個人的な意見ですが、WSGI の最大の問題点は、`start_response()` というコールバック関数の存在でしょう。これがあるせいで、初心者が WSGI を理解するにはまず「関数を受けとる関数 (高階関数)」を理解しなければならず、敷居が高いです[^2]。
[^2]: 「高階関数なんて簡単に理解できるだろ」とおっしゃる上級者の方は、初心者がどこでつまづくかを理解する才能が根本的に欠けてるので、初心者の相手はせずに関数型言語の世界にでもお帰りください。名選手、名監督にあらず。スポーツ万能な人は運動オンチの指導には向かない。
+また WSGI アプリケーションを呼び出すのも、`start_response()` のせいで無駄に手間がかかります。これ、ほんと面倒。
+
```python
## いちいちこういうのを用意しないと、
class StartResponse(object):
def __call__(self, status, headers):
self.status = status
self.headers = headers
## WSGIアプリケーションを呼び出せない
app = WSGIApplication()
environ = {'REQUEST_METHOD': 'GET', ...(snip)... }
start_response = StartResponse()
body = app.__call__(environ, start_response)
print(start_response.status)
print(start_response.headers)
```
-(実はWSGI ([PEP-333]) に対し、この点を改善した Web3 ([PEP-444]) という仕様が過去に提案されていました。この Web3 ではコールバック関数を廃し、Rack と同じように `status, headers, body` を返す仕様になっていました。個人的に期待してたんですが、結局は採用されませんでした。残念です。)
+(実は WSGI ([PEP-333]) に対し、この点を改善した Web3 ([PEP-444]) という仕様が過去に提案されていました。この Web3 ではコールバック関数を廃し、Rack と同じように `status, headers, body` を返す仕様になっていました。個人的に期待してたんですが、結局は採用されませんでした。残念です。)
[PEP-333]: https://www.python.org/dev/peps/pep-0333/
[PEP-444]: https://www.python.org/dev/peps/pep-0444/
また WSGI では、レスポンスヘッダーがハッシュ (辞書) オブジェクトではなく、キーと値のリストになっているのもちょっと困ります。なぜなら、ヘッダーを設定するときにいちいちリストを検索しなければならないからです。
```python
## たとえばこんなレスポンスヘッダーがあるとして、
resp_headers = [
('Content-Type', "text/html"),
('Content-Disposition', "attachment;filename=index.html"),
('Content-Encoding', "gzip"),
]
## 値を設定するにはいちいちリストを検索する必要がある
key = 'Content-Length'
val = str(len(content))
for i, (k, v) in enumerate(resp_headers):
if k == key: # or k.tolower() == key.tolower()
break
else:
i = -1
if i >= 0: # あれば上書き
resp_headers[i] = (key, val)
else: # なければ追加
resp_headers.append((key, val))
```
これは面倒くさい。専用のユーティリティ関数を定義するのもいいですけど、どうせならハッシュ (辞書) オブジェクトを使ったほうがよかったです。
```python
## ハッシュオブジェクト (辞書オブジェクト) なら…
resp_headers = {
'Content-Type': "text/html",
'Content-Disposition': "attachment;filename=index.html",
'Content-Encoding': "gzip",
]
## 値を設定するのがとても簡単!
## (ただしキー名の大文字小文字が統一されていることが前提)
resp_headers['Content-Length'] = str(len(content))
```
# Rack (Ruby) の問題点
Rack (Ruby) は、WSGI (Python) を参考にして決められた仕様です。Rack は WSGI ととてもよく似ていますが、よりシンプルになるように改良されています。
```ruby
class RackApp
def call(env) # env はリクエストを表すハッシュオブジェクト
status = 200
headers = {
'Content-Type' => 'text/plain;charset=utf-8',
}
body = "Hello World"
return status, headers, [body] # この3つがレスポンスを表す
end
end
```
+具体的な違いは次の通りです。
+
* WSGI と比べると、Rack ではコールバック関数を使っていません。
そのおかげで、「リクエストを受け取りレスポンスを返す」という動作が、とても自然に表現されてます。
これなら初心者でも理解できるでしょう。
* Rack では、レスポンスヘッダーはハッシュオブジェクトを使います。
そのため、値の設定がとても簡単にできます (ただしキーがたとえば `'Content-Type'` だったり `'content-type'` だったりまちまちだと問題になる可能性があるので、統一しておく必要があります)。
* Rack では、ステータスコードは数値で表します。カスタムステータスコードを指定した場合の挙動は仕様外であり、各アプリケーションサーバごとに設定が必要となるでしょう。
-さて、Rack ではレスポンスヘッダーハッシュオブジェクトを使います。
-この場合、`Set-Cookie` のように複数回登場する可能性があるヘッダーはどうしたらいいでしょうか。
+さて、Rack ではレスポンスヘッダーハッシュオブジェクトで表します。この場合、`Set-Cookie` のように複数回登場する可能性があるヘッダーはどうしたらいいでしょうか。
[Rack の仕様]では、次のような記述があります。
[Rack の仕様]: http://www.rubydoc.info/github/rack/rack/master/file/SPEC
> The values of the header must be Strings, consisting of lines (for multiple header values, e.g. multiple Set-Cookie values) separated by "\n".
つまりヘッダーの値が複数行の文字列なら、ヘッダーが複数回登場したとみなします。
しかしこの仕様はどうかと思います。なぜならすべてのレスポンスヘッダーに対して、改行文字を含むかどうかを調べる必要があるからです。これはパフォーマンスを落とします。
```ruby
headers.each do |k, v|
v.split(/\n/).each do |s| # ←二重ループ ;-(
puts "#{k}: #{s}"
end
end
```
これよりは、「複数回登場するヘッダーは値を配列にする」という仕様のほうがよさそうです。
```ruby
headers.each do |k, v|
if v.is_a?(Array) # ←こっちのほうがマシ
v.each {|s| puts "#{k}: #{s}" }
else
puts "#{k}: #{v}"
end
end
```
あるいは、Set-Cookie ヘッダーだけ特別扱いするのでもいいです。複数回登場する可能性のあるヘッダーは Set-Cookie ぐらいでしょうから[^3]、この仕様でも悪くないです。
```ruby
set_cookie = "Set-Cookie"
headers.each do |k, v|
if k == set_cookie # ← Set-Cookieだけ特別扱い
v.split(/\n/).each {|s| puts "#{k}: #{s}" }
else
puts "#{k}: #{v}"
end
end
```
[^3]: ほかに Via ヘッダーがあったと思うけど、Rack や WSGI の範疇では扱わないから Set-Cooki だけ考慮すればいいはず。
もう一点、レスポンスボディの `close()` メソッドについて。
-Rack や WSGI の仕様では、レスポンスボディのオブジェクトが 'close()' というメソッドを持っている場合、クライアントへのレスポンスが完了したらアプリケーションサーバが `close()` を呼び出すという仕様になっています。これは、主にレスポンスボディが File オブジェクトの場合を想定した仕様です。
+Rack や WSGI の仕様では、レスポンスボディのオブジェクトが `close()` というメソッドを持っている場合、クライアントへのレスポンスが完了したらアプリケーションサーバが `close()` を呼び出すという仕様になっています。これは、主にレスポンスボディが File オブジェクトの場合を想定した仕様です。
```ruby
def call(env)
filename = "logo.png"
headers = {'Content-Type' => "image/png",
'Content-Length' => File.size(filename).to_s}
## ファイルを open する
body = File.open(filename, 'rb')
## open したファイルは、レスポンス完了時にはアプリサーバによって
## 自動的に close() が呼ばれる
return [200, headers, body]
end
```
けどこれは、`each()` メソッドの終わりでファイルを close すればいいだけのように思います。
```ruby
class AutoClose
def initialize(file)
@file = file
end
def each
## これは行単位での読み出しなので効率はよくない
#@file.each |line|
# yield line
#end
## もっと大きいサイズで読み出したほうが効率はいい
while (s = @file.read(8192))
yield s
end
ensure # ファイルを全部読み出したら or エラーがあったら
@file.close() # 自動的に close する
end
end
```
この、`close()` メソッドがあれば呼び出すという仕様は、レスポンスボディの `each()` メソッドが一回も呼ばれないケースでは必要なんでしょう。個人的には、こんな「File オブジェクトのことしか考えてません」的な仕様よりは、xUnit における `teardown()` のような後始末用の仕様をきちんと考えるべきだったと思います (とはいえ妙案があるわけでもないのですが)。
# Environment オブジェクトについて
Rack でも WSGI でも、HTTP リクエストはハッシュ (辞書) オブジェクトとして表されます。これは Rack や WSGI の仕様では Environment と呼ばれています。
これがどんなものか、表示してみましょう。
```ruby
## Filename: sample1.ru
require 'rack'
class SampleApp
## Inspect Environment data
def call(env)
status = 200
headers = {'Content-Type' => "text/plain;charset=utf-8"}
body = env.map {|k, v| "%-25s: %s\n" % [k.inspect, v.inspect] }.join()
return status, headers, [body]
end
end
app = SampleApp.new
run app
```
これを `rackup sample1.ru -E production -s puma -p 9292` で実行し、ブラウザで http://localhost:9292/index?x=1 にアクセスすると、たとえばこんな結果になりました。これが Environment の中身です。
```
"rack.version" : [1, 3]
"rack.errors" : #<IO:<STDERR>>
"rack.multithread" : true
"rack.multiprocess" : false
"rack.run_once" : false
"SCRIPT_NAME" : ""
"QUERY_STRING" : "x=1"
"SERVER_PROTOCOL" : "HTTP/1.1"
"SERVER_SOFTWARE" : "2.15.3"
"GATEWAY_INTERFACE" : "CGI/1.2"
"REQUEST_METHOD" : "GET"
"REQUEST_PATH" : "/index"
"REQUEST_URI" : "/index?x=1"
"HTTP_VERSION" : "HTTP/1.1"
"HTTP_HOST" : "localhost:9292"
"HTTP_CACHE_CONTROL" : "max-age=0"
"HTTP_COOKIE" : "_ga=GA1.1.1305719166.1445760613"
"HTTP_CONNECTION" : "keep-alive"
"HTTP_ACCEPT" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
"HTTP_USER_AGENT" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9"
"HTTP_ACCEPT_LANGUAGE" : "ja-jp"
"HTTP_ACCEPT_ENCODING" : "gzip, deflate"
"HTTP_DNT" : "1"
"SERVER_NAME" : "localhost"
"SERVER_PORT" : "9292"
"PATH_INFO" : "/index"
"REMOTE_ADDR" : "::1"
"puma.socket" : #<TCPSocket:fd 14>
"rack.hijack?" : true
"rack.hijack" : #<Puma::Client:0x3fd60649ac48 @ready=true>
"rack.input" : #<Puma::NullIO:0x007fac0c896060>
"rack.url_scheme" : "http"
"rack.after_reply" : []
```
(rack.hijack は Rack 1.5 から導入された新機能です。詳しくは[こちら]をご覧ください。)
[こちら]: http://kwatch.houkagoteatime.net/blog/2013/01/25/rack-hijacking-api/
この Environment には、大きく分けると 3 種類のデータが入ってます。
* HTTP リクエストヘッダー
* キーが「HTTP_」で始まるもの (ただし HTTP_VERSION は除く)
* (この例にはないが) CONTENT_TYPE と CONTENT_LENGTH
* ヘッダー以外のリクエスト情報
* REQEUST_METHOD
* PATH_INFO
* QUERY_STRING
* HTTP_VERSION
* REMOTE_ADDR
* rack.input
* rack.url_scheme
* puma.socket
* アプリケーションサーバからの情報
* SERVER_NAME
* SERVER_PORT
* rack.version などキーが「rack.」で始まるもの
これらがごっちゃになって格納されているのが Environment です。個人的にはこのような仕様が好きではなく、せめてリクエストヘッダーとそれ以外ぐらいは分けてほしいと思います。
なぜこんな仕様になっているかというと、CGI の仕様をもとにしているからです。今の若い人は CGI なんか知らないと思いますが、そういうのがあって、昔はとてもよく使われてたんですよ。この CGI の仕様を借りて WSGI が Environment の仕様を決め、Rack もそれを継承しています。そのため、CGI を知らない人から見ると奇妙な仕様に見えるでしょう。「なんで User-Agent ヘッダーが HTTP_USER_AGENT に変わってるの?そのまま User-Agent 文字列を使えばいいのに。」のような感想が出てきそうです。
# Environment オブジェクトの問題点
すでに見たように、Environment オブジェクトは要素を何十個も含んだハッシュオブジェクトです。
パフォーマンスの観点から言うと、要素が何十個もあるようなハッシュオブジェクトの生成は、Ruby や Python では動作コストがかなりかかるため、望ましくありません。たとえば Ruby on Rails より 100 倍速いフレームワークである [Keight.rb] の場合、**リクエストの処理にかかる時間よりも、Environment オブジェクトを生成するほうに時間がかかる場合があります**。
[Keight.rb]: https://github.com/kwatch/keight/tree/ruby
実際に、ベンチマークスクリプトで確かめてみましょう。
```ruby
# -*- coding: utf-8 -*-
require 'rack'
require 'keight'
require 'benchmark/ips'
## アクションクラス (MVC でいうところのコントローラ) を作成
class API < K8::Action
mapping '/hello', :GET=>:say_hello
def say_hello()
return "<h1>Hello, World!</h1>"
end
end
## Rack アプリケーションを作成し、アクションクラスを割り当てる
mapping = [
['/api', API],
]
rack_app = K8::RackApplication.new(mapping)
## 実行例
expected = [
200,
{"Content-Length"=>"22", "Content-Type"=>"text/html; charset=utf-8"},
["<h1>Hello, World!</h1>"]
]
actual = rack_app.call(Rack::MockRequest.env_for("/api/hello"))
actual == expected or raise "assertion failed"
## GET /api/hello を表す Environemnt オブジェクト
env = Rack::MockRequest.env_for("/api/hello")
## ベンチマーク
Benchmark.ips do |x|
x.config(:time => 5, :warmup => 1)
## Environment オブジェクトを新しく生成する (コピーする)
x.report("just copy env") do |n|
i = 0
while (i += 1) <= n
env.dup()
end
end
## Environment オブジェクトを生成して、リクエストを処理する
x.report("Keight (copy env)") do |n|
i = 0
while (i += 1) <= n
actual = rack_app.call(env.dup)
end
actual == expected or raise "assertion failed"
end
## Environment オブジェクトを再利用して、リクエストを処理する
x.report("Keight (reuse env)") do |n|
i = 0
while (i += 1) <= n
actual = rack_app.call(env)
end
actual == expected or raise "assertion failed"
end
x.compare!
end
```
これを実行すると、たとえば次のような結果が得られました (Ruby 2.3, Keight.rb 0.2, OSX El Capitan)。
```
Calculating -------------------------------------
just copy env 12.910k i/100ms
Keight (copy env) 5.523k i/100ms
Keight (reuse env) 12.390k i/100ms
-------------------------------------------------
just copy env 147.818k (± 8.0%) i/s - 735.870k
Keight (copy env) 76.103k (± 4.4%) i/s - 381.087k
Keight (reuse env) 183.065k (± 4.8%) i/s - 916.860k
Comparison:
Keight (reuse env): 183064.5 i/s
just copy env: 147818.2 i/s - 1.24x slower
Keight (copy env): 76102.8 i/s - 2.41x slower
```
最後の 3 行から次のことがわかります。
* Environment オブジェクトを生成せず再利用すれば 18.3 万 i/s なのに、リクエストごとに生成すると半分以下の 7.6 万 i/s になった。
* 再利用してリクエストを処理した場合 (18.3 万 i/s) よりも、リクエストを処理せずコピーするだけの場合 (14.8 万 i/s) のほうが遅い。つまりリクエストの処理にかかる時間よりも、Environment オブジェクトを生成するだけのほうがコストが高い。
このような状況なので、これ以上フレームワークを高速化してもアプリケーションは大して速くはならないでしょう。この行き詰まった状況を打破するには、Rack の仕様自体を改善するのがよさそうです。
# デコレータパターンの問題点
(TODO)
# 次世代の Rack や WSGI を考えてみる
さて、ようやく本題に入ります。
今まで説明したような問題点を解消するために、現行の Rack や WSGI にとってかわるものを考えてみました。いわゆる、「ぼくのかんがえたさいきょうのらっく」ですね。
新しい仕様でも、HTTP リクエストとレスポンスを抽象化することには変わりません。なので、この 2 つをどう抽象化するかを中心に説明します。
+また現在の Rack や WSGI では CGI の仕様を部分的に継承しています。しかし CGI は環境変数経由でデータを渡すことを想定した、古い時代の仕様です。今の時代にはふさわしくありませんので、CGI の仕様は忘れていいでしょう。
+
## HTTP リクエスト
HTTP リクエストは、以下のような要素に分けられます。
* リクエストメソッド (GET/POST/PUT/DELETE/PATCH/HEADER/OPTIONS/TRACE)
* リクエストパス (ex: '/index.html')
* リクエストヘッダー (ex: {'Host'=>..., 'User-Agent'=>...})
* クエリパラメータ (ex: '?x=1')
* I/O (rack.input, rack.errors, rack.hijack or puma.socket)
* その他リクエスト情報 (HTTP_VERSION, REMOTE_ADDR, rack.url_scheme)
* サーバー情報 (SERVER_NAME, SERVER_PORT, rack.version)
リクエストメソッドは、大文字の文字列か Symbol でいいでしょう。性能を考えると Symbol のほうがよさそうです。
```ruby
meth = :GET
```
リクエストパスは、文字列でいいでしょう。Rack では PATH_INFO だけでなく SCRIPT_NAME も考慮する必要がありますが、いまや SCRIPT_NAME を使う人もいないでしょうから、PATH_INFO 相当だけを考えることにします。
```ruby
path = "/index.html"
```
リクエストヘッダーは、ハッシュオブジェクトでいいでしょう。また User-Agent → HTTP_USER_AGENT のような変換はしたくないですが、HTTP/2 ではヘッダー名が小文字らしいので、それに合わせることになるでしょう。
```ruby
headers = {
"host" => "www.example.com",
"user-agent" => "Mozilla/5.0 ....(snip)....",
....(snip)....,
}
```
クエリパラメータは、`nil` か、または文字列です。`?` がなければ `nil` になって、あれば文字列になります (空文字の可能性もあります)。
```ruby
query = "x=1"
```
I/O 関連 (rack.input と rack.errors と rack.hijack or puma.socket) は、1 つの配列にすればよさそうです。これらはちょうど、stdin と stderr と stdout に相当する・・・んじゃないかな?もしかしたら socket は rack.input を兼ねるかもしれないけど、よく知らないのでここでは分けておきます。
```ruby
ios = [
StringIO.new(), # rack.input
$stderr, # rack.errors
puma_socket,
]
```
その他リクエスト情報は、リクエストごとに値が変わります。これはハッシュオブジェクトにすればいいでしょう。
```ruby
options = {
http: "1.1", # HTTP_VERSION
client: "::1", # REMOTE_ADDR
protocol: "http", # rack.url_scheme
}
```
最後のサーバ情報は、アプリケーションサーバに変更がない限りは値が変わらないはずです。だから一度ハッシュオブジェクトとして作れば、使い回しができます。
```ruby
server = {
name: "localhost".freeze, # SERVER_NAME
port: "9292".freeze, # SERVER_PORT
'rack.version': [1, 3].freeze,
'rack.multithread': true,
'rack.multiprocess': false,
'rack.run_once': false,
}.freeze
```
これらを受けとるような Rack アプリケーションを考えてみましょう。
```ruby
class RackApp
def call(meth, path, headers, query, ios, options, server)
input, errors, socket = ios
...
end
end
```
うわー、引数が 7 個もあるわー。
これはちょっとイケてないですね。最初の 3 つ (meth と path と headers) はリクエストのコアというべき部分なので単独の引数のままにしておくとして、query と ios は options にまとめられそうです。
```ruby
options = {
query: "x=1", # QUERY_STRING
#
input: StringIO.new, # rack.input,
error: $stderr, # rack.erros,
socket: puma_socket, # rack.hijack or puma.socket
#
http: "1.1", # HTTP_VERSION
client: "::1", # REMOTE_ADDR
protocol: "http", # rack.url_scheme
}
```
こうすると、引数が 7 つから 5 つに減ります。
```ruby
+class RackApp
def call(meth, path, headers, options, server)
query = options[:query]
input = options[:input]
error = options[:error]
socket = options[:socket] # or :output ?
...
end
+end
```
まあ、これなら使ってもいいかなと思えます。
## HTTP レスポンス
HTTP レスポンスは従来通り、ステータスとヘッダーとボディの 3 つで表せばいいでしょう。
-```rb
-def call(meth, path, headers, options, server)
- status = 200
- headers = {"content-type"=>"application/json"}
- body = '{"message":"Hello!"}'
- return status, headers, body
-end
+```ruby
+ def call(meth, path, headers, options, server)
+ status = 200
+ headers = {"content-type"=>"application/json"},
+ body = '{"message":"Hello!"}'
+ return status, headers, body
+ end
```
ただ、Content-Type ヘッダーは特別扱いしてもいいかなと思います。なぜなら現在の Rack アプリケーションでは、ヘッダとして `{"Content-Type"=>"text/html}"` や `{"Content-Type"=>"application/json"}` のように、Content-Type しか含まないケースが多いからです。そのため、Content-Type だけ特別扱いして独立させると、少し簡潔になります。
```ruby
- def call(*args)
+ def call(meth, path, headers, options, server)
## これよりも
return 200, {"Content-Type"=>"text/plain"}, ["Hello"]
## こっちのほうが簡潔
return 200, "text/plain", {}, ["Hello"]
end
```
ほかにも、いくつか論点があります。
<dl>
<dt>ステータスは整数か文字列か?</dt>
<dd>整数でいいと思いますが、カスタムステータスを指定する方法があったほうがいいでしょう。ただそれは Rack の仕様ではなく、各アプリケーションサーバごとに登録する方法があればそれでいいと思います。</dd>
<dt>ヘッダーはハッシュかリストか?</dt>
<dd>これはもうハッシュでいいでしょう。</dd>
<dt>Set-Cookie ヘッダーが複数個ある場合はどうするか?</dt>
<dd>これはすでに説明したように、ヘッダーの値に文字列の配列を許せばいいでしょう。そしてヘッダー値は改行文字を含んではならないと決めましょう。</dd>
<dt>ボディに文字列を許すかどうか?</dt>
<dd>現行の Rack の仕様では、ボディは `each()` メソッドを実装していなければならないため、ボディに文字列を直接指定できません。かわりに、文字列の配列を指定するのが定番です。
しかしほとんどのレスポンスがボディとして文字列を返すのだから、いちいち配列で包むのは無駄です。できれば、ボディは「文字列、または `each()` で文字列を返すオブジェクト」とするのがいいでしょう。</dd>
<dt>レスポンス完了時にボディの `close()` メソッドを呼び出すべきか?</dt>
<dd>これは難しい問題です。すでに説明したように、`each()` メソッドが必ず呼び出されることが保証されるなら、この仕様はなくても構わないです。ただそういう保証はないので、かわりに `close()` を呼び出すことを保証しているのでしょう。
しかし本当に望ましいのは、`teardown()` 相当の機能を用意することでしょう。具体的な仕様が思いつかないのが残念です[^4]。</dd>
[^4]: `rack.after_reply` というのがそれかなと思いましたが、どうも Puma の独自機能のようです。
## デコレータパターン
(TODO)
## イベント駆動や non-blocking I/O
(TODO)
## HTTP/2 対応
(TODO)
# おわりに
識者のご意見をいただきたい。
# 参考文献
## WSGI 関連
* PEP-0333 -- Python Web Server Gateway Interface v1.0.1
* https://www.python.org/dev/peps/pep-3333/ (Python3 に対応した改訂版)
* https://www.python.org/dev/peps/pep-0333/ (オリジナル)
* WSGI の仕様。すべてはここから始まった。
* PEP-0444 -- Python Web3 Interface
* https://www.python.org/dev/peps/pep-0444/#values-returned-by-a-web3-application
* WSGI の後継となるべく提案された仕様。残念ながら採択には至らず。
## Rack 関連
* Rack: a Ruby Webserver Interface
* http://rack.github.io/ (Webサイト)
* http://rubydoc.info/github/rack/rack/master/file/SPEC
* 最近の仕様。ハイジャックAPIについても記載あり。
* Rubyist Magazine: Rack 仕様 (翻訳)
* http://magazine.rubyist.net/?0033-TranslationArticle
* Rack 1.1 時代なので古いことに注意。
* Rack 1.5 新機能「ハイジャックAPI」について
* http://kwatch.houkagoteatime.net/blog/2013/01/25/rack-hijacking-api/
* 正直、この仕様はどうかと思う。
+
+
+# 関連リンク
+
+ちょうど Rack のメーリングリストに [HTTP2 対応についての質問が出てた](https://groups.google.com/forum/#!topic/rack-devel/_sPwu9vVsYA)。関連して Rack2 の話題が少し出たので、いろいろぐぐってみた。
+
+* Wardrop/Rack-Next: For discussion and planning of the next ruby web interface.
+ * https://github.com/Wardrop/Rack-Next
+ * Rack の次期バージョンに期待すること。あとで読む。
+* Rack 2.0, or Rack for the Future
+ * https://gist.github.com/raggi/11c3491561802e573a47
+ * Rack の良かった点、悪かった点を列挙している。
+* the_metal: A spike for thoughts about Rack 2.0
+ * https://github.com/tenderlove/the_metal
+ * Nokogiri の作者 tenderlove による、Request オブジェクトと Response オブジェクトについて最小限の仕様を決めようという実験。[Issue](https://github.com/tenderlove/the_metal/issues) に興味深い意見が見られる。
+* halorgium/rack2: async hax for rack
+ * https://github.com/halorgium/rack2
+ * Rack を非同期対応にする試み?