
简介这份资源面向具备一定C#基础、希望掌握TCP网络通信的开发者围绕TcpClient类讲解客户端发送与接收数据的完整实现。内容涵盖命名空间导入、连接远程服务器、通过NetworkStream读写字节流、关闭连接释放资源并延伸至异常处理与BeginConnect、BeginRead等异步操作思路适合网络编程入门与实战练习。压缩包共31个文件以9个cs源码文件为核心配合sln解决方案、csproj工程文件、config配置、resx资源与exe可执行程序等整体约51KB结构紧凑便于直接运行调试。目前已有7156人学习下载说明其参考价值获得较多认可。借助其中的示例代码读者可对照理解连接建立、数据编解码与缓冲区读取等关键环节并在此基础上扩展异步通信与异常重连逻辑为构建稳定的TCP客户端应用打下基础。1. 从一次“连上了却收不到数据”的调试说起如果你正在用 C# 写上位机、设备网关或者内网工具大概率绕不开TcpClient。我见过太多人第一次写 TCP 通信时代码跑起来显示连接成功可数据就是收不到或者收一半卡死最后怀疑是“网络玄学”。其实问题往往出在粘包处理、异步读取和超时设置这几个点上。这份资源围绕c# tcpClient的发送和接收展开把连接建立、数据收发、异常断开、心跳保活这几件事拆成了可以直接抄的代码片段。它适合两类人一类是刚接触 Socket 编程、想找一个能跑通的最小闭环的新手另一类是在项目里维护老旧通信模块、需要快速补上断线重连和粘包处理的熟手。下面我按“先跑通、再拆解、最后改造”的顺序把这份资源里的关键实现讲清楚。2. 环境准备与最小可运行示例把连接、发送、接收串起来2.1 项目类型与依赖选择这份资源基于 .NET 平台推荐用 .NET 6 或 .NET 8 的控制台项目来验证因为TcpClient在 .NET Core 之后跨平台支持已经比较稳定。如果你维护的是 .NET Framework 4.x 的老项目代码结构基本一致只是异步 API 的写法略有差别。新建项目时不需要额外 NuGet 包System.Net.Sockets是标准库自带的。我一般会先建一个控制台程序把发送端和接收端放在两个不同的进程里跑这样能直观看到数据流向而不是在一个进程里自问自答。dotnet new console -n TcpDemo cd TcpDemo dotnet add package System.Net.Sockets上面这条dotnet add package其实可以省略因为命名空间已经内置。写出来是为了提醒新手不要看到TcpClient就以为要装第三方库。真正需要额外引入的通常是日志组件或者 JSON 序列化库通信本身不依赖它们。2.2 服务端监听与客户端连接先看服务端。资源里给的是一个最简监听循环绑定本机端口等待客户端接入。这里有个细节TcpListener的Start()只是开始监听真正拿到连接要靠AcceptTcpClientAsync()。很多人写成同步的AcceptTcpClient()结果界面直接卡死这就是没有理解阻塞调用的代价。// 服务端监听指定端口接受一个客户端连接 var listener new TcpListener(IPAddress.Any, 9000); listener.Start(); Console.WriteLine(服务端已启动等待连接...); // 异步等待避免阻塞主线程 using var client await listener.AcceptTcpClientAsync(); Console.WriteLine($客户端已接入{client.Client.RemoteEndPoint}); // 拿到网络流后续收发都基于它 NetworkStream stream client.GetStream();IPAddress.Any表示监听本机所有网卡地址端口 9000 是示例值实际项目里建议从配置文件读取。AcceptTcpClientAsync()返回的TcpClient对象封装了底层 SocketGetStream()拿到的是双向流既能读也能写。注意using关键字它保证连接释放时底层句柄被回收否则反复连接会耗尽端口。客户端这边对应的是ConnectAsync()。资源里特意强调了连接超时的问题默认的ConnectAsync在某些网络环境下可能等很久才抛异常所以最好配合CancellationToken做超时控制。// 客户端连接服务端设置 3 秒超时 using var client new TcpClient(); using var cts new CancellationTokenSource(TimeSpan.FromSeconds(3)); try { await client.ConnectAsync(127.0.0.1, 9000, cts.Token); Console.WriteLine(连接成功); } catch (OperationCanceledException) { Console.WriteLine(连接超时请检查服务端是否启动); }CancellationTokenSource传入 3 秒超时后ConnectAsync会抛出OperationCanceledException。这里不要用Socket.Connect的同步版本去硬等异步加取消令牌是目前比较稳妥的写法。连接成功后客户端和服务端就各自持有一个NetworkStream接下来就是收发数据。2.3 发送与接收的基本写法发送数据用stream.WriteAsync()接收用stream.ReadAsync()。资源里给的是一个循环读取的模板因为 TCP 是流式协议一次ReadAsync不保证拿到完整消息。// 发送把字符串转成字节数组写入流 string message hello tcp; byte[] sendBuffer Encoding.UTF8.GetBytes(message); await stream.WriteAsync(sendBuffer, 0, sendBuffer.Length); Console.WriteLine($已发送{message}); // 接收循环读取直到对方关闭连接 byte[] recvBuffer new byte[1024]; int bytesRead; while ((bytesRead await stream.ReadAsync(recvBuffer, 0, recvBuffer.Length)) 0) { string received Encoding.UTF8.GetString(recvBuffer, 0, bytesRead); Console.WriteLine($收到{received}); } Console.WriteLine(连接已断开);WriteAsync的第三个参数是写入长度这里就是整个字节数组。ReadAsync返回实际读到的字节数如果返回 0说明对方正常关闭了连接。注意Encoding.UTF8.GetString只转换实际读到的部分不要用整个recvBuffer否则会带出上一次残留的数据。这个最小闭环跑通后你就能看到客户端发一句、服务端收一句的效果。但真实项目里不会这么简单下一章就讲粘包、断线和并发的问题。3. 核心模块拆解粘包处理、心跳保活与断线重连3.1 粘包与半包长度前缀法的实现TCP 是字节流没有消息边界。你发两次 “abc” 和 “def”对方可能一次收到 “abcdef”也可能分两次收到 “ab” 和 “cdef”。资源里采用的是长度前缀法先发 4 个字节表示消息体长度再发消息体。接收端先读 4 字节解析出长度再按长度读满整个消息。// 发送带长度前缀的消息 async Task SendMessageAsync(NetworkStream stream, string message) { byte[] body Encoding.UTF8.GetBytes(message); byte[] lengthPrefix BitConverter.GetBytes(body.Length); // 4 字节长度头 // 先发长度头再发消息体 await stream.WriteAsync(lengthPrefix, 0, lengthPrefix.Length); await stream.WriteAsync(body, 0, body.Length); } // 接收端先读 4 字节长度头再读消息体 async Taskstring ReadMessageAsync(NetworkStream stream) { byte[] lengthBuffer new byte[4]; int totalRead 0; // 确保读满 4 字节长度头 while (totalRead 4) { int read await stream.ReadAsync(lengthBuffer, totalRead, 4 - totalRead); if (read 0) throw new IOException(连接已关闭); totalRead read; } int bodyLength BitConverter.ToInt32(lengthBuffer, 0); byte[] bodyBuffer new byte[bodyLength]; totalRead 0; // 确保读满整个消息体 while (totalRead bodyLength) { int read await stream.ReadAsync(bodyBuffer, totalRead, bodyLength - totalRead); if (read 0) throw new IOException(连接已关闭); totalRead read; } return Encoding.UTF8.GetString(bodyBuffer); }BitConverter.GetBytes把 int 转成 4 字节数组默认是小端序收发两端保持一致即可。两个while循环是关键ReadAsync可能只返回部分数据必须循环读到指定长度。如果省略这个循环就会遇到“半包”问题表现为消息解析到一半报错。资源里还提醒长度头本身也可能分两次到达所以第一个循环同样不能省。3.2 心跳机制判断对方是否还活着TCP 连接建立后如果网线被拔掉双方可能都感知不到直到下一次发送数据才发现异常。资源里加了一个心跳包客户端每隔 5 秒发一个固定格式的短消息服务端收到后回复一个确认。如果连续 3 次没收到确认就判定连接失效主动关闭并触发重连。// 心跳发送循环 async Task HeartbeatLoopAsync(NetworkStream stream, CancellationToken token) { while (!token.IsCancellationRequested) { try { await SendMessageAsync(stream, PING); await Task.Delay(5000, token); // 每 5 秒一次 } catch (Exception ex) { Console.WriteLine($心跳失败{ex.Message}); break; // 跳出循环交给外层重连 } } }Task.Delay传入取消令牌外部取消时能及时退出。心跳消息本身也走长度前缀协议所以服务端收到 “PING” 后回复 “PONG”。这里要注意心跳不能和业务消息共用一个发送锁否则业务繁忙时心跳被阻塞会误判掉线。常见做法是给发送操作加一个SemaphoreSlim保证同一时刻只有一个写入但心跳和业务消息按优先级排队。3.3 断线重连指数退避与状态管理断线重连不是简单地while(true)里反复ConnectAsync那样会在服务端没恢复时疯狂重试把资源耗尽。资源里用的是指数退避第一次等 1 秒第二次 2 秒第三次 4 秒最多等到 30 秒并且每次重连成功后重置计数。// 带指数退避的重连逻辑 async Task ReconnectAsync(TcpClient client, string host, int port) { int retryDelay 1000; // 初始 1 秒 const int maxDelay 30000; // 最大 30 秒 while (true) { try { await client.ConnectAsync(host, port); Console.WriteLine(重连成功); return; // 成功后退出重连循环 } catch (Exception ex) { Console.WriteLine($重连失败{ex.Message}{retryDelay / 1000} 秒后重试); await Task.Delay(retryDelay); retryDelay Math.Min(retryDelay * 2, maxDelay); // 翻倍但不超过上限 } } }Math.Min保证延迟不会无限增长。实际项目里还会加一个最大重试次数超过后通知上层人工介入。另外重连前要先把旧的TcpClient释放掉否则句柄泄漏。资源里建议把连接状态封装成一个枚举Disconnected、Connecting、Connected、Reconnecting界面或日志根据状态变化做提示避免用户看到“假在线”。4. 避坑与排查那些让 TCP 连接“假死”的细节4.1 现象连接成功但收不到数据日志停在 ReadAsync原因通常是接收端没有循环读取或者读取缓冲区太小导致半包被丢弃。解决方法是按 3.1 的长度前缀法先读长度头再读消息体并且每次ReadAsync都检查返回值。如果返回值是 0说明对方关闭了连接不要继续循环。4.2 现象发送大量数据时程序卡死或内存暴涨原因是在 UI 线程上同步调用了Write或者没有做发送队列导致数据堆积。解决方法是所有收发都走异步并且给发送加一个Channel或有界队列超过容量时丢弃或等待。资源里用SemaphoreSlim控制并发写入避免多个线程同时写同一个流。4.3 现象服务端重启后客户端一直连不上原因是客户端没有处理SocketException或者重连延迟太短导致端口被占用。解决方法是捕获具体异常码比如ConnectionRefused时等待后重试AddressAlreadyInUse时先释放旧连接。指数退避能缓解这个问题但根本还是要确保TcpClient被正确Dispose。4.4 现象心跳正常但业务消息超时原因是心跳和业务消息共用了同一个发送锁业务消息阻塞时心跳也发不出去。解决方法是把心跳独立成一个轻量任务或者用优先级队列让心跳插队。资源里建议心跳消息不参与业务排队直接走单独的发送通道。4.5 现象中文乱码或消息截断原因是收发两端编码不一致或者长度头按字符数而不是字节数计算。解决方法是统一用Encoding.UTF8并且长度头始终是字节数组的长度。如果消息体包含二进制数据不要用字符串转换直接传字节数组。5. 二次开发改造把示例代码变成可复用的通信组件5.1 封装成独立类库示例代码跑通后下一步是把它抽成一个TcpClientWrapper类对外暴露ConnectAsync、SendAsync、ReceiveAsync和事件OnDisconnected。这样业务层不需要关心NetworkStream和粘包细节。我一般会把长度前缀的编解码放在一个MessageCodec静态类里方便单元测试。public class TcpClientWrapper : IDisposable { private TcpClient _client; private NetworkStream _stream; private CancellationTokenSource _cts; public event Actionstring OnMessageReceived; public event Action OnDisconnected; public async Task ConnectAsync(string host, int port) { _client new TcpClient(); await _client.ConnectAsync(host, port); _stream _client.GetStream(); _cts new CancellationTokenSource(); // 启动接收循环收到消息后触发事件 _ Task.Run(() ReceiveLoopAsync(_cts.Token)); } private async Task ReceiveLoopAsync(CancellationToken token) { try { while (!token.IsCancellationRequested) { string msg await ReadMessageAsync(_stream); OnMessageReceived?.Invoke(msg); } } catch { OnDisconnected?.Invoke(); } } public void Dispose() { _cts?.Cancel(); _stream?.Dispose(); _client?.Dispose(); } }事件机制让业务代码解耦收到消息后直接更新界面或写数据库。注意ReceiveLoopAsync里捕获异常后触发OnDisconnected外层可以订阅这个事件来启动重连。5.2 增加配置化与日志把主机地址、端口、心跳间隔、重连上限放到appsettings.json里用IConfiguration读取。日志用ILogger接口方便替换成文件或控制台。资源里没有强制用某个日志框架但建议至少记录连接、断开、重连、异常四类事件排查问题时能还原时间线。5.3 验证方法用回环测试和压力测试改造完成后不要只靠手动点按钮验证。写一个回环测试启动服务端客户端连上后连续发送 1000 条随机长度消息服务端逐条校验内容是否一致。再写一个断线测试服务端中途关闭观察客户端是否在预期时间内重连成功。压力测试可以用Parallel.For模拟多客户端并发检查是否有内存泄漏或线程安全问题。5.4 一个具体技巧用SocketAsyncEventArgs替代ReadAsync如果项目对性能要求高比如每秒上万条消息ReadAsync的Task分配会成为瓶颈。这时可以改用SocketAsyncEventArgs做零分配接收。资源里没有展开这部分但思路是预先分配一个大的接收缓冲区用事件回调处理数据到达避免每次读取都创建Task。改造时先保证功能正确再逐步替换热点路径。从那以后我每次写 TCP 通信都会先把长度前缀和心跳循环跑一遍再往上叠业务逻辑。这个顺序能省掉很多“连上了却收不到”的返工。希望帮到你。本文还有配套的精品资源点击获取