ユーニックス総合研究所

  • home
  • archives
  • python-socket

PythonのsocketでTCPサーバー/クライアントを作る

  • 作成日: 2020-12-07
  • 更新日: 2023-12-24
  • カテゴリ: Python

socketでTCPサーバー/クライアント

昨今のソフトウェアはネットワーク通信が当たり前になっています。
たとえばHTTPサーバーはクライアントと双方向に通信を行い、結果をクライアントに返します。

Pythonではこういった双方向の通信を実現するために低レイヤ―な通信を行えるモジュールがあります。
その名もsocketです。
このモジュールを使うと本記事で解説するような簡易的なTCPサーバーやTCPクライアントを作ることが可能になります。
TCPサーバー/クライアントはソケット通信における基礎的なソフトウェアといえますが、これの作り方を習得しておくことでその後の色々なソケット通信を使ったソフトウェアの開発方面にシフトしていくことができるでしょう。

この記事ではsocketの使い方を解説しますが、具体的には↓を見ていきます。

  • ソケット通信の概要
  • 簡易的なTCPサーバーを作る
  • 簡易的なTCPクライアントを作る
  • 動作を見る

ソケット通信の概要

インターネットは偉大な人類の発明です。
みなさんもインターネットを日頃からよく利用されていることと思います。

インターネットはローカルなネットを繋ぎ合わせたものです。
ローカルなネットとは、ホスト(パソコン)とホスト同士の通信ネットです。
ホストとホストの間で通信をするときに使われる仕組みが「ソケット通信」と呼ばれるものです。

これは「ソケット」と呼ばれる疑似的なエンドポイントを作成し、そのソケットに対して抽象化された通信を行います。
ホストが持つソケットとソケットを繋ぐことで、そのホスト同士がお互いに通信できるようなります。
ソケットを使うことで通信のための複雑な処理が隠蔽されます。
また、ソケットはファイルに似たインターフェースを持っているため、普通のファイル入出力のように扱うことが出来ます。
このように「ソケット」には多数のメリットがあります。

ホスト同士のネットワーク通信を学ぶということは、ソケット通信を学ぶということです。
そしてソケット通信を学ぶということは、ソケットの作り方、接続方法、送信/受信方法を学ぶということになります。

ソケット通信には種類があります。
たとえばIPv4インターネットプロトコルを使用した通信や、IPv6インターネットプロトコルを使用した通信です。
また、データの送信と受信方法にも種類があります。
大きく分けて信頼性のある双方向のストリーム通信(TCP)と、信頼性のない一方向の通信(UDP)です。

ソケットの作成のときにこれらの通信タイプを設定することで、いろいろな通信手段を選べることになります。
今回作成するTCPサーバー/クライアントではIPv4とTCPを使います。
このタイプの通信はソケット通信では基礎的なもので、最初に学ぶにはまさにうってつけといえます(時代によって変わりますが)。

TCPソケットを使うことでホスト同士のソケット間に信頼性のある通信路が確立され、双方向にデータを送受信することが可能になります。
サーバー・ホストはクライアント・ホストからの接続を待ち受け、接続が来たら受信を開始し、受信が完了したら今度はクライアントにデータを送信します。
クライアントはサーバーにデータを送信したら今度はサーバーからのデータの受信を待ち、受信が完了したら次の処理を行います。

このようなサーバー/クライアント間の通信の具体的な手順の取り決めを「プロトコル」といいます。
今回は↑のようなプロトコルで通信を行います。
プロトコルには代表的なものにHTTPやFTPなどがありますが、今回はこれらは解説しません。

注意点として、サーバーとクライアントではソケット通信の手順が異なります。
これは以降で解説します。

簡易的なTCPサーバーを作る

では最初に簡易的なTCPサーバーを作ってみたいと思います。
↓がコード全文です。

import socket  

# クライアントとの通信に使うポート番号(クライアントと共有)  
PORT = 3123  

# サーバー用ソケットの作成  
# AF_INETはIPv4インターネットプロトコルを表す  
# SOCK_STREAMは順序性と信頼性のある双方向のバイトストリームによる接続を表す  
servsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  

# ソケットをアドレスにバインドする  
# 今回のサンプルは同ホスト内の通信のみを行う  
# そのためsocket.gethostname()で自ホスト名に対してバインドを行っている  
# ポート番号はクライアントと同じものを使う  
servsock.bind((socket.gethostname(), PORT))  

# サーバーを有効にして接続を受け付けるようにする  
servsock.listen()  

# サーバーのループ  
while True:  
    # クライアントからの接続を受け付ける  
    # clisockにはクライアントに接続済みのソケットが入る  
    # addrにはアドレス(タプル)が入る  
    print('accept...')  
    clisock, addr = servsock.accept()  

    # クライアントからデータを受信する  
    data = clisock.recv(1024)  
    print(data)  

    # クライアントへデータを送信する  
    clisock.send(b'ok')  

    # クライアントとの接続を切る  
    # 今回はシングルスレッドでクライアントとの接続を処理している  
    # HTTPサーバーのようにしたい場合はマルチスレッドにしてクライアントのソケットを別スレッドで処理するようにする  
    clisock.close()  

具体的に見ていきます。
まず↓の部分です。

import socket  

Pythonでソケット通信を使うにはsocketモジュールを↑のようにインポートします。
こうすることでsocketモジュールの持つ関数や定数を利用することが可能になります。

# サーバー用ソケットの作成  
# AF_INETはIPv4インターネットプロトコルを表す  
# SOCK_STREAMは順序性と信頼性のある双方向のバイトストリームによる接続を表す  
servsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  

最初に↑のようにしてソケットを作成します。
socket.socket()は引数に応じてソケットを作成する関数です。
↑の例ではアドレス・ファミリーにAF_INET(IPv4)を指定し、タイプにSOCK_STREAM(双方向のTCP通信)を指定しています。
socket.socket()は返り値でソケット・オブジェクトを返しますので、これを変数に代入しておきます。

# ソケットをアドレスにバインドする  
# 今回のサンプルは同ホスト内の通信のみを行う  
# そのためsocket.gethostname()で自ホスト名に対してバインドを行っている  
# ポート番号はクライアントと同じものを使う  
servsock.bind((socket.gethostname(), PORT))  

次に↑のようにして作成したソケットを指定のアドレスにバインド(紐づけ)します。
作成直後のソケットはいわばどこにも繋がっていない状態で、宙ぶらりんのソケットです。
ソケット・オブジェクトのbind()メソッドを使うことでソケットを指定のアドレスに繋げます。

bind()にはタプルで引数を渡します。
タプルの第1要素にはアドレス、第2要素にはポート番号を渡します。
今回は別のホストには接続せず、自ホスト内でのみ通信を行います。
そのためsocket.gethostname()で自ホストの名前を取得し、これをアドレスに設定しています。

ポート番号とは、ソケットが通信で使うデータの出入り口のことです。
この番号はサーバーとクライアントで同じものを使う必要があります。
データの出入口が違っていたら通信ができないからです。

# サーバーを有効にして接続を受け付けるようにする  
servsock.listen()  

次に↑のようにしてソケット・オブジェクトのlisten()メソッドを呼び出します。
こうすることで、ソケットがサーバーとして接続を受け付けるようになります。
このようにソケット・オブジェクトは呼び出すメソッドによってその役割がサーバーになったりクライアントになったりします。

    # クライアントからの接続を受け付ける  
    # clisockにはクライアントに接続済みのソケットが入る  
    # addrにはアドレス(タプル)が入る  
    print('accept...')  
    clisock, addr = servsock.accept()  

次にwhile文のループに入って↑のようにソケット・オブジェクトのメソッドaccept()を呼び出します。
accept()はクライアントからの接続を待機し、接続があったらクライアントに繋がっているソケットを返すメソッドです。
返り値はタプルで、第1要素にはクライアントと接続済みのソケット・オブジェクト、第2要素にはタプルのアドレスが入ります。

accept()でクライアントと接続済みのソケットを入手出来たら、あとはここで取得したソケットに対して処理を行います。

    # クライアントからデータを受信する  
    data = clisock.recv(1024)  
    print(data)  

まずサーバーでは↑のようにクライアントからのデータを受信するようにします。
ソケットオブジェクトのrecv()メソッドを使うことでソケットからデータを受信することが出来ます。
recv()メソッドの引数には受信する最大バイト数を指定します。
データを受信したらprint(data)でデータを出力します。

    # クライアントへデータを送信する  
    clisock.send(b'ok')  

データを受信したら、今度は↑のようにサーバーからクライアントにデータを送信します。
send()メソッドは引数のバイト列を接続先へ送信します。

    # クライアントとの接続を切る  
    # 今回はシングルスレッドでクライアントとの接続を処理している  
    # HTTPサーバーのようにしたい場合はマルチスレッドにしてクライアントのソケットを別スレッドで処理するようにする  
    clisock.close()  

ループの最後でソケットを切断します。
ソケット通信では、このソケットのclose()が非常に重要な役割を持っています。
このclose()がらみでバグがよく発生します。
通信が完了したソケットをどこで切断するかと言うのは、サーバーの設計において非常に重要です。

以上でサーバーのプログラムは終わりです。

簡易的なTCPクライアントを作る

次にクライアントのプログラムを作ります。
↓がコード全文です。

import socket  

# サーバーとの通信に使うポート番号(サーバーと共有)  
PORT = 3123  

# クライアント用のソケットを作成  
# サーバーの説明を参照  
cliesock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  

# ソケットをアドレスに接続する  
# サーバーのbind()の説明を参照  
cliesock.connect((socket.gethostname(), PORT))  

# サーバーにデータを送信する  
cliesock.send(b'hello, world!')  

# サーバーからデータを受信する  
data = cliesock.recv(1024)  
print(data)  

# ソケットを閉じる  
cliesock.close()  

順を追って見ていきます。

# クライアント用のソケットを作成  
# サーバーの説明を参照  
cliesock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  

↑のソケットの作成はサーバーと同じです。

# ソケットをアドレスに接続する  
# サーバーのbind()の説明を参照  
cliesock.connect((socket.gethostname(), PORT))  

↑の部分でクライアントのソケットをサーバーに接続します。
connect()はタプルを引数に取ります。
bind()と同様にタプルの第1要素はアドレス、第2要素はポート番号になります。

# サーバーにデータを送信する  
cliesock.send(b'hello, world!')  

接続後、最初に↑のようにサーバーにsend()でデータを送信します。
サーバーの処理の順序とは逆になっている点に注意してください。
つまりサーバーが受信をしているとき、クライアントは送信している状態になっているわけです。
逆にサーバーが送信をしているときは、クライアントは受信を行います。

# サーバーからデータを受信する  
data = cliesock.recv(1024)  
print(data)  

次に↑のようにしてサーバーからデータを受信してprint()でデータを表示します。

# ソケットを閉じる  
cliesock.close()  

最後に↑のようにしてソケットを閉じます。
クライアントのプログラムはサーバーと比べるとずいぶん簡単なことがわかると思います。
今回のクライアントは、起動するとデータを送信して受信、そしてデータを表示して終了するだけのプログラムです。

動作を見る

サーバーのプログラムを起動すると↓のように出力が流れます。

accept...  
b'hello, world!'  
accept...  

↑はクライアントからhello, world!というでデータを受信し、出力している所です。
accept()を呼び出すとプログラムは入力待ちのような状態になります。
そのため↑ではaccept...と表示されたまま待機しています。

クライアントのプログラムは実行すると↓のような出力を行います。

b'ok'  

サーバーにデータを送信し、その後にサーバーからokというデータを受信しています。

おわりに

ソケット通信を使えるようになるとネットワーク通信を使ったソフトウェアを作れるようになります。
これは非常におもしろい技術で、作れるソフトウェアの幅を広げてくれます。
そういうことなのでぜひ押さえておきたい技術と言えます。

🦝 < ソケット通信であんなことこんなことをしよう

参照