- リクエストを適切にルーティングする基礎が知りたい!
このような疑問にお答えします。
nginx のルーティングの流れを簡単に示すと、以下になります。
- リクエストを受け付ける
- 適切なサーバーを探す
- リクエストの処理方法を決める
- 静的ファイルを探して返す
- プロキシ経由で処理する
- 結果を返す
「うわ、結構な量があるな…!」と思った方も多いかもですが、これより粒度を下げてしまうと理解が難しくなってしまうので細かめに分類しました。
nginx を触るのが初めての方でもお分かりいただけるように、説明はできるだけ平易にしました。
それでは、一つ一つの処理を丁寧に説明していきます。
1. リクエストを受け付ける
ブラウザなどから送信された「HTTP リクエスト」を nginx で受け取ります。
この段階では、nginx の内部で次のことが行われます。
- リクエストの受け入れ
- リクエストデータの解析
少し掘り下げて解説しましょう。
リクエストの受け入れ
クライアント(Web ブラウザやアプリなど)からサーバーへの接続が成功し、データの交換が始まるプロセスです。
知識として、以下が実行されていることを覚えておくと良いです。
- 接続の確率
- TCPハンドシェイク
- HTTPリクエスト受信
- セキュリティチェック
- リクエストのバッファリング
リクエストデータの解析
クライアントから送られてきたリクエストの解析を行います。
- HTTPメソッドの識別
- URIの解析
- クエリパラメータの抽出
- ヘッダの解析
- ボディデータの読み取り
- エンコーディング・文字セットの処理
ここで、リクエストの意図を判断していくことになります。
2. 接続先のサーバーを決定
nginx には通常、複数のサーバーブロック(server
)が定義されます。
「定義されたサーバーブロックのうち、どのサーバーに接続すべきか?」を制御しているのが、このステップです。
nginx では、以下の流れでマッチするサーバーブロックを探しにいきます。
- ホスト名
- IPアドレス / ポート番号
- 上記に当てはまらない場合
詳細を確認していきましょう。
ホスト名
クライアントから送信されたリクエストには、「ドメイン名」が格納されたHost
ヘッダーというものがあります。
つまり、最初のステップとして「リクエストにあるドメイン名」と nginx 側の「server_name
ディレクティブに定義されたホスト名」とを照合して一致を確認します。
例えば、以下のようなリクエストが送られたとします。
GET /index.html HTTP/1.1
Host: www.example.com
リクエストのHost
ヘッダにはwww.example.com
となっていますから、これがドメイン名であることがわかります。
nginx 側の設定ファイルは以下としましょう。
http {
server {
listen 80;
server_name www.example.com;
root /var/www/example;
location / {
try_files $uri $uri/ =404;
}
}
server {
listen 80;
server_name www.another-example.com;
root /var/www/another-example;
location / {
try_files $uri $uri/ =404;
}
}
}
1つ目のserver_name
がwww.example.com
と一致しますので、このブロックが選択されます。
IPアドレス / ポート番号
ドメイン名が設定ファイルから見つけられなかった場合、このステップに進みます。
リクエストが送られたサーバーの「IPアドレスとポート番号の組み合わせ」と、nginx 設定ファイルの「listen
ディレクティブ」を比較して一致を確認しにいきます。
例えば、以下のようなリクエストが送られたとします。
GET / HTTP/1.1
このリクエストにはHostヘッダがありませんが、リクエスト元のサーバー情報が以下だったと仮定しましょう。
- IPアドレス:xxx.xxx.x.xxx
- ポート番号:8080
nginx 側の設定ファイルは以下としましょう。
http {
# グローバル設定...
server {
listen xxx.xxx.x.xxx:8080;
server_name _;
root /var/www/default;
location / {
try_files $uri $uri/ =404;
}
}
server {
listen xxx.xxx.x.xxx:9090;
server_name _;
root /var/www/special;
location / {
try_files $uri $uri/ =404;
}
}
}
一つ目のサーバーブロックのlisten
ディレクティブと一致するので、これが選択されます。
上記に当てはまらない場合
「デフォルトサーバーブロック」を用意しておくことで、どのserver_nameディレクティブにも一致しない場合のアクセス先を決めておくことができます。
default_server
オプションをつけておくことで対応可能です。
例えば、以下のようなリクエストが送られたとします。
GET /about.html HTTP/1.1
Host: unknown.com
nginx 側の設定ファイルは以下としましょう。
http {
# グローバル設定...
server {
listen 80 default_server;
server_name _;
root /var/www/default;
location / {
try_files $uri $uri/ =404;
}
}
server {
listen 80;
server_name example.com;
root /var/www/example;
location / {
try_files $uri $uri/ =404;
}
}
}
リクエストのHost
ヘッダにあるunknown.com
がどこにも見当たりません。
ところが5行目のlisten
ディレクティブ内にdefault_server
オプションが設定されているので、消去法的に一つ目のサーバーブロックが選択されます。
3. リクエストの処理方法を決める
ここからはlocation
ディレクティブのお話です。
これまでの処理でドメイン部分が特定されましたので、このlocation
ディレクティブではURLのうちドメイン部分を除いたパスの部分を扱うステップになります。
パスの文字列パターンと振り向け先を定義しておき、それにヒットするとリクエストが振り向け先に流れていくイメージとなります。
基本的な書き方
以下はlocation
ディレクティブの基本的な構文です。
location /パターン/ {
# 処理ルール
}
記入例を以下に示します。
http {
# グローバル設定...
server {
listen 80;
server_name example.com;
root /var/www/html;
location /images/ {
root /var/www/image;
}
location /api/ {
proxy_pass http://backend.example.com;
}
}
}
上記の例では、以下のルーティングがされます。
example.com/
:/var/www/html
にアクセスexample.com/images/
:/var/www/image
にアクセスexample.com/api/
: バックエンドサーバーにたらい回しする
修飾子の使い方
location
と/パターン/
の間に「修飾子」をつけることができます。(オプションなので、なくても可)
修飾子とは、URIのパターンマッチングを細かく制御するものです。
修飾子には、以下の種類があります。
修飾子 | 効果 |
---|---|
= | 完全一致 |
^~ | 優先プレフィックスマッチ |
~ | 大文字小文字を区別「する」正規表現マッチ |
~* | 大文字小文字を区別「しない」正規表現マッチ |
イメージが湧きやすいよう、以下に具体例を挙げます。
完全一致 (=
)
以下の場合、/exact/match.html
だけにヒットします。
location = /exact/match.html {
root /var/www/html;
}
優先プレフィックスマッチ (^~
)
以下では、/images/
で始まるすべてのリクエストに適用されます。
location ^~ /images/ {
root /var/www/html;
}
例えば以下のようなリクエストがヒットします。
http://example.com/images/photo.jpg
http://example.com/images/2023/march/spring.png
http://example.com/images/
location
が/images/
で始まるものが全てヒットするイメージです。
大文字小文字を区別する正規表現マッチ (~
)
以下の場合には、末尾が.php
で終わるすべてのリクエストに適用されます。
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
$
は文字列の最後を表し、.
は一文字以上の文字列を表します。
つまり、このパターンにヒットする URI は、例えば以下になります。
http://example.com/index.php
http://example.com/user/profile.php
http://example.com/api/v1/update.php
URIの最後にphp
とあるものは、全てこのlocation
ディレクティブで処理されることになります。
大文字小文字を区別しない正規表現マッチ (~*
)
以下では、拡張子が.jpg
, .jpeg
, .png
のファイルへのリクエストに適用されます。
location ~* \.(jpg|jpeg|png)$ {
root /var/www/images;
}
例えば、以下のような URI にマッチします。
http://example.com/photos/example.JPG
http://example.com/images/2023/march/spring.jpeg
http://example.com/assets/icons/icon.png
4. 静的ファイルを探して返す
ここからは、リクエストの対象が「静的ファイル」の場合のお話です。
静的ファイルがリクエストされると、root
ディレクティブで指定されたディレクトリから該当のファイルを探し、クライアントにレスポンスとして送信します。
処理の流れは以下の通りです。
- リクエストの受信と対象ディレクトリの特定
- ファイルの検索
- レスポンスとして返す
上記の処理でファイルが見つからなかった場合にはエラーが返されます。
それぞれの処理を詳しく見ていきましょう。
リクエストの受信と対象ディレクトリの特定
まずは、静的ファイルを探すディレクトリを特定します。
この手順は、すでに以下のセクションで解説しました。
流れを軽く説明すると、以下になります。
まず、クライアントからリクエストが飛んできます。
続いて「リクエストヘッダ等の内容」と「nginx の設定ファイルのサーバーブロック」を比較して、一致するものを接続先サーバーとして決定。
さらに具体的にどのパスを探すかをlocation
ディレクティブから探し出します。
ファイルの検索
location
ディレクティブ等に定義されているroot
ディレクティブを対象に、リクエスト URI に従ってファイルを検索します。
例えば、以下のようなリクエストを受け取ったとします。
http://example.com/images/picture.jpg
そして、以下がnginx の設定ファイルだとしましょう。
server {
listen 80;
server_name example.com;
root /var/www/html; # 全体のドキュメントルート
location / {
try_files $uri $uri/ =404;
}
location /images/ {
try_files $uri $uri/ =404; # ファイルが見つからない場合は404エラーを返す
}
}
リクエストは/image/
というパターンにマッチするので、10-12行目のlocation
ディレクティブで処理されます。
ファイルはサーバーの/var/www/html/images/
というディレクトリから静的ファイルを探しにいくことになります。
もしも探しにいきたいディレクトリがimages
ではなくspecial
の場合には、以下のようにroot
設定を追加します。
server {
listen 80;
server_name example.com;
root /var/www/html; # 全体のドキュメントルート
location / {
try_files $uri $uri/ =404;
}
location /images/ {
root /var/www/html/special; # 振り向け先のディレクトリを "special" にする
try_files $uri $uri/ =404;
}
}
レスポンスとして返す
取得された静的ファイルをレスポンスとして返します。
この静的ファイルの処理はアプリケーションサーバーを介さずに、Webサーバー(nginx)だけで完結します。
そのため、アプリケーションサーバーを介する場合に比べてパフォーマンス面でのメリットがあります。
5. プロキシ経由で処理する
これまではroot
ディレクティブというものを使いましたが、代わりにproxy_pass
というディレクティブを使うことができます。
ここではプロキシ経由で処理するための知識をお伝えします。
root
とproxy_pass
の違い
それぞれの違いをごく簡単にまとめると、以下となります。
ディレクティブ | 処理の違い |
---|---|
root | nginx が存在するサーバー内のリソースを活用 |
proxy_pass | 他のサーバーに転送 |
つまり、proxy_passを使うとnginx が仲介役となって別のバックエンドサーバーにリクエストを転送することになります。
そして、バックエンドサーバーからのレスポンスを受け取り、クライアントに返すという流れです。
プロキシ設定の方法
以下により、/api/
というパターンへのアクセスをバックエンドサーバーに転送します。
location /api/ {
proxy_pass http://backend-server.com;
}
root
の代わりにproxy_pass
ディレクティブに置き換える形になります。
6. 結果を返す
HTTP プロトコルに従い、クライアントにレスポンスを返却します。
レスポンスには以下の内容を含みます。
- ステータスコード
- ヘッダー
- ボディデータ
- 送信プロセス
それぞれ、解説します。
ステータスコード
ステータスコードはリクエストが成功したか、失敗したかを示すコードです。
以下の種類があります。
コード | 内容 |
---|---|
2xx | リクエストが成功して、レスポンスが返された |
3xx | 他の URL にリダイレクト |
4xx | リクエストに問題があった |
5xx | サーバー側に問題があった |
「404 Not Found」や「500 Internal Server Error」などはブラウザで表示されるので、目にした覚えのある方は多いのではないでしょうか。
ヘッダー
レスポンスに関する追加情報を提供するのがヘッダです。
例えば、以下のものを含みます。
- コンテンツタイプ
- キャッシュポリシー
- セキュリティ関連の設定
- クロスドメインポリシー
- クッキー設定
クライアントに対して、レスポンスの処理方法などを支持するものになります。
ボディデータ
レスポンスのうち、データ部分に相当します。
具体的には、以下のようなものがあります。
- HTML文書
- 画像データ
- JSON / XMLなどのデータ
- テキストデータ
リクエストに応じて様々なボディデータが返却されます。
送信プロセス
以上の内容を組み合わせて HTTP レスポンスを構築して、TCP/IP 経由でクライアントにレスポンスを送信します。
クライアントはレスポンスを受け取ると、以下の処理を行います。
- ステータスコードを元に取るべき措置を決定
- ヘッダーを元にレスポンスデータを処理
以上が nginx の内部で行われているルーティングの流れでした。
まとめ
文章だと長文になってしまいましたが、普段 Web アプリケーションを作っている方なら「あぁ、ここがこう繋がっているんだな!」と合点がいくポイントが結構あると思います。
本記事では基礎編だったのでベーシックな内容に留めています。
ぜひ理解を深め、nginx を自在に使えるようになりましょう!
コメント