diff --git a/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs b/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs index ad78b3c..a9b6d94 100644 --- a/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs +++ b/AMWD.Protocols.Modbus.Tcp/Extensions/StreamExtensions.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using AMWD.Protocols.Modbus.Tcp.Utils; namespace System.IO { @@ -22,5 +23,23 @@ namespace System.IO cancellationToken.ThrowIfCancellationRequested(); return buffer; } + + public static async Task ReadExpectedBytesAsync(this NetworkStreamWrapper stream, int expectedBytes, CancellationToken cancellationToken = default) + { + byte[] buffer = new byte[expectedBytes]; + int offset = 0; + do + { + int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken); + if (count < 1) + throw new EndOfStreamException(); + + offset += count; + } + while (offset < expectedBytes && !cancellationToken.IsCancellationRequested); + + cancellationToken.ThrowIfCancellationRequested(); + return buffer; + } } } diff --git a/AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs b/AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs new file mode 100644 index 0000000..a7a831d --- /dev/null +++ b/AMWD.Protocols.Modbus.Tcp/Utils/IPEndPointWrapper.cs @@ -0,0 +1,32 @@ +using System.Net; + +namespace AMWD.Protocols.Modbus.Tcp.Utils +{ + internal class IPEndPointWrapper + { + private IPEndPoint _ipEndPoint; + + public IPEndPointWrapper(EndPoint endPoint) + { + _ipEndPoint = (IPEndPoint)endPoint; + } + + #region Properties + + /// + public virtual IPAddress Address + { + get => _ipEndPoint.Address; + set => _ipEndPoint.Address = value; + } + + /// + public virtual int Port + { + get => _ipEndPoint.Port; + set => _ipEndPoint.Port = value; + } + + #endregion Properties + } +} diff --git a/AMWD.Protocols.Modbus.Tcp/Utils/NetworkStreamWrapper.cs b/AMWD.Protocols.Modbus.Tcp/Utils/NetworkStreamWrapper.cs index dfa5c7d..5a70685 100644 --- a/AMWD.Protocols.Modbus.Tcp/Utils/NetworkStreamWrapper.cs +++ b/AMWD.Protocols.Modbus.Tcp/Utils/NetworkStreamWrapper.cs @@ -12,10 +12,6 @@ namespace AMWD.Protocols.Modbus.Tcp.Utils { private readonly NetworkStream _stream; - [Obsolete("Constructor only for mocking on UnitTests!", error: true)] - public NetworkStreamWrapper() - { } - public NetworkStreamWrapper(NetworkStream stream) { _stream = stream; diff --git a/AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs b/AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs new file mode 100644 index 0000000..4308038 --- /dev/null +++ b/AMWD.Protocols.Modbus.Tcp/Utils/SocketWrapper.cs @@ -0,0 +1,29 @@ +using System; +using System.Net.Sockets; + +namespace AMWD.Protocols.Modbus.Tcp.Utils +{ + internal class SocketWrapper : IDisposable + { + private Socket _socket; + + public SocketWrapper(Socket socket) + { + _socket = socket; + } + + /// + public virtual bool DualMode + { + get => _socket.DualMode; + set => _socket.DualMode = value; + } + + /// + public virtual bool IsBound + => _socket.IsBound; + + public virtual void Dispose() + => _socket.Dispose(); + } +} diff --git a/AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapper.cs b/AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapper.cs index 9bfa3a0..8bae78c 100644 --- a/AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapper.cs +++ b/AMWD.Protocols.Modbus.Tcp/Utils/TcpClientWrapper.cs @@ -3,19 +3,30 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using System.Transactions; namespace AMWD.Protocols.Modbus.Tcp.Utils { /// [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - internal class TcpClientWrapper(AddressFamily addressFamily) : IDisposable + internal class TcpClientWrapper : IDisposable { #region Fields - private readonly TcpClient _client = new(addressFamily); + private readonly TcpClient _client; #endregion Fields + public TcpClientWrapper(AddressFamily addressFamily) + { + _client = new TcpClient(addressFamily); + } + + public TcpClientWrapper(TcpClient client) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + } + #region Properties /// diff --git a/AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs b/AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs new file mode 100644 index 0000000..5a6cc2f --- /dev/null +++ b/AMWD.Protocols.Modbus.Tcp/Utils/TcpListenerWrapper.cs @@ -0,0 +1,91 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace AMWD.Protocols.Modbus.Tcp.Utils +{ + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal class TcpListenerWrapper : IDisposable + { + #region Fields + + private TcpListener _tcpListener; + + #endregion Fields + + #region Constructor + + public TcpListenerWrapper(IPAddress localaddr, int port) + { + _tcpListener = new TcpListener(localaddr, port); + } + + #endregion Constructor + + #region Properties + + /// + public virtual IPEndPointWrapper LocalIPEndPoint + => new(_tcpListener.LocalEndpoint); + + public virtual SocketWrapper Socket + => new(_tcpListener.Server); + + #endregion Properties + + #region Methods + + /// + /// Accepts a pending connection request as a cancellable asynchronous operation. + /// + /// + /// + /// This operation will not block. The returned object will complete after the TCP connection has been accepted. + /// + /// + /// Use the method to obtain the underlying of the returned in the . + /// The will provide you with methods for sending and receiving with the remote host. + /// When you are through with the , be sure to call its method. + /// + /// + /// A cancellation token that can be used to cancel the asynchronous operation + /// + /// The task object representing the asynchronous operation. + /// The property on the task object returns a used to send and receive data. + /// + /// The listener has not been started with a call to . + /// + /// Use the property to obtain the specific error code. + /// When you have obtained this code, you can refer to the + /// Windows Sockets version 2 API error code + /// documentation for a detailed description of the error. + /// + /// The cancellation token was canceled. This exception is stored into the returned task. + public virtual async Task AcceptTcpClientAsync(CancellationToken cancellationToken = default) + { +#if NET8_0_OR_GREATER + var tcpClient = await _tcpListener.AcceptTcpClientAsync(cancellationToken); +#else + var tcpClient = await _tcpListener.AcceptTcpClientAsync(); +#endif + return new TcpClientWrapper(tcpClient); + } + + public virtual void Start() + => _tcpListener.Start(); + + public virtual void Stop() + => _tcpListener.Stop(); + + public virtual void Dispose() + { +#if NET8_0_OR_GREATER + _tcpListener.Dispose(); +#endif + } + + #endregion Methods + } +} diff --git a/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpConnectionTest.cs b/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpConnectionTest.cs index 8b987b2..5dcf5f1 100644 --- a/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpConnectionTest.cs +++ b/AMWD.Protocols.Modbus.Tests/Tcp/ModbusTcpConnectionTest.cs @@ -498,7 +498,7 @@ namespace AMWD.Protocols.Modbus.Tests.Tcp private ModbusTcpConnection GetTcpConnection() { - _networkStreamMock = new Mock(); + _networkStreamMock = new Mock(null); _networkStreamMock .Setup(ns => ns.WriteAsync(It.IsAny>(), It.IsAny())) .Callback, CancellationToken>((req, _) => _networkRequestCallbacks.Add(req.ToArray()))