🐙

【Python入門】bind() の意味を SOCK_DGRAM のサンプルコードを使って確認してみた

に公開

はじめに

ソケットAPIの bind() の挙動について疑問を持ったことがこの記事を書くきっかけになりました。
「バインドすることで、なぜ通信の相手がそのアドレスにアクセスできるようになるのか?」という点を初心者視点で調べ、実際に動かして確かめた内容をまとめます。

背景

UNIXドメインソケット(ソケットタイプ SOCK_DGRAM)を使って、プロセス間通信を実装していました。
UNIXドメインソケットは IP アドレス不要ですが、代わりに ファイルパスをアドレスとして使用 します。

ここで疑問に思ったのが、

  • クライアントは自分のファイルパスをサーバにどう伝えているのか?
  • サーバはどうやって送信元を認識できるのか?

という点です。

簡易プログラムを作成

CLI だけで動作するクライアント・サーバを用意しました。
クライアントが送信したデータをサーバが受け取り、加工してレスポンスを返します。

クライアント

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

サーバ

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

開発環境

  • OS: macOS Sequoia 15.6.1
  • PC: MacBook Pro(M2チップ)
  • エディタ: VS Code
  • 言語: Python 3.13.5
  • 使用ライブラリ: 標準ライブラリのみ(os, json, socket)

ディレクトリ構成

unix-domain-socket/
├── tcp/ # 今回は使用しません
├── tmp/
├── udp/
│   ├── udp-client.py
│   └── udp-server.py
└── config.json

実装コード

{
    "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")    

バインドとアドレス認識の流れ

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

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

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

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

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

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

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

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

実際の動作

1. サーバ起動

2. クライアント起動

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

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

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

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

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

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

補足

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

まとめ

「なぜ相手のファイルパスがわかるのか?」という疑問に対して、OS がアドレス情報を自動で扱っているという点がポイントでした。

bind() はソケットに住所を与える操作であり、通信の受付窓口を作ります。
sendto() / recvfrom() のやりとりの中で、カーネルが送信元アドレスを自動的に扱ってくれます。
そのため、クライアントが自分の住所を明示的に送信する必要はなく、相手に接続先情報を送ることができます。

今回の内容が何かのお役に立てれば幸いです。

最後までお読みいただき、ありがとうございました。

参考URL

https://recursionist.io/dashboard/course/31/lesson/1099

Discussion