Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions EchoTcpServer/ConsoleLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using EchoServer.Abstractions;

namespace EchoServer.Implementations
{
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}


public void LogError(string message)
{
// Можна додати колір, як описано в аналізі
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"ERROR: {message}");
Console.ResetColor();
}
}
}
10 changes: 10 additions & 0 deletions EchoTcpServer/ILogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace EchoServer.Abstractions
{
public interface ILogger
{
void Log(string message);
void LogError(string message);
}
}
35 changes: 35 additions & 0 deletions EchoTcpServer/ITcpNetworkAbstractions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace EchoServer.Abstractions
{
// Абстракція для NetworkStream
public interface INetworkStreamWrapper : IDisposable
{
Task<int> ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken);
Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken);
}

// Абстракція для TcpClient
public interface ITcpClientWrapper : IDisposable
{
INetworkStreamWrapper GetStream();
void Close();
}

// Абстракція для TcpListener
public interface ITcpListenerWrapper : IDisposable
{
void Start();
void Stop();
Task<ITcpClientWrapper> AcceptTcpClientAsync();
}

// Фабрика для створення Listener
public interface ITcpListenerFactory
{
ITcpListenerWrapper Create(IPAddress address, int port);
}
}
176 changes: 29 additions & 147 deletions EchoTcpServer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,172 +1,54 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using EchoServer.Implementations;
using EchoServer.Abstractions;
using EchoServer.Services;
using System.Threading;

namespace EchoServer
{
public class EchoServer
public static class Program
{
private readonly int _port;
private TcpListener _listener;
private CancellationTokenSource _cancellationTokenSource;

//constuctor
public EchoServer(int port)
public static void Main(string[] args)
{
_port = port;
_cancellationTokenSource = new CancellationTokenSource();
}
// 1. Створення залежностей (Composition Root)
ILogger logger = new ConsoleLogger();
ITcpListenerFactory factory = new TcpListenerFactory();

public async Task StartAsync()
{
_listener = new TcpListener(IPAddress.Any, _port);
_listener.Start();
Console.WriteLine($"Server started on port {_port}.");
int tcpPort = 5000;

while (!_cancellationTokenSource.Token.IsCancellationRequested)
// 2. Створення сервісів з DI
using (EchoServerService server = new EchoServerService(tcpPort, logger, factory))
{
try
{
TcpClient client = await _listener.AcceptTcpClientAsync();
Console.WriteLine("Client connected.");
// Запуск сервера в окремому завданні
_ = Task.Run(() => server.StartAsync());

_ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token));
}
catch (ObjectDisposedException)
{
// Listener has been closed
break;
}
}

Console.WriteLine("Server shutdown.");
}
string host = "127.0.0.1";
int udpPort = 60000;
int intervalMilliseconds = 5000;

private async Task HandleClientAsync(TcpClient client, CancellationToken token)
{
using (NetworkStream stream = client.GetStream())
{
try
using (var sender = new UdpTimedSender(host, udpPort, logger))
{
byte[] buffer = new byte[8192];
int bytesRead;
logger.Log("Press 'q' to quit...");
sender.StartSending(intervalMilliseconds);

while (!token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
// Обробка користувацького вводу
while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q)
{
// Echo back the received message
await stream.WriteAsync(buffer, 0, bytesRead, token);
Console.WriteLine($"Echoed {bytesRead} bytes to the client.");
// Просто чекаємо 'q'
}
}
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);

// Start the server in a separate task
_ = Task.Run(() => server.StartAsync());
// Graceful shutdown
sender.StopSending();
server.Stop();

string host = "127.0.0.1"; // Target IP
int port = 60000; // Target Port
int intervalMilliseconds = 5000; // Send every 3 seconds


using (var sender = new UdpTimedSender(host, port))
{
Console.WriteLine("Press any key to stop sending...");
sender.StartSending(intervalMilliseconds);

Console.WriteLine("Press 'q' to quit...");
while (Console.ReadKey(intercept: true).Key != ConsoleKey.Q)
{
// Just wait until 'q' is pressed
logger.Log("Application stopped.");
}

sender.StopSending();
server.Stop();
Console.WriteLine("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();
Thread.Sleep(100);
}
}
}
107 changes: 107 additions & 0 deletions EchoTcpServer/Services/EchoServerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using EchoServer.Abstractions;
using EchoServer.Implementations; // Потрібно для TcpListenerWrapper, якщо він не у папці Implementations

namespace EchoServer.Services
{
// Це рефакторений клас EchoServer
public class EchoServerService : IDisposable

Check warning on line 12 in EchoTcpServer/Services/EchoServerService.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Fix this implementation of 'IDisposable' to conform to the dispose pattern. (https://rules.sonarsource.com/csharp/RSPEC-3881)
{
private readonly int _port;
private readonly ILogger _logger;
private readonly ITcpListenerFactory? _listenerFactory;
private ITcpListenerWrapper _listener; // Тепер використовуємо Wrapper
private CancellationTokenSource _cancellationTokenSource;

Check warning on line 18 in EchoTcpServer/Services/EchoServerService.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Make '_cancellationTokenSource' 'readonly'. (https://rules.sonarsource.com/csharp/RSPEC-2933)

// DI: Впроваджуємо всі залежності (Logger, Factory)
public EchoServerService(int port, ILogger logger, ITcpListenerFactory listenerFactory)

Check warning on line 21 in EchoTcpServer/Services/EchoServerService.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Non-nullable field '_listener' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
{
// Валідація вхідних параметрів
_port = port > 0 ? port : throw new ArgumentOutOfRangeException(nameof(port));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_listenerFactory = listenerFactory ?? throw new ArgumentNullException(nameof(listenerFactory));

_cancellationTokenSource = new CancellationTokenSource();
}

public async Task StartAsync()
{
// Створюємо listener через Factory
_listener = _listenerFactory.Create(IPAddress.Any, _port);

Check warning on line 34 in EchoTcpServer/Services/EchoServerService.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Dereference of a possibly null reference.
_listener.Start();
_logger.Log($"Server started on port {_port}.");

while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
try
{
// ВИКОРИСТОВУЄМО АБСТРАКЦІЮ ITcpClientWrapper
ITcpClientWrapper client = await _listener.AcceptTcpClientAsync();
_logger.Log("Client connected.");

// Запускаємо обробку клієнта
_ = Task.Run(() => HandleClientAsync(client, _cancellationTokenSource.Token));
}
catch (ObjectDisposedException)
{
// Listener було закрито під час виклику Stop()
break;
}
catch (Exception ex)
{
// Обробка некритичних помилок у циклі
if (!_cancellationTokenSource.Token.IsCancellationRequested)
_logger.LogError($"Error accepting client: {ex.Message}");
}
}

_logger.Log("Server shutdown.");
}

// Приймає ITcpClientWrapper та використовує INetworkStreamWrapper
private async Task HandleClientAsync(ITcpClientWrapper client, CancellationToken token)
{
// ВИКОРИСТОВУЄМО АБСТРАКЦІЮ INetworkStreamWrapper
using (INetworkStreamWrapper stream = client.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.LogError($"Error handling client: {ex.Message}");
}
finally
{
client.Close();
_logger.Log("Client disconnected.");
}
}
}

public void Stop()
{
_cancellationTokenSource.Cancel();
_listener.Stop();
}

public void Dispose()
{
Stop();
_listener?.Dispose();
_cancellationTokenSource.Dispose();
}
}
}
Loading
Loading