pythonでwebサーバーを構築

Pythonを使ってwebサーバーを構築に関するメモ

http.serverというモジュールを使えば一瞬でできるが,もう少し低レベルのところまで扱いたいと思う.

Webサーバーは,クライアント(Webブラウザ)との間で,HTTPという通信規約に基づいて通信するプログラムである.

なのでサーバーに必要な機能は,クライアントとサーバーでそれぞれプログラムを実行しているプロセスの間で通信を確保し,クライアントからの要求に基づいて適当な情報を送信すること.

プロセス間通信(IPC)には色々なやり方がある.ファイルを媒体にするとか,パイプを経由するとか.Webの場合はプログラムを実行しているプラットフォームが限定されない(異なるOSとか)が,そういう場合はソケットを使う方法が支配的らしい.

ソケットはプロセス間通信のAPI(アプリケーションプログラミングインターフェース)である.こういう言い方をするとなんかややこしいけれど,つまるところはプロセス間通信をプログラムする上でのインタフェースとなる部分の仕組みの1つがソケットということ.プロセス間通信でもどんなプロトコルを採用するかなどの違いがあるけれど,どんな方式にしても共通のインターフェースとしてソケットを利用するのが一般的なんだとか.OSI参照モデルでいうと,ソケットはアプリケーション層が,トランスポート層TCPとか)やネットワーク層(IPとか)とデータをやり取りするためのインターフェースになる.

では実際にどういう仕組なのかというと,ソケットという端点を用意し,そこにサーバー・クライアントの接続を確立してデータをやり取りするというものになっている.より具体的には下の図を参考にしてほしい.ソケットを作成する際にはプロトコルを指定するが,それにより通信の方法も変わってくる.例えばbindで接続先のアドレスを指定するが,TCPならばこれがIPアドレスとポート番号の組み合わせとなり,さらにUDPならば接続を確立する必要はないため,listenやconnectなどの作業は不要となる,といった具合.


f:id:simcode:20181007152903p:plain
ソケット通信の概要図


では実際にwebサーバーのプログラムを作っていく.やることはソケットを使って通信を確保し情報をやり取りすること.これ自体はかなり単純だけれど,1対1の通信ということがwebサーバーとしてはネックとなる.あるクライアントと通信している間は処理がブロックされてしまうため,他のクライアントとの通信が行えない.通信がハングしたら終わりである.そのため要求のたびにクライアントとの通信に使うソケット(クライアントソケット)を複数作って,並列処理させることが必要になる.マルチスレッドやマルチプロセスを使うことも方法の1つだが,かなり多くの通信をさばくことを考えると非同期処理が良い.ということで,下のREFのサイトを参考に書いたコードが以下.

HTTP/1.1ではTCP接続が標準で持続的(Keep-Alive)となる.よって,レスポンスのヘッダで送信するデータ量を明示しないと,(サーバー側から接続を切らない限り)クライアントはデータを待ち続けてハングする.なので Content-Type と Content-Length はきちんと送らないとダメ.

import socket
import select

# IP, TCPを使う
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# ノンブロッキング
serversocket.setblocking(0)
# IPアドレス-ポートの組み合わせでアドレスを割当
serversocket.bind(('localhost', 1234))
# 接続要求は5つまで受け入れる
serversocket.listen(5)

# recvを待つソケットリスト
r_list = [serversocket]
# sendを待つソケットリスト
w_list = []

# どんな要求に対してもこのメッセージを返す
res_message = b"""
HTTP/1.1 200 OK
Server: test-python-server
Content-Type: text/html; charset=UTF-8
Content-Length: 123

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <h1>test message</h1>
  </body>
</html>
"""

while r_list:
    # recv/send出来るソケットのリストを取得
    readable, writable, exceptional = select.select(r_list, w_list, r_list, 60)

    for sock in readable:
        if sock is serversocket:
            # 接続する準備が出来た
            clientsocket, client_address = sock.accept()
            clientsocket.setblocking(0)
            r_list.append(clientsocket)
        else:
            # クライアントソケットでrecv
            data = sock.recv(1024)
            if data:
                if sock not in w_list:
                    w_list.append(sock)
            else:
                # ソケットがshutdownされた.接続を切る.
                if sock in w_list:
                    w_list.remove(sock)
                r_list.remove(sock)
                sock.close()

    for sock in writable:
        # メッセージを送信
        sock.send(res_message)
        w_list.remove(sock)

    for sock in exceptional:
        r_list.remove(sock)
        if sock in w_list:
            w_list.remove(sock)
        sock.close()

[REF]
ソケットプログラミング HOWTO — Python 3.6.5 ドキュメント
How to Work with TCP Sockets in Python (with Select Example)