TCP 接続
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 configuration
でallow multiple instances
などで複数実行可能になります。
- vscode では、
-
vscode でも PyCharm でも、二つのプログラムそれぞれに対してコンソールが現れます。
- vscode では、「ターミナル」の右側に複数の
Python Debug Console
アイコンが並ぶので、それで選べます。 - PyCharm では、タブが複数現れます。
- vscode では、「ターミナル」の右側に複数の
ネットワーク接続に関するプログラム説明
実現したいのは、以下の手順
- Server が外部ポートを開いて client を socket で待っている。
- Client が上記ホスト&ポートに接続し、server-client 間を socket で接続する。
サーバプログラム (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()
やっている内容は
- 最初ソケット生成
- socketクラスを使ってます。
bind
でソケットを指定ポートPORT
に接続しHOST
は複数のネットワーク接続があるような場合意味がでる。今回は全てを表すINADDR_ANY
相当の''
を選択。
listen
で当該ソケット&ポートを接続待ちに- ちなみに、ポートが使用中だと例外が起きる
accept
でリクエストが来るまで待つaccept()
は、相手が来るまで帰ってこない- こういうI/O を blocking I/O という
send_file
の中で、実際のデータ送信作業(後述)with
構文で socket を開いたので、ブロック終了時、socket を閉じてくれる
クライアントプログラム
def recv_client():
# クライアントはサーバに接続
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((SERVER, PORT))
# サーバとの接続成立し、socket をつかって通信処理
recv_file(sock)
- まずソケットを生成。
SERVER
,PORT
を指定して接続 (connect
)- あとは
recv_file
の処理(後述) with
構文で socket を開いたので、ブロック終了時、socket を閉じてくれる
てことで、二つのプログラムが、一方がサーバ、もう一方がクライアントとして、TCP 接続するところまで紹介してきました。 動かし方まで含めて、理解しておきましょう。
データ送受信編
接続が確立されて、Socket を取得されれば、後はファイルの中身を送受信するだけ。 今回は、ファイルからデータを読込んで (bytes)、そのまま socket に送ってみましょう。
- Server: ファイルからデータ (bytes) を読んで、そのまま socket に送る
- Client: socket からデータ (bytes) を読んで、表示しつつ
bytearray
にためる- 最後に、
bytearray
に溜まった bytes をutf-8
だと思って文字列に変換して出力
- 最後に、
送信側(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
とする。 - あとは、 一定サイズ
file_in
から取り込んでは、sock
に送るのを繰り返す - ここで送受信しているのは、 byte 列 (bytes) です。文字列以外のデータもありです。
with
ブロック終了時、ファイルを閉じる
受信側(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
result
: 受け取った bytes を貯めていくための bytearrayresult.extend(data)
でdata
を追記できる
- 一定サイズ
sock
から取り込む (sock.recv(BUF_SIZE)
) のを繰り返し、受信データdata
が取れなくなるまでresult
に追記 - 最後に、
result
をdecode
で文字列text
に変換して、標準出力に表示
今回、入力されるのがテキストファイルなので、受け取った data (型は bytes) を表示させると、中身 が見えますよね。受信側は BUS_SIZE を小さく設定しているので、実行結果はこんな感じ。 loop 7 あたりから日本語が混ざってきてます。
loop 0 : b'<!DOCTYPE html>\r'
loop 1 : b'\n<html lang="ja"'
loop 2 : b'>\r\n<head>\r\n <'
loop 3 : b'meta charset="UT'
loop 4 : b'F-8">\r\n <titl'
loop 5 : b'e>Sample HTML</t'
loop 6 : b'itle>\r\n</head>\r\n'
loop 7 : b'<body>\r\n\xe3\x82\xb5\xe3\x83\xb3\xe3\x83'
...