DiscoveryService.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. using System.Net;
  2. using System.Net.Sockets;
  3. using System.Runtime.InteropServices;
  4. using System.Text;
  5. using System.Text.Json;
  6. using System.Text.Json.Serialization;
  7. using NetworkTool.Client.Models;
  8. namespace NetworkTool.Client.Services;
  9. public sealed class DiscoveryService
  10. {
  11. private const int DiscoveryPort = 50000;
  12. private static readonly TimeSpan DiscoveryTimeout = TimeSpan.FromSeconds(5);
  13. private static readonly TimeSpan DiscoveryRetryInterval = TimeSpan.FromSeconds(1);
  14. [DllImport("iphlpapi.dll", ExactSpelling = true)]
  15. private static extern int SendARP(int destinationIp, int sourceIp, byte[] macAddress, ref int physicalAddressLength);
  16. public async Task<IReadOnlyList<DiscoveredDevice>> DiscoverManyAsync(
  17. string localIPv4,
  18. Action<DiscoveredDevice>? onDeviceDiscovered = null,
  19. CancellationToken cancellationToken = default)
  20. {
  21. if (string.IsNullOrWhiteSpace(localIPv4))
  22. {
  23. return [];
  24. }
  25. using var client = new UdpClient(AddressFamily.InterNetwork)
  26. {
  27. EnableBroadcast = true,
  28. };
  29. client.Client.Bind(new IPEndPoint(IPAddress.Parse(localIPv4), 0));
  30. var request = new
  31. {
  32. protocol_version = 1,
  33. message_type = "discover",
  34. request_id = Guid.NewGuid().ToString(),
  35. client_name = Environment.MachineName,
  36. };
  37. var payload = JsonSerializer.SerializeToUtf8Bytes(request);
  38. var broadcastEndPoint = new IPEndPoint(IPAddress.Broadcast, DiscoveryPort);
  39. await client.SendAsync(payload, payload.Length, broadcastEndPoint);
  40. using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  41. timeoutCts.CancelAfter(DiscoveryTimeout);
  42. var devicesByIp = new Dictionary<string, DiscoveredDevice>(StringComparer.OrdinalIgnoreCase);
  43. var nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
  44. while (!timeoutCts.IsCancellationRequested)
  45. {
  46. try
  47. {
  48. var retryDelay = nextRetryAt - DateTimeOffset.UtcNow;
  49. if (retryDelay <= TimeSpan.Zero)
  50. {
  51. await client.SendAsync(payload, payload.Length, broadcastEndPoint);
  52. nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
  53. continue;
  54. }
  55. using var receiveCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token);
  56. var receiveTask = client.ReceiveAsync(receiveCts.Token).AsTask();
  57. var retryDelayTask = Task.Delay(retryDelay, timeoutCts.Token);
  58. var completedTask = await Task.WhenAny(receiveTask, retryDelayTask);
  59. if (completedTask == retryDelayTask)
  60. {
  61. receiveCts.Cancel();
  62. try
  63. {
  64. await receiveTask;
  65. }
  66. catch (OperationCanceledException)
  67. {
  68. }
  69. if (timeoutCts.IsCancellationRequested)
  70. {
  71. break;
  72. }
  73. await client.SendAsync(payload, payload.Length, broadcastEndPoint);
  74. nextRetryAt = DateTimeOffset.UtcNow + DiscoveryRetryInterval;
  75. continue;
  76. }
  77. var result = await receiveTask;
  78. var response = JsonSerializer.Deserialize<DiscoveryResponse>(result.Buffer);
  79. if (response is null
  80. || response.MessageType != "discover_response"
  81. || string.IsNullOrWhiteSpace(response.Lan2Ip)
  82. || !response.Lan2Ip.StartsWith("169.254.", StringComparison.Ordinal))
  83. {
  84. continue;
  85. }
  86. var mac = response.Mac;
  87. if (string.IsNullOrWhiteSpace(mac))
  88. {
  89. mac = ResolveMacAddress(result.RemoteEndPoint.Address);
  90. }
  91. var device = new DiscoveredDevice
  92. {
  93. DeviceId = response.DeviceId ?? string.Empty,
  94. Hostname = response.Hostname ?? string.Empty,
  95. ServerVersion = response.ServerVersion ?? string.Empty,
  96. Mac = mac ?? string.Empty,
  97. Lan2Ip = response.Lan2Ip,
  98. HttpPort = response.HttpPort,
  99. AuthRequired = response.AuthRequired,
  100. };
  101. devicesByIp[response.Lan2Ip] = device;
  102. onDeviceDiscovered?.Invoke(device);
  103. }
  104. catch (OperationCanceledException)
  105. {
  106. break;
  107. }
  108. }
  109. return devicesByIp.Values.OrderBy(device => device.Lan2Ip).ToList();
  110. }
  111. private static string ResolveMacAddress(IPAddress ipAddress)
  112. {
  113. if (ipAddress.AddressFamily != AddressFamily.InterNetwork)
  114. {
  115. return string.Empty;
  116. }
  117. var macAddress = new byte[6];
  118. var macAddressLength = macAddress.Length;
  119. var destinationIp = BitConverter.ToInt32(ipAddress.GetAddressBytes(), 0);
  120. if (SendARP(destinationIp, 0, macAddress, ref macAddressLength) != 0 || macAddressLength <= 0)
  121. {
  122. return string.Empty;
  123. }
  124. return string.Join(":", macAddress.Take(macAddressLength).Select(value => value.ToString("X2")));
  125. }
  126. private sealed class DiscoveryResponse
  127. {
  128. [JsonPropertyName("message_type")]
  129. public string? MessageType { get; set; }
  130. [JsonPropertyName("device_id")]
  131. public string? DeviceId { get; set; }
  132. [JsonPropertyName("hostname")]
  133. public string? Hostname { get; set; }
  134. [JsonPropertyName("server_version")]
  135. public string? ServerVersion { get; set; }
  136. [JsonPropertyName("mac")]
  137. public string? Mac { get; set; }
  138. [JsonPropertyName("lan2_ip")]
  139. public string? Lan2Ip { get; set; }
  140. [JsonPropertyName("http_port")]
  141. public int HttpPort { get; set; }
  142. [JsonPropertyName("auth_required")]
  143. public bool AuthRequired { get; set; }
  144. }
  145. }