1. kwatch

    Posted

    kwatch
Changes in title
+次世代の Rack や WSGI を考えてみる
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,696 @@
+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」は簡単に指定できても「Randwidth 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()` のせいで無駄に手間がかかります。これ、ほんと面倒。
+
+ [^2]: 「高階関数なんて簡単に理解できるだろ」とおっしゃる上級者の方は、初心者がどこでつまづくかを理解する才能が根本的に欠けてるので、初心者の相手はせずに関数型言語の世界にでもお帰りください。名選手、名監督にあらず。スポーツ万能な人は運動オンチの指導には向かない。
+
+```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` を返す仕様になっていました。個人的に期待してたんですが、結局は採用されませんでした。残念です。)
+
+ [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 の仕様]では、次のような記述があります。
+
+ [Rac の仕様]: 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 オブジェクトの場合を想定した仕様です。
+
+```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 つをどう抽象化するかを中心に説明します。
+
+
+## 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
+ def call(meth, path, headers, options, server)
+ query = options[:query]
+ input = options[:input]
+ error = options[:error]
+ socket = options[:socket] # or :output ?
+ ...
+ end
+```
+
+まあ、これなら使ってもいいかなと思えます。
+
+
+## HTTP レスポンス
+
+HTTP レスポンスは従来通り、ステータスとヘッダーとボディの 3 つで表せばいいでしょう。
+
+def call(meth, path, headers, options, server)
+ status = 200
+ headers = {"content-type"=>"appliation/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)
+ ## これよりも
+ 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/
+ * 正直、この仕様はどうかと思う。