pythonでローカルhttpサーバーを立ててみる

http,python,通信

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モジュールにおけるハンドラクラスとは、クライアントからのリクエストに対してどのようにレスポンスを返すかを決定するためのクラスです。

以下のいずれかをサーバークラスの初期値として設定する必要があります。

クラス説明
BaseHTTPRequestHandlerHTTPリクエストを受け取り、HTTPレスポンスを生成する
すべてのHTTPメソッド(GET、PUT等)に対するハンドラを定義することができる
SimpleHTTPRequestHandlerBaseHTTPRequestHandlerを継承したハンドラクラス
HTTPリクエストに対してローカルファイルシステム上のファイルを返すことが出来る
CGIHTTPRequestHandlerBaseHTTPRequestHandlerを継承したハンドラクラス
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クライアントのアドレス(ホスト名、ポート番号)を表すタプル
commandHTTPコマンドを表す文字列("GET"、"POST"など)
request_versionHTTPプロトコルバージョンを表す文字列("HTTP/1.0″、"HTTP/1.1″など)
headersHTTPリクエストヘッダーを格納する辞書オブジェクト
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.htmlsub.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など、より応用的な実装確認を進めていく予定です。

http,pythonhttp,python,通信