これは、なにをしたくて書いたもの?
Pythonにはhttp.serverというライブラリがあり、簡単にHTTPサーバー(Webサーバー)を起動することができます。
21.22. http.server --- HTTP サーバ — Python 3.6.9 ドキュメント
こんな感じで起動して、簡単にカレントディレクトリをドキュメントルートにしたHTTPサーバーを立てられます。
$ python3 -m http.server Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
これでカレントディレクトリ配下にあるファイルを公開できるわけですが、そうではなくてhttp.serverライブラリを使って自分でHTTPサーバーを
書く方法について調べてみました、と。
環境
今回の環境は、こちら。
$ python3 -V Python 3.6.8
BaseHTTPRequestHandlerのサブクラスを作成する
答えとしては、BaseHTTPRequestHandlerを継承したクラスを作成し、HTTPServerのハンドラとして設定します。
http.server.BaseHTTPRequestHandler
こんな感じで。
from http.server import BaseHTTPRequestHandler, HTTPServer address = ('localhost', 8080) class MyHTTPRequestHandler(BaseHTTPRequestHandler): ... with HTTPServer(address, MyHTTPRequestHandler) as server: server.serve_forever()
BaseHTTPRequestHandlerを継承したクラスでは、呼び出されたHTTPメソッドに応じてdo_XXXメソッドが呼び出されるように
なっているので、対応するメソッドを作成すればOKです。
https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L399
例えば、do_GETやdo_POSTなど。
リクエストの情報は、MyHTTPRequestHandlerのインスタンス自身(self)に格納されています。
例えば、リクエストされたパスはpath、HTTPヘッダーはheaders、HTTPボディはrfileなどです。
レスポンスを返す時は、send_responseでHTTPステータスコードを指定し、必要に応じてHTTPヘッダーをsend_headerで指定し、
HTTPヘッダーの終了をend_headersで指定。HTTPボディを送るにはwfileを使用します。
このあたりを使って、簡単なHTTPサーバーを書いてみます
サンプル
GETとPOSTを簡単に扱う、HTTPサーバーを書いてみましょう。
こんな感じで。
simple_http_server.py
from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import parse_qs, urlparse address = ('localhost', 8080) class MyHTTPRequestHandler(BaseHTTPRequestHandler): def do_GET(self): print('path = {}'.format(self.path)) parsed_path = urlparse(self.path) print('parsed: path = {}, query = {}'.format(parsed_path.path, parse_qs(parsed_path.query))) print('headers\r\n-----\r\n{}-----'.format(self.headers)) self.send_response(200) self.send_header('Content-Type', 'text/plain; charset=utf-8') self.end_headers() self.wfile.write(b'Hello from do_GET') def do_POST(self): print('path = {}'.format(self.path)) parsed_path = urlparse(self.path) print('parsed: path = {}, query = {}'.format(parsed_path.path, parse_qs(parsed_path.query))) print('headers\r\n-----\r\n{}-----'.format(self.headers)) content_length = int(self.headers['content-length']) print('body = {}'.format(self.rfile.read(content_length).decode('utf-8'))) self.send_response(200) self.send_header('Content-Type', 'text/plain; charset=utf-8') self.end_headers() self.wfile.write(b'Hello from do_POST') with HTTPServer(address, MyHTTPRequestHandler) as server: server.serve_forever()
まずは、do_GETメソッドから。
self.pathにリクエストされたパスが入っているのですが、QueryStringも一緒に入っているので、こちらをパースしたければurllibなどを使う必要が
あります。self.headersには、HTTPヘッダーの内容が辞書として入っています。
print('path = {}'.format(self.path)) parsed_path = urlparse(self.path) print('parsed: path = {}, query = {}'.format(parsed_path.path, parse_qs(parsed_path.query))) print('headers\r\n-----\r\n{}-----'.format(self.headers))
レスポンスを返す時は、こんな感じで。send_headerは使わなくてもいいですが、end_headersは呼び出しておく必要があります。
self.send_response(200) self.send_header('Content-Type', 'text/plain; charset=utf-8') self.end_headers() self.wfile.write(b'Hello from do_GET')
HTTPボディはwfileで書き出すわけですが、こちらの実体はBufferedIOBaseです。
この状態でサーバーを起動して
$ python3 simple_http_server.py
curlでアクセス。
$ curl -i 'localhost:8080?param1=value1¶m2=value2' HTTP/1.0 200 OK Server: BaseHTTP/0.6 Python/3.6.8 Date: Mon, 12 Aug 2019 12:42:28 GMT Content-Type: text/plain; charset=utf-8 Hello from do_GET
この時のサーバー側のログとしては、こんな感じのものが出力されます。
path = /?param1=value1¶m2=value2 parsed: path = /, query = {'param1': ['value1'], 'param2': ['value2']} headers ----- Host: localhost:8080 User-Agent: curl/7.58.0 Accept: */* ----- 127.0.0.1 - - [12/Aug/2019 21:42:28] "GET /?param1=value1¶m2=value2 HTTP/1.1" 200 -
アクセスログっぽいものが出力されていますね。これは、send_responseを呼び出した時にlog_requestを介して、log_messageが
呼び出されるからです。
https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L483
https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L535-L536
https://github.com/python/cpython/blob/v3.6.8/Lib/http/server.py#L552-L572
log_messageを使うと、アクセスログのようなフォーマットでログ出力ができますが、出力先は標準エラー出力(sys.stderr)なので、ご注意を。
続いて、do_POST。
こちらは、do_GETの内容に加えて、HTTPボディをログ出力するように変更しています。
print('path = {}'.format(self.path)) parsed_path = urlparse(self.path) print('parsed: path = {}, query = {}'.format(parsed_path.path, parse_qs(parsed_path.query))) print('headers\r\n-----\r\n{}-----'.format(self.headers)) content_length = int(self.headers['content-length']) print('body = {}'.format(self.rfile.read(content_length).decode('utf-8')))
rfile.readする時に、Content-Lengthで読み出す長さを指定しないと、ずっと待ち続けるので注意を…。
残りは同じように。
self.send_response(200) self.send_header('Content-Type', 'text/plain; charset=utf-8') self.end_headers() self.wfile.write(b'Hello from do_POST')
curlで動作確認。意図的にQueryStringも仕込んでいます。
$ curl -i localhost:8080?query_param=query_value1 -d 'param1=value1¶m2=value2' HTTP/1.0 200 OK Server: BaseHTTP/0.6 Python/3.6.8 Date: Mon, 12 Aug 2019 12:54:29 GMT Content-Type: text/plain; charset=utf-8 Hello from do_POST
ログ。
path = /?query_param=query_value1 parsed: path = /, query = {'query_param': ['query_value1']} headers ----- Host: localhost:8080 User-Agent: curl/7.58.0 Accept: */* Content-Length: 27 Content-Type: application/x-www-form-urlencoded ----- body = param1=value1¶m2=value2 127.0.0.1 - - [12/Aug/2019 21:54:29] "POST /?query_param=query_value1 HTTP/1.1" 200 -
とまあ、こんな感じで。
これで、簡単なHTTPサーバーは書けそうですね。
なお、このサーバーをマルチスレッドで動かしたい場合は、こちらを参照。