From 34b5f6eb920cda3a26b62df44f9fd23519b4c9c3 Mon Sep 17 00:00:00 2001 From: Christoph Seiler Date: Tue, 20 May 2025 10:28:20 +0200 Subject: [PATCH] feat: Introduce TcpClientFactory Allows injecting custom TcpClient for monitoring and logging --- S7.Net.UnitTest/ConnectionCloseTest.cs | 7 +++---- S7.Net/PLC.cs | 24 +++++++++++++++++------- S7.Net/PlcAsynchronous.cs | 2 +- S7.Net/Tcp/ITcpClient.cs | 21 +++++++++++++++++++++ S7.Net/Tcp/ITcpClientFactory.cs | 6 ++++++ S7.Net/Tcp/TcpClientWrapper.cs | 24 ++++++++++++++++++++++++ S7.Net/Tcp/TcpClientWrapperFactory.cs | 9 +++++++++ 7 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 S7.Net/Tcp/ITcpClient.cs create mode 100644 S7.Net/Tcp/ITcpClientFactory.cs create mode 100644 S7.Net/Tcp/TcpClientWrapper.cs create mode 100644 S7.Net/Tcp/TcpClientWrapperFactory.cs diff --git a/S7.Net.UnitTest/ConnectionCloseTest.cs b/S7.Net.UnitTest/ConnectionCloseTest.cs index 87c9ea99..330b7951 100644 --- a/S7.Net.UnitTest/ConnectionCloseTest.cs +++ b/S7.Net.UnitTest/ConnectionCloseTest.cs @@ -1,11 +1,10 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; -using System.Linq; -using System.Net.Sockets; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using S7.Net.Tcp; namespace S7.Net.UnitTest { @@ -92,7 +91,7 @@ public async Task Test_CancellationDuringTransmission() } // Set a value to tcpClient field so we can later ensure that it has been closed. - tcpClientField.SetValue(plc, new TcpClient()); + tcpClientField.SetValue(plc, new TcpClientWrapper()); var tcpClientValue = tcpClientField.GetValue(plc); Assert.IsNotNull(tcpClientValue); @@ -147,7 +146,7 @@ public async Task Test_CancellationBeforeTransmission() } // Set a value to tcpClient field so we can later ensure that it has been closed. - tcpClientField.SetValue(plc, new TcpClient()); + tcpClientField.SetValue(plc, new TcpClientWrapper()); var tcpClientValue = tcpClientField.GetValue(plc); Assert.IsNotNull(tcpClientValue); diff --git a/S7.Net/PLC.cs b/S7.Net/PLC.cs index 35d038e0..b8cd5dd0 100644 --- a/S7.Net/PLC.cs +++ b/S7.Net/PLC.cs @@ -5,6 +5,7 @@ using System.Net.Sockets; using S7.Net.Internal; using S7.Net.Protocol; +using S7.Net.Tcp; using S7.Net.Types; @@ -27,8 +28,11 @@ public partial class Plc : IDisposable private readonly TaskQueue queue = new TaskQueue(); + private readonly ITcpClientFactory _tcpClientFactory; + //TCP connection to device - private TcpClient? tcpClient; + private ITcpClient? tcpClient; + private NetworkStream? _stream; private int readTimeout = DefaultTimeout; // default no timeout @@ -124,8 +128,9 @@ public int WriteTimeout /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. /// If you use an external ethernet card, this must be set accordingly. - public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot) - : this(cpu, ip, DefaultPort, rack, slot) + /// Factory to provide the underlying for network communication. + public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot, ITcpClientFactory? tcpClientFactory = null) + : this(cpu, ip, DefaultPort, rack, slot, tcpClientFactory) { } @@ -141,8 +146,9 @@ public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot) /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. /// If you use an external ethernet card, this must be set accordingly. - public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot) - : this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot)) + /// Factory to provide the underlying for network communication. + public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot, ITcpClientFactory? tcpClientFactory = null) + : this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot), tcpClientFactory) { if (!Enum.IsDefined(typeof(CpuType), cpu)) throw new ArgumentException( @@ -162,7 +168,9 @@ public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot) /// /// Ip address of the PLC /// The TSAP addresses used for the connection request. - public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair) + /// Factory to provide the underlying for network communication. + public Plc(string ip, TsapPair tsapPair, ITcpClientFactory? tcpClientFactory = null) + : this(ip, DefaultPort, tsapPair, tcpClientFactory) { } @@ -173,7 +181,8 @@ public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair) /// Ip address of the PLC /// Port number used for the connection, default 102. /// The TSAP addresses used for the connection request. - public Plc(string ip, int port, TsapPair tsapPair) + /// Factory to provide the underlying for network communication. + public Plc(string ip, int port, TsapPair tsapPair, ITcpClientFactory? tcpClientFactory = null) { if (string.IsNullOrEmpty(ip)) throw new ArgumentException("IP address must valid.", nameof(ip)); @@ -182,6 +191,7 @@ public Plc(string ip, int port, TsapPair tsapPair) Port = port; MaxPDUSize = 240; TsapPair = tsapPair; + _tcpClientFactory = tcpClientFactory ?? new TcpClientWrapperFactory(); } /// diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 8b828f3c..ddb6d612 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -46,7 +46,7 @@ await queue.Enqueue(async () => private async Task ConnectAsync(CancellationToken cancellationToken) { - tcpClient = new TcpClient(); + tcpClient = _tcpClientFactory.Create(); ConfigureConnection(); #if NET5_0_OR_GREATER diff --git a/S7.Net/Tcp/ITcpClient.cs b/S7.Net/Tcp/ITcpClient.cs new file mode 100644 index 00000000..600448e5 --- /dev/null +++ b/S7.Net/Tcp/ITcpClient.cs @@ -0,0 +1,21 @@ +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace S7.Net.Tcp +{ + public interface ITcpClient + { + public int ReceiveTimeout { get; set; } + + public int SendTimeout { get; set; } + + public bool Connected { get; } + + public void Close(); + + public Task ConnectAsync(string ip, int port, CancellationToken cancellationToken = default); + + public NetworkStream GetStream(); + } +} diff --git a/S7.Net/Tcp/ITcpClientFactory.cs b/S7.Net/Tcp/ITcpClientFactory.cs new file mode 100644 index 00000000..260c2cd0 --- /dev/null +++ b/S7.Net/Tcp/ITcpClientFactory.cs @@ -0,0 +1,6 @@ +namespace S7.Net.Tcp; + +public interface ITcpClientFactory +{ + ITcpClient Create(); +} \ No newline at end of file diff --git a/S7.Net/Tcp/TcpClientWrapper.cs b/S7.Net/Tcp/TcpClientWrapper.cs new file mode 100644 index 00000000..9d5419e7 --- /dev/null +++ b/S7.Net/Tcp/TcpClientWrapper.cs @@ -0,0 +1,24 @@ +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace S7.Net.Tcp; + +internal class TcpClientWrapper : TcpClient, ITcpClient +{ + public async Task ConnectAsync(string ip, int port, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + await base.ConnectAsync(ip, port, cancellationToken).ConfigureAwait(false); +#else + await base.ConnectAsync(ip, port).ConfigureAwait(false); +#endif + } + + public void Close() + { +#if NET20_OR_GREATER + base.Close(); +#endif + } +} \ No newline at end of file diff --git a/S7.Net/Tcp/TcpClientWrapperFactory.cs b/S7.Net/Tcp/TcpClientWrapperFactory.cs new file mode 100644 index 00000000..65f7ea51 --- /dev/null +++ b/S7.Net/Tcp/TcpClientWrapperFactory.cs @@ -0,0 +1,9 @@ +namespace S7.Net.Tcp; + +internal class TcpClientWrapperFactory : ITcpClientFactory +{ + public ITcpClient Create() + { + return new TcpClientWrapper(); + } +} \ No newline at end of file