TCP 接続

はじめに

まずは、直接 TCP 接続をおこなってネットワーク接続の基本を確認しましょう。 実行させやすいように、サーバとクライアントでプログラムを分けてあります。

今回おこなうのは、サーバからクライアントへのファイル転送ですね。

  • send_server.py
    • サーバプログラムとして、指定 port で待っている。
    • recv_client との接続を確立すると、サーバからのデータ送信(ファイル転送)をおこなう。
      • このプログラムは 1 回クライアントの相手をしたら終了。
  • recv_client.py
    • ホスト名とポート名を指定して、send_server に接続を行う。
    • 接続が確立すると、サーバからのデータを受信して、ファイルに格納する

となっています。

実行手順

まずは実行させてみましょう。

ネットワーク関連の注意事項

今回は、一つのマシンで二つのプログラムを起動して、通信してもらいます。 以下のような状況では例外がおきますので、実行手順を把握して動かして下さい。

  • サーバ用ポートが使用中だと例外
    • 同じポートを使ったプログラムを複数動かしていないか確認すること(デバッグ中プログラムを実行させたままにしている場合とか特に注意)
  • サーバ起動させていないと、クライアント接続時に失敗
    • 先にサーバ側を起動させること

あと、プログラムの通信相手ホストは、default で localhost になっていますが、変えてもらっても大丈夫です。 ただ、firewall が動いている場合、サーバが動いていても外部からの接続はキックされます。

で、皆さんが自宅などでネットワーク系プログラムを起動する場合、firewall が、 「皆さんの起動したプログラムに、外部マシンからのアクセスを許可するか?」聞いてくるかと思います。 普通に、自マシンのなかでサーバ&クライアントを起動して実験するだけなら、** アクセスを許可する必要はありません。** 別のマシンからアクセスしてほしければ、そのマシンがいるネットワークに対して許可する必要が出てきます。

プログラムを複数実行

プログラムの起動に関してですが、

  • vscode でも PyCharm でも、対象ファイルを選んで run すれば OK です。

    • vscode では、launch はすでに実行中です。別のインスタンスを実行しますか? とか聞かれますが、当然 「はい」です。
    • PyCharm の場合、「同じプログラムを複数起動する」のを制限したりします(今回は関係ないけど)。許可したい場合は、run configurationallow multiple instances などで複数実行可能になります。
  • vscode でも PyCharm でも、二つのプログラムそれぞれに対してコンソールが現れます。

    • vscode では、「ターミナル」の右側に複数の Python Debug Console アイコンが並ぶので、それで選べます。
    • PyCharm では、タブが複数現れます。

ネットワーク接続に関するプログラム説明

実現したいのは、以下の手順

  1. Server が外部ポートを開いて client を socket で待っている。
  2. Client が上記ホスト&ポートに接続し、server-client 間を socket で接続する。

socket.png

サーバプログラム (SendServer)

def send_server():
    check_cwd()  # 実行 directory の調整用のおまじない
    # まずは server socket を開いて
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_sock:
        server_sock.bind((HOST, PORT))
        server_sock.listen()
        # クライアントの到着を待つ
        print('waiting for clients...')
        sock, client = server_sock.accept()
        # クライアントとの接続 socket を使って通信処理開始
        print('connection established.')
        send_file(sock, READ_FILENAME)
        print('transfer completed.')
        sock.close()

やっている内容は

  1. 最初ソケット生成
    • socketクラスを使ってます。
  2. bind でソケットを指定ポートPORTに接続し
    • HOST は複数のネットワーク接続があるような場合意味がでる。今回は全てを表すINADDR_ANY 相当の  '' を選択。
  3. listen で当該ソケット&ポートを接続待ちに
    • ちなみに、ポートが使用中だと例外が起きる
  4. accept でリクエストが来るまで待つ
    • accept() は、相手が来るまで帰ってこない
    • こういうI/O を blocking I/O という
  5. send_file の中で、実際のデータ送信作業(後述)

with ブロックについては、次ページで説明します。

クライアントプログラム

def recv_client():
    # クライアントはサーバに接続
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((SERVER, PORT))
        # サーバとの接続成立し、socket をつかって通信処理
        recv_file(sock)
  1. まずソケットを生成。
  2. SERVER, PORT を指定して接続 (connect)
  3. あとはrecv_file の処理(後述)

てことで、二つのプログラムが、一方がサーバ、もう一方がクライアントとして、TCP 接続するところまで紹介してきました。 動かし方まで含めて、理解しておきましょう。

データ送受信編

接続が確立されて、Socket を取得されれば、後はファイルの中身を送受信するだけ。

  • Server: file からデータを読んで、 socket に送る
  • Client: socket からデータを読んで、文字列出力(標準出力)
    • 今回は、生データ(bytes)を UTF-8 な文字列(str)に変換して出力

rcopy.png

送信側(send_file)

def send_file(sock: socket, filename):
    # ファイルを開いて
    with open(filename, 'rb') as file_in:
        while True:
            # ファイルの中身を
            data = file_in.read(BUF_SIZE)
            if not data:  # ファイルを読み終わったら終了
                break
            # socket から送る
            sock.sendall(data)
  • 指定ファイルを開いて file_in とする。(with 構文は次ページ)
  • あとは、 一定サイズ file_in から取り込んでは、sock に送るのを繰り返す
  • ここで送受信しているのは、 byte 列 (bytes) です。文字列以外のデータもありです。
  • file の使い方は、こちらを参照

受信側(recv_file)

def recv_file(sock: socket):
    # 受信した bytes を貯める場所
    result = bytearray()
    i = 0
    while True:
        # socket から指定サイズ分受け取り
        # data は bytes
        data = sock.recv(BUF_SIZE)
        if not data:  # 受け取るものが無くなったら loop 終了
            break
        print('loop ', i, ':', data)  # data を表示してみる
        result.extend(data)  # result に追記
        i += 1
    text = result.decode(encoding='utf-8')  # bytes から str に
    print(text)  # str

今回、入力されるのがテキストファイルという前提で、受信側は、入力データを文字列 (str) に変換して、表示してみます。

  • result: 受け取った bytes を貯めていくための bytearray
    • result.extend(data)data を追記できる
  • 一定サイズ sock から取り込む (sock.recv(BUF_SIZE)) のを繰り返し、データがなくなるまで result に追記
  • 最後に、resultdecode で文字列 text に変換して、標準出力に表示