ここではクライアントからサーバーに接続しメッセージを送ると、そのメッセージがサーバーからエコーバックしてクライアント戻ってくるサーバーのTCP接続バージョンとUDP接続バージョンの2つのバージョンで作成します。
Transmission Control Protocol (TCP)接続はコネクション指向(Connection-Oriented)な通信です。User Datagram Protocol (UDP)接続はコネクションレス (Connectionless)な通信です。
TCPの通信は、サーバー側とクライアント側がコネクションを確立させた上で通信を行います。TCPの接続開始時に3ウェイ・ハンドシェイクと呼ばれるシーケンス手順を行い接続相手を確定しています。
この仕組みを悪用したのがTCP SYN Flood攻撃(参考 RFC4987)です。クライアント側にいる攻撃者から大量にSYNパケットを一方的にサーバーに送りつけます。サーバー側はTCP接続に必要な資源を予約し、SYN-ACKパケットを送り、クライアント側からのACKパケットを待ちます。ACKを無視し、さらにSYNを送りつづけるとサーバー側は予約した資源を抱えたまま、最後は資源を使い切り、それ以上の接続が出来なくなります。そのような状況になると、他の正常なTCP接続を試みても、接続する資源が枯渇しているので接続ができなくなります。
クライアントから送られサーバーに到着したIPパケット(に含まれるデータ)はプログラムに渡る際にはストリーム(連続した一連のデータ)に再構成されます。ネットワーク転送中にパケットが欠落したなどしサーバーに到着しない場合、それを検知し再送を要求するメカニズムを持っています。そのため、プログラムが受け取る(読み込む)ストリームはデータが欠損していたり、あるいはデータの順番が違っていたりするようなことはありません。ネットワークがなんらかの理由で再送すらできなくなり接続が維持できなくなった場合は、コネクションを切断するという形でセッションが終了します。
エコーサーバーは受け取った値を、そのままクライアントに送り返すプログラムです。tcpserv.pyはTCP接続を使うサーバー側プログラムの例です。AF_INET (インターネット接続=IP接続)を指定し、そのタイプはSOCK_STREAM (TCP接続)を指定しています。listen()は、ソケットを接続待ちソケット(passive socket)として使うための初期化処理で、引数は接続待ちをいくつにするか、という値になります。ここでは1と明示的に与えていますが、指定しなければデフォルトの値に設定されるとマニュアルには記載されています。その場合、待ちキューの大きさはシステムに依存します。
# File: tcpserv.py
#
# エコーサーバー
# https://docs.python.org/ja/3/library/socket.html
#
import socket
HOST = '0.0.0.0' # すべてのIPアドレスからの接続を許す
PORT = 54321 # ポート番号 / ダイナミック・ポートの領域を利用
RECVBUFSIZE = 1024
# TCPソケットを開く
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT)) # ソケットをポートにバインド
s.listen(1) # 接続準備
while True:
conn, addr = s.accept() # 接続待ち
with conn:
print('Connected by', addr)
while True:
data = conn.recv(RECVBUFSIZE) # 受信
if not data: # データがなければ
break # 接続終了
conn.sendall(data) # 受信したデータをエコーバックtcpclie.pyはエコーサーバーのクライアントです。tcpserv.pyは同一マシン上で動作していることを前提としているので、接続ホストはlocalhostとしています。
サーバーに接続し、’こんにちは世界’という文字列を送付して、そのエコーバックを出力しています。ここで注意して欲しいのは’こんにちは世界’はstrタイプではなく、byteタイプに変換された上で送り出している点です。また、送られてきたものはstrタイプに変換した後、出力されている点です。
# File: tcpclie.py
# エコー クライアント
# https://docs.python.org/ja/3/library/socket.html
import socket
HOST = 'localhost' # ローカルホストを指定
PORT = 54321 # サーバーのポートを指定
RECVBUFSIZE = 1024
# # ソケットでTCPを開く
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT)) # ホストとポートを指定して相手に接続
s.sendall('こんにちは世界'.encode('utf-8')) # データを送る / UTF-8にエンコード
data = s.recv(RECVBUFSIZE) # エコーバックを受信
print('Echo:',data.decode('utf-8')) # UTF-8で出力今度はUDPプロトコルを使ったエコーサーバーを考えてみます。このエコーサーバーはパケットが届いて、そのアドレスに受け取ったパケットの内容を標準出力に出力し、その後、その内容をそのまま送ってきたアドレスに送り返します。とてもシンプルな構造になっています。UDPですからTCPのように通信を開始するための3ウェイ・ハンドシェイクなど必要ありません。そのため、通信を行うということだけを考えると、最小限の通信しかしないわけですから「処理が速い」(あくまでもカッコつきの「処理が速い」)といえます。
しかし、そのかわりクライアント側から送られたパケットがサーバー側に確実に届くという保証(より正確には届かないという検出を)するメカニズムがUDPにはありません。またサーバー側からみると送られてきたIPパケットに付けられている送付元アドレスが正しいアドレスなのか確認するすべは、素のUDPプロトコルレイヤーのレベルでは用意されていません。もし、送付元アドレスに偽造のアドレスが設定されてしまっていた場合、サーバーは偽造アドレス先に返却するパケットを送ることになります。これを悪用されると、UDPパケットによるリフレクション攻撃(参考 JPNIC)となります。
# File: udpserv.py
# エコーサーバー
# https://docs.python.org/ja/3/library/socket.html
#
import socket
HOST = '0.0.0.0' # すべてのIPアドレスからの接続を許す
PORT = 12345 # ポート番号 / ダイナミック・ポートの領域を利用
RECVBUFSIZE = 1024
# UDPのソケットを開く
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind((HOST, PORT)) # ソケットをポートにバインド
while True: # 無限ループ
data, addr = s.recvfrom(RECVBUFSIZE) # 受信
print(data.decode('utf-8'),addr) # 出力
s.sendto(data,addr) # エコーバッククライアントはシンプルです。sendtoで送るデータ(byteタイプ)とIPアドレス(あるいはホスト名)とポート番号のタプルを設定するだけです。
# File: udpclie.py
# エコー クライアント
# https://docs.python.org/ja/3/library/socket.html
import socket
HOST = 'localhost' # ローカルホストを指定
PORT = 12345 # サーバーのポートを指定
RECVBUFSIZE = 1024
# UDPのソケットを開く
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
send_len = s.sendto('こんにちは世界'.encode('utf-8'), (HOST,PORT)) # 送出
data, addr = s.recvfrom(RECVBUFSIZE) # 返りメッセージを受信
print('Echo:',data.decode('utf-8')) # UTF-8で出力TCPとUDPの使い分けは、その利点と欠点を十分に理解した上で使い分ける必要があります。おおよそ、データの入力・処理・出力という処理モデルではTCPプロトコルで十分に機能すると考えられます。その反対に、少量のデータで、かつ送り先へのデータの到着を必ずしも確実にする必要がないようなデータの場合や、遅滞したデータは必要なくなるような/再送が必要ないようなデータの場合はUDPで実装するほうが良い結果が得られるものもあるでしょう。両者のプロトコルの違いを理解した上で適切に選択するすることで良いアプリケーションが設計、そして実装できることでしょう。