diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index aa15a43..4509e8b 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -63,6 +63,9 @@ jobs: /d:sonar.cpd.cs.minimumTokens=40 ` /d:sonar.cpd.cs.minimumLines=5 ` /d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml ` + /d:sonar.tests=NetSdrClientAppTests,EchoServerTests ` + /d:sonar.test.inclusions=**/*Tests.cs,**/*Test.cs ` + /d:sonar.coverage.exclusions=**/*Tests/**,**/*Test/** ` /d:sonar.qualitygate.wait=true shell: pwsh # 2) BUILD & TEST diff --git a/EchoServerTests/ClientWrapperFactoryTests.cs b/EchoServerTests/ClientWrapperFactoryTests.cs new file mode 100644 index 0000000..78d40fc --- /dev/null +++ b/EchoServerTests/ClientWrapperFactoryTests.cs @@ -0,0 +1,33 @@ +using EchoServer; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServerTests +{ + [TestFixture] + public class ClientWrapperFactoryTests + { + [Test] + public void CreateClientWrapper_ShouldReturnWrapper() + { + // Arrange + var factory = new ClientWrapperFactory(); + var tcpClient = new TcpClient(); + + // Act + var wrapper = factory.CreateClientWrapper(tcpClient); + + // Assert + Assert.IsNotNull(wrapper); + Assert.IsInstanceOf(wrapper); + + // Cleanup + wrapper.Dispose(); + } + } +} diff --git a/EchoServerTests/ConsoleLoggerTests.cs b/EchoServerTests/ConsoleLoggerTests.cs new file mode 100644 index 0000000..00166b9 --- /dev/null +++ b/EchoServerTests/ConsoleLoggerTests.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EchoServer; + +namespace EchoServerTests +{ + [TestFixture] + public class ConsoleLoggerTests + { + [Test] + public void Log_ShouldNotThrow() + { + // Arrange + var logger = new ConsoleLogger(); + + // Act & Assert + Assert.DoesNotThrow(() => logger.Log("Test message")); + } + } +} diff --git a/EchoServerTests/EchoServerTests.cs b/EchoServerTests/EchoServerTests.cs new file mode 100644 index 0000000..6a2306a --- /dev/null +++ b/EchoServerTests/EchoServerTests.cs @@ -0,0 +1,248 @@ +using NUnit.Framework; +using Moq; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using EchoServer; +using EchoServer.Interfaces; + +namespace EchoServerTests +{ + [TestFixture] + public class EchoServerTests + { + private Mock _mockLogger; + private Mock _mockClientFactory; + private Mock _mockListener; + private CancellationTokenSource _cts; + + [SetUp] + public void SetUp() + { + _mockLogger = new Mock(); + _mockClientFactory = new Mock(); + _mockListener = new Mock(); + _cts = new CancellationTokenSource(); + } + + [TearDown] + public void TearDown() + { + _cts?.Dispose(); + } + + [Test] + public void Constructor_ShouldInitializeWithDefaultDependencies() + { + // Arrange & Act + var server = new EchoServer.EchoServer(5000); + + // Assert + Assert.IsFalse(server.IsRunning); + } + + [Test] + public void Constructor_ShouldAcceptCustomLogger() + { + // Arrange & Act + var server = new EchoServer.EchoServer(5000, _mockLogger.Object); + + // Assert + Assert.IsNotNull(server); + Assert.IsFalse(server.IsRunning); + } + + [Test] + public async Task StartAsync_ShouldStartServerAndHandleClients() + { + // Arrange + var mockStream = new Mock(); + var mockClient = new Mock(); + var mockTcpClient = new Mock(); + var cts = new CancellationTokenSource(); + + byte[] testData = new byte[] { 1, 2, 3, 4, 5 }; + + mockStream.SetupSequence(s => s.ReadAsync(It.IsAny(), 0, It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(testData.Length)) + .Returns(Task.FromResult(0)); + + mockStream.Setup(s => s.WriteAsync(It.IsAny(), 0, It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + mockClient.Setup(c => c.GetStream()).Returns(mockStream.Object); + mockClient.Setup(c => c.Dispose()); + + _mockClientFactory.Setup(f => f.CreateClientWrapper(It.IsAny())) + .Returns(mockClient.Object); + + _mockListener.Setup(l => l.Start()); + _mockListener.Setup(l => l.Stop()); + _mockListener.SetupSequence(l => l.AcceptTcpClientAsync()) + .ReturnsAsync(mockTcpClient.Object) + .Returns(Task.FromException(new ObjectDisposedException("Listener"))); + + Func listenerFactory = (addr, port) => _mockListener.Object; + var server = new EchoServer.EchoServer(5000, _mockLogger.Object, _mockClientFactory.Object, listenerFactory); + + // Act + var serverTask = Task.Run(() => server.StartAsync()); + await Task.Delay(100); // Give server time to start + server.Stop(); + await serverTask; + + // Assert + _mockLogger.Verify(l => l.Log(It.Is(msg => msg.Contains("Server started"))), Times.Once); + _mockLogger.Verify(l => l.Log("Client connected."), Times.AtLeastOnce); + } + + [Test] + public async Task StartAsync_ShouldHandleMultipleClients() + { + // Arrange + var mockStream = new Mock(); + var mockClient1 = new Mock(); + var mockClient2 = new Mock(); + var mockTcpClient1 = new Mock(); + var mockTcpClient2 = new Mock(); + + mockStream.SetupSequence(s => s.ReadAsync(It.IsAny(), 0, It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(10)) + .Returns(Task.FromResult(0)); + + mockStream.Setup(s => s.WriteAsync(It.IsAny(), 0, It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + mockClient1.Setup(c => c.GetStream()).Returns(mockStream.Object); + mockClient2.Setup(c => c.GetStream()).Returns(mockStream.Object); + + _mockClientFactory.SetupSequence(f => f.CreateClientWrapper(It.IsAny())) + .Returns(mockClient1.Object) + .Returns(mockClient2.Object); + + _mockListener.Setup(l => l.Start()); + _mockListener.Setup(l => l.Stop()); + _mockListener.SetupSequence(l => l.AcceptTcpClientAsync()) + .ReturnsAsync(mockTcpClient1.Object) + .ReturnsAsync(mockTcpClient2.Object) + .Returns(Task.FromException(new ObjectDisposedException("Listener"))); + + Func listenerFactory = (addr, port) => _mockListener.Object; + var server = new EchoServer.EchoServer(5000, _mockLogger.Object, _mockClientFactory.Object, listenerFactory); + + // Act + var serverTask = Task.Run(() => server.StartAsync()); + await Task.Delay(150); + server.Stop(); + await serverTask; + + // Assert + _mockLogger.Verify(l => l.Log("Client connected."), Times.AtLeast(2)); + } + + [Test] + public async Task StartAsync_ShouldStopOnCancellation() + { + // Arrange + var cts = new CancellationTokenSource(); + + _mockListener.Setup(l => l.Start()); + _mockListener.Setup(l => l.Stop()); + _mockListener.Setup(l => l.AcceptTcpClientAsync()) + .Returns(async () => + { + await Task.Delay(5000); + return new TcpClient(); + }); + + Func listenerFactory = (addr, port) => _mockListener.Object; + var server = new EchoServer.EchoServer(5000, _mockLogger.Object, _mockClientFactory.Object, listenerFactory); + + // Act + var serverTask = Task.Run(() => server.StartAsync()); + await Task.Delay(50); + server.Stop(); + await serverTask; + + // Assert + _mockLogger.Verify(l => l.Log(It.Is(msg => msg.Contains("Server started"))), Times.Once); + _mockLogger.Verify(l => l.Log("Server shutdown."), Times.Once); + } + + [Test] + public async Task StartAsync_ShouldHandleClientExceptions() + { + // Arrange + var mockTcpClient = new Mock(); + + _mockListener.Setup(l => l.Start()); + _mockListener.Setup(l => l.Stop()); + _mockListener.SetupSequence(l => l.AcceptTcpClientAsync()) + .ReturnsAsync(mockTcpClient.Object) + .Returns(Task.FromException(new ObjectDisposedException("Listener"))); + + var mockClient = new Mock(); + var mockStream = new Mock(); + + mockStream.Setup(s => s.ReadAsync(It.IsAny(), 0, It.IsAny(), It.IsAny())) + .ThrowsAsync(new IOException("Network error")); + + mockClient.Setup(c => c.GetStream()).Returns(mockStream.Object); + _mockClientFactory.Setup(f => f.CreateClientWrapper(It.IsAny())) + .Returns(mockClient.Object); + + Func listenerFactory = (addr, port) => _mockListener.Object; + var server = new EchoServer.EchoServer(5000, _mockLogger.Object, _mockClientFactory.Object, listenerFactory); + + // Act + var serverTask = Task.Run(() => server.StartAsync()); + await Task.Delay(150); + server.Stop(); + await serverTask; + + // Assert + _mockLogger.Verify(l => l.Log(It.Is(msg => msg.Contains("Error:"))), Times.AtLeastOnce); + } + + [Test] + public void Stop_ShouldStopServer() + { + // Arrange + Func listenerFactory = (addr, port) => _mockListener.Object; + var server = new EchoServer.EchoServer(5000, _mockLogger.Object, _mockClientFactory.Object, listenerFactory); + + // Act + server.Stop(); + + // Assert + _mockLogger.Verify(l => l.Log("Server stopped."), Times.Once); + Assert.IsFalse(server.IsRunning); + } + + [Test] + public void Stop_ShouldNotThrowWhenCalledMultipleTimes() + { + // Arrange + var server = new EchoServer.EchoServer(5000, _mockLogger.Object); + + // Act & Assert + Assert.DoesNotThrow(() => server.Stop()); + Assert.DoesNotThrow(() => server.Stop()); + } + + [Test] + public void Dispose_ShouldStopServer() + { + // Arrange + var server = new EchoServer.EchoServer(5000, _mockLogger.Object); + + // Act + server.Dispose(); + + // Assert + Assert.IsFalse(server.IsRunning); + } + } +} \ No newline at end of file diff --git a/EchoServerTests/EchoServerTests.csproj b/EchoServerTests/EchoServerTests.csproj new file mode 100644 index 0000000..427fe94 --- /dev/null +++ b/EchoServerTests/EchoServerTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + diff --git a/EchoServerTests/TcpClientWrapperTests.cs b/EchoServerTests/TcpClientWrapperTests.cs new file mode 100644 index 0000000..728fa21 --- /dev/null +++ b/EchoServerTests/TcpClientWrapperTests.cs @@ -0,0 +1,39 @@ +using EchoServer; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServerTests +{ + [TestFixture] + public class TcpClientWrapperTests + { + [Test] + public void GetStream_ShouldReturnNetworkStreamWrapper() + { + // Arrange + var tcpClient = new TcpClient(); + var wrapper = new TcpClientWrapper(tcpClient); + + // Act & Assert + // Cannot test without actual connection, but verify no exception + Assert.DoesNotThrow(() => wrapper.Dispose()); + } + + [Test] + public void Dispose_ShouldCloseClient() + { + // Arrange + var tcpClient = new TcpClient(); + var wrapper = new TcpClientWrapper(tcpClient); + + // Act & Assert + Assert.DoesNotThrow(() => wrapper.Dispose()); + } + } +} diff --git a/EchoServerTests/TcpListenerWrapperTests.cs b/EchoServerTests/TcpListenerWrapperTests.cs new file mode 100644 index 0000000..45ae3c3 --- /dev/null +++ b/EchoServerTests/TcpListenerWrapperTests.cs @@ -0,0 +1,56 @@ +using EchoServer; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServerTests +{ + [TestFixture] + public class TcpListenerWrapperTests + { + [Test] + public void Start_ShouldStartListener() + { + // Arrange + var wrapper = new TcpListenerWrapper(IPAddress.Loopback, 0); // Port 0 = random available port + + // Act & Assert + Assert.DoesNotThrow(() => wrapper.Start()); + + // Cleanup + wrapper.Stop(); + } + + [Test] + public void Stop_ShouldStopListener() + { + // Arrange + var wrapper = new TcpListenerWrapper(IPAddress.Loopback, 0); + wrapper.Start(); + + // Act & Assert + Assert.DoesNotThrow(() => wrapper.Stop()); + } + + [Test] + public void AcceptTcpClientAsync_ShouldWaitForConnection() + { + // Arrange + var wrapper = new TcpListenerWrapper(IPAddress.Loopback, 0); + wrapper.Start(); + + // Act + var acceptTask = wrapper.AcceptTcpClientAsync(); + + // Assert + Assert.IsFalse(acceptTask.IsCompleted); // Should be waiting for connection + + // Cleanup + wrapper.Stop(); + } + } + +} diff --git a/EchoServerTests/UdpTimedSenderTests.cs b/EchoServerTests/UdpTimedSenderTests.cs new file mode 100644 index 0000000..a65b439 --- /dev/null +++ b/EchoServerTests/UdpTimedSenderTests.cs @@ -0,0 +1,83 @@ +using EchoServer; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EchoServer.Interfaces; + +namespace EchoServerTests +{ + [TestFixture] + public class UdpTimedSenderTests + { + private Mock _mockLogger; + + [SetUp] + public void SetUp() + { + _mockLogger = new Mock(); + } + + [Test] + public void Constructor_ShouldInitialize() + { + // Arrange & Act + using var sender = new UdpTimedSender("127.0.0.1", 5000, _mockLogger.Object); + + // Assert + Assert.IsNotNull(sender); + } + + [Test] + public void StartSending_ShouldThrowWhenAlreadyRunning() + { + // Arrange + using var sender = new UdpTimedSender("127.0.0.1", 5000, _mockLogger.Object); + sender.StartSending(1000); + + // Act & Assert + var ex = Assert.Throws(() => sender.StartSending(1000)); + Assert.That(ex.Message, Does.Contain("already running")); + + // Cleanup + sender.StopSending(); + } + + [Test] + public void StopSending_ShouldStopTimer() + { + // Arrange + using var sender = new UdpTimedSender("127.0.0.1", 5000, _mockLogger.Object); + sender.StartSending(1000); + + // Act + sender.StopSending(); + + // Assert - no exception should be thrown + Assert.Pass(); + } + + [Test] + public void StopSending_ShouldNotThrowWhenNotStarted() + { + // Arrange + using var sender = new UdpTimedSender("127.0.0.1", 5000, _mockLogger.Object); + + // Act & Assert + Assert.DoesNotThrow(() => sender.StopSending()); + } + + [Test] + public void Dispose_ShouldCleanupResources() + { + // Arrange + var sender = new UdpTimedSender("127.0.0.1", 5000, _mockLogger.Object); + sender.StartSending(1000); + + // Act & Assert + Assert.DoesNotThrow(() => sender.Dispose()); + } + } +} diff --git a/EchoTspServer/ClientWrapperFactory.cs b/EchoTspServer/ClientWrapperFactory.cs new file mode 100644 index 0000000..91b1e28 --- /dev/null +++ b/EchoTspServer/ClientWrapperFactory.cs @@ -0,0 +1,18 @@ +using EchoServer.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer +{ + public class ClientWrapperFactory : IClientWrapperFactory + { + public IClientWrapper CreateClientWrapper(TcpClient client) + { + return new TcpClientWrapper(client); + } + } +} diff --git a/EchoTspServer/ConsoleLogger.cs b/EchoTspServer/ConsoleLogger.cs new file mode 100644 index 0000000..e25e364 --- /dev/null +++ b/EchoTspServer/ConsoleLogger.cs @@ -0,0 +1,17 @@ +using EchoServer.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer +{ + public class ConsoleLogger : ILogger + { + public void Log(string message) + { + Console.WriteLine(message); + } + } +} diff --git a/EchoTspServer/EchoServer.cs b/EchoTspServer/EchoServer.cs new file mode 100644 index 0000000..46fb0e0 --- /dev/null +++ b/EchoTspServer/EchoServer.cs @@ -0,0 +1,110 @@ +using EchoServer.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer +{ + public class EchoServer : IDisposable + { + private readonly int _port; + private readonly ILogger _logger; + private readonly IClientWrapperFactory _clientFactory; + private readonly Func _listenerFactory; + + private ITcpListenerWrapper _listener; + private CancellationTokenSource _cancellationTokenSource; + + public bool IsRunning { get; private set; } + + public EchoServer(int port, ILogger logger = null, + IClientWrapperFactory clientFactory = null, + Func listenerFactory = null) + { + _port = port; + _logger = logger ?? new ConsoleLogger(); + _clientFactory = clientFactory ?? new ClientWrapperFactory(); + _listenerFactory = listenerFactory ?? ((addr, p) => new TcpListenerWrapper(addr, p)); + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async Task StartAsync() + { + _listener = _listenerFactory(IPAddress.Any, _port); + _listener.Start(); + IsRunning = true; + _logger.Log($"Server started on port {_port}."); + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + TcpClient client = await _listener.AcceptTcpClientAsync(); + _logger.Log("Client connected."); + + _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); + } + catch (ObjectDisposedException) + { + break; + } + catch (Exception ex) + { + _logger.Log($"Error accepting client: {ex.Message}"); + } + } + + IsRunning = false; + _logger.Log("Server shutdown."); + } + + internal virtual async Task HandleClientAsync(TcpClient client, CancellationToken token) + { + using (var wrapper = _clientFactory.CreateClientWrapper(client)) + using (var stream = wrapper.GetStream()) + { + try + { + byte[] buffer = new byte[8192]; + int bytesRead; + + while (!token.IsCancellationRequested && + (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) + { + await stream.WriteAsync(buffer, 0, bytesRead, token); + _logger.Log($"Echoed {bytesRead} bytes to the client."); + } + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + _logger.Log($"Error: {ex.Message}"); + } + finally + { + _logger.Log("Client disconnected."); + } + } + } + + public void Stop() + { + if (!IsRunning && _cancellationTokenSource.IsCancellationRequested) + return; + + _cancellationTokenSource.Cancel(); + _listener?.Stop(); + IsRunning = false; + _logger.Log("Server stopped."); + } + + public void Dispose() + { + Stop(); + _cancellationTokenSource?.Dispose(); + } + } +} diff --git a/EchoTspServer/Interfaces/IClientWrapper.cs b/EchoTspServer/Interfaces/IClientWrapper.cs new file mode 100644 index 0000000..95bd79c --- /dev/null +++ b/EchoTspServer/Interfaces/IClientWrapper.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer.Interfaces +{ + public interface IClientWrapper : IDisposable + { + INetworkStreamWrapper GetStream(); + } +} diff --git a/EchoTspServer/Interfaces/IClientWrapperFactory.cs b/EchoTspServer/Interfaces/IClientWrapperFactory.cs new file mode 100644 index 0000000..94f6e27 --- /dev/null +++ b/EchoTspServer/Interfaces/IClientWrapperFactory.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer.Interfaces +{ + public interface IClientWrapperFactory + { + IClientWrapper CreateClientWrapper(TcpClient client); + } +} diff --git a/EchoTspServer/Interfaces/ILogger.cs b/EchoTspServer/Interfaces/ILogger.cs new file mode 100644 index 0000000..7d9db06 --- /dev/null +++ b/EchoTspServer/Interfaces/ILogger.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer.Interfaces +{ + public interface ILogger + { + void Log(string message); + } +} diff --git a/EchoTspServer/Interfaces/INetworkStreamWrapper.cs b/EchoTspServer/Interfaces/INetworkStreamWrapper.cs new file mode 100644 index 0000000..9739839 --- /dev/null +++ b/EchoTspServer/Interfaces/INetworkStreamWrapper.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer.Interfaces +{ + public interface INetworkStreamWrapper : IDisposable + { + Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token); + Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token); + } +} diff --git a/EchoTspServer/Interfaces/ITcpListenerWrapper.cs b/EchoTspServer/Interfaces/ITcpListenerWrapper.cs new file mode 100644 index 0000000..b406c95 --- /dev/null +++ b/EchoTspServer/Interfaces/ITcpListenerWrapper.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer.Interfaces +{ + public interface ITcpListenerWrapper + { + void Start(); + void Stop(); + Task AcceptTcpClientAsync(); + } +} diff --git a/EchoTspServer/Interfaces/IUdpSender.cs b/EchoTspServer/Interfaces/IUdpSender.cs new file mode 100644 index 0000000..7eb285f --- /dev/null +++ b/EchoTspServer/Interfaces/IUdpSender.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer.Interfaces +{ + public interface IUdpSender : IDisposable + { + void StartSending(int intervalMilliseconds); + void StopSending(); + } +} diff --git a/EchoTspServer/NetworkStreamWrapper.cs b/EchoTspServer/NetworkStreamWrapper.cs new file mode 100644 index 0000000..a894af8 --- /dev/null +++ b/EchoTspServer/NetworkStreamWrapper.cs @@ -0,0 +1,35 @@ +using EchoServer.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer +{ + public class NetworkStreamWrapper : INetworkStreamWrapper + { + private readonly NetworkStream _stream; + + public NetworkStreamWrapper(NetworkStream stream) + { + _stream = stream; + } + + public Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken token) + { + return _stream.ReadAsync(buffer, offset, count, token); + } + + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken token) + { + return _stream.WriteAsync(buffer, offset, count, token); + } + + public void Dispose() + { + _stream?.Dispose(); + } + } +} diff --git a/EchoTspServer/Program.cs b/EchoTspServer/Program.cs index 82721a2..d0bb2b7 100644 --- a/EchoTspServer/Program.cs +++ b/EchoTspServer/Program.cs @@ -7,166 +7,34 @@ namespace EchoServer { - public class EchoServer + public class Program { - private readonly int _port; - private TcpListener _listener; - private CancellationTokenSource _cancellationTokenSource; - - //constuctor - public EchoServer(int port) - { - _port = port; - _cancellationTokenSource = new CancellationTokenSource(); - } - - public async Task StartAsync() - { - _listener = new TcpListener(IPAddress.Any, _port); - _listener.Start(); - Console.WriteLine($"Server started on port {_port}."); - - while (!_cancellationTokenSource.Token.IsCancellationRequested) - { - try - { - TcpClient client = await _listener.AcceptTcpClientAsync(); - Console.WriteLine("Client connected."); - - _ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token)); - } - catch (ObjectDisposedException) - { - // Listener has been closed - break; - } - } - - Console.WriteLine("Server shutdown."); - } - - private async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (NetworkStream stream = client.GetStream()) - { - try - { - byte[] buffer = new byte[8192]; - int bytesRead; - - while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) - { - // Echo back the received message - await stream.WriteAsync(buffer, 0, bytesRead, token); - Console.WriteLine($"Echoed {bytesRead} bytes to the client."); - } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - Console.WriteLine($"Error: {ex.Message}"); - } - finally - { - client.Close(); - Console.WriteLine("Client disconnected."); - } - } - } - - public void Stop() - { - _cancellationTokenSource.Cancel(); - _listener.Stop(); - _cancellationTokenSource.Dispose(); - Console.WriteLine("Server stopped."); - } - public static async Task Main(string[] args) { - EchoServer server = new EchoServer(5000); + var logger = new ConsoleLogger(); + using var server = new EchoServer(5000, logger); - // Start the server in a separate task _ = Task.Run(() => server.StartAsync()); - string host = "127.0.0.1"; // Target IP - int port = 60000; // Target Port - int intervalMilliseconds = 5000; // Send every 3 seconds + string host = "127.0.0.1"; + int port = 60000; + int intervalMilliseconds = 5000; - using (var sender = new UdpTimedSender(host, port)) + using (var sender = new UdpTimedSender(host, port, logger)) { - Console.WriteLine("Press any key to stop sending..."); + logger.Log("Press any key to stop sending..."); sender.StartSending(intervalMilliseconds); - Console.WriteLine("Press 'q' to quit..."); + logger.Log("Press 'q' to quit..."); while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q) { - // Just wait until 'q' is pressed + // Очікуємо натискання 'q' } sender.StopSending(); server.Stop(); - Console.WriteLine("Sender stopped."); + logger.Log("Sender stopped."); } } } - - - public class UdpTimedSender : IDisposable - { - private readonly string _host; - private readonly int _port; - private readonly UdpClient _udpClient; - private Timer _timer; - - public UdpTimedSender(string host, int port) - { - _host = host; - _port = port; - _udpClient = new UdpClient(); - } - - public void StartSending(int intervalMilliseconds) - { - if (_timer != null) - throw new InvalidOperationException("Sender is already running."); - - _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); - } - - ushort i = 0; - - private void SendMessageCallback(object state) - { - try - { - //dummy data - Random rnd = new Random(); - byte[] samples = new byte[1024]; - rnd.NextBytes(samples); - i++; - - byte[] msg = (new byte[] { 0x04, 0x84 }).Concat(BitConverter.GetBytes(i)).Concat(samples).ToArray(); - var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); - - _udpClient.Send(msg, msg.Length, endpoint); - Console.WriteLine($"Message sent to {_host}:{_port} "); - } - catch (Exception ex) - { - Console.WriteLine($"Error sending message: {ex.Message}"); - } - } - - public void StopSending() - { - _timer?.Dispose(); - _timer = null; - } - - public void Dispose() - { - StopSending(); - _udpClient.Dispose(); - } - } } \ No newline at end of file diff --git a/EchoTspServer/TcpClientWrapper.cs b/EchoTspServer/TcpClientWrapper.cs new file mode 100644 index 0000000..c3258b9 --- /dev/null +++ b/EchoTspServer/TcpClientWrapper.cs @@ -0,0 +1,31 @@ +using EchoServer.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer +{ + public class TcpClientWrapper : IClientWrapper + { + private readonly TcpClient _client; + + public TcpClientWrapper(TcpClient client) + { + _client = client; + } + + public INetworkStreamWrapper GetStream() + { + return new NetworkStreamWrapper(_client.GetStream()); + } + + public void Dispose() + { + _client?.Close(); + _client?.Dispose(); + } + } +} diff --git a/EchoTspServer/TcpListenerWrapper.cs b/EchoTspServer/TcpListenerWrapper.cs new file mode 100644 index 0000000..b462017 --- /dev/null +++ b/EchoTspServer/TcpListenerWrapper.cs @@ -0,0 +1,29 @@ +using EchoServer.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer +{ + public class TcpListenerWrapper : ITcpListenerWrapper + { + private readonly TcpListener _listener; + + public TcpListenerWrapper(IPAddress address, int port) + { + _listener = new TcpListener(address, port); + } + + public void Start() => _listener.Start(); + public void Stop() => _listener.Stop(); + + public async Task AcceptTcpClientAsync() + { + return await _listener.AcceptTcpClientAsync(); + } + } +} diff --git a/EchoTspServer/UdpTimedSender.cs b/EchoTspServer/UdpTimedSender.cs new file mode 100644 index 0000000..cc5a426 --- /dev/null +++ b/EchoTspServer/UdpTimedSender.cs @@ -0,0 +1,93 @@ +using EchoServer.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace EchoServer +{ + public class UdpTimedSender : IUdpSender + { + private readonly string _host; + private readonly int _port; + private readonly ILogger _logger; + private readonly UdpClient _udpClient; + private readonly RandomNumberGenerator _random; + private Timer _timer; + private ushort _sequenceNumber = 0; + + public UdpTimedSender(string host, int port, ILogger logger = null) + { + _host = host; + _port = port; + _logger = logger ?? new ConsoleLogger(); + _udpClient = new UdpClient(); + _random = RandomNumberGenerator.Create(); + } + + public void StartSending(int intervalMilliseconds) + { + if (_timer != null) + throw new InvalidOperationException("Sender is already running."); + + _timer = new Timer(SendMessageCallback, null, 0, intervalMilliseconds); + } + + private void SendMessageCallback(object state) + { + try + { + byte[] samples = new byte[1024]; + _random.GetBytes(samples); + _sequenceNumber++; + + byte[] msg = CombineArrays( + new byte[] { 0x04, 0x84 }, + BitConverter.GetBytes(_sequenceNumber), + samples + ); + + var endpoint = new IPEndPoint(IPAddress.Parse(_host), _port); + _udpClient.Send(msg, msg.Length, endpoint); + _logger.Log($"Message sent to {_host}:{_port}"); + } + catch (Exception ex) + { + _logger.Log($"Error sending message: {ex.Message}"); + } + } + + private byte[] CombineArrays(params byte[][] arrays) + { + int totalLength = 0; + foreach (var arr in arrays) + totalLength += arr.Length; + + byte[] result = new byte[totalLength]; + int offset = 0; + foreach (var arr in arrays) + { + Buffer.BlockCopy(arr, 0, result, offset, arr.Length); + offset += arr.Length; + } + return result; + } + + public void StopSending() + { + _timer?.Dispose(); + _timer = null; + } + + public void Dispose() + { + StopSending(); + _udpClient?.Dispose(); + _random?.Dispose(); + } + } +} diff --git a/NetSdrClient.sln b/NetSdrClient.sln index d8ca20f..60c9ec2 100644 --- a/NetSdrClient.sln +++ b/NetSdrClient.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSdrClientAppTests", "Net EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServer", "EchoTspServer\EchoServer.csproj", "{9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EchoServerTests", "EchoServerTests\EchoServerTests.csproj", "{74C727EC-21D7-488C-8A46-274109E6304E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,8 +29,15 @@ Global {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Debug|Any CPU.Build.0 = Debug|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.ActiveCfg = Release|Any CPU {9179F2F7-EBEE-4A5D-9FD9-F6E3C18DD263}.Release|Any CPU.Build.0 = Release|Any CPU + {74C727EC-21D7-488C-8A46-274109E6304E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74C727EC-21D7-488C-8A46-274109E6304E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74C727EC-21D7-488C-8A46-274109E6304E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74C727EC-21D7-488C-8A46-274109E6304E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BFFD196B-B6C2-4760-8BC1-00C8A87FF864} + EndGlobalSection EndGlobal