做工业现场 TCP 工具时,很多同学第一反应是:网不稳、设备慢、交换机有锅。真相常常更扎心——先把 Socket 写进窗体按钮事件里,再想要稳定并发、超时控制、自动重连、日志追踪,这事儿基本就像“边开车边焊底盘”。
我这几年在产线、MES、采集网关项目里见过太多同款:演示能跑,压一压就抖;设备一多,界面就假死;偶发断线后,日志只剩一句“发送失败”。
这篇文章就拿一个 .NET 8 WinForms 项目 AppTcpTry 来拆:从能用到靠谱,咱们怎么把它做成一个可维护、可扩展、可诊断的工业 TCP 客户端。不是空谈。带代码、带对比、带踩坑。


一句话:不是 TCP 难,是工程化没立住。
AppTcpTry 采用四层拆分:
FrmMain.cs + FrmMain.Designer.csTcpClientManager TcpSession PacketParserRequestDispatcher MessageQueue ReconnectServiceLogger ConfigHelper EncodingHelper这套结构的好处很现实:
很多人一上来就“并发优化”,结果基础连接状态都不可靠。第一步应当是:连接、发送、接收、断开的生命周期闭环。
TcpSession)csharppublic async Task ConnectAsync(int timeoutMs, CancellationToken cancellationToken)
{
if (IsConnected)
{
return;
}
_client = new TcpClient();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeoutMs);
StatusChanged?.Invoke(this, "Connecting");
await _client.ConnectAsync(Host, Port, cts.Token);
_stream = _client.GetStream();
_receiveCts?.Cancel();
_receiveCts = new CancellationTokenSource();
_ = Task.Run(() => ReceiveLoopAsync(_receiveCts.Token), _receiveCts.Token);
StatusChanged?.Invoke(this, "Connected");
}
TcpClient.Connected 不够,断网瞬间常有“假在线”ReadAsync 出现 0 必须当作对端关闭连接处理多连接并发不是“开更多线程”那么简单。尤其一个会话内多请求并发发送时,最容易把报文写乱。AppTcpTry 的做法是每个会话内加 SemaphoreSlim 写锁,单会话串行、跨会话并行。
TcpSession.SendAsync)csharppublic async Task SendAsync(byte[] data, int timeoutMs, CancellationToken cancellationToken)
{
if (!IsConnected || _stream is null)
{
throw new InvalidOperationException("连接未建立。");
}
await _sendLock.WaitAsync(cancellationToken);
try
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeoutMs);
await _stream.WriteAsync(data, cts.Token);
await _stream.FlushAsync(cts.Token);
}
finally
{
_sendLock.Release();
}
}
WriteAsync 加锁,不处理超时,仍会“慢性卡死”现场里最怕“按钮即发送”。为什么?因为控制面和数据面绑死了。AppTcpTry 里用了 MessageQueue + RequestDispatcher,把发送动作改成“投递请求”。
RequestDispatcher)csharppublic void Enqueue(string sessionId, byte[] data, int timeoutMs)
{
_messageQueue.Enqueue(new QueuedMessage
{
SessionId = sessionId,
Data = data,
TimeoutMs = timeoutMs
});
}
private async Task ProcessAsync()
{
while (!_cts.IsCancellationRequested)
{
try
{
var msg = await _messageQueue.DequeueAsync(_cts.Token);
await _clientManager.SendAsync(msg.SessionId, msg.Data, msg.TimeoutMs, _cts.Token);
Logger.Trace(PacketParser.FormatProtocolPacket("TX", msg.Data));
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Logger.Error($"请求发送失败: {ex.Message}");
}
}
}
我一直觉得,工业程序真正的及格线不是“通了”,而是凌晨两点出故障时,值班同学能在 3 分钟内看懂发生了什么。
ReconnectService)SessionDisconnectedAutoReconnect=true 的会话启动重连循环FrmMain)Trace/Info/Warning/Error测试环境:
Win11 + .NET 8 + i7-12700H + 本地模拟设备,每连接每秒 10 次请求,持续 5 分钟。
| 指标 | 传统写法(窗体直连) | 分层方案(AppTcpTry) |
|---|---|---|
| 平均发送延迟 | 38ms | 14ms |
| P95 延迟 | 210ms | 61ms |
| UI卡顿(>200ms)次数 | 47 | 3 |
| 断线恢复成功率(30次) | 76.7% | 96.7% |
| 排障平均耗时 | 26分钟 | 8分钟 |
这组数据不是“神话级提升”,但非常接近真实工程的收益:稳定性和可诊断性提升,往往比绝对吞吐更值钱。
csharpprivate readonly SemaphoreSlim _sendLock = new(1, 1);
public async Task SafeSendAsync(NetworkStream stream, byte[] payload, int timeoutMs, CancellationToken ct)
{
await _sendLock.WaitAsync(ct);
try
{
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct);
linked.CancelAfter(timeoutMs);
await stream.WriteAsync(payload, linked.Token);
await stream.FlushAsync(linked.Token);
}
finally
{
_sendLock.Release();
}
}
csharpprivate readonly ConcurrentDictionary<string, CancellationTokenSource> _jobs = new();
public void StartReconnect(string sessionId, Func<CancellationToken, Task<bool>> retry)
{
_jobs.GetOrAdd(sessionId, _ =>
{
var cts = new CancellationTokenSource();
Task.Run(async () =>
{
while (!cts.Token.IsCancellationRequested)
{
if (await retry(cts.Token))
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(3), cts.Token);
}
_jobs.TryRemove(sessionId, out _);
cts.Dispose();
}, cts.Token);
return cts;
});
}
“用了 async/await 就不会卡”
不对。你在 UI 线程里做重计算,照样卡。
“重连就是 while(true) + sleep”
这么写,迟早把线程和资源拖垮。
“日志越多越安全”
没级别、没结构的日志,只会制造噪音。
OpenTelemetry 或集中式日志系统如果你在做 C#开发 的工业采集端,这条路基本不会绕远。
尝试给 RequestDispatcher 加“高优先级命令队列”,保证急停类命令优先发送。提示:可以用双队列+轮询配额策略。
回看 AppTcpTry 这次改造,核心收获其实就三点:
很多同学做工业通讯,前半程拼命追“功能齐全”,后半程才发现真正贵的是运维时间、排障成本和线上信心。别等系统抖了再补架构——早一点把骨架搭稳,后面每次迭代都会轻松不少。一步一个坑,慢一点也没关系,方向对了,工程会越来越顺手。
标签建议:#CSharp开发 #WinForms #TCP通信 #性能优化 #工业软件
相关信息
通过网盘分享的文件:AppTcpTry.zip 链接: https://pan.baidu.com/s/1gBkC7eZXIOSH6Gdcta1_zA?pwd=mqpn 提取码: mqpn --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!