SsdpHandler.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. using MediaBrowser.Common;
  2. using MediaBrowser.Common.Configuration;
  3. using MediaBrowser.Common.Events;
  4. using MediaBrowser.Controller.Configuration;
  5. using MediaBrowser.Controller.Dlna;
  6. using MediaBrowser.Dlna.Server;
  7. using MediaBrowser.Model.Logging;
  8. using System;
  9. using System.Collections.Concurrent;
  10. using System.Collections.Generic;
  11. using System.Globalization;
  12. using System.Linq;
  13. using System.Net;
  14. using System.Net.Sockets;
  15. using System.Text;
  16. using System.Threading;
  17. using System.Threading.Tasks;
  18. using Microsoft.Win32;
  19. namespace MediaBrowser.Dlna.Ssdp
  20. {
  21. public class SsdpHandler : IDisposable, ISsdpHandler
  22. {
  23. private Socket _multicastSocket;
  24. private readonly ILogger _logger;
  25. private readonly IServerConfigurationManager _config;
  26. const string SSDPAddr = "239.255.255.250";
  27. const int SSDPPort = 1900;
  28. private readonly string _serverSignature;
  29. private readonly IPAddress _ssdpIp = IPAddress.Parse(SSDPAddr);
  30. private readonly IPEndPoint _ssdpEndp = new IPEndPoint(IPAddress.Parse(SSDPAddr), SSDPPort);
  31. private Timer _notificationTimer;
  32. private bool _isDisposed;
  33. private readonly ConcurrentDictionary<string, List<UpnpDevice>> _devices = new ConcurrentDictionary<string, List<UpnpDevice>>();
  34. private readonly IApplicationHost _appHost;
  35. private readonly int _unicastPort = 1901;
  36. private UdpClient _unicastClient;
  37. public SsdpHandler(ILogger logger, IServerConfigurationManager config, IApplicationHost appHost)
  38. {
  39. _logger = logger;
  40. _config = config;
  41. _appHost = appHost;
  42. _config.NamedConfigurationUpdated += _config_ConfigurationUpdated;
  43. _serverSignature = GenerateServerSignature();
  44. }
  45. private string GenerateServerSignature()
  46. {
  47. var os = Environment.OSVersion;
  48. var pstring = os.Platform.ToString();
  49. switch (os.Platform)
  50. {
  51. case PlatformID.Win32NT:
  52. case PlatformID.Win32S:
  53. case PlatformID.Win32Windows:
  54. pstring = "WIN";
  55. break;
  56. }
  57. return String.Format(
  58. "{0}{1}/{2}.{3} UPnP/1.0 DLNADOC/1.5 Emby/{4}",
  59. pstring,
  60. IntPtr.Size * 8,
  61. os.Version.Major,
  62. os.Version.Minor,
  63. _appHost.ApplicationVersion
  64. );
  65. }
  66. void _config_ConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
  67. {
  68. if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
  69. {
  70. ReloadAliveNotifier();
  71. }
  72. }
  73. public event EventHandler<SsdpMessageEventArgs> MessageReceived;
  74. private async void OnMessageReceived(SsdpMessageEventArgs args, bool isMulticast)
  75. {
  76. if (IgnoreMessage(args, isMulticast))
  77. {
  78. return;
  79. }
  80. LogMessageReceived(args, isMulticast);
  81. var headers = args.Headers;
  82. string st;
  83. if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase) && headers.TryGetValue("st", out st))
  84. {
  85. TimeSpan delay = GetSearchDelay(headers);
  86. if (_config.GetDlnaConfiguration().EnableDebugLog)
  87. {
  88. _logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds);
  89. }
  90. await Task.Delay(delay).ConfigureAwait(false);
  91. RespondToSearch(args.EndPoint, st);
  92. }
  93. EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger);
  94. }
  95. internal void LogMessageReceived(SsdpMessageEventArgs args, bool isMulticast)
  96. {
  97. var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
  98. if (enableDebugLogging)
  99. {
  100. var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
  101. var headerText = string.Join(",", headerTexts.ToArray());
  102. var protocol = isMulticast ? "Multicast" : "Unicast";
  103. var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
  104. _logger.Debug("{0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
  105. }
  106. }
  107. internal bool IgnoreMessage(SsdpMessageEventArgs args, bool isMulticast)
  108. {
  109. if (!isMulticast)
  110. {
  111. return false;
  112. }
  113. string usn;
  114. if (args.Headers.TryGetValue("USN", out usn))
  115. {
  116. // USN=uuid:b67df29b5c379445fde78c3774ab518d::urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1
  117. if (RegisteredDevices.Select(i => i.USN).Contains(usn, StringComparer.OrdinalIgnoreCase))
  118. {
  119. //var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
  120. //var headerText = string.Join(",", headerTexts.ToArray());
  121. //var protocol = isMulticast ? "Multicast" : "Unicast";
  122. //var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
  123. //_logger.Debug("IGNORING {0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
  124. return true;
  125. }
  126. }
  127. string serverId;
  128. if (args.Headers.TryGetValue("X-EMBY-SERVERID", out serverId))
  129. {
  130. if (string.Equals(serverId, _appHost.SystemId, StringComparison.OrdinalIgnoreCase))
  131. {
  132. //var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
  133. //var headerText = string.Join(",", headerTexts.ToArray());
  134. //var protocol = isMulticast ? "Multicast" : "Unicast";
  135. //var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
  136. //_logger.Debug("IGNORING {0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
  137. return true;
  138. }
  139. }
  140. return false;
  141. }
  142. public IEnumerable<UpnpDevice> RegisteredDevices
  143. {
  144. get
  145. {
  146. var devices = _devices.Values.ToList();
  147. return devices.SelectMany(i => i).ToList();
  148. }
  149. }
  150. public void Start()
  151. {
  152. DisposeSocket();
  153. StopAliveNotifier();
  154. RestartSocketListener();
  155. ReloadAliveNotifier();
  156. CreateUnicastClient();
  157. SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
  158. SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
  159. }
  160. void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
  161. {
  162. if (e.Mode == PowerModes.Resume)
  163. {
  164. Start();
  165. }
  166. }
  167. public void SendSearchMessage(EndPoint localIp)
  168. {
  169. var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  170. values["HOST"] = "239.255.255.250:1900";
  171. values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/1.0.4.2";
  172. values["X-EMBY-SERVERID"] = _appHost.SystemId;
  173. values["MAN"] = "\"ssdp:discover\"";
  174. // Search target
  175. values["ST"] = "ssdp:all";
  176. // Seconds to delay response
  177. values["MX"] = "3";
  178. var header = "M-SEARCH * HTTP/1.1";
  179. var msg = new SsdpMessageBuilder().BuildMessage(header, values);
  180. // UDP is unreliable, so send 3 requests at a time (per Upnp spec, sec 1.1.2)
  181. SendDatagram(msg, _ssdpEndp, localIp, true);
  182. SendUnicastRequest(msg);
  183. }
  184. public async void SendDatagram(string msg,
  185. EndPoint endpoint,
  186. EndPoint localAddress,
  187. bool isBroadcast,
  188. int sendCount = 3)
  189. {
  190. var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
  191. for (var i = 0; i < sendCount; i++)
  192. {
  193. if (i > 0)
  194. {
  195. await Task.Delay(200).ConfigureAwait(false);
  196. }
  197. var dgram = new Datagram(endpoint, localAddress, _logger, msg, isBroadcast, enableDebugLogging);
  198. dgram.Send();
  199. }
  200. }
  201. /// <summary>
  202. /// According to the spec: http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0-20080424.pdf
  203. /// Device responses should be delayed a random duration between 0 and this many seconds to balance
  204. /// load for the control point when it processes responses. In my testing kodi times out after mx
  205. /// so we will generate from mx - 1
  206. /// </summary>
  207. /// <param name="headers">The mx headers</param>
  208. /// <returns>A timepsan for the amount to delay before returning search result.</returns>
  209. private TimeSpan GetSearchDelay(Dictionary<string, string> headers)
  210. {
  211. string mx;
  212. headers.TryGetValue("mx", out mx);
  213. int delaySeconds = 0;
  214. if (!string.IsNullOrWhiteSpace(mx)
  215. && int.TryParse(mx, NumberStyles.Any, CultureInfo.InvariantCulture, out delaySeconds)
  216. && delaySeconds > 1)
  217. {
  218. delaySeconds = new Random().Next(delaySeconds - 1);
  219. }
  220. return TimeSpan.FromSeconds(delaySeconds);
  221. }
  222. private void RespondToSearch(EndPoint endpoint, string deviceType)
  223. {
  224. var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
  225. var isLogged = false;
  226. const string header = "HTTP/1.1 200 OK";
  227. foreach (var d in RegisteredDevices)
  228. {
  229. if (string.Equals(deviceType, "ssdp:all", StringComparison.OrdinalIgnoreCase) ||
  230. string.Equals(deviceType, d.Type, StringComparison.OrdinalIgnoreCase))
  231. {
  232. if (!isLogged)
  233. {
  234. if (enableDebugLogging)
  235. {
  236. _logger.Debug("Responding to search from {0} for {1}", endpoint, deviceType);
  237. }
  238. isLogged = true;
  239. }
  240. var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  241. values["CACHE-CONTROL"] = "max-age = 600";
  242. values["DATE"] = DateTime.Now.ToString("R");
  243. values["EXT"] = "";
  244. values["LOCATION"] = d.Descriptor.ToString();
  245. values["SERVER"] = _serverSignature;
  246. values["ST"] = d.Type;
  247. values["USN"] = d.USN;
  248. var msg = new SsdpMessageBuilder().BuildMessage(header, values);
  249. var ipEndPoint = endpoint as IPEndPoint;
  250. if (ipEndPoint != null)
  251. {
  252. SendUnicastRequest(msg, ipEndPoint);
  253. }
  254. else
  255. {
  256. SendDatagram(msg, endpoint, null, false, 2);
  257. SendDatagram(msg, endpoint, new IPEndPoint(d.Address, 0), false, 2);
  258. //SendDatagram(header, values, endpoint, null, true);
  259. }
  260. if (enableDebugLogging)
  261. {
  262. _logger.Debug("{1} - Responded to a {0} request to {2}", d.Type, endpoint, d.Address.ToString());
  263. }
  264. }
  265. }
  266. }
  267. private void RestartSocketListener()
  268. {
  269. if (_isDisposed)
  270. {
  271. return;
  272. }
  273. try
  274. {
  275. _multicastSocket = CreateMulticastSocket();
  276. _logger.Info("MultiCast socket created");
  277. Receive();
  278. }
  279. catch (Exception ex)
  280. {
  281. _logger.ErrorException("Error creating MultiCast socket", ex);
  282. //StartSocketRetryTimer();
  283. }
  284. }
  285. private void Receive()
  286. {
  287. try
  288. {
  289. var buffer = new byte[1024];
  290. EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort);
  291. _multicastSocket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpoint, ReceiveCallback, buffer);
  292. }
  293. catch (ObjectDisposedException)
  294. {
  295. if (!_isDisposed)
  296. {
  297. //StartSocketRetryTimer();
  298. }
  299. }
  300. catch (Exception ex)
  301. {
  302. _logger.Debug("Error in BeginReceiveFrom", ex);
  303. }
  304. }
  305. private void ReceiveCallback(IAsyncResult result)
  306. {
  307. if (_isDisposed)
  308. {
  309. return;
  310. }
  311. try
  312. {
  313. EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort);
  314. var length = _multicastSocket.EndReceiveFrom(result, ref endpoint);
  315. var received = (byte[])result.AsyncState;
  316. var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
  317. if (enableDebugLogging)
  318. {
  319. _logger.Debug(Encoding.ASCII.GetString(received));
  320. }
  321. var args = SsdpHelper.ParseSsdpResponse(received);
  322. args.EndPoint = endpoint;
  323. OnMessageReceived(args, true);
  324. }
  325. catch (ObjectDisposedException)
  326. {
  327. if (!_isDisposed)
  328. {
  329. //StartSocketRetryTimer();
  330. }
  331. }
  332. catch (Exception ex)
  333. {
  334. _logger.ErrorException("Failed to read SSDP message", ex);
  335. }
  336. if (_multicastSocket != null)
  337. {
  338. Receive();
  339. }
  340. }
  341. public void Dispose()
  342. {
  343. _config.NamedConfigurationUpdated -= _config_ConfigurationUpdated;
  344. SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
  345. _isDisposed = true;
  346. DisposeUnicastClient();
  347. DisposeSocket();
  348. StopAliveNotifier();
  349. }
  350. private void DisposeSocket()
  351. {
  352. if (_multicastSocket != null)
  353. {
  354. _multicastSocket.Close();
  355. _multicastSocket.Dispose();
  356. _multicastSocket = null;
  357. }
  358. }
  359. private Socket CreateMulticastSocket()
  360. {
  361. var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
  362. socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
  363. socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
  364. socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
  365. socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(_ssdpIp, 0));
  366. socket.Bind(new IPEndPoint(IPAddress.Any, SSDPPort));
  367. return socket;
  368. }
  369. private void NotifyAll()
  370. {
  371. var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
  372. if (enableDebugLogging)
  373. {
  374. _logger.Debug("Sending alive notifications");
  375. }
  376. foreach (var d in RegisteredDevices)
  377. {
  378. NotifyDevice(d, "alive", enableDebugLogging);
  379. }
  380. }
  381. private void NotifyDevice(UpnpDevice dev, string type, bool logMessage)
  382. {
  383. const string header = "NOTIFY * HTTP/1.1";
  384. var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  385. // If needed later for non-server devices, these headers will need to be dynamic
  386. values["HOST"] = "239.255.255.250:1900";
  387. values["CACHE-CONTROL"] = "max-age = 600";
  388. values["LOCATION"] = dev.Descriptor.ToString();
  389. values["SERVER"] = _serverSignature;
  390. values["NTS"] = "ssdp:" + type;
  391. values["NT"] = dev.Type;
  392. values["USN"] = dev.USN;
  393. values["X-EMBY-SERVERID"] = _appHost.SystemId;
  394. if (logMessage)
  395. {
  396. _logger.Debug("{0} said {1}", dev.USN, type);
  397. }
  398. var msg = new SsdpMessageBuilder().BuildMessage(header, values);
  399. SendDatagram(msg, _ssdpEndp, new IPEndPoint(dev.Address, 0), true);
  400. //SendUnicastRequest(msg, 1);
  401. }
  402. public void RegisterNotification(string uuid, Uri descriptionUri, IPAddress address, IEnumerable<string> services)
  403. {
  404. var list = _devices.GetOrAdd(uuid, new List<UpnpDevice>());
  405. list.AddRange(services.Select(i => new UpnpDevice(uuid, i, descriptionUri, address)));
  406. NotifyAll();
  407. _logger.Debug("Registered mount {0} at {1}", uuid, descriptionUri);
  408. }
  409. public void UnregisterNotification(string uuid)
  410. {
  411. List<UpnpDevice> dl;
  412. if (_devices.TryRemove(uuid, out dl))
  413. {
  414. foreach (var d in dl.ToList())
  415. {
  416. NotifyDevice(d, "byebye", true);
  417. }
  418. _logger.Debug("Unregistered mount {0}", uuid);
  419. }
  420. }
  421. private void CreateUnicastClient()
  422. {
  423. if (_unicastClient == null)
  424. {
  425. try
  426. {
  427. _unicastClient = new UdpClient(_unicastPort);
  428. }
  429. catch (Exception ex)
  430. {
  431. _logger.ErrorException("Error creating unicast client", ex);
  432. }
  433. UnicastSetBeginReceive();
  434. }
  435. }
  436. private void DisposeUnicastClient()
  437. {
  438. if (_unicastClient != null)
  439. {
  440. try
  441. {
  442. _unicastClient.Close();
  443. }
  444. catch (Exception ex)
  445. {
  446. _logger.ErrorException("Error closing unicast client", ex);
  447. }
  448. _unicastClient = null;
  449. }
  450. }
  451. /// <summary>
  452. /// Listen for Unicast SSDP Responses
  453. /// </summary>
  454. private void UnicastSetBeginReceive()
  455. {
  456. try
  457. {
  458. var ipRxEnd = new IPEndPoint(IPAddress.Any, _unicastPort);
  459. var udpListener = new UdpState { EndPoint = ipRxEnd };
  460. udpListener.UdpClient = _unicastClient;
  461. _unicastClient.BeginReceive(UnicastReceiveCallback, udpListener);
  462. }
  463. catch (Exception ex)
  464. {
  465. _logger.ErrorException("Error in UnicastSetBeginReceive", ex);
  466. }
  467. }
  468. /// <summary>
  469. /// The UnicastReceiveCallback receives Http Responses
  470. /// and Fired the SatIpDeviceFound Event for adding the SatIpDevice
  471. /// </summary>
  472. /// <param name="ar"></param>
  473. private void UnicastReceiveCallback(IAsyncResult ar)
  474. {
  475. var udpClient = ((UdpState)(ar.AsyncState)).UdpClient;
  476. var endpoint = ((UdpState)(ar.AsyncState)).EndPoint;
  477. if (udpClient.Client != null)
  478. {
  479. try
  480. {
  481. var responseBytes = udpClient.EndReceive(ar, ref endpoint);
  482. var args = SsdpHelper.ParseSsdpResponse(responseBytes);
  483. args.EndPoint = endpoint;
  484. OnMessageReceived(args, false);
  485. UnicastSetBeginReceive();
  486. }
  487. catch (ObjectDisposedException)
  488. {
  489. }
  490. catch (SocketException)
  491. {
  492. }
  493. }
  494. }
  495. private void SendUnicastRequest(string request, int sendCount = 3)
  496. {
  497. if (_unicastClient == null)
  498. {
  499. return;
  500. }
  501. _logger.Debug("Sending unicast search request");
  502. var ipSsdp = IPAddress.Parse(SSDPAddr);
  503. var ipTxEnd = new IPEndPoint(ipSsdp, SSDPPort);
  504. SendUnicastRequest(request, ipTxEnd, sendCount);
  505. }
  506. private async void SendUnicastRequest(string request, IPEndPoint toEndPoint, int sendCount = 3)
  507. {
  508. if (_unicastClient == null)
  509. {
  510. return;
  511. }
  512. _logger.Debug("Sending unicast search request");
  513. byte[] req = Encoding.ASCII.GetBytes(request);
  514. try
  515. {
  516. for (var i = 0; i < sendCount; i++)
  517. {
  518. if (i > 0)
  519. {
  520. await Task.Delay(50).ConfigureAwait(false);
  521. }
  522. _unicastClient.Send(req, req.Length, toEndPoint);
  523. }
  524. }
  525. catch (Exception ex)
  526. {
  527. _logger.ErrorException("Error in SendUnicastRequest", ex);
  528. }
  529. }
  530. private readonly object _notificationTimerSyncLock = new object();
  531. private int _aliveNotifierIntervalMs;
  532. private void ReloadAliveNotifier()
  533. {
  534. var config = _config.GetDlnaConfiguration();
  535. if (!config.BlastAliveMessages)
  536. {
  537. StopAliveNotifier();
  538. return;
  539. }
  540. var intervalMs = config.BlastAliveMessageIntervalSeconds * 1000;
  541. if (_notificationTimer == null || _aliveNotifierIntervalMs != intervalMs)
  542. {
  543. lock (_notificationTimerSyncLock)
  544. {
  545. if (_notificationTimer == null)
  546. {
  547. _logger.Debug("Starting alive notifier");
  548. const int initialDelayMs = 3000;
  549. _notificationTimer = new Timer(state => NotifyAll(), null, initialDelayMs, intervalMs);
  550. }
  551. else
  552. {
  553. _logger.Debug("Updating alive notifier");
  554. _notificationTimer.Change(intervalMs, intervalMs);
  555. }
  556. _aliveNotifierIntervalMs = intervalMs;
  557. }
  558. }
  559. }
  560. private void StopAliveNotifier()
  561. {
  562. lock (_notificationTimerSyncLock)
  563. {
  564. if (_notificationTimer != null)
  565. {
  566. _logger.Debug("Stopping alive notifier");
  567. _notificationTimer.Dispose();
  568. _notificationTimer = null;
  569. }
  570. }
  571. }
  572. public class UdpState
  573. {
  574. public UdpClient UdpClient;
  575. public IPEndPoint EndPoint;
  576. }
  577. }
  578. }