Python 3のソケットを介したシステムコマンドのオンデマンドリモート実行。パート2.データ転送プロトコル

前の記事で、組み込みソケットを使用してPython 3でサーバーとクライアントを作成する方法について説明しました。 しかし、このアプリケーションには多くの欠点がありました。これについては、これ以降の記事で修正していきます。



それでは、私たちのアプリケーションの欠点は何ですか?



今日は、最初の問題を解決する方法と、TCPについて少し説明します。



プロトコルの説明


ベアTCPプロトコルを使用して、サーバーとクライアント間でデータを転送しました。 TCPはストリーミングプロトコルであり、一連のバイトでデータを送信します。 アプリケーションの最初のバージョンでは、ネットワーク経由で引数を指定してコマンドを渡すと、受信したパケットから1024バイトのデータのみが読み取られます。 しかし、データが1024バイトに収まらない場合はどうでしょうか? 解決方法は1つだけです。データを1つのホストで複数のパケットに分割し、別のホストで受信したときにそれらを1つのピースに「接着」します。 しかし、1つのチームが(引数とともに)終了し、別のチームがいつ開始するかをどのように知るのでしょうか これを行うには、送信されたメッセージ全体の長さを知る必要があります。



メッセージの長さを事前に知ることができないため、いずれかのパッケージでメッセージを送信する必要があります。 もちろん、これは最初のパッケージの最初に行うのが最適です。 メッセージの長さを格納するために4バイトのみを割り当てたので、40億文字を超える長さのメッセージを転送できます! メッセージの長さは、それに関する情報、つまりヘッダーの一部であり、プロトコルのヘッダーです。 どのプロトコルを尋ねますか? ウィキペディアを信じているなら、

データ転送プロトコル-異なるプログラム間のデータ交換を定義する一連の論理レベルのインターフェース規則。


TCPを介していくつかのパケットでデータを送信し、最初のパケットのデータの先頭にメッセージ全体の長さ(バイト)を保存することに同意しました。 そこで、簡単なプロトコルを設計しました! プロトコルはTCPに基づいていることを覚えておく必要があります。つまり、TCPと同じ機能を備えています。



プロトコル開発


プロトコルを説明しましたが、それを開発する時です!



Pythonのソケットsendallメソッドは、データを独自のパケットに分割し、サーバーに送信します。 ここではスチームバスは使えません。 長さを転送するには、組み込みライブラリstructを使用して、 unsigned int型(4バイト)のC構造体に変換します。

import struct #    def send(connection, data): #      data = bytes(data, "utf8") #  C struct  unsigned int  4 ,   .   4 ,   ,   . connection.sendall(struct.pack('>I', len(data)) + data)
      
      





struct.pack関数に渡されるパラメーター'> I'は、2番目のパラメーターを逆バイト順( > )で符号なしintI )に変換するように要求します。 これについては少し後で検討します。



 #   def recv_packets(connection, n): piece = b'' #        <b>n</b> while len(piece) < n: #     ,      <b>n</b> packet = connection.recv(n - len(piece)) #     ,     if not packet: return None piece += packet return piece #     def recv(connection): #  ,    length_data = recv_packets(connection, 4) #    ,     if not length_data: return None #     data_len = struct.unpack('>I', length_data)[0] # ,   UTF-8.    "utf8", "utf-8", "UTF8", "UTF-8",     . return recv_packets(connection, data_len).decode("utf8")
      
      





recv_packets関数は、必要な長さのメッセージの一部を取得するまで、すでに受信したデータを読み取ります。 ソケットのrecvメソッドが何も返さない場合、メッセージを完全に受信できませんでした。 この場合、何も返されず、プロトコルのrecv関数も何も返しません。



プロトコルの使用


これで、別のプロトコル-TCPの上に記述されたプロトコルを使用できます。 同時に、ボンネットの下でこの瞬間に何が起こっているかについてお話します。



開始するには、作成したprotocol.pyファイルがあるディレクトリに移動し、2つの端末、コマンドラインインタープリター、または開発環境でPython 3インタープリター(通常はpython3コマンド)を実行します。 両方で、次のコマンドを1つずつ入力します。

 >>> import socket, protocol >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      
      





1行目はソケットライブラリとミニプロトコルをインポートし、2行目はTCP / IPv4プロトコルでソケットを作成します。 AF_INET-ペア(ドメイン/ IPv4、ポート)、SOCK_STREAM-TCPプロトコルのベースとなるストリーミング接続タイプ。



次の2行が最初の端末に入力されます。

 >>> sock.bind(("localhost", 65043)) >>> sock.listen(True)
      
      





この時点で、オペレーティングシステムであるシステムは、アプリケーションに対してlocalhost:65043取得し、アプリケーションはそれをリッスンし始めます。 それでもわからない場合、ソケットはオペレーティングシステムによって作成されるソフトウェアインターフェイスです。



2番目のターミナルを介してサーバーに参加します。

 >>> sock.connect(("localhost", 65043))
      
      





この時点で、次のことが起こります。 顧客は小さなパッケージを送ります。 私が持っている最初のパケットの長さは74バイトです。 (長さは常に同じではありません。接続が確立されたときに送信される内容がほぼ明確になるように引用しています。) 8バイト— 2つのイーサネットアドレス、20バイト— IPヘッダー、46バイト— TCPヘッダー。 TCPヘッダーからのパケットには2ビットがあります( synおよびack) 。 最初のパケットでは、 syn = 1、ack = 0です。 その後、サーバーは同じ長さ(74バイト)のパケットを送信し、受信を確認し、 syn = 1、ack = 1で接続を許可します。 次に、クライアントは3番目のパケットを送信しますが、66バイトではなく66バイト長です。 3番目のパケットでは、 syn = 0、ack = 1です。 このパケットは最終的に接続を確立し、パケットを送受信できるようになりました。 これが、正常な接続の外観です。 TCPやその他の可能性のあるケースをさらに詳しく調べたい場合は、たとえばTanentbaumの著書Computer Networksでこれについて読むことができます。



 >>> conn, addr = sock.accept()
      
      





クライアントに関する情報を読みます。 現在、パケットは送信されていません。記録済みのデータのみを取得します。



2番目のターミナルで、入力します

 >>> protocol.send(sock, "Hello, localhost!")
      
      





この行を使用して、ミニプロトコルを使用してサーバーにメッセージを送信します。

この時点で、次のコンテンツ(ヘッダーを除く)を含むパッケージをサーバーに送信します。

00:00:00:11:48:65:6c:6c:6f:2c:20:6c:6f:63:61:6c:68:6f:73:74:21







00:00:00: 11-0x11または10進数の17は、送信されたメッセージの長さです(この場合、データは4バイトの長さ+メッセージなので、データ、つまりメッセージではありません)。

48:65:6c:6c:6f:2c:20:6c:6f:63:61:6c:68:6f:73:74:21-送信された文字列Hello、localhost!

サーバーは別のパケットで応答し、受信を確認します。



最後に、最初の端末であるクライアントでメッセージを読み取り、デコードします。

 >>> protocol.recv(conn)
      
      





2番目の端末に入力して接続を切断できます

 >>> sock.close()
      
      





クライアントは、 finビットが設定されたパケットをサーバーに送信し、送信するデータがなくなったため切断することを要求します。 応答として、サーバーはfin = 1ビットのパケットも送信します。

最初の端末で同じコマンドを実行すると、サーバーが停止し、リスニングが停止します。



おわりに


すべての処理が完了した後、クライアントアプリケーションとサーバーアプリケーション間でメッセージを簡単に転送できます。 変更点に大きな違いはないため、記事にコードを記載しませんでしたが、プロジェクトをGitHubにアップロードしました。 次の記事では、セキュリティについて説明します。 今後の記事で知りたい他の何かに興味がある場合は、コメントに書いてください。



All Articles