简介这是一份面向C#初学者与WinForm开发者的TCP通信入门示例源码围绕服务端与客户端两个独立工程展开帮助理解面向连接、可靠传输的网络编程模型。压缩包共61个文件约120KB以cs源码、csproj工程文件、sln解决方案、config配置、resx资源与exe可执行文件为主另含pdb调试符号、manifest清单及少量缓存文件结构完整可直接编译运行。已有2090人学习下载说明该示例在入门阶段具备一定参考价值。服务端工程演示TcpListener监听指定IP与端口、AcceptTcpClient阻塞接收连接客户端工程演示TcpClient发起Connect请求双方通过NetworkStream配合StreamReader与StreamWriter完成文本收发并涵盖连接建立与关闭的完整流程。读者可据此掌握Socket通信基本套路并将其作为文件传输、在线聊天等客户端-服务器应用的起步模板在此基础上扩展多线程、心跳与异常处理等功能。1. 从 FrmTcpServer TcpClient.rar 说起一个压缩包背后的 TCP 调试工具链拿到FrmTcpServer TcpClient.rar这个包第一反应通常是里面装的是一个带窗体的 TCP 服务端加一个客户端多半是 C# WinForms 写的调试小工具。这类东西在工控、物联网、上位机圈子里非常常见——现场设备要联调串口助手不够用就自己撸一个 TCP 收发窗口服务端监听、客户端连接、十六进制收发、日志滚动一套下来两三百行代码。问题在于很多人拿到 rar 之后卡在第一步解压软件不趁手或者解压出来一堆.cs、.csproj、.sln文件不知道怎么跑起来。热搜里「rar解压软件」「7zip可以解压rar文件吗」这类词恰恰说明这个包在传播过程中被反复卡在解压环节。这篇笔记就顺着这个包把「解压 → 还原工程 → 跑通服务端与客户端 → 联调排错」整条链路讲清楚适合手里有类似窗体 TCP 工具、想自己改一版或者想搞懂 WinForms 网络编程怎么落地的人。2. 解压与工程还原从 rar 到能编译的解决方案2.1 解压工具怎么选7zip 到底能不能开 rar先说结论7-Zip 本身对 RAR 的支持是「只读解压」能开、能解但不能压。如果你只是要把FrmTcpServer TcpClient.rar里的文件拿出来7-Zip 完全够用装完右键「7-Zip → 提取到当前位置」即可。但要注意两个坑一是老版本 7-Zip比如 9.20 之前对 RAR5 格式支持不全遇到 RAR5 会报「不支持的压缩方法」这时候要么升级到 19.00 以上要么换 WinRAR、Bandizip。二是如果这个包是分卷的.part1.rar、.part2.rar必须把所有分卷放同一个目录只解第一个否则会提示「需要下一压缩分卷」。命令行解压更可控适合批量处理# 用 7z 解压-o 指定输出目录-y 覆盖不询问 7z x FrmTcpServer TcpClient.rar -o./FrmTcpServer_TcpClient -y # 查看压缩包内容先确认结构再解避免解出一堆散文件 7z l FrmTcpServer TcpClient.rarx是保留目录结构解压e是不保留目录结构全部摊平——这里必须用x否则.cs和.csproj会被摊到同一层工程结构就废了。-o后面不要有空格-y在脚本里很有用避免卡在交互确认。提示如果压缩包带密码而你又忘了别去试那些「强制解压」的野路子RAR 的 AES 加密没有捷径老老实实找原作者要密码或者放弃这个包重新找一份。2.2 解压后目录长什么样怎么判断工程类型解压完先别急着双击用tree或资源管理器看一眼结构。典型的 WinForms TCP 工具包解压后大概是这样文件/目录作用是否必须FrmTcpServer.sln解决方案文件双击用 VS 打开是FrmTcpServer.csproj服务端工程文件是FrmTcpServer.cs/FrmTcpServer.Designer.cs窗体逻辑与设计器代码是TcpClient/目录客户端独立工程视包而定bin/obj/编译产物可删否packages.configNuGet 依赖清单视包而定判断工程类型看.csproj里的TargetFramework如果是net48或net472那是 .NET FrameworkWindows 上直接能跑如果是net6.0-windows、net8.0-windows需要装对应版本的 .NET SDK。这一步判断错了后面「找不到 System.Windows.Forms」的报错会把你绕晕。2.3 用 Visual Studio 或 dotnet CLI 还原并编译有 VS 的话双击.sln右键解决方案「还原 NuGet 包」然后生成。没有 VS 或者想走命令行用 dotnet CLI# 进入解压后的目录 cd FrmTcpServer_TcpClient # 还原依赖 dotnet restore # 编译-c Release 出发布版 dotnet build -c Release # 如果是 .NET Framework 老工程dotnet build 可能不认改用 msbuild msbuild FrmTcpServer.sln /p:ConfigurationReleasedotnet restore会读.csproj里的PackageReference去拉包老工程用的是packages.config得用nuget restore或者让 VS 自动还原。编译报错先看第一条后面的往往是连锁反应。常见的第一条错误是「找不到目标框架」那就是 SDK 没装对如果是「命名空间 System.Net.Sockets 不存在」检查是不是把工程类型建成了类库而不是 Windows 应用。3. 服务端窗体逻辑监听、接受连接与数据收发3.1 TcpListener 的初始化与端口占用排查服务端核心就三件事TcpListener起监听、循环AcceptTcpClient、拿到NetworkStream收发。WinForms 里必须把阻塞操作丢到后台线程否则窗体直接假死——这是新手最常翻车的地方。// 服务端监听初始化放在窗体加载或「启动」按钮里 private TcpListener _listener; private CancellationTokenSource _cts; private void StartServer(int port) { // IPAddress.Any 表示监听本机所有网卡端口冲突会在这里抛异常 _listener new TcpListener(IPAddress.Any, port); _listener.Start(); _cts new CancellationTokenSource(); // 用 Task 跑接受循环避免阻塞 UI 线程 Task.Run(() AcceptLoop(_cts.Token)); } private async Task AcceptLoop(CancellationToken token) { while (!token.IsCancellationRequested) { try { // AcceptTcpClientAsync 不阻塞 UIawait 后回到线程池 TcpClient client await _listener.AcceptTcpClientAsync(); _ Task.Run(() HandleClient(client, token)); } catch (ObjectDisposedException) { // 监听器被 Stop 后正常退出 break; } } }IPAddress.Any对应0.0.0.0监听所有网卡如果只想本机调试用IPAddress.Loopback。端口被占用时Start()抛SocketException错误码 10048这时候用netstat -ano | findstr :端口号找到占用进程要么换端口要么结束进程。AcceptTcpClientAsync是 .NET 提供的异步版本比BeginAcceptTcpClient那套 APM 写法清爽得多。3.2 跨线程更新 UIInvoke 与 BeginInvoke 的区别后台线程收到数据后要往 TextBox 或 ListBox 里追加直接写会抛「线程间操作无效」。正确做法是Invoke或BeginInvokeprivate void AppendLog(string text) { // 判断是否在 UI 线程不是就切回去 if (txtLog.InvokeRequired) { // BeginInvoke 异步不阻塞后台线程Invoke 同步会等 UI 处理完 txtLog.BeginInvoke(new Action(() AppendLog(text))); return; } txtLog.AppendText($[{DateTime.Now:HH:mm:ss}] {text}\r\n); }Invoke是同步的后台线程会等 UI 线程执行完才继续高频数据下容易拖慢接收BeginInvoke是异步投递推荐用后者。InvokeRequired在窗体句柄还没创建时会返回 false所以别在构造函数里调放到Load事件之后。3.3 粘包与拆包为什么你的消息总是「连在一起」TCP 是字节流没有消息边界。客户端连发两条hello和world服务端一次Read可能收到helloworld也可能只收到hel。这是 TCP 调试工具最核心的坑热搜里那些「TCP 调试助手」的差评多半来自这里。常见解法有三种固定长度、分隔符、长度前缀。长度前缀最通用// 发送4 字节长度头 正文 private async Task SendMessage(NetworkStream stream, byte[] payload) { byte[] len BitConverter.GetBytes(payload.Length); // 小端序 await stream.WriteAsync(len, 0, 4); await stream.WriteAsync(payload, 0, payload.Length); } // 接收先读 4 字节长度再按长度读正文 private async Taskbyte[] ReadMessage(NetworkStream stream) { byte[] lenBuf new byte[4]; await ReadExactAsync(stream, lenBuf, 4); int len BitConverter.ToInt32(lenBuf, 0); byte[] body new byte[len]; await ReadExactAsync(stream, body, len); return body; } // 循环读满指定字节数Read 不保证一次读完 private async Task ReadExactAsync(NetworkStream stream, byte[] buf, int count) { int offset 0; while (offset count) { int n await stream.ReadAsync(buf, offset, count - offset); if (n 0) throw new IOException(连接已关闭); offset n; } }BitConverter默认小端序两端必须一致否则长度解析出来是天文数字。ReadExactAsync是关键NetworkStream.Read返回的是「本次读到的字节数」不是「请求的字节数」不循环读就会拆包。如果对方协议是分隔符比如\r\n那就得自己维护一个缓冲区按分隔符切分逻辑更绕但兼容性好。4. 客户端连接与联调从连不上到收发正常4.1 TcpClient 连接超时与重连策略客户端TcpClient.Connect默认超时很长受系统 TCP 超时控制可能 20 秒以上界面上点一下「连接」卡半天体验极差。用ConnectAsync加Task.WhenAny做超时private async Taskbool ConnectWithTimeout(string host, int port, int timeoutMs) { var client new TcpClient(); var connectTask client.ConnectAsync(host, port); var timeoutTask Task.Delay(timeoutMs); // 谁先完成用谁的结果 var finished await Task.WhenAny(connectTask, timeoutTask); if (finished timeoutTask) { client.Close(); return false; // 超时 } await connectTask; // 把异常抛出来 _client client; return true; }Task.WhenAny返回先完成的任务超时就关掉 client 释放资源。注意ConnectAsync失败会抛SocketException错误码 10061 是「连接被拒绝」服务端没起或防火墙拦了10060 是「连接超时」网络不通或 IP 错。重连策略别写死循环用指数退避1 秒、2 秒、4 秒、8 秒封顶 30 秒避免把服务端打爆。4.2 本机回环测试先排除网络因素联调第一步永远是本机127.0.0.1自测。服务端起在 8888客户端连127.0.0.1:8888能收发说明代码逻辑没问题再去连局域网 IP。如果本机都不通检查三件事服务端Start()有没有真的执行、防火墙有没有拦、端口是不是被别的进程占了。Windows 防火墙对监听端口的拦截是静默的客户端只会看到「连接被拒绝」很容易误判成代码 bug。# 本机测试服务端端口是否在监听 netstat -ano | findstr :8888 # 用 telnet 快速验证端口通不通Windows 需先启用 telnet 客户端 telnet 127.0.0.1 8888netstat看到LISTENING说明服务端起成功了。telnet能连上说明 TCP 层通连不上就是监听或防火墙问题。这一步能省掉大量「到底是代码还是网络」的扯皮。4.3 十六进制收发与编码选择工控设备经常发二进制帧界面上要支持 HEX 显示和发送。核心是BitConverter.ToString和Convert.FromHexString.NET 5或手写解析// 字节数组转 HEX 字符串显示 string hex BitConverter.ToString(data).Replace(-, ); // 输出形如 01 03 00 00 00 0A // HEX 字符串转字节数组.NET 5 byte[] bytes Convert.FromHexString(0103000000 0A.Replace( , )); // 老框架手写解析 byte[] ParseHex(string s) { s s.Replace( , ); byte[] result new byte[s.Length / 2]; for (int i 0; i result.Length; i) result[i] Convert.ToByte(s.Substring(i * 2, 2), 16); return result; }编码选择上文本协议用Encoding.UTF8但要注意中文占 3 字节长度前缀算的是字节数不是字符数。二进制协议直接操作byte[]别经过字符串转换否则0x00会被截断。Convert.FromHexString遇到奇数长度或非法字符会抛FormatException界面上要 try-catch 给提示别让程序直接崩。5. 避坑与排查窗体 TCP 工具最容易翻车的 5 个点5.1 现象窗体一点「启动」就卡死鼠标转圈原因AcceptTcpClient或Read是阻塞调用直接写在按钮事件里UI 线程被占住。解决所有网络阻塞操作丢Task.RunUI 更新走BeginInvoke。已经卡死的别强杀进程先看是不是有未释放的TcpListener。5.2 现象客户端显示已连接但服务端收不到数据原因多半是NetworkStream的Write之后没Flush或者用了带缓冲的StreamWriter没设置AutoFlush。解决直接用NetworkStream.WriteAsync写字节或者StreamWriter构造时传AutoFlush true。另外检查是不是把数据写到了错误的流上——一个TcpClient只有一个GetStream()。5.3 现象中文显示乱码英文正常原因两端编码不一致一端 UTF8 一端 GBK。解决统一用 UTF8或者在界面上加编码下拉框。注意Encoding.Default在 .NET Core 里默认是 UTF8在 .NET Framework 里是系统 ANSI中文系统即 GBK跨框架移植时这是个大坑。5.4 现象连接数一多就报「无法分配请求的地址」原因客户端频繁短连接端口进入 TIME_WAIT 状态本地端口耗尽。解决客户端复用TcpClient长连接或者设置Socket的ReuseAddress。服务端侧检查有没有及时Close掉断开的 clientTcpClient不关会泄漏句柄。5.5 现象Release 编译后运行报错Debug 正常原因Debug 和 Release 的代码优化、条件编译不同常见于#if DEBUG块里的初始化代码在 Release 被跳过。解决检查所有#if DEBUG把必要的初始化移出来。另外 Release 下async void的异常不会被捕获直接崩进程事件处理器里的async void一定要包 try-catch。6. 进阶把窗体工具改造成可复用的 TCP 调试组件跑通之后下一步是把这套逻辑从窗体里剥出来做成不依赖 UI 的类库这样控制台、WPF、甚至 ASP.NET 都能复用。核心是把TcpServer和TcpClientWrapper抽成独立类事件回调代替直接操作控件。// 服务端封装事件驱动不依赖任何 UI 类型 public class TcpServer { private TcpListener _listener; public event ActionTcpClient ClientConnected; public event ActionTcpClient, byte[] DataReceived; public event ActionTcpClient ClientDisconnected; public void Start(int port) { _listener new TcpListener(IPAddress.Any, port); _listener.Start(); Task.Run(AcceptLoop); } private async Task AcceptLoop() { while (true) { var client await _listener.AcceptTcpClientAsync(); ClientConnected?.Invoke(client); _ Task.Run(() ReceiveLoop(client)); } } private async Task ReceiveLoop(TcpClient client) { var stream client.GetStream(); var buf new byte[4096]; try { while (true) { int n await stream.ReadAsync(buf, 0, buf.Length); if (n 0) break; var data buf.Take(n).ToArray(); DataReceived?.Invoke(client, data); } } catch (IOException) { /* 连接异常断开 */ } finally { ClientDisconnected?.Invoke(client); client.Close(); } } }窗体里只订阅事件把DataReceived的数据丢给BeginInvoke显示。这样单元测试可以直接 new 一个TcpServer用TcpClient连上去发数据断言事件触发不用起窗体。验证方法写个 xUnit 测试Start(0)让系统分配随机端口从_listener.LocalEndpoint拿实际端口客户端连上去发 100 条消息断言收到 100 次事件且内容一致。这一步做完这个 rar 里的工具就从「能跑的 demo」变成了「能进项目的组件」。我自己踩过的教训是早期图省事把收发逻辑全写在窗体里后来要加个命令行版本复制粘贴改到怀疑人生。现在习惯是任何网络逻辑先抽类库窗体只做展示改需求时省下的时间远超当初多写的那几十行。希望帮到你。本文还有配套的精品资源点击获取