将 StandardScene 各插件的配置/监控窗体从 WinForms 迁移到 CycleGUI(删除 .Designer.cs/.resx,重写为 PanelBuilder 立即模式 UI,新增 CycleUiHelper 统一对话框)。 同时修复代码审核中的问题: - 后台文件写入加锁 + try/catch(ButtonBoxManager / DoorManager,对齐 LoopViewer.SaveTasks 模式) - CoderFieldsMetadata.cs 启用 #nullable enable,消除 CS8632 警告 - DummyCar 移除已废弃的 rightClickAction()/SetPosition() - CarRemoteHelper.OpenVehicleWebPage 的 Process.Start 加 try/catch - 重命名名不副实的 Mstsc()(现为打开网页) - 统一弃元命名为 _ - TrafficInterlockViewer 改用稳定 Id(GUID)做选择/编辑,替代行索引 - csproj 改用 $(CGUILibDir) 解析 CycleGUI,绝对路径收敛到 Directory.Build.props 构建:dotnet build StandardScene.sln → 0 错误,30 警告(均为历史遗留)。 注:static 单例状态重构(审核第 8 项)暂未处理,留待单独任务。
501 lines
15 KiB
C#
501 lines
15 KiB
C#
using SimpleCore.Library;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Net;
|
|
using System.Net.NetworkInformation;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace StandardScene.TCP
|
|
{
|
|
/// <summary>
|
|
/// 异步 TCP 客户端
|
|
/// </summary>
|
|
public class AsyncTcpClient : IDisposable
|
|
{
|
|
private sealed class DatagramReadState
|
|
{
|
|
public TcpClient Client { get; set; }
|
|
public byte[] Buffer { get; set; }
|
|
}
|
|
|
|
public event EventHandler<TcpDatagramReceivedEventArgs<byte[]>> DatagramReceived; // 接收到数据报文事件
|
|
public event EventHandler<TcpDatagramReceivedEventArgs<byte[]>> PlaintextReceived; // 接收到数据报文明文事件
|
|
public event EventHandler<TcpServerConnectedEventArgs> ServerConnected; // 与服务器的连接已建立事件
|
|
public event EventHandler<TcpServerDisconnectedEventArgs> ServerDisconnected; // 与服务器的连接已断开事件
|
|
public event EventHandler<TcpServerExceptionOccurredEventArgs> ServerExceptionOccurred; // 与服务器的连接发生异常事件
|
|
|
|
private TcpClient tcpClient;
|
|
private bool disposed = false;
|
|
private int retries = 0; // 重连计数
|
|
|
|
private readonly object _reconnectGate = new object();
|
|
private bool _closing = false;
|
|
private bool _isConnecting = false;
|
|
private bool _isReconnecting = false;
|
|
private Timer _reconnectTimer;
|
|
|
|
public AsyncTcpClient(IPAddress remoteIPAddress, int remotePort)
|
|
{
|
|
this.Addresses = remoteIPAddress;
|
|
this.Port = remotePort;
|
|
this.Encoding = Encoding.Default;
|
|
this.Retries = 3;
|
|
this.RetryInterval = 5;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 是否已与服务器建立连接
|
|
/// </summary>
|
|
public bool Connected
|
|
{
|
|
get
|
|
{
|
|
try
|
|
{
|
|
return this.tcpClient != null && this.tcpClient.Connected;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 远端服务器的IP地址列表
|
|
/// </summary>
|
|
public IPAddress Addresses { get; private set; }
|
|
|
|
/// <summary>
|
|
/// 远端服务器的端口
|
|
/// </summary>
|
|
public int Port { get; private set; }
|
|
|
|
/// <summary>
|
|
/// 连接重试次数
|
|
/// </summary>
|
|
public int Retries { get; set; }
|
|
|
|
/// <summary>
|
|
/// 连接重试间隔
|
|
/// </summary>
|
|
public int RetryInterval { get; set; }
|
|
|
|
/// <summary>
|
|
/// 远端服务器终结点
|
|
/// </summary>
|
|
public IPEndPoint RemoteIPEndPoint
|
|
{
|
|
get
|
|
{
|
|
return new IPEndPoint(this.Addresses, this.Port);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 通信所使用的编码
|
|
/// </summary>
|
|
public Encoding Encoding { get; set; }
|
|
uint on = 1;
|
|
|
|
/// <summary>
|
|
/// 连接到服务器
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public AsyncTcpClient Connect()
|
|
{
|
|
lock (_reconnectGate)
|
|
{
|
|
_closing = false;
|
|
}
|
|
|
|
if (this.Connected)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
this.ConnectInternal(resetRetries: true);
|
|
return this;
|
|
}
|
|
|
|
private void ConnectInternal(bool resetRetries)
|
|
{
|
|
TcpClient client;
|
|
|
|
lock (_reconnectGate)
|
|
{
|
|
if (_closing || disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Prevent multiple in-flight connection attempts.
|
|
if (_isConnecting || _isReconnecting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_isConnecting = true;
|
|
if (resetRetries)
|
|
{
|
|
retries = 0;
|
|
}
|
|
|
|
client = new TcpClient();
|
|
this.tcpClient = client;
|
|
}
|
|
|
|
try
|
|
{
|
|
client.Client.IOControl(IOControlCode.KeepAliveValues, KeepAlive(1, 500, 500), null);
|
|
client.BeginConnect(this.Addresses, this.Port, new AsyncCallback(this.HandleTcpServerConnected), client);
|
|
}
|
|
catch
|
|
{
|
|
try { client.Close(); } catch { }
|
|
|
|
lock (_reconnectGate)
|
|
{
|
|
_isConnecting = false;
|
|
}
|
|
|
|
if (!_closing && !disposed)
|
|
{
|
|
ScheduleReconnect("connect begin failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleRemoteDisconnect(TcpClient client, string reason)
|
|
{
|
|
if (!ReferenceEquals(client, this.tcpClient))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try { client.Close(); } catch { }
|
|
|
|
this.RaiseServerDisconnected(this.Addresses, this.Port);
|
|
|
|
if (!_closing && !disposed)
|
|
{
|
|
ScheduleReconnect(reason);
|
|
}
|
|
}
|
|
|
|
private void ScheduleReconnect(string reason)
|
|
{
|
|
lock (_reconnectGate)
|
|
{
|
|
if (_closing || disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (this.Connected)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_isConnecting || _isReconnecting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Keep reconnecting until the client is closed/disposed.
|
|
// Preserve the counter only for logging (avoid int overflow by wrapping).
|
|
if (retries == int.MaxValue)
|
|
{
|
|
retries = 0;
|
|
}
|
|
retries++;
|
|
_isReconnecting = true;
|
|
|
|
if (_reconnectTimer != null)
|
|
{
|
|
try { _reconnectTimer.Dispose(); } catch { }
|
|
_reconnectTimer = null;
|
|
}
|
|
|
|
Diagnosis.Post($"[AsyncTcpClient] schedule reconnect attempt {retries}/{this.Retries} in {this.RetryInterval}s. Reason={reason}");
|
|
|
|
Timer t = null;
|
|
t = new Timer(_ =>
|
|
{
|
|
try
|
|
{
|
|
lock (_reconnectGate)
|
|
{
|
|
_isReconnecting = false;
|
|
_reconnectTimer = null;
|
|
}
|
|
|
|
this.ConnectInternal(resetRetries: false);
|
|
}
|
|
catch { }
|
|
finally
|
|
{
|
|
try { t.Dispose(); } catch { }
|
|
}
|
|
}, null, TimeSpan.FromSeconds((double)this.RetryInterval), Timeout.InfiniteTimeSpan);
|
|
|
|
_reconnectTimer = t;
|
|
}
|
|
}
|
|
|
|
private byte[] KeepAlive(int onOff, int keepAliveTime, int keepAliveInterval)
|
|
{
|
|
byte[] buffer = new byte[12];
|
|
BitConverter.GetBytes(onOff).CopyTo(buffer, 0);
|
|
BitConverter.GetBytes(keepAliveTime).CopyTo(buffer, 4);
|
|
BitConverter.GetBytes(keepAliveInterval).CopyTo(buffer, 8);
|
|
return buffer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 关闭与服务器的连接
|
|
/// </summary>
|
|
/// <returns>异步TCP客户端</returns>
|
|
public AsyncTcpClient Close()
|
|
{
|
|
TcpClient clientToClose = null;
|
|
bool wasConnected = false;
|
|
|
|
lock (_reconnectGate)
|
|
{
|
|
_closing = true;
|
|
retries = 0;
|
|
_isConnecting = false;
|
|
_isReconnecting = false;
|
|
|
|
if (_reconnectTimer != null)
|
|
{
|
|
try { _reconnectTimer.Dispose(); } catch { }
|
|
_reconnectTimer = null;
|
|
}
|
|
|
|
clientToClose = this.tcpClient;
|
|
wasConnected = clientToClose != null && clientToClose.Connected;
|
|
this.tcpClient = null;
|
|
}
|
|
|
|
if (clientToClose != null)
|
|
{
|
|
try { clientToClose.Close(); } catch { }
|
|
}
|
|
|
|
if (wasConnected)
|
|
{
|
|
this.RaiseServerDisconnected(this.Addresses, this.Port);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
private void HandleTcpServerConnected(IAsyncResult ar)
|
|
{
|
|
TcpClient client = (TcpClient)ar.AsyncState;
|
|
try
|
|
{
|
|
if (!ReferenceEquals(client, this.tcpClient))
|
|
{
|
|
// Stale connect callback for an old TcpClient instance.
|
|
try { client.Close(); } catch { }
|
|
return;
|
|
}
|
|
|
|
client.EndConnect(ar);
|
|
this.RaiseServerConnected(this.Addresses, this.Port);
|
|
|
|
lock (_reconnectGate)
|
|
{
|
|
this.retries = 0;
|
|
_isConnecting = false;
|
|
_isReconnecting = false;
|
|
|
|
if (_reconnectTimer != null)
|
|
{
|
|
try { _reconnectTimer.Dispose(); } catch { }
|
|
_reconnectTimer = null;
|
|
}
|
|
}
|
|
|
|
byte[] buffer = new byte[client.ReceiveBufferSize];
|
|
var state = new DatagramReadState { Client = client, Buffer = buffer };
|
|
client.GetStream().BeginRead(buffer, 0, buffer.Length, new AsyncCallback(this.HandleDatagramReceived), state);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!ReferenceEquals(client, this.tcpClient))
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_reconnectGate)
|
|
{
|
|
_isConnecting = false;
|
|
}
|
|
|
|
if (!_closing && !disposed)
|
|
{
|
|
Diagnosis.Post($" HandleTcpServerConnected {this.Addresses} {this.Port} 连接断开,尝试重连...");
|
|
ScheduleReconnect("connect failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleDatagramReceived(IAsyncResult ar)
|
|
{
|
|
DatagramReadState state = null;
|
|
TcpClient client = null;
|
|
byte[] buffer = null;
|
|
|
|
try
|
|
{
|
|
state = (DatagramReadState)ar.AsyncState;
|
|
client = state.Client;
|
|
buffer = state.Buffer;
|
|
|
|
if (!ReferenceEquals(client, this.tcpClient))
|
|
{
|
|
// Stale read callback for an old TcpClient instance.
|
|
return;
|
|
}
|
|
|
|
NetworkStream stream = client.GetStream();
|
|
int numberOfReadBytes = 0;
|
|
try
|
|
{
|
|
numberOfReadBytes = stream.EndRead(ar);
|
|
}
|
|
catch
|
|
{
|
|
numberOfReadBytes = 0;
|
|
}
|
|
|
|
if (numberOfReadBytes == 0)
|
|
{
|
|
HandleRemoteDisconnect(client, "zero-byte read");
|
|
return;
|
|
}
|
|
|
|
byte[] receivedBytes = new byte[numberOfReadBytes];
|
|
Buffer.BlockCopy(buffer, 0, receivedBytes, 0, numberOfReadBytes);
|
|
this.RaiseDatagramReceived(client, receivedBytes);
|
|
this.RaisePlaintextReceived(client, receivedBytes);
|
|
|
|
// then start reading from the network again
|
|
stream.BeginRead(buffer, 0, buffer.Length, new AsyncCallback(this.HandleDatagramReceived), state);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Trace.WriteLine(ex.Message);
|
|
|
|
if (client != null && ReferenceEquals(client, this.tcpClient) && !_closing && !disposed)
|
|
{
|
|
HandleRemoteDisconnect(client, "read failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RaiseDatagramReceived(TcpClient sender, byte[] datagram)
|
|
{
|
|
if (this.DatagramReceived != null)
|
|
{
|
|
this.DatagramReceived(this, new TcpDatagramReceivedEventArgs<byte[]>(sender, datagram));
|
|
}
|
|
}
|
|
|
|
private void RaisePlaintextReceived(TcpClient sender, byte[] datagram)
|
|
{
|
|
if (this.PlaintextReceived != null)
|
|
{
|
|
//this.PlaintextReceived(this, new TcpDatagramReceivedEventArgs<string>(sender, this.Encoding.GetString(datagram, 0, datagram.Length)));
|
|
this.PlaintextReceived(this, new TcpDatagramReceivedEventArgs<byte[]>(sender, datagram));
|
|
}
|
|
}
|
|
|
|
private void RaiseServerConnected(IPAddress ipAddresses, int port)
|
|
{
|
|
if (this.ServerConnected != null)
|
|
{
|
|
this.ServerConnected(this, new TcpServerConnectedEventArgs(ipAddresses, port));
|
|
}
|
|
}
|
|
|
|
private void RaiseServerDisconnected(IPAddress ipAddresses, int port)
|
|
{
|
|
if (this.ServerDisconnected != null)
|
|
{
|
|
this.ServerDisconnected(this, new TcpServerDisconnectedEventArgs(ipAddresses, port));
|
|
}
|
|
}
|
|
|
|
private void RaiseServerExceptionOccurred(IPAddress ipAddresses, int port, Exception innerException)
|
|
{
|
|
if (this.ServerExceptionOccurred != null)
|
|
{
|
|
this.ServerExceptionOccurred(this, new TcpServerExceptionOccurredEventArgs(ipAddresses, port, innerException));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 发送报文
|
|
/// </summary>
|
|
/// <param name="datagram"></param>
|
|
public void Send(byte[] datagram)
|
|
{
|
|
if (datagram == null)
|
|
{
|
|
throw new ArgumentNullException("datagram");
|
|
}
|
|
if (!this.Connected)
|
|
{
|
|
this.RaiseServerDisconnected(this.Addresses, this.Port);
|
|
throw new InvalidProgramException("This client has not connected to server.");
|
|
}
|
|
this.tcpClient.GetStream().BeginWrite(datagram, 0, datagram.Length, new AsyncCallback(this.HandleDatagramWritten), this.tcpClient);
|
|
}
|
|
|
|
private void HandleDatagramWritten(IAsyncResult ar)
|
|
{
|
|
((TcpClient)ar.AsyncState).GetStream().EndWrite(ar);
|
|
}
|
|
|
|
public void Send(string datagram)
|
|
{
|
|
this.Send(this.Encoding.GetBytes(datagram));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 释放非托管资源
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
this.Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!this.disposed)
|
|
{
|
|
this.disposed = true;
|
|
|
|
if (disposing)
|
|
{
|
|
try
|
|
{
|
|
this.Close();
|
|
}
|
|
catch// (SocketException ex)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
} |