「Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門」が良かったのでPythonで書いてみた

Pythonでhttp.serverを使っているうちに、せっかくならもう少し下のレイヤについても知りたくなりました。

何か良い資料がないかを探したところ、本当の基礎からのWebアプリケーション入門――Webサーバを作ってみようというページを見つけ、さらに書籍「Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門」発売されます - プログラミング言語を作る日記にて、書籍として発売されたのを知りました。

 
自分はEPUB/PDFセットをGihyo Digital Publishingで買って読みました。

gihyo.jp

 
感想としては、

  • Webサーバを作るのに必要な用語が、1つずつ丁寧に詳しく解説されている
  • どうしてこう書くのかという理由が書かれており、出典としてRFCを示している
  • チュートリアル的にWebサーバを作っていくので、理解しやすい
    • 知りたかったソケットプログラミングから始まっていたのも良かった
  • Apacheの設定にも一部ふれられていた

など、まさに自分が知りたいポイントについて書かれていたため、とてもためになりました。ありがとうございました。

 
書籍のソースコードJavaだったのですが、別の言語で実装すればさらに理解が深まるだろうと考え、第1章・第2章のWebサーバ作成をPythonで書いてみました。なお、仕様の一部は簡略化しました。

Python版のソースコードGitHubに置いておきます*1
thinkAmi-sandbox/syakyo_create_web_server

 
第3章以降はTomcatのようなものを作っていたため、現時点では読むだけにしました。もし、第3章以降もPythonで書くとしたら、WSGIで実装する形になるのでしょうか。
java - What is the Python equivalent of Tomcat? - Stack Overflow

 
以下、Pythonで書いた時に悩んだことをメモしておきます。

 
目次

 

環境

  • Windows10 x64
  • Python 3.5.2 32bit

 

1.3.2 TCPサーバ/クライアントのプログラム

ソケットを使ったプログラムの流れとしては、書籍中の他、以下のページが参考になりました。
サーバプログラム

Pythonでソケットを使うには、標準ライブラリのsocketを使います。
18.1. socket — 低水準ネットワークインターフェース — Python 3.5.1 ドキュメント

Pythonでのソケット通信については、以下が参考になりました。
プログラミングと慶應通信 : Pythonによるsocket programming入門

なお、Python版では終了マークとして"0"を送信するは実装しませんでした。

 

ソケットを使った送受信

Pythonでソケットを使ってデータを送受信する際、そのデータはバイト列とする必要があります。

 
そのため、

# 送信
socket.send(data.encode("utf-8"))

# 受信
data = socket.recv(1024)
data.decode("utf-8")

のように、送信前にエンコード・受信後にデコードしました。

ただ、今回エンコード・デコード用文字コードとしてutf-8を使いましたが、本来どの文字コードを使うべきかは分かりませんでした。

 

ファイルの入出力

以下が参考になりました。
ファイル - Dive Into Python 3 日本語版

 

1.5.3 1つのHTMLファイルを返す

HTTPレスポンスヘッダのDateを出力する

Pythonではどのようにやるのがいいのかを探したところ、stackoverflowに回答がありました。
http - RFC 1123 Date Representation in Python? - Stack Overflow

いくつか方法がありましたが、email.utilsライブラリは今でも機能追加されていることから、email.utils.formatdate(usegmt=True)を使うことにしました。

 

1.5.4 普通にWebページを表示できるようにする

Pythonのマルチスレッドについて

今回は標準ライブラリのthreadingを使います。
17.1. threading — スレッドベースの並列処理 — Python 3.5.1 ドキュメント

PythonのThreadやProcessについては、以下が参考になりました。
python - Should I use fork or threads? - Stack Overflow

 

accept()中の停止について

Ctrl+Cでは停止できなかったため、Breakキーで強制停止します。
Python socket accept in the main thread prevents quitting - Stack Overflow

 

レスポンスボディの送信について

Java版とは異なり、テキストデータと画像データを同じ方法で返すやり方が分からなかったため、

if "text" in content_type:
    with open(DOCUMENT_ROOT + path, encoding="utf-8") as f:
        r = f.read()
        write_line(sock, r)
elif "image" in content_type:
    with open(DOCUMENT_ROOT + path, mode="rb") as f: 
        r = f.read()
        # 画像の場合、読み込んだバイナリに改行コードを加えてはいけないので、そのまま送る
        sock.send(r)

のように、Content-Typeを見て処理分岐する形にしました。

 

2.6 Modoki/0.2のソースコード

エンコードされた日本語URLのデコード

Python3のため、urllib.parse.unquote()を使ってデコードしました。

 

Windowsでのos.path.join()

Windowsの場合、os.path.join()を使うと

DOCUMENT_ROOT = "D:/Sandbox/syakyo_create_web_server/src/static"
path = "/index.html"
print(os.path.join(DOCUMENT_ROOT, path))
# => D:/index.html

のように、相対パスが返ってきます。
os.path.join() | 11.2. os.path — 共通のパス名操作 — Python 3.5.1 ドキュメント

 
今回は絶対パスが欲しかったため、stackoverflowを参考にして、

docroot_drive = DOCUMENT_ROOT[0:2]
docroot_path = DOCUMENT_ROOT[2:]
full_path = os.path.join(docroot_drive, os.sep, docroot_path + path)
print("fullpath: {}".format(full_path))
#=> D:/Sandbox/syakyo_create_web_server/src/static/index.html

としました。
Python os.path.join on Windows - Stack Overflow

 

301リダイレクト時のChromeの挙動について

書籍とは異なりChromeで動作確認をしていたのですが、301リダイレクト時にキャッシュされているような挙動がありました。

調べてみると、やはりキャッシュされているようでした。
chromeは、301リダイレクトをキャッシュしている - too_youngの日記  
 

htmlの文字コード設定について

書籍のままだとChromeで文字化けしたため、<meta charset="UTF-8">タグを追加しました。

 

ソースコード

再掲となりますが、以下となります。
thinkAmi-sandbox/syakyo_create_web_server

*1:理解のためのコメントをたくさん書いてありますが、あまり気にしないでください...