このページでは、Python でのソケット通信について解説していきます。
ソケット通信を使いこなすことができれば、複数のプログラム間でデータのやりとりを行うシステムや、簡単なネット対戦機能を搭載したゲームも開発できるようになります。
このページでは、まずソケット通信自体について説明し、続いてソケット通信を使いこなす上で必要になる前提知識について説明していきます。その後、Python でソケット通信プログラムを開発するために利用する socket
モジュールと socket
クラスおよび、その socket
クラスを利用した簡単なソケット通信のプログラム例を示していきます。最後に、socket
モジュールと他の通信ライブラリ・通信モジュールとの違いについて説明していきます。
最後までページを読んでいただければ、ソケット通信を使いこなす上で必要になる前提知識も身に付き、Python でのソケット通信の使い方についても理解できると思いますので、是非ページを読み進めていただければと思います!
Contents
ソケット通信
まず、ソケット通信について説明していきます。
ソケット通信とは、その名のとおりソケットを利用した通信のことを言います。
ソケット
ソケットとは、PC 等の端末上で動作するプログラム・アプリの仮想的な通信インターフェースのことを言います。要は、各プログラムの持つ “仮想的な通信の出入り口” です。このソケットをプログラムが操作・制御することで、他のソケットに対してデータを送信したり、他のソケットからデータを受信することができるようになります。
そして、このソケットを介して通信を行うことをソケット通信と言います。
このソケット通信を利用すれば、複数の端末(PC やスマホ等)で動作するプログラムの間でデータの送受信を行なったり、一方のプログラムから他方のプログラムに対して通信で指示を出すようなことが可能となります。
スポンサーリンク
Python でのソケット通信
Python の場合は、Python の標準モジュールである socket
モジュールの socket
クラスによって、このソケットの作成や、ソケットを介した通信を行うことになります。
前述のとおり、ソケットは通信の入り口と出口となるため、ソケット通信を行うためには事前にソケットを用意しておく必要があります。このソケットは、socket
クラスのコンストラクタを実行することで生成することができます。プログラム的にはコンストラクタを実行することでインスタンスが生成され、そのインスタンスによってソケットが管理されることになります。そして、この socket
クラスのインスタンスからメソッドを実行させることで、その生成されたソケットをプログラムから操作・制御することができ、これによってソケットを介したデータの送信やデータの受信などの通信が行われることになります。
なので、ソケット通信を行う際には、まず socket
クラスのコンストラクタを実行してインスタンスを生成する必要があります。さらに、そのインスタンスにメソッドを実行させることでソケットの操作・制御を行なってソケット通信を実現していくことになります。そして、ソケット通信が終了したら、生成したインスタンスを閉じる処理を行います(ソケットを閉じる)。
なので、イメージとしてはソケット通信は下記のような流れで実現されることになります。socket.socket()
が socket
クラスのコンストラクタで、sock
がコンストラクタによって生成された socket
クラスのインスタンスです。
import socket
# ソケットを生成する
sock = socket.socket()
sock.データを送信するメソッド(引数)
data = sock.データを受信するメソッド(引数)
sock.ソケットを閉じるメソッド()
ということで、ソケット通信においても基本的な処理の実行方法は他のモジュールや他のクラスと同様になります。ですが、ソケット通信プログラムの開発を行う上では、”通信” に関する前提知識を踏まえたうえで開発をしていくことが重要となります。
そのため、次はソケット通信プログラムを開発していくうえで事前に知っておいたほうが良い基本的な知識について解説していきます。
IP アドレスとポート番号
ソケット通信プログラムを開発していくうえでは、まず下記の3つに理解しておいた方が良いと思います。これらを理解しておくことで、より実践的なソケット通信プログラムを開発していくことができるようになりますし、ソケット通信プログラムを開発していく中で、どういったことを考慮しながら開発をしていく必要があるのかを自身で自然と思いつくようになると思います。
- IP アドレスとポート番号
- プロトコル
- サーバーとクライアント
そのため、まずは上記3つについて説明したのちに、実際の Python でのソケット通信の実現方法について解説していきたいと思います。
最初に「IP アドレスとポート番号」について解説していきます。ソケット通信を理解するためには「IP アドレスとポート番号」の知識が必須となります。
まず前提として、通信は1つのプログラムで成立するものではなく、通信相手となるプログラムが存在して初めて成立するものになります。そして、通信プログラムを開発する上では、目的の通信相手と適切に通信を行うことが必要となります。このために必要となる要素の1つが、ここで説明する「IP アドレスとポート番号」となります。
IP アドレス
まず IP アドレスについて説明していきます。
IP アドレス:通信先を指定するための識別子
IP アドレスとは、PC やスマホなどの通信端末を特定するための識別子になります。より正確に言えば、ネットワークインターフェースを特定するための識別子になるのですが、これについては後述するものとし、まずは通信端末を特定するための識別子であるとシンプルに考えてください。
世の中には無数の通信端末が存在します。これらの無数の通信端末の中から目当てとなる端末に対して通信を行う必要があります。
ソケット通信において、その目当ての端末を指定するための識別子が IP アドレスになります。
端末の識別子としてホストが利用されることもあります
これは、IP アドレスのような数値ではなく、端末に名付けられた名前の文字列となります
文字列なので人間にも直感的に理解することが出来ます
が、結局はホストも IP アドレスに変換したうえで通信が行われることになりますので、まずは IP アドレスが識別子であると考えてください
例えばデータを送信するときには、この IP アドレスで目当ての端末を指定する必要があります。”アドレス” という単語が含まれている通り、IP アドレスは住所みたいなものです。
こんな感じで、目当てとなる通信端末に対するデータの送信を実現するためには IP アドレスの指定が必要となります。
IP アドレスとネットワークインターフェース
また、1つの通信端末が複数の IP アドレスを持つ場合もあります。PC は Wi-Fi に接続して無線で通信を行うこともできますし、イーサネットケーブルのような有線で接続して通信を行うこともできます。このように複数の経路で通信を行うことが可能なのは PC が複数の通信機器(ネットワークカードなど)を備えているからになります。このような通信機器をネットワークインターフェースと呼びます。前述の例の場合は、無線接続用のネットワークインターフェースと有線接続用のネットワークインターフェースを備えていることになります。
そして、IP アドレスは、これらの各ネットワークインターフェースに割り当てられることになります。したがって、1つの通信端末は複数の IP アドレスを持つことがあります。例えば、下の図で考えれば、PC1は2つのネットワークインターフェース(無線接続用・有線接続用)を持っています。そして、それぞれのネットワークインターフェースに IP アドレス 192.168.10.5
と 192.168.11.5
とが割り当てられていることになります。
この時、PC1はスマホ1からデータを受信する際は、IP アドレス 192.168.10.5
が割り当てられたネットワークインターフェースからデータを受信する必要がありますし、PC2からデータを受信する際は IP アドレス 192.168.11.5
が割り当てられたネットワークインターフェースからデータを受信する必要があります。
このように、世の中には無数の通信端末が存在し、さらに通信端末には複数のネットワークインターフェースが存在します。そして、それぞれのネットワークインターフェースに IP アドレスが割り当てられています。ソケット通信では、データを送信するときにも送信先のネットワークインターフェース、さらにデータを受信するときにも受信先のネットワークインターフェースを特定するために IP アドレスを指定することが必要となります。
論理的なネットワークインターフェース
また、ネットワークインターフェースには物理的なものだけではなく論理的なネットワークインターフェースも存在します。その1つに、自分自身と通信を行うためのネットワークインターフェースが存在します。
そして、このネットワークインターフェースには IP アドレスとして 127.0.0.1
が割り振られます。つまり、IP アドレス 127.0.0.1
にデータを送信すると自分自身がそのデータを受信することになります。この IP アドレス 127.0.0.1
はループバックアドレスと呼ばれる特別な IP アドレスです。通信プログラムは、複数の通信端末(PC 等)の間で通信を行うことになるため、通信プログラムの動作確認には普通に考えれば2つ以上の PC 等が必要になります。ですが、このループバックアドレスが存在するため自分自身の PC と通信を行うことが可能で、1つの PC のみで通信プログラムの動作確認を行うことが可能です。
もし、複数の通信端末を使って通信プログラムの動作確認を行いたいけど PC を1つしか持っていないような場合は、ラズパイなどを購入して動作確認を行うのも良いと思います。PC を買おうと思えば 10 万円くらいかかりますが、ラズパイの場合は本体だけで 1 万円くらい、スターターキット付きの場合は 2 万円くらいで買えるのでオススメです。もちろん、通信プログラムの動作確認だけでなく、お試しで Linux を利用してみたり IoT に関する実験を行なってみるような用途でも利用可能です。
スポンサーリンク
ポート番号
次に、ポート番号について説明していきます。
PC 等の通信端末上では複数のアプリ・プログラムが動作している可能性があります。そして、それらのプログラムが通信を行うためにソケットを持っている可能性があります。つまり、1つの通信端末上には複数のソケットが存在しています。これらの複数のソケットのうち、どのソケットに対して通信を行いたいのかを指定するための識別子がポート番号になります。
前述の通り、IP アドレスを指定してデータの送信を行えば、その IP アドレスが割り当てられたネットワークインターフェースに対してデータが届くことになります。さらに、データの送信時にポート番号を指定しておくことで、そのデータは指定されたポート番号で受信待ちしているソケットに届けられることになります。そして、データがソケットに届けられれば、そのデータをプログラムが受信することができます。
つまり、特定の端末(ネットワークインターフェース)にデータを届けるためには IP アドレスの指定が必要となり、その端末上で動作する特定のソケットにデータを届けるためにはポート番号の指定が必要となります。IP アドレスをマンションの住所と考えれば、そのマンションの部屋番号を示すのがポート番号となります。
ソケット通信と IP アドレス / ポート番号
ここまでの説明のように、ソケット通信で適切に相手にデータを送信したり、データを受信したりするためには IP アドレスやポート番号の指定が必要になります。
そのため、詳細は socket クラスのメソッド で説明しますが、ソケット通信時に使用するメソッドには IP アドレスとポート番号を引数で指定する必要があるものが多いです。
例えば、ソケット通信でデータを送信するメソッドには sendto
が存在し、このメソッドでは引数で IP アドレスとポート番号を指定することができるようになっています。そして、この sendto
メソッドを実行すると、指定された IP アドレスを割り振られたネットワークインターフェースにデータが送信され、さらに指定されたポート番号で待っているソケットにデータが割り振られることになります。
また、bind
メソッドを実行すると、そのメソッドを実行したインスタンスのソケットが、引数で指定された IP アドレス&ポート番号に関連付けられることになります。そして、その状態のソケットで受信待ちを行うと(受信待ちは recv
や recvfrom
メソッドで実行する)、指定された IP アドレスのネットワークインターフェースとポート番号に対してデータの受信待ちが行われることになり、そこに送信されてきたデータを受信することができるようになります(データ受信時には、どのネットワークインターフェースからデータを受信し、どのポート番号宛のデータを受け取るのかを bind
で事前に設定 [関連付け] しておく必要があります)。
こんな感じで、ソケット通信を行う上ではデータの送信やデータの受信に IP アドレスやポート番号の指定が必要となります。データを相手に届けるためには、通信相手が受信待ちしている IP アドレス・ポート番号に対してデータを送信する必要がありますし、データを相手から受け取るためには、通信相手が送信してくる IP アドレス・ポート番号に対してデータの受信待ちを行う必要があります。
この辺りの、通信プログラムの作り方については後述で詳細を解説していきます。まずは、ソケット通信を行ううえでは IP アドレスとポート番号が非常に重要な存在であることは覚えておいてください。
プロトコル
続いてプロトコルについて解説していきます。
スポンサーリンク
プロトコルの重要性
プロトコルとは、簡単に言えば通信を行う上でのルールになります。
通信は、自分だけでなく相手がいて成立するものです。そして、通信は相手が期待するデータや順序でデータの送受信を行うことで成立します。勝手に好きなようにデータを送信しても相手が混乱して通信が破綻することになります。通信プログラムを初めて開発する人にとっては、この点が難しい点であり戸惑う点になると思います。
例えば何も考えずに好き勝手プログラムを開発すると、お互いのプログラムが同時にデータの受信待ちを行い、延々と待ち続けるようなプログラムが出来上がってしまうこともあります。
なので、通信を行う複数のプログラムを開発する際には、通信時のルールを定め、そのルールに従って通信が行われるように開発することが重要になります。このルールには、データの送り方や送受信するデータのフォーマット、データの送受信の順序やタイミングが含まれます。そして、このルールがプロトコルと呼ばれます。ルールが無ければ互いのプログラムが好き勝手動作して通信が破綻することになりかねないですが、同一のルールに従ってプログラムを開発するようにすれば、それらのプログラム間でうまく通信が行えるようになります。
つまり、プロトコルと聞くと難しそうに感じるかもしれませんが、通信プログラムを開発する上での手順書のようなものでもあり、通信プログラムの開発を手助けしてくれるものになります。
トランスポート層のプロトコル
このプロトコルには数多くのものが存在します。また、独自でプロトコルを決めて通信プログラムを開発することもあります。
これらのプロトコルのうち、ソケット通信を行う上で、まず意識すべきプロトコルが TCP と UDP になります。
TCP と UDP は共に、TCP / IP モデルのトランスポート層のプロトコルになります。TCP / IP モデルは複数の端末間で通信を行う際のルールを階層的に定めたものになります。このモデルは「ネットワークインターフェース層」「インターネット層」「トランスポート層」「アプリケーション層」の4階層で表現されているのですが、特にソケット通信プログラムを開発する上で意識すべきなのは、後者の2つの「トランスポート層」と「アプリケーション層」になります。アプリケーション層については後述で解説します。
ソケット通信を行う上ではソケットが必要で、このソケットを作成してからソケットを利用して通信を行うことになるのですが、ソケット作成時には TCP or UDP のどちらの通信を行うのかを選択する必要があります。なので、ソケット通信プログラムを開発するためには、特徴を踏まえたうえで TCP or UDP のどちらで通信するのかを決定する必要があり、TCP と UDP についての最低限の理解が必要となります。
厳密に言えばもっと他のプロトコルを選択することもできるのですが、このページでは TCP or UDP のどちらか一方でソケット通信を行うことを前提に解説していきます
TCP と UDP の共通点
これらの TCP と UDP はトランスポート層のプロトコルであり、この層のプロトコルではポート番号に応じたデータの振り分けや信頼性に関する制御が規定されています。このページでは、簡単にこれらを “データの届け方” と呼ばせていただきます。
IP アドレスとポート番号 の節で、ソケット間でデータの送受信を行うためには IP アドレスとポート番号が必要になると説明しましたが、このポート番号によってソケットにデータを振り分けることがトランスポート層の役割の1つになります。このポート番号によるソケットへのデータの振り分けは TCP でも UDP でも共通的に行われることになります。また、IP アドレスによってネットワークインターフェースにデータを届けるのは、トランスポート層よりも下位の層(インターネット層とネットワークインターフェース層)の役割になります。
TCP と UDP の違い
また、TCP の場合は信頼性の高い通信を実現するため、例えばデータの送信時には再送制御と呼ばれる制御を行うことがルール化されています。そのため、TCP の場合、データを送信した後に相手からデータを受信したことを示す応答を受け取るような制御が必要となります。そして、応答がなければ相手がデータを受信できなかったとみなして再度データを送信するような制御が必要となります。このような制御を行うことで TCP では信頼性の高いデータの送信が実現されています。それに対して UDP の場合は、この再送制御が行われません。UDP の場合はデータを単に送信するだけで、相手がデータを受信したかどうかの確認は行われません。
また、再送制御のような制御を行うために、TCP ではデータの送受信を行う前にソケット間で接続の確立(コネクションの確率)を行う必要があります。それに対し、UDP の場合は接続の確立は不要で、いきなりデータの送受信を行うことができます。
このように、ポート番号に応じてデータをソケットに振り分けるという点では共通なのですが、データの送受信時の制御やデータの送受信するための前準備が TCP と UDP とで異なります。そして、これが異なるため TCP と UDP とでは通信の信頼性が異なります。TCP の方が通信の信頼性が高いですし、UDP の場合は通信の信頼性が低いですが、通信が高速であるという特徴があります。
なので、ソケット作成時には、上記のような TCP と UDP の違いを理解した上で、TCP と UDP のどちらで通信するのかを選択する必要があります。開発するプログラムがリアルタイム性を重視するものであれば UDP を選択し、信頼性を重視するものである場合は TCP を選択する、といった感じですね。
ソケット通信とトランスポート層のプロトコルの関係
ということで、ソケット通信を利用するためには TCP や UDP についての最低限の理解が必要となります。なんですが、実はソケット通信プログラムを開発する際には、前述で紹介したような再送制御を行なったり、ポート番号によるデータの振り分けを行なったりするような制御は不要となります。
なぜなら、ソケット通信用のライブラリやモジュールがトランスポート層以下の制御を勝手に行なってくれるからです。Python の場合は、socket
モジュールの socket
クラスのメソッドを実行すれば、トランスポート層以下のプロトコルに応じた制御が勝手に行われることになります。例えばソケット生成時に TCP を選択すればデータ送信時に勝手に再送制御が行われます。UDP を選択すれば再送制御なしに高速な通信が行われます。
なので、ソケット通信を行う上では、TCP と UDP を選択する必要があるものの、TCP や UDP に従った通信制御は不要となります。基本的には socket
クラスが提供するメソッドを実行すれば良いだけです。ただ、後述で解説するように、TCP と UDP とでは使用するメソッドが異なりますし、前述のように TCP ではデータの送受信を行う前に接続の確立が必要であり、そのためのメソッドの実行も必要となります。また、ソケットを生成する際には TCP or UDP のどちらを利用するかを指定する必要があります。
逆に言えば、これらさえ行えば、ソケット通信プログラム開発時はトランスポート層以下のプロトコルに関しては意識する必要はありません。ということで、ソケット通信プログラムの開発を行うのであれば、最低限でも良いので TCP と UDP の違いであったり、TCP 通信の場合に実行する必要のあるメソッド& UDP 通信の場合に実行する必要のあるメソッドくらいは理解しておくと良いでしょう。
アプリケーション層のプロトコル
続いて、トランスポート層の上位の層であるアプリケーション層のプロトコルについて解説していきます。
ソケット通信とアプリケーション層のプロトコル
前述の通り、トランスポート層以下に関してはソケット通信ライブラリ・モジュールが勝手にプロトコルに応じた制御・通信を実行してくれます。ですが、トランスポート層の上位層である “アプリケーション層” のプロトコルに関してはソケット通信モジュールは関与しません。なので、アプリケーション層のプロトコルに関しては、開発者がプロトコルに従うようにプログラムを開発する必要があります。
トランスポート層のプロトコルでデータの届け方のルールが規定されているのに対し、このアプリケーション層の各種プロトコルでは、送受信するデータのフォーマットや送受信する順序・タイミングが規定されています。HTTP や SMTP、FTP などといったプロトコルはアプリケーション層のプロトコルになります。
つまり、ソケット通信プログラムの開発者は、アプリケーション層のプロトコルで定められたフォーマットのデータを用意し、さらにプロトコルで定められた順序・タイミングに従ってデータの送受信が行われるようにプログラムを開発する必要があります。そして、前述の通り、socket
クラスのメソッドさえ実行すれば、後はメソッド内でトランスポート層以下の制御が行われて相手にデータが届くようになっています。
ただし、TCP or UDP でデータ送信時やデータ送信前に実行すべきメソッドが異なるため、これらの使い分け等も考慮しながら開発していく必要があります。
さらに、実際に通信プログラムを開発する上では、どのアプリケーション層のプロトコルに従い、どのトランスポート層のプロトコルに従ったものを開発していくのかを事前に決めておく必要もあります。そして、これらのプロトコルに従うようにプログラムを開発していく必要があります。
独自のプロトコルでの通信プログラムの開発
ただし、通信プログラムは、特に通信相手と同じプロトコルに従ってデータの送受信を行うことこそが重要であり、必ずしも HTTP や SMTP などの一般的なプロトコルに従わなければならないというわけではありません。例えば、自分で決めた独自のプロトコルに従って通信プログラムを開発することも可能です。自分で送受信するデータのフォーマットやデータの送受信を行う順序・タイミングを決め、それに合わせて開発を行なっても、そのプロトコルが破綻していない&複数のプログラムが同じプロトコルに従ってデータの送受信を行うように開発されていれば、データの送受信は上手く行うことができます。
例えば、何かしらの組み込み製品を開発するような場合、その製品内で動作する複数のプログラム間でのみ通信が行われるのであれば、開発者やチームで決めたプロトコルで通信させることも多いです。要は、自分達が開発するプログラム間でのみ通信を行うのであれば、それらのプログラムが同じプロトコルに従ってデータの送受信を行えば良いのですから、それが独自に決めたプロトコルでも問題ありません。
一般的なプロトコルでの通信プログラムの開発
ですが、HTTP 等の一般的なプロトコルで動作している “既存の通信プログラム” とデータの送受信を行いたいのであれば、その既存の通信プログラムが従っているプロトコルに合わせてプログラムを開発する必要があります。前述のとおり、通信プログラムは通信相手と同じプロトコルに従ってデータの送受信を行う必要があります。なので、既に何かしらのアプリケーション層のプロトコルに従って動作している通信プログラムと通信を行うためには、そのプログラムと同じプロトコルに従うようにプログラムを開発する必要があります(もしくは既存の通信プログラムの方を変更する必要があります)。
例えば、ウェブページを表示するアプリを開発したい場合、ウェブページを表示するためのデータ(例えば HTML)をサーバー装置から受信する必要があります。このサーバー装置上では HTTP サーバーというソフトが動作しており、この HTTP サーバーは HTTP というプロトコルに従って動作しています。そのため、ウェブページを表示するアプリを開発するのであれば、HTTP というプロトコルに従ってデータの送受信を行うように開発する必要があります。これに従わずにプログラムを開発すると、HTTP サーバー側が期待するフォーマットのデータを受信することができなくなるため、エラーが返却されたり応答が無かったりすることになります。
で、こういった場合は、特定のアプリケーション層のプロトコルに従ったプログラムを開発する必要があるため、アプリケーション層のプロトコルを熟知した上でプログラムを開発する必要があります。このページでは、一般的なアプリケーション層のプロトコルに関しては深く説明しませんが、また機会があれば HTTP 等のプロトコルに従ったソケット通信プログラムの作り方についても別途説明していきたいと思います。
また、既存のプログラムと通信するためには、アプリケーション層だけでなく、トランスポート層のプロトコルも既存のプログラムと合わせる形で開発を行う必要があります。例えば、上記でも登場した HTTP サーバーに関して言えばトランスポート層のプロトコルとしては TCP が採用されていることがほとんどだと思いますので、HTTP サーバーと通信するプログラムを開発するのであれば、そのプログラムが利用するトランスポート層のプロトコルも TCP にしてやる必要があります。なので、こういった既存のプログラムと通信を行うのであれば、採用するアプリケーション層のプロトコル、さらにはトランスポート層のプロトコルが自然と一意に定まることになります。
スポンサーリンク
プロトコルとソケット通信プログラムの開発
プロトコルに関してまとめると、まず通信プログラムは、通信相手と同じプロトコルに従って通信・データの送受信を行うことが必要です。
このプロトコルのうち、トランスポート層のプロトコルに関しては socket
クラスのメソッド側でプロトコルに従った通信・制御が行われるようになっています。なので、socket
クラスのメソッドを適切に実行してやれば、トランスポート層以下の通信は socket
クラスが勝手に行なってくれることになります。ただし、TCP or UDP をソケット生成時に選択したり、TCP or UDP に応じて実行するメソッドを変更したりする必要はあります。
それに対し、アプリケーション層のプロトコルに関しては socket
クラスは関与しないため、データ送信時はアプリケーション層のプロトコルで決められたフォーマットに従ったデータを用意してやる必要がありますし、データの送受信の順序やタイミングもアプリケーション層のプロトコルに従って行われるようにメソッドを実行するようにする必要があります。ただ、このプロトコルは独自で決めたものでも良いです。とにかく、通信相手と同じプロトコルに従ってデータの送受信を行うことが重要です。
後は、開発したいプログラムに応じて、どのプロトコルを採用するのかを予め決めておくことも重要です。通信相手と採用するプロトコルが異なれば通信が上手く行えないため、採用するアプリケーション層・トランスポート層のプロトコルを決めておき、それに従ってプログラムを開発していくことが必要となります(ソケット通信プログラムでは、アプリケーション層のプロトコルに関しては独自に決めたものでも良いです。また、通信相手が既に何らかのプロトコルに従って通信しているのであれば、それと同じプロトコルに従って通信するようにプログラムを開発する必要があります。
ということで、ソケット通信プログラムは主に下の図の破線で囲った部分のプロトコルを意識しながら開発していくことになります。トランスポート層も多少は意識する必要がありますが、プロトコルに従った制御・通信は socket
クラスが実施してくれます。
サーバーとクライアント
ここまで通信プログラムは通信相手と同じプロトコルに従うように開発することが重要であると説明してきました。この説明を聞いて通信を行う2つのプログラムが全く同じ処理をすることをイメージされた方もおられるかもしれません。そのようにプログラムを開発することも可能なのですが、実際には2つのプログラムが異なる処理を行うように開発することの方が多いと思います。
具体的には、2つのプログラムをサーバーとクライアントという2つの異なる立場のプログラムとして開発することが多いです。例えば HTTP や SMTP 等のアプリケーション層のプロトコルでは、2つのプログラムをサーバーとクライアントの役割に分類し、それらが送受信するデータのフォーマットや送受信する順序やタイミングが定められています。つまり、サーバーとクライアントの2種類の異なるプログラム間で通信を行うことを前提としてプロトコルが定められています。そういったプロトコルは多いですし、何より対等のプログラムを開発するよりも異なる役割のプログラムを開発した方が通信プログラムの開発が楽になります(理由は後述します)。
サーバーとクライアントの違い
ここからは、上記で登場した “サーバー” と “クライアント” について解説していきます。
サーバーとはサービスを提供するプログラム・ソフトウェア(もしくはハードウェア)であり、クライアントとはサーバーが提供するサービスを利用するプログラム・ソフトウェア(もしくはハードウェア)となります。
これらのサーバーとクライアントが通信を行なって1つのシステムを形成するようなソフトウェアの設計モデルをクライアントサーバーモデルと呼びます。
プロトコル の節で説明したように、通信プログラムは通信相手と同じプロトコルに従って通信を行う必要があるのですから、クライアントサーバーモデルでは、サーバーとクライアントが同じプロトコルに従って通信を行う必要があります。
ただし、これらのサーバーとクライアントとでは処理の流れが異なります。
サーバーは受身のプログラムで、基本的にはクライアントからデータが送信されてくるのを待ち続けるプログラムとなります。そして、クライアントからデータが送信されてきた際に動き出し、そのデータに応じた処理を実行するようになっています。その後は、またクライアントからデータが送信されてくるのを待ち続け、同じことを繰り返します。
それに対し、クライアントは能動的なプログラムで、サービスの利用が必要になったタイミングでサーバーに対してデータの送信を行うプログラムとなります。このデータの送信によって、サーバーにサービスを利用させて欲しいと要求します(この要求は、リクエストと呼ばれることがあります)。例えば、GUI アプリであれば、ユーザーが何かしらのボタンをクリックしたときにデータを送信するようなイメージでデータの送信が行われます。
で、この時にクライアントがサーバーに送信するデータはアプリケーション層のプロトコルで規定されたフォーマットのものである必要があります。さらに、プロトコルによっては、サーバーがデータを受け取った後にデータ(レスポンス)を返却するような場合もあり、このデータのフォーマットもプロトコルで規定されたものである必要があります。要は、このクライアントとサーバー間のデータの送受信のルールがアプリケーション層のプロトコルで決められており、このプロトコルに従って双方のプログラムがデータを送受信する必要があります。
スポンサーリンク
クライアントサーバーモデルのメリット
このクライアントサーバーモデルの一番の特徴は、サーバーとクライアントが対等ではないという点になります。どちらが偉いというわけでもないのですが、前述の通りサーバーとクライアントとでは役割が異なります。そして、役割が違うため通信プログラムが開発しやすいです。
前述の通り、サーバーは基本的に常にクライアントからのデータの送信を待っているのですから、サーバーがまず行う処理はデータの受信待ちになります。そして、データの受信を行なったのちに、その受信したデータに応じた処理を実行すれば良いことになります。処理結果を返却する必要があるのであれば、処理を実行したのちにデータの送信をクライアントに対して行えば良いだけです。もちろん、プロトコルによってはもっと複雑なデータの送受信を行う必要がある場合もありますが、最初にデータの受信待ちを行い、データ受信後にプロトコルに応じた処理を行えば良いだけになります。
それに対し、クライアントは、サーバーは常にデータの受信待ちを行なっているのですから、好きなタイミングでデータの送信を行えば良いことになります。もちろんプロトコルに従ったフォーマットのデータを送信する必要あはります。そして、その後はプロトコルに応じてサーバー側からのデータの受信待ち等を行えば良いことになります。
もちろん、TCP と UDP の場合とでデータの送受信前の接続の確立の必要性の有無が異なるのですが、データの送受信に関して言えば、上記のようにサーバーとクライアントとでは基本的に最初の一手が決まっていることになります。なので、例えばクライアントは自身がデータを送信する前に相手からデータが送信されてくるようなことは心配する必要がありません。サーバーに関しても同様のことが言えます。そのため、基本的にサーバーとクライアントでタイミングによって実行する処理・メソッドが一意に決まることになります。なので、サーバーとクライアントそれぞれの処理の流れはある程度パターン化されることになり、プログラム開発が容易となります。
ただし、プロトコル の節でも解説したように、TCP と UDP の場合とで利用するメソッドや接続の確立の必要性の有無が異なることになりますので、基本的な処理の流れは、サーバーとクライアントだけでなく、TCP と UDP も掛け合わせた下記の4パターンに分けられることになります。
- TCP 通信を行うサーバー
- TCP 通信を行うクライアント
- UDP 通信を行うサーバー
- UDP 通信を行うクライアント
もちろん、これらでは最初のデータの送受信までの処理の流れがパターン化されているだけで、それ以降の処理や送受信するデータの用意はアプリケーション層のプロトコルに従って実装する必要があります。ですが、最初のデータの送受信までの処理の流れだけでもパターン化されているとかなり通信プログラムが開発しやすくなるため、これらの4つのパターンは頭に入れておいた方が良いと思います。
これら4つのパターンのスクリプトの実例に関しては、ソケットの生成方法や socket
クラスのメソッドの説明を行なった後に、ソケット通信プログラムの例 の節で紹介したいと思います。
少し前置きが長くなりましたが、ここまでがソケット通信プログラムを開発する上で知っておいた方が良い知識の解説になります。ここからは、結局はここまで解説してきた内容を実現するためのメソッドの利用方法やスクリプトの例の解説になりますので、ここまで解説してきた内容を頭に入れておいていただければ、ここからの説明もすんなり理解できると思います。
ソケットの生成
では、ここからは socket
モジュールの socket
クラスを利用した Python でのソケット通信の作り方を説明していきたいと思います。まずは、ソケットの生成方法について説明し、その後、データの送受信や通信の前準備を行う socket
クラスの各種メソッドの紹介を行っていきたいと思います。
socket
のインスタンスの生成
ここまで説明してきたとおり、ソケット通信はソケットを利用した通信であり、ソケット通信を行うためにはソケットを生成しておく必要があります。また、これも前述で少し説明しましたが、ソケットの生成は socket
モジュールの socket
クラスのコンストラクタで生成することができます。
このコンストラクタを実行することでソケットが生成され、そのソケットを管理するためのインスタンスが返却されます。そして、その生成されたソケットでのソケット通信は、その返却されたインスタンスにメソッドを実行させることで行うことが出来ます。
socket
クラスのコンストラクタ
ということで、ソケット通信を行うためには、まず socket
クラスのコンストラクタを実行してインスタンスを生成し、その後、そのインスタンスから各種メソッドを実行するような処理を記述していく必要があります。
ここでは、その socket
クラスのコンストラクタについて説明していきます。
まず、socket
クラスのコンストラクタは、socket
モジュールを import
しておけば socket.socket()
から実行することができます。そして、この socket.socket()
の返却値が socket
クラスのインスタンスとなります。
socket
クラスのコンストラクタ、すなわち socket.socket()
の引数には下記が用意されています。これらの引数の指定によって生成するソケットのオプションを指定することができます。
family
type
proto
fileno
これらを指定しなければ適当な初期値がオプションに設定されてソケットが生成されることになりますが、少なくとも family
と type
に関しては引数を指定することが多いです。proto
と fileno
は指定しないことも多いため、このページでは解説を省略し、family
と type
に対してのみ解説を行います。
引数 family
まず引数 family
(第1引数) にはアドレスファミリーの種類を指定します。
socket
モジュールでは、通信相手を決めるために様々なメソッドで引数に address
が指定できるようになっています。例えば、後述で解説する sendto
メソッドでは、引数 address
に対してデータの送信が行われます。この address
のフォーマットが、family
によって異なります。
で、この引数 familiy
には様々な値を指定することが出来るのですが、まずは引数 family
には socket.AF_INET
を指定するのでよいと思います。これにより、IPv4 で通信を行うソケットを生成することができ、この場合の address
のフォーマットは (IPv4アドレス, ポート番号)
となります。すなわち、第1要素を IPv4のアドレスを表す文字列、第2要素をポート番号を表す整数とするタプルとなります。そして、これらを指定することで、IP アドレスとポート番号 の節で説明したように任意のソケットに対してデータの送信等を行うことが出来るようになります。
また、実はここまでの解説に関しても、引数 family
には socket.AF_INET
を指定することを前提とした説明を行ってきていますし、ここからの解説も、引数 family
に socket.AF_INET
を指定することを前提に説明していきたいと思います。
引数 type
引数 type
(第2引数) にはソケットのタイプを指定するのですが、この引数には基本的には下記の2つのいずれかを指定することが多いです。
socket.SOCK_STREAM
:TCP 通信を行うsocket.SOCK_DGRAM
:UDP 通信を行う
トランスポート層のプロトコル で解説したように、ソケット通信では基本的に TCP or UDP の通信を行うことになります。TCP or UDP のどちらのプロトコルで通信を行うのかを引数 type
で指定することになります。
具体的には、TCP 通信を行うプログラムを開発するのであれば、socket
クラスのコンストラクタの引数 type
に socket.SOCK_STREAM
を指定してソケットを生成し、UDP 通信を行うプログラムを開発するのであれば socket.SOCK_DGRAM
を指定します。
通信を行うプログラム同士は同じプロトコルに従って通信を行うことが必要となります。従うプロトコルが異なっていると基本的に通信プログラムは意図通りに動作しません。そのため、通信を行うプログラム同士はソケットを生成するときに同じ type
引数を指定する必要があります(引数 family
も同様です)。
また、トランスポート層のプロトコル でも少し触れましたが、TCP or UDP のどちらで通信を行うのかで使用するメソッドや接続確立の必要性の有無が異なります。なので、この引数 type
はソケット通信プログラムを開発していく上で非常に重要な引数となります。
socket
クラスのコンストラクタの実行例
下記に socket
クラスのコンストラクタの実行例を示します。
import socket
# ソケットを生成する
sock = socket.socket(socket.AF_INET, socket.SOCKET_DGRAM)
上記では、第1引数 (引数 family
) に socket.AF_INET
を指定し、さらに第2引数 (引数 type
) に SOCKET_DGRAM
を指定しているため、このコンストラクタの実行によって生成されるソケットは IPv4 で UDP 通信を行うためのソケットとなります。そして、そのソケットの操作を行うインスタンスが変数 sock
から参照されることになります。
なので、この sock
からメソッドを実行させて、様々なソケットの制御やソケットでの通信を行うことができることになります。これらのメソッドに関しては、socket クラスのメソッド で紹介していきます。
スポンサーリンク
ソケットを閉じる (close
)
コンストラクタと一緒に覚えておきたいのが close
メソッドになります。一般的なファイル等と同様に、ソケットは不要になったら閉じる必要があります。
このソケットを閉じるためには、そのソケットに対応する socket
クラスのインスタンスから close
メソッドを実行する必要があります。このソケットを閉じる処理が行われないと、生成したソケットがずっとプログラム内に残ってしまうことになります。そして、close
せずにソケットの生成を何回も行うと、いずれはソケットの生成可能数の上限に達し、ソケットの生成に失敗することになります。
なので、ソケットは不要になったら close
メソッドで閉じる必要があります。
Python の場合はガベージコレクション機能がありますので、close
メソッドの実行を忘れても socket
クラスのインスタンスへの参照が無くなればガベージコレクションによって socket
クラスのインスタンスが破棄され、それによって自動的にソケットが閉じられるようになっています。ですが、基本的には不要になったソケットは close
メソッドで明示的に閉じるようにするのが無難だと思います。
close
メソッドの使用例
close
メソッドの使用例を下記に示します。
import socket
# ソケットを生成する
sock = socket.socket()
sock.データを送信するメソッド(引数)
data = sock.データを受信するメソッド(引数)
sock.close()
with
を利用したソケットのクローズ
また、with
文を利用し、with
文のブロックを抜けたときに自動的にソケットを閉じるようにするのでも問題ありません。要は、不要になったソケットが確実に閉じられるようにすることが重要です。
import socket
# ソケットを生成する
with socket.socket() as sock:
sock.データを送信するメソッド(引数)
data = sock.データを受信するメソッド(引数)
# with文のブロックを抜けるとソケットが閉じられる
また、このページでの説明は省略しますが、socket
クラスには shutdown
メソッドが存在します。shutdown
メソッドは、そのソケットでの通信の遮断のみ行うメソッドであり、メソッド名としては意味合いが似ていそうなメソッドではありますが、shutdown
メソッドは “ソケット自体を閉じるメソッドではない” ことに注意してください。
socket
クラスのメソッド
ここまで、ソケットを生成するコンストラクタおよび、ソケットを閉じる close
メソッドの紹介を行ってきました。これらは、ソケット通信を行うために最低限実行が必要な前処理&後処理になります。
ここからは、実際にソケット通信を行うための socket
クラスのメソッドの紹介を行っていきます。ここで紹介するメソッドが socket
クラスのメソッドの全てというわけではありませんが、ここで紹介するメソッドはソケット通信を行う上で非常によく利用するメソッドであり、まずこれらのメソッドを使いこなせば、基本的なソケット通信は行えるようになります。
また、詳細に関しては ソケット通信プログラムの例 で説明しますが、UDP の場合はクライアントとサーバーで下図のような流れでメソッドを実行していくことになります。
さらに、TCP の場合はクライアントとサーバーで下図のような流れでメソッドを実行していくことになります。
このように UDP と TCP とで、さらにクライアントとサーバーとで利用するメソッドが異なります。実行するメソッドが異なる理由や、各種メソッド1つ1つの使い方の詳細に関してはここから説明していきますが、まずは上記のような流れを簡単に理解しておいていただくと、以降の説明がより理解しやすくなると思います。
では、各種メソッドの説明を行っていきます。
sendto
まず最初に紹介するのが sendto
メソッドになります。
sendto
メソッド
sendto
メソッドは、引数 data
(第1引数) に指定したデータを引数 address
(第2引数) に指定した IP アドレス&ポート番号に送信するメソッドになります。sendto
メソッドは、送信に成功すると送信したデータのサイズを返却します。
data
には byte-like なオブジェクト(例えばバイト型のデータ)を指定する必要があります。また、address
にはタプルを指定し、このタプルの第1要素には IP アドレスを示す文字列、第2要素にはポート番号を指定する必要があります。
コンストラクタ実行時に引数 family
に socket.AF_INET
を指定して生成したソケットの場合、IP アドレスを示す文字列には IPv4 形式のものを文字列で指定する必要があります。例えば、'192.168.10.100'
といったように、4つの数字をピリオド (.
) 区切りで並べた文字列を指定します。
上記のように引数を指定して sendto
メソッドを実行すれば、引数 data
に指定したデータが、指定した IP アドレスが割り当てられたネットワークインターフェースに送信されます。さらに通信端末内の指定したポート番号でデータの受信待ちを行なっているソケットに割り振られることになります。
通信で主に行うことはデータの送信とデータの受信であり、前者を行うメソッドの1つが、この sendto
になります。この sendto
は UDP 通信で利用し、TCP 通信では、後述で紹介する send
メソッドを利用することになります。この理由については、次の send で説明します。
sendto
メソッドの使用例
sendto
メソッドの使用例を下記に示します。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
data = b'Hello World'
address = ('192.168.10.100', 40001)
# dataをaddressに送信する
sock.sendto(data, address)
sock.close()
上記を実行することで、data
が IP アドレス 192.168.10.100
の PC (スマホなどの可能性もあり) のポート番号 40001
で受信待ちしているソケットにデータが送信されることになります。また、b'Hello World'
のように文字列の前に b
を指定しているのは、文字列をバイト型に変換するためになります。ソケット通信では直接文字列を送受信することはできないため、バイト型に変換してから sendto
メソッドの引数に指定するようにしています。
スポンサーリンク
send
データを送信するメソッドには、sendto
だけでなく send
も存在します。
send
メソッド
send
は TCP 通信時にデータを送信する際に利用するメソッドとなります。
send
メソッドを実行するときに必須となる引数は data
(第1引数) のみとなります。sendto
同様に、引数 data
には byte-like なオブジェクトを指定する必要があり、引数 data
を適切に指定して send
メソッドを実行することで、data
に指定したデータを送信することができます。ですが、sendto
メソッドとは異なり、引数 address
の指定は不要です。というか指定すると例外が発生します。
つまり、send
メソッド実行時には送信先を指定することができません。では、send
メソッドを実行すると、どこに data
が送信されることになるのでしょうか?
この送信先は、既に接続を確立しているソケットとなります。TCP の場合は通信相手と接続を確立してからデータの送受信を行う必要があり(コネクション型通信)、接続を確立した後に send
メソッドでデータを送信すれば、その接続を確立したソケットにデータが送信されることになります。そのため、send
メソッドの場合は送信先を示すパラメータを引数で指定する必要はありません。それに対し、UDP の場合は接続を確立しませんので、データの送信時に送信先となる IP アドレスとポート番号を明示的に指定する必要があります。そのため、UDP でデータを送信する際には、引数で IP アドレスとポート番号を指定可能な sendto
メソッドを利用することになります。
また、send
メソッドは、sendto
メソッド同様に、送信に成功すると送信したデータのサイズを返却します。
send
メソッドの使用例
下記に send
メソッドの使用例を示します。詳細は後述で解説しますが、下記における connect
が接続を確立するメソッドになります。connect
メソッドについては connect の節で説明します。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
address = ('192.168.10.100', 40001)
sock.connect(address)
data = b'Hello World'
# connectした相手にdataを送信する
sock.send(data)
sock.close()
上記を実行することで、まず IP アドレス 192.168.10.100
の PC (スマホなどの可能性もあり) のポート番号 40001
で受信待ちしているソケットと接続が確立されます。そして、その後、接続を確立した相手に data
が送信されることになります。
sendall
データを送信するメソッドの3つ目は、sendall
になります。
sendall
メソッド
sendall
は send
同様に TCP 通信時にデータを送信する際に利用するメソッドとなります。
使い方に関しては、ほぼ send
メソッドと同じになります。つまり、sendall
メソッドの引数 data
(第1引数) 引数には byte-like なオブジェクトを指定する必要があり、sendall
メソッドを実行すると、接続を確立している相手にデータが送信されることになります。
ただし、send
メソッドとは異なり、sendall
メソッドは返却値が無いので注意してください(None
が返却される)。send
メソッドでは、送信できたデータのサイズが返却されることになります。なので、send
メソッドの場合、返却値から送信できたデータのサイズが確認することが可能です。では、sendall
メソッドの場合、送信できたデータのサイズはどうやって確認すればよいのでしょうか?
sendall
メソッドの場合、送信できたデータのサイズは返却値から確認するような行為は不要となります。なぜなら、sendall
メソッドの場合は、引数に指定したデータの全体が送信されることが保証されるメソッドとなっているからです。したがって、sendall
メソッドが例外の発生無しに終了したのであれば、データの全体が送信されたことになり、引数に指定したデータのサイズそのものが送信できたデータのサイズとなります(送信の途中で通信が切断された等の理由でデータの全体が送信できない場合は例外が発生します)。
逆に言うと、send
メソッドの場合は、引数で指定したデータ全体が送信されることが保証されないメソッドとなっています。TCP の仕組み上、相手と通信可能な状態でデータを送信すれば相手に確実にデータが届けられるようになっているはずですが、send
メソッドの場合は、おそらく送信自体が行われない場合があるようです。なので、send
メソッドの場合、送信しようとしたデータのサイズよりも返却値が小さい場合は、送信できていないデータを送信するために send
メソッドを再度実行するような処理が必要となります。
ただ、これは単に私の経験上の話ですが、今のところ send
メソッドで引数に指定したデータの全体が送信できなかったことは無いです。ですが、メソッドの仕組みや役割で考えると、send
メソッドよりも sendall
メソッドを利用する方が無難だと思います。
sendall
メソッドの使用例
下記に sendall
メソッドの使用例を示します。といっても、send の節で紹介した send
メソッドの使用例における なので、send
メソッド実行部分を sendall
に置き換えただけになります。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
address = ('192.168.10.100', 40001)
sock.connect(address)
data = b'Hello World'
# connectした相手にdataを送信する
sock.sendall(data)
sock.close()
recv
ここまで sendto
と send
の紹介を行いましたが、これらはデータを送信するメソッドになります。通信では互いにデータのやり取りを行うため、データの送信だけではなくデータの受信も必要となります。
recv
メソッド
そのデータの受信を行うメソッドの1つが recv
になります。recv
メソッドは、引数 bufsize
(第1引数) に一度に受信するデータの最大サイズを整数として指定して実行します。また、recv
メソッドは TCP でも UDP でも利用可能なメソッドとなります。
recv
メソッドを実行すると、ソケットは、そのソケットに関連付けされたネットワークインターフェース・ポート番号に対しての受信待ちを行います。そして、その設定されたネットワークインターフェース・ポート番号に対してデータが送信されてくるとデータの受信(読み込み)が行われ、recv
メソッドがその受信したデータを返却します。返却されるデータはバイト型となります。
上記の説明からも分かるように、recv
メソッドを実行するためには、その recv
メソッドを実行するソケットに対してネットワークインターフェース・ポート番号を事前に “関連付け” しておく必要があります。この設定を行ってから recv
メソッドを実行すれば、複数存在するネットワークインターフェースのうち、その設定されたネットワークインターフェースに対しての受信待ちが行われるようになります。そして、そのソケットに関連付けされたポート番号宛に送信されてきたデータが送信されてくると、ソケットがそのデータを受信することになります。
この関連付けに関しては socket
クラスのインスタンスに bind
メソッドを実行させることで行うことができます。bind
メソッドに関しては bind の節で解説します。
recv
メソッドの使用例
下記に recv
メソッドの使用例を示します。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('192.168.10.5', 40001))
# bindしたaddressからデータを受信
data = sock.recv(1024)
print(data)
sock.close()
上記を実行することで、192.168.10.100
が割り当てられたネットワークインターフェース&ポート番号 40001
に対して受信待ちが行われます。そして、これらのネットワークインターフェース・ポート番号宛にデータが送信されてくるとデータを受信し、受信したデータは recv
メソッドの返却値として得ることができます。
スポンサーリンク
recvfrom
recv
同様に、recvfrom
もデータを受信するメソッドとなります。
recvfrom
メソッド
recvfrom
の使い方は recv
メソッドとほぼ同じです。
ですが、返却値が2つであるという点に注意してください。1つ目の返却値は recv
同様に受信したデータになります。2つ目の返却値は、データを送信してきたソケットの情報になります。より具体的には、データを送信してきたソケットに応答するためのネットワークインターフェースとポート番頭を要素とするタプルになります。
したがって、UDP 通信の場合、recvfrom
の返却値の2つ目をそのまま sendto
の第2引数(引数 addr
)に指定して sendto
メソッドを実行すれば、データを送信してきた相手に応答となるデータを送り返すことができることになります。recvfrom
も TCP と UDP の両方で利用可能なメソッドとなりますが、この “データを送信してきた相手へのデータの返却” を実現するために、特に UDP でのデータの受信では recvfrom
を利用する機会が多いです。
TCP の場合は、send
メソッドを実行すれば既に接続を確立しているソケットにデータが送信されるようになっているため、わざわざ recvfrom
メソッド利用して相手のソケットの情報を取得する機会は少ないです。
recvfrom
メソッドの使用例
下記に recvfrom
メソッドの使用例を示します。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('192.168.10.5', 40001))
# bindしたaddressからデータを受信
data, addr = sock.recvfrom(1024)
# データを送信してきた相手に返事を送信
sock.sendto(b'Thank you!', addr)
sock.close()
上記を実行することで、まず 192.168.10.100
が割り当てられたネットワークインターフェース&ポート番号 40001
に対して受信待ちが行われます。そして、これらのネットワークインターフェース・ポート番号宛にデータが送信されてくるとデータが受信されることになります。さらに、上記の場合は、データを送信してきたソケットの情報が recvfrom
の2つ目の返却値 addr
として得られます。なので、sock.sendto(b'Thank you!', addr)
を実行することで、そのデータを送信してきたソケットに対して b'Thank you!'
が返却されることになります。
bind
ここまで、データの送受信を行うメソッドの紹介を行ってきました。通信で主に行うことは「データを相手に届けること」「データを相手から受け取ること」となり、これらを行うのがここまで紹介してきたメソッドとなります。
ただ、これらを行うためには、その前準備や後処理が必要となり、それらを行うためのメソッドも適切に使いこなす必要があります。ここからは、データの送受信を行うために実行が必要となるメソッドを紹介していきます。
bind
メソッド
最初に紹介するメソッドは bind
になります。
IP アドレスとポート番号 等でも説明したように、特定の端末内の特定のソケットにデータを届けるためには、データ送信時に IP アドレスとポート番号を指定する必要があります。これにより、指定された IP アドレスが割り当てられたネットワークインターフェースにデータが届けられ、さらに、指定されたポート番号で受信待ちしているソケットにデータが割り振られることになります(プロトコル の節で説明したように、このポート番号へのソケットの割り振りまでは socket
クラスのメソッド内で行われることになります)。
逆に言えば、データを受信する側のソケットは、相手からデータを受け取るために特定のポート番号で受信待ちをしておく必要があります。さらに、端末内に複数のネットワークインターフェースが存在する場合もあるため、どのネットワークインターフェースからデータを受信するのかも設定しておく必要があります。
これらの設定をソケットに対して実施するのが bind
メソッドになります。そして、この設定は “バインド” や “関連付け” と呼ばれます。
bind
メソッドは、引数 address
(第1引数) にネットワークインターフェースの IP アドレスおよびポート番号を要素とする下記のような形式のタプルを指定して実行します。
(IPアドレス, ポート番号)
これにより、bind
メソッドを実行したソケットに、引数に指定した IP アドレスのネットワークインターフェースおよびポート番号が関連付けされることになります。そして、recv で説明したように、bind
実行後に recv
メソッドや recvfrom
メソッドを実行すれば、それらの関連付けされたネットワークインターフェースおよびポート番号に対して受信待ちが行われます。
また、後述で紹介する listen
に関しても、受信待ちするのが接続要求と呼ばれる特殊なデータではあるものの、結局 recv
同様にデータの受信待ちを行うためのメソッドですので、listen
を実行する前にも bind
メソッドによるネットワークインターフェース&ポート番号の関連付けが必要となります。
少しややこしい話ではあるのですが、まずは、何かしらの受信待ちを行うためには「待ちの対象となるネットワークインターフェースやポート番号を事前にソケットに関連付けしておく必要がある」とシンプルに考えていただくのが良いと思います。そして、その関連付けを行うための方法の1つが bind
メソッドの実行になります。
ただ、ソケットに対して関連付けられる IP アドレスは1つのみではありません。bind
実行時に IP アドレスに '0.0.0.0'
を指定すれば、スクリプトを実行した PC の全ネットワークインターフェースをソケットに設定することもできます。それに対し、関連付けられるポート番号は1つのみとなります。
bind
メソッドの使用例
下記に bind
メソッドの使用例を示します。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 40001))
# bindしたaddressからデータを受信
data = sock.recv(1024)
sock.close()
上記を実行すれば、まず bind
メソッドによって sock
に対して PC 内の全ネットワークインターフェース&ポート番号 40001
が関連付けられ、さらに recv
メソッドによって、これらのネットワークインターフェース・ポート番号宛にデータが送信されてくるとデータが受信されることになります。
bind
メソッドを利用しない関連付け
このソケットへのネットワークインターフェースとポート番号の関連付けは、bind
メソッドの実行なしに自動的に行われることもあります。
例えば、bind
メソッドを実行する前に sendto で紹介した sendto
メソッドや、後述の connect で紹介する connect
メソッドを実行すれば、それらを実行したソケットに対してネットワークインターフェースとポート番号が自動的に関連付けされます。そのため、これらを実行した後に recv
メソッド等でデータの受信待ちを行うのであれば bind
メソッドを実行する必要がなく、その自動的に関連付けされたネットワークインターフェースとポート番号に対して受信待ちが行われることになります。
上記のような仕組みが存在するため、最初に sendto
メソッドや connect
メソッドを実行するクライアント側のプログラムに関しては bind
メソッドの実行なしに recv
メソッド等でのデータの受信を行うことが可能です(クライアントのプログラムの処理の流れに関しては ソケット通信プログラムの例 で詳細を説明します)。
逆に、サーバープログラムに関しては、データの受信を行う recv
メソッドや listen
メソッドを最初に実行する必要があるため、bind
メソッドの実行が必須となります。
また、明示的にポート番号を指定したいような場合や、自動的なポート番号等の関連付を避けたいような場合は、クライアント側でも bind
メソッドを実行してソケットに関連付けするポート番号等を明示的に指定するようなことも可能です。
connect
connect
は、相手に接続要求を送信するメソッドになります。
connect
メソッド
TCP 通信においては、データの送受信を行う前に通信相手と接続(コネクション)を確立しておく必要があります。この接続を確立するために、相手に対して接続の要求を送信するメソッドが connect
になります。
この connect
を実行するのはクライアント側のプログラムとなります。そして、後述で説明する accept
メソッドによってサーバーが接続受付を行うことで、クライアントとサーバー間で接続が確立されることになります。
そして、接続を確立した後は基本的には接続相手とデータの送受信を行うことになります。そのため、send の節で説明したように、接続を確立した後に send
メソッドを実行すれば、メソッド実行時に宛先を指定しなくても接続相手に対してデータが送信されることになります。
また、上の図からも分かるように、サーバー側ではデータの送受信と connect
の受信待ちとでは異なるソケットを利用して通信を行うことになります。この辺りは後述で解説していきます。
connect
メソッドの使用例
下記に connect
メソッドの使用例を示します。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
address = ('192.168.10.100', 40001)
sock.connect(address)
data = b'Hello World'
# connectした相手にdataを送信する
sock.send(data)
sock.close()
上記を実行すれば、まず IP アドレス 192.168.10.100
が割り当てられたネットワークインターフェースに接続要求が送信され、さらにその通信端末内でポート番号 40001
に対して接続要求が割り振られます。接続要求を受け取った相手によって接続が受け付けられれば接続が確立され、その相手に send
メソッドによって data
が送信されることになります。
スポンサーリンク
listen
先ほどの connect の説明の中で、connect
はクライアントからサーバーに対して接続要求を送信するメソッドであることを説明しました。この接続要求を受け取って接続を確立するためには、サーバー側では2つのメソッドを実行する必要があります。
listen
メソッド
その1つ目のメソッドが、ここで説明する listen
になります。listen
はソケットをリッスン状態に移行するためのメソッドとなります。リッスン状態とは、特定のポートを空けて相手からのデータの受信が可能となっている状態となります。TCP 通信の場合、サーバーは listen
メソッドを実行してクライアントでの connect
実行による接続要求を受信可能な状態にしておく必要があります。
サーバーが listen
メソッドを実行していない場合、そのサーバーは接続要求の受信が不可の状態となるため、この状態でクライアントが connect
で接続要求を送信するとクライアント側で ConnectionRefuesedError
の例外が発生することになります。
なので、クライアントが connect
を実行するのであれば、基本的にサーバーは connect
が実行される前に listen
メソッドを実行しておく必要があります。
また、ソケットをリッスン状態にするためには、あらかじめ接続要求の受信を行うネットワークインターフェース・ポート番号をソケットに関連付けしておく必要があります。つまり、listen
メソッドを実行するためには、事前に bind
メソッドを実行してソケットに対してネットワークインターフェース・ポート番号の関連付けを行っておく必要があります。
接続要求のキュー
もう少し listen
メソッドの詳細を説明しておくと、listen
メソッドを実行することで接続要求を一時保存するためのキュー(バッファー)が用意されることになります。接続要求は複数のクライアントから送信されてくる可能性もあるため、複数のクライアントから同じタイミングで接続要求が送信されてくる可能性もあります。また、特定のクライアントと接続を確立してデータの送受信を行っている間に他のクライアントから接続要求が送信されてくる可能性もあります。そんな時でも、一旦接続要求を保存しておけるように保存先となるキューが必要となります。イメージとしては、このキューを用意するのが isten
メソッドで、さらに、このキューから接続要求を取得するのが、次に説明する accept
メソッドとなります。
そして、このキューのサイズは変更可能で、このサイズは listen
メソッドの第1引数(引数 backlog
)を指定することで変更することができます。指定しなければ、適当なデフォルトのサイズでキューが用意されることになります。
listen
メソッドの使用例
listen
メソッドの使用例に関しては、次の accept
メソッドの使用例と一緒に紹介したいと思います。
accept
先ほど紹介した listen
メソッドの次に実行する必要のあるメソッドが accept
となります。
accept
メソッド
accept
は、クライアントからの接続要求に対して接続の受付(接続の許可)を行い、さらに接続の確立を行うメソッドになります。accept
メソッドは引数なしで実行可能です。
また、accept
を実行する前には、listen
メソッドを実行してソケットを接続要求を受信可能な状態にしておく必要があります。listen
メソッドを実行せずに accept
メソッドを実行すると OSError
の例外が発生するので注意してください。
もう少し詳細を説明すると、listen で説明したように、listen
メソッドを実行したソケットは接続要求が受け付け可能となり、送信されてきた接続要求はキューに保存されます。そして、accept
メソッドが実行された際に、accept
メソッドがキューから接続要求を取得し、その接続要求の受付&接続の確立を行います。キューが空の場合、すなわち接続要求が送信されてきていない時は、accept
メソッドは接続要求がキューに保存されるまで待ちの状態となります。
また、接続が確立できれば、accept
メソッドは socket
クラスの新たなインスタンスと接続の要求を送信してきた相手の情報の2つのオブジェクトを返却して終了します。後者は recvfrom
で説明した2つ目の返却値と同様なので説明は省略します。
ポイントになるのが1つ目の返却値となる socket
クラスの新たなインスタンスで、このインスタンスは接続を確立した状態のソケットとなります。そして、このソケットは accept
メソッドを実行したインスタンスとは異なるものとなります。
accept
メソッドを実行したインスタンスのソケットは accept
メソッドを実行してもリッスン状態のままとなっています。なので、サーバーが accept
メソッド実行後もクライアントは接続要求を送信することが可能です(接続要求を一時保存するキューが空いていれば)。逆に言えば、リッスン状態なので、このソケットを利用したデータの送受信は不可です。
それに対し、accept
メソッドが接続確立後に返却する socket
クラスのインスタンスは、それとは異なるソケットに対応するインスタンスであり、このソケットは接続確立状態(エスタブリッシュ)となっています。そして、この状態のソケットであれば、データの送受信を行うことが可能です。
なので、accept
メソッドが接続確立をしてメソッドが完了した後は、accept
メソッドが返却したインスタンスを利用してデータの送受信を行う必要があります。
また、accept
メソッドから返却されたインスタンスに関しても、不要になったら close
する必要があります。
accept
メソッドの使用例
下記に accept
メソッドの使用例を示します。listen
メソッドの使用例に関しても、下記の例で理解していただけるのではないかと思います。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 40001))
sock.listen(5)
e_sock, addr = sock.accept()
data = e_sock.recv(1024)
e_sock.close()
sock.close()
上記を実行すれば、bind
メソッドと listen
メソッドの実行により、ソケットは本スクリプトを実行した端末に存在する全ネットワークインターフェース&ポート番号 40001
に対する接続要求を受信可能となります。そして、accept
メソッドの実行により、接続要求の受信待ち状態となります。さらに、クライアントから接続要求が送信されてきた際に、待ち状態だった accept
メソッドが動作し、その接続要求の受付と接続の確立が行われます。そして、接続確立後のソケットが、1つ目の返却値として返却されることになります。
このソケットは接続確立状態となっていますので、後は、このソケット e_sock
から recv
メソッドや send
メソッドを実行してデータの送受信を行うことになります。
基本的なメソッドの紹介は以上となります。次は、ソケット通信プログラムの例を確認していきましょう!
ソケット通信プログラムの例
では、次はソケット通信プログラムの例を示していきたいと思います。
プロトコル の節で説明したように、ここまで紹介してきたメソッドを適切に実行することで TCP or UDP のプロトコルに従った通信が行われるようになります。ただし、アプリケーション層のプロトコルに関してはソケット通信のライブラリは関与しません。そのため、ソケット通信プログラムを開発していく際には、まずは従うべきアプリケーション層のプロトコルを決め、それに従うようデータの送受信を行うサーバープログラムとクライアントプログラムを開発していく必要があります。
今回は下記のプロトコルに従うプログラムを開発していきたいと思います。サーバー目線で記述したプロトコル、というかルールになります。これは私が独自に決めたものですが、これらを他のプロトコルに当てはめれば、そのプロトコルに従った通信プログラムを開発していけることになります。
- サーバーは半角英数字の文字列をクライアントから受信する
- サーバーは受信した文字列を大文字に変換した結果をクライアントに送信する
非常に簡単なプロトコルになりますが、おそらくこのページをここまで読んでくださっている方はソケット通信初心者の方が多いと思いますので、ソケット通信の最初の一歩となるプログラム開発には丁度良い難易度になると思います。
ここからは、このプロトコルに従ったサーバーとクライアントのスクリプトの例を示していきます。さらに、TCP と UDP とで実行するメソッド等が異なるため、TCP 通信を行うサーバーとクライアント、さらに UDP 通信を行うサーバーとクライアントの計4つのスクリプト例を示していきます。
さて、上記のプロトコルを読んで、どんなプログラムを開発すればよいかが大体イメージできたでしょうか?
データの送受信のみに焦点を当てれば、まずサーバーは最初にクライアントからのデータの受信待ちを行う必要があります。そして、サーバーはクライアントからデータを受信したら受信したデータを大文字に変換し、それをクライアントに送信することになります。これだけです。サーバーとクライアント で解説したように、サーバーが最初に行う処理は、ソケット利用のための準備や接続の確立等を除けば基本的には受信待ちになります。
それに対し、クライアントは最初にデータの送信を行う必要があります。そして、データ送信後にサーバーがデータを送信してくるため、それを受信するためにデータの受信待ちを行う必要があります。
また、クライアントからサーバーへ送信するデータおよびサーバーからクライアントへ送信するデータのフォーマットは文字列となります。もっと細かく言えば半角英数字の文字列ですね。ただし、ソケット通信で送信可能なデータはバイト型であるため、データを送信する際には文字列をバイト型に変換してから送信する必要がありますし、データを受信した際にはバイト型のデータを文字列に変換する必要があります。
さらに、IP アドレスとポート番号 や socket クラスのメソッド で説明したように、sendto
メソッドによるデータの送信時や connect
メソッドによる接続要求の送信時には、送信先となる IP アドレスとポート番号を引数で指定する必要があります。さらに、サーバー側が recv
or recvfrom
メソッドによってデータを受信したり、listen
メソッドによって接続要求を受信可能したりする際には、bind
メソッドによってソケットに対してネットワークインターフェースとポート番号を関連付け、どのネットワークインターフェース&ポート番号に対して受信待ちするのかを設定しておく必要があります。今回は、サーバーは全ネットワークインターフェースに対してポート番号 40001
で受信待ちを行うように設定したいと思います。また、今回はサーバーとクライアントが同じ PC 上で動作することを前提に開発を行っていきたいと思います。したがってクライアントは 127.0.0.1
のポート番号 40001
にデータを送信すればよいことになります。
さらにさらに、プロトコル や socket クラスのメソッド で説明したように、TCP と UDP によって実行するメソッドを使い分ける必要がありますし、ソケットの生成 で説明したように、そもそも通信を TCP or UDP に従って行うのかをソケット生成時に引数で指定する必要があります。
ということで、上記のような簡単なプロトコルで通信を行うだけでも、このページでここまで解説してきた内容全てを考慮してプログラムを開発していく必要があります。是非、各スクリプトで実行するメソッドの意味合いや、なぜそのメソッドの実行が必要なのかを考えながら紹介するスクリプトを読んでみていただければと思います。とはいえ、各プログラムのスクリプトはそこまで難しいものではなく、思ったよりもシンプルだと思います。なので、気軽にこの先も読み進めていただければと思います!
スポンサーリンク
UDP 通信を行うプログラム
前置きが長くなりましたが、ここからは実際にスクリプトを作成する上でのポイントと、実際のスクリプトのソースコードを示していきたいと思います。
まずは UDP 通信を行うプログラムのスクリプトの作り方について説明していきます。
プロトコル で解説したように、UDP の場合は接続の確立が不要なので、処理の流れは TCP の場合に比べてシンプルになります。
まずサーバー側はクライアントからのデータの受信を行うため、ソケットの生成および bind
メソッドの実行を行った後、recvfrom
でデータの受信待ちを行います。ここまでが UDP 通信におけるサーバーの処理の基本パターンとなります。また、前述で示したプロトコルに従うため、データの受信後には文字列を大文字に変換した結果をクライアントに返却する必要があります。この返却には sendto
メソッドを利用し、recvfrom
の2つ目の返却値として得られたデータを第2引数に指定して sendto
メソッドを実行してやればクライアントにデータを送り返すことができます。
socket クラスのメソッド で説明したように UDP ではデータを送信するときに sendto
メソッドを利用します。また、データを受信した後に、データの送信元に処理結果を応答や返却したい場合は、データ受信時に recvfrom
を利用するのが便利です。
クライアント側に関しては、ソケット生成後に sendto
メソッドでサーバーが受信待ちをしているネットワークインターフェース・ポート番号に対してデータを送信します。今回は、前述のとおり 127.0.0.1
のポート番号 40001
に対してデータを送信します。さらに、今回従うプロトコルでは、クライアントからデータを受け取ったサーバーがデータを返却してくるようになっているため、データの送信後には recv
or recvfrom
でデータの受信を行う必要があります。bind で説明したように、クライアント側のソケットに関しては、ネットワークインターフェース・ポート番号が自動的に関連付けされるため bind
は不要になります。
ということで、UDP 通信を行うサーバー・クライアント間のメソッドの実行の流れは下図のようなシーケンスとなります。あとは、このシーケンスに合わせてサーバーとクライアントのスクリプトをコーディングしてやれば良いことになります。
UDP 通信のサーバー
続いて、具体的なスクリプト例を示していきます。UDP 通信のサーバープログラムのスクリプトの例は下記のようになります。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 40001))
# 無限ループで常駐させる
while True:
# データの受信待ち
data, addr = sock.recvfrom(1024)
# サービス・受信データに応じた処理
str_data = data.decode()
upper_str_data = str_data.upper()
# 処理結果の送信
send_data = upper_str_data.encode()
sock.sendto(send_data, addr)
sock.close()
上記スクリプトのポイントを簡単に説明しておきます。
上記スクリプトでは、まず socket.socket
によってソケットの生成を行なっています。第2引数(引数 type
)に socket.SOCK_DGRAM
を指定しているため、このソケットでは UDP で通信が行われることになります。
さらに、次に bind
を実行して、このプログラムを実行する端末内に存在する全ネットワークインターフェース&ポート番号 40001
とを関連付けています。bind で説明したように、bind
メソッドの引数に指定するタプルの第1要素を '0.0.0.0'
とすることで、全ネットワークインターフェースとソケットを関連付けることができます。
あとは、クライアントからデータが送信されてくるのを recvfrom
で待ち、データが受信できれば sendto
で応答を返却するだけです。recvfrom
の返却値の2つ目はデータの送信元のネットワークインターフェース・ポート番号を要素とするタプルとなるため、これを sendto
の第2引数(引数 address
)に指定してやれば、データの送信元のクライアントに応答をデータを送信することが可能です。
サーバーは、クライアントが好きなタイミングでサービスの利用を要求できるように、基本的に常駐して常にクライアントからのデータの受信待ちを行なっておく必要があります。そのため、上記のような無限ループの中で受信待ちしておく、というのがサーバーの基本的な動作パターンとなります。これは、後述で紹介する TCP の場合も同様です。
また、recvfrom
や recv
で受信するデータはバイト型、さらに sendto
や send
で送信するデータもバイト型となり、受信したデータはそのままでは文字列として扱えませんし、文字列はそのままでは送信することができません。そのため、受信したバイト型のデータを文字列に変換するために decode
を、文字列をバイト型に変換するために encode
を実行するようにしています。これに関しては、説明は省略しますが、後述で紹介するスクリプトでも同様となります。
UDP 通信のクライアント
次に、クライアントプログラムのスクリプトを紹介します。
先ほど示したサーバープログラムの相手となるクライアントプログラムのスクリプトの例は下記のようになります。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
input_str_data = input('文字列を入力してください : ')
# データの送信
data = input_str_data.encode()
address = ('127.0.0.1', 40001)
sock.sendto(data, address)
# データの受信
data = sock.recv(1024)
str_data = data.decode()
print(str_data)
sock.close()
サーバーは基本的に常にデータの受信待ちを行っているのですから、クライアントは好きなタイミングでデータの送信を行ってサービスの提供を要求すればよいことになります。なので、サーバープログラムを起動した後であれば、基本的にいつでも sendto
でデータを送信しても良いことになります。上記では、ユーザーが文字列の入力を完了したタイミングで sendto
メソッドを実行するようにしています。
そして、UDP 通信のサーバー で示したスクリプトでは、サーバーはデータ受信後に応答となるデータをクライアントに返却してくるようになっているため、クライアントはデータ送信後にデータの受信待ちを行う必要があります。この受信待ちを、上記では recv
メソッドで行っています。bind で説明したように、クライアント側のソケットは自動的な関連付けが行われるため、受信待ちを行う場合でも bind
の実行は必須ではありません。
こんな感じで、特に UDP 通信の場合、クライアントは最初にデータを sendto
メソッドで送信し、その結果を recv
や recvfrom
メソッド等で受け取るという流れが基本的な動作になると思います。
ただ、1つ注意点があって、それはクライアントからのデータの送信前にサーバー側に受信待ちを行わせておく必要があるという点になります。そのため、基本的にはサーバーを先に起動し、その後にクライアントからデータの送信を行うような流れで動作させる必要があります。そういった背景もあり、サーバープログラムに関しては、PC 等の通信端末が起動するタイミングで一緒に起動し、その後ずっと受信待ちを続けるような動作になっていることが多いです。
UDP 通信を行うプログラムの実行方法
UDP 通信のサーバー と UDP 通信のクライアント とで示したスクリプトの実行方法を紹介しておきます。
まず、UDP 通信のサーバー で示したスクリプトを udp_server.py
という名前、さらに UDP 通信のクライアント で示したスクリプトを udp_client.py
という名前で適当な同じフォルダに保存してください。そして、ターミナルアプリ(Windows の場合はコマンドプロンプトや PowerShell 等)を2つ起動し、cd
コマンドを実行して両方のターミナルでスクリプトを保存したフォルダーに移動してください。
続いて、一方側のターミナルで下記のように udp_server.py
を python
コマンドで実行してください。このターミナルでの操作は以上で終了です。これによりサーバーが立ち上がり、クライアントからのデータの送信を待ち続けるようになります。
python udp_server.py
次に、他方側のターミナルで下記のように udp_client.py
を python
コマンドで実行してください。
python udp_client.py
すると、文字列の入力が促されますので、適当に小文字で文字列を入力してエンターキーを押してください。
文字列を入力してください :
これにより、入力した文字列を大文字に変換した結果が表示されるはずです。
文字列を入力してください : hello world HELLO WORLD
これは、サーバーがクライアントから受け取った文字列を大文字に変換した結果となります。この結果をクライアントが受け取り、それを表示しています。
簡単ですが、これによりサーバーとクライアント間でデータの送受信を行う様子が確認できたことになります。また、サーバーは起動したままになっていますので、再度 udp_client.py
を実行して同様の操作を行えば、また文字列を大文字に変換した結果が表示されることを確認できるはずです。こんな感じで、サーバーは常に起動してクライアントからのデータの受信を待ち続けるようにしておくことが多いです。サーバーを終了させたい場合は、udp_server.py
を実行した方のターミナルで control
+ c
を入力してプログラムを強制終了させてください。
TCP 通信を行うプログラム
次は、TCP 通信を行うプログラムを紹介していきます。
プロトコル の節で解説したように、TCP 通信ではデータの送受信を行う前に、通信を行う2つのソケット間で接続を確立しておく必要があります。この接続を確立する必要がある点が UDP 通信の場合との決定的な違いで、この違いがあるために若干 UDP 通信に比べて TCP 通信の方が処理が複雑になります。ですが、接続を確立してしまえば、データの送受信は接続を確立した相手と勝手に行われるようになるため、わざわざ送信時に IP アドレスやポート番号を指定する必要がなく、データの送受信自体の処理はシンプルになります。
また、TCP と UDP とでデータの送り方の違いがあるものの、アプリケーション層としては送受信するデータのフォーマットやデータの送受信の順序・タイミングは TCP と UDP とで同じとなります。したがって、ソケット通信プログラムとしては、接続を確立した後に送受信するデータのフォーマットや送受信の順序・タイミング等は UDP の場合と同じと考えて問題ありません。実際には、TCP と UDP とでは送受信されるデータのヘッダーなどが異なりますし、送受信の順序も異なるのですが、このあたりの違いは socket
クラス側で吸収してくれます。なので、ソケット通信プログラム開発者は、このあたりを考慮して開発する必要はありません。
具体的なプログラムの処理の流れを解説すると、まずサーバーはクライアントからの接続要求を受信するために、ソケット生成および bind
実行後に listen
メソッドと accept
メソッドを実行する必要があります。クライアントから接続要求が送信されてくれば、accept
メソッドにより接続が確立されますので、後はクライアントからのデータの受信待ちを行えば良いです。ここまでが TCP 通信を行う場合のサーバーの典型的な処理の流れとなります。その後は、アプリケーション層のプロトコルに合わせてデータの送受信や提供するサービスに応じた処理を行うことになります。この時に利用するソケットは接続確立済みのもの、すなわち accept
メソッドが返却したソケットである必要があります。
それに対し、クライアントはサーバーとデータの送受信を行うために、まずは connect
メソッドを実行して接続要求をサーバーに対して送信します。これにより、サーバーが接続要求の受信待ち状態、すなわち accept
実行中であれば、サーバーによって接続要求が受け入れられて接続の確立が行われます。接続が確立されれば、クライアントはサーバーに対してデータの送信を行います。今回従うプロトコルにおいては、サーバーがクライアントに応答を返却するようになっているため、クライアントはその応答を受信するために受信待ちを行います。
TCP 通信の場合は、接続が確立されたソケット、すなわちサーバー側における accept
メソッドから返却されるソケット、および、クライアント側における connect
メソッドを実行したソケットでデータの送受信を行えば、自動的に接続を確立しているソケットの間でデータの送受信が行われることになるため、データの送信時には sendto
で IP アドレスやポート番号を指定する必要はありません。その代わりにデータの送信時には send
を利用することになります。
したがって、TCP 通信におけるクライアントとサーバーのメソッド実行の流れは下記のようなシーケンスとなります。あとは、このシーケンスに合わせてサーバーとクライアントのスクリプトをコーディングしてやれば良いことになります。
TCP 通信のサーバー
続いて、具体的なスクリプト例を示していきます。TCP 通信のサーバープログラムのスクリプトの例は下記のようになります。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 40001))
sock.listen(5)
# 無限ループで常駐させる
while True:
# 接続要求の受付 / 接続の確立
e_sock, addr = sock.accept()
# データの受信待ち
data = e_sock.recv(1024)
# サービス・受信データに応じた処理
str_data = data.decode()
upper_str_data = str_data.upper()
# 処理結果の送信
send_data = upper_str_data.encode()
e_sock.send(send_data)
e_sock.close()
sock.close()
UDP 通信のサーバー で示したスクリプトとの差を比較していただければ、TCP 通信と UDP 通信とでサーバー側のスクリプトがどのように異なるのかを理解していただけるかと思います。
まず、TCP 通信を行うのですから、ソケット生成時に実行する socket.socket
の第2引数には socket.SOCK_STREAM
を指定する必要があります。bind
に関しては UDP 通信の時と同様に実行してやれば良いのですが、その次に接続の確立を行うために listen
と accept
を実行する必要があります。accept
を実行すれば、クライアントから接続要求が送信してくるまで accept
は終了せずに待機することになります。なので、上記のように while
ループの中で accept
を実行すれば、基本的にサーバーは常駐してクライアントからの接続要求待ち続ける動作を実現することができます。
クライアントから接続要求を受け取れば、accept
は終了して新たなソケットを返却します。このソケットがクライアントと接続確立済みのものとなりますので、あとはこのソケットを利用してデータの送受信を行えば良いだけです。ただし、データの送信先は接続確立済みのソケットとなりますので、sendto
ではなく send
メソッドで行う必要があります。
クライアントとの一連のデータの送受信が完了すれば、その接続確立済みのソケットは不要になるため close
メソッドでソケットを閉じ、ループの最初に戻って accept
を実行することになります。これにより、クライアントからの新たな接続要求を受け取ることができるようになります。
TCP 通信のクライアント
また、先ほど示したサーバープログラムの相手となるクライアントプログラムの例は下記のようになります。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
input_str_data = input('文字列を入力してください : ')
# 接続要求の送信
address = ('127.0.0.1', 40001)
sock.connect(address)
# データの送信
data = input_str_data.encode()
sock.send(data)
# データの受信
data = sock.recv(1024)
str_data = data.decode()
print(str_data)
sock.close()
こちらも、UDP 通信のクライアント で示したスクリプトとの差を比較していただくと、TCP 通信と UDP 通信とのクライアントで実行すべきメソッドが理解しやすいと思います。
やはり UDP 通信のクライアントと比べて一番大きな差があるのは connect
メソッドを実行する必要がある点になると思います。この connect
メソッドの実行により、引数で指定した IP アドレス・ポート番号に接続要求が送信されることになります。この引数には、接続要求を受け付け可能、すなわち listen
メソッドを実行したソケットに関連付けられている IP アドレスとポート番号を指定する必要があります。
さらにサーバー側の accept
メソッドにより接続要求が受け付けられれば、connect
を実行したソケットが接続確立済みの状態となります。あとは、このソケットを使ってデータの送受信を行えば良いだけです。このソケットで send
メソッドを実行すれば、接続確立済みの相手側のソケットに対してデータが送信されることになるため、UDP 通信の時のように sendto
メソッドを使ってわざわざ送信先を引数で指定することは不要となります。
TCP 通信を行うプログラムの実行方法
スクリプトの実行の仕方に関しては UDP 通信を行うプログラムの実行方法 で説明した実行の仕方と同様なので説明は省略させていただきます。また、得られる結果も TCP と UDP とで同じになるはずです。このように、スクリプトの作り方は TCP / UDP で異なるものの、どちらでも同様のシステムが作成可能であることが確認できると思います。ただし、プロトコル で説明したようにデータ送受信の仕方が TCP と UDP とで異なるため、実際には通信の信頼性や通信速度が異なることになります。
その他の通信ライブラリ・通信モジュールとの関係性
ここまでの説明を理解していただければ、ソケットを利用した TCP 通信 or UDP 通信を行うサーバー・クライアントを開発し、お互いに通信し合うシステムやプログラムを開発できるようになると思います。
ソケット通信を利用することができるようになれば、開発可能なプログラムの幅が一気に広がるので、是非ソケット通信を利用した様々なプログラムの開発に挑戦してみてください。
ただし、全ての通信プログラムを socket
モジュールを使って実現する必要はありません。他にも通信に利用できるライブラリやモジュールが存在します。このページの最後に、これらのライブラリ・モジュールと、ソケット通信との関係性について解説しておきます。
ここまで、ソケット通信のライブラリ・モジュールではトランスポート層以下の通信・制御を行ってくれることを説明しました。ですが、アプリケーション層のプロトコルに従った処理に関しては開発者自身が実装する必要があります。
なんですが、実は Python にはアプリケーション層のプロトコルに関する通信・制御を行ってくれるライブラリ・モジュールも数多く存在しています。
HTTP であれば、requests
モジュール等がそれにあたります。この requests
は HTTP (のクライアント) に特化したモジュールであり、例えば特定のメソッドに送信したいデータの本文を指定さえしてやれば、それがメソッド内で HTTP に従ったフォーマットのデータに変換され、そのデータが通信相手に送信されるようになっています。そして、そのメソッドの返却値として、通信相手から受信したデータが得られるようになっています。
つまり、socket
モジュールではトランスポート層以下のプロトコルに従った制御・通信しか行われませんが、アプリケーション層のプロトコルまで含めた制御・通信を行ってくれるモジュールが存在します。この場合は、メソッドの実行等を行うだけで、アプリケーション層のプロトコルに従ったフォーマットのデータの送信及びプロトコルに従った順序・タイミングでのデータの送受信まで行われることになります。おそらく、メソッド内部でソケット通信が行われているのだとは思うのですが、これらを利用することで開発者はソケット通信を意識することなく通信を行うことが可能となります。
なので、アプリケーション層のプロトコル用のモジュールが存在するのであれば、実はわざわざ socket
モジュールを利用してソケット通信を行わなくても通信を行うことは可能です。なんですが、例えば requests
モジュールが HTTP のみに対応しているように、これらは特定のプロトコルにのみ対応したモジュールとなりますので、そのプロトコルに対応したモジュールが存在しなければ socket
モジュールを利用し、アプリケーション層のプロトコル部分に関しては開発者が実装しなければならないことになります。また、独自のプロトコルを採用するような場合も、socket
モジュールを利用し、その独自のプロトコルに従うように開発者が実装する必要があります。
socket
モジュールはアプリケーション層のプロトコルに関与しないため、逆に言えば、開発者が頑張って開発を行えば、どんなアプリケーション層のプロトコルにも対応可能です。非常に汎用性の高いモジュールであると言えると思います。それに対し、特定のアプリケーション層のプロトコルに対応したモジュールに関しては、そのプロトコルにしか対応はできませんが、より簡単な実装や使い方でそのプロトコルを実現することができます。
こういった違いも理解しながら、使用するモジュールの選択も行っていただければと思います!
また、たくさんの便利な通信モジュールが存在するので、もしかしたらソケット通信を利用する機会は少ないかもしれませんが、少なくとも通信プログラムを開発するのであればソケット通信について学んでおくことは無駄ではないと思います。是非、このページで解説した内容に関しては頭の片隅にでも置いておいていただければと思います。
スポンサーリンク
まとめ
このページでは、Python でのソケット通信の使い方、および、ソケット通信プログラムを開発する上で身につけておきたい知識について解説しました!
ソケット通信プログラムを開発する上では、IP アドレスとポート番号・プロトコル・サーバーとクライアントに関しては理解しておいた方が良いと思いますし、これらを理解しておくことで、ソケット通信プログラムを開発していく中で、実行が必要になる処理やメソッドも自然と頭の中で思いつくようになると思います。
プロトコルに関しては難しそうな言葉なので苦手意識を持っている方も多いと思いますが、特にトランスポート層のプロトコルに関しては、まずは TCP と UDP の特徴および、TCP と UDP とで利用するメソッドの違いに関して最低限理解しておけば良いと思います。
独立して単独で動作するプログラムに関してはメソッドやクラスを使いこなすだけでやりたいことが実現できますが、通信プログラムに関しては通信相手と息を合わせて処理が行われるように開発する必要があり、この点が通信プログラム開発での一番難しいところになるのではないかと思います。クライアントサーバーモデルを採用したり、アプリケーション層のプロトコルを理解することで、この難しい点も解消できると思いますので、この辺りもしっかり理解しておくと良いと思います!
ぜひ、このページで学んだことを活かし、様々な通信プログラムの開発に挑戦してみてください!