UDPで片道遅延を測れるプログラムを生成AIと開発してみた

# はじめに
- 過去にpingの使い方を解説する記事を書いたのですが、往復遅延しか測れないので片道遅延を測定できるスクリプトを書いてみました
- この記事は生成AI(gemini)との共著です
- 技術記事の執筆にどこまで生成AIを活用できるかのテストを兼ねて投稿します

https://qiita.com/Nenshu_agetai/items/948e99081f18433478f2

# この記事について
- ネットワーク遅延は、オンラインゲームやビデオ会議などのアプリケーションで重要です
- [過去の記事](https://qiita.com/Nenshu_agetai/items/948e99081f18433478f2)ではpingを使った往復遅延(RTT)の測定方法について解説しました
- 片道遅延は、データが送信元から送信先へ到達する時間を指し、往復遅延（RTT）とは異なります
- この記事では、UDPを使って片道遅延を測定するPythonプログラムを生成AIと開発する過程を紹介します
- TCPではなくUDPを使う理由、TCPとの違い、片道遅延測定の課題、高精度測定の考慮点なども解説します

# 片道遅延測定の課題
片道遅延を測定するのってそんなに難しいの？という疑問に答えるべく、以下に片道遅延の課題を整理します。

1. 測定の難しさ
RTTは送信元だけで測定可能だが、片道遅延は送信元と送信先の両方で時刻を記録し、その差を計算する必要がある。

2. 時刻同期の必要性
時刻がずれていると片道遅延を正しく測定できない。高精度な測定には高精度な時刻同期が必要となります。
:::note warn
例：送信元10:00:00.000、送信先10:00:00.100と記録されても、送信先の時計が遅れていれば実際の遅延は異なる
:::

生成AIによると、NTPの精度（数ミリ秒〜数十ミリ秒）は不十分で、PTP（より高度な時刻同期プロトコル）が必要とのこと。

この記事のプログラムでは、時刻同期はシステムクロックに依存するため精度は限定的ですが、片道遅延の概念とプログラムの動作を理解するには十分かと思います。

# 片道遅延測定pythonスクリプト
このセクションでは、UDPを用いて片道遅延を測定するPythonプログラムのコードを解説します。このプログラムは、送信側と受信側の2つの部分で構成されています。

## 送信側(sender.py)

``` sender.py
import socket
import time
import struct
import sys

# 送信先のアドレスとポート
SERVER_IP = "127.0.0.1"  # 受信側のIPアドレス
SERVER_PORT = 5005

# UDPソケットを作成
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

try:
    while True:
        # 現在時刻をナノ秒で取得 (高精度な時刻源に置き換える必要あり)
        if sys.version_info >= (3, 7):
          send_time_ns = time.time_ns()
        else:
          send_time_ns = int(time.time() * 1e9) # ナノ秒に変換

        # タイムスタンプをバイト列に変換
        data = struct.pack("!Q", send_time_ns)

        # データを送信
        sock.sendto(data, (SERVER_IP, SERVER_PORT))

        print(f"Sent timestamp: {send_time_ns}")

        time.sleep(1)  # 1秒ごとに送信

except KeyboardInterrupt:
    print("Exiting...")
finally:
    sock.close()
```

:::note info
`SERVER_IP`だけ、宛先となるサーバのIPアドレスの書き換えてください。
:::

以下にプログラムの動作フローを示します。

```plantuml
start

:UDPソケットを作成;
:送信先アドレス/ポートを設定;

while (Ctrl+Cが押されるまで) is (Yes)
  :現在時刻をナノ秒で取得 (send_time_ns);
  :send_time_nsをバイト列に変換;
  :データを送信 (UDPパケット);
  :送信タイムスタンプを表示;
  :1秒待機;
end while (No)

:ソケットをクローズ;
stop
```


## 受信側(receiver.py)

```receiver.py
import socket
import time
import struct
import sys

# 受信側のアドレスとポート
BIND_IP = "0.0.0.0"  # すべてのインターフェースでリッスン
BIND_PORT = 5005

# UDPソケットを作成
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((BIND_IP, BIND_PORT))

print(f"Listening on {BIND_IP}:{BIND_PORT}")

try:
    while True:
        # データを受信
        data, addr = sock.recvfrom(8) # タイムスタンプは8バイト

        # 受信時刻をナノ秒で取得 (高精度な時刻源に置き換える必要あり)
        if sys.version_info >= (3, 7):
          recv_time_ns = time.time_ns()
        else:
          recv_time_ns = int(time.time() * 1e9) # ナノ秒に変換

        # 受信したタイムスタンプをアンパック
        send_time_ns = struct.unpack("!Q", data)[0]

        # 片道遅延を計算 (ナノ秒単位)
        one_way_delay_ns = recv_time_ns - send_time_ns

        print(f"Received timestamp: {send_time_ns}, One-way delay: {one_way_delay_ns} ns")

except KeyboardInterrupt:
    print("Exiting...")
finally:
    sock.close()
```

:::note info
`BIND_PORT`を送信側と合わせてください。
:::

以下にプログラムの動作フローを示します。


```plantuml
start

:UDPソケットを作成;
:指定したアドレス/ポートにバインド;
:受信待機;

while (Ctrl+Cが押されるまで) is (Yes)
  :データを受信 (UDPパケット);
  :受信時刻をナノ秒で取得 (recv_time_ns);
  :受信データ(タイムスタンプ)をアンパック (send_time_ns);
  :片道遅延を計算 (recv_time_ns - send_time_ns);
  :受信タイムスタンプと片道遅延を表示;
end while (No)

:ソケットをクローズ;
stop
```

## 送受信のシーケンス図

以下に送受信のプログラムのシーケンス図を示します。
<details><summary>送受信のプログラムのシーケンス図</summary>


```plantuml
participant Sender
participant Receiver

activate Sender
Sender -> Sender: UDPソケット作成
Sender -> Sender: 送信先設定

loop 1秒ごと
    Sender -> Sender: 現在時刻取得 (send_time_ns)
    Sender -> Sender: タイムスタンプをバイト列に変換
    Sender -> Receiver: UDPパケット送信 (タイムスタンプ)
    activate Receiver
    Receiver -> Receiver: データ受信
    Receiver -> Receiver: 受信時刻取得 (recv_time_ns)
    Receiver -> Receiver: タイムスタンプをアンパック
    Receiver -> Receiver: 片道遅延計算
    Receiver -> Receiver: 結果表示
    deactivate Receiver
end

deactivate Sender
```

</details>

# プログラムの用法

このセクションでは、作成したPythonプログラムを実行し、片道遅延の測定結果を確認する方法を説明します。

## 実行手順
**1. ファイルの保存:**
送信側のコードを sender.py、受信側のコードを receiver.py というファイル名で保存します。

**2. 実行環境:**
正しい測定には、Python 3.7以降がインストールされている環境が必要です。それ以前のバージョンだと時刻を無理やりナノ秒に変換しています。

**3. 受信側の実行:**
受信側のプログラム receiver.py を先に実行します。ターミナルまたはコマンドプロンプトで、receiver.py が保存されているディレクトリに移動し、以下のコマンドを実行します。

```Bash
python receiver.py
```
実行すると、以下のようなメッセージが表示され、受信待機状態になります。

```Bash
Listening on 0.0.0.0:5005
```
**4. 送信側の実行:**
別のターミナルまたはコマンドプロンプトを開き、送信側のプログラム sender.py を実行します。sender.py が保存されているディレクトリに移動し、以下のコマンドを実行します。

```Bash
python sender.py
```

実行すると、以下のようなメッセージが表示され、タイムスタンプが送信されます。

```Bash
Sent timestamp: 1678886400000000000
Sent timestamp: 1678886401000000000
...
```

**5. 結果の確認:**
受信側のターミナルには、以下のような出力が表示されます。

```Bash
Received timestamp: 1678886400000000000, One-way delay: 123456 ns
Received timestamp: 1678886401000000000, One-way delay: 98765 ns
...
```

ここで、One-way delay の値が片道遅延を表しています。単位はナノ秒です。上記の例では、1回目の測定で約123マイクロ秒、2回目の測定で約99マイクロ秒の遅延が発生していることが分かります。

## 結果の解釈
出力結果から、送信側から受信側への片道遅延をナノ秒単位で確認できます。ただし、前述の通り、**このプログラムで使用しているタイムスタンプはシステムクロックに基づいているため、精度は高くありません。**

特に、**送信側と受信側のマシンで時刻が大きくずれている場合や、ネットワークの状況によって遅延が大きく変動する場合は、測定結果の信頼性が低下します。**

また、出力される遅延の値は、プログラムの処理時間やOSのスケジューリングなどの影響も受けるため、純粋なネットワーク遅延だけを表しているわけではありません。特に、遅延の値が非常に小さい場合は、これらの影響が無視できなくなる可能性があります。

# 高精度な測定に向けて

前述の通り、今回作成したプログラムでは、タイムスタンプの取得にPythonの time.time_ns() (または time.time()) を使用しています。これはシステムクロックに基づいており、精度はミリ秒オーダーです。そのため、マイクロ秒以下の精度で片道遅延を測定することは困難です。

高精度な測定を実現するためには、より正確な時刻同期が必要です。そこで重要な役割を果たすのが、PTP (Precision Time Protocol) です。

## PTP (Precision Time Protocol) とは
PTPは、ネットワークに接続された機器間で高精度な時刻同期を実現するためのプロトコルです。IEEE 1588で標準化されており、ナノ秒レベルの精度で時刻を同期することができます。

NTPとPTPの違いに関する解説記事を見つけましたので、引用しておきます。

https://www.gmc-timesyncing.com/faq/ntp-ptp.html


PTPでは、ネットワーク内に「マスタークロック」となる機器が存在し、他の機器（「スレーブクロック」）はマスタークロックに時刻を同期します。マスタークロックは、非常に高精度なハードウェアクロック（例えば、原子時計など）を持っていることが理想的です。

## PTPの仕組み
PTPは、NTP (Network Time Protocol) よりも高精度な時刻同期を実現するために、以下のような仕組みを備えています。

- **ハードウェアタイムスタンプ:** 
ネットワークインターフェースカード (NIC) のハードウェアでタイムスタンプを生成することで、ソフトウェア処理による遅延を最小限に抑えます。
- **遅延測定:** 
パケットの送受信にかかる遅延を測定し、時刻同期の精度を向上させます。

## PTPの導入方法
Linux環境では、ptp4l というPTPの実装が広く使われています。ptp4l は、PTPのマスタークロックまたはスレーブクロックとして動作することができます。

自身の環境ではハードウェアスペック上確認が難しかったため、他の方が書いている技術記事を引用しつつ、geminiに教えてもらった導入方法を記載します。

https://network.oreda.net/knowledge/ptp-linux#google_vignette


以下、geminiの出力を一部改変して記載します。

PTPを導入して高精度な片道遅延測定を行うためには、以下の手順が必要になります。

- **PTP対応のハードウェア:** 
PTPを使用するには、ネットワークインターフェースカード (NIC) がPTPのハードウェアタイムスタンプに対応している必要があります。

- **ptp4l のインストール:**
各マシンに ptp4l をインストールします。

- **PTPの設定:**
ネットワーク環境に合わせて ptp4l を設定します。マスタークロックとなるマシンと、スレーブクロックとなるマシンを設定する必要があります。

- **プログラムの修正:**
プログラム内で、PTPで同期された時刻を取得するように修正します。Linux環境であれば、/dev/ptp0 などのデバイスファイルから時刻を取得することができます。

これらの手順はやや複雑であり、この記事では詳細な設定方法については割愛しますが、PTPを使用することで、より高精度な片道遅延測定が可能になることを理解していただければと思います。

# まとめ

UDPを用いて片道遅延を測定するPythonプログラムを、生成AIと共同で開発する過程を紹介しました。

この記事を書き始めて書き終わるまで約1時間でした。

**普段記事を書くと、２~４時間くらいかかるので、効率にして2倍以上！すごい！**

今後もちょくちょく生成AIに頼りながら記事を書きたいと思います。