UDPでパケットが失われたり入れ替わることを検証

実際のところ、UDPはどのぐらいパケットを失い（ドロップ）、順序が入れ替わる（リオーダー）のか検証する。

# 検証環境

ローカル(LAN)環境
```
[受信PC] - [スイッチングHUB] - [Wi-Fiルーター] - (Wi-Fi 5G) - [送信PC]
```

インターネット環境
```
[受信PC] - [スイッチングHUB] - [ルーター] - (インターネット) - [ルーター] - [スイッチングHUB] - [送信PC]
```

tracertしたときのポップ数は12程度。

# 実行環境

* Windows 10
* .NET 7

検証コードは後述。

# 検証方法
10万個の連番（0 - 99,999）をUDPで送信し、受信した数値を調べる。

```
パケット長 = IPヘッダー 20 byte + UDPヘッダー 8 byte + 連番 4 byte = 32 byte
総パケット長 = 32 byte * 100,000 = 3,200,000 byte ≒ 3.05 MB
```

インターネット環境では大量のパケットを一度に送信すると大量のドロップが発生してしまったため、パケットを送信するたびに100マイクロ秒ウェイトすることにした。

# インターネット環境（送信間隔: 100マイクロ秒）

ローカル環境ではドロップもリオーダーもなし。💯

インターネット環境では両方発生した。

```
result_2022-12-23_19-03-50-028.txt
 received: 99,781 / 100,000 = 99.8%
  dropped: 219
reordered: 2

result_2022-12-23_19-08-03-479.txt
 received: 99,585 / 100,000 = 99.6%
  dropped: 415
reordered: 1

result_2022-12-23_19-10-54-347.txt
 received: 97,226 / 100,000 = 97.2%
  dropped: 2,774
reordered: 1

result_2022-12-23_19-11-24-592.txt
 received: 98,819 / 100,000 = 98.8%
  dropped: 1,181
reordered: 2

result_2022-12-23_19-11-54-398.txt
 received: 99,148 / 100,000 = 99.1%
  dropped: 852
reordered: 2

result_2022-12-23_19-12-28-634.txt
 received: 97,621 / 100,000 = 97.6%
  dropped: 2,379
reordered: 2
```

リオーダーは最初の15パケット中に発生していた。
一度ルーティングが確立するとリオーダーは発生しにくいのかもしれない。

ドロップはランダムに発生するようだが、1パケットずつ消えるとかではなく、まとめて消滅しているようだった。
輻輳が発生している可能性があるため、送信間隔を変更してみる。

# インターネット環境（送信間隔: 500マイクロ秒）

輻輳を回避するために送信間隔を5倍(500マイクロ秒)にした。

```
result_2022-12-23_23-19-22-308.txt
 received: 99,838 / 100,000 = 99.8%
  dropped: 162
reordered: 0

result_2022-12-23_23-21-38-476.txt
 received: 97,826 / 100,000 = 97.8%
  dropped: 2,174
reordered: 0

result_2022-12-23_23-22-49-568.txt
 received: 99,627 / 100,000 = 99.6%
  dropped: 373
reordered: 0

result_2022-12-23_23-23-58-278.txt
 received: 99,868 / 100,000 = 99.9%
  dropped: 132
reordered: 0

result_2022-12-23_23-25-13-015.txt
 received: 99,904 / 100,000 = 99.9%
  dropped: 96
reordered: 0

result_2022-12-23_23-26-39-229.txt
 received: 99,771 / 100,000 = 99.8%
  dropped: 229
reordered: 0
```

これ輻輳じゃなくて回線が💩なのでは...？

# インターネット環境（送信間隔: 0マイクロ秒）

あえて送信しまくる。

```
result_2022-12-24_00-37-48-245.txt
 received: 16,171 / 100,000 = 16.2%
  dropped: 83,829
reordered: 19

result_2022-12-24_00-43-07-057.txt
 received: 16,667 / 100,000 = 16.7%
  dropped: 83,333
reordered: 10

result_2022-12-24_00-43-26-198.txt
 received: 13,455 / 100,000 = 13.5%
  dropped: 86,545
reordered: 16

result_2022-12-24_00-43-41-478.txt
 received: 13,787 / 100,000 = 13.8%
  dropped: 86,213
reordered: 14

result_2022-12-24_00-43-59-230.txt
 received: 14,164 / 100,000 = 14.2%
  dropped: 85,836
reordered: 16

result_2022-12-24_00-44-14-371.txt
 received: 13,515 / 100,000 = 13.5%
  dropped: 86,485
reordered: 17
```

ドロップが大量発生。
しかしリオーダーは最初の方（200パケット程度）でしか起こらない。

# ローカル環境（送信間隔: 0マイクロ秒）

ローカル環境でも大量送信。

```
result_2022-12-24_00-16-49-144.txt
 received: 99,917 / 100,000 = 99.9%
  dropped: 83
reordered: 0

result_2022-12-24_00-17-13-855.txt
 received: 99,109 / 100,000 = 99.1%
  dropped: 891
reordered: 0

result_2022-12-24_00-17-29-220.txt
 received: 98,903 / 100,000 = 98.9%
  dropped: 1,097
reordered: 0

result_2022-12-24_00-17-42-538.txt
 received: 98,754 / 100,000 = 98.8%
  dropped: 1,246
reordered: 0

result_2022-12-24_00-17-55-748.txt
 received: 99,751 / 100,000 = 99.8%
  dropped: 249
reordered: 0

result_2022-12-24_00-18-08-066.txt
 received: 100,000 / 100,000 = 100.0%
  dropped: 0
reordered: 0
```

さすがにローカル環境でもドロップする。
リオーダーは発生しない。

# まとめ

ドロップは頻繁に発生する。
リオーダーはあまり発生しない。

リオーダーが発生する条件はよくわかっていないが、等間隔で送信しない場合はリオーダーが頻発する可能性はありそう。データサイズにばらつきがある場合も変わるかもしれない。

# 検証コード

```cs
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;

var command = args.Length >= 1 ? args[0] : null;

var cts = new CancellationTokenSource();

Console.CancelKeyPress += (s, e) =>
{
    cts.Cancel();
    e.Cancel = true;
};

switch (command)
{
    case "send":
        {
            var endPoint = IPEndPoint.Parse(args.Length > 1 ? args[1] : string.Empty);
            var interval = long.Parse(args.Length > 2 ? args[2] : "100");
            UdpTester.Send(endPoint, interval, cts.Token);
            break;
        }
    case "receive":
        {
            var endPoint = IPEndPoint.Parse(args.Length >= 2 ? args[1] : string.Empty);
            await UdpTester.Receive(endPoint, cts.Token);
            break;
        }
    case "report":
        {
            var file = args.Length >= 2 ? args[1] : throw new InvalidOperationException("file not specified.");
            await UdpTester.Report(file, cts.Token);
            break;
        }
    default:
        Console.WriteLine("unknown command");
        return;
}

static class UdpTester
{
    private const int NumberOfPacket = 100_000;

    public static void Send(IPEndPoint endPoint, long interval, CancellationToken cancellationToken)
    {
        using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
        {
            SendBufferSize = 100 * 1024 * 1024
        };

        var extraLength = 0;
        var buffer = new byte[4 + extraLength];

        Console.WriteLine($"sending... {endPoint} interval:{interval:#,0}");

        var beforeTick = Environment.TickCount;

        try
        {
            for (var i = 0; i < NumberOfPacket; i++)
            {
                cancellationToken.ThrowIfCancellationRequested();

                MemoryMarshal.Write(buffer, ref i);
                socket.SendTo(buffer, endPoint);

                var tick = Environment.TickCount;
                var elapsed = tick - beforeTick;
                if (elapsed >= 1000)
                {
                    Console.WriteLine(i);
                    beforeTick = tick;
                }

                if (interval > 0)
                {
                    SleepByStopwatch(interval);
                }
            }
        }
        catch (OperationCanceledException)
        {
        }

        Console.WriteLine("completed");
    }

    private static void SleepByStopwatch(long durationMicroSeconds)
    {
        var durationTicks = durationMicroSeconds * Stopwatch.Frequency / 1000 / 1000;
        var endTime = Stopwatch.GetTimestamp() + durationTicks;

        while (Stopwatch.GetTimestamp() < endTime);
    }

    public static async Task Receive(IPEndPoint endPoint, CancellationToken cancellationToken)
    {
        using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
        {
            ReceiveBufferSize = 100 * 1024 * 1024,
        };

        socket.Bind(endPoint);

        Memory<byte> buffer = new byte[1024];
        EndPoint remoteEndPoint = new IPEndPoint(0, 0);

        using var output = new StreamWriter($"./result_{DateTime.Now:yyyy-MM-dd_HH-mm-ss-fff}.txt", false, Encoding.UTF8);
        Console.WriteLine("receiving...");

        var beforeTick = Environment.TickCount;
        var count = 0;

        try
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                var result = await socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint, cancellationToken);
                if (result.ReceivedBytes == 4)
                {
                    var value = MemoryMarshal.Read<int>(buffer.Span);
                    var isNG = count != value;
                    output.WriteLine(value);
                    count++;

                    var tick = Environment.TickCount;
                    var elapsed = tick - beforeTick;
                    if (elapsed >= 1000)
                    {
                        Console.WriteLine(count);
                        beforeTick = tick;
                    }
                }
                else
                {
                    Console.WriteLine($"{result.ReceivedBytes}: invalid length");
                }
            }
        }
        catch (OperationCanceledException)
        {
        }

        Console.WriteLine("completed");
    }

    public static async Task Report(string file, CancellationToken cancellationToken)
    {
        var lines = await File.ReadAllLinesAsync(file, Encoding.UTF8, cancellationToken);
        var values = lines.Select(v => int.Parse(v)).ToArray();
        var reordered = values.Order().ToArray();

        var correctValue = 0;
        var droppedCount =
            reordered.Aggregate(0, (p, v) =>
            {
                var dropped = v - correctValue;
                correctValue = v + 1;
                return dropped == 0 ? p : p + dropped;
            })
            + (NumberOfPacket - reordered.DefaultIfEmpty(-1).LastOrDefault() - 1);

        var beforeValue = -1;
        var reorderedCount =
            values.Aggregate(0, (p, v) =>
            {
                var isReorder = v < beforeValue;
                beforeValue = v;
                return isReorder ? p + 1 : p;
            });

        Console.WriteLine($" received: {values.Length:#,0} / {NumberOfPacket:#,0} = {(double)values.Length / NumberOfPacket:0.0%}");
        Console.WriteLine($"  dropped: {droppedCount:#,0}");
        Console.WriteLine($"reordered: {reorderedCount:#,0}");
    }
}
```