├── 01-app.md ├── 02-middleware.md ├── 03-server.md ├── LICENSE └── README.md /01-app.md: -------------------------------------------------------------------------------- 1 | # 第1章: Rackアプリケーションをいろいろな書き方で実装してみよう 2 | 3 | この章では、Rackを使ってWebアプリケーションを実装してみます。Rackの基本的な仕組みを理解し、シンプルなアプリケーションから始めて、徐々にRackアプリケーションとはなんなのか、理解を深めてみましょう。 4 | 5 | --- 6 | 7 | ## 1. セットアップ 8 | 9 | RubyGemsを使ってRackをインストールします。 10 | ここではRackアプリケーションのためのコア機能を提供するrack gemと、Rackアプリケーションを起動するrackupコマンドを提供するrackup gemをインストールします。 11 | 12 | ```console 13 | $ gem install rack rackup webrick 14 | ``` 15 | 16 | gemをインストールしたらrackupコマンドが実行できることを確認します。 17 |   18 | ```console 19 | $ rackup --help 20 | Usage: rackup [ruby options] [rack options] [rackup config] 21 | ... 22 | ``` 23 | 24 | これでRackアプリケーションを書く準備は完了です。 25 | 26 | --- 27 | 28 | ## 2. シンプルなRackアプリケーションの作成 29 | 30 | まずは、最もシンプルなRackアプリケーションを作成してみましょう。 31 | 32 | ### 手順 33 | 34 | 1. 新しいファイル `app.ru` を作成し、以下のコードを記述します。 35 | ```ruby 36 | class App 37 | def call(env) 38 | [ 39 | 200, 40 | {}, 41 | ["hello"], 42 | ] 43 | end 44 | end 45 | 46 | run App.new 47 | ``` 48 | 2. `call` メソッドは、Rackアプリケーションの環境情報とHTTPリクエストの情報が格納されたHash `env` を受け取り、ステータスコード、ヘッダー、ボディの配列を返します。 49 | 50 | ### ポイント 51 | 52 | - `call` メソッドは必ず `[status, headers, body]` の形式でレスポンスを返す必要があります。 53 | - `body` は通常、文字列を含む配列を返します。 54 | - .ru という拡張子は rackup config ファイルの拡張子で、`run` などのRack独自のメソッドが定義されたRubyのDSLで記述されます。 55 | - 例として app.ru という名前を指定しましたが、拡張子以外の部分の名前は任意です。rackupコマンドはデフォルトだと config.ru という設定ファイルを探すので、config.ru という名前にすると rackup コマンドを実行するだけでアプリケーションが起動します。 56 | 57 | ### 実行方法 58 | 59 | ターミナルで以下のコマンドを実行します。 60 | 61 | ```console 62 | $ rackup app.ru 63 | ``` 64 | 65 | ブラウザやcurlコマンド等で `http://localhost:9292` にアクセスして、`hello` と表示されれば成功です。 66 | 67 | --- 68 | 69 | ## 3. envの中身を確認してみる 70 | 71 | Rackアプリケーションが受け取る `env` には、リクエストに関するさまざまな情報が含まれています。これを確認してみましょう。 72 | 73 | ### 手順 74 | 75 | 1. `call` メソッド内に `binding.irb` を挿入します。 76 | ```ruby 77 | class App 78 | def call(env) 79 | binding.irb 80 | # ... 81 | end 82 | end 83 | ``` 84 | 85 | ### 実行方法 86 | 87 | ターミナルでRackアプリケーションを実行して `http://localhost:9292` にアクセスすると、ターミナル上でirbセッションが開始されます。`env` を調べてみましょう。 88 | 89 | ```console 90 | $ rackup app.ru 91 | [2024-10-20 20:43:59] INFO WEBrick 1.8.1 92 | [2024-10-20 20:43:59] INFO ruby 3.3.4 (2024-07-09) [arm64-darwin23] 93 | [2024-10-20 20:43:59] INFO WEBrick::HTTPServer#start: pid=68048 port=9292 94 | 95 | From: /Users/hogelog/repos/hogelog/kaigionrails/01-app/app.ru @ line 3 : 96 | 97 | 1: class App 98 | 2: def call(env) 99 | => 3: binding.irb 100 | 4: [ 101 | 5: 200, 102 | 6: {"content-type" => "text/plain"}, 103 | 7: ["hello"], 104 | 8: ] 105 | irb(#):001> env 106 | => 107 | {"GATEWAY_INTERFACE"=>"CGI/1.1", 108 | "PATH_INFO"=>"/", 109 | "QUERY_STRING"=>"", 110 | "REMOTE_ADDR"=>"::1", 111 | "REMOTE_HOST"=>"::1", 112 | "REQUEST_METHOD"=>"GET", 113 | "REQUEST_URI"=>"http://localhost:9292/", 114 | ... 115 | ``` 116 | 117 | `env` にはリクエストに関する情報が含まれており、リクエストメソッドやパス、ヘッダー、クエリパラメータなどが確認できます。様々なパスやメソッドでアクセスして、`env` の中身が変わることを確認してみましょう。 118 | 119 | --- 120 | 121 | ## 4. レスポンスヘッダーを設定する 122 | 123 | 次はHTTPレスポンスヘッダーを設定する方法を確認してみましょう。 124 | 125 | ### 手順 126 | 127 | 1. `call` メソッドで返すヘッダーに `"content-type" => "text/plain"` を追加します。 128 | ```ruby 129 | class App 130 | def call(env) 131 | [ 132 | 200, 133 | {"content-type" => "text/plain"}, 134 | ["hello"], 135 | ] 136 | end 137 | end 138 | ``` 139 | 140 | ### ポイント 141 | 142 | - レスポンスヘッダーはハッシュで表現します。 143 | - HTTP/1.xではレスポンスヘッダーのキーの大文字小文字は無視されますが、Rackアプリケーションでは小文字で統一するよう定められています。 144 | 145 | ### 実行方法 146 | 147 | これまでと同様rackupコマンドで起動し、ブラウザの開発者ツールやcurlコマンドでレスポンスヘッダーを確認してみましょう。 148 | 149 | ```console 150 | $ curl -i http://localhost:9292/ 151 | HTTP/1.1 200 OK 152 | content-type: text/plain 153 | Content-Length: 5 154 | 155 | hello 156 | ``` 157 | 158 | またcontent-type以外にも任意のヘッダーを設定可能なので、他のヘッダーも設定してみてください。 159 | 160 | --- 161 | 162 | ## 5. ルーティングの実装 163 | 164 | 次に、リクエストのパスやメソッドに応じて、異なるレスポンスを返すようにしてみましょう。 165 | 166 | ### 手順 167 | 168 | 1. `call` メソッド内でリクエストの情報を取得します。 169 | ```ruby 170 | class App 171 | def call(env) 172 | method = env["REQUEST_METHOD"] 173 | path = env["PATH_INFO"] 174 | 175 | # メソッド、パスに応じた処理を実装 176 | # ... 177 | end 178 | end 179 | 180 | run App.new 181 | ``` 182 | 2. メソッド、パスに応じた処理を実装してください。 183 | - `GET /` リクエストが来たら `It works!` を返すよう実装。 184 | - `GET /hello/foobar` のようなリクエストには `Hello foobar!` を返すよう実装。 185 | 186 | ### ポイント 187 | 188 | - `env` からリクエストメソッド `REQUEST_METHOD` やリクエストパス `PATH_INFO` を取得できます。それらの値に応じて処理を分岐してみましょう。 189 | 190 | ### 実行方法 191 | 192 | これまでと同様rackupコマンドで起動し、ブラウザの開発者ツールやcurlコマンドでレスポンスを確認してみましょう。 193 | 194 | ```console 195 | $ curl http://localhost:9292/ 196 | It works! 197 | $ curl http://localhost:9292/hello/hogelog 198 | Hello hogelog! 199 | ``` 200 | 201 | 適切に実装できていれば、上記したようなレスポンスが返ってくるはずです。 202 | 203 | --- 204 | 205 | ## 6. Rackのライブラリを活用する 206 | 207 | 先ほどまではenvに含まれた値をそのまま扱って処理を実装していました。ここではRackが提供する便利なクラスやメソッドを使ってコードを簡潔に記述してみましょう。 208 | 209 | ### 手順 210 | 211 | 1. `Rack::Request` と `Rack::Response` を使用して、以下のようにリクエストとレスポンスを扱ってみます。 212 | ```ruby 213 | require "rack/request" 214 | require "rack/response" 215 | 216 | class App 217 | def call(env) 218 | request = Rack::Request.new(env) 219 | case [request.request_method, request.path_info] 220 | in ["GET", "/"] 221 | Rack::Response.new("It works!", 200).finish 222 | # ... 223 | end 224 | end 225 | end 226 | 227 | run App.new 228 | ``` 229 | 2. 上記実装に加え、パスに応じた処理を実装してください。 230 | - `GET /hello/foobar` のようなリクエストには `Hello foobar!` を返すよう実装。 231 | - `GET /`, `GET /hello/foobar` 以外のリクエストには404ステータスコードで `Not Found` を返すよう実装。 232 | 233 | ### ポイント 234 | 235 | - `Rack::Request` はリクエストに対する便利なメソッドを提供します。 236 | - `Rack::Request.new` は `call(env)` で渡される env を引数として受け取ります。 237 | - `Rack::Response` はレスポンスの組み立てをわかりやすくします。 238 | - `Rack::Response.new` は body, status, headers を引数として受け取ります。 239 | - `Rack::Response#finish` メソッドで `[status, headers, body]` 形式のレスポンスを返します 240 | 241 | ### 実行方法 242 | 243 | これまでと同様rackupコマンドで起動し、ブラウザの開発者ツールやcurlコマンドでレスポンスを確認してみましょう。 244 | 245 | ```console 246 | $ curl http://localhost:9292/ 247 | It works! 248 | $ curl http://localhost:9292/hello/hogelog 249 | Hello hogelog! 250 | $ curl -i http://localhost:9292/foobar 251 | HTTP/1.1 404 Not Found 252 | content-type: text/plain 253 | Content-Length: 9 254 | 255 | Not Found 256 | ``` 257 | 258 | 正しく実装できていれば、以上のようにリクエストに応じたレスポンスが返ってくるはずです。 259 | 260 | --- 261 | 262 | ## 7. Sinatraを使ったRackアプリケーション 263 | 264 | 次に、Rubyの軽量なWebフレームワークであるSinatraを使って、同様の機能を実装してみましょう。 265 | 266 | ### 手順 267 | 268 | 1. `sinatra.ru` というファイルを作成し、Sinatraのベースクラスを継承した `App` クラスを以下のように定義します。 269 | ```ruby 270 | require "sinatra/base" 271 | 272 | class App < Sinatra::Base 273 | get "/" do 274 | "It works!" 275 | end 276 | 277 | get "/hello/:name" do 278 | "Hello #{params[:name]}" 279 | end 280 | end 281 | 282 | run App.new 283 | ``` 284 | 285 | ### ポイント 286 | 287 | - Sinatraは `Sinatra::Base` を継承したクラス内でDSLを使ってルートを定義します。 288 | - "GET /foo" のリクエストに対応する処理は `get '/foo' do ... end` のように記述します。 289 | - リクエストのクエリやURLパラメタなどは `params` ハッシュから取得できます。 290 | 291 | ### 実行方法 292 | 293 | sinatra gemをインストールした上で、これまでのようにrackupコマンドでアプリケーションを起動しましょう。 294 | 295 | ```console 296 | $ gem install sinatra 297 | ... 298 | $ rackup sinatra.ru 299 | ... 300 | ``` 301 | 302 | 起動したアプリケーションに対しブラウザやcurlコマンドで動作を確認してみましょう。 303 | 304 | ```console 305 | $ curl http://localhost:9292/ 306 | It works! 307 | $ curl http://localhost:9292/hello/hogelog 308 | Hello hogelog! 309 | ``` 310 | 311 | Sinatraを使うことでRackアプリケーションを非常に簡潔に記述できることがわかります。 312 | 313 | --- 314 | 315 | ## 8. Railsを使ったRackアプリケーション 316 | 317 | 最後に、Railsを使って、同じ機能を実装してみましょう。Railsは起動するときなども通常rackupコマンドを使わず、意識することも少ないかもしれませんが、RailsアプリケーションがRackアプリケーションの形で提供されています。 318 | 319 | ここではフルセットのRailsアプリケーションではなく、1ファイルのシンプルな形でRailsアプリケーション定義をしてrackupコマンドで起動してみます。 320 | 321 | ### 手順 322 | 1. `rails.ru` というファイルを作成し、ここにRailsアプリケーションを定義していきます。 323 | 2. 最小限のRails機能をロードするために、`action_controller/railtie` をロードします。 324 | ```ruby 325 | require "action_controller/railtie" 326 | ``` 327 | 3. 最低限のRailsアプリケーション、コントローラ定義します。 328 | ```ruby 329 | require "action_controller/railtie" 330 | 331 | class App < Rails::Application 332 | config.secret_key_base = "secret_key_base" 333 | config.logger = Logger.new($stdout) 334 | Rails.logger = config.logger 335 | 336 | routes.draw do 337 | root "apps#index" 338 | resources :apps, only: :show, path: "hello" 339 | end 340 | end 341 | 342 | class AppsController < ActionController::Base 343 | def index 344 | render plain: "It works!" 345 | end 346 | 347 | def show 348 | render plain: "Hello #{params[:id]}!" 349 | end 350 | end 351 | 352 | run Rails.application 353 | ``` 354 | 4. 最後に、`run Rails.application` でアプリケーションを起動します。 355 | ```ruby 356 | run Rails.application 357 | ``` 358 | 359 |
360 | rails.ru 全体 361 | 362 | ```ruby 363 | require "action_controller/railtie" 364 | 365 | class App < Rails::Application 366 | config.secret_key_base = "secret_key_base" 367 | config.logger = Logger.new($stdout) 368 | Rails.logger = config.logger 369 | 370 | routes.draw do 371 | root "apps#index" 372 | resources :apps, only: :show, path: "hello" 373 | end 374 | end 375 | 376 | class AppsController < ActionController::Base 377 | def index 378 | render plain: "It works!" 379 | end 380 | 381 | def show 382 | render plain: "Hello #{params[:id]}!" 383 | end 384 | end 385 | 386 | run Rails.application 387 | ``` 388 | 389 |
390 | 391 | ### 実行方法 392 | 393 | rails gemをインストールし、rackupコマンドでアプリケーションを起動します。 394 | 395 | ```console 396 | $ gem install rails 397 | ... 398 | $ rackup rails.ru 399 | [2024-10-21 00:57:36] INFO WEBrick 1.8.1 400 | [2024-10-21 00:57:36] INFO ruby 3.3.4 (2024-07-09) [arm64-darwin23] 401 | [2024-10-21 00:57:36] INFO WEBrick::HTTPServer#start: pid=74850 port=9292 402 | ``` 403 | 404 | ブラウザやcurlなどでアクセスし、以下のように正しくレスポンスを返せていることを確認してみましょう。 405 | 406 | ```console 407 | $ curl http://localhost:9292/ 408 | It works! 409 | $ curl http://localhost:9292/hello/hogelog 410 | Hello hogelog! 411 | ``` 412 | 413 | このRackアプリケーションはRailsの機能のごく一部しか利用していませんが、RailsアプリケーションがRackアプリケーションを定義するものであることがわかります。 414 | 415 | --- 416 | 417 | ## 9. まとめ 418 | 419 | この章では、Rackアプリケーションの基本的な構造から始めて、環境変数の確認、レスポンスヘッダーの設定、ルーティングの実装、Rackライブラリの活用、SinatraやRailsを使った実装までを学びました。これらの基礎を押さえることで、さまざまなフレームワークやライブラリを使ったアプリケーションの構築に役立てることができます。 420 | 421 | これまでの内容を通じて、以下のポイントを理解できたと思います。 422 | 423 | - **Rackの基本構造**: `call` メソッドで `[status, headers, body]` を返す。 424 | - **Rackアプリケーション引数 `env` の利用**: リクエストに関する情報を取得できる。 425 | - **Rackのライブラリの活用**: `Rack::Request` や `Rack::Response` といった便利なクラスの提供。 426 | - **SinatraやRailsとの組み合わせ**: SinatraやRailsといったフレームワークとRackの関係性。  427 | 428 | --- 429 | 430 | 次の章では、Rackミドルウェアの作成や、ミドルウェアを使った機能拡張について学んでいきます。 431 | -------------------------------------------------------------------------------- /02-middleware.md: -------------------------------------------------------------------------------- 1 | # 第2章: Rackミドルウェアをいろいろな書き方で実装してみよう 2 | 3 | この章では、Rackミドルウェアの基本的な仕組みを理解し、さまざまな方法でミドルウェアを実装してみます。ミドルウェアを活用することで、アプリケーションの機能を拡張したり、共通の処理を分離することができます。 4 | 5 | --- 6 | 7 | ## 1. Rackミドルウェアとは 8 | Rackミドルウェアは、Rackアプリケーションのリクエストとレスポンスの間に介在し、リクエストを加工したり、レスポンスに処理を加えたりするコンポーネントです。ミドルウェアをチェーンのようにつなげることで、複雑な処理をシンプルに組み立てることができます。 9 | 10 | --- 11 | 12 | ## 2. シンプルなミドルウェアの作成 13 | まずは、簡単なミドルウェアを作成してみましょう。レスポンスヘッダーにカスタムヘッダーを追加するミドルウェアを実装します。 14 | 15 | ### 手順 16 | 1. 新しいファイル `middleware.ru` を作成します。 17 | 2. 適当なRackアプリケーションと、Rackミドルウェアを定義します。 18 | ```ruby 19 | class App 20 | def call(env) 21 | [200, {}, ["hello"]] 22 | end 23 | end 24 | 25 | class Middleware 26 | def initialize(app, name) 27 | @app = app 28 | @name = name 29 | end 30 | 31 | def call(env) 32 | status, headers, body = @app.call(env) 33 | headers["hello"] = @name 34 | [status, headers, body] 35 | end 36 | end 37 | 38 | use Middleware, "rails" 39 | run App.new 40 | ``` 41 | 42 | ### ポイント 43 | 44 | - ミドルウェアは、`initialize` と `call` メソッドを持つクラスで定義します。 45 | - ミドルウェアは `initialize` メソッドで、次のアプリケーションや任意の引数を受け取ります。 46 | - `call` メソッドで、リクエストを受け取って次のアプリケーションに渡し、レスポンスを返します。 47 | - ここで、リクエストやレスポンスに加工を加えることができます。 48 | - `use` キーワードを使ってミドルウェアを登録します。 49 | 50 | ### 実行方法 51 | 52 | ターミナルで以下のコマンドを実行します。 53 | 54 | ```console 55 | $ rackup middleware.ru 56 | ``` 57 | 58 | ブラウザやcurlで `http://localhost:9292` にアクセスしてヘッダを確認してみてください。 59 | 60 | ```console 61 | $ curl -i http://localhost:9292/ 62 | HTTP/1.1 200 OK 63 | hello: rails 64 | Content-Length: 5 65 | 66 | hello 67 | ``` 68 | 69 | `hello` ヘッダーが追加されていることがわかります。 70 | 71 | --- 72 | 73 | ## 3. Rackの標準ミドルウェアを活用する 74 | Rackが提供する標準のミドルウェアを使って機能を拡張してみましょう。ここではリクエストの処理時間を測定する `Rack::Runtime` と、Basic認証を行う `Rack::Auth::Basic` を使用します。 75 | 76 | ### 手順 77 | 78 | 1. 必要なライブラリを読み込みます。 79 | ```ruby 80 | require "rack/runtime" 81 | require "rack/auth/basic" 82 | ``` 83 | 2. ミドルウェアを追加します。 84 | ```ruby 85 | use Rack::Runtime 86 | use Rack::Auth::Basic do |username, password| 87 | username == "rubyist" && password == "onrack" 88 | end 89 | ``` 90 | 91 | ### ポイント 92 | - ミドルウェアは上から順に適用されます。 93 | 94 | ### 実行方法 95 | 96 | これまでと同様にrackupコマンドで起動し、ブラウザやcurlでアクセスしてみましょう。 97 | 98 | ```console 99 | $ curl -i http://localhost:9292/ 100 | HTTP/1.1 401 Unauthorized 101 | content-type: text/plain 102 | www-authenticate: Basic realm="" 103 | x-runtime: 0.000038 104 | hello: rails 105 | Content-Length: 0 106 | ``` 107 | 108 | `Rack::Auth::Basic` により、認証情報なしでは401 Unauthorizedが返されます。 109 | また `Rack::Runtime` により、レスポンスヘッダーに `x-runtime` ヘッダーが追加されていることがわかります。 110 | 111 | ```console 112 | $ curl -i http://rubyist:onrack@localhost:9292/ 113 | HTTP/1.1 200 OK 114 | x-runtime: 0.000055 115 | hello: rails 116 | Content-Length: 6 117 | 118 | hello 119 | ``` 120 | 121 | 認証情報を付与すると、`hello` が表示されます。 122 | 123 | --- 124 | 125 | ## 4. まとめ 126 | 127 | この章では、Rackミドルウェアの基本的な実装方法から、Rackの標準ミドルウェアの活用を学びました。 128 | 129 | ここではごくシンプルなRackミドルウェアを実装し、Rack標準ミドルウェアを利用してみました。 130 | 世の中にはRack標準以外にも様々なライブラリがRackミドルウェアの形で世の中に公開されており、それらを組み合わせることで、多様な機能をアプリケーションに組み込むことができます。 131 | 132 | これまでの内容を通じて、以下のポイントを理解できたと思います。 133 | 134 | - **ミドルウェアの基本構造**: `initialize` と `call` メソッドを持つクラス。 135 | - **Rackの標準ミドルウェア**: `Rack::Runtime` や `Rack::Auth::Basic` などの活用方法。 136 | 137 | --- 138 | 139 | 次の章では、Rackアプリケーションを起動するためのRackサーバを自分で実装してみましょう。 140 | -------------------------------------------------------------------------------- /03-server.md: -------------------------------------------------------------------------------- 1 | # 第3章: 自作のRackサーバを実装してみよう 2 | この章ではRackサーバを自前で実装してみます。既存のRackサーバ(Puma, Pitchfork, Unicorn, WEBRickなど)を使わずに自作のサーバを作ることで、Rackの内部的な仕組みやRackサーバの基本的な動作原理を理解することができます。 3 | 4 | --- 5 | 6 | ## 1. シンプルなRackサーバの作成 7 | 8 | まずは最もシンプルなRackサーバを実装してみましょう。以下の手順に従って、サーバを作成していきます。 9 | 10 | ここではまず、ごく一部のリクエストタイプ(GETメソッド)にしか対応していない単純なRackサーバを実装してみましょう。 11 | 12 | ### 手順 13 | 1. **必要なライブラリの読み込み** 14 | 新しいファイル `server.ru` を作成し、以下ライブラリを追加します。 15 | ```ruby 16 | require "socket" 17 | require "logger" 18 | ``` 19 | - `socket` はソケット通信を扱う標準ライブラリです。 20 | - 21 | - `logger` はログを記録するための標準ライブラリです。 22 | - 23 | - 必要に応じてここで指定した以外のライブラリを追加しても問題ありません。 24 | 2. **アプリケーションクラスの定義** 25 | ごく単純なRackアプリケーションを定義します。 26 | ```ruby 27 | class App 28 | def call(env) 29 | if env["PATH_INFO"] == "/" 30 | [200, {}, ["It works!"]] 31 | else 32 | [404, {}, ["Not Found"]] 33 | end 34 | end 35 | end 36 | ``` 37 | 3. **サーバークラスの定義** 38 | 自作のサーバークラス `SimpleServer` を定義します。 39 | 40 | ```ruby 41 | class SimpleServer 42 | def self.run(app, **options) 43 | new(app, options).start 44 | end 45 | 46 | def initialize(app, options) 47 | @app = app 48 | @options = options 49 | @logger = Logger.new($stdout) 50 | end 51 | 52 | def start 53 | # サーバーのメインループ 54 | end 55 | end 56 | ``` 57 | - `self.run` クラスメソッドでサーバーを起動します。 58 | - `initialize` メソッドでアプリケーションとオプションを受け取ります。 59 | - `start` メソッドでクライアントからの接続を受けてレスポンスを返す、サーバーのメインループを実装します。 60 | 4. **ソケットサーバーの起動** 61 | `start` メソッド内で、以下の処理を行います。 62 | 63 | - `TCPServer` を使って指定されたポートでサーバーを起動します。 64 | - 65 | - 無限ループでクライアントからの接続を待ち受けます。 66 | - クライアントからの接続があったら、リクエストを読み込みます。 67 | 68 | ```ruby 69 | def start 70 | @logger.info "SimpleServer starting..." 71 | server = TCPServer.new(@options[:Port].to_i) 72 | loop do 73 | client = server.accept 74 | 75 | # リクエストの受信と解析 76 | # ... 77 | end 78 | end 79 | ``` 80 | 5. **リクエストの解析** 81 | - クライアントから送られてきたリクエストライン(例: `"GET /hello HTTP/1.1"`)を読み込みます。 82 | ```ruby 83 | client = server.accept 84 | 85 | # リクエストラインの解析 86 | request_line = client.gets&.chomp 87 | # ... 88 | path = # ... 89 | ``` 90 | - クライアントの切断時に `nil` が返ることを考慮して、`&.` 演算子を使っています。 91 | - リクエストヘッダーを読み込みます。 92 | ```ruby 93 | # リクエストラインの解析 94 | request_line = client.gets&.chomp 95 | # ... 96 | path = # ... 97 | 98 | request_headers = {} 99 | loop do 100 | header_field = client.gets.chomp 101 | # ヘッダーの解析 102 | # ... 103 | end 104 | ``` 105 | - 必要であれば、リクエストボディを読み込みます。 106 | - 参考: GETメソッドのリクエストはクライアントから以下のような `"リクエストライン\r\n(任意行繰り返されるヘッダフィールド\r\n\nリクエストボディ" `形式で送られてきます。 107 | ``` 108 | GET /hello HTTP/1.1 109 | Host: localhost:9292 110 | User-Agent: curl/8.7.1 111 | 112 | ... (リクエストボディ) 113 | ``` 114 | 6. **Rackアプリケーション入力 `env` の構築** 115 | Rackアプリケーションに渡す `env` ハッシュを作成します。 116 | ```ruby 117 | env = { 118 | Rack::REQUEST_METHOD => "GET", 119 | Rack::SCRIPT_NAME => "", 120 | Rack::PATH_INFO => path, 121 | Rack::SERVER_NAME => @options[:Host], 122 | Rack::SERVER_PORT => @options[:Port].to_s, 123 | Rack::SERVER_PROTOCOL => "HTTP/1.1", 124 | Rack::RACK_INPUT => client, 125 | Rack::RACK_ERRORS => $stderr, 126 | Rack::QUERY_STRING => "", 127 | Rack::RACK_URL_SCHEME => "http", 128 | } 129 | ``` 130 | - ここでは動作に必要な最小限の値のみ設定しています。 131 | 7. **アプリケーションの呼び出しとレスポンスの送信** 132 | - Rackアプリケーションの `call` メソッドを呼び出し、レスポンスを取得します。 133 | ```ruby 134 | status, headers, body = @app.call(env) 135 | ``` 136 | - クライアントにHTTPレスポンスとして返します。 137 | ```ruby 138 | client.puts "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" 139 | headers.each do |key, value| 140 | client.puts "#{key}: #{value}" 141 | end 142 | client.puts 143 | body.each do |chunk| 144 | client.write chunk 145 | end 146 | ``` 147 | 8. **ログの記録とクリーンアップ** 148 | - クライアントとの接続を確立しているソケットを閉じます。 149 | ```ruby 150 | client.close 151 | ``` 152 | - リクエストとレスポンスの情報をログに記録します。 153 | ```ruby 154 | @logger.info "GET #{path} => #{status}" 155 | ``` 156 | 9. **Rackハンドラーへの登録** 157 | 自作のサーバーをRackハンドラーとして登録します。 158 | ```ruby 159 | Rackup::Handler.register "simple_server", SimpleServer 160 | ``` 161 | 10. **アプリケーションの実行** 162 | 最後に、`run` メソッドでアプリケーションを指定します。 163 | ```ruby 164 | run App.new 165 | ``` 166 | 167 |
168 | server.ru 全体コード 169 | 170 | ```ruby 171 | require "socket" 172 | require "logger" 173 | 174 | class App 175 | def call(env) 176 | if env["PATH_INFO"] == "/" 177 | [200, {}, ["It works!"]] 178 | else 179 | [404, {}, ["Not Found"]] 180 | end 181 | end 182 | end 183 | 184 | class SimpleServer 185 | def self.run(app, **options) 186 | new(app, options).start 187 | end 188 | 189 | def initialize(app, options) 190 | @app = app 191 | @options = options 192 | @logger = Logger.new($stdout) 193 | end 194 | 195 | def start 196 | @logger.info "SimpleServer starting..." 197 | server = TCPServer.new(@options[:Port].to_i) 198 | loop do 199 | client = server.accept 200 | 201 | request_line = client.gets&.chomp 202 | %r[^GET (?.+) HTTP/1.1$].match(request_line) 203 | path = Regexp.last_match(:path) 204 | 205 | unless path 206 | client.puts "HTTP/1.1 501 Not Implemented" 207 | client.close 208 | next 209 | end 210 | 211 | request_headers = {} 212 | loop do 213 | header_field = client.gets.chomp 214 | match = %r[^(?[^:]+):\s+(?.+)$].match(header_field) 215 | break unless match 216 | 217 | request_headers[match[:name]] = match[:value] 218 | end 219 | 220 | env = { 221 | Rack::REQUEST_METHOD => "GET", 222 | Rack::SCRIPT_NAME => "", 223 | Rack::PATH_INFO => path, 224 | Rack::SERVER_NAME => @options[:Host], 225 | Rack::SERVER_PORT => @options[:Port].to_s, 226 | Rack::SERVER_PROTOCOL => "HTTP/1.1", 227 | Rack::RACK_INPUT => client, 228 | Rack::RACK_ERRORS => $stderr, 229 | Rack::QUERY_STRING => "", 230 | Rack::RACK_URL_SCHEME => "http", 231 | } 232 | 233 | status, headers, body = @app.call(env) 234 | 235 | client.puts "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" 236 | headers.each do |name, value| 237 | client.puts "#{name}: #{value}" 238 | end 239 | client.puts 240 | body.each do |line| 241 | client.puts line 242 | end 243 | client.close 244 | 245 | @logger.info "GET #{path} => #{status}" 246 | end 247 | end 248 | end 249 | 250 | Rackup::Handler.register "simple_server", SimpleServer 251 | 252 | run App.new 253 | ``` 254 | 255 |
256 | 257 | ### ポイント 258 | - 大まかに言うとクライアントからHTTPリクエストを受け取り、Rackプロトコルで定まった値を詰め込んだ `env` ハッシュを作成してRackアプリケーションに渡し、Rackアプリケーションから帰ってきた配列を元にクライアントにHTTPレスポンスを返すのがRackサーバの仕事です。 259 | - .ru ファイルの中でRackアプリケーション、Rackサーバ実装どちらも含めた例となっています。 260 | - ここで示したステップにはエラーハンドリングが含まれていません。期待する入力以外では正常に動かないことがあります。 261 | - Rackアプリケーション `App`, Rackサーバ `SimpleServer` をつなぎこみ、 `SimpleServer.run` や `SimpleServer#start` を呼び出したりしてるのはrackupコマンドがしています。詳細を把握したい方は を参照してください。 262 | 263 | ### 実行方法 264 | 265 | できあがった server.ru をターミナルで以下のコマンドを実行します。 266 | 267 | ```console 268 | $ rackup --server simple_server server.ru 269 | I, [2024-10-22T00:30:13.983048 #16660] INFO -- : SimpleServer starting... 270 | ``` 271 | 272 | `-s simple_server` オプションで、自作のRackサーバー SimpleServer 使用を指定します。 273 | 274 | ブラウザやcurlコマンド等で `http://localhost:9292` にアクセスして、`It works!` と表示されれば成功です。 275 | 276 | ```console 277 | $ curl -i http://localhost:9292/ 278 | HTTP/1.1 200 OK 279 | content-length: 9 280 | 281 | It works! 282 | $ curl -i http://localhost:9292/hello 283 | HTTP/1.1 404 Not Found 284 | content-length: 9 285 | 286 | Not Found 287 | ``` 288 | 289 | --- 290 | 291 | ## 2. fork を使った並列処理サーバの実装 292 | SimpleServerのメインループはクライアントの接続を待ち、Rackアプリケーションにリクエストを処理させ、クライアントにレスポンスを返すというシンプルな実装です。 293 | この実装では1つのリクエストを処理している間、他のリクエストを処理できません。ここでは`fork` を使って並列にリクエストを処理できるサーバを実装してみましょう。 294 | 295 | ### 手順 296 | 1. **ForkServerクラスの定義** 297 | 先ほどと同様に .ru ファイルの中で `ForkServer` クラスを定義します。ただしForkServerはメインループの中で `fork` を使って並列処理を行います。 298 | ```ruby 299 | class ForkServer 300 | def self.run(app, **options) 301 | new(app, options).start 302 | end 303 | 304 | def initialize(app, options) 305 | @app = app 306 | @options = options 307 | @logger = Logger.new($stdout) 308 | end 309 | 310 | def start 311 | @logger.info "ForkServer starting..." 312 | server = TCPServer.new(@options[:Port].to_i) 313 | loop do 314 | client = server.accept 315 | # ... 316 | end 317 | end 318 | end 319 | ``` 320 | 4. **並列処理の実装** 321 | - クライアントからの接続を受け付けたら、`fork` を使って子プロセスを生成します。 322 | ```ruby 323 | loop do 324 | client = server.accept 325 | fork do 326 | # 子プロセス内でリクエストを処理 327 | end 328 | client.close 329 | end 330 | ``` 331 | - 子プロセス内でリクエストの受信、`env` の構築、アプリケーションの呼び出し、レスポンスの送信を行います。 332 | - 子プロセス内では不要な接続をクローズするため、fork後に `server.close` と、レスポンス送信後に `client.close` を行います。 333 | 5. **ForkServerの登録** 334 | `ForkServer` を `fork_server` ハンドラーとして登録します。 335 | ```ruby 336 | Rackup::Handler.register "fork_server", ForkServer 337 | ``` 338 | 339 |
340 | 341 | server.ru 全体 (fork版) 342 | 343 | ```ruby 344 | require "socket" 345 | require "logger" 346 | 347 | class App 348 | def call(env) 349 | if env["PATH_INFO"] == "/" 350 | [200, {}, ["It works!"]] 351 | else 352 | [404, {}, ["Not Found"]] 353 | end 354 | end 355 | end 356 | 357 | class ForkServer 358 | def self.run(app, **options) 359 | new(app, options).start 360 | end 361 | 362 | def initialize(app, options) 363 | @app = app 364 | @options = options 365 | @logger = Logger.new($stdout) 366 | end 367 | 368 | def start 369 | @logger.info "ForkServer starting..." 370 | server = TCPServer.new(@options[:Port].to_i) 371 | loop do 372 | client = server.accept 373 | fork do 374 | server.close 375 | 376 | request_line = client.gets&.chomp 377 | %r[^GET (?.+) HTTP/1.1$].match(request_line) 378 | path = Regexp.last_match(:path) 379 | 380 | unless path 381 | client.puts "HTTP/1.1 501 Not Implemented" 382 | client.close 383 | next 384 | end 385 | 386 | request_headers = {} 387 | while %r[^(?[^:]+):\s+(?.+)$].match(client.gets.chomp) 388 | request_headers[Regexp.last_match(:name)] = Regexp.last_match(:value) 389 | end 390 | 391 | env = ENV.to_hash.merge( 392 | Rack::REQUEST_METHOD => "GET", 393 | Rack::SCRIPT_NAME => "", 394 | Rack::PATH_INFO => path, 395 | Rack::SERVER_NAME => @options[:Host], 396 | Rack::RACK_INPUT => client, 397 | Rack::RACK_ERRORS => $stderr, 398 | Rack::QUERY_STRING => "", 399 | Rack::REQUEST_PATH => path, 400 | Rack::RACK_URL_SCHEME => "http", 401 | Rack::SERVER_PROTOCOL => "HTTP/1.1", 402 | ) 403 | status, headers, body = @app.call(env) 404 | 405 | client.puts "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" 406 | 407 | headers.each do |name, value| 408 | client.puts "#{name}: #{value}" 409 | end 410 | client.puts 411 | body.each do |line| 412 | client.puts line 413 | end 414 | 415 | @logger.info "GET #{path} => #{status}" 416 | ensure 417 | client.close 418 | end 419 | client.close 420 | end 421 | end 422 | end 423 | 424 | Rackup::Handler.register "fork_server", ForkServer 425 | 426 | run App.new 427 | ``` 428 | 429 |
430 | 431 | ### ポイント 432 | 433 | - `fork` を使うことで、リクエストごとに新しいプロセスを生成し、並列に処理できます。 434 | - プロセスの生成はオーバーヘッドが大きいため、大量のリクエストには不向きです。 435 | - 上限を設けずにリクエストの度に子プロセスを生成するような実装は、大量リクエストによりサーバがダウンするリスクがあり、現実的な実装ではありません。 436 | 437 | ### 実行方法 438 | 439 | できあがった server.ru をターミナルで以下のコマンドを実行します。 440 | 441 | ```console 442 | $ rackup --server fork_server server.ru 443 | I, [2024-10-22T00:31:19.618183 #16730] INFO -- : ForkServer starting... 444 | ``` 445 | 446 | `-s fork_server` オプションで、自作のRackサーバー SimpleServer 使用を指定します。 447 | 448 | ブラウザやcurlコマンド等で `http://localhost:9292` にアクセスして、`It works!` と表示されれば成功です。 449 | 450 | ```console 451 | $ curl -i http://localhost:9292/ 452 | HTTP/1.1 200 OK 453 | content-length: 9 454 | 455 | It works! 456 | $ curl -i http://localhost:9292/hello 457 | HTTP/1.1 404 Not Found 458 | content-length: 9 459 | 460 | Not Found 461 | ``` 462 | 463 | Rackアプリケーションの中にsleepを仕込むなどして、複数リクエストを同時に送ってみてください。SimpleServerでは同時にリクエストを処理できないのに対し、ForkServerでは同時に複数のリクエストを処理できることが確認できるでしょう。 464 | 465 | --- 466 | 467 | ## 3. 発展課題 468 | 469 | ここまでで実装したサーバは、Rackの動作を理解するための最低限の機能のみ持つものです。 470 | 時間に余裕のある人は、以下の課題に挑戦してみてはいかがでしょうか。 471 | 472 | - GET以外の主要なHTTPメソッドに適切に対応する。 473 | - POST, PUT, DELETE, PATCH, HEAD など。  474 | - Rackの仕様に正しく準拠する。 475 | - 仕様は に説明があります。 476 | - Rack::Lint ミドルウェアを使うことで、Rackの仕様に準拠しているかを確認できます。 477 | - 適切なエラーハンドリングの実装。 478 | - クライアントからの不正なリクエストや、対応していないHTTPメソッドに対する適切なエラーレスポンスを返すなど、エラーハンドリングは不足しています。 479 | - pre-forkingやスレッドプールを使った並列処理を実装する。 480 | - 上述した ForkServer はリクエストごとに新しいプロセスを生成するため、現実的に使えるアプリケーションではありません。事前にプロセスやスレッドを生成しておき、リクエストを受け付けるときにそのプロセスを使い回すような実装を考えてみましょう。 481 | - Ractorを使ったRackサーバを実装する。 482 | - Ruby 3.0から導入された新しい並列処理の仕組み、Ractor を使用してみましょう。 483 | - Rack と Ractor、名前も似ていますしきっと相性が良いはずです。 484 | 485 | --- 486 | 487 | ## 4. まとめ 488 | この章では、自作のRackサーバを実装することで、Rackサーバの基本的な動作やRackの内部構造について理解を深めました。 489 | 490 | 今後Rackサーバを実装する機会がなかったとしても、今回Rackサーバを自分で実装し理解を深めることはPuma, Unicorn, Pitchforkといった既存のRackサーバの挙動を読み解く際にもきっと役に立つでしょう。 491 | 492 | --- 493 | 494 | これで本ハンズオンワークショップのテキストは終了です、お疲れさまでした。 495 | 是非ここで学んだ内容を活かして、より高度な開発に挑戦してみてください。 496 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 hogelog 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rackを理解するための技術ハンズオンワークショップ 2 | 3 | このワークショップでは、Railsを使ったWebアプリケーション開発において重要な役割を果たすRackについて学びます。シンプルなアプリケーションから始めて、ミドルウェアやサーバの実装まで、段階的に理解を深めていきましょう。 4 | 5 | --- 6 | 7 | ## コンテンツ 8 | - [README.md](README.md): このファイルです。Rackの概要とこのワークショップの概要について説明します。 9 | - [01-app.md](01-app.md): Rackアプリケーションを作成し、基本的な仕組みを理解します。 10 | - [02-middleware.md](02-middleware.md): Rackミドルウェアを作成しRackアプリケーションのカスタマイズについて学んでいきます。 11 | - [03-server.md](03-server.md): Rackサーバを自作します。 12 | 13 | Day 1で02-middleware.mdのRackミドルウェア作成まで完了し、Day 2にRackサーバの自作に取り組む予定です。 14 | 15 | ## 準備: Rackとは何か 16 | 17 | ### Rackの概要 18 | 19 | Rackは、RubyでWebサーバーとWebアプリケーションをつなぐインターフェースです。 20 | Rackを利用すると、HTTPリクエストを処理するWebアプリケーションは以下のように非常にシンプルなRubyコードで記述できます。 21 | 22 | ```ruby 23 | class HelloRack 24 | def call(env) # CGI-style environment 25 | [ 26 | 200, # status 27 | {"content-type" => "text/html"}, # headers 28 | ["Hello, Rack!"], # body 29 | ] 30 | end 31 | end 32 | ``` 33 | 34 | このRackインターフェースを共通で利用することで、異なるRackサーバー(例: Puma, Pitchfork, Unicorn)と異なるWebアプリケーションフレームワーク(例: Ruby on Rails, Sinatra)を組み合わせることができます。 35 | 36 | ### Rackの役割 37 | 38 | - **統一的なインターフェースの提供**: Rackを使用することで異なるWebサーバーとWebフレームワーク間で共通のやり取りが可能になります。これにより、アプリケーションは特定のサーバーや環境に依存せずに動作します。 39 | - **ミドルウェアの活用**: Rackは、リクエストとレスポンスの間にミドルウェアを挟むことができます。ミドルウェアは、認証、ロギング、セッション管理などの共通機能を実装するのに役立ちます。 40 | - **ライブラリの提供**: RackはWebアプリケーションを開発するための便利なライブラリが多数提供しています。これにより、開発者は簡単に機能を追加したり、カスタマイズしたりすることができます。 41 | 42 | ### RackとRuby on Railsの関係 43 | 44 | Railsは、フルスタックなWebアプリケーションフレームワークであり、内部的にRackを利用しています。具体的には、以下のような関係性があります。 45 | 46 | - **Rack互換性**: RailsアプリケーションはRackインターフェースに準拠しており、Rack準拠のWebサーバやRackミドルウェアを利用できます。 47 | - **ミドルウェアの活用**: Railsの多くの機能は多数のRackミドルウェアの形で提供されており、セッション管理、リクエストのパラメータパース、クッキーの処理などを行っています。 48 | 49 | ### なぜRackを学ぶのか 50 | 51 | - **基礎理解の向上**: Rackの仕組みを理解することで、RubyのWebアプリケーションがどのように動作しているかを深く理解できます。 52 | - **パフォーマンス最適化**: RailsアプリケーションパフォーマンスはRackミドルウェアやRackサーバ構成により大きく変わってくるため、アプリケーションのパフォーマンス向上させるにはRack理解が重要です。 53 | - **問題解決力の向上**: Rackレベルでのデバッグやカスタマイズが可能になるため、複雑な問題にも対処しやすくなります。 54 | 55 | ### 注意事項 56 | - このワークショップでは説明の簡単化のため記述を一ファイルにまとめて提示していますが、実際のアプリケーション開発では適切なファイル分割や設計をしましょう。 57 | - SinatraやRailsといったウェブアプリケーションフレームワークそのものについての説明は省略しています。 58 | - ワークショップのコンテンツは最新リリース版Ruby 3.3.5 で進めることを推奨しています。 59 | - ワークショップ内ではRackの正確な仕様を実装しきるところまではおこないません。より詳細な仕様については公式の仕様 を参照してください。 60 | 61 | ### 参考資料 62 | - rack/rack <> 63 | - Introducing Rack <> 64 | --------------------------------------------------------------------------------