243 lines
6.8 KiB
C#
243 lines
6.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace StandardScene.MagCarSimulator
|
|
{
|
|
public sealed class MagCarSimTcpServer : IDisposable
|
|
{
|
|
private readonly IPAddress _address;
|
|
private readonly int _port;
|
|
private readonly Func<byte[], byte[]> _handler;
|
|
private readonly object _clientsLock = new object();
|
|
private readonly HashSet<TcpClient> _clients = new HashSet<TcpClient>();
|
|
private TcpListener _listener;
|
|
private CancellationTokenSource _cts;
|
|
private int _clientCount;
|
|
private bool _running;
|
|
|
|
public MagCarSimTcpServer(IPAddress address, int port, Func<byte[], byte[]> handler)
|
|
{
|
|
_address = address ?? IPAddress.Any;
|
|
_port = port;
|
|
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
|
|
}
|
|
|
|
public bool IsRunning => _running;
|
|
public int ClientCount => Volatile.Read(ref _clientCount);
|
|
|
|
public void Start()
|
|
{
|
|
if (_running)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_cts = new CancellationTokenSource();
|
|
_listener = new TcpListener(_address, _port);
|
|
_listener.Server.NoDelay = true;
|
|
_listener.Start();
|
|
_running = true;
|
|
_ = Task.Run(() => AcceptLoop(_cts.Token));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_running = false;
|
|
try
|
|
{
|
|
_cts?.Cancel();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
try
|
|
{
|
|
_listener?.Stop();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
List<TcpClient> clients;
|
|
lock (_clientsLock)
|
|
{
|
|
clients = new List<TcpClient>(_clients);
|
|
_clients.Clear();
|
|
}
|
|
|
|
foreach (var client in clients)
|
|
{
|
|
try
|
|
{
|
|
client.Close();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
|
|
_cts?.Dispose();
|
|
_cts = null;
|
|
_listener = null;
|
|
}
|
|
|
|
private async Task AcceptLoop(CancellationToken token)
|
|
{
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
TcpClient client;
|
|
try
|
|
{
|
|
client = await _listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
return;
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
lock (_clientsLock)
|
|
{
|
|
_clients.Add(client);
|
|
}
|
|
|
|
_ = Task.Run(() => HandleClient(client, token), token);
|
|
}
|
|
}
|
|
|
|
private void HandleClient(TcpClient client, CancellationToken token)
|
|
{
|
|
Interlocked.Increment(ref _clientCount);
|
|
try
|
|
{
|
|
client.NoDelay = true;
|
|
client.ReceiveTimeout = 0;
|
|
using (var stream = client.GetStream())
|
|
{
|
|
var buffer = new byte[MagCarSimProtocol.FrameLength];
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
if (!ReadExact(stream, buffer, token))
|
|
{
|
|
return;
|
|
}
|
|
|
|
byte[] response;
|
|
try
|
|
{
|
|
response = _handler(buffer);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MagCarSimLog.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] TCP 处理异常: {ex.Message}");
|
|
return;
|
|
}
|
|
|
|
if (response == null || response.Length != MagCarSimProtocol.FrameLength)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
stream.Write(response, 0, response.Length);
|
|
stream.Flush();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex) when (IsBenign(ex))
|
|
{
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MagCarSimLog.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] TCP 连接异常: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
lock (_clientsLock)
|
|
{
|
|
_clients.Remove(client);
|
|
}
|
|
|
|
try
|
|
{
|
|
client.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
Interlocked.Decrement(ref _clientCount);
|
|
}
|
|
}
|
|
|
|
private static bool ReadExact(NetworkStream stream, byte[] buffer, CancellationToken token)
|
|
{
|
|
var offset = 0;
|
|
while (offset < buffer.Length)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
int read;
|
|
try
|
|
{
|
|
read = stream.Read(buffer, offset, buffer.Length - offset);
|
|
}
|
|
catch (Exception ex) when (IsBenign(ex))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (read == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
offset += read;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool IsBenign(Exception ex)
|
|
{
|
|
for (var current = ex; current != null; current = current.InnerException)
|
|
{
|
|
if (current is ObjectDisposedException || current is OperationCanceledException)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (current is SocketException socketEx)
|
|
{
|
|
switch (socketEx.SocketErrorCode)
|
|
{
|
|
case SocketError.OperationAborted:
|
|
case SocketError.Interrupted:
|
|
case SocketError.ConnectionAborted:
|
|
case SocketError.ConnectionReset:
|
|
case SocketError.Shutdown:
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|