pythonでローカルhttpサーバーを立ててみる
httpの勉強をいろいろやってきたので、ここらでpythonを使用しhttpサーバーを実際に立てて通信確認なんかをしてみます。
今回はhttp.serverモジュールの使い方を調べました。
http.serverモジュールについて
http.serverモジュールは、HTTPプロトコルを用いたサーバーを簡単に構築できるようにするためのモジュールです。
このモジュールを使用すると、Pythonで簡単にWebサーバーを立ち上げることができます。
サーバクラス
http.serverモジュールにはサーバクラスが二つあり、それぞれHTTPリクエストを受信し適切なハンドラに処理を委譲します
クラス | 説明 |
---|---|
HTTPServer | 単一のクライアント接続をシングルスレッドで処理するHTTPサーバー |
ThreadingHTTPServer | 複数のクライアント接続をマルチスレッドで処理するHTTPサーバー |
HTTPServerだと「Ctrl」+「c」で停止しなくなったので、ThreadingHTTPServerの使用をお勧めします。
サーバークラスで実行するメソッドは以下です。
メソッド | 説明 |
---|---|
__init__(server_address, RequestHandlerClass) | サーバーアドレスとリクエストハンドラクラスを引数にとり、HTTPサーバーを初期化する |
serve_forever() | HTTPサーバーを起動してリクエストを受け付ける |
ハンドラクラス
http.serverモジュールにおけるハンドラクラスとは、クライアントからのリクエストに対してどのようにレスポンスを返すかを決定するためのクラスです。
以下のいずれかをサーバークラスの初期値として設定する必要があります。
クラス | 説明 |
---|---|
BaseHTTPRequestHandler | HTTPリクエストを受け取り、HTTPレスポンスを生成する すべてのHTTPメソッド(GET、PUT等)に対するハンドラを定義することができる |
SimpleHTTPRequestHandler | BaseHTTPRequestHandlerを継承したハンドラクラス HTTPリクエストに対してローカルファイルシステム上のファイルを返すことが出来る |
CGIHTTPRequestHandler | BaseHTTPRequestHandlerを継承したハンドラクラス HTTPリクエストに対してCGIスクリプトを実行し、その出力をレスポンスとして返す |
今回はBaseHTTPRequestHandlerに絞って実装確認をしていきたいと思います。
BaseHTTPRequestHandlerで使用する基本的なメソッドは以下。
メソッド | 説明 |
---|---|
address_string() | クライアントアドレスを返す |
date_time_string() | 現在の日付と時刻を返す |
end_headers() | HTTPレスポンスのヘッダーの終了(空白行の追加)を示す |
log_data_time_string() | ロギング用にフォーマットされた現在の時刻を返す |
log_error(…) | エラーを記録する |
log_message(format, …) | 任意のメッセージを記録する |
log_request([code[, size]]) | リクエストをログに記録する |
parse_request() | リクエストを解析する |
send_error(code[, message]) | エラー応答を送信してログに記録する |
send_response(code[, message]) | 応答ヘッダーを送信し、応答コードをログに記録する |
send_header(keyword, value) | ヘッダーを送信する |
version_string() | サーバー ソフトウェアのバージョン文字列を返す |
BaseHTTPRequestHandlerのインスタンス変数は以下。
インスタンス変数 | 説明 |
---|---|
client_address | クライアントのアドレス(ホスト名、ポート番号)を表すタプル |
command | HTTPコマンドを表す文字列("GET"、"POST"など) |
request_version | HTTPプロトコルバージョンを表す文字列("HTTP/1.0″、"HTTP/1.1″など) |
headers | HTTPリクエストヘッダーを格納する辞書オブジェクト |
path | リクエストされたリソースのパスを表す文字列 |
requestline | クライアントから送信されたHTTPリクエストラインを表すバイト列 |
rfile | リクエスト本文を読み取るためのファイルオブジェクト |
wfile | レスポンス本文を書き込むためのファイルオブジェクト |
サンプルコードと動作確認
実際にいくつかの実装を行い動作確認をしてみます。
HTTPリクエストの送信は、ブラウザで行うか、以前取り上げたpostmanなんかを使ってみてください
リクエスト取得応答
基本的なHTTPサーバのリクエスト取得と応答は以下。
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
ADDRESS = "127.0.0.2"
PORT = 8080
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
message = "Hello, world!"
self.wfile.write(bytes(message, "utf8"))
return
httpd = ThreadingHTTPServer((ADDRESS, PORT), MyHandler)
httpd.serve_forever()
任意のIPアドレス(127.0.0.2:8080)に対してGETメソッドを受け付けます。
コード実行後、ブラウザを立ち上げてhttp://127.0.0.2:8080/へアクセスすれば以下のような結果が得られます。
wiresharkのキャプチャ画面。
コマンドプロンプロ表示。
127.0.0.1 - - [07/May/2023 14:36:23] "GET / HTTP/1.1" 200 -
リクエスト内容を確認
HTTPサーバが受信したリクエスト内容をコマンドプロンプトへ出力してみます。
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
ADDRESS = "127.0.0.2"
PORT = 8080
class MyHandler(BaseHTTPRequestHandler):
def do_POST(self):
# リクエスト確認
print("------------client_address------------\n{}".format(self.client_address))
print("------------command------------\n{}".format(self.command))
print("------------request_version------------\n{}".format(self.request_version))
print("------------headers------------\n{}".format(self.headers))
print("------------path------------\n{}".format(self.path))
print("------------requestline------------\n{}".format(self.requestline))
content_length = int(self.headers['Content-Length'])
print("------------rfile------------\n{}".format(self.rfile.read(content_length)))
# 応答
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
message = "Hello, world!"
self.wfile.write(bytes(message, "utf8"))
self.requestline
return
httpd = ThreadingHTTPServer((ADDRESS, PORT), MyHandler)
httpd.serve_forever()
リクエスト送信後以下のような表示がコマンドプロンプト上で確認できます。
(試験用リクエストデータ送信時のHTTPメソッドはPOST、URLはhttp://127.0.0.2:8080/test、bodyのデータは"testbody“です。)
------------client_address------------
('127.0.0.1', 62855)
------------command------------
POST
------------request_version------------
HTTP/1.1
------------headers------------
Content-Type: text/plain
User-Agent: PostmanRuntime/7.32.2
Accept: */*
Postman-Token: 54d085ba-b76d-4003-9544-09a76c0cd575
Host: 127.0.0.2:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 8
------------path------------
/test
------------requestline------------
POST /test HTTP/1.1
------------rfile------------
b'testbody'
127.0.0.1 - - [07/May/2023 15:33:10] "POST /test HTTP/1.1" 200 -
リクエストURLのパスに応じてhtmlファイルを判断し応答
最後に、リクエストURLのパスに応じて適切なhtmlファイルを判断し応答するような処理を実装してみます。
動作としては以下をイメージ。
事前にmain.htmlとsub.htmlを用意して任意のフォルダに格納してください。
実際のコードは以下です。
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import os
ADDRESS = "127.0.0.2"
PORT = 8080
FILEDIR = os.getcwd() + "/html/"
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
if "/" == self.path or "/main" == self.path :
with open( FILEDIR + 'main.html', 'rb') as f:
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(f.read())
self.requestline
elif "/sub" == self.path :
with open( FILEDIR + 'sub.html', 'rb') as f:
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(f.read())
self.requestline
else:
self.send_error(404)
return
httpd = ThreadingHTTPServer((ADDRESS, PORT), MyHandler)
httpd.serve_forever()
存在しないパスをURLで指定した場合、以下のような応答が帰ってきます。
最後に
http.serverモジュールの基本的な部分は抑えられたかと思います。
次回はcookieなど、より応用的な実装確認を進めていく予定です。