Chapter 12

複数回のHTTPリクエストに繰り返し応答できるようにする

Webサーバーがリクエストを1回だけしか処理できない問題

そろそろこの問題に対処しましょう。

皆さんにこれまで作ってもらったWebサーバーは、一回のHTTPリクエストを処理するとすぐに終了してしまいます。

そのため、繰り返しリクエストに応答しようと思うと毎回サーバーを起動しなおさなければいけません。

開発中に動作確認のたびにサーバーを起動するのがめんどくさいというのもありますが、一般的なWebページを正常に表示する上でも問題があります。

HTMLから外部ファイルの参照ができない

例えば、前章で作ってもらったindex.htmlを下記のように変更してみてください。

study/static/index.html

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>HenaServer</title>
  <link rel="stylesheet" href="index.css">
</head>
<body>
  <img alt="logo" src="logo.png">
  <h1>Welcome to HenaServer!</h1>
</body>
</html>

https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter12/static/index.html

6行目: <link rel="stylesheet" href="index.css">
10行目: <img alt="logo" src="logo.png">

を追加しました。

よくある外部CSSファイルの読み込みと、画像ファイルの読み込みです。

次に、読み込もうとしているファイルを、同じディレクトリ内に新しく用意します。

CSSファイルの内容は下記のようにしています。

study/static/index.css

h1 {
    color: red;
}

https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter12/static/index.css

画像ファイルはこちらです。

study/static/logo.png

https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter12/static/logo.png

画像ファイルは何でも良いのですが、本書では「いらすとや」[1] から拝借しています。お好きな画像を使っていただいて構いません。


見ていただければ分かるように、通常のWebページであればChromeにはロゴ画像が表示され、文字にはCSSが適用されて赤色に表示されるはずです。

では、サーバーを起動してChromeでhttp://localhost:8080/index.htmlへアクセスしてみましょう。

これはよくないですね。
画像もCSSも読み込まれていません。


ブラウザはWebサーバーからレスポンスを受け取った際、レスポンスボディのHTML内に外部ファイル参照(<img src=""><script src=""><link href="">など)が記載されていると、再度リクエストを送信しなおしてファイル内容を取得しようとします。

しかし、私たちのWebサーバーは最初のリクエストを処理したあと、すぐにプログラムを終了させてしまうため、追加のリクエスト(今回でいうとCSSと画像のリクエスト)を処理できていないのです。

その様子を、もう少し具体的に見てみましょう。

ChromeにはHTTPリクエストの通信結果を詳細に見れる「開発者ツール」という機能が備わっています。
そちらを使って、実際に行われたリクエストの様子を確認していきます。

さきほどChromeでhttp://localhost:8080/index.html にアクセスした画面で、ctrl + shift + jを押してみましょう。
(または、画面を右クリックして検証を選択し、Consoleタブを開きます)

図のように、既に index.csslogo.png を取得する際に、 Webサーバーとのコネクションに失敗したことを示すエラーログが表示されています。

(Chromeは他にも、特に指示がなくても勝手にファビコンの画像を取得しにくような仕様になっており、そちらのエラーも表示されていますが、本書では特に気にする必要はありません。)

次に、開発者ツールのNetworkタブを開き、サーバーを起動してからリロードしてみましょう。
(ネットワークタブは、開発者ツールを開いて以降の通信のみ情報を表示するため、リロードする必要があります)

Chromeはこのページを表示するために、全部で4件の通信を行っていることが分かります。
(バージョンや環境によって内容は異なるかもしれません、)

内訳を見てみると、index.htmlを取得する通信は成功(status200)しており、index.csslogo.pngは通信に失敗(statusfailed)していることが分かります。

繰り返しリクエストを処理できるようにする

このままでは「ただ面倒くさい」だけではなく、CSSや画像、JSなどを使った普通のWebページ1つすら表示できないということが分かりました。

では、Webサーバーを改良して、これらの問題を解決していきましょう。

ソースコード

まずは、コネクションを確立してレスポンスを返す処理を無限ループに中に入れることで、繰り返しリクエストに対応できるようにします。

ソースコードがこちらです。

study/webserver.py

https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter12/webserver.py

解説

30行目

            while True:

まず一番大きな変更点として、「クライアントからのコネクションを待つ」〜「コネクションを終了する」までの処理(31行目-97行目)をまるごと無限ループの中にいれたところです。
(無限ループの記法が分からない方は、「python while true」で調べてみてください。)

たった1行ですが、これにより、1つのリクエストの処理が完了し、コネクションを終了させた後、ループの先頭にもどり再度リクエストを待機することになります。
次のリクエストの処理が完了すると、またループの先頭に戻り、次のリクエストを待ちます。

つまり、プログラムを起動した人が明示的にプログラムを中断させるまで、無限にリクエストをさばき続けるプログラムになります。

89-97行目

                except Exception:
                    # リクエストの処理中に例外が発生した場合はコンソールにエラーログを出力し、
                    # 処理を続行する
                    print("=== リクエストの処理中にエラーが発生しました ===")
                    traceback.print_exc()

                finally:
                    # 例外が発生した場合も、発生しなかった場合も、TCP通信のcloseは行う
                    client_socket.close()

ついでに、例外処理を追加しておきました。

例外処理をしておかないとループの途中で例外が発生した場合にプログラム全体が停止してしまいますが、上記のようにハンドリングすることでその時扱っているリクエストの処理だけ中断させますが、プログラム全体は停止せずに次のループへ進むことになります。

また、client_socketclose()はtry句の末尾でやるのではなく、finally句で行います。
try句の末尾でやってしまうと、途中で例外が発生した場合にコネクションの切断がスキップされてしまうためです。

動かしてみる

では実際に動かしてみましょう。

いつもどおりコンソールからサーバーを起動します。

$ python webserver.py
=== サーバーを起動します ===
=== クライアントからの接続を待ちます ===

次に、Chromeからhttp://localhost:8080/index.html へアクセスしてみます。

やりましたね!画像が表示されました。

それに、このページをリロードしてみてください。
何回読み込み直しても毎回ページが表示されるはずです。

サーバーのログも確認してみましょう。
サーバーを起動したコンソールのタブを見てください。

繰り返しコネクションを確立させ、リクエストを処理している様子が分かるはずです。

$ python webserver.py
=== サーバーを起動します ===
=== クライアントからの接続を待ちます ===
=== クライアントとの接続が完了しました remote_address: ('127.0.0.1', 50404) ===
=== クライアントからの接続を待ちます ===
=== クライアントとの接続が完了しました remote_address: ('127.0.0.1', 50405) ===
=== クライアントからの接続を待ちます ===
=== クライアントとの接続が完了しました remote_address: ('127.0.0.1', 50407) ===
=== クライアントからの接続を待ちます ===
=== クライアントとの接続が完了しました remote_address: ('127.0.0.1', 50418) ===
=== クライアントからの接続を待ちます ===
=== クライアントとの接続が完了しました remote_address: ('127.0.0.1', 50419) ===
=== クライアントからの接続を待ちます ===
=== クライアントとの接続が完了しました remote_address: ('127.0.0.1', 50421) ===
=== クライアントからの接続を待ちます ===

・・・しかし、Chromeの画面をよく見ると CSSはまだ適用されていなさそう ですね?

様子がおかしいので、開発者ツールのNetworkタブで見てみましょう。

今度は先程と違い、cssも画像もstatus200になっていますので、通信には成功していそうです。

詳しく調べるために、cssファイルの行をクリックして、レスポンスの詳細を確認します。
詳細画面のResponseタブをクリックすると、レスポンスボディが確認できます。

どうやら、ちゃんと意図どおりのCSSがレスポンスボディとして取得できているようです。

何が起きているのでしょうか?


このケースのように、データとしては正しいものが渡せているのに思った挙動をしてくれないという場合によくあるのが、読み込みフォーマット問題です。
例えばjpegファイルをpngファイルだと思って読み込んでも画像は表示できませんし、エクセルファイルをPDFだと思って読み込んでも当然正常に読み込めません。

今回でいうと、実はレスポンスのヘッダーを作るとき に手抜きをして固定でContent-Type: text/htmlを返すようにしてしまったことが原因となっています。
このヘッダーのせいで、Chromeは h1 { color: red; } という文字列をCSSだと理解できずにいるのです[2]

検証ツールを使って実際にこの様子を確認しておきましょう。
先程と同様に、cssファイルのレスポンスの詳細画面の、Headerタブを開いてみましょう。

実際にレスポンスボディで送っているのはCSS形式の文字列なのに、サーバーは「htmlだと思って読んでね」と指示してしまっている状態です。

適切なContent-Typeを返せるようにする

Webサーバーを更に改良して、ファイル形式に沿ったContent-Typeを返せるようにしていきましょう。

ソースコード

改良したものがこちらです。

study/webserver.py

https://github.com/bigen1925/introduction-to-web-application-with-python/blob/main/codes/chapter12-2/webserver.py

解説

16-27行目

    # 拡張子とMIME Typeの対応
    MIME_TYPES = {
        "html": "text/html",
        "css": "text/css",
        "png": "image/png",
        "jpg": "image/jpg",
        "gif": "image/gif",
    }

拡張子と、それに対応するMIME Typeの対応を辞書で保持しています。

本当はもっとたくさんの種類のMIME Typeがありますが、まずは最低限のものだけで先に進めましょう。

ここにない拡張子のファイルを使ってみたい方は、こちら を参考に自分で追加してみてください。

88-96行目

                    # ヘッダー生成のためにContent-Typeを取得しておく
                    # pathから拡張子を取得
                    if "." in path:
                        ext = path.rsplit(".", maxsplit=1)[-1]
                    else:
                        ext = ""
                    # 拡張子からMIME Typeを取得
                    # 知らない対応していない拡張子の場合はoctet-streamとする
                    content_type = self.MIME_TYPES.get(ext, "application/octet-stream")

pathから拡張子(=ext)を取得し、拡張子と先ほど定義した辞書を使って対応するMIME Typeを取得しています。

拡張子が存在しない(pathが.で区切られていない)場合は、拡張子は空文字としています。

ちなみに、
path.rsplit(".", maxsplit=1)[-1]
は、pathを.で右から1回だけ分割し、得られたリストの-1番目(= 最後尾)を取得しています。
.が含まれない(=要素数が1になってしまう)パターンは事前に除外しては除外していますので、今回に限っては
path.rsplit(".", maxsplit=1)[1]
と同じ意味になります。

104行目

                    response_header += f"Content-Type: {mime_type}\r\n"

忘れずにContent-Typeヘッダーを変数を使って生成するよう変更しておきましょう。

動かしてみる

それでは動作確認をしてみましょう。

まずは、サーバーを再起動させます。

サーバーが実行中のコンソールのタブを開き、ctrl + Cを入力します。
するとサーバーが停止しますので、あらためてサーバーを起動させます。

次に、またChromeからhttp://localhost:8080/index.htmlへアクセスします。

コラム: 「サーバーの再起動」

本章から私達のWebサーバーは1回起動すると実行しっぱなしになっているわけですが、ソースコードはプログラムを実行したタイミングで一度だけ読み込まれ、その時点のソースコードをもとに動作します。
そのため、ソースコードの変更をプログラムの挙動に反映させるためにはサーバーを再起動する必要があることに気をつけてください。
ただし、サーバーのソースコード自体ではなく、リクエストを受けたときに毎回読み込み直すファイル(例えば、static配下のhtmlファイルやcssファイル)の変更を反映させたい場合は、サーバーを再起動する必要はありません。

サーバーのソースコードはプログラム実行時に一度だけ読み込まれ、staticのファイルはリクエストが来た時に毎回読み込まれるという違いを理解しておくことは重要です。

このあたりの挙動が分かってくるようになると、サーバーの設定を触り始めた中級者がハマりがちな
「ファイルを変更したんだけど、サーバーの再起動が必要なタイミングがよく分からない」
「何かファイルを変えたら不安だからとりあえず毎回サーバーを再起動している」
といった壁を超えることができるでしょう。

完璧です!

CSSも適用されて、画像も表示されました。

ここまでくれば、もう「阿部寛のホームページ」[3] ぐらいのものは世間に提供できるシロモノになってきましたね。

最後に、検証ツールを使って本当に私達の目論見通りのレスポンスになっているのか確認しておきましょう。

検証ツールのNetworkタブを開いて、ページをリロードします。
(サーバーを再起動しなくていいのは良いですね〜)

index.cssHeaderタブを確認してみると、たしかにContent-Type: text/cssになっていますね。

めでたしめでたし。

脚注
  1. https://www.irasutoya.com/ ↩︎
  2. この理屈でいくと画像ファイルも読み込めないはずなのですが、Chromeさんはとても賢いので、テキストファイルと画像ファイルぐらい全然違うものだと自動でファイル形式を認識してくれるようです。 ↩︎

  3. シンプルで軽量なホームページとして巷で有名。 http://abehiroshi.la.coocan.jp/ ↩︎