アプリケーションサーバーを理解する
🎄Merry Christmas🎄 WWWAVE アドベントカレンダーの12/8の記事です!
ごきげんよう
国内向け漫画配信サイトComicFestaの開発をしているほさざえもんです
今回は、簡易的な puma もどきを自作して、rails s を使わずに Rails アプリを動かしてみました。
アプリケーションサーバーの理解度を上げるのが目的です!
きっかけ
日々の開発や障害調査の中で、
「クライアント⇄アプリの通信のどこで問題が起きてるの?」
っていう切り分けがめちゃくちゃ大事だと感じています。
ただ、自分自身がアプリケーションサーバーの仕組みを理解しきれていないと気づいたので、
今回「pumaって結局何やってるの?」を自作して確かめてみました🫠
アプリケーションサーバーとは?
アプリのロジックを実行するサーバーのことです。
Rails だと puma, unicorn が代表的ですね。
クライアント⇄アプリの通信のざっくりとした流れはこんな感じです

アプリケーションサーバーの役割
より具体にするとRailsアプリを実行するために以下のことをしています
- OS の上で TCPポートを開いて HTTP リクエストを受け取る
- 受け取ったリクエストから Rack の
envを組み立ててapp.call(env)を呼ぶ - Rails などの Rack アプリから返ってきた
[status, headers, body]をHTTPレスポンスとしてクライアントに返す - 複数の worker / thread を使って、同時にたくさんのリクエストをさばく
Rackってなに
RackはアプリケーションサーバーとRailsアプリをつなぐ共通インターフェースのことです
Rackの仕様で、以下のコードでRailsアプリを呼び出すことができます
status, headers, body = app.call(env)
実装してみよう。やってみよう〜♪ (WANIMA)
TCPサーバーを立ち上げてリクエストを受け取り、Rack互換のRailsアプリケーションに渡してレスポンスを返す仕組みを実装します。1リクエストごとにスレッドを生成して並列処理を行います。
Codex の力を借りながら実装!実装!実装!

実装したコード
require "socket"
require "rack"
require "stringio"
require_relative "./my_app/config/environment"
app = Rails.application
STATUS_TEXT = {
200 => "OK",
201 => "Created",
204 => "No Content",
302 => "Found",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error",
}.freeze
Thread.abort_on_exception = true
server = TCPServer.new("127.0.0.1", 9292)
puts "[mini-puma] Running Rails on http://127.0.0.1:9292"
loop do
socket = server.accept
# 1リクエスト = 1スレッド で処理(超シンプルな並列モデル)
Thread.new(socket) do |client|
begin
# --- ① リクエストラインを読む ---
request_line = client.gets
unless request_line && !request_line.strip.empty?
client.close
next
end
method, full_path, http_version = request_line.split(" ")
path, query = full_path.split("?", 2)
puts "[mini-puma] #{Thread.current.object_id} #{method} #{full_path} (#{http_version})"
# --- ② ヘッダ行を読む ---
headers = {}
while (line = client.gets)
line = line.chomp
break if line.empty?
key, value = line.split(":", 2)
next unless key && value
headers[key] = value.strip
end
# --- ③ ボディ(POSTデータなど)を読む ---
body = ""
content_length = headers["Content-Length"]&.to_i
if content_length && content_length > 0
body = client.read(content_length) || ""
end
# --- ④ Rack env を組み立てる ---
env = {
"REQUEST_METHOD" => method,
"PATH_INFO" => path,
"QUERY_STRING" => query || "",
"SERVER_NAME" => "127.0.0.1",
"SERVER_PORT" => "9292",
"rack.version" => Rack::VERSION,
"rack.url_scheme" => "http",
"rack.input" => StringIO.new(body),
"rack.errors" => $stderr,
"rack.multithread" => true,
"rack.multiprocess" => false,
"rack.run_once" => false,
}
headers.each do |k, v|
env_key = "HTTP_" + k.upcase.tr("-", "_")
env[env_key] = v
end
if headers["Content-Type"]
env["CONTENT_TYPE"] = headers["Content-Type"]
end
if headers["Content-Length"]
env["CONTENT_LENGTH"] = headers["Content-Length"]
end
# --- ⑤ Rackアプリ(= Railsアプリ)を呼ぶ ---
status, res_headers, body_enum = app.call(env)
# --- ⑥ ステータス行を組み立てる ---
status_code = status.to_i
reason = STATUS_TEXT[status_code] || "OK"
client.print "HTTP/1.1 #{status_code} #{reason}\r\n"
# --- ⑦ レスポンスヘッダを書き出す ---
res_headers.each do |k, v|
value = v.is_a?(Array) ? v.join(", ") : v
client.print "#{k}: #{value}\r\n"
end
client.print "\r\n"
# --- ⑧ ボディを書き出す ---
body_enum.each do |chunk|
client.print chunk
end
body_enum.close if body_enum.respond_to?(:close)
puts "[mini-puma] #{Thread.current.object_id} -> status=#{status_code}"
rescue => e
warn "[mini-puma:error] #{e.class}: #{e.message}"
warn e.backtrace.join("\n")
ensure
client.close rescue nil
end
end
end
上記コードを実行します。
hosazaemoooon@Mac puma-study % ruby mini_puma_step4.rb
・・・
[mini-puma] Running Rails on http://127.0.0.1:9292
[mini-puma] 17040 GET / (HTTP/1.1)
[mini-puma] 17040 -> status=200
[mini-puma] 24140 GET /icon.png (HTTP/1.1)
[mini-puma] 24140 -> status=200
こんな感じでrails sしなくてもサーバーが立ち上がりましたわ〜〜!

リクエストとレスポンスの流れも追ってみる
試しにrailsアプリのroutes.rbに以下を追加して/helloにアクセスしたときにレスポンスが帰ってくることも見てみましょう
get '/hello', to: ->(env) { [200, { "Content-Type" => "text/plain" }, ["Hello, World!"]] }
↓ログ
[mini-puma] 24160 GET /hello (HTTP/1.1)
[mini-puma] 24160 -> status=200

普段あまり意識したことはなかったのですが、pumaは頑張ってくれていることがわかりました
いつもありがとう。
おわりに
実際アプリケーションサーバーって導入した後に改修/開発する機会ってほぼないと思いますが、アプリケーションサーバーを理解することによって、相乗的にWebサーバーやCloudFrontの解像度が上がったような気がします
一筆書き終えたので、私は一足先にお正月モードに突入したいと思います
みなさん。良いお年を😘
株式会社ウェイブのエンジニアによるテックブログです。 弊社では、電子コミック、アニメ配信などのエンタメコンテンツを自社開発で運営しております! wwwave.jp/service/
Discussion