diff --git a/Build/CoAPnet.nuspec b/Build/CoAPnet.nuspec index 6810b79..011c36e 100644 --- a/Build/CoAPnet.nuspec +++ b/Build/CoAPnet.nuspec @@ -12,19 +12,16 @@ true CoAPnet is a high performance .NET library for CoAP based communication. It provides a CoAP client and a CoAP server. It also has DTLS support out of the box. -[DTLS] Improved error handling and exposed received DTLS alerts. -[DTLS] Improved support for cancellation token. +[Client] Fixed high CPU load issues when closing the client. Copyright Christian Kratky 2019-2020 CoAP HTTP DTLS Message Telemetry Transport CoAPClient CoAPServer NETStandard IoT InternetOfThings Messaging Hardware Arduino Sensor Actuator M2M ESP Smart Home Cities Automation Xamarin - - diff --git a/Build/nuget.exe b/Build/nuget.exe new file mode 100644 index 0000000..0f83b17 Binary files /dev/null and b/Build/nuget.exe differ diff --git a/Source/CoAPnet.Extensions.DTLS/CoapClientConnectOptionsBuilderExtensions.cs b/Source/CoAPnet.Extensions.DTLS/CoapClientConnectOptionsBuilderExtensions.cs index 69afee9..05df00c 100644 --- a/Source/CoAPnet.Extensions.DTLS/CoapClientConnectOptionsBuilderExtensions.cs +++ b/Source/CoAPnet.Extensions.DTLS/CoapClientConnectOptionsBuilderExtensions.cs @@ -17,7 +17,7 @@ public static CoapClientConnectOptionsBuilder WithDtlsTransportLayer(this CoapCl throw new ArgumentNullException(nameof(options)); } - clientConnectOptionsBuilder.WithTransportLayer(new DtlsCoapTransportLayer() + clientConnectOptionsBuilder.WithTransportLayer(() => new DtlsCoapTransportLayer { Credentials = options.Credentials }); diff --git a/Source/CoAPnet.Extensions.DTLS/DtlsClient.cs b/Source/CoAPnet.Extensions.DTLS/DtlsClient.cs index 4da797a..9330ad6 100644 --- a/Source/CoAPnet.Extensions.DTLS/DtlsClient.cs +++ b/Source/CoAPnet.Extensions.DTLS/DtlsClient.cs @@ -3,7 +3,7 @@ namespace CoAPnet.Extensions.DTLS { - public class DtlsClient : DefaultTlsClient + public sealed class DtlsClient : DefaultTlsClient { readonly ProtocolVersion _protocolVersion; readonly PreSharedKey _preSharedKey; diff --git a/Source/CoAPnet.Extensions.DTLS/DtlsCoapTransportLayer.cs b/Source/CoAPnet.Extensions.DTLS/DtlsCoapTransportLayer.cs index f6954ea..0ed9393 100644 --- a/Source/CoAPnet.Extensions.DTLS/DtlsCoapTransportLayer.cs +++ b/Source/CoAPnet.Extensions.DTLS/DtlsCoapTransportLayer.cs @@ -2,9 +2,9 @@ using Org.BouncyCastle.Crypto.Tls; using Org.BouncyCastle.Security; using System; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using CoAPnet.Internal; namespace CoAPnet.Extensions.DTLS { @@ -72,12 +72,28 @@ public async Task ConnectAsync(CoapTransportLayerConnectOptions connectOptions, public Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) { + if (buffer.Array == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + cancellationToken.ThrowIfCancellationRequested(); using (cancellationToken.Register(() => _udpTransport.Close())) { - var received = _dtlsTransport.Receive(buffer.Array, buffer.Offset, buffer.Count, 0); - return Task.FromResult(received); + try + { + var received = _dtlsTransport.Receive(buffer.Array, buffer.Offset, buffer.Count, 0); + return Task.FromResult(received); + } + catch (ObjectDisposedException) + { + return Task.FromResult(0); + } + catch (SocketException) + { + return Task.FromResult(0); + } } } diff --git a/Source/CoAPnet.Extensions.DTLS/PreSharedKeyWrapper.cs b/Source/CoAPnet.Extensions.DTLS/PreSharedKeyWrapper.cs index 4fddd1b..7cb2937 100644 --- a/Source/CoAPnet.Extensions.DTLS/PreSharedKeyWrapper.cs +++ b/Source/CoAPnet.Extensions.DTLS/PreSharedKeyWrapper.cs @@ -3,7 +3,7 @@ namespace CoAPnet.Extensions.DTLS { - public class PreSharedKeyWrapper : TlsPskIdentity + public sealed class PreSharedKeyWrapper : TlsPskIdentity { readonly PreSharedKey _preSharedKey; diff --git a/Source/CoAPnet.Extensions.DTLS/UdpTransport.cs b/Source/CoAPnet.Extensions.DTLS/UdpTransport.cs index e990854..7228b9d 100644 --- a/Source/CoAPnet.Extensions.DTLS/UdpTransport.cs +++ b/Source/CoAPnet.Extensions.DTLS/UdpTransport.cs @@ -39,11 +39,7 @@ public int Receive(byte[] buf, int off, int len, int waitMillis) } EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); - var length = _socket.ReceiveFrom(buf, off, len, SocketFlags.None, ref remoteEndPoint); - - Console.WriteLine(length); - - return length; + return _socket.ReceiveFrom(buf, off, len, SocketFlags.None, ref remoteEndPoint); } public void Send(byte[] buf, int off, int len) @@ -61,6 +57,11 @@ public void Send(byte[] buf, int off, int len) _socket.SendTo(buf, off, len, SocketFlags.None, _connectOptions.EndPoint); } + public void Close() + { + Dispose(); + } + public void Dispose() { _isDisposed = true; @@ -68,10 +69,5 @@ public void Dispose() // There is no need to call "Disconnect" because we use UDP. _socket?.Dispose(); } - - public void Close() - { - Dispose(); - } } } diff --git a/Source/CoAPnet.Tests/CoAPnet.Tests.csproj b/Source/CoAPnet.Tests/CoAPnet.Tests.csproj index d6a1b6b..5a0c07d 100644 --- a/Source/CoAPnet.Tests/CoAPnet.Tests.csproj +++ b/Source/CoAPnet.Tests/CoAPnet.Tests.csproj @@ -7,16 +7,17 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Source/CoAPnet.Tests/CoapClient_Tests.cs b/Source/CoAPnet.Tests/CoapClient_Tests.cs new file mode 100644 index 0000000..4adfd44 --- /dev/null +++ b/Source/CoAPnet.Tests/CoapClient_Tests.cs @@ -0,0 +1,66 @@ +using System.Threading; +using System.Threading.Tasks; +using CoAPnet.Client; +using CoAPnet.Exceptions; +using CoAPnet.Extensions.DTLS; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CoAPnet.Tests +{ + [TestClass] + public class CoapClient_Tests + { + [TestMethod] + [ExpectedException(typeof(CoapCommunicationException))] + public async Task Connect_Invalid_Host() + { + using (var coapClient = new CoapFactory().CreateClient()) + { + await coapClient.ConnectAsync(new CoapClientConnectOptions() + { + Host = "invalid_host" + }, CancellationToken.None); + } + } + + [TestMethod] + [ExpectedException(typeof(CoapCommunicationException))] + public async Task Connect_Valid_Host_With_Invalid_Port_Dtls() + { + using (var coapClient = new CoapFactory().CreateClient()) + { + var options = new CoapClientConnectOptionsBuilder() + .WithHost("127.0.0.1") + .WithPort(5555) + .WithDtlsTransportLayer(o => + { + o.WithPreSharedKey("a", "b"); + }) + .Build(); + + await coapClient.ConnectAsync(options, CancellationToken.None); + } + } + + [TestMethod] + [ExpectedException(typeof(CoapCommunicationTimedOutException))] + public async Task Timeout_When_No_Response_Received() + { + using (var coapClient = new CoapFactory().CreateClient()) + { + var options = new CoapClientConnectOptionsBuilder() + .WithHost("127.0.0.1") + .WithPort(5555) + .Build(); + + await coapClient.ConnectAsync(options, CancellationToken.None); + + var request = new CoapRequestBuilder() + .WithMethod(CoapRequestMethod.Get) + .Build(); + + await coapClient.RequestAsync(request, CancellationToken.None); + } + } + } +} diff --git a/Source/CoAPnet/Client/CoapClient.cs b/Source/CoAPnet/Client/CoapClient.cs index ca5108e..bf6423f 100644 --- a/Source/CoAPnet/Client/CoapClient.cs +++ b/Source/CoAPnet/Client/CoapClient.cs @@ -150,6 +150,9 @@ internal async Task RequestAsync(CoapMessage requestMessage, Cancel public void Dispose() { + _cancellationToken?.Cancel(false); + _cancellationToken?.Dispose(); + _lowLevelClient?.Dispose(); } @@ -160,13 +163,17 @@ async Task ReceiveMessages(CancellationToken cancellationToken) try { var message = await _lowLevelClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + if (message == null) { continue; } - cancellationToken.ThrowIfCancellationRequested(); - if (!_messageDispatcher.TryHandleReceivedMessage(message)) { if (!await _observationManager.TryHandleReceivedMessage(message).ConfigureAwait(false)) @@ -180,7 +187,7 @@ async Task ReceiveMessages(CancellationToken cancellationToken) } catch (Exception exception) { - _logger.Error(nameof(CoapClient), exception, "Error while receiving message."); + _logger.Error(nameof(CoapClient), exception, "Error while receiving messages."); } } } diff --git a/Source/CoAPnet/Client/CoapClientConnectOptions.cs b/Source/CoAPnet/Client/CoapClientConnectOptions.cs index 8103b12..a0cdaa8 100644 --- a/Source/CoAPnet/Client/CoapClientConnectOptions.cs +++ b/Source/CoAPnet/Client/CoapClientConnectOptions.cs @@ -15,9 +15,6 @@ public string Host public TimeSpan CommunicationTimeout { get; set; } = TimeSpan.FromSeconds(10); - public ICoapTransportLayer TransportLayer - { - get; set; - } + public Func TransportLayerFactory { get; set; } = () => new UdpCoapTransportLayer(); } } diff --git a/Source/CoAPnet/Client/CoapClientConnectOptionsBuilder.cs b/Source/CoAPnet/Client/CoapClientConnectOptionsBuilder.cs index 07c29d6..80501dd 100644 --- a/Source/CoAPnet/Client/CoapClientConnectOptionsBuilder.cs +++ b/Source/CoAPnet/Client/CoapClientConnectOptionsBuilder.cs @@ -8,19 +8,9 @@ public class CoapClientConnectOptionsBuilder { readonly CoapClientConnectOptions _options = new CoapClientConnectOptions { - TransportLayer = new UdpCoapTransportLayer() // This is the protocols default transport. + TransportLayerFactory = () => new UdpCoapTransportLayer() // This is the protocols default transport. }; - public CoapClientConnectOptions Build() - { - if (_options.TransportLayer == null) - { - throw new CoapClientConfigurationInvalidException("Transport layer is not set.", null); - } - - return _options; - } - public CoapClientConnectOptionsBuilder WithHost(string value) { _options.Host = value ?? throw new ArgumentNullException(nameof(value)); @@ -45,20 +35,30 @@ public CoapClientConnectOptionsBuilder WithUnencryptedPort() public CoapClientConnectOptionsBuilder WithTcpTransportLayer() { - _options.TransportLayer = new TcpCoapTransportLayer(); + _options.TransportLayerFactory = () => new TcpCoapTransportLayer(); return this; } - public CoapClientConnectOptionsBuilder WithTransportLayer(ICoapTransportLayer value) + public CoapClientConnectOptionsBuilder WithTransportLayer(Func transportLayerFactory) { - _options.TransportLayer = value ?? throw new ArgumentNullException(nameof(value)); + _options.TransportLayerFactory = transportLayerFactory ?? throw new ArgumentNullException(nameof(transportLayerFactory)); return this; } public CoapClientConnectOptionsBuilder WithUdpTransportLayer() { - _options.TransportLayer = new UdpCoapTransportLayer(); + _options.TransportLayerFactory = () => new UdpCoapTransportLayer(); return this; } + + public CoapClientConnectOptions Build() + { + if (_options.TransportLayerFactory == null) + { + throw new CoapClientConfigurationInvalidException("Transport layer is not set.", null); + } + + return _options; + } } } \ No newline at end of file diff --git a/Source/CoAPnet/LowLevelClient/LowLevelCoapClient.cs b/Source/CoAPnet/LowLevelClient/LowLevelCoapClient.cs index d65894c..539bb22 100644 --- a/Source/CoAPnet/LowLevelClient/LowLevelCoapClient.cs +++ b/Source/CoAPnet/LowLevelClient/LowLevelCoapClient.cs @@ -35,9 +35,16 @@ public async Task ConnectAsync(CoapClientConnectOptions options, CancellationTok { _connectOptions = options ?? throw new ArgumentNullException(nameof(options)); + var transportLayer = options.TransportLayerFactory?.Invoke(); + + if (transportLayer == null) + { + throw new InvalidOperationException("No CoAP transport layer is set."); + } + cancellationToken.ThrowIfCancellationRequested(); - _transportLayerAdapter = new CoapTransportLayerAdapter(options.TransportLayer, _logger); + _transportLayerAdapter = new CoapTransportLayerAdapter(transportLayer, _logger); try { var transportLayerConnectOptions = new CoapTransportLayerConnectOptions @@ -93,14 +100,21 @@ async Task ResolveIPEndPoint(CoapClientConnectOptions connectOptions await Task.FromResult(0); throw new NotSupportedException("Resolving DNS end points is not supported for NETSTANDARD1_3. Please pass IP address instead."); #else - var hostIPAddresses = await Dns.GetHostAddressesAsync(connectOptions.Host).ConfigureAwait(false); - if (hostIPAddresses.Length == 0) + try { - throw new CoapCommunicationException("Failed to resolve DNS end point", null); - } + var hostIPAddresses = await Dns.GetHostAddressesAsync(connectOptions.Host).ConfigureAwait(false); + if (hostIPAddresses.Length == 0) + { + throw new CoapCommunicationException("Failed to resolve DNS end point", null); + } - // We only use the first address for now. - return new IPEndPoint(hostIPAddresses[0], _connectOptions.Port); + // We only use the first address for now. + return new IPEndPoint(hostIPAddresses[0], _connectOptions.Port); + } + catch (Exception exception) + { + throw new CoapCommunicationException("Error while resolving DNS name.", exception); + } #endif } }