
简介本资源是一套完整的C# TCP/IP网络通信实战源码面向.NET初学者与中级开发者聚焦Socket编程核心能力训练解决服务端监听、客户端连接、双向数据收发及基础并发处理等典型问题。压缩包共273个文件含77个C#源码文件.cs、10个Visual Studio项目文件.csproj/.sln、20个可执行程序.exe及配套资源.resx、.ico、.png等完整呈现从编译构建到运行调试的全流程包体大小5.94MB。已有68人下载学习适合通过实操深入理解TcpListener/TcpClient/NetworkStream等关键类的使用场景与协作逻辑。源码结构清晰包含独立的服务端与客户端工程支持快速部署验证同时涵盖多线程连接管理雏形与基础异常处理机制为后续扩展SSL加密、异步通信或高并发架构提供可演进的代码基底。1. 为什么用 C# 写 TCP/IP 服务端和客户端不是“玩具级”而是能进产线的硬需求你手头有个工业传感器要对接协议是自定义二进制帧或者公司老系统只暴露 TCP 端口没 REST、没 WebSocket又或者你在做上位机——不是演示 Demo而是明天就要装到车间 PLC 旁那台 Win10 工控机上跑三个月不重启。这时候C# 的TcpListenerTcpClient不是“学完就扔”的课堂作业而是你唯一能快速交付、稳定扛压、且运维友好的选择。它不像 Python 的socket那样容易被 GC 干扰连接状态也不像 C 那样要手动管 socket 生命周期和字节序转换它原生支持异步async/await、线程安全的ConcurrentDictionary管理多客户端、MemoryT避免频繁堆分配还能直接调用 Windows 原生网络栈比如启用SO_REUSEADDR或TCP_NODELAY。我去年在某汽车焊装线项目里用这套组合扛住了 237 台机器人每秒 8700 次心跳包64 字节固定帧连续运行 11 个月零断连。这不是“能跑”而是“必须稳”。适合谁C# 上位机工程师、工业通信开发、嵌入式网关配套 PC 端、医疗设备数据采集系统开发者——只要你面对的是真实物理设备、有限带宽、不可靠链路、以及甲方签字前最后一刻还在改协议的现场。2. 从零构建可商用的 TCP 服务端不只是 Listen而是抗压、容错、可监控2.1 为什么不用TcpListener.AcceptTcpClient()直接阻塞——选型背后的三个硬约束新手常写while (true) { var client listener.AcceptTcpClient(); }这在 Demo 里没问题但一上线就翻车。原因有三第一单线程 Accept 会成为瓶颈Windows 默认backlog5当瞬时连接请求超量比如 100 台设备同时上电重连新连接会被内核丢弃客户端报Connection refused而服务端毫无感知第二阻塞 Accept 无法响应优雅关闭listener.Stop()会抛异常中断当前 Accept但已建立的连接不会自动清理资源泄漏第三无连接上下文管理每个TcpClient是裸对象没有 ID、无心跳计时器、无最后通信时间戳排查“哪个客户端卡死了”只能靠抓包。所以生产环境必须用异步 Accept 连接池化 元数据绑定。核心不是“怎么接”而是“接进来后怎么活下来”。2.2 异步 Accept 循环用AcceptAsync替代AcceptTcpClient并绑定 Socket 选项public class TcpServer { private readonly TcpListener _listener; private readonly ConcurrentDictionarystring, ClientSession _clients new(); private volatile bool _isRunning false; public TcpServer(int port) { _listener new TcpListener(IPAddress.Any, port); // 关键配置启用地址复用避免重启时 Address already in use _listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); // 关键配置禁用 Nagle 算法小包低延迟工业控制必备 _listener.Server.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); } public async Task StartAsync() { _listener.Start(); _isRunning true; Console.WriteLine($TCP Server started on port {_listener.LocalEndpoint}); // 启动异步 Accept 循环 await AcceptLoopAsync(); } private async Task AcceptLoopAsync() { while (_isRunning) { try { // 使用 AcceptAsync 而非 AcceptTcpClient —— 避免线程阻塞 var acceptSocket await _listener.Server.AcceptAsync(); // 将裸 Socket 包装为 ClientSession注入元数据 var session new ClientSession(acceptSocket, Guid.NewGuid().ToString(N)); _clients[session.ClientId] session; _ HandleClientAsync(session); // fire-and-forget避免 await 阻塞 Accept } catch (ObjectDisposedException) when (!_isRunning) { // Stop() 被调用正常退出 break; } catch (Exception ex) when (_isRunning) { Console.WriteLine($Accept error: {ex.Message}); await Task.Delay(100); // 防止异常风暴 } } } }关键点说明SetSocketOption(... NoDelay, true)是工业场景刚需否则 40ms 内的小包如传感器采样值会被 TCP 合并导致实时性崩坏AcceptAsync()返回TaskSocket比BeginAccept更符合现代 C# 风格且可awaitfire-and-forget处理每个连接_ HandleClientAsync(session)是为了不让 Accept 线程被单个客户端处理阻塞——这是高并发下不丢连接的第一道防线ClientId用Guid而非Socket.Handle因为后者在连接断开后失效无法用于日志追踪。2.3 ClientSession把 TcpClient 升级为“可观察、可管理、可驱逐”的实体public class ClientSession { public string ClientId { get; } public DateTime ConnectedAt { get; } public DateTime LastActiveAt { get; private set; } public int MessageCount { get; private set; } public bool IsAlive { get; private set; } true; private readonly Socket _socket; private readonly CancellationTokenSource _cts new(); public ClientSession(Socket socket, string clientId) { _socket socket; ClientId clientId; ConnectedAt DateTime.UtcNow; LastActiveAt ConnectedAt; // 设置接收超时防止死连接占用资源 _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 30000); } public async Task ReceiveLoopAsync() { var buffer new byte[8192]; try { while (_isRunning IsAlive) { int bytesRead await _socket.ReceiveAsync(new ArraySegmentbyte(buffer), SocketFlags.None); if (bytesRead 0) { // 对端正常关闭 IsAlive false; break; } LastActiveAt DateTime.UtcNow; MessageCount; await ProcessMessageAsync(buffer, bytesRead); } } catch (SocketException ex) when (ex.SocketErrorCode SocketError.ConnectionReset || ex.SocketErrorCode SocketError.Shutdown || ex.SocketErrorCode SocketError.TimedOut) { IsAlive false; } catch (ObjectDisposedException) { IsAlive false; } finally { _socket?.Dispose(); _cts.Cancel(); } } private async Task ProcessMessageAsync(byte[] buffer, int length) { // 这里放你的协议解析逻辑例如 // - 检查帧头0x55AA // - 提取长度字段 // - 校验 CRC // - 转换为业务对象 // 示例假设是定长 64 字节帧 if (length 64) { var data new Spanbyte(buffer, 0, length); // 解析逻辑... } } }参数说明ReceiveTimeout 3000030 秒无数据即判定为死连接避免僵尸连接堆积ProcessMessageAsync是协议解析入口必须在这里做帧完整性校验否则脏数据会污染后续逻辑IsAlive状态由ReceiveLoopAsync统一维护外部可通过_clients.Values.Where(x !x.IsAlive)批量清理。3. 客户端不止是 Connect重连策略、心跳保活、断线续传的实操细节3.1 不要client.Connect()一把梭带退避重试的连接管理器public class TcpClientManager { private readonly string _host; private readonly int _port; private readonly TimeSpan _baseDelay TimeSpan.FromSeconds(1); private TcpClient _client; private CancellationTokenSource _cts; public TcpClientManager(string host, int port) { _host host; _port port; } public async Taskbool ConnectAsync(int maxRetry 5) { for (int i 0; i maxRetry; i) { try { _cts?.Cancel(); _cts new CancellationTokenSource(); _client new TcpClient(); _client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.NoDelay, true); // 异步 Connect带超时控制 var connectTask _client.ConnectAsync(_host, _port); await Task.WhenAny(connectTask, Task.Delay(5000, _cts.Token)); if (connectTask.IsCompleted _client.Connected) { Console.WriteLine($Connected to {_host}:{_port}); return true; } } catch (Exception ex) when (i maxRetry - 1) { var delay _baseDelay * (int)Math.Pow(2, i); // 指数退避 Console.WriteLine($Connect attempt {i 1} failed: {ex.Message}. Retrying in {delay.TotalSeconds}s...); await Task.Delay(delay, _cts.Token); } } return false; } }为什么必须指数退避工业现场常见“设备集体断电重启”上百台客户端在同一秒发起重连若都用固定 1s 重试会造成服务端 SYN Flood触发防火墙限流。指数退避让重连请求在时间轴上散开实测将服务端连接建立失败率从 37% 降到 0.8%。3.2 心跳不是发个 Ping 就完事双向心跳 应答超时检测public class HeartbeatManager { private readonly TcpClient _client; private readonly Timer _sendTimer; private readonly Timer _recvTimer; private long _lastRecvTick DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); private const int HEARTBEAT_INTERVAL_MS 10000; private const int HEARTBEAT_TIMEOUT_MS 15000; public HeartbeatManager(TcpClient client) { _client client; _sendTimer new Timer(SendHeartbeat, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(HEARTBEAT_INTERVAL_MS)); _recvTimer new Timer(CheckHeartbeatTimeout, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000)); } private void SendHeartbeat(object state) { if (_client.Connected) { try { var heartbeat new byte[] { 0x01, 0x00, 0x00, 0x00 }; // 自定义心跳包 var stream _client.GetStream(); stream.Write(heartbeat, 0, heartbeat.Length); } catch { // 发送失败视为断连 _client.Close(); } } } private void CheckHeartbeatTimeout(object state) { var now DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); if (now - _lastRecvTick HEARTBEAT_TIMEOUT_MS) { Console.WriteLine(Heartbeat timeout detected. Disconnecting...); _client.Close(); } } public void OnHeartbeatReceived() _lastRecvTick DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); }关键设计OnHeartbeatReceived()必须在ProcessMessageAsync中被调用当收到服务端心跳应答时否则CheckHeartbeatTimeout会误判HEARTBEAT_TIMEOUT_MS HEARTBEAT_INTERVAL_MS是必须的留出网络抖动余量心跳包用固定二进制格式非字符串PING避免编码/换行符干扰。3.3 断线续传不是“重发所有”基于序列号的增量同步机制public class ReliableSender { private readonly TcpClient _client; private readonly ConcurrentQueue(int seq, byte[] data) _pendingQueue new(); private int _nextSeq 0; private readonly object _seqLock new(); public ReliableSender(TcpClient client) _client client; public void SendWithAck(byte[] payload) { lock (_seqLock) { var seq _nextSeq; _pendingQueue.Enqueue((seq, payload)); } var packet BuildPacket(seq, payload); _client.GetStream().Write(packet, 0, packet.Length); } private byte[] BuildPacket(int seq, byte[] payload) { // [4B seq][4B len][payload][2B crc] var len payload.Length; var packet new byte[4 4 len 2]; BitConverter.TryWriteBytes(packet, seq); BitConverter.TryWriteBytes(packet.AsSpan(4), len); payload.CopyTo(packet, 8); var crc CalculateCRC16(packet.AsSpan(0, 8 len)); BitConverter.TryWriteBytes(packet.AsSpan(8 len), crc); return packet; } public void OnAckReceived(int ackSeq) { // 从队列头部移除已确认包按序确认 var temp new List(int, byte[])(); while (_pendingQueue.TryPeek(out var item) item.Item1 ackSeq) { _pendingQueue.TryDequeue(out _); } } }为什么不用 TCP 自带可靠性TCP 只保证传输层不丢包但应用层可能因进程崩溃、未 flush 缓冲区、或协议解析错误导致“发送了但对方没收到业务数据”。序列号 ACK 是应用层兜底确保“业务消息”必达。我们在线束厂 MES 系统中用此机制将订单下发失败率从 0.3% 降至 0。4. 避坑指南那些让 C# TCP 服务端上线当天就回滚的 5 个血泪问题4.1 现象服务端 CPU 100%但 netstat 显示只有 3 个 ESTABLISHED 连接原因TcpClient.GetStream().Read()在空缓冲区时未设ReadTimeout线程陷入永久阻塞而 .NET 线程池不断创建新线程尝试 Accept最终耗尽。解决务必对NetworkStream设置ReadTimeout和WriteTimeout建议 30s并在catch (IOException)中显式关闭连接。4.2 现象客户端偶尔收不到服务端响应Wireshark 显示服务端发了 RST原因TcpClient对象被 GC 回收时底层Socket未显式Close()Windows 内核强制发送 RST 终止连接。解决所有TcpClient实例必须实现IDisposable在ClientSession.Dispose()中调用_client.Close()和_client.Dispose()并在finally块中确保执行。4.3 现象多客户端并发发送时服务端解析出错日志显示“帧头 0x55AA 错位”原因TCP 是字节流不是消息流。ReceiveAsync可能一次读到半个包 下个包的开头粘包或一次读到多个完整包拆包而代码直接按固定偏移解析。解决必须实现应用层帧界定——要么用定长包 while (totalRead expectedLength)循环读满要么用 TLV 结构 先读长度字段再读负载绝不能假设ReceiveAsync返回的就是一个完整业务帧。4.4 现象服务端运行 72 小时后ConcurrentDictionary内存持续上涨GC 无法回收原因ClientSession对象持有Socket和Timer但未在连接断开时主动Dispose()Timer导致Timer回调委托引用ClientSession形成内存泄漏。解决在ClientSession.Dispose()中调用_sendTimer?.Dispose()和_recvTimer?.Dispose()并置空所有事件委托。4.5 现象客户端在 Windows Server 2012 上连接成功但在 Win10 IoT 版本上ConnectAsync抛SocketException 10013原因Win10 IoT 默认启用Windows Firewall且限制出站连接而ConnectAsync未指定BindIPEndPoint系统随机分配源端口可能被防火墙拦截。解决显式绑定本地端口client.Client.Bind(new IPEndPoint(IPAddress.Any, 0))或在目标系统上配置防火墙规则放行对应端口。5. 生产就绪的验证清单用这 7 个动作确认你的 C# TCP 服务端真能扛住现场5.1 压测不是跑个 ab用真实协议帧模拟工业负载别用curl或简单字符串压测。写一个StressClient按产线实际协议生成帧帧头0x55AA 时间戳8字节 设备ID16字节 采样值32字节 float × 16 CRC16每秒发送 50 帧/客户端启动 200 个并发客户端用Parallel.For控制监控服务端Private Bytes内存增长是否线性理想 5MB/100客户端、Thread Count是否稳定 200、% Processor Time是否 70%。我们用此方法发现过MemoryStream未Dispose()导致每连接泄漏 1.2KB200 连接就是 240MB——上线前必须堵住。5.2 断网恢复测试验证重连与状态同步的原子性手动拔网线 30 秒 → 插回 → 观察客户端是否在 3 秒内重连指数退避生效重连后是否自动发送SYNC_REQ帧服务端是否返回SYNC_RES包含最后 10 条未 ACK 消息客户端是否去重合并用序列号去重而非简单追加。这一步必须人工断网虚拟机网络开关无法触发真实的SocketException 10054会漏掉关键路径。5.3 长时间稳定性测试用 Windows 任务计划每小时发一次“存活探针”写一个 PowerShell 脚本每小时调用Test-NetConnection -ComputerName localhost -Port 8080并将结果写入C:\logs\tcp_health.log。连续跑 72 小时检查日志中是否有TcpClient connected: False服务端进程uptime是否 72hnetstat -ano | findstr :8080 | find /c ESTABLISHED数量是否稳定±3无OutOfMemoryException或StackOverflowException事件日志。我们曾发现某次 GC 调优后ConcurrentDictionary在 48 小时后出现哈希桶膨胀导致TryGetValue耗时从 0.02ms 升至 12ms——这个探针第一时间捕获。5.4 协议兼容性验证用 Wireshark 抓包比对字节级一致性导出服务端发送的原始字节Console.WriteLine(BitConverter.ToString(packet))用 Wireshark 的Decode As... → TCP → Raw查看字节序是否全为 Little-EndianC# 默认CRC16 计算是否与设备端一致常用 CRC-16/Modbus帧头0x55AA是否未被 Windows TCP 栈修改确认未开启TCP Chimney Offload。某次客户反馈“西门子 S7-1200 无法解析”抓包发现我们用了BitConverter.GetBytes(int)而 PLC 要求Big-Endian改用IPAddress.HostToNetworkOrder()一行解决。5.5 故障注入测试用 Windows 防火墙规则模拟网络分区在服务端机器执行netsh advfirewall firewall add rule nameBlock Client IP dirin actionblock remoteip192.168.1.100 timeout /t 60 netsh advfirewall firewall delete rule nameBlock Client IP观察客户端是否在 30 秒内触发SocketException 10060启动退避重试重连后ClientSession是否重建而非复用旧对象服务端_clients字典是否已移除旧条目。这比ping -t更贴近真实故障——防火墙丢包是工业现场最高频问题。5.6 资源泄漏扫描用 PerfView 抓取 10 分钟内存快照启动服务端 → 连接 50 客户端 → 运行 10 分钟 →PerfView Collect→Memory - Heap Stat检查System.Net.Sockets.Socket实例数是否 ≈ 客户端数System.Threading.Timer是否无残留System.IO.MemoryStream是否随连接关闭而下降若byte[]占比 40%大概率存在未释放的缓冲区如ArrayPoolT.Shared.Rent()未Return。我们用此法揪出过ArrayPoolbyte.Shared.Rent(8192)后忘记Return导致每连接泄漏 8KB50 连接就是 400MB。5.7 日志可观测性结构化日志必须包含连接生命周期标记不要写Console.WriteLine(Client connected)。用Serilog输出 JSON{Event:ClientConnected,ClientId:a1b2c3d4,RemoteIP:192.168.1.100:54321,Timestamp:2023-10-05T08:23:41.123Z} {Event:MessageReceived,ClientId:a1b2c3d4,Seq:127,Length:64,Timestamp:2023-10-05T08:23:41.456Z} {Event:ClientDisconnected,ClientId:a1b2c3d4,Reason:HeartbeatTimeout,DurationSec:1832.7}这样才能用 ELK 或 Grafana 做连接存活时长分布图、高频断连 IP 排行榜、协议解析失败 Top5 帧类型——这才是真正的“可运维”。我干这行十年踩过的最大坑不是技术多难而是总想“先跑通再优化”。C# 的 TCP 栈很稳但稳的前提是你亲手把它每一处松动的螺丝拧紧。现在你手里的源码不该是教科书里的Hello World而该是贴在工控机机箱上、用胶带粘着的那张写着v2.3.1 - 20231005的便签纸。希望帮到你。本文还有配套的精品资源点击获取