using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using NModbus.IO;
using NModbus.Logging;
namespace NModbus.Device
{
#if TIMER
using System.Timers;
#endif
///
/// Modbus TCP slave device.
///
public class ModbusTcpSlaveNetwork : ModbusSlaveNetwork, IModbusTcpSlaveNetwork
{
private const int TimeWaitResponse = 1000;
private readonly object _serverLock = new object();
private readonly ConcurrentDictionary _masters =
new ConcurrentDictionary();
private TcpListener _server;
#if TIMER
private Timer _timer;
#endif
public ModbusTcpSlaveNetwork(TcpListener tcpListener, IModbusFactory modbusFactory, IModbusLogger logger)
: base(new EmptyTransport(modbusFactory), modbusFactory, logger)
{
if (tcpListener == null)
{
throw new ArgumentNullException(nameof(tcpListener));
}
_server = tcpListener;
}
#if TIMER
private ModbusTcpSlave(byte unitId, TcpListener tcpListener, double timeInterval)
: base(unitId, new EmptyTransport())
{
if (tcpListener == null)
{
throw new ArgumentNullException(nameof(tcpListener));
}
_server = tcpListener;
_timer = new Timer(timeInterval);
_timer.Elapsed += OnTimer;
_timer.Enabled = true;
}
#endif
///
/// Gets the Modbus TCP Masters connected to this Modbus TCP Slave.
///
public ReadOnlyCollection Masters
{
get
{
return new ReadOnlyCollection(_masters.Values.Select(mc => mc.TcpClient).ToList());
}
}
///
/// Gets the server.
///
/// The server.
///
/// This property is not thread safe, it should only be consumed within a lock.
///
private TcpListener Server
{
get
{
if (_server == null)
{
throw new ObjectDisposedException("Server");
}
return _server;
}
}
/////
///// Modbus TCP slave factory method.
/////
//public static ModbusTcpSlave CreateTcp(byte unitId, TcpListener tcpListener)
//{
// return new ModbusTcpSlave(unitId, tcpListener);
//}
#if TIMER
///
/// Creates ModbusTcpSlave with timer which polls connected clients every
/// milliseconds on that they are connected.
///
public static ModbusTcpSlave CreateTcp(byte unitId, TcpListener tcpListener, double pollInterval)
{
return new ModbusTcpSlave(unitId, tcpListener, pollInterval);
}
#endif
///
/// Start slave listening for requests.
///
public override async Task ListenAsync(CancellationToken cancellationToken = new CancellationToken())
{
Logger.Information("Start Modbus Tcp Server.");
// TODO: add state {stopped, listening} and check it before starting
Server.Start();
// Cancellation code based on https://stackoverflow.com/a/47049129/11066760
using (cancellationToken.Register(() => Server.Stop()))
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
TcpClient client = await Server.AcceptTcpClientAsync().ConfigureAwait(false);
var masterConnection = new ModbusMasterTcpConnection(client, this, ModbusFactory, Logger);
masterConnection.ModbusMasterTcpConnectionClosed += OnMasterConnectionClosedHandler;
_masters.TryAdd(client.Client.RemoteEndPoint.ToString(), masterConnection);
}
}
catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
{
//Swallow this
}
catch (InvalidOperationException)
{
// Either Server.Start wasn't called (a bug!)
// or the CancellationToken was cancelled before
// we started accepting (giving an InvalidOperationException),
// or the CancellationToken was cancelled after
// we started accepting (giving an ObjectDisposedException).
//
// In the latter two cases we should surface the cancellation
// exception, or otherwise rethrow the original exception.
cancellationToken.ThrowIfCancellationRequested();
throw;
}
}
}
///
/// Releases unmanaged and - optionally - managed resources
///
///
/// true to release both managed and unmanaged resources; false to release only
/// unmanaged resources.
///
/// Dispose is thread-safe.
protected override void Dispose(bool disposing)
{
if (disposing)
{
// double-check locking
if (_server != null)
{
lock (_serverLock)
{
if (_server != null)
{
_server.Stop();
_server = null;
#if TIMER
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}
#endif
foreach (var key in _masters.Keys)
{
ModbusMasterTcpConnection connection;
if (_masters.TryRemove(key, out connection))
{
connection.ModbusMasterTcpConnectionClosed -= OnMasterConnectionClosedHandler;
connection.Dispose();
}
}
}
}
}
}
}
private static bool IsSocketConnected(Socket socket)
{
bool poll = socket.Poll(TimeWaitResponse, SelectMode.SelectRead);
bool available = (socket.Available == 0);
return poll && available;
}
#if TIMER
private void OnTimer(object sender, ElapsedEventArgs e)
{
foreach (var master in _masters.ToList())
{
if (IsSocketConnected(master.Value.TcpClient.Client) == false)
{
master.Value.Dispose();
}
}
}
#endif
private void OnMasterConnectionClosedHandler(object sender, TcpConnectionEventArgs e)
{
ModbusMasterTcpConnection connection;
if (!_masters.TryRemove(e.EndPoint, out connection))
{
string msg = $"EndPoint {e.EndPoint} cannot be removed, it does not exist.";
throw new ArgumentException(msg);
}
connection.Dispose();
Logger.Information($"Removed Master {e.EndPoint}");
}
}
}