matobaの学んだこと

とあるPythonエンジニアのブログ。ソフトウェア開発、執筆活動、ライフログ。

実際にソケット通信を行うプログラムをPythonで作ってみた

こんにちは。matobaです。 この記事は、BeProud Advant Calendarの16日目の記事です。

先日、ソケット通信の仕組みを改めて勉強していたので、今回は、実際にソケット通信を行うプログラムをPythonで作ってみて思ったことを書きたいと思います。

HTTPのクライアントを作ってみる

とりあえず、ざっくりクライアントを作ってみました。

import socket

HOST = "127.0.0.1"
PORT = 8000

CRLF = "\r\n"

def main():
    # IPv4のTCP通信用のオブジェクトを作る
    # note: socket.AF_INET はIPv4
    # note: socket.SOCK_STREAM はTPC
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 指定したホストとポートに接続
    client.connect((HOST, PORT))

    # HTTPのリクエストを作る
    message_lists = [
        'GET / HTTP/1.1',
        'host:127.0.0.1'
    ]
    send_message = ""
    for message in message_lists:
        send_message += message + CRLF
    
    # see: https://triple-underscore.github.io/RFC2616-ja.html#section-4
    # ヘッダーフィールドの後の最初の空行でリクエストは終わり
    send_message += CRLF

    # バイナリで送る
    send_binary = send_message.encode()
    client.send(send_binary)

    # 流れてくるメッセージを読み込む
    recieve_messages = []
    bytes_recd = 0
    MSGLEN = 4096
    while bytes_recd < MSGLEN:
        recieve = client.recv(min(MSGLEN - bytes_recd, 2048))
        if recieve == b"":
            # とりあえず空が来たら終わる
            break
        recieve_messages.append(recieve)
        bytes_recd = bytes_recd + len(recieve)

    print(b"".join(recieve_messages))

if __name__ == "__main__":
    main()

クライアントを作って思ったこと

  • 当たり前なのかもしれないけど、TCPなのかUDPなのかで、socketをオブジェクトの初期化から違う。IPv4かIPv6かもsocketオブジェクトを初期化した時点から違う。
  • Pythonのsocketパッケージは、本当にLinuxのソケットライブラリをそのままラップしてるんだなと思った。 Man page of SOCKET
  • リクエストを送る時は、ホストとポートを指定して、接続してからデータを送る。それぞれにタイムアウトがあるんだろうな。
  • HTTPのRPCを軽く眺めたけど、HTTPクライアントを実装するのはすごく大変そうだったなと思った。ハイパーテキスト転送プロトコル -- HTTP/1.1
  • Pythonの公式ドキュメントにあったソケットプログラミングHOW TOを読んだけど、通信のクライアントを実装する時、いろんな異常系を考えるのが大変そうだと思った。

とりあえず、いつもrequestsとか使って、すごく簡単にHTTPリクエストを送るプログラミングをしていたり、ブラウザを使ってHTTPをサクサク送ってますが、裏側はこういう風になってるんだーというのをみれてよかったです。

サーバーを作る

とりあえず、サーバーもサクッと作ってみます。 HTTPのサーバーを実装しようかと思ったのですが、ヘッダーを読んでリソースを判定する処理を書くのが面倒だったのでとりあえず、応答するだけのコードを書きました。

import socket

PORT = 8001

def main():

    # 待ち受け用のソケットオブジェクト
    server = socket.socket()
    try:
        # 待ち受けポートに割り当て
        server.bind(('127.0.0.1', PORT))
        while True:
            # 待ち受け開始
            server.listen(5)

            # 要求がいたら受け付け
            client, addr = server.accept()

            # 受け取ったメッセージを出力
            recieve_messege = client.recv(4096).decode()
            print('message:{}'.format(recieve_messege))

            # endというメッセージを受け取ったら終わる
            if recieve_messege == "end\r\n":
                break

            # 返答
            client.send(b"I am socket server...\n")
            client.close()
    finally:
        server.close()

if __name__ == "__main__":
    main()

telnetを使って、接続してメッセージを送ってみます。

接続してメッセージを送ります。

$ telnet 127.0.0.1 8001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
test message #送ったメッセージ
I am socket server... #受け取ったメッセージ
Connection closed by foreign host. # 接続を切られた

サーバー側もメッセージを受け取れています。

$ python server.py
message:test message

endというメッセージを送ると、

$ telnet 127.0.0.1 8001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
end
Connection closed by foreign host.

無事にサーバーが終了します。

$ python server.py
message:end

$

サーバーを作って思ったこと

  • 今の所あんまり複雑なことをしてなくて、ホストとポートを指定しておくとそこに流れてくるデータを待ち受けて情報を受け取って返すことができる。
  • やっぱりHTTPのようなきっちりと仕様になった通信をやろうとすると色々、手間がかかりそう。
  • あと、サーバーを適当に終了するとポートを掴んだままになってしまって、結構邪魔だったりしたので、きちんと終了しないと色々な問題が起きそう。
  • サーバーのようにずっと常駐しているアプリは、リソースを食い続けないように注意しないと問題が起きるんだろうなあ、と思った。

Python標準ライブラリのhttpserverのコードを読む

ここまでの知識とある程度のPythonの文法への理解があれば、標準ライブラリのhttpserverを読んで理解できるのでは?と思いました。

というわけで、ざっくり眺めてみました。ソースはこちら。

github.com

読んでみた感想。

  • 思ったより、ソースの量が少ない。
  • 詳しいところまでは追いかけてないけど、なんとなくこんなことしてるのかなーというのは雰囲気でわかった。
  • socketserverという標準ライブラリがあるようで、socketサーバー自体の実装はそちらに任せていて、このライブラリでは、HTTPのロジックだけを実装しているように見えた。
  • socketserverもざっと見たところ、TCPServerとUDPServerのクラスがあった。ということはその気になれば、自分でTCPやUDPのサーバーを作れるのかもと思った。

終わり

今回は、いつもブラックボックスで使ってるソケット通信の中身について学んでみました。 gunicornの中を見たりもしたかったのですが、今回は時間が足りませんでした。

ブラックボックスで使っていると、中でどんなことをやっているのかわからず、すごく怖いというか不安な感じがあるんですが、いざ中を開けてみると、たくさんの検討の末にすごくシンプルな構造になっていたように思いました。

また、普段はブラックボックスで使ってるものの中身を調べていく時間が取れたら良いなと思います。

ではでは。