前回、requests
モジュールなしでHTTPクライアント側を実装した。
サーバー側は適当に実装したが、今回はもう少しまじめにエコーHTTPサーバーを実装してみる。 目指すは開発時に手元でさっと起動できるHTTPサーバースクリプトである。 なお、『The Python 3 Standard Library by Example』を参照した。*1
$ python -m http.server
の中身
Python標準モジュールであるhttp.server
を使えば起動したディレクトリ以下にあるファイルをそのままHTTPで扱うことができる。
デフォルトのポートは8000であるが引数を渡せば別のポートでも起動ができる。
$ python -m http.server 8888
これを実行すると何が起きているのか? 実際にソースコードを参照すると次のような処理が行われている。
def test(HandlerClass=BaseHTTPRequestHandler, ServerClass=HTTPServer, protocol="HTTP/1.0", port=8000, bind=""): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). """ server_address = (bind, port) HandlerClass.protocol_version = protocol with ServerClass(server_address, HandlerClass) as httpd: sa = httpd.socket.getsockname() serve_message = "Serving HTTP on {host} port {port} (http://{host}:{port}/) ..." print(serve_message.format(host=sa[0], port=sa[1])) try: httpd.serve_forever() except KeyboardInterrupt: print("\nKeyboard interrupt received, exiting.") sys.exit(0) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--cgi', action='store_true', help='Run as CGI Server') parser.add_argument('--bind', '-b', default='', metavar='ADDRESS', help='Specify alternate bind address ' '[default: all interfaces]') parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') args = parser.parse_args() if args.cgi: handler_class = CGIHTTPRequestHandler else: handler_class = SimpleHTTPRequestHandler test(HandlerClass=handler_class, port=args.port, bind=args.bind)
細かい点はいくつもあるが、次の2つを実装すれば自分の好きなHTTPサーバーが実装できそうである。
- サーバー(
HTTPServer
の代わりになるもの) - ハンドラー (
BaseHTTPRequestHandler
の代わりになるもの)
HTTPサーバー
HTTPサーバーに関してはできることは少なく、http.server
にあるhttp.server.HTTPServer
をそのまま使うことになる。
http.server.HTTPServer
はsocketserver.TCPServer
の単純なサブクラスである。
class HTTPServer(socketserver.TCPServer): allow_reuse_address = 1 # Seems to make sense in testing environment def server_bind(self): """Override server_bind to store the server name.""" socketserver.TCPServer.server_bind(self) host, port = self.server_address[:2] self.server_name = socket.getfqdn(host) self.server_port = port
今回は開発時にさっと使えるHTTPサーバーを目指すので並列処理や並行処理を考慮するのはオーバースペックであるが、socketserver.ThreadingMixIn
やsocketserver.ForkingMixIn
クラスを使えば複数の処理を非同期に処理するHTTPサーバーを実装できる。実装も簡単でsocketserver.ThreadingMixIn
かsocketserver.ForkingMixIn
に多重継承すればよい。
class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer): """HTTPリクエストをスレッドに分割して処理するHTTPサーバー""" pass class ForkedHTTPServer(socketserver.ForkingMixIn, http.server.HTTPServer): """HTTPリクエストをプロセスに分割して処理するHTTPサーバー""" pass
なお、socketserver.ForkingMixIn
はos.fork()
が利用可能な環境のみ利用できる。つまりWindowsでは動作しない。
`
HTTPリクエストハンドラー
ハンドラーはhttp.server.BaseHTTPRequestHandler
を継承してdo_METHOD
という名前のメソッドを実装するのが基本である。
HTTP 1.0の範囲ではGET, POST, HEADができれば十分である。
今回はエコーサーバーを目指すので、受け取ったパラメータをそのまま表示させればよい。
パラメータに関してはドキュメントにある属性を参照すればよい。
曲者はPOSTされたデータをどうするかである。
『The Python 3 Standard Library by Example』ではcgi.FieldStorage
を駆使して処理する方法が紹介されていた。
流れとしてはBaseHTTPRequestHandler.rfile
をcgi.FieldStorage
に渡してPOSTされた中身を処理する、となる。
スクリプトの全体図
import argparse import cgi import http.server import io import socketserver import sys import urllib.parse class EchoHandler(http.server.BaseHTTPRequestHandler): """リクエストを処理するハンドラ""" def gen_message(self): """HTTPメソッドに共通したメッセージを生成する""" parsed_path = urllib.parse.urlparse(self.path) message_parts = [ "CLIENT_VALUES:", "client_address={} ({})".format(self.client_address, self.address_string()), "command={}".format(self.command), "path={}".format(self.path), "real_path={}".format(parsed_path.path), "query={}".format(parsed_path.query), "request_version".format(self.request_version), "", "SERVER VALUES:", "server_version={}".format(self.server_version), "sys_version={}".format(self.sys_version), "protocol_version={}".format(self.protocol_version), "", "HEADERS RECEIVED:" ] for name, value in sorted(self.headers.items()): message_parts.append("{}={}".format(name, value.rstrip())) else: message_parts.append("") message = "\n".join(message_parts) return message def do_GET(self): message = self.gen_message() self.send_response(http.HTTPStatus.OK) self.send_header('Content-length', len(message)) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(message.encode("utf8")) def do_HEAD(self): message = self.gen_message() self.send_response(http.HTTPStatus.OK) self.send_header('Content-length', len(message)) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() def do_POST(self): form = cgi.FieldStorage( fp=self.rfile, headers=self.headers, environ={ "REQUEST_METHOD": "POST", "CONTENT_TYPE": self.headers["Content-Type"], }, ) message = self.gen_message() self.send_response(http.HTTPStatus.OK) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(message.encode("utf8")) self.wfile.write("\n".encode("utf8")) out = io.TextIOWrapper( self.wfile, encoding="utf8", line_buffering=False, write_through=True, ) out.write("FORM DATA:\n") for field in form.keys(): field_item = form[field] if field_item.filename: file_data = field_item.file.read() file_len = len(file_data) del file_data out.write("Upload {} as {!r} ({} bytes)\n".format(field, field_item.filename, file_len)) else: out.write("{}={}\n".format(field, form[field].value)) else: out.detach() class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer): """HTTPリクエストをスレッドに分割して処理するHTTPサーバー""" pass class ForkedHTTPServer(socketserver.ForkingMixIn, http.server.HTTPServer): """HTTPリクエストをプロセスに分割して処理するHTTPサーバー""" pass def runserver(server, handler, protocol="HTTP/1.0", port=8000, bind=""): server_address = (bind, port) handler.protocol_version = protocol httpd = server(server_address, handler) sa = httpd.socket.getsockname() serve_message = "Serving HTTP on {host} port {port} (http://{host}:{port}/) ..." print(serve_message.format(host=sa[0], port=sa[1])) try: httpd.serve_forever() except KeyboardInterrupt: print("\nKeyboard interrupt received, exiting.") sys.exit(0) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--thread', action='store_true', help='Run Threading HTTP Server') parser.add_argument('--process', action='store_true', help='Run Processing HTTP Server') parser.add_argument('--bind', '-b', default='', metavar='ADDRESS', help='Specify alternate bind address ' '[default: all interfaces]') parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') args = parser.parse_args() if args.thread: server = ThreadedHTTPServer elif args.process: server = ForkedHTTPServer else: server = http.server.HTTPServer handler = EchoHandler runserver(server=server, handler=handler, port=args.port, bind=args.bind)
使用例
開発用にさっと使えるHTTPサーバーであるので、http.server
と同じように使える。
同じように書いたのだから当たり前である。
実行例は次の通りになる。
GET
$ curl -v -X GET http://127.0.0.1:8000/foo?q=bar Note: Unnecessary use of -X or --request, GET is already inferred. * Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0) > GET /foo?q=bar HTTP/1.1 > Host: 127.0.0.1:8000 > User-Agent: curl/7.52.1 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Server: BaseHTTP/0.6 Python/3.5.3 < Date: Fri, 28 Jul 2017 14:07:55 GMT < Content-length: 302 < Content-Type: text/plain; charset=utf-8 < CLIENT_VALUES: client_address=('127.0.0.1', 60502) (127.0.0.1) command=GET path=/foo?q=bar real_path=/foo query=q=bar request_version SERVER VALUES: server_version=BaseHTTP/0.6 sys_version=Python/3.5.3 protocol_version=HTTP/1.0 HEADERS RECEIVED: Accept=*/* Host=127.0.0.1:8000 User-Agent=curl/7.52.1 * Curl_http_done: called premature == 0 * Closing connection 0
HEAD
$ curl -v -I http://127.0.0.1:8000/foo?q=bar * Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0) > HEAD /foo?q=bar HTTP/1.1 > Host: 127.0.0.1:8000 > User-Agent: curl/7.52.1 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK HTTP/1.0 200 OK < Server: BaseHTTP/0.6 Python/3.5.3 Server: BaseHTTP/0.6 Python/3.5.3 < Date: Fri, 28 Jul 2017 14:09:41 GMT Date: Fri, 28 Jul 2017 14:09:41 GMT < Content-length: 303 Content-length: 303 < Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8 < * Curl_http_done: called premature == 0 * Closing connection 0
POST
$ curl -v -X POST http://127.0.0.1:8000/ -F name=dhellmann -F foo=bar -F datafile=@profiles.ini Note: Unnecessary use of -X or --request, POST is already inferred. * Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0) > POST / HTTP/1.1 > Host: 127.0.0.1:8000 > User-Agent: curl/7.52.1 > Accept: */* > Content-Length: 527 > Expect: 100-continue > Content-Type: multipart/form-data; boundary=------------------------0508b2c32fa178f2 > * Done waiting for 100-continue * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Server: BaseHTTP/0.6 Python/3.5.3 < Date: Fri, 28 Jul 2017 14:10:42 GMT < Content-Type: text/plain; charset=utf-8 < CLIENT_VALUES: client_address=('127.0.0.1', 60509) (127.0.0.1) command=POST path=/ real_path=/ query= request_version SERVER VALUES: server_version=BaseHTTP/0.6 sys_version=Python/3.5.3 protocol_version=HTTP/1.0 HEADERS RECEIVED: Accept=*/* Content-Length=527 Content-Type=multipart/form-data; boundary=------------------------0508b2c32fa178f2 Expect=100-continue Host=127.0.0.1:8000 User-Agent=curl/7.52.1 FORM DATA: name=dhellmann foo=bar Upload datafile as 'profiles.ini' (122 bytes) * Curl_http_done: called premature == 0 * Closing connection 0
この先は
方向性としては、WSGIを調べる、socketserver
を調べるとなるが、なかなか大変そうである。
これ以上複雑なHTTPサーバーが必要ならばNginxをインストールしたほうが楽そうではある。