Pythonでのソケットプログラミング(ガイド)

Pythonでのソケットプログラミング(ガイド)

ソケットとソケットAPIは、ネットワーク経由でメッセージを送信するために使用されます。 それらはinter-process communication (IPC)の形式を提供します。 ネットワークは、コンピューターへの論理的なローカルネットワーク、または他のネットワークへの独自の接続を備えた外部ネットワークに物理的に接続されたものです。 明らかな例は、ISP経由で接続するインターネットです。

このチュートリアルには、ソケットサーバーとクライアントをPythonで構築する3つの異なる反復があります。

  1. 簡単なソケットサーバーとクライアントを見て、チュートリアルを開始します。

  2. この最初の例でAPIとその仕組みを確認したら、複数の接続を同時に処理する改善されたバージョンを見ていきます。

  3. 最後に、独自のカスタムヘッダーとコンテンツを備えた本格的なソケットアプリケーションのように機能するサンプルサーバーとクライアントの構築に進みます。

このチュートリアルを終了すると、Pythonのsocket moduleの主要な関数とメソッドを使用して、独自のクライアントサーバーアプリケーションを作成する方法を理解できるようになります。 これには、カスタムクラスを使用してエンドポイント間でメッセージとデータを送信し、独自のアプリケーションで構築して利用する方法の表示が含まれます。

このチュートリアルの例では、Python 3.6を使用しています。 source code on GitHubを見つけることができます。

ネットワークとソケットは大きなテーマです。 リテラルボリュームがそれらについて書かれています。 ソケットやネットワークを初めて使用する場合、すべての用語や要素に圧倒されると感じるのは完全に普通のことです。 私はやったことを知っています!

がっかりしないでください。 このチュートリアルを作成しました。 Pythonで行うように、一度に少し学ぶことができます。 ブラウザのブックマーク機能を使用して、次のセクションの準備ができたら戻ってください。

始めましょう!

バックグラウンド

ソケットには長い歴史があります。 それらは1971年にoriginated with ARPANETを使用し、その後1983年にリリースされたBerkeley Software Distribution(BSD)オペレーティングシステムでBerkeley socketsと呼ばれるAPIになりました。

1990年代にWorld Wide Webでインターネットが普及したとき、ネットワークプログラミングも普及しました。 新しく接続されたネットワークを利用してソケットを使用するアプリケーションは、Webサーバーとブラウザーだけではありません。 すべての種類とサイズのクライアントサーバーアプリケーションが広く使用されるようになりました。

今日、ソケットAPIで使用される基礎となるプロトコルは長年にわたって進化しており、新しいものを見てきましたが、低レベルAPIは同じままです。

最も一般的なタイプのソケットアプリケーションは、クライアントサーバーアプリケーションです。このアプリケーションでは、一方がサーバーとして機能し、クライアントからの接続を待機します。 これは、このチュートリアルで扱うアプリケーションのタイプです。 具体的には、Internet socketsのソケットAPIを見ていきます。これは、バークレーまたはBSDソケットと呼ばれることもあります。 同じホスト上のプロセス間の通信にのみ使用できるUnix domain socketsもあります。

ソケットAPIの概要

Pythonのsocket moduleは、Berkeley sockets APIへのインターフェースを提供します。 これは、このチュートリアルで使用および説明するモジュールです。

このモジュールの主なソケットAPI関数とメソッドは次のとおりです。

  • socket()

  • bind()

  • listen()

  • accept()

  • connect()

  • connect_ex()

  • send()

  • recv()

  • close()

Pythonは、Cの対応するこれらのシステムコールに直接マップする便利で一貫性のあるAPIを提供します。 次のセクションでは、これらがどのように併用されるかを見ていきます。

Pythonには、標準ライブラリの一部として、これらの低レベルソケット関数の使用を容易にするクラスもあります。 このチュートリアルでは取り上げていませんが、ネットワークサーバーのフレームワークであるsocketserver moduleを参照してください。 HTTPやSMTPなどの高レベルのインターネットプロトコルを実装するモジュールも多数あります。 概要については、Internet Protocols and Supportを参照してください。

TCPソケット

すぐにわかるように、socket.socket()を使用してソケットオブジェクトを作成し、ソケットタイプをsocket.SOCK_STREAMとして指定します。 これを行う場合、使用されるデフォルトのプロトコルはTransmission Control Protocol (TCP)です。 これは適切なデフォルトであり、おそらくあなたが望むものです。

なぜTCPを使用する必要があるのですか? 伝送制御プロトコル(TCP):

  • ネットワークでドロップされたIs reliable:パケットは、送信者によって検出され、再送信されます。

  • Has in-order data delivery:データは、送信者によって書き込まれた順序でアプリケーションによって読み取られます。

対照的に、socket.SOCK_DGRAMで作成されたUser Datagram Protocol (UDP)ソケットは信頼性が低く、受信者が読み取ったデータは送信者の書き込みと順序が狂う可能性があります。

何でこれが大切ですか? ネットワークはベストエフォート型の配信システムです。 データが目的地に到着すること、または送信されたものを受け取ることを保証するものではありません。

ネットワークデバイス(ルーターやスイッチなど)には、使用可能な帯域幅が有限であり、固有のシステム制限があります。 クライアントやサーバーと同様に、CPU、メモリ、バス、インターフェイスパケットバッファーがあります。 TCPを使用すると、packet lossやデータの順序が狂って到着するなど、ネットワークを介して通信しているときに必ず発生する多くのことを心配する必要がなくなります。

下の図では、TCPのソケットAPI呼び出しとデータフローのシーケンスを見てみましょう。

左側の列はサーバーを表します。 右側にクライアントがあります。

左上の列から始めて、サーバーが「リスニング」ソケットをセットアップするために行うAPI呼び出しに注意してください。

  • socket()

  • bind()

  • listen()

  • accept()

リスニングソケットは、まさにそのように聞こえます。 クライアントからの接続をリッスンします。 クライアントが接続すると、サーバーはaccept()を呼び出して、接続を受け入れるか完了します。

クライアントはconnect()を呼び出してサーバーへの接続を確立し、スリーウェイハンドシェイクを開始します。 ハンドシェイク手順は、接続の両側がネットワーク内で到達可能であること、つまりクライアントがサーバーに到達できること、およびその逆も可能であることを保証するため重要です。 一方のホスト、クライアント、またはサーバーのみが他方に到達できる場合があります。

中央はラウンドトリップセクションで、send()recv()への呼び出しを使用してクライアントとサーバー間でデータが交換されます。

下部では、クライアントとサーバーがそれぞれのソケットをclose()します。

Echoクライアントとサーバー

ソケットAPIの概要とクライアントとサーバーの通信方法を確認したので、最初のクライアントとサーバーを作成しましょう。 簡単な実装から始めます。 サーバーは、受け取ったものをクライアントにエコーするだけです。

エコーサーバー

これがサーバーecho-server.pyです:

#!/usr/bin/env python3

import socket

HOST = '127.0.0.1'  # Standard loopback interface address (localhost)
PORT = 65432        # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

Note:今は上記のすべてを理解する必要はありません。 これらの数行のコードでは多くのことが行われています。 これは単なる出発点であるため、基本的なサーバーが動作しているのを確認できます。

このチュートリアルの最後にreference sectionがあり、詳細情報と追加のリソースへのリンクがあります。 チュートリアル全体でこれらのリソースや他のリソースにリンクします。

各API呼び出しを順に見て、何が起こっているのかを見てみましょう。

socket.socket()は、context manager typeをサポートするソケットオブジェクトを作成するため、with statementで使用できます。 s.close()を呼び出す必要はありません。

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    pass  # Use the socket object without calling s.close().

socket()に渡される引数は、address familyとソケットタイプを指定します。 AF_INETは、IPv4のインターネットアドレスファミリです。 SOCK_STREAMは、ネットワークでメッセージを転送するために使用されるプロトコルであるTCPのソケットタイプです。

bind()は、ソケットを特定のネットワークインターフェイスおよびポート番号に関連付けるために使用されます。

HOST = '127.0.0.1'  # Standard loopback interface address (localhost)
PORT = 65432        # Port to listen on (non-privileged ports are > 1023)

# ...

s.bind((HOST, PORT))

bind()に渡される値は、ソケットのaddress familyによって異なります。 この例では、socket.AF_INET(IPv4)を使用しています。 したがって、2タプル((host, port))が必要です。

hostは、ホスト名、IPアドレス、または空の文字列にすることができます。 IPアドレスを使用する場合、hostはIPv4形式のアドレス文字列である必要があります。 IPアドレス127.0.0.1loopbackインターフェースの標準IPv4アドレスであるため、ホスト上のプロセスのみがサーバーに接続できます。 空の文字列を渡すと、サーバーは利用可能なすべてのIPv4インターフェイスで接続を受け入れます。

portは、1-65535の整数である必要があります(0は予約済みです)。 クライアントからの接続を受け入れるのはTCP portの数値です。 ポートが1024未満の場合、一部のシステムではスーパーユーザー権限が必要になる場合があります。

bind()でホスト名を使用する際の注意事項は次のとおりです。

「IPv4 / v6ソケットアドレスのホスト部分でホスト名を使用すると、PythonはDNS解決から返された最初のアドレスを使用するため、プログラムが非決定的な動作を示す場合があります。 ソケットアドレスは、DNS解決やホスト構成の結果に応じて、実際のIPv4 / v6アドレスに異なる方法で解決されます。 決定論的な動作には、ホスト部分に数値アドレスを使用してください。」 (Source)

これについては後でUsing Hostnamesで説明しますが、ここで言及する価値があります。 今のところ、ホスト名を使用すると、名前解決プロセスから返された結果に応じて異なる結果が表示される可能性があることを理解してください。

それは何でもありえます。 アプリケーションを初めて実行するときは、アドレス10.1.2.3である可能性があります。 次回は別のアドレス、192.168.0.1です。 3回目は、172.16.7.8などになります。

サーバーの例を続けると、listen()はサーバーがaccept()接続できるようにします。 これは、「リスニング」ソケットになります。

s.listen()
conn, addr = s.accept()

listen()にはbacklogパラメータがあります。 新しい接続を拒否する前にシステムが許可する、受け入れられない接続の数を指定します。 Python 3.5以降では、オプションです。 指定しない場合、デフォルトのbacklog値が選択されます。

サーバーが同時に多数の接続要求を受信する場合、backlog値を増やすと、保留中の接続のキューの最大長を設定すると役立つ場合があります。 最大値はシステムに依存します。 たとえば、Linuxでは、/proc/sys/net/core/somaxconnを参照してください。

accept()blocksそして着信接続を待ちます。 クライアントが接続すると、接続を表す新しいソケットオブジェクトと、クライアントのアドレスを保持するタプルを返します。 タプルには、IPv4接続の場合は(host, port)、IPv6の場合は(host, port, flowinfo, scopeid)が含まれます。 タプル値の詳細については、リファレンスセクションのSocket Address Familiesを参照してください。

理解する必要があることの1つは、accept()からの新しいソケットオブジェクトがあることです。 クライアントとの通信に使用するソケットなので、これは重要です。 サーバーが新しい接続を受け入れるために使用している待機ソケットとは異なります。

conn, addr = s.accept()
with conn:
    print('Connected by', addr)
    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.sendall(data)

accept()からクライアントソケットオブジェクトconnを取得した後、無限のwhileループを使用してblocking callsからconn.recv()にループします。 これにより、クライアントが送信したデータがすべて読み取られ、conn.sendall()を使用してエコーバックされます。

conn.recv()が空のbytesオブジェクトb''を返す場合、クライアントは接続を閉じ、ループは終了します。 withステートメントはconnとともに使用され、ブロックの最後でソケットを自動的に閉じます。

エコークライアント

次に、クライアントecho-client.pyを見てみましょう。

#!/usr/bin/env python3

import socket

HOST = '127.0.0.1'  # The server's hostname or IP address
PORT = 65432        # The port used by the server

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)

print('Received', repr(data))

サーバーと比較すると、クライアントは非常に単純です。 ソケットオブジェクトを作成し、サーバーに接続し、s.sendall()を呼び出してメッセージを送信します。 最後に、s.recv()を呼び出してサーバーの応答を読み取り、それを出力します。

Echoクライアントとサーバーの実行

クライアントとサーバーを実行して、それらの動作を確認し、何が起こっているのかを調べてみましょう。

Note:コマンドラインからサンプルや独自のコードを実行するのに問題がある場合は、How Do I Make My Own Command-Line Commands Using Python?をお読みください。Windowsを使用している場合は、Python Windows FAQを確認してください。

ターミナルまたはコマンドプロンプトを開き、スクリプトを含むディレクトリに移動して、サーバーを実行します。

$ ./echo-server.py

端末がハングしているように見えます。 これは、サーバーが呼び出しでblocked(一時停止)されているためです。

conn, addr = s.accept()

クライアント接続を待っています。 次に、別のターミナルウィンドウまたはコマンドプロンプトを開き、クライアントを実行します。

$ ./echo-client.py
Received b'Hello, world'

サーバーウィンドウに次のように表示されます。

$ ./echo-server.py
Connected by ('127.0.0.1', 64623)

上記の出力では、サーバーはs.accept()から返されたaddrタプルを出力しました。 これはクライアントのIPアドレスとTCPポート番号です。 ポート番号64623は、マシンで実行すると異なる可能性があります。

ソケット状態の表示

ホスト上のソケットの現在の状態を確認するには、netstatを使用します。 macOS、Linux、Windowsでデフォルトで利用可能です。

サーバーを起動した後のmacOSからのnetstat出力は次のとおりです。

$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  127.0.0.1.65432        *.*                    LISTEN

Local Address127.0.0.1.65432であることに注意してください。 echo-server.pyHOST = '127.0.0.1'の代わりにHOST = ''を使用した場合、netstatは次のように表示します。

$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  *.65432                *.*                    LISTEN

Local Address*.65432です。これは、アドレスファミリをサポートする使用可能なすべてのホストインターフェイスが着信接続の受け入れに使用されることを意味します。 この例では、socket()の呼び出しで、socket.AF_INETが使用されました(IPv4)。 これは、Proto列:tcp4で確認できます。

上記の出力をトリミングして、エコーサーバーのみを表示しました。 実行しているシステムに応じて、より多くの出力が表示される可能性があります。 注意すべき点は、列ProtoLocal Address、および(state)です。 上記の最後の例では、netstatは、エコーサーバーがすべてのインターフェイス(*.65432)のポート65432でIPv4 TCPソケット(tcp4)を使用しており、リスニング状態(LISTEN)にあることを示しています。 s)。

これを確認するもう1つの方法は、追加の役立つ情報とともに、lsof(開いているファイルのリスト)を使用することです。 macOSではデフォルトで使用可能ですが、パッケージマネージャーを使用してLinuxにインストールできます(まだインストールされていない場合)。

$ lsof -i -n
COMMAND     PID   USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
Python    67982 nathan    3u  IPv4 0xecf272      0t0  TCP *:65432 (LISTEN)

lsofは、-iオプションとともに使用すると、開いているインターネットソケットのCOMMANDPID(プロセスID)、およびUSER(ユーザーID)を提供します。 上記はエコーサーバープロセスです。

netstatlsofには多くのオプションがあり、それらを実行しているOSによって異なります。 両方のmanページまたはドキュメントを確認してください。 少し時間をかけて知り合う価値があります。 あなたは報われるでしょう。 macOSおよびLinuxでは、man netstatおよびman lsofを使用します。 Windowsの場合、netstat /?を使用します。

リスニングソケットのないポートに接続しようとすると表示される一般的なエラーは次のとおりです。

$ ./echo-client.py
Traceback (most recent call last):
  File "./echo-client.py", line 9, in 
    s.connect((HOST, PORT))
ConnectionRefusedError: [Errno 61] Connection refused

指定されたポート番号が間違っているか、サーバーが実行されていません。 または、接続をブロックしているパスにファイアウォールがあるかもしれませんが、これは簡単に忘れることができます。 エラーConnection timed outも表示される場合があります。 クライアントがTCPポートに接続できるようにするファイアウォールルールを追加します!

参照セクションに一般的なerrorsのリストがあります。

コミュニケーションの内訳

クライアントとサーバーが相互に通信する方法を詳しく見てみましょう。

Sockets loopback interface

loopbackインターフェイス(IPv4アドレス127.0.0.1またはIPv6アドレス::1)を使用する場合、データがホストを離れたり、外部ネットワークにアクセスしたりすることはありません。 上の図では、ループバックインターフェイスはホスト内に含まれています。 これは、ループバックインターフェイスの内部の性質を表し、それを通過する接続とデータはホストに対してローカルです。 これが、ループバックインターフェイスと「localhost」と呼ばれるIPアドレス127.0.0.1または::1も聞こえる理由です。

アプリケーションはループバックインターフェイスを使用して、ホストで実行されている他のプロセスと通信し、外部ネットワークからのセキュリティと分離を実現します。 内部にあり、ホスト内からのみアクセスできるため、公開されません。

独自のプライベートデータベースを使用するアプリケーションサーバーがある場合は、これを実際に見ることができます。 他のサーバーが使用するデータベースではない場合、おそらくループバックインターフェイスでのみ接続をリッスンするように構成されています。 この場合、ネットワーク上の他のホストは接続できません。

アプリケーションで127.0.0.1または::1以外のIPアドレスを使用する場合、外部ネットワークに接続されているEthernetインターフェースにバインドされている可能性があります。 これは、「localhost」王国以外のホストへのゲートウェイです。

Sockets ethernet interface

そこに注意してください。 厄介で残酷な世界です。 「localhost」の安全な範囲から離れる前に、必ずセクションUsing Hostnamesをお読みください。ホスト名を使用せず、IPアドレスのみを使用している場合でも適用されるセキュリティに関する注意事項があります。

複数の接続の処理

エコーサーバーには間違いなく制限があります。 最大の利点は、1つのクライアントのみにサービスを提供してから終了することです。 エコークライアントにもこの制限がありますが、追加の問題があります。 クライアントが次の呼び出しを行うと、s.recv()b'Hello, world'からb'H'の1バイトのみを返す可能性があります。

data = s.recv(1024)

上記で使用した1024bufsize引数は、一度に受信するデータの最大量です。 recv()1024バイトを返すという意味ではありません。

send()もこのように動作します。 send()は、送信されたバイト数を返します。これは、渡されたデータのサイズよりも小さい場合があります。 これを確認し、すべてのデータを送信するために必要な回数だけsend()を呼び出す必要があります。

「アプリケーションは、すべてのデータが送信されたことを確認する責任があります。一部のデータのみが送信された場合、アプリケーションは残りのデータの配信を試みる必要があります。」 (Source)

sendall()を使用して、これを行う必要を回避しました。

「send()とは異なり、このメソッドは、すべてのデータが送信されるかエラーが発生するまで、バイトからデータを送信し続けます。 成功しても何も返されません。」 (Source)

この時点で2つの問題があります。

  • 複数の接続を同時に処理するにはどうすればよいですか?

  • すべてのデータが送受信されるまで、send()recv()を呼び出す必要があります。

私たちは何をしますか? concurrencyには多くのアプローチがあります。 最近では、Asynchronous I/Oを使用するのが一般的なアプローチです。 asyncioは、Python3.4の標準ライブラリに導入されました。 従来の選択は、threadsを使用することです。

並行性の問題は、正しいことをするのが難しいことです。 考慮すべき多くの微妙な点があります。 必要なのは、これらのいずれかが現れることであり、アプリケーションはそれほど微妙ではない方法で突然失敗する可能性があります。

並行プログラミングの学習と使用からあなたを怖がらせるためにこれを言っているのではありません。 アプリケーションを拡張する必要がある場合、複数のプロセッサまたはコアを使用する必要があります。 ただし、このチュートリアルでは、スレッドよりも伝統的で推論しやすいものを使用します。 システムコールの祖父select()を使用します。

select()を使用すると、複数のソケットでI / Oの完了を確認できます。 したがって、select()を呼び出して、読み取りおよび/または書き込みの準備ができているI / Oを備えているソケットを確認できます。 しかし、これはPythonなので、他にもあります。 標準ライブラリのselectorsモジュールを使用するので、実行しているオペレーティングシステムに関係なく、最も効率的な実装が使用されます。

「このモジュールにより、選択モジュールのプリミティブに基づいて構築された高レベルで効率的なI / O多重化が可能になります。 使用するOSレベルのプリミティブを正確に制御したい場合を除いて、代わりにこのモジュールを使用することをお勧めします。」 (Source)

select()を使用することにより、同時に実行することはできませんが、ワークロードによっては、このアプローチは依然として十分に高速である可能性があります。 それは、リクエストを処理するときにアプリケーションが何をする必要があるかと、サポートする必要があるクライアントの数に依存します。

asyncioは、シングルスレッドの協調マルチタスクとイベントループを使用してタスクを管理します。 select()を使用して、より単純かつ同期的に、独自のバージョンのイベントループを作成します。 複数のスレッドを使用する場合、並行性がある場合でも、現在、GILCPython and PyPyを使用する必要があります。 これにより、とにかく並行して実行できる作業量が事実上制限されます。

select()を使用することは完全に良い選択かもしれないことを説明するために、これらすべてを言います。 asyncio、スレッド、または最新の非同期ライブラリを使用する必要があるとは思わないでください。 通常、ネットワークアプリケーションでは、アプリケーションはI / Oバウンドです。ローカルネットワーク、ネットワークの反対側のエンドポイント、ディスクなどで待機している可能性があります。

CPUバウンド作業を開始するクライアントからリクエストを受け取っている場合は、concurrent.futuresモジュールを確認してください。 これには、プロセスのプールを使用して呼び出しを非同期に実行するクラスProcessPoolExecutorが含まれています。

複数のプロセスを使用する場合、オペレーティングシステムは、GILを使用せずに、複数のプロセッサまたはコアで並列に実行するPythonコードをスケジュールできます。 アイデアとインスピレーションについては、PyConトークJohn Reese - Thinking Outside the GIL with AsyncIO and Multiprocessing - PyCon 2018を参照してください。

次のセクションでは、これらの問題に対処するサーバーとクライアントの例を見ていきます。 select()を使用して複数の接続を同時に処理し、send()recv()を必要な回数だけ呼び出します。

マルチ接続クライアントとサーバー

次の2つのセクションでは、selectorsモジュールから作成されたselectorオブジェクトを使用して、複数の接続を処理するサーバーとクライアントを作成します。

マルチ接続サーバー

まず、マルチ接続サーバーmulticonn-server.pyを見てみましょう。 リスニングソケットを設定する最初の部分は次のとおりです。

import selectors
sel = selectors.DefaultSelector()
# ...
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)

このサーバーとエコーサーバーの最大の違いは、lsock.setblocking(False)を呼び出して、ソケットを非ブロッキングモードで構成することです。 このソケットに対して行われた呼び出しは、blockではなくなります。 以下に示すように、sel.select()とともに使用すると、1つ以上のソケットでイベントを待機し、準備ができたらデータの読み取りと書き込みを行うことができます。

sel.register()は、関心のあるイベントについてsel.select()で監視するソケットを登録します。 リスニングソケットには、読み取りイベントselectors.EVENT_READが必要です。

dataは、ソケットとともに任意のデータを格納するために使用されます。 select()が戻ると返されます。 dataを使用して、ソケットで送受信されたものを追跡します。

次はイベントループです。

import selectors
sel = selectors.DefaultSelector()

# ...

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        if key.data is None:
            accept_wrapper(key.fileobj)
        else:
            service_connection(key, mask)

sel.select(timeout=None)blocksは、I / Oの準備ができているソケットができるまで続きます。 (キー、イベント)タプルのリスト、各ソケットに1つを返します。 keyは、fileobj属性を含むSelectorKeynamedtupleです。 key.fileobjはソケットオブジェクトであり、maskは準備ができている操作のイベントマスクです。

key.dataNoneの場合、それはリスニングソケットからのものであることがわかり、接続をaccept()する必要があります。 独自のaccept()ラッパー関数を呼び出して、新しいソケットオブジェクトを取得し、セレクターに登録します。 すぐに見ていきます。

key.dataNoneでない場合、それはすでに受け入れられているクライアントソケットであることがわかり、サービスを提供する必要があります。 次に、service_connection()が呼び出され、keymaskが渡されます。これには、ソケットで操作するために必要なすべてのものが含まれています。

accept_wrapper()関数が何をするか見てみましょう:

def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print('accepted connection from', addr)
    conn.setblocking(False)
    data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(conn, events, data=data)

リスニングソケットはイベントselectors.EVENT_READに登録されているため、読み取る準備ができているはずです。 sock.accept()を呼び出し、すぐにconn.setblocking(False)を呼び出して、ソケットを非ブロッキングモードにします。

このバージョンのサーバーでは、blockにしたくないため、これが主な目的であることを忘れないでください。 ブロックされると、サーバー全体が復帰するまで停止します。 これは、他のソケットが待機していることを意味します。 これは、サーバーを望ましくない状態にする「恐怖」状態です。

次に、クラスtypes.SimpleNamespaceを使用して、ソケットとともに含めたいデータを保持するオブジェクトを作成します。 クライアント接続の読み取りと書き込みの準備ができたことを知りたいので、これらのイベントは両方とも以下を使用して設定されます。

events = selectors.EVENT_READ | selectors.EVENT_WRITE

次に、eventsのマスク、ソケット、およびデータオブジェクトがsel.register()に渡されます。

次に、service_connection()を見て、準備ができたときにクライアント接続がどのように処理されるかを確認しましょう。

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            data.outb += recv_data
        else:
            print('closing connection to', data.addr)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if data.outb:
            print('echoing', repr(data.outb), 'to', data.addr)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]

これは、単純なマルチ接続サーバーの心臓部です。 keyは、ソケットオブジェクト(fileobj)とデータオブジェクトを含むselect()から返されるnamedtupleです。 maskには、準備ができているイベントが含まれています。

ソケットが読み取りの準備ができている場合、mask & selectors.EVENT_READはtrueであり、sock.recv()が呼び出されます。 読み取られたデータはすべてdata.outbに追加されるため、後で送信できます。

データが受信されない場合、else:ブロックに注意してください。

if recv_data:
    data.outb += recv_data
else:
    print('closing connection to', data.addr)
    sel.unregister(sock)
    sock.close()

これは、クライアントがソケットを閉じたことを意味するため、サーバーも閉じる必要があります。 ただし、最初にsel.unregister()を呼び出すことを忘れないでください。そうすると、select()によって監視されなくなります。

ソケットが書き込みの準備ができると(正常なソケットの場合は常にそうであるはずです)、data.outbに格納されている受信データは、sock.send()を使用してクライアントにエコーされます。 送信されたバイトは、送信バッファーから削除されます。

data.outb = data.outb[sent:]

マルチ接続クライアント

次に、マルチ接続クライアントmulticonn-client.pyを見てみましょう。 サーバーと非常によく似ていますが、接続をリッスンする代わりに、start_connections()を介して接続を開始することから始まります。

messages = [b'Message 1 from client.', b'Message 2 from client.']


def start_connections(host, port, num_conns):
    server_addr = (host, port)
    for i in range(0, num_conns):
        connid = i + 1
        print('starting connection', connid, 'to', server_addr)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setblocking(False)
        sock.connect_ex(server_addr)
        events = selectors.EVENT_READ | selectors.EVENT_WRITE
        data = types.SimpleNamespace(connid=connid,
                                     msg_total=sum(len(m) for m in messages),
                                     recv_total=0,
                                     messages=list(messages),
                                     outb=b'')
        sel.register(sock, events, data=data)

num_connsは、サーバーに対して作成する接続の数であるコマンドラインから読み取られます。 サーバーと同様に、各ソケットは非ブロックモードに設定されます。

connect()はすぐにBlockingIOError例外を発生させるため、connect()の代わりにconnect_ex()が使用されます。 connect_ex()は、接続の進行中に例外を発生させるのではなく、最初にエラーインジケータerrno.EINPROGRESSを返します。 接続が完了すると、ソケットは読み取りと書き込みの準備が整い、select()によってそのように返されます。

ソケットのセットアップ後、ソケットとともに保存するデータは、クラスtypes.SimpleNamespaceを使用して作成されます。 クライアントがサーバーに送信するメッセージは、各接続がsocket.send()を呼び出してリストを変更するため、list(messages)を使用してコピーされます。 クライアントが送信する必要があるもの、送受信したもの、およびメッセージの合計バイト数を追跡​​するために必要なすべてのものがオブジェクトdataに格納されます。

service_connection()を見てみましょう。 基本的にサーバーと同じです。

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            print('received', repr(recv_data), 'from connection', data.connid)
            data.recv_total += len(recv_data)
        if not recv_data or data.recv_total == data.msg_total:
            print('closing connection', data.connid)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if not data.outb and data.messages:
            data.outb = data.messages.pop(0)
        if data.outb:
            print('sending', repr(data.outb), 'to connection', data.connid)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]

重要な違いが1つあります。 サーバーから受信したバイト数を追跡​​するため、接続側を閉じることができます。 サーバーはこれを検出すると、接続の側も閉じます。

これを行うことにより、サーバーはクライアントが正常に動作していることに依存することに注意してください。サーバーは、メッセージの送信が完了したときにクライアントが接続の側を閉じることを期待します。 クライアントが閉じない場合、サーバーは接続を開いたままにします。 実際のアプリケーションでは、サーバーでこれを防止し、一定時間後にリクエストが送信されない場合にクライアント接続が蓄積しないようにすることができます。

マルチ接続クライアントおよびサーバーの実行

それでは、multiconn-server.pymulticonn-client.pyを実行してみましょう。 どちらもコマンドライン引数を使用します。 引数なしで実行して、オプションを表示できます。

サーバーの場合、hostおよびport番号を渡します。

$ ./multiconn-server.py
usage: ./multiconn-server.py  

クライアントの場合は、作成する接続の数num_connectionsもサーバーに渡します。

$ ./multiconn-client.py
usage: ./multiconn-client.py   

以下は、ポート65432でループバックインターフェイスをリッスンするときのサーバー出力です。

$ ./multiconn-server.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)
accepted connection from ('127.0.0.1', 61354)
accepted connection from ('127.0.0.1', 61355)
echoing b'Message 1 from client.Message 2 from client.' to ('127.0.0.1', 61354)
echoing b'Message 1 from client.Message 2 from client.' to ('127.0.0.1', 61355)
closing connection to ('127.0.0.1', 61354)
closing connection to ('127.0.0.1', 61355)

以下は、上のサーバーへの2つの接続を作成するときのクライアント出力です。

$ ./multiconn-client.py 127.0.0.1 65432 2
starting connection 1 to ('127.0.0.1', 65432)
starting connection 2 to ('127.0.0.1', 65432)
sending b'Message 1 from client.' to connection 1
sending b'Message 2 from client.' to connection 1
sending b'Message 1 from client.' to connection 2
sending b'Message 2 from client.' to connection 2
received b'Message 1 from client.Message 2 from client.' from connection 1
closing connection 1
received b'Message 1 from client.Message 2 from client.' from connection 2
closing connection 2

アプリケーションクライアントとサーバー

複数接続のクライアントとサーバーの例は、私たちが始めた場所と比べて間違いなく改善されています。 ただし、最後の実装での前の「multiconn」の例の欠点であるアプリケーションクライアントとサーバーにもう1つのステップを踏み、対処しましょう。

他の接続が影響を受けないように、エラーを適切に処理するクライアントとサーバーが必要です。 明らかに、例外がキャッチされない場合、クライアントまたはサーバーが激怒してクラッシュすることはありません。 これは今まで議論していないことです。 例では簡潔さと明瞭さのために、エラー処理を意図的に省略しました。

基本的なAPI、ノンブロッキングソケット、およびselect()について理解したので、エラー処理を追加して、大きなカーテンの後ろに隠しておいた「部屋の中の象」について説明します。あそこ。 はい、導入部で述べたカスタムクラスについて話しています。 忘れないでしょ

まず、エラーに対処しましょう。

「すべてのエラーは例外を発生させます。 無効な引数タイプとメモリ不足状態の通常の例外が発生する可能性があります。 Python 3.3以降、ソケットまたはアドレスのセマンティクスに関連するエラーにより、OSErrorまたはそのサブクラスの1つが発生します。」 (Source)

OSErrorをキャッチする必要があります。 エラーに関して言及していないもう1つのことは、タイムアウトです。 ドキュメントの多くの場所で議論されています。 タイムアウトが発生し、「通常の」エラーです。 ホストとルーターが再起動され、スイッチポートが不良になり、ケーブルが不良になり、ケーブルが外れます。 これらのエラーやその他のエラーに備え、コードでそれらを処理する必要があります。

「部屋の中の象」はどうですか?ソケットタイプsocket.SOCK_STREAMで示唆されているように、TCPを使用する場合は、バイトの連続ストリームから読み取っています。 ディスク上のファイルを読み取るようなものですが、代わりにネットワークからバイトを読み取ります。

ただし、ファイルの読み取りとは異なり、f.seek()はありません。 つまり、ソケットポインターがある場合は、その位置を変更することはできず、いつでも好きなときにデータを読み取りながらランダムに移動できます。

バイトがソケットに到着すると、ネットワークバッファが関係します。 それらを読んだら、どこかに保存する必要があります。 recv()を再度呼び出すと、ソケットから使用可能な次のバイトストリームが読み取られます。

これが意味することは、あなたは塊からソケットから読み取ることです。 アプリケーションにとって意味のある完全なメッセージを取得するのに十分なバイトを読み取るまで、recv()を呼び出し、データをバッファに保存する必要があります。

メッセージの境界がどこにあるかを定義し、追跡するのはあなた次第です。 TCPソケットに関する限り、ネットワークとの間で生のバイトを送受信するだけです。 これらの生バイトの意味については何も知りません。

これにより、アプリケーション層プロトコルを定義できます。 アプリケーション層プロトコルとは何ですか? 簡単に言えば、アプリケーションはメッセージを送受信します。 これらのメッセージは、アプリケーションのプロトコルです。

つまり、これらのメッセージに選択する長さと形式は、アプリケーションのセマンティクスと動作を定義します。 これは、ソケットからのバイトの読み取りに関する前の段落で説明した内容に直接関連しています。 recv()でバイトを読み取る場合は、読み取られたバイト数を把握し、メッセージの境界がどこにあるかを把握する必要があります。

これはどのように行われますか? 1つの方法は、常に固定長のメッセージを送信することです。 常に同じサイズであれば、簡単です。 そのバイト数をバッファーに読み込むと、1つの完全なメッセージがあることがわかります。

ただし、固定長のメッセージを使用することは、埋めるためにパディングを使用する必要がある小さなメッセージには非効率的です。 また、1つのメッセージに収まらないデータをどう処理するかという問題も残っています。

このチュートリアルでは、一般的なアプローチを取ります。 HTTPを含む多くのプロトコルで使用されているアプローチ。 コンテンツの長さや必要な他のフィールドを含むヘッダーをメッセージの前に付けます。 これにより、ヘッダーについていくだけで済みます。 ヘッダーを読み取った後、メッセージのコンテンツの長さを判断するためにヘッダーを処理し、そのバイト数を読み取ってそれを消費できます。

これを実装するには、テキストデータまたはバイナリデータを含むメッセージを送受信できるカスタムクラスを作成します。 独自のアプリケーション向けに改善および拡張できます。 最も重要なことは、これがどのように行われるかの例を見ることができるということです。

あなたに影響を与える可能性のあるソケットとバイトに関して何か言及する必要があります。 前に説明したように、ソケットを介してデータを送受信する場合、生のバイトを送受信しています。

データを受信し、4バイトの整数など、複数バイトとして解釈されるコンテキストで使用する場合は、マシンのCPUにネイティブではない形式である可能性があることを考慮する必要があります。 反対側のクライアントまたはサーバーには、自分のものとは異なるバイト順を使用するCPUが搭載されている場合があります。 その場合は、使用する前にホストのネイティブバイトオーダーに変換する必要があります。

このバイトオーダーは、CPUのendiannessと呼ばれます。 詳細については、リファレンスセクションのByte Endiannessを参照してください。 メッセージヘッダーにUnicodeを利用し、エンコードUTF-8を使用することにより、この問題を回避します。 UTF-8は8ビットエンコーディングを使用するため、バイトオーダーの問題はありません。

説明はPythonのEncodings and Unicodeドキュメントにあります。 これはテキストヘッダーにのみ適用されることに注意してください。 送信されるコンテンツのヘッダーで定義された明示的なタイプとエンコード、メッセージペイロードを使用します。 これにより、必要なデータ(テキストまたはバイナリ)を任意の形式で転送できます。

sys.byteorderを使用すると、マシンのバイト順序を簡単に判別できます。 たとえば、私のIntelラップトップでは、これが起こります。

$ python3 -c 'import sys; print(repr(sys.byteorder))'
'little'

emulatesがビッグエンディアンCPU(PowerPC)である仮想マシンでこれを実行すると、次のようになります。

$ python3 -c 'import sys; print(repr(sys.byteorder))'
'big'

このサンプルアプリケーションでは、アプリケーション層プロトコルは、ヘッダーをUTF-8エンコーディングのUnicodeテキストとして定義します。 メッセージの実際のコンテンツであるメッセージペイロードについては、必要に応じてバイトオーダーを手動で交換する必要があります。

これは、アプリケーションと、エンディアンネスが異なるマシンからのマルチバイトバイナリデータを処理する必要があるかどうかによって異なります。 HTTPと同様に、ヘッダーを追加し、それらを使用してパラメーターを渡すことにより、クライアントまたはサーバーがバイナリサポートを実装するのを支援できます。

これがまだ意味をなさない場合でも心配しないでください。 次のセクションでは、これらすべてがどのように機能し、どのように組み合わされるかを確認します。

アプリケーションプロトコルヘッダー

プロトコルヘッダーを完全に定義しましょう。 プロトコルヘッダーは次のとおりです。

  • 可変長テキスト

  • エンコードUTF-8を使用したUnicode

  • JSONを使用してシリアル化されたPython辞書

プロトコルヘッダーの辞書に必要なヘッダーまたはサブヘッダーは次のとおりです。

Name 説明

byteorder

マシンのバイト順序(sys.byteorderを使用)。 これは、アプリケーションでは必要ない場合があります。

content-length

コンテンツの長さ(バイト単位)。

content-type

ペイロード内のコンテンツのタイプ。たとえば、text/jsonまたはbinary/my-binary-type

content-encoding

コンテンツで使用されるエンコーディング。たとえば、Unicodeテキストの場合はutf-8、バイナリデータの場合はbinary

これらのヘッダーは、メッセージのペイロードのコンテンツについて受信者に通知します。 これにより、十分な情報を提供しながら任意のデータを送信できるため、受信者はコンテンツを正しくデコードおよび解釈できます。 ヘッダーは辞書にあるため、必要に応じてキー/値のペアを挿入することにより、ヘッダーを簡単に追加できます。

アプリケーションメッセージの送信

まだ少し問題があります。 可変長のヘッダーがあります。これは便利で柔軟性がありますが、recv()でヘッダーを読み取るときに、ヘッダーの長さをどのように知ることができますか?

以前にrecv()とメッセージ境界の使用について説明したときに、固定長ヘッダーは非効率的である可能性があると述べました。 それは事実ですが、長さを含むJSONヘッダーの前に、小さな2バイトの固定長ヘッダーを使用します。

これは、メッセージ送信のハイブリッドアプローチと考えることができます。 実際には、最初にヘッダーの長さを送信することにより、メッセージ受信プロセスをブートストラップします。 これにより、受信者はメッセージを簡単に分解できます。

メッセージ形式をよりよく理解するために、メッセージ全体を見てみましょう。

Sockets application message

メッセージは、ネットワークバイト順の整数である2バイトの固定長ヘッダーで始まります。 これは、次のヘッダーである可変長JSONヘッダーの長さです。 recv()で2バイトを読み取ったら、2バイトを整数として処理し、UTF-8JSONヘッダーをデコードする前にそのバイト数を読み取ることができることがわかります。

JSON headerには、追加のヘッダーの辞書が含まれています。 それらの1つはcontent-lengthです。これは、メッセージのコンテンツのバイト数です(JSONヘッダーは含まれません)。 recv()を呼び出してcontent-lengthバイトを読み取ると、メッセージの境界に到達し、メッセージ全体を読み取ります。

アプリケーションメッセージクラス

最後に、見返り! Messageクラスを見て、ソケットで読み取りおよび書き込みイベントが発生したときにselect()でどのように使用されるかを見てみましょう。

このサンプルアプリケーションでは、クライアントとサーバーが使用するメッセージの種類についてのアイデアを考え出す必要がありました。 この時点では、トイエコークライアントとサーバーをはるかに超えています。

物事をシンプルに保ちながら、実際のアプリケーションで物事がどのように機能するかを示すために、基本的な検索機能を実装するアプリケーションプロトコルを作成しました。 クライアントは検索要求を送信し、サーバーは一致の検索を行います。 クライアントから送信されたリクエストが検索として認識されない場合、サーバーはそれがバイナリリクエストであると見なし、バイナリレスポンスを返します。

以下のセクションを読み、例を実行し、コードを試した後、どのように機能するかがわかります。 次に、Messageクラスを開始点として使用し、自分で使用できるように変更できます。

私たちは、「multiconn」クライアントとサーバーの例からそれほど遠くありません。 イベントループコードは、app-client.pyapp-server.pyで同じままです。 私が行ったことは、メッセージコードをMessageという名前のクラスに移動し、ヘッダーとコンテンツの読み取り、書き込み、および処理をサポートするメソッドを追加することです。 これは、クラスを使用するための素晴らしい例です。

前に説明したように、以下で見るように、ソケットの操作には状態の保持が含まれます。 クラスを使用することにより、すべての状態、データ、およびコードをまとめて整理された単位にまとめます。 クラスのインスタンスは、接続が開始または受け入れられると、クライアントとサーバーの各ソケットに対して作成されます。

クラスは、ラッパーメソッドとユーティリティメソッドのクライアントとサーバーの両方でほとんど同じです。 Message._json_encode()のように、アンダースコアで始まります。 これらのメソッドは、クラスでの作業を簡素化します。 これらは、他の方法を短くし、DRYの原則をサポートできるようにすることで他の方法を支援します。

サーバーのMessageクラスは、基本的にクライアントのクラスと同じように機能し、その逆も同様です。 違いは、クライアントが接続を開始して要求メッセージを送信し、その後にサーバーの応答メッセージを処理することです。 逆に、サーバーは接続を待機し、クライアントの要求メッセージを処理してから、応答メッセージを送信します。

それはこのように見えます:

Step 終点 アクション/メッセージコンテンツ

1

クライアント

リクエストコンテンツを含むMessageを送信します

2

サーバ

クライアント要求Messageを受信して​​処理します

3

サーバ

応答コンテンツを含むMessageを送信します

4

クライアント

サーバー応答Messageを受信して​​処理します

ファイルとコードのレイアウトは次のとおりです。

応用 File Code

サーバ

app-server.py

サーバーのメインスクリプト

サーバ

libserver.py

サーバーのMessageクラス

クライアント

app-client.py

クライアントのメインスクリプト

クライアント

libclient.py

クライアントのMessageクラス

メッセージエントリポイント

Messageクラスがどのように機能するかについて、最初に、私にはすぐにはわからなかった設計の側面について説明したいと思います。 少なくとも5回リファクタリングして初めて、現在の状態に到達しました。 Why? 状態の管理。

Messageオブジェクトが作成されると、selector.register()を使用してイベントを監視するソケットに関連付けられます。

message = libserver.Message(sel, conn, addr)
sel.register(conn, selectors.EVENT_READ, data=message)

Note:このセクションのコード例の一部は、サーバーのメインスクリプトとMessageクラスからのものですが、このセクションと説明はクライアントにも同様に適用されます。 異なる場合はクライアントのバージョンを表示して説明します。

ソケットでイベントの準備ができると、selector.select()によって返されます。 次に、keyオブジェクトのdata属性を使用してメッセージオブジェクトへの参照を取得し、Messageのメソッドを呼び出すことができます。

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        # ...
        message = key.data
        message.process_events(mask)

上記のイベントループを見ると、sel.select()が運転席にあることがわかります。 ブロックされ、ループの先頭でイベントを待機しています。 ソケットで読み取りおよび書き込みイベントを処理する準備ができたときに起動する責任があります。 つまり、間接的に、メソッドprocess_events()の呼び出しも担当します。 これは、メソッドprocess_events()がエントリポイントであると言うときの意味です。

process_events()メソッドの機能を見てみましょう。

def process_events(self, mask):
    if mask & selectors.EVENT_READ:
        self.read()
    if mask & selectors.EVENT_WRITE:
        self.write()

それは良いことです。process_events()は単純です。 実行できるのは、read()write()の2つだけです。

これにより、管理状態に戻ります。 いくつかのリファクタリングの後、別のメソッドが特定の値を持つ状態変数に依存している場合、それらはread()write()からのみ呼び出されると判断しました。 これにより、イベントが処理のためにソケットに到着するので、ロジックが可能な限り単純になります。

これは明白に思えるかもしれませんが、クラスの最初の数回の反復は、現在の状態をチェックし、値に応じて、read()またはwrite()の外部のデータを処理するために他のメソッドを呼び出すいくつかのメソッドの組み合わせでした。 結局、これは管理が遅れずに対応するには複雑すぎることが判明しました。

自分のニーズに合わせてクラスを確実に変更して、最適に機能するようにする必要がありますが、状態チェックと、その状態に依存するメソッドの呼び出しをread()と%(t1 )可能であればメソッド。

read()を見てみましょう。 これはサーバーのバージョンですが、クライアントのバージョンは同じです。 process_request()の代わりにprocess_response()という別のメソッド名を使用するだけです。

def read(self):
    self._read()

    if self._jsonheader_len is None:
        self.process_protoheader()

    if self._jsonheader_len is not None:
        if self.jsonheader is None:
            self.process_jsonheader()

    if self.jsonheader:
        if self.request is None:
            self.process_request()

_read()メソッドが最初に呼び出されます。 socket.recv()を呼び出して、ソケットからデータを読み取り、受信バッファーに格納します。

socket.recv()が呼び出されたとき、完全なメッセージを構成するすべてのデータがまだ到着していない可能性があることに注意してください。 socket.recv()を再度呼び出す必要がある場合があります。 これが、メッセージを処理する適切なメソッドを呼び出す前に、メッセージの各部分の状態チェックがある理由です。

メソッドは、メッセージの一部を処理する前に、最初に十分なバイトが受信バッファーに読み込まれたことを確認します。 存在する場合、それぞれのバイトを処理し、バッファからそれらを削除し、次の処理段階で使用される変数に出力を書き込みます。 メッセージには3つのコンポーネントがあるため、3つの状態チェックとprocessメソッド呼び出しがあります。

メッセージコンポーネント 方法 出力

固定長ヘッダー

process_protoheader()

self._jsonheader_len

JSONヘッダー

process_jsonheader()

self.jsonheader

コンテンツ

process_request()

self.request

次に、write()を見てみましょう。 これはサーバーのバージョンです。

def write(self):
    if self.request:
        if not self.response_created:
            self.create_response()

    self._write()

write()は最初にrequestをチェックします。 存在し、応答が作成されていない場合、create_response()が呼び出されます。 create_response()は、状態変数response_createdを設定し、応答を送信バッファーに書き込みます。

送信バッファにデータがある場合、_write()メソッドはsocket.send()を呼び出します。

socket.send()が呼び出されたとき、送信バッファー内のすべてのデータが送信のためにキューに入れられていない可能性があることに注意してください。 ソケットのネットワークバッファがいっぱいである可能性があり、socket.send()を再度呼び出す必要がある場合があります。 これが状態チェックがある理由です。 create_response()は1回だけ呼び出す必要がありますが、_write()は複数回呼び出す必要があると予想されます。

write()のクライアントバージョンは似ています:

def write(self):
    if not self._request_queued:
        self.queue_request()

    self._write()

    if self._request_queued:
        if not self._send_buffer:
            # Set selector to listen for read events, we're done writing.
            self._set_selector_events_mask('r')

クライアントはサーバーへの接続を開始し、最初に要求を送信するため、状態変数_request_queuedがチェックされます。 リクエストがキューに入れられていない場合は、queue_request()を呼び出します。 queue_request()はリクエストを作成し、送信バッファに書き込みます。 また、状態変数_request_queuedを設定して、1回だけ呼び出されるようにします。

サーバーと同様に、送信バッファにデータがある場合、_write()socket.send()を呼び出します。

クライアントのバージョンのwrite()の顕著な違いは、リクエストがキューに入れられているかどうかを確認する最後のチェックです。 これについてはセクションClient Main Scriptで詳しく説明しますが、これはselector.select()に書き込みイベントのソケットの監視を停止するように指示するためです。 要求がキューに入れられていて、送信バッファーが空の場合、書き込みは完了しており、読み取りイベントのみに関心があります。 ソケットが書き込み可能であることを通知する理由はありません。

このセクションの最後に、考えを1つ残しておきます。 このセクションの主な目的は、selector.select()がメソッドprocess_events()を介してMessageクラスを呼び出していることを説明し、状態の管理方法を説明することでした。

process_events()は接続の存続期間中に何度も呼び出されるため、これは重要です。 したがって、一度だけ呼び出されるメソッドは、状態変数自体をチェックするか、メソッドによって設定された状態変数が呼び出し元によってチェックされるようにしてください。

サーバーメインスクリプト

サーバーのメインスクリプトapp-server.pyでは、リッスンするインターフェイスとポートを指定する引数がコマンドラインから読み取られます。

$ ./app-server.py
usage: ./app-server.py  

たとえば、ポート65432のループバックインターフェイスでリッスンするには、次のように入力します。

$ ./app-server.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)

すべてのインターフェイスでリッスンするには、<host>に空の文字列を使用します。

ソケットを作成した後、オプションsocket.SO_REUSEADDRを使用してsocket.setsockopt()が呼び出されます。

# Avoid bind() exception: OSError: [Errno 48] Address already in use
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

このソケットオプションを設定すると、エラーAddress already in useが回避されます。 これは、サーバーを起動すると、同じポートで以前に使用されたTCPソケットに、TIME_WAIT状態の接続がある場合に表示されます。

たとえば、サーバーがアクティブに接続を閉じた場合、オペレーティングシステムによっては、サーバーは2分以上TIME_WAIT状態のままになります。 TIME_WAIT状態が期限切れになる前にサーバーを再起動しようとすると、Address already in useOSError例外が発生します。 これは、ネットワーク内の遅延パケットが間違ったアプリケーションに配信されないようにするための安全策です。

イベントループはエラーをキャッチし、サーバーが稼働し続け、実行を継続できるようにします。

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        if key.data is None:
            accept_wrapper(key.fileobj)
        else:
            message = key.data
            try:
                message.process_events(mask)
            except Exception:
                print('main: error: exception for',
                      f'{message.addr}:\n{traceback.format_exc()}')
                message.close()

クライアント接続が受け入れられると、Messageオブジェクトが作成されます。

def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print('accepted connection from', addr)
    conn.setblocking(False)
    message = libserver.Message(sel, conn, addr)
    sel.register(conn, selectors.EVENT_READ, data=message)

Messageオブジェクトは、sel.register()の呼び出しでソケットに関連付けられ、最初は読み取りイベントのみを監視するように設定されています。 リクエストが読み取られたら、書き込みイベントのみをリッスンするように変更します。

サーバーでこのアプローチを採用する利点は、ほとんどの場合、ソケットが正常であり、ネットワークの問題がない場合、常に書き込み可能であることです。

sel.register()EVENT_WRITEも監視するように指示した場合、イベントループはすぐにウェイクアップし、これが当てはまることを通知します。 ただし、この時点では、ウェイクアップしてソケットでsend()を呼び出す理由はありません。 リクエストはまだ処理されていないため、送信する応答はありません。 これは貴重なCPUサイクルを消費し、無駄にします。

サーバーメッセージクラス

セクションMessage Entry Pointでは、process_events()を介してソケットイベントの準備ができたときに、Messageオブジェクトがどのように実行されるかを確認しました。 次に、ソケットでデータが読み取られ、メッセージのコンポーネントまたは一部がサーバーで処理される準備ができたときに何が起こるかを見てみましょう。

サーバーのメッセージクラスはlibserver.pyです。 source code on GitHubを見つけることができます。

メソッドは、メッセージの処理が行われる順序でクラスに表示されます。

サーバーが少なくとも2バイトを読み取った場合、固定長ヘッダーを処理できます。

def process_protoheader(self):
    hdrlen = 2
    if len(self._recv_buffer) >= hdrlen:
        self._jsonheader_len = struct.unpack('>H',
                                             self._recv_buffer[:hdrlen])[0]
        self._recv_buffer = self._recv_buffer[hdrlen:]

固定長ヘッダーは、JSONヘッダーの長さを含むネットワーク(ビッグエンディアン)バイトオーダーの2バイト整数です。 struct.unpack()は、値を読み取り、デコードして、self._jsonheader_lenに格納するために使用されます。 担当するメッセージの一部を処理した後、process_protoheader()はそのメッセージを受信バッファから削除します。

固定長ヘッダーと同様に、JSONヘッダーを格納するのに十分なデータが受信バッファーにある場合、同様に処理できます。

def process_jsonheader(self):
    hdrlen = self._jsonheader_len
    if len(self._recv_buffer) >= hdrlen:
        self.jsonheader = self._json_decode(self._recv_buffer[:hdrlen],
                                            'utf-8')
        self._recv_buffer = self._recv_buffer[hdrlen:]
        for reqhdr in ('byteorder', 'content-length', 'content-type',
                       'content-encoding'):
            if reqhdr not in self.jsonheader:
                raise ValueError(f'Missing required header "{reqhdr}".')

メソッドself._json_decode()は、JSONヘッダーをデコードして辞書に逆シリアル化するために呼び出されます。 JSONヘッダーはUTF-8エンコードのUnicodeとして定義されているため、utf-8は呼び出しでハードコーディングされます。 結果はself.jsonheaderに保存されます。 担当するメッセージの一部を処理した後、process_jsonheader()はそのメッセージを受信バッファから削除します。

次は、メッセージの実際のコンテンツまたはペイロードです。 self.jsonheaderのJSONヘッダーで記述されます。 content-lengthバイトが受信バッファーで使用可能な場合、要求を処理できます。

def process_request(self):
    content_len = self.jsonheader['content-length']
    if not len(self._recv_buffer) >= content_len:
        return
    data = self._recv_buffer[:content_len]
    self._recv_buffer = self._recv_buffer[content_len:]
    if self.jsonheader['content-type'] == 'text/json':
        encoding = self.jsonheader['content-encoding']
        self.request = self._json_decode(data, encoding)
        print('received request', repr(self.request), 'from', self.addr)
    else:
        # Binary or unknown content-type
        self.request = data
        print(f'received {self.jsonheader["content-type"]} request from',
              self.addr)
    # Set selector to listen for write events, we're done reading.
    self._set_selector_events_mask('w')

メッセージの内容をdata変数に保存した後、process_request()はそれを受信バッファーから削除します。 次に、コンテンツタイプがJSONの場合、それをデコードし、逆シリアル化します。 そうでない場合、このサンプルアプリケーションでは、バイナリリクエストであると想定し、コンテンツタイプを単に印刷します。

process_request()が最後に行うことは、書き込みイベントのみを監視するようにセレクターを変更することです。 サーバーのメインスクリプトapp-server.pyでは、ソケットは最初は読み取りイベントのみを監視するように設定されています。 リクエストが完全に処理されたので、読む必要がなくなりました。

これで、応答を作成してソケットに書き込むことができます。 ソケットが書き込み可能である場合、create_response()write()から呼び出されます。

def create_response(self):
    if self.jsonheader['content-type'] == 'text/json':
        response = self._create_response_json_content()
    else:
        # Binary or unknown content-type
        response = self._create_response_binary_content()
    message = self._create_message(**response)
    self.response_created = True
    self._send_buffer += message

応答は、コンテンツタイプに応じて他のメソッドを呼び出すことによって作成されます。 このサンプルアプリケーションでは、action == 'search'のときにJSONリクエストに対して単純なディクショナリルックアップが実行されます。 ここで呼び出される他のメソッドを独自のアプリケーションに定義できます。

応答メッセージを作成した後、状態変数self.response_createdが設定されるため、write()create_response()を再度呼び出すことはありません。 最後に、応答が送信バッファーに追加されます。 これは_write()によって表示され、送信されます。

理解するのが難しいのは、応答が書き込まれた後に接続を閉じる方法です。 メソッド_write()close()の呼び出しを行います。

def _write(self):
    if self._send_buffer:
        print('sending', repr(self._send_buffer), 'to', self.addr)
        try:
            # Should be ready to write
            sent = self.sock.send(self._send_buffer)
        except BlockingIOError:
            # Resource temporarily unavailable (errno EWOULDBLOCK)
            pass
        else:
            self._send_buffer = self._send_buffer[sent:]
            # Close when the buffer is drained. The response has been sent.
            if sent and not self._send_buffer:
                self.close()

多少「隠されている」ものの、Messageクラスが接続ごとに1つのメッセージしか処理しないことを考えると、許容できるトレードオフだと思います。 応答が書き込まれた後、サーバーが行うことは何もありません。 作業が完了しました。

クライアントメインスクリプト

クライアントのメインスクリプトapp-client.pyでは、引数がコマンドラインから読み取られ、リクエストを作成してサーバーへの接続を開始するために使用されます。

$ ./app-client.py
usage: ./app-client.py    

例を示しましょう。

$ ./app-client.py 127.0.0.1 65432 search needle

コマンドライン引数からの要求を表すディクショナリを作成した後、ホスト、ポート、および要求ディクショナリがstart_connection()に渡されます。

def start_connection(host, port, request):
    addr = (host, port)
    print('starting connection to', addr)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setblocking(False)
    sock.connect_ex(addr)
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    message = libclient.Message(sel, sock, addr, request)
    sel.register(sock, events, data=message)

requestディクショナリを使用して、サーバー接続とMessageオブジェクト用のソケットが作成されます。

サーバーと同様に、Messageオブジェクトは、sel.register()の呼び出しでソケットに関連付けられます。 ただし、クライアントの場合、ソケットは最初に読み取りイベントと書き込みイベントの両方を監視するように設定されています。 リクエストが書き込まれると、読み取りイベントのみをリッスンするように変更します。

このアプローチには、サーバーと同じ利点があります。CPUサイクルを無駄にしないことです。 リクエストが送信された後、書き込みイベントに関心がなくなったため、イベントを起動して処理する理由はありません。

クライアントメッセージクラス

セクションMessage Entry Pointでは、process_events()を介してソケットイベントの準備ができたときに、メッセージオブジェクトがどのように呼び出されて動作するかを確認しました。 次に、ソケットでデータが読み書きされ、クライアントがメッセージを処理できる状態になった後の動作を見てみましょう。

クライアントのメッセージクラスはlibclient.pyです。 source code on GitHubを見つけることができます。

メソッドは、メッセージの処理が行われる順序でクラスに表示されます。

クライアントの最初のタスクは、リクエストをキューに入れることです:

def queue_request(self):
    content = self.request['content']
    content_type = self.request['type']
    content_encoding = self.request['encoding']
    if content_type == 'text/json':
        req = {
            'content_bytes': self._json_encode(content, content_encoding),
            'content_type': content_type,
            'content_encoding': content_encoding
        }
    else:
        req = {
            'content_bytes': content,
            'content_type': content_type,
            'content_encoding': content_encoding
        }
    message = self._create_message(**req)
    self._send_buffer += message
    self._request_queued = True

コマンドラインで渡された内容に応じて、リクエストの作成に使用されるディクショナリは、クライアントのメインスクリプトapp-client.pyにあります。 Messageオブジェクトが作成されると、要求ディクショナリが引数としてクラスに渡されます。

要求メッセージが作成され、送信バッファに追加されます。送信バッファは、_write()によって表示され、送信されます。 状態変数self._request_queuedが設定されているため、queue_request()は再度呼び出されません。

要求が送信された後、クライアントはサーバーからの応答を待ちます。

クライアントでメッセージを読み取り、処理する方法はサーバーと同じです。 応答データがソケットから読み取られると、processヘッダーメソッドが呼び出されます:process_protoheader()およびprocess_jsonheader()

違いは、最終的なprocessメソッドの名前と、それらが応答を作成するのではなく処理しているという事実です:process_response()_process_response_json_content()、および_process_response_binary_content()

最後に、もちろん重要なことですが、process_response()の最後の呼び出しです。

def process_response(self):
    # ...
    # Close when response has been processed
    self.close()
メッセージクラスのまとめ

Messageクラスのディスカッションを締めくくるには、いくつかのサポート方法で注意すべき重要な点をいくつか挙げます。

クラスによって発生した例外は、メインスクリプトのexcept句でキャッチされます。

try:
    message.process_events(mask)
except Exception:
    print('main: error: exception for',
          f'{message.addr}:\n{traceback.format_exc()}')
    message.close()

最後の行に注意してください:message.close()

これは、複数の理由から非常に重要な行です! ソケットが閉じていることを確認するだけでなく、message.close()はソケットをselect()による監視から削除します。 これにより、クラス内のコードが大幅に簡素化され、複雑さが軽減されます。 例外がある場合、または明示的に例外を発生させた場合は、close()がクリーンアップを処理することがわかっています。

メソッドMessage._read()およびMessage._write()にも、興味深いものが含まれています。

def _read(self):
    try:
        # Should be ready to read
        data = self.sock.recv(4096)
    except BlockingIOError:
        # Resource temporarily unavailable (errno EWOULDBLOCK)
        pass
    else:
        if data:
            self._recv_buffer += data
        else:
            raise RuntimeError('Peer closed.')

except行:except BlockingIOError:

_write()にも1つあります。 これらの行は、一時的なエラーをキャッチし、passを使用してスキップするため重要です。 一時的なエラーは、ソケットがblockになる場合です。たとえば、ネットワークまたは接続のもう一方の端(ピア)で待機している場合などです。

passで例外をキャッチしてスキップすることにより、select()は最終的に再度呼び出しを行い、データの読み取りまたは書き込みを行う機会が再び得られます。

アプリケーションクライアントとサーバーの実行

この大変な作業をすべて終えた後、楽しみながら検索を実行しましょう。

これらの例では、サーバーを実行して、host引数に空の文字列を渡すことですべてのインターフェイスをリッスンします。 これにより、クライアントを実行し、別のネットワーク上にある仮想マシンから接続できます。 ビッグエンディアンのPowerPCマシンをエミュレートします。

まず、サーバーを起動しましょう。

$ ./app-server.py '' 65432
listening on ('', 65432)

クライアントを実行して検索を入力しましょう。 彼を見つけることができるかどうか見てみましょう:

$ ./app-client.py 10.0.1.1 65432 search morpheus
starting connection to ('10.0.1.1', 65432)
sending b'\x00d{"byteorder": "big", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 41}{"action": "search", "value": "morpheus"}' to ('10.0.1.1', 65432)
received response {'result': 'Follow the white rabbit. 🐰'} from ('10.0.1.1', 65432)
got result: Follow the white rabbit. 🐰
closing connection to ('10.0.1.1', 65432)

私の端末はUnicode(UTF-8)のテキストエンコーディングを使用するシェルを実行しているため、上記の出力は絵文字でうまく印刷されます。

子犬が見つかるかどうか見てみましょう:

$ ./app-client.py 10.0.1.1 65432 search 🐶
starting connection to ('10.0.1.1', 65432)
sending b'\x00d{"byteorder": "big", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 37}{"action": "search", "value": "\xf0\x9f\x90\xb6"}' to ('10.0.1.1', 65432)
received response {'result': '🐾 Playing ball! 🏐'} from ('10.0.1.1', 65432)
got result: 🐾 Playing ball! 🏐
closing connection to ('10.0.1.1', 65432)

sending行の要求に対してネットワーク経由で送信されたバイト文字列に注意してください。 子犬の絵文字を表す16進数で印刷されたバイト(🐶)を探すと、簡単に確認できます。 私の端末はエンコードUTF-8でUnicodeを使用しているので、検索にenter the emojiを実行できました。

これは、ネットワークを介して生のバイトを送信しており、正しく解釈するには受信側でデコードする必要があることを示しています。 これが、コンテンツタイプとエンコードを含むヘッダーを作成するためにすべての問題に取り組んだ理由です。

上記の両方のクライアント接続からのサーバー出力は次のとおりです。

accepted connection from ('10.0.2.2', 55340)
received request {'action': 'search', 'value': 'morpheus'} from ('10.0.2.2', 55340)
sending b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 43}{"result": "Follow the white rabbit. \xf0\x9f\x90\xb0"}' to ('10.0.2.2', 55340)
closing connection to ('10.0.2.2', 55340)

accepted connection from ('10.0.2.2', 55338)
received request {'action': 'search', 'value': '🐶'} from ('10.0.2.2', 55338)
sending b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 37}{"result": "\xf0\x9f\x90\xbe Playing ball! \xf0\x9f\x8f\x90"}' to ('10.0.2.2', 55338)
closing connection to ('10.0.2.2', 55338)

sending行を見て、クライアントのソケットに書き込まれたバイトを確認します。 これはサーバーの応答メッセージです。

action引数がsearch以外の場合は、サーバーへのバイナリ要求の送信をテストすることもできます。

$ ./app-client.py 10.0.1.1 65432 binary 😃
starting connection to ('10.0.1.1', 65432)
sending b'\x00|{"byteorder": "big", "content-type": "binary/custom-client-binary-type", "content-encoding": "binary", "content-length": 10}binary\xf0\x9f\x98\x83' to ('10.0.1.1', 65432)
received binary/custom-server-binary-type response from ('10.0.1.1', 65432)
got response: b'First 10 bytes of request: binary\xf0\x9f\x98\x83'
closing connection to ('10.0.1.1', 65432)

リクエストのcontent-typetext/jsonではないため、サーバーはリクエストをカスタムバイナリタイプとして扱い、JSONデコードを実行しません。 単にcontent-typeを出力し、最初の10バイトをクライアントに返します。

$ ./app-server.py '' 65432
listening on ('', 65432)
accepted connection from ('10.0.2.2', 55320)
received binary/custom-client-binary-type request from ('10.0.2.2', 55320)
sending b'\x00\x7f{"byteorder": "little", "content-type": "binary/custom-server-binary-type", "content-encoding": "binary", "content-length": 37}First 10 bytes of request: binary\xf0\x9f\x98\x83' to ('10.0.2.2', 55320)
closing connection to ('10.0.2.2', 55320)

トラブルシューティング

必然的に、何かが機能しなくなり、あなたは何をすべきか疑問に思うでしょう。 心配しないで、それは私たち全員に起こります。 願わくば、このチュートリアル、デバッガ、お気に入りの検索エンジンの助けを借りて、ソースコードの部分に戻っていただけることを願っています。

そうでない場合、最初に立ち寄るのはPythonのsocket moduleドキュメントです。 呼び出している各関数またはメソッドのすべてのドキュメントを必ず読んでください。 また、アイデアについては、Referenceセクションをお読みください。 特に、Errorsセクションを確認してください。

時には、ソースコードだけではないこともあります。 ソースコードが正しい場合がありますが、それは他のホスト、クライアント、またはサーバーだけです。 または、ルーター、ファイアウォール、または中間者を演じる他のネットワークデバイスなどのネットワークでもかまいません。

これらのタイプの問題には、追加のツールが不可欠です。 以下は、いくつかの手がかりを提供する、または少なくとも提供するいくつかのツールとユーティリティです。

ping

pingは、ICMPエコー要求を送信することにより、ホストが稼働していてネットワークに接続されているかどうかを確認します。 オペレーティングシステムのTCP / IPプロトコルスタックと直接通信するため、ホストで実行されているアプリケーションとは独立して動作します。

以下は、macOSでpingを実行する例です。

$ ping -c 3 127.0.0.1
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.058 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.165 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.164 ms

--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.058/0.129/0.165/0.050 ms

出力の最後の統計に注意してください。 これは、断続的な接続の問題を発見しようとしているときに役立ちます。 たとえば、パケット損失はありますか? 遅延はどれくらいありますか(往復時間を参照)。

あなたと他のホストの間にファイアウォールがある場合、pingのエコーリクエストは許可されない可能性があります。 一部のファイアウォール管理者は、これを実施するポリシーを実装しています。 ホストが発見されることを望まないという考えです。 その場合、ホストが通信できるようにファイアウォールルールを追加している場合は、ルールがICMPがホスト間を通過することも許可していることを確認してください。

ICMPは、pingによって使用されるプロトコルですが、TCPおよびその他の低レベルプロトコルがエラーメッセージの通信に使用するプロトコルでもあります。 奇妙な動作や遅い接続が発生している場合、これが原因である可能性があります。

ICMPメッセージは、タイプとコードによって識別されます。 彼らが運ぶ重要な情報のアイデアをあなたに与えるために、ここにいくつかあります:

ICMPタイプ ICMPコード 説明

8

0

エコーリクエスト

0

0

エコー返信

3

0

宛先ネットワークに到達できません

3

1

宛先ホストに到達できません

3

2

宛先プロトコルに到達できません

3

3

宛先ポートに到達できません

3

4

断片化が必要で、DFフラグが設定されています

11

0

TTLは転送中に期限切れになりました

断片化とICMPメッセージに関する情報については、記事Path MTU Discoveryを参照してください。 これは、前述した奇妙な動作を引き起こす可能性のあるものの例です。

ネットスタット

セクションViewing Socket Stateでは、netstatを使用してソケットとその現在の状態に関する情報を表示する方法について説明しました。 このユーティリティは、macOS、Linux、およびWindowsで使用できます。

出力例では、列Recv-QSend-Qについては触れませんでした。 これらの列には、送信または受信のためにキューに入れられているネットワークバッファーに保持されているバイト数が表示されますが、何らかの理由でリモートまたはローカルアプリケーションによって読み書きされていません。

つまり、バイトはオペレーティングシステムのキュー内のネットワークバッファーで待機しています。 1つの理由は、アプリケーションがCPUにバインドされているか、socket.recv()またはsocket.send()を呼び出してバイトを処理できないことである可能性があります。 または、輻輳、ネットワークハードウェアの障害、ケーブル接続など、通信に影響するネットワークの問題がある可能性があります。

これを実証し、エラーが発生する前に送信できるデータ量を確認するために、テストサーバーに接続してsocket.send()を繰り返し呼び出すテストクライアントを作成しました。 テストサーバーがsocket.recv()を呼び出すことはありません。 接続を受け入れるだけです。 これにより、サーバー上のネットワークバッファーがいっぱいになり、最終的にクライアントでエラーが発生します。

まず、サーバーを起動しました。

$ ./app-server-test.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)

次に、クライアントを実行しました。 エラーが何であるか見てみましょう:

$ ./app-client-test.py 127.0.0.1 65432 binary test
error: socket.send() blocking io exception for ('127.0.0.1', 65432):
BlockingIOError(35, 'Resource temporarily unavailable')

これは、クライアントとサーバーがまだ実行されている間のnetstatの出力であり、クライアントは上記のエラーメッセージを複数回出力します。

$ netstat -an | grep 65432
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4  408300      0  127.0.0.1.65432        127.0.0.1.53225        ESTABLISHED
tcp4       0 269868  127.0.0.1.53225        127.0.0.1.65432        ESTABLISHED
tcp4       0      0  127.0.0.1.65432        *.*                    LISTEN

最初のエントリはサーバーです(Local Addressのポート65432):

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4  408300      0  127.0.0.1.65432        127.0.0.1.53225        ESTABLISHED

Recv-Q408300に注意してください。

2番目のエントリはクライアントです(Foreign Addressにはポート65432があります):

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0 269868  127.0.0.1.53225        127.0.0.1.65432        ESTABLISHED

Send-Q269868に注意してください。

クライアントは確かにバイトを書き込もうとしていましたが、サーバーはそれらを読み取っていませんでした。 これにより、サーバーのネットワークバッファキューが受信側でいっぱいになり、クライアントのネットワークバッファキューが送信側でいっぱいになりました。

Windows

Windowsを使用している場合は、まだチェックしていない場合は必ずチェックする必要のある一連のユーティリティがあります:Windows Sysinternals

それらの1つはTCPView.exeです。 TCPViewは、Windows用のグラフィカルなnetstatです。 アドレス、ポート番号、ソケット状態に加えて、送受信されたパケットとバイトの数の合計を表示します。 Unixユーティリティlsofと同様に、プロセス名とIDも取得します。 他の表示オプションについては、メニューを確認してください。

TCPView screenshot

ワイヤーシャーク

ワイヤーで何が起こっているかを確認する必要がある場合があります。 アプリケーションログの内容や、ライブラリ呼び出しから返される値は忘れてください。 ネットワーク上で実際に送信または受信されているものを確認したい場合。 デバッガーと同じように、表示する必要がある場合に代わるものはありません。

Wiresharkは、macOS、Linux、Windowsなどで実行されるネットワークプロトコルアナライザーおよびトラフィックキャプチャアプリケーションです。 wiresharkという名前のGUIバージョンと、tsharkという名前のターミナルのテキストベースバージョンがあります。

トラフィックキャプチャの実行は、アプリケーションがネットワーク上でどのように動作するかを監視し、送受信するもの、および頻度と量に関する証拠を収集するための優れた方法です。 また、クライアントまたはサーバーが接続を閉じたり中断したり、応答を停止したりすることも確認できます。 この情報は、トラブルシューティングを行う際に非常に役立ちます。

WiresharkとTSharkの基本的な使用方法を説明する優れたチュートリアルやその他のリソースがWeb上に多数あります。

ループバックインターフェイスでWiresharkを使用したトラフィックキャプチャの例を次に示します。

Wireshark screenshot

tsharkを使用した上記と同じ例を次に示します。

$ tshark -i lo0 'tcp port 65432'
Capturing on 'Loopback'
    1   0.000000    127.0.0.1 → 127.0.0.1    TCP 68 53942 → 65432 [SYN] Seq=0 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=0 SACK_PERM=1
    2   0.000057    127.0.0.1 → 127.0.0.1    TCP 68 65432 → 53942 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=940533635 SACK_PERM=1
    3   0.000068    127.0.0.1 → 127.0.0.1    TCP 56 53942 → 65432 [ACK] Seq=1 Ack=1 Win=408288 Len=0 TSval=940533635 TSecr=940533635
    4   0.000075    127.0.0.1 → 127.0.0.1    TCP 56 [TCP Window Update] 65432 → 53942 [ACK] Seq=1 Ack=1 Win=408288 Len=0 TSval=940533635 TSecr=940533635
    5   0.000216    127.0.0.1 → 127.0.0.1    TCP 202 53942 → 65432 [PSH, ACK] Seq=1 Ack=1 Win=408288 Len=146 TSval=940533635 TSecr=940533635
    6   0.000234    127.0.0.1 → 127.0.0.1    TCP 56 65432 → 53942 [ACK] Seq=1 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
    7   0.000627    127.0.0.1 → 127.0.0.1    TCP 204 65432 → 53942 [PSH, ACK] Seq=1 Ack=147 Win=408128 Len=148 TSval=940533635 TSecr=940533635
    8   0.000649    127.0.0.1 → 127.0.0.1    TCP 56 53942 → 65432 [ACK] Seq=147 Ack=149 Win=408128 Len=0 TSval=940533635 TSecr=940533635
    9   0.000668    127.0.0.1 → 127.0.0.1    TCP 56 65432 → 53942 [FIN, ACK] Seq=149 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
   10   0.000682    127.0.0.1 → 127.0.0.1    TCP 56 53942 → 65432 [ACK] Seq=147 Ack=150 Win=408128 Len=0 TSval=940533635 TSecr=940533635
   11   0.000687    127.0.0.1 → 127.0.0.1    TCP 56 [TCP Dup ACK 6#1] 65432 → 53942 [ACK] Seq=150 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
   12   0.000848    127.0.0.1 → 127.0.0.1    TCP 56 53942 → 65432 [FIN, ACK] Seq=147 Ack=150 Win=408128 Len=0 TSval=940533635 TSecr=940533635
   13   0.001004    127.0.0.1 → 127.0.0.1    TCP 56 65432 → 53942 [ACK] Seq=150 Ack=148 Win=408128 Len=0 TSval=940533635 TSecr=940533635
^C13 packets captured

参照

このセクションは、追加情報および外部リソースへのリンクを含む一般的なリファレンスとして機能します。

Pythonドキュメント

エラー

以下は、Pythonのsocketモジュールのドキュメントからのものです。

「すべてのエラーは例外を発生させます。 無効な引数タイプとメモリ不足状態の通常の例外が発生する可能性があります。 Python 3.3以降、ソケットまたはアドレスのセマンティクスに関連するエラーにより、OSErrorまたはそのサブクラスの1つが発生します。」 (Source)

ソケットの操作中に発生する可能性のある一般的なエラーを次に示します。

例外 errno定数 説明

BlockingIOError

EWOULDBLOCK

リソースは一時的に利用できません。 たとえば、非ブロッキングモードでは、send()を呼び出していて、ピアがビジーで読み取りを行っていない場合、送信キュー(ネットワークバッファ)がいっぱいです。 または、ネットワークに問題があります。 うまくいけば、これは一時的な状態です。

OSError

EADDRINUSE

すでに使用されているアドレス。 同じポート番号を使用している別のプロセスが実行されておらず、サーバーがソケットオプションSO_REUSEADDRsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)を設定していることを確認してください。

ConnectionResetError

ECONNRESET

ピアによって接続がリセットされました。 リモートプロセスがクラッシュしたか、ソケットを適切に閉じませんでした(クリーンでないシャットダウン)。 または、ルールが欠落しているか、誤動作しているファイアウォールまたはその他のデバイスがネットワークパスにあります。

TimeoutError

ETIMEDOUT

操作がタイムアウトしました。 ピアからの応答がありません。

ConnectionRefusedError

ECONNREFUSED

接続拒否。 指定されたポートでリッスンしているアプリケーションはありません。

ソケットアドレスファミリ

socket.AF_INETsocket.AF_INET6は、socket.socket()の最初の引数に使用されるアドレスとプロトコルファミリを表します。 アドレスを使用するAPIは、ソケットがsocket.AF_INETまたはsocket.AF_INET6のどちらで作成されたかに応じて、特定の形式であると想定します。

住所家族 プロトコル アドレスタプル 説明

socket.AF_INET

IPv4

(host, port)

hostは、'www.example.com'のようなホスト名または'10.1.2.3'のようなIPv4アドレスを持つ文字列です。 portは整数です。

socket.AF_INET6

IPv6

(host, port, flowinfo, scopeid)

hostは、'www.example.com'のようなホスト名または'fe80::6203:7ab:fe88:9c23'のようなIPv6アドレスを持つ文字列です。 portは整数です。 flowinfoおよびscopeidは、C構造体sockaddr_in6sin6_flowinfoおよびsin6_scope_idメンバーを表します。

アドレスタプルのhost値に関する、Pythonのソケットモジュールのドキュメントからの以下の抜粋に注意してください。

「IPv4アドレスの場合、ホストアドレスの代わりに2つの特別な形式が受け入れられます。空の文字列はINADDR_ANYを表し、文字列'<broadcast>'INADDR_BROADCASTを表します。 この動作はIPv6と互換性がないため、PythonプログラムでIPv6をサポートする場合は、これらを回避することをお勧めします。」 (Source)

詳細については、PythonのSocket families documentationを参照してください。

このチュートリアルではIPv4ソケットを使用しましたが、ネットワークでサポートされている場合は、可能であればIPv6をテストして使用してみてください。 これを簡単にサポートする1​​つの方法は、関数socket.getaddrinfo()を使用することです。 hostおよびport引数を、そのサービスに接続されたソケットを作成するために必要なすべての引数を含む5タプルのシーケンスに変換します。 socket.getaddrinfo()は、IPv4に加えて、渡されたIPv6アドレスとIPv6アドレスに解決されるホスト名を理解して解釈します。

次の例では、ポート80example.orgへのTCP接続のアドレス情報を返します。

>>>

>>> socket.getaddrinfo("example.org", 80, proto=socket.IPPROTO_TCP)
[(, ,
 6, '', ('2606:2800:220:1:248:1893:25c8:1946', 80, 0, 0)),
 (, ,
 6, '', ('93.184.216.34', 80))]

IPv6が有効になっていない場合、システムによって結果が異なる場合があります。 上記で返された値は、socket.socket()およびsocket.connect()に渡すことで使用できます。 PythonのソケットモジュールのドキュメントのExample sectionに、クライアントとサーバーの例があります。

ホスト名の使用

コンテキストとして、このセクションは主に、ループバックインターフェイス「localhost」を使用する場合に、bind()connect()、またはconnect_ex()でホスト名を使用する場合に適用されます。ただし、ホスト名を使用している場合はいつでも適用され、特定のアドレスに解決され、アプリケーションにとってその動作や前提に影響を与える特別な意味を持つことが期待されます。 これは、ホスト名を使用して、www.example.comなどのDNSによって解決されるサーバーに接続するクライアントの典型的なシナリオとは対照的です。

以下は、Pythonのsocketモジュールのドキュメントからのものです。

「IPv4 / v6ソケットアドレスのホスト部分でホスト名を使用すると、PythonはDNS解決から返された最初のアドレスを使用するため、プログラムが非決定的な動作を示す場合があります。 ソケットアドレスは、DNS解決やホスト構成の結果に応じて、実際のIPv4 / v6アドレスに異なる方法で解決されます。 決定論的な動作には、ホスト部分に数値アドレスを使用してください。」 (Source)

「https://en.wikipedia.org/wiki/Localhost[localhost]」という名前の標準的な規則では、ループバックインターフェイスである127.0.0.1または::1に解決されます。 これはおそらくあなたのシステムの場合に当てはまりますが、そうではないかもしれません。 システムが名前解決のためにどのように構成されているかによります。 ITのあらゆるものと同様に、例外は常に存在し、「localhost」という名前を使用してループバックインターフェイスに接続する保証はありません。

たとえば、Linuxでは、Name Service Switch構成ファイルのman nsswitch.confを参照してください。 macOSとLinuxをチェックするもう1つの場所は、ファイル/etc/hostsです。 Windowsでは、C:\Windows\System32\drivers\etc\hostsを参照してください。 hostsファイルには、単純なテキスト形式でマッピングをアドレス指定するための名前の静的テーブルが含まれています。 DNSは、まったく別のパズルのピースです。

興味深いことに、この記事の執筆時点(2018年6月)には、「localhost」という名前の使用に関する規則、前提条件、およびセキュリティについて説明しているRFCドラフトLet ‘localhost’ be localhostがあります。

理解することが重要なのは、アプリケーションでホスト名を使用する場合、返されるアドレスは文字通り何でもかまいません。 セキュリティに敏感なアプリケーションを使用している場合、名前に関して仮定をしないでください。 アプリケーションと環境に応じて、これは懸念事項である場合とそうでない場合があります。

Note:アプリケーションが「セキュリティに敏感」でない場合でも、セキュリティに関する注意事項とベストプラクティスが引き続き適用されます。アプリケーションがネットワークにアクセスする場合は、セキュリティで保護して維持する必要があります。 これは、少なくとも以下を意味します。

  • Pythonを含むシステムソフトウェアの更新とセキュリティパッチが定期的に適用されます。 サードパーティのライブラリを使用していますか? その場合は、それらもチェックおよび更新されていることを確認してください。

  • 可能であれば、専用またはホストベースのファイアウォールを使用して、信頼できるシステムへの接続のみを制限してください。

  • どのDNSサーバーが構成されていますか? 彼らとその管理者を信頼していますか?

  • 要求データは、それを処理する他のコードを呼び出す前に、可能な限りサニタイズおよび検証されていることを確認してください。 これには(ファズ)テストを使用し、定期的に実行してください。

ホスト名を使用しているかどうかに関係なく、アプリケーションで安全な接続(暗号化と認証)をサポートする必要がある場合は、TLSの使用を検討することをお勧めします。 これは独自のトピックであり、このチュートリアルの範囲外です。 開始するには、Pythonのssl module documentationを参照してください。 これは、WebブラウザーがWebサイトに安全に接続するために使用するプロトコルと同じです。

考慮すべきインターフェイス、IPアドレス、および名前解決では、多くの変数があります。 あなたは何をするべきか? ネットワークアプリケーションのレビュープロセスがない場合に使用できる推奨事項を次に示します。

応用 使用法 勧告

サーバ

ループバックインターフェイス

127.0.0.1::1などのIPアドレスを使用します。

サーバ

イーサネットインターフェース

10.1.2.3などのIPアドレスを使用します。 複数のインターフェースをサポートするには、すべてのインターフェース/アドレスに空の文字列を使用します。 上記のセキュリティノートを参照してください。

クライアント

ループバックインターフェイス

127.0.0.1::1などのIPアドレスを使用します。

クライアント

イーサネットインターフェース

名前解決の一貫性と非依存性のためにIPアドレスを使用します。 一般的なケースでは、ホスト名を使用します。 上記のセキュリティノートを参照してください。

クライアントまたはサーバーの場合、接続先のホストを認証する必要がある場合は、TLSの使用を検討してください。

通話のブロック

アプリケーションを一時的に中断するソケット関数またはメソッドは、ブロッキング呼び出しです。 たとえば、accept()connect()send()、およびrecv()は「ブロック」します。彼らはすぐには戻りません。 ブロッキングコールは、値を返す前にシステムコール(I / O)が完了するまで待機する必要があります。 そのため、発信者であるあなたは、完了するか、タイムアウトまたはその他のエラーが発生するまでブロックされます。

ブロッキングソケットコールは、すぐに戻るように非ブロッキングモードに設定できます。 これを行う場合は、準備が整ったときにソケット操作を処理するように、少なくともアプリケーションをリファクタリングまたは再設計する必要があります。

呼び出しはすぐに戻るため、データの準備ができていない可能性があります。 呼び出し先はネットワークで待機しており、作業を完了する時間がありません。 この場合、現在のステータスはerrnosocket.EWOULDBLOCKです。 ノンブロッキングモードはsetblocking()でサポートされています。

デフォルトでは、ソケットは常にブロッキングモードで作成されます。 3つのモードの説明については、Notes on socket timeoutsを参照してください。

接続を閉じる

TCPで注意すべき興味深い点は、クライアントまたはサーバーが接続の一方の側を閉じたまま、反対側を開いたままにすることは完全に合法であることです。 これは「ハーフオープン」接続と呼ばれます。 これが望ましいかどうかは、アプリケーションの決定です。 一般的にはそうではありません。 この状態では、接続の終わりを閉じた側はデータを送信できなくなります。 彼らはそれを受け取ることができるだけです。

このアプローチを取ることを推奨するわけではありませんが、一例として、HTTPは「接続」という名前のヘッダーを使用して、アプリケーションが開いている接続を閉じたり維持したりする方法を標準化します。 詳細については、section 6.3 in RFC 7230, Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routingを参照してください。

アプリケーションとそのアプリケーション層プロトコルを設計および作成するときは、先に進み、接続がどのように閉じられると予想されるかを考えることをお勧めします。 これは明白で簡単な場合もあれば、最初のプロトタイピングとテストが必要になる場合もあります。 これは、アプリケーションと、予想されるデータを使用したメッセージループの処理方法によって異なります。 作業が完了した後、ソケットが常にタイムリーに閉じられていることを確認してください。

バイトエンディアン

さまざまなCPUがバイト順序をメモリに格納する方法の詳細については、Wikipedia’s article on endiannessを参照してください。 個々のバイトを解釈するとき、これは問題ではありません。 ただし、4バイト整数など、単一の値として読み取られて処理される複数のバイトを処理する場合、異なるエンディアンを使用するマシンと通信している場合、バイトの順序を逆にする必要があります。

バイト順は、Unicodeのようなマルチバイトシーケンスとして表されるテキスト文字列にとっても重要です。 常に「true」の厳密なASCIIを使用し、クライアントとサーバーの実装を制御する場合を除いて、UTF-8などのエンコーディングまたはbyte order mark (BOM)をサポートするエンコーディングでUnicodeを使用することをお勧めします。

アプリケーション層プロトコルで使用されるエンコーディングを明示的に定義することが重要です。 これを行うには、すべてのテキストをUTF-8にするか、エンコードを指定する「content-encoding」ヘッダーを使用します。 これにより、アプリケーションがエンコードを検出する必要がなくなります。可能な場合は、これを避ける必要があります。

これは、ファイルまたはデータベースに保存されている関連データがあり、そのエンコードを指定するメタデータが利用できない場合に問題になります。 データが別のエンドポイントに転送されると、エンコードを検出する必要があります。 議論については、RFC 3629: UTF-8, a transformation format of ISO 10646を参照するWikipedia’s Unicode articleを参照してください。

「ただし、UTF-8標準であるRFC 3629は、UTF-8を使用するプロトコルでバイトオーダーマークを禁止することを推奨していますが、これが不可能な場合については説明しています。 さらに、UTF-8で可能なパターンに対する大きな制限(たとえば、上位ビットが設定された単独のバイトはあり得ない)は、BOMに依存せずにUTF-8を他の文字エンコードと区別できるはずであることを意味します。」 (Source)

これの要点は、アプリケーションで処理されるデータに使用されるエンコードが、変化する可能性がある場合は常に保存することです。 つまり、エンコードが常にUTF-8またはBOMを使用するその他のエンコードではない場合、何らかの方法でエンコードをメタデータとして保存するようにしてください。 次に、そのエンコードをヘッダーと共にデ​​ータとともに送信して、受信者にそれが何であるかを伝えることができます。

TCP / IPで使用されるバイト順序はbig-endianであり、ネットワーク順序と呼ばれます。 ネットワーク順序は、IPアドレスやポート番号など、プロトコルスタックの下位層の整数を表すために使用されます。 Pythonのソケットモジュールには、ネットワークとホストのバイト順との間で整数を変換する関数が含まれています。

関数 説明

socket.ntohl(x)

32ビットの正の整数をネットワークからホストのバイト順序に変換します。 ホストのバイト順序がネットワークのバイト順序と同じであるマシンでは、これは何もしません。それ以外の場合は、4バイトのスワップ操作を実行します。

socket.ntohs(x)

16ビットの正の整数をネットワークからホストのバイトオーダーに変換します。 ホストのバイト順序がネットワークのバイト順序と同じであるマシンでは、これは何もしません。それ以外の場合は、2バイトのスワップ操作を実行します。

socket.htonl(x)

32ビットの正の整数をホストからネットワークのバイトオーダーに変換します。 ホストのバイト順序がネットワークのバイト順序と同じであるマシンでは、これは何もしません。それ以外の場合は、4バイトのスワップ操作を実行します。

socket.htons(x)

16ビットの正の整数をホストからネットワークのバイトオーダーに変換します。 ホストのバイト順序がネットワークのバイト順序と同じであるマシンでは、これは何もしません。それ以外の場合は、2バイトのスワップ操作を実行します。

struct moduleを使用して、フォーマット文字列を使用してバイナリデータをパックおよびアンパックすることもできます。

import struct
network_byteorder_int = struct.pack('>H', 256)
python_int = struct.unpack('>H', network_byteorder_int)[0]

結論

このチュートリアルでは多くのことを説明しました。 ネットワークとソケットは大きなテーマです。 ネットワークやソケットを初めて使用する場合は、すべての用語や頭字語に落胆しないでください。

すべてがどのように連携するかを理解するために、慣れ親しむべき多くの要素があります。 ただし、Pythonのように、個々のピースを理解し、より多くの時間を費やすようになると、より意味を持ち始めます。

Pythonのsocketモジュールの低レベルソケットAPIを調べ、それを使用してクライアントサーバーアプリケーションを作成する方法を確認しました。 また、独自のカスタムクラスを作成し、アプリケーション層プロトコルとして使用して、エンドポイント間でメッセージとデータを交換しました。 このクラスを使用して構築し、独自のソケットアプリケーションの作成を簡単かつ迅速に学習および支援することができます。

source code on GitHubを見つけることができます。

最後までおめでとうございます! これで、独自のアプリケーションでソケットを使用できるようになりました。

このチュートリアルが、ソケット開発の旅を始めるために必要な情報、例、インスピレーションを与えてくれることを願っています。