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

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

ソケットとソケットAPIは、ネットワーク経由でメッセージを送信するために使用されます。 それらはhttps://en.wikipedia.org/wiki/Inter-process_communication [プロセス間通信(IPC)]の形式を提供します。 ネットワークは、コンピューターへの論理的なローカルネットワーク、または他のネットワークへの独自の接続を備えた外部ネットワークに物理的に接続されたものです。 明らかな例は、ISP経由で接続するインターネットです。

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

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

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

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

このチュートリアルの終わりまでに、Pythonのhttps://docs.python.org/3/library/socket.html[socket module]の主要な関数とメソッドを使用して、独自のクライアントサーバーアプリケーションを記述する方法を理解できます。 。 これには、カスタムクラスを使用してエンドポイント間でメッセージとデータを送信し、独自のアプリケーションで構築して利用する方法の表示が含まれます。

このチュートリアルの例では、Python 3.6を使用しています。 GitHubのソースコードをご覧ください。

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

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

始めましょう!

バックグラウンド

ソケットには長い歴史があります。 それらの使用は、1971年にhttps://en.wikipedia.org/wiki/Network_socket#History[ARPANETで作成]になり、後にhttps://en.wikipediaと呼ばれる1983年にリリースされたBerkeley Software Distribution(BSD)オペレーティングシステムのAPIになりました。 .org/wiki/Berkeley_sockets [バークレーソケット]。

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

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

最も一般的なタイプのソケットアプリケーションは、クライアントサーバーアプリケーションです。このアプリケーションでは、一方がサーバーとして機能し、クライアントからの接続を待機します。 これは、このチュートリアルで扱うアプリケーションのタイプです。 より具体的には、https://en.wikipedia.org/wiki/Berkeley_sockets [インターネットソケット]のソケットAPIを調べます。これは、バークレーまたはBSDソケットとも呼ばれます。 また、https://en.wikipedia.org/wiki/Unix_domain_socket [Unix domain sockets]もあり、同じホスト上のプロセス間の通信にのみ使用できます。

ソケットAPIの概要

Pythonのhttps://docs.python.org/3/library/socket.html[socket module]は、https://en.wikipedia.org/wiki/Berkeley_sockets [Berkeley sockets API]へのインターフェースを提供します。 これは、このチュートリアルで使用および説明するモジュールです。

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

  • + socket()+

  • + bind()+

  • + listen()+

  • + accept()+

  • + connect()+

  • + connect_ex()+

  • + send()+

  • + recv()+

  • + close()+

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

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

TCPソケット

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

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

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

  • *順序どおりのデータ配信:*データは、送信者によって書き込まれた順序でアプリケーションによって読み取られます。

対照的に、https://en.wikipedia.org/wiki/User_Datagram_Protocol [User Datagram Protocol(UDP)] `+ socket.SOCK_DGRAM +`で作成されたソケットは信頼性が低く、受信者が読み取ったデータが範囲外になる可能性があります送信者の書き込みからの順序。

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

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

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

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

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

  • + socket()+

  • + bind()+

  • + listen()+

  • + accept()+

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

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

真ん中にあるのはラウンドトリップセクションで、クライアントとサーバーの間で `+ 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)

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

このチュートリアルの最後にあるリンク:#reference [参照セクション]には、詳細情報と追加リソースへのリンクがあります。 チュートリアル全体でこれらのリソースや他のリソースにリンクします。

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

+ socket.socket()+`はhttps://docs.python.org/3/reference/datamodel.html#context-managers[context manager type]をサポートするソケットオブジェクトを作成するので、 https://docs.python.org/3/reference/compound_stmts.html#with [+ with ` statement]。 ` s.close()+`を呼び出す必要はありません。

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

https://docs.python.org/3/library/socket.html#socket.socket [+ socket()+]に渡される引数は、リンクを指定します:#socket-address-families [address family]およびsocketタイプ。 `+ AF_INET `はhttps://en.wikipedia.org/wiki/IPv4[IPv4]のインターネットアドレスファミリです。 ` SOCK_STREAM +`はlink:#tcp-sockets [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()`に渡される値は、ソケットのlink:#socket-address-families [address family]に依存します。 この例では、「 socket.AF_INET 」(IPv4)を使用しています。 したがって、2つのタプル「(host、port)+」が必要です。

`+ host `には、ホスト名、IPアドレス、または空の文字列を指定できます。 IPアドレスを使用する場合、「 host 」はIPv4形式のアドレス文字列にする必要があります。 IPアドレス「+127.0.0.1」はhttps://en.wikipedia.org/wiki/Localhost[loopback]インターフェースの標準IPv4アドレスであるため、ホスト上のプロセスのみがサーバーに接続できます。 空の文字列を渡すと、サーバーは利用可能なすべてのIPv4インターフェイスで接続を受け入れます。

+ port +`は `+ 1 + -` + 65535 + の整数でなければなりません( + 0 + は予約されています)。 クライアントからの接続を受け入れるためのhttps://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports[TCPポート]番号です。 ポートが<+ 1024 +`の場合、システムによってはスーパーユーザー権限が必要になる場合があります。

ホスト名と `+ bind()+`の使用に関する注意事項は次のとおりです。

_ 「IPv4/v6ソケットアドレスのホスト部分でホスト名を使用すると、PythonはDNS解決から返された最初のアドレスを使用するため、プログラムが非決定的な動作を示す場合があります。 ソケットアドレスは、DNS解決やホスト構成の結果に応じて、実際のIPv4/v6アドレスに異なる方法で解決されます。 確定的な動作には、ホスト部分に数値アドレスを使用します。」 https://docs.python.org/3/library/socket.html [(ソース)] _

これについては、リンク:#using-hostnames [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の場合、https://serverfault.com/questions/518862/will-increasing-net-core-somaxconn-make-a-difference/519152 [+/proc/sys/net/core/somaxconn +`を参照してください。 ]。

+ accept()+ link:#blocking-calls [blocks]着信接続を待ちます。 クライアントが接続すると、接続を表す新しいソケットオブジェクトと、クライアントのアドレスを保持するタプルを返します。 タプルには、IPv4接続の場合は「(host、port)」、IPv6の場合は「(host、port、flowinfo、scopeid)」が含まれます。 タプル値の詳細については、リファレンスセクションのlink:#socket-address-families [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 `ループを使用してリンクをループします。 `。 これは、クライアントが送信したデータを読み取り、 ` conn.sendall()+`を使用してエコーバックします。

+ conn.recv()+`が空のhttps://docs.python.org/3/library/stdtypes.html#bytes-objects [+ 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クライアントとサーバーの実行

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

*注:*コマンドラインから実行するサンプルまたは独自のコードを取得できない場合は、https://dbader.org/blog/how-to-make-command-line-commands-with-pythonを参照してください。 [Pythonを使用して独自のコマンドラインコマンドを作成する方法] Windowsを使用している場合は、https://docs.python.org/3.6/faq/windows.html [Python Windows FAQ]を確認してください。

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

$ ./echo-server.py

端末がハングしているように見えます。 これは、サーバーがlink:#blocking-calls [blocked](suspended)in call:

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 Address `は ` 127.0.0.1.65432 `であることに注意してください。 ` echo-server.py `が ` HOST = '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 +

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

これを確認する別の方法は、追加の役立つ情報とともに、 + 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 +`オプションとともに使用すると、開いているインターネットソケットの `+ COMMAND ++ PID +(プロセスID)、および + USER +(ユーザーID)を提供します。 上記はエコーサーバープロセスです。

`+ netstat `と ` lsof `には多くのオプションがあり、それらを実行している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 <module>
    s.connect((HOST, PORT))
ConnectionRefusedError: [Errno 61] Connection refused

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

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

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

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

Sockets loopback interface、 width = 1134、height = 800

loopbackインターフェース(IPv4アドレス `+ 127.0.0.1 `またはIPv6アドレス `

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

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

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

アプリケーションで「127.0.0.1」または「+

1+」以外のIPアドレスを使用する場合、おそらくhttps://en.wikipedia.org/wiki/Ethernet[Ethernet]インターフェースにバインドされています外部ネットワークに接続されています。 これは、「localhost」王国以外のホストへのゲートウェイです。

Sockets ethernet interface、幅= 1280、高さ= 780

そこに注意してください。 厄介で残酷な世界です。 「localhost」の安全な境界から抜け出す前に、必ずlink:#using-hostnames [Using Hostnames]セクションを読んでください。ホスト名を使用せず、IPアドレスのみを使用している場合でも適用されるセキュリティ上の注意があります。

複数の接続の処理

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

data = s.recv(1024)

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

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

_ 「アプリケーションは、すべてのデータが送信されたことを確認する責任があります。一部のデータのみが送信された場合、アプリケーションは残りのデータの配信を試みる必要があります。」 https://docs.python.org/3/library/socket.html#socket.socket.send [(ソース)] _

`+ sendall()+`を使用することでこれを行う必要がなくなりました:

_ 「send()とは異なり、このメソッドは、すべてのデータが送信されるかエラーが発生するまで、バイトからデータを送信し続けます。 成功すると何も返されません。」 https://docs.python.org/3/library/socket.html#socket.socket.sendall [(ソース)] _

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

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

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

私たちは何をしますか? concurrencyには多くのアプローチがあります。 最近では、https://docs.python.org/3/library/asyncio.html [非同期I/O]を使用するのが一般的なアプローチです。 `+ asyncio +`はPython 3.4の標準ライブラリに導入されました。 伝統的な選択は、https://docs.python.org/3/library/threading.html [threads]を使用することです。

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

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

`+ select()`を使用すると、複数のソケットでI/O完了を確認できます。 そのため、 ` select()+`を呼び出して、どのソケットにI/Oが読み書き可能かを確認できます。 しかし、これはPythonなので、他にもあります。 標準ライブラリのhttps://docs.python.org/3/library/selectors.html[selectors]モジュールを使用するので、実行しているオペレーティングシステムに関係なく、最も効率的な実装が使用されます。オン:

_ 「このモジュールにより、選択モジュールのプリミティブに基づいて構築された高レベルで効率的なI/O多重化が可能になります。 使用するOSレベルのプリミティブを正確に制御する必要がない限り、ユーザーは代わりにこのモジュールを使用することをお勧めします。 https://docs.python.org/3/library/selectors.html [(ソース)] _

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

https://docs.python.org/3/library/asyncio.html [+ asyncio +]は、シングルスレッドの協調マルチタスクとイベントループを使用してタスクを管理します。 `+ select()+`を使用して、イベントループの独自のバージョンを記述しますが、より単純かつ同期的に実行します。 複数のスレッドを使用する場合、同時実行性がありますが、現在、https://wiki.python.org/moin/GlobalInterpreterLock [CPython and PyPyでhttps://realpython.com/python-gil/[GIL]を使用する必要があります]。 これにより、とにかく並行して実行できる作業量が事実上制限されます。

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

CPUバウンド作業を開始するクライアントからリクエストを受け取っている場合は、https://docs.python.org/3/library/concurrent.futures.html [concurrent.futures]モジュールをご覧ください。 これには、プロセスのプールを使用して非同期的に呼び出しを実行するクラスhttps://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor[ProcessPoolExecutor]が含まれています。

複数のプロセスを使用する場合、オペレーティングシステムは、GILを使用せずに、複数のプロセッサまたはコアで並列に実行するPythonコードをスケジュールできます。 アイデアとインスピレーションについては、PyConのトークhttps://www.youtube.com/watch?v=0kXaLh8Fz3k[John Reese-AILIOとマルチプロセッシングでGILの外側を考える-PyCon 2018]を参照してください。

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

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

次の2つのセクションでは、https://docs.python.org/3/library/selectors.html [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)`の呼び出しです。 このソケットへの呼び出しはもうリンクしません:#blocking-calls [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)

https://docs.python.org/3/library/selectors.html#selectors.BaseSelector.select [+ sel.select(timeout = None)+] link:#blocking-calls [blocks]ソケットがあるまでI/Oの準備ができました。 (キー、イベント)タプルのリスト、各ソケットに1つを返します。 `+ key `はhttps://docs.python.org/3/library/selectors.html#selectors.SelectorKey[SelectorKey] ` namedtuple `で、 ` fileobj `属性が含まれています。 ` key.fileobj `はソケットオブジェクトで、 ` mask +`は準備ができている操作のイベントマスクです。

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

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

`+ 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)+`を呼び出してソケットを非ブロックモードにします。

リンクする必要がないため、これがこのバージョンのサーバーの主な目的であることを思い出してください:#blocking-calls [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.py 」と「 multiconn-client.py +」を実行しましょう。 どちらもコマンドライン引数を使用します。 引数なしで実行して、オプションを表示できます。

サーバーには、 `+ host `と ` port +`番号を渡します:

$ ./multiconn-server.py
usage: ./multiconn-server.py <host> <port>

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

$ ./multiconn-client.py
usage: ./multiconn-client.py <host> <port> <num_connections>

以下は、ポート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つが発生します。 https://docs.python.org/3/library/socket.html [(ソース)] _

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

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

ただし、ファイルの読み取りとは異なり、https://docs.python.org/3/tutorial/inputoutput.html#methods-of-file-objects [+ f.seek()+]はありません。 つまり、ソケットポインターがある場合は、その位置を変更することはできず、いつでも好きなときにデータを読み取りながらランダムに移動できます。

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

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

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

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

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

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

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

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

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

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

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

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

Pythonのhttps://docs.python.org/3/library/codecs.html#encodings-and-unicode [エンコードとUnicode]のドキュメントで説明を見つけることができます。 これはテキストヘッダーにのみ適用されることに注意してください。 送信されるコンテンツのヘッダーで定義された明示的なタイプとエンコード、メッセージペイロードを使用します。 これにより、必要なデータ(テキストまたはバイナリ)を任意の形式で転送できます。

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

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

ビッグエンディアンCPU(PowerPC)をhttps://www.qemu.org/[emulates]する仮想マシンでこれを実行すると、次のようになります。

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

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

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

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

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

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

  • 可変長テキスト

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

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

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

Name Description

byteorder

The byte order of the machine (uses sys.byteorder). This may not be required for your application.

content-length

The length of the content in bytes.

content-type

The type of content in the payload, for example, text/json or binary/my-binary-type.

content-encoding

The encoding used by the content, for example, utf-8 for Unicode text or binary for binary data.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

クラスは、ラッパーメソッドとユーティリティメソッドのクライアントとサーバーの両方でほとんど同じです。 `+ Message._json_encode()+`のようなアンダースコアで始まります。 これらのメソッドは、クラスでの作業を簡素化します。 これらは、他の方法を短くして、https://en.wikipedia.org/wiki/Don%27t_repeat_yourself [DRY]原則をサポートできるようにすることで役立ちます。

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

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

Step Endpoint Action/Message Content

1

Client

Sends a Message containing request content

2

Server

Receives and processes client request Message

3

Server

Sends a Message containing response content

4

Client

Receives and processes server response Message

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

Application File Code

Server

app-server.py

The server’s main script

Server

libserver.py

The server’s Message class

Client

app-client.py

The client’s main script

Client

libclient.py

The client’s Message class

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

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

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

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

*注意:*このセクションのコード例の一部はサーバーのメインスクリプトと `+ 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()`は簡単です。 2つのことしかできません: ` read()`と ` write()+`を呼び出します。

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

これは明白に思えるかもしれませんが、クラスの最初の数回の反復は、現在の状態をチェックし、値に応じて、 `+ read()`または ` write( )+ `。 結局、これは管理が遅れずに対応するには複雑すぎることが判明しました。

クラスを自分のニーズに合うように修正する必要がありますので、それはあなたに最適です。しかし、状態チェックと、その状態に依存するメソッドの呼び出しを `+ read()`と `に保持することをお勧めします。可能な場合、 write()+ `メソッド。

`+ 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 +`メソッド呼び出しがあります。

Message Component Method Output

Fixed-length header

process_protoheader()

self._jsonheader_len

JSON header

process_jsonheader()

self.jsonheader

Content

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 +`を設定して、一度だけ呼び出されるようにします。

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

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

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

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

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

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

$ ./app-server.py
usage: ./app-server.py <host> <port>

たとえば、ポート `+ 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ソケットがhttp://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implicationsに接続されます-for-protocols-and-scalable-servers.html [TIME_WAIT]状態。

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

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

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サイクルを消費し、無駄にします。

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

link:#message-entry-point [Message Entry Point]セクションでは、 `+ process_events()`を介してソケットイベントの準備ができたときに、 ` Message +`オブジェクトがどのようにアクションに呼び出されるかを見ました。 次に、ソケットでデータが読み取られ、メッセージのコンポーネントまたは一部がサーバーで処理される準備ができたときに何が起こるかを見てみましょう。

サーバーのメッセージクラスは `+ libserver.py +`にあります。 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バイト整数です。 https://docs.python.org/3/library/struct.html [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 <host> <port> <action> <value>

例を示しましょう。

$ ./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サイクルを無駄にしないことです。 リクエストが送信された後、書き込みイベントに関心がなくなったため、イベントを起動して処理する理由はありません。

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

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

クライアントのメッセージクラスは `+ libclient.py +`にあります。 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()`にもあります。 これらの行は一時的なエラーをキャッチし、 ` pass +`を使用してスキップするため重要です。 一時的なエラーは、ソケットがリンクするときです:#blocking-calls [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進数で印刷されたバイト「 \ xf0 \ x9f \ x90 \ xb6 + `」を探すと簡単にわかります。 私の端末はエンコードUTF-8でUnicodeを使用しているため、検索用にhttps://support.apple.com/en-us/HT201586 [絵文字を入力]できました。

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

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

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-type 」は「 text/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のhttps://docs.python.org/3/library/socket.html[socket module]ドキュメントを参照してください。 呼び出している各関数またはメソッドのすべてのドキュメントを必ず読んでください。 また、アイデアについては、link:#reference [リファレンス]セクションをお読みください。 特に、link:#errors [Errors]セクションを確認してください。

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

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

ping

`+ ping +`は、https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol [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 Type ICMP Code Description

8

0

Echo request

0

0

Echo reply

3

0

Destination network unreachable

3

1

Destination host unreachable

3

2

Destination protocol unreachable

3

3

Destination port unreachable

3

4

Fragmentation required, and DF flag set

11

0

TTL expired in transit

フラグメンテーションおよびICMPメッセージに関する情報については、記事https://en.wikipedia.org/wiki/Path_MTU_Discovery#Problems_with_PMTUD[Path MTU Discovery]を参照してください。 これは、前述した奇妙な動作を引き起こす可能性のあるものの例です。

ネットスタット

link:#viewing-socket-state [ソケットの状態の表示]セクションでは、ソケットとその現在の状態に関する情報を表示するために `+ netstat +`をどのように使用できるかを見ました。 このユーティリティは、macOS、Linux、およびWindowsで使用できます。

出力例では、列「+ Recv-Q 」と「 Send-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-Q +: `+ 408300 +`に注意してください。

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-Q +: `+ 269868 +`に注目してください。

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

Windows

Windowsを使用する場合は、https://docs.microsoft.com/en-us/sysinternals/[Windows Sysinternals]をまだチェックしていない場合は、必ずチェックアウトする必要があるユーティリティスイートがあります。

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

TCPView screenshot、width = 1242、height = 588

ワイヤーシャーク

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

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

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

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

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

Wireshark screenshot、width = 2524、height = 1448

上記の `+ 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のhttps://docs.python.org/3/library/socket.html [ソケットモジュール]

  • Pythonのhttps://docs.python.org/3/howto/sockets.html#socket-howto[Socket Programming HOWTO]

エラー

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

_ 「すべてのエラーは例外を発生させます。 無効な引数タイプおよびメモリ不足状態の通常の例外が発生する可能性があります。 Python 3.3以降、ソケットまたはアドレスのセマンティクスに関連するエラーにより、 `+ OSError +`またはそのサブクラスの1つが発生します。 https://docs.python.org/3/library/socket.html [(ソース)] _

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

Exception errno Constant Description

BlockingIOError

EWOULDBLOCK

Resource temporarily unavailable. For example, in non-blocking mode, when calling send() and the peer is busy and not reading, the send queue (network buffer) is full. Or there are issues with the network. Hopefully this is a temporary condition.

OSError

EADDRINUSE

Address already in use. Make sure there’s not another process running that’s using the same port number and your server is setting the socket option SO_REUSEADDR: socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1).

ConnectionResetError

ECONNRESET

Connection reset by peer. The remote process crashed or did not close its socket properly (unclean shutdown). Or there’s a firewall or other device in the network path that’s missing rules or misbehaving.

TimeoutError

ETIMEDOUT

Operation timed out. No response from peer.

ConnectionRefusedError

ECONNREFUSED

Connection refused. No application listening on specified port.

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

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

Address Family Protocol Address Tuple Description

socket.AF_INET

IPv4

(host, port)

host is a string with a hostname like 'www.example.com' or an IPv4 address like '10.1.2.3'. port is an integer.

socket.AF_INET6

IPv6

(host, port, flowinfo, scopeid)

host is a string with a hostname like 'www.example.com' or an IPv6 address like 'fe80::6203:7ab:fe88:9c23'. port is an integer. flowinfo and scopeid represent the sin6_flowinfo and sin6_scope_id members in the C struct sockaddr_in6.

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

_ 「IPv4アドレスの場合、ホストアドレスの代わりに2つの特別な形式が受け入れられます。空の文字列は `+ INADDR_ANY `を表し、文字列 ` '<broadcast>' `は ` INADDR_BROADCAST +`を表します。 この動作はIPv6と互換性がないため、PythonプログラムでIPv6をサポートする予定がある場合は、これらを避けることができます。」 https://docs.python.org/3/library/socket.html [(ソース)] _

詳細については、Pythonのhttps://docs.python.org/3/library/socket.html#socket-families[Socket family documentation]をご覧ください。

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

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

>>>

>>> socket.getaddrinfo("example.org", 80, proto=socket.IPPROTO_TCP)
[(<AddressFamily.AF_INET6: 10>, <SocketType.SOCK_STREAM: 1>,
 6, '', ('2606:2800:220:1:248:1893:25c8:1946', 80, 0, 0)),
 (<AddressFamily.AF_INET: 2>, <SocketType.SOCK_STREAM: 1>,
 6, '', ('93.184.216.34', 80))]

IPv6が有効になっていない場合、システムによって結果が異なる場合があります。 上記で返された値は、 `+ socket.socket()`と ` socket.connect()+`に渡すことで使用できます。 Pythonのソケットモジュールドキュメントのhttps://docs.python.org/3/library/socket.html#example [サンプルセクション]にクライアントとサーバーのサンプルがあります。

ホスト名の使用

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

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

_ 「IPv4/v6ソケットアドレスのホスト部分でホスト名を使用すると、PythonはDNS解決から返された最初のアドレスを使用するため、プログラムが非決定的な動作を示す場合があります。 ソケットアドレスは、DNS解決やホスト構成の結果に応じて、実際のIPv4/v6アドレスに異なる方法で解決されます。 確定的な動作には、ホスト部分に数値アドレスを使用します。」 https://docs.python.org/3/library/socket.html [(ソース)] _

「https://en.wikipedia.org/wiki/Localhost[localhost]」という名前の標準的な規則は、ループバックインターフェイスである「127.0.0.1」または「+

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

たとえば、Linuxの場合、ネームサービススイッチの設定ファイルである「+ man nsswitch.conf 」を参照してください。 macOSおよびLinuxで確認するもう1つの場所は、ファイル `/etc/hosts `です。 Windowsでは、「 C:\ Windows \ System32 \ drivers \ etc \ hosts + `」を参照してください。 `+ hosts +`ファイルには、名前からアドレスへのマッピングの単純なテキスト形式の静的テーブルが含まれています。 DNSは完全に別のパズルのピースです。

興味深いことに、この記事の執筆時点(2018年6月)では、RFCドラフトhttps://tools.ietf.org/html/draft-ietf-dnsop-let-localhost-be-localhost-02[「localhost」をlocalhostにする]「localhost」という名前の使用に関する規則、前提条件、およびセキュリティについて説明しています。

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

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

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

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

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

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

ホスト名を使用しているかどうかに関係なく、アプリケーションが安全な接続(暗号化と認証)をサポートする必要がある場合、おそらくhttps://en.wikipedia.org/wiki/Transport_Layer_Security[TLS]の使用を検討する必要があります。 。 これは独自のトピックであり、このチュートリアルの範囲外です。 Pythonのhttps://docs.python.org/3/library/ssl.html[sslモジュールのドキュメント]をご覧ください。 これは、WebブラウザーがWebサイトに安全に接続するために使用するプロトコルと同じです。

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

Application Usage Recommendation

Server

loopback interface

Use an IP address, for example, 127.0.0.1 or ::1.

Server

ethernet interface

Use an IP address, for example, 10.1.2.3. To support more than one interface, use an empty string for all interfaces/addresses. See the security note above.

Client

loopback interface

Use an IP address, for example, 127.0.0.1 or ::1.

Client

ethernet interface

Use an IP address for consistency and non-reliance on name resolution. For the typical case, use a hostname. See the security note above.

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

通話のブロック

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

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

呼び出しはすぐに戻るため、データの準備ができていない可能性があります。 呼び出し先はネットワークで待機しており、作業を完了する時間がありません。 この場合、現在のステータスは「+ errno 」値「 socket.EWOULDBLOCK +」です。 非ブロックモードはhttps://docs.python.org/3/library/socket.html#socket.socket.setblocking [setblocking()]でサポートされています。

デフォルトでは、ソケットは常にブロッキングモードで作成されます。 3つのモードの説明については、https://docs.python.org/3/library/socket.html#notes-on-socket-timeouts [ソケットタイムアウトに関する注意事項]を参照してください。

接続を閉じる

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

このアプローチを取ることを推奨するわけではありませんが、一例として、HTTPは「接続」という名前のヘッダーを使用して、アプリケーションが開いている接続を閉じたり維持したりする方法を標準化します。 詳細については、https://tools.ietf.org/html/rfc7230#section-6.3 [RFC 7230のセクション6.3、ハイパーテキスト転送プロトコル(HTTP/1.1):メッセージの構文とルーティング]を参照してください。

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

バイトエンディアン

さまざまなCPUがメモリにバイト順を保存する方法の詳細については、https://en.wikipedia.org/wiki/Endianness [Wikipediaのエンディアンネスに関する記事]を参照してください。 個々のバイトを解釈するとき、これは問題ではありません。 ただし、4バイト整数など、単一の値として読み取られて処理される複数のバイトを処理する場合、異なるエンディアンを使用するマシンと通信している場合、バイトの順序を逆にする必要があります。

バイト順は、Unicodeのようなマルチバイトシーケンスとして表されるテキスト文字列にとっても重要です。 常に「true」を使用し、厳密にhttps://en.wikipedia.org/wiki/ASCII[ASCII]を使用し、クライアントとサーバーの実装を制御している場合を除き、UTF-8などのエンコードを使用してUnicodeを使用することをお勧めしますまたはhttps://en.wikipedia.org/wiki/Byte_order_mark [バイトオーダーマーク(BOM)]をサポートするもの。

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

これは、ファイルまたはデータベースに保存されている関連データがあり、そのエンコードを指定するメタデータが利用できない場合に問題になります。 データが別のエンドポイントに転送されると、エンコードを検出する必要があります。 議論については、https://tools.ietf.org/html/rfc3629#page-6 [RFC 3629:UTF-8を参照するhttps://en.wikipedia.org/wiki/Unicode[WikipediaのUnicode記事]を参照してください。 ISO 10646の変換フォーマット]:

_ 「ただし、UTF-8標準であるRFC 3629は、UTF-8を使用するプロトコルでバイトオーダーマークを禁止することを推奨していますが、これが不可能な場合については説明しています。 さらに、UTF-8で可能なパターンに大きな制限がある(たとえば、高ビットが設定された孤立したバイトは存在できない)ことは、BOMに依存せずにUTF-8を他の文字エンコーディングと区別できることを意味します。 https://en.wikipedia.org/wiki/Unicode [(ソース)] _

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

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

Function Description

socket.ntohl(x)

Convert 32-bit positive integers from network to host byte order. On machines where the host byte order is the same as network byte order, this is a no-op; otherwise, it performs a 4-byte swap operation.

socket.ntohs(x)

Convert 16-bit positive integers from network to host byte order. On machines where the host byte order is the same as network byte order, this is a no-op; otherwise, it performs a 2-byte swap operation.

socket.htonl(x)

Convert 32-bit positive integers from host to network byte order. On machines where the host byte order is the same as network byte order, this is a no-op; otherwise, it performs a 4-byte swap operation.

socket.htons(x)

Convert 16-bit positive integers from host to network byte order. On machines where the host byte order is the same as network byte order, this is a no-op; otherwise, it performs a 2-byte swap operation.

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

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

結論

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

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

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

GitHubのソースコードをご覧ください。

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

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