Pythonでhttp.serverを使っているうちに、せっかくならもう少し下のレイヤについても知りたくなりました。
何か良い資料がないかを探したところ、本当の基礎からのWebアプリケーション入門――Webサーバを作ってみようというページを見つけ、さらに書籍「Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門」発売されます - プログラミング言語を作る日記にて、書籍として発売されたのを知りました。
自分はEPUB/PDFセットをGihyo Digital Publishingで買って読みました。
感想としては、
- 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で書いた時に悩んだことをメモしておきます。
目次
- 環境
- 1.3.2 TCPサーバ/クライアントのプログラム
- 1.5.3 1つのHTMLファイルを返す
- 1.5.4 普通にWebページを表示できるようにする
- 2.6 Modoki/0.2のソースコード
- ソースコード
環境
- 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() | 18.1. socket — 低水準ネットワークインターフェース — Python 3.5.1 ドキュメント
- socket.recv() | 18.1. socket — 低水準ネットワークインターフェース — Python 3.5.1 ドキュメント
そのため、
# 送信 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()
を使ってデコードしました。
- urllib.parse.unquote() | 21.8. urllib.parse — URL を解析して構成要素にする — Python 3.5.1 ドキュメント
- encoding - Url decode UTF-8 in Python - Stack Overflow
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:理解のためのコメントをたくさん書いてありますが、あまり気にしないでください...