Files
ADP/DeviceCommand/Base/TCP.cs
2026-06-25 09:13:44 +08:00

240 lines
8.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Model.Models;
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Base
{
public class Tcp : ITcp, IDisposable
{
public string IPAddress { get; set; } = "127.0.0.1";
public int Port { get; set; } = 502;
public int SendTimeout { get; set; } = 3000;
public int ReceiveTimeout { get; set; } = 3000;
private TcpClient _tcpClient;
public bool IsConnected => _tcpClient?.Connected ?? false;
protected readonly SemaphoreSlim _commLock = new(1, 1);
public Tcp()
{
_tcpClient = new TcpClient();
}
/// <summary>
/// 通过 <see cref="TcpConfig"/> 一次性配置 TCP 通信参数。
/// </summary>
public Tcp(TcpConfig config) : this()
{
if (config == null) return;
ConfigureDevice(config.IPAddress, config.Port, config.SendTimeout, config.ReceiveTimeout);
}
public void ConfigureDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
IPAddress = ipAddress;
Port = port;
SendTimeout = sendTimeout;
ReceiveTimeout = receiveTimeout;
}
public virtual async Task<bool> ConnectAsync(CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
return await ResetConnectionAsync(ct);
}
finally
{
_commLock.Release();
}
}
/// <summary>
/// 核心内部方法:无锁状态下安全重置并重建连接
/// </summary>
private async Task<bool> ResetConnectionAsync(CancellationToken ct)
{
if (_tcpClient != null)
{
_tcpClient.Close();
_tcpClient.Dispose();
}
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(IPAddress, Port, ct).ConfigureAwait(false);
return true;
}
public virtual void Close()
{
if (_tcpClient?.Connected == true) _tcpClient.Close();
}
private async Task LoglessSendAsync(byte[] buffer, CancellationToken ct)
{
if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
NetworkStream stream = _tcpClient.GetStream();
try
{
if (SendTimeout > 0)
{
// 使用 WaitAsync 控制发送超时
await stream.WriteAsync(buffer, 0, buffer.Length, ct)
.WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct)
.ConfigureAwait(false);
}
else
{
await stream.WriteAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
}
}
catch (TimeoutException ex)
{
// 发送超时,安全起见直接断开重建
await ResetConnectionAsync(ct);
throw new TimeoutException($"TCP 发送数据超时({SendTimeout} ms连接已重置以确保安全。", ex);
}
}
public async Task SendAsync(byte[] buffer, CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
await LoglessSendAsync(buffer, ct);
}
finally
{
_commLock.Release();
}
}
public async Task SendAsync(string str, CancellationToken ct = default)
{
await SendAsync(Encoding.UTF8.GetBytes(str), ct);
}
public async Task<byte[]> ReadAsync(int length, CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
NetworkStream stream = _tcpClient.GetStream();
byte[] buffer = new byte[length];
int offset = 0;
try
{
while (offset < length)
{
ct.ThrowIfCancellationRequested();
// 计算剩余超时时间(这里简单使用配置值,若要求极精准可加入不长计算)
int read = ReceiveTimeout > 0
? await stream.ReadAsync(buffer, offset, length - offset, ct)
.WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct)
.ConfigureAwait(false)
: await stream.ReadAsync(buffer, offset, length - offset, ct).ConfigureAwait(false);
if (read == 0) throw new IOException("远程主机已关闭连接");
offset += read;
}
return offset == 0 ? Array.Empty<byte>() : buffer[..offset];
}
catch (TimeoutException ex)
{
// 关键安全重置WaitAsync 超时后抛弃老连接,防止未完成的 Read 污染后续 Buffer
await ResetConnectionAsync(ct);
throw new TimeoutException($"TCP 读取定长数据超时({ReceiveTimeout} ms连接已重置避免数据错乱。", ex);
}
}
finally
{
_commLock.Release();
}
}
private async Task<string> LoglessReadAsync(string delimiter, CancellationToken ct)
{
if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
delimiter ??= "\n";
var sb = new StringBuilder();
byte[] buffer = new byte[1024];
NetworkStream stream = _tcpClient.GetStream();
try
{
while (true)
{
ct.ThrowIfCancellationRequested();
int bytesRead = ReceiveTimeout > 0
? await stream.ReadAsync(buffer, 0, buffer.Length, ct)
.WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct)
.ConfigureAwait(false)
: await stream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
if (bytesRead == 0) throw new IOException("远程主机已关闭连接");
sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
string currentText = sb.ToString();
int index = currentText.IndexOf(delimiter, StringComparison.Ordinal);
if (index >= 0)
{
return currentText.Substring(0, index).Trim();
}
}
}
catch (TimeoutException ex)
{
// 当 SCPI 仪表由于命令发错不响应时WaitAsync 会立功并抛出 TimeoutException
// 此时我们在内部秒速“重置连接”,上层业务只会收到一个干净的超时报错,但由于锁和自动重连,下一次通信直接恢复!
await ResetConnectionAsync(ct);
throw new TimeoutException($"SCPI 读取超时(未收到结束符 '{delimiter}',等待:{ReceiveTimeout} ms通信链路已自动刷新。", ex);
}
}
public async Task<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
return await LoglessReadAsync(delimiter, ct);
}
finally
{
_commLock.Release();
}
}
public async Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
await LoglessSendAsync(Encoding.UTF8.GetBytes(command), ct);
return await LoglessReadAsync(delimiter, ct);
}
finally
{
_commLock.Release();
}
}
public void Dispose()
{
_tcpClient?.Dispose();
_commLock?.Dispose();
}
}
}