C# で特定の相手とUDP通信を行う

# 今回の目的

C# を用いて、以下を指定したUDP通信を行う。

* 通信相手のアドレス
* 通信相手のポート
* 自分の (通信相手から送ってもらう) ポート

いわゆる「クライアントとサーバー」という形態ではなく、相互に通信相手の情報を指定して通信を行う。

![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/d7e7b3e0-521a-f46b-abf8-7e228d53939b.png)

# 用いるAPI

[UdpClient](https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.udpclient?view=net-8.0) クラスを用いることで、UDP通信を行うことができる。

## プロトコルと自分のポートの設定

[UdpClient(Int32, AddressFamily)](https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.udpclient.-ctor?view=net-8.0#system-net-sockets-udpclient-ctor(system-int32-system-net-sockets-addressfamily)) コンストラクターにより、用いるプロトコルと自分のポートを指定し、`UdpClient` クラスのオブジェクトを生成できる。
今後の通信には、このオブジェクトを用いる。

第1引数で、自分のポートを整数で指定する。
第2引数は、以下を指定する。

|用いるプロトコル|指定する値|
|:-:|---|
|IPv4|`AddressFamily.InterNetwork`|
|IPv6|`AddressFamily.InterNetworkV6`|

```csharp
// IPv4 を使用し、自分のポートを 33333 としてオブジェクトを生成する
UdpClient client = new UdpClient(33333, AddressFamily.InterNetwork);
```

## 通信相手の設定

[Connect(String, Int32)](https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.udpclient.connect?view=net-8.0#system-net-sockets-udpclient-connect(system-string-system-int32)) メソッドにより、通信相手のアドレスとポートを設定できる。

第1引数で、通信相手のアドレスを指定する。
第2引数で、通信相手のポートを指定する。

```csharp
// 通信相手のアドレスを 198.51.100.123 ポートを 22222 に設定する
client.Connect("198.51.100.123", 22222);
```

通信相手のアドレスとして、ホスト名も指定できる。

```csharp
// 通信相手のアドレスを example.com ポートを 22222 に設定する
client.Connect("example.com", 22222);
```

## データの送信

[Send(Byte[], Int32)](https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.udpclient.send?view=net-8.0#system-net-sockets-udpclient-send(system-byte()-system-int32)) メソッドにより、設定した相手にデータを送信できる。

第1引数で、送信するデータ (バイト列) を指定する。
第2引数で、送信するデータの長さ (バイト数) を指定する。

```csharp
byte[] payload = new byte[]{0xde, 0xad, 0xbe, 0xef};
// 配列 payload の全体を送信する
client.Send(payload, payload.Length);
```

## データの受信

[ReceiveAsync()](https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.udpclient.receiveasync?view=net-8.0#system-net-sockets-udpclient-receiveasync) メソッドにより、データを受信すると完了する [Task](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task-1?view=net-8.0) を作成できる。
また、この `Task` は通信に用いる `UdpClient` を閉じたときにも完了する。

この `Task` に対し [ContinueWith(Action&lt;Task,Object&gt;, Object)](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.continuewith?view=net-8.0#system-threading-tasks-task-continuewith(system-action((system-threading-tasks-task-system-object))-system-object)) メソッドを用いることで、このタスクの完了時にメソッドを呼び出してもらうことができる。
第1引数で、呼び出してもらうメソッドを指定する。
第2引数で、メソッドを呼び出す際に渡すオブジェクトを指定する。
呼び出すメソッドの第1引数は完了した `Task`、第2引数は指定したオブジェクトである。
このオブジェクトとして `UdpClient` を渡しておくことで、受信後に次のデータを受信するため再び `ReceiveAsync()` を呼び出すことができる。

```csharp
// データ受信 (タスク完了) 時、OnReceive メソッドを呼び出す
// 呼び出す際、メソッドに完了したタスクとともに client を渡す
client.ReceiveAsync().ContinueWith(OnReceive, client);
```

`Task` の完了時、データの受信に成功したかどうかは `Task` の [Status](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.status?view=net-8.0#system-threading-tasks-task-status) プロパティにより判別できる。
このプロパティの値が [TaskStatus.RanToCompletion](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.taskstatus?view=net-8.0) であれば、データの受信に成功している。

データの受信に成功した際、`Task` の [Result](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task-1.result?view=net-8.0#system-threading-tasks-task-1-result) プロパティで [UdpReceiveResult](https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.udpreceiveresult?view=net-8.0) 構造体のデータを参照できる。
この構造体の [Buffer](https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.udpreceiveresult.buffer?view=net-8.0#system-net-sockets-udpreceiveresult-buffer) プロパティに、受信したデータ (`byte[]`) が格納されている。

データの受信に失敗した際、`Task` の [Exception](https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.exception?view=net-8.0#system-threading-tasks-task-exception) プロパティで失敗の原因となった例外を参照できる。
このプロパティは [AggregateException](https://learn.microsoft.com/ja-jp/dotnet/api/system.aggregateexception?view=net-8.0) クラスで、その [InnerException](https://learn.microsoft.com/ja-jp/dotnet/api/system.exception.innerexception?view=net-8.0#system-exception-innerexception) プロパティに実際の例外が格納されている。
この例外は、`UdpClient` が閉じられたために受信に失敗した場合は [ObjectDisposedException](https://learn.microsoft.com/ja-jp/dotnet/api/system.objectdisposedexception?view=net-8.0)、その他のエラーのために受信に失敗した場合は [SocketException](https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.socketexception?view=net-8.0) となる。

```csharp
void OnReceive(Task<UdpReceiveResult> task, Object client)
{
	if (task.Status == TaskStatus.RanToCompletion)
	{
		// 受信に成功した
		byte[] data = task.Result.Buffer; // 受信したデータ
	}
	else if (task.Exception.InnerException is ObjectDisposedException)
	{
		// 通信クライアントが閉じられた
		return; // 新しい受信タスクを起動しない
	}
	else
	{
		// その他のエラー
	}
	// 次の受信を行う
	((UdpClient)client).ReceiveAsync().ContinueWith(OnReceive, client);
}
```

# サンプルプログラム

以下のように実行する。
これは、通信相手のアドレスが `example.com`、通信相手のポートが `55555`、自分のポートが `33333` であることを表している。

```text
UdpTest example.com 55555 33333
```

この通信の相手側では、以下のように通信相手として反対側 (自分側) のアドレスを指定し、ポートの指定も逆になる。

```text
UdpTest 203.0.113.222 33333 55555
```

また、IPv6 を用いる場合は、第4引数として `1` (非零の整数) を追加する。

```text
UdpTest example.com 55555 33333 1
```

実行すると、適当なデータ ([Guid.NewGuid()](https://learn.microsoft.com/ja-jp/dotnet/api/system.guid.newguid?view=net-8.0) メソッドで生成した GUID) を約1秒おきに30回送信する。
また、相手から受信したデータを出力する。

```csharp:UdpTest.cs
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class UdpTest
{
	public static void Main(string[] args)
	{
		if (args.Length != 3 && args.Length != 4)
		{
			Console.WriteLine("Usage: UdpTest dest_addr dest_port local_port [use_ipv6]");
			return;
		}
		string destAddress = args[0];
		int destPort = int.Parse(args[1]);
		int localPort = int.Parse(args[2]);
		int useIPv6 = args.Length >= 4 ? int.Parse(args[3]) : 0;

		UdpClient client = new UdpClient(
			localPort,
			useIPv6 != 0 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork
		);
		client.Connect(destAddress, destPort);
		client.ReceiveAsync().ContinueWith(OnReceive, client);
		for (int i = 0; i < 30; i++)
		{
			string message = Guid.NewGuid().ToString();
			byte[] messageBytes = Encoding.UTF8.GetBytes(message);
			Console.WriteLine("sending: {0}", message);
			client.Send(messageBytes, messageBytes.Length);
			Task.Delay(1000).Wait();
		}
		Console.WriteLine("closing");
		client.Close();
		Task.Delay(1000).Wait();
	}

	private static void OnReceive(Task<UdpReceiveResult> task, Object client)
	{
		if (task.Status == TaskStatus.RanToCompletion)
		{
			string receivedMessage = null;
			try
			{
				receivedMessage = Encoding.UTF8.GetString(task.Result.Buffer);
			}
			catch (Exception)
			{
				receivedMessage = BitConverter.ToString(task.Result.Buffer);
			}
			Console.WriteLine("received: {0}", receivedMessage);
		}
		else if (task.Exception.InnerException is ObjectDisposedException)
		{
			Console.WriteLine("receiver: connection closed");
			return; // 新しい受信タスクを起動しない
		}
		else
		{
			Console.WriteLine("receiving faulted:");
			Console.WriteLine(task.Exception);
		}
		((UdpClient)client).ReceiveAsync().ContinueWith(OnReceive, client);
	}
}
```

# 自分との通信におけるエラー

手元の Windows 11 環境で、このサンプルプログラムを用い、自分 (`localhost` および自分のプライベートIPアドレス) との通信を試みると、データを送信した際、受信タスクが以下の例外で完了した。

```text
System.AggregateException: 1 つ以上のエラーが発生しました。 ---> System.Net.Sockets.SocketException: 既存の接続はリモー ト ホストに強制的に切断されました。
   場所 System.Net.Sockets.Socket.EndReceiveFrom(IAsyncResult asyncResult, EndPoint& endPoint)
   場所 System.Net.Sockets.UdpClient.EndReceive(IAsyncResult asyncResult, IPEndPoint& remoteEP)
   場所 System.Net.Sockets.UdpClient.<ReceiveAsync>b__64_1(IAsyncResult ar)
   場所 System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
   --- 内部例外スタック トレースの終わり ---
---> (内部例外 #0) System.Net.Sockets.SocketException (0x80004005): 既存の接続はリモート ホストに強制的に切断されました 。
   場所 System.Net.Sockets.Socket.EndReceiveFrom(IAsyncResult asyncResult, EndPoint& endPoint)
   場所 System.Net.Sockets.UdpClient.EndReceive(IAsyncResult asyncResult, IPEndPoint& remoteEP)
   場所 System.Net.Sockets.UdpClient.<ReceiveAsync>b__64_1(IAsyncResult ar)
   場所 System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)<---
```

これはサンプルプログラムを1個起動した状態でのことである。
サンプルプログラムをもう1個ポートの指定を逆にして起動すると、例外の発生は止まり、送受信に成功した。

通信相手を外部のアドレスにした場合は、通信相手でこのプログラムを起動していない状態でも、単にデータを受信しないだけでこのような例外は発生しなかった。

よって、通信相手を自分に設定した場合は、通常の UDP 通信を行うかわりに、何か特殊な処理が行われているようである。

# まとめ

`UdpClient` クラスを使用し、C# で互いに通信相手を指定した UDP 通信を行うことができた。
最初にモードを設定することで、IPv4 だけでなく IPv6 を用いた通信もできた。

ただし、通信相手として自分を指定した場合は、通常の UDP 通信ではなく特殊な処理が行われ、送信先がデータを受け取る準備ができていないと例外が発生してしまうようである。