C# 上位机开发 - TCP Socket 通信(一篇搞定 建议收藏)
专栏导航 :上一篇:ModbusTCP主站实现 | 下一篇:无(本章完结)
前言
除 Modbus TCP 外,视觉相机、AGV、MES 接口常使用 裸 TCP JSON/二进制 。本文实现 AsyncTcpClient (粘包处理、自动重连)、TcpServer 简易多连接,以及面向设备的 NetworkDeviceService,与串口篇架构一致,便于上位机统一通信层。
TCP 粘包与拆包
TCP 是 流协议 ,无消息边界。策略同第三篇:定长 / 长度前缀 / 分隔符 / 协议状态机 。Modbus TCP 用 MBAP 的 Length 字段;自定义 JSON 常用 \n 或 Content-Length。
AsyncTcpClient
csharp
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace UpperComputer.Communication.Tcp
{
public class AsyncTcpClient : IDisposable
{
private TcpClient _client;
private NetworkStream _stream;
private readonly string _host;
private readonly int _port;
private CancellationTokenSource _cts;
private Task _receiveTask;
private int _reconnectDelayMs = 3000;
public event Action<byte[]> Received;
public event Action Connected;
public event Action Disconnected;
public bool AutoReconnect { get; set; } = true;
public AsyncTcpClient(string host, int port)
{
_host = host;
_port = port;
}
public async Task ConnectAsync()
{
_cts = new CancellationTokenSource();
await ConnectCoreAsync();
_receiveTask = Task.Run(() => ReceiveLoop(_cts.Token));
}
private async Task ConnectCoreAsync()
{
_client?.Dispose();
_client = new TcpClient();
await _client.ConnectAsync(_host, _port);
_stream = _client.GetStream();
Connected?.Invoke();
}
public async Task SendAsync(byte[] data)
{
if (_stream == null) throw new InvalidOperationException("未连接");
await _stream.WriteAsync(data, 0, data.Length);
}
private async Task ReceiveLoop(CancellationToken token)
{
var buffer = new byte[4096];
try
{
while (!token.IsCancellationRequested)
{
if (_stream == null || !_client.Connected)
{
if (!AutoReconnect) break;
await Task.Delay(_reconnectDelayMs, token);
try { await ConnectCoreAsync(); } catch { continue; }
}
int read = await _stream.ReadAsync(buffer, 0, buffer.Length, token);
if (read == 0)
{
Disconnected?.Invoke();
_stream = null;
continue;
}
var chunk = new byte[read];
Buffer.BlockCopy(buffer, 0, chunk, 0, read);
Received?.Invoke(chunk);
}
}
catch (OperationCanceledException) { }
}
public void Dispose()
{
_cts?.Cancel();
_stream?.Dispose();
_client?.Dispose();
}
}
}
粘包解析在 Received 订阅里接 FrameParser 或 ProtocolParser(第五篇)。
长度前缀拆包示例
csharp
public class LengthPrefixFramer
{
private readonly List<byte> _buf = new List<byte>();
public void Feed(byte[] data, Action<byte[]> onFrame)
{
_buf.AddRange(data);
while (_buf.Count >= 4)
{
int len = (_buf[0] << 24) | (_buf[1] << 16) | (_buf[2] << 8) | _buf[3];
if (_buf.Count < 4 + len) return;
onFrame(_buf.GetRange(4, len).ToArray());
_buf.RemoveRange(0, 4 + len);
}
}
}
TcpServer(多客户端)
csharp
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace UpperComputer.Communication.Tcp
{
public class TcpServer : IDisposable
{
private readonly TcpListener _listener;
private CancellationTokenSource _cts;
private readonly ConcurrentDictionary<string, TcpClient> _clients = new();
public event Action<string, byte[]> ClientDataReceived;
public TcpServer(int port)
{
_listener = new TcpListener(IPAddress.Any, port);
}
public void Start()
{
_cts = new CancellationTokenSource();
_listener.Start();
_ = AcceptLoop(_cts.Token);
}
private async Task AcceptLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
var client = await _listener.AcceptTcpClientAsync(token);
string id = client.Client.RemoteEndPoint.ToString();
_clients[id] = client;
_ = HandleClient(client, id, token);
}
}
private async Task HandleClient(TcpClient client, string id, CancellationToken token)
{
var stream = client.GetStream();
var buf = new byte[4096];
try
{
while (!token.IsCancellationRequested)
{
int n = await stream.ReadAsync(buf, 0, buf.Length, token);
if (n == 0) break;
var data = new byte[n];
Buffer.BlockCopy(buf, 0, data, 0, n);
ClientDataReceived?.Invoke(id, data);
}
}
finally
{
_clients.TryRemove(id, out _);
client.Dispose();
}
}
public void Dispose()
{
_cts?.Cancel();
_listener?.Stop();
foreach (var c in _clients.Values) c.Dispose();
}
}
}
NetworkDeviceService
csharp
public class NetworkDeviceService : IDisposable
{
private readonly AsyncTcpClient _client;
private readonly ProtocolParser _parser = new ProtocolParser();
public NetworkDeviceService(string ip, int port)
{
_client = new AsyncTcpClient(ip, port);
_client.Received += data =>
{
_parser.Feed(data);
while (_parser.TryReadFrame(out var frame))
OnFrame(frame);
};
}
public event Action<ProtocolFrame> FrameReceived;
protected virtual void OnFrame(ProtocolFrame frame) => FrameReceived?.Invoke(frame);
public Task StartAsync() => _client.ConnectAsync();
public Task SendAsync(ProtocolFrame frame) => _client.SendAsync(frame.ToBytes());
public void Dispose() => _client.Dispose();
}
将 TCP 传输 与 协议解析 分离,单元测试可 Mock Received 喂字节。
自动重连策略
| 策略 | 说明 |
|---|---|
| 固定间隔 | 3~5 s,简单 |
| 指数退避 | 1s→2s→30s 封顶,防风暴 |
| 心跳 | 定时 Ping,失败触发重连 |
| 状态机 | Disconnected / Connecting / Online |
重连成功后应 重新订阅/读初始参数,避免半连接状态。
FAQ
Q1:ReadAsync 返回 0?
对端关闭;触发重连或通知 UI。
Q2:Send 要不要 lock?
单线程发送可不加;多线程 SendAsync 应对 _stream lock 或 Channel 排队。
Q3:SSL/TLS?
SslStream 包装 NetworkStream,证书校验按 MES 要求配置。
Q4:与 WebSocket 选型?
浏览器 HMI 用 WebSocket;纯 C# 工控内网 TCP 更轻。
本章回顾
| 序号 | 主题 |
|---|---|
| 01 | SerialPort 基础 |
| 02 | DataReceived 线程安全 |
| 03 | 粘包与 RingBuffer |
| 04 | CRC16 |
| 05 | 自定义协议 AA55 |
| 06 | Modbus RTU |
| 07 | Modbus TCP |
| 08 | TCP Socket |
小结
AsyncTcpClient + 拆帧 + 重连 是网口上位机标配。通信基础章完结,后续可进入 OPC UA、MQTT、数据库落库 等专题。
专栏导航 :上一篇:ModbusTCP主站实现 | 下一篇:无(本章完结)