TCP Socket 通信

C# 上位机开发 - TCP Socket 通信(一篇搞定 建议收藏)

专栏导航上一篇:ModbusTCP主站实现 | 下一篇:无(本章完结)


前言

除 Modbus TCP 外,视觉相机、AGV、MES 接口常使用 裸 TCP JSON/二进制 。本文实现 AsyncTcpClient (粘包处理、自动重连)、TcpServer 简易多连接,以及面向设备的 NetworkDeviceService,与串口篇架构一致,便于上位机统一通信层。

TCP 粘包与拆包

TCP 是 流协议 ,无消息边界。策略同第三篇:定长 / 长度前缀 / 分隔符 / 协议状态机 。Modbus TCP 用 MBAP 的 Length 字段;自定义 JSON 常用 \nContent-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 订阅里接 FrameParserProtocolParser(第五篇)。

长度前缀拆包示例

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主站实现 | 下一篇:无(本章完结)