0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python入門】UNIXドメインソケットでクライアント・サーバ通信を実装してみた

Posted at

はじめに

プロセス間通信には、パイプ・名前付きパイプ・ソケットなどがあります。
ソケットはソケットドメインとソケットタイプの組み合わせでどのようにプロセス間通信を行なうかを決めることができます。

この記事では、以下の学習サイトで紹介されているUNIXドメインソケットを使ったクライアントとサーバのプロセス間通信の簡単な実装例(一部改良)を交えながら、ソケットの基本と処理の流れについて解説していきます。

参考サイト

ソケットとは

IBMさんの定義によると、ソケットとは以下のものを指します。

ソケットとは、通信を行うための「接続の端点」のことです。
ネットワーク越しのプロセス間通信だけでなく、同一マシン上でも利用でき、双方向通信が可能です。
https://www.ibm.com/docs/ja/i/7.4.0?topic=communications-socket-programming より引用

ソケットを使った通信では、パイプのような単方向通信ではなく、送信と受信を同時に行なうことができる双方向通信を実現します。

ソケットは、特定のソケットドメインソケットタイプに基づいて作成され、これによりソケットがどのように通信を行なうかを決定します。

ソケットドメイン

UNIXドメインソケット

ネットワークソケット

ソケットドメインは、ソケットが使用する通信の形式を定義します。
これにより、ソケットがどのようなネットワーク上で通信を行なうかが決定されます。
ソケットが使用する代表的なネットワークとしてUNIXドメインソケット・ネットワークソケットがあります。

トランスポート層のプロトコルの選択

代表的なソケットタイプとして、SOCK_STREAMSOCK_DGRAM があります。
これらはトランスポート層の特定のプロトコルに対応しています。
そこで、それぞれが対応しているプロトコルについて簡単に整理します。

TCP

SOCK_STREAM は、TCPプロトコルに対応しています。
TCPでは、相手の存在を確認してから通信を行なうという特徴があります。

TCPは、データ通信を開始する前に、送信側と受信側が互いに通信できる状態にあることを確認し、コネクションを確立する、コネクション型プロトコルです。

データが欠落したり、順番が入れ替わったりしないよう、受信確認や再送制御の仕組みなどを持っており、信頼性の高さと順序の保証が実現します。
これは、特にメールの送受信やファイルの転送などのデータの正確性が不可欠なアプリケーションで使用されます。

接続の信頼性は確保できますが、接続の確立と維持には余分な手順が必要なため、リアルタイム性が求められる一部のアプリケーションには適していません。また、シーケンス番号や確認応答番号などの追加情報がパケットに含まれるため、全体のデータ量が増加します。

UDP

SOCK_DGRAM は、UDPプロトコルに対応しています。
UDPでは、相手の存在を確認せずデータ送信を行なうという特徴があります。
これは、UDPがコネクションレス型プロトコルで、データグラム(パケット)を送信先のIPアドレスとポート番号へ直接「投げっぱなし」で送ります。

そのため、接続の確立は不要で、送信側は、送信先のIPアドレスとポート番号を知っていれば、事前の接続手続きなしにデータの送信を開始できます。

UDPは相手がいようがいなかろうが、通信相手の確認をせず、問答無用でパケットを送信するプロトコルということです。

TCPと違い、通信相手の確認作業などがない分、通信は高速です。
ビデオストリーミングやオーディオストリーミングなどリアルタイムのアプリケーションに適しています。

ただし、TCPのように宛先の確認やパケットの順序の保証がないなど、信頼性が低い通信を提供している点には留意する必要があります。

代表的なソケットタイプ

ソケットタイプは、アプリケーションが通信する方法を決定します。
これは、特に信頼性、メッセージの順序付け、重複の防止といった特性に影響を与えます。

SOCK_STREAM

SOCK_STREAM は、信頼性の高い、順序通りの、エラーのないバイトストリームの伝送を提供します。
これは通常、トランスポート層の TCP プロトコルを使用します。

SOCK_DGRAM

SOCK_DGRAM では、データは個々のパケット(データグラム)として送受信されます。
これは通常、トランスポート層の UDP プロトコルを使用します。

UDPはコネクションレスなので、送信先アドレスを毎回指定する必要があります。
ただし、connect() を呼ぶことで、送信先を固定して簡単に扱うことも可能です。

簡易プログラムを実装してみる

1. ソケットタイプ: SOCK_STREAM

実装する流れのイメージ

実装では、バックログキューに1つしか格納できないように設定しているため、1つのリクエストしか捌けない仕様にしています。

※クライアントからのリクエストが1つしか受付できないことは現実的ではありませんが、ここでは全体の流れを理解することを目的にしています。
※ソケットAPIというライブラリの呼び出しとソケットなどを一緒に表記
※現時点の知識でまとめているため、間違いの可能性もあります。あらかじめご了承ください。

listen()accept()といったメソッドは、TCPソケットと同じインターフェースを持つため使用されますが、その内部の動作は TCP/IP スタックとは異なります。
UNIXドメインソケットの場合、listen()は接続待ちのキューを作成する役割を果たし、accept()はキューに溜まった接続要求を受け入れる役割を果たします。

実装内容

クライアント

  • 文字列をサーバに送信
  • サーバからレスポンスを受け取り、プログラム終了

サーバ

  • クライアントから文字列を受け取り
  • 受け取った文字列に文字列を追加してクライアントにレスポンス
  • レスポンス送信後は他のリクエストを待機

1-2. ディレクトリ構成

実際に使用したディレクトリ構成
unix-domain-socket/
├── tmp/
├── client.py
├── config.json
└── server.py

1-3. 実装コード

設定ファイル
{
    "filepath" : "/tmp/socket_file/"
}
server.py(サーバ)
import os
import json
import socket

# 1. ソケットオブジェクトの作成
unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

config = json.load(open('config.json'))
server_address = config['filepath']

# ソケットを使えるようにアドレスを準備
# 古いサーバアドレスの削除
try:
    os.unlink(server_address)
except FileNotFoundError:
    pass

print(f"Starting up on {server_address}")

# 2. ソケットのバインド
# 作成済みのソケットを特定の場所に紐づける
unix_socket.bind(server_address)

# 3. ソケットが接続要求を待機
# 「同時に1つの接続要求をキューに入れておくことができる」という設定
unix_socket.listen(1)

# 4. クライアントからの接続待機
while True:
    # connection: 新しく生成された接続ソケットオブジェクト
    # client_address: クライアントのアドレス
    connection, client_address = unix_socket.accept()

    # 5. 接続ソケット確立後
    with connection:
        while True:
            # 1度の読み込みで64バイトの読み込みに設定
            data = connection.recv(64)
            # 受け取ったデータはバイナリ形式なので、それを文字列に変換
            data_str = data.decode('utf-8')
            print(f"Received from client: {data_str}")

            # クライアントからメッセージがきた場合の処理
            if data:
                response = f"Processing {data_str}"
                # 処理したメッセージをバイナリ形式にエンコードしてクライアントに送り返す
                connection.sendall(response.encode('utf-8'))
            else:
                print(f"No data from {client_address}")
                break
    # 6. 接続ソケット終了    
    print("Closing current connection")
client.py(クライアント)
import sys
import json
import socket

unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

config = json.load(open('config.json'))
server_address = config['filepath']

print(f"Start to connect to {server_address}")

# サーバに接続
try:
    unix_socket.connect(server_address)
except OSError as err:
    print(err)
    sys.exit(1)

# サーバ接続成功後
try:
    # 送信メッセージ入力
    message_str = input("Sending a message to the server: ")
    # 文字列をバイト形式にエンコード
    message_bytes = message_str.encode('utf-8')

    with unix_socket:
        unix_socket.sendall(message_bytes)

        # タイムアウトの設定(サーバからの応答に最大5秒待つように設定)
        # 目的: プログラムがサーバからの応答がないまま永遠に待ち続けることを防ぐ
        unix_socket.settimeout(5)

        # サーバからの応答待ち
        try:
            while True:
                # データの受け取り・デコード
                data = unix_socket.recv(64).decode('utf-8')
                
                if data:
                    print(f"Server response: {data}")
                else:
                    break
        except TimeoutError:
            print("Socket timeout, ending listening for server messages")

except OSError as err:
    print(err)

finally:
    print("closing socket")

1-4. コードの意味

try:
    os.unlink(server_address)
except FileNotFoundError:
    pass

このコードでは、パイプが存在しないときに備えています。

  • プログラムの初回実行時: まだ一度もパイプが作成されていない
  • 正常終了時: 前回の実行でパイプがすでに削除されている

このような場合、os.unlink(server_address) を実行すると、FileNotFoundError が発生し、プログラムが停止してしまいます。
これを防ぐために、try-except を使ってエラーを捕捉し、何もしない pass 文を実行することで、プログラムの実行を継続させています。

1-5. UNIXドメインソケットのバインドの仕組み

UNIXドメインソケットは、同じホスト(マシン)内のプロセス間通信に特化しています。
そのため、ネットワーク通信で使われるIPアドレスやポート番号は必要ありません。

unix_socket.bind(server_address)を実行すると、server_address で指定されたパスに特殊なファイル(ソケットファイル)が作成されます。このソケットファイルが、サーバープロセスとクライアントプロセスが通信するための「窓口」となります。

サーバー側は、このソケットファイルを作成し、クライアントからの接続を待ち受けます。
クライアント側は、このソケットファイルパスを指定してサーバーに接続します。

したがって、UNIXドメインソケットにとっての「バインド」は、IPアドレスとポート番号の組み合わせではなく、ファイルシステム上にアドレスとなるソケットファイルを作成することを意味します。
これにより、クライアントとサーバーは、共通のファイルパスを介して互いを認識し、通信を開始することができるのです。

1-6. connection, client_address = unix_socket.accept()が意味すること

accept()メソッドは、接続要求が来るまで処理をブロック(停止)させます。
接続が確立すると、サーバーは新しい接続用ソケット(connection)を使ってクライアントと通信し、同時に元のソケット(unix_socket)で次の接続要求を待ち受けることができるようになります。

  1. connection: 接続ソケットオブジェクト
    これは、新しく確立されたクライアントとの通信に使用される新しいソケットオブジェクトです。
    このソケットを使って、データの送受信(send()recv())を行ないます。
    元々のサーバーソケット(unix_socket)は、引き続き他の接続を待ち受けるために使われます。

  2. client_address: クライアントのアドレス
    これは、接続を要求してきたクライアントのアドレス情報です。
    UNIXドメインソケットの場合、このアドレスは多くの場合、空の文字列を返すようです。
    TCP/IPソケットの場合は、通常、(IPアドレス, ポート番号)のタプルになります。

※実際に空文字列が返却されました

1-7. 実際の流れを確かめる

1-7-1. サーバを起動する

1-7-2. クライアントを起動する

1-7-3. クライアント側でメッセージを入力し送信(Enterキー)

1-7-4. サーバ側でデータ受け取り

1-7-5. クライアント側でサーバからのレスポンスを取得

1-7-6. 5秒後にクライアント終了

1-7-7. サーバの接続用ソケット終了

ここでは特定の1クライアントの通信が終了したのみで、引き続きクライアントからのリクエストを受け付けています。

2. ソケットタイプ: SOCK_DGRAM

2-1. ディレクトリ構成

unix-domain-socket/
├── tmp/
├── udp/
│   ├── udp-client.py
│   └── udp-server.py
└── config.json

2-3. 実装コード

{
    "udp_server_socket_filepath": "/tmp/udp_socket_file",
    "udp_client_socket_filepath": "/tmp/udp_client_socket_file"
}
udp-server.py
import os
import json
import socket

# 1. ソケットの作成
udp_server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)

config = json.load(open('config.json'))
server_address = config['udp_server_socket_filepath']

try:
    os.unlink(server_address)
except FileNotFoundError:
    pass

# 2. ソケットにバインド
udp_server_socket.bind(server_address)

# 3. データ待機
while True:
    print("\nwaiting to receive messages...")

    data_from_client, client_address = udp_server_socket.recvfrom(4096)
    # 受け取ったメッセージをデコード 
    decoded_client_message = data_from_client.decode('utf-8')
    print(f"\nreceived message detail")
    print(f"- client address: {client_address}")
    print(f"- content(byte): {data_from_client}")
    print(f"- content(str): {decoded_client_message}")
    print(f"- size: {len(data_from_client)} bytes")

    # 4. データ送信
    if data_from_client:
        # クライアントから受け取った文字列に文字列を追加
        server_message =  f"Server received ~{decoded_client_message}~"
        # 送信メッセージをエンコード
        encoded_server_message = server_message.encode('utf-8')
        # 送信したバイト数を変数に格納
        sent_server_message_byte = udp_server_socket.sendto(encoded_server_message, client_address)
        print("\nsent message detail")
        print(f"- sent address: {client_address}")
        print(f"- content(byte): {encoded_server_message}")
        print(f"- content(str): {server_message}")
        print(f"- size: {sent_server_message_byte} bytes")
udp-client.py
import os
import json
import socket

# 1. ソケットの作成
udp_client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)

config = json.load(open('config.json'))
server_address = config['udp_server_socket_filepath']
client_address = config['udp_client_socket_filepath']

# ソケットファイルが残っていてもいなくても対応できる
try:
    os.unlink(client_address)
except FileNotFoundError:
    pass

# 2. ソケットのバインド
udp_client_socket.bind(client_address)

# 3. メッセージの送信・サーバからの応答待ち
with udp_client_socket:
    message = input("Enter your message: ").encode('utf-8')
    sent_message_byte = udp_client_socket.sendto(message, server_address)
    print("\nsending message datail")
    print(f"- content(byte): {message}")
    print(f"- size: {sent_message_byte} bytes")

    print("\nwaiting to receive...")
    data_from_server, server_address = udp_client_socket.recvfrom(4096)
    decoded_server_message = data_from_server.decode('utf-8')
    print("\nreceived message datail from server")
    print(f"- server address: {server_address}")
    print(f"- content(byte): {data_from_server}")
    print(f"- content(str): {decoded_server_message}")
    print(f"- size: {len(data_from_server)} bytes")

# 4. ソケット終了    
print("\nclosing socket\n")    

2-4. コードの意味

2-4-1. recv()recvfrom()

recv()

recv() はコネクション型ソケット(TCPなど)で使われるメソッドです。
これは、すでに確立された接続からデータを受け取ります。接続が確立されているため、データの送信元(クライアント)はすでに分かっており、その情報を受け取る必要がありません。

  • 使用するソケットタイプ: socket.SOCK_STREAM
  • 戻り値: 受信したデータ(バイト列)のみ
  • 特徴: データの送信元を特定する必要がない

recvfrom()

recvfrom() はコネクションレス型ソケット(UDPなど)で使われるメソッドです。
UDPは「投げっぱなし」の通信なので、サーバーはどのクライアントからデータが送られてきたかを知る必要があります。recvfrom() は、受信したデータに加えて、そのデータの送信元アドレスも一緒に返します。

  • 使用するソケットタイプ: socket.SOCK_DGRAM
  • 戻り値: (受信したデータ, 送信元のアドレス)のタプル
  • 特徴: データの送信元アドレスを一緒に取得する

2-5. バインドとアドレス認識の流れ

2-5-1. bind(): クライアントの住所登録

udp_client_socket.bind(client_address) によって、クライアントは自分の住所(/tmp/udp_client_socket_file)を OS に登録します。これで OS は「このソケットはこのファイルパスに対応」と認識します。

2-5-2. sendto(): OS が送信元を自動添付

クライアントが sendto(message, server_address) を呼ぶと、カーネルは次の情報をまとめて処理します。

  1. 宛先アドレス (server_address)
  2. データ本体 (message)
  3. 送信元アドレス (client_address)

送信元アドレスはユーザーが明示しなくても、カーネルがバインド済みのファイルパスを自動的に添付します。

2-5-3. recvfrom(): 住所を読み取る

サーバ側の recvfrom() は (データ, 送信元アドレス) を返します。
つまり、サーバは追加のやり取りなしで「どのクライアントから来たのか」を知ることができます。

2-6. 実際の動作

2-6-1. サーバ起動

2-6-2. クライアント起動

2-6-3. クライアントがメッセージを入力・送信


「Hello! I'm Mavo」と入力してみます。

2-6-4. サーバがデータ受け取り

2-6-5. サーバがレスポンス送信

2-6-6. クライアントがデータを受け取り・終了

2-6-7. サーバは引き続きリクエスト待機

実際に動作することが確認できます。
この後、クライアントを再度起動して同じ動作を行なった場合も結果は同じでした。

2-7. 補足

UNIXドメインソケットはファイルパスを「名前解決」に使用しますが、データ転送自体はカーネル内部で行われます。実際にファイルへ書き込まれるわけではない点に注意しましょう。

まとめ

  • ソケットは「通信の端点」を表す仕組みで、UNIXドメインソケットではファイルパスがアドレスになる
  • SOCK_STREAM は接続型通信(TCP相当)、SOCK_DGRAM は非接続型通信(UDP相当)を実現できる
  • Pythonの socket モジュールを使えば、同一マシン上で簡単にプロセス間通信を実装できる

今回の記事が理解の助けになれば幸いです。
最後までお読みいただき、ありがとうございました。

参考URL

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?