拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C# TCP通讯入门:最简客户端服务端例程与避坑指南

简介一个面向C#初学者的TCP/IP通信入门例程包包含可直接运行的服务端与客户端工程解决网络编程新手在Socket编程中无参考模板的问题。包内共55个文件以C#源码cs、编译后的可执行程序exe、文本说明txt及项目配置sln、csproj、resources等为主压缩包仅450KB小巧易用。开发环境基于Visual Studio服务端使用TcpListener监听8888端口客户端通过TcpClient连接本机地址演示了建立连接、发送与接收字符串等核心流程。压缩包同时附带了编译好的exe无需搭建环境即可先观察运行效果源码中窗体界面便于调试与改造。已有204人学习下载适合课程设计、毕业设计或自学练手时快速搭建TCP通信模型为进一步扩展多线程并发、数据粘包处理等高级主题打下基础。 相信搞过C#上位机或者工业通讯的朋友都绕不开TCP/IP这关。不管是跟PLC、扫码枪、视觉相机打交道还是自己写个数据中转工具TCP/IP永远是最基础也最实用的一门手艺。网上关于C# TCP的教程一搜一大把但很多都写得绕来绕去要么贴一堆不痛不痒的代码片段要么把异步、多线程、粘包拆包全塞进来直接把新手劝退。这篇文章我就直接给你最精简的玩意儿——一个能跑的C# TCP客户端和服务端例程。代码量不多但每一行都有用你照着敲一遍就能通通了之后再往里面加业务逻辑就顺手多了。我会把实现思路、关键API的选择、踩过的坑一次讲清楚让你不光是能跑通还知道为什么这么写。1. 整体设计与思路拆解动手写代码之前先花两分钟想清楚要做什么。TCP通讯其实就两个角色服务端等着别人来连客户端主动去连别人。这个模型跟打电话一模一样服务端是开机待机的座机客户端是主动拨出去的手机。理解了这一点整个程序的骨架就出来了。1.1 为什么选择TcpListener和TcpClientC#里做TCP其实有好几条路——直接用Socket类、用TcpListener/TcpClient封装类、或者赶上时髦用NetworkStream配合异步API。对于最简单例程这个目标我的建议是用TcpListener做服务端用TcpClient做客户端。原因很简单TcpListener和TcpClient是Socket的封装把很多底层细节比如绑定地址、端口复用、连接握手都给你包好了用起来省心。打个比方Socket像手动挡汽车啥都得自己操作TcpListener/TcpClient像自动挡踩油门就走最适合日常通勤。真到了性能极致要求的场景你再回头去抠原始Socket也不迟。很多教程上来就贴一大段BeginAccept、BeginReceive这类异步代码新手看得云里雾里。我的观点很明确第一版例程先写同步代码把通讯流程跑通同步版能跑顺了再去理解异步就完全是水到渠成的事。因为异步的本质还是那个流程只是回调把你的逻辑撕成了碎片。1.2 同步模型为什么适合入门同步模型的好处是直观。代码从上往下读就是先建立连接、再收发数据、最后关闭连接。每一步做完才走下一步调试的时候看代码就能推断出当前状态。搞过上位机的人都有体会跟设备通讯最怕的就是不知道现在卡在哪一步同步模型至少让你心里有数。它的缺点也很明显在接收数据时如果对方没发数据程序会卡在接收函数那里界面会假死。解决办法后面会讲最基础的就是把收发操作丢到后台线程里跑。这个咱们例程里肯定会处理到不然你根本没办法在WinForm里用。2. 服务端核心逻辑与实操要点服务端的职责一句话就能说清楚创建监听、等客户端来连、收数据、回数据。但这里面的细节足够让新手卡上半天。2.1 服务端三步走框架第一步创建TcpListener开始监听 第二步循环等待客户端连接 第三步处理每个客户端的收发消息对应到代码核心就三行TcpListener listener new TcpListener(IPAddress.Any, 9000); listener.Start(); TcpClient client listener.AcceptTcpClient();IPAddress.Any表示监听本机所有网卡地址不管客户端连的是局域网IP还是127.0.0.1都能接进来。端口号9000是我随手选的实际使用别选那些已经被占用的端口比如80、443、3306尽量选9000以上的高位端口不容易撞车。AcceptTcpClient是个阻塞方法程序跑到这里会一直等着直到有客户端连进来才继续往下走。这也是为什么后面必须把这个逻辑放进线程或Task里——你总不想让界面卡在等待连接上吧。2.2 接收数据的底层逻辑和中文乱码问题连接建立之后通过client.GetStream()拿到NetworkStream这是双方数据的管道。接收数据用的是stream.Read(buffer, 0, buffer.Length)读到的字节存进byte数组里然后转字符串。这里必须说一个99%新手都会踩的坑——编码问题。网络上的数据本质是字节流字节怎么转成字符串取决于你用什么编码解析。如果你用Encoding.Default在.NET Core / .NET 5 环境里默认是UTF-8而你的设备端比如某些扫码枪、老款PLC可能发的是GB2312/GBK于是中文全部变成乱码。我的习惯是收发统一用Encoding.UTF8如果对接的设备只支持GBK那收发两端都改成Encoding.GetEncoding(GBK)。关键是发和收保持一致这一点必须在设计通讯协议的时候就定死不然后面联调头大。2.3 服务端完整代码与逐行说明我写一个最精简的控制台服务端程序using System; using System.Net; using System.Net.Sockets; using System.Text; class TcpServerDemo { static void Main() { int port 9000; TcpListener listener new TcpListener(IPAddress.Any, port); listener.Start(); Console.WriteLine($服务端已启动监听端口{port}); while (true) { // 等待客户端连接阻塞 TcpClient client listener.AcceptTcpClient(); Console.WriteLine($客户端已连接{client.Client.RemoteEndPoint}); // 每个客户端开一个线程处理避免影响后续连接 System.Threading.Thread t new System.Threading.Thread(() HandleClient(client)); t.IsBackground true; t.Start(); } } static void HandleClient(TcpClient client) { try { NetworkStream stream client.GetStream(); byte[] buffer new byte[1024]; int bytesRead 0; while ((bytesRead stream.Read(buffer, 0, buffer.Length)) 0) { string msg Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine($收到消息{msg}); // 原样返回做个回显 byte[] sendBytes Encoding.UTF8.GetBytes($服务端已收到{msg}); stream.Write(sendBytes, 0, sendBytes.Length); } } catch (Exception ex) { Console.WriteLine($连接异常{ex.Message}); } finally { client.Close(); Console.WriteLine(客户端已断开。); } } }这里最关键的设计是每接一个客户端就开一个线程。因为AcceptTcpClient接完一个客户端之后程序要回到while(true)继续等下一个连接不能卡在第一个客户端的数据处理上。在实际的上位机项目里更正规的做法是用线程池或者Task.Run但就例程而言直接用Thread最直白逻辑一眼就能看穿。Read方法的返回值很重要它表示本次读到了多少个字节。如果返回0通常意味着对方关闭了连接。所以while循环的判断条件直接拿它来用既简洁又安全。3. 客户端实现细节与关键代码客户端的逻辑比服务端简单得多——创建连接、发数据、收返回。但有一个点是整个TCP通讯里最容易被忽略的TCP是流协议不是消息协议。3.1 理解TCP的粘包和半包这句话怎么理解你调用一次stream.Write发了一条完整的你好对端调用Read读到的可能不是完整的你好可能是你和好分两次到达也可能是你好和别的数据黏在一起到达。这就是传说中的粘包半包问题。对于最简单例程这个定位我先不给你上分包协议那些重型武器。最简单的办法是每条消息末尾加特殊结束符最常用的是\r\n或\n。发的时候带上收的时候不断累积字节直到看见结束符才认为凑齐了一条完整消息。不过为了代码清爽下面的客户端例程我先用读一次算一次的方式逻辑上对于交互式一问一答的场景已经够用。真到了处理大量持续上报数据的场景必须老老实实做分包这个后面单独开一篇。3.2 客户端完整代码using System; using System.Net.Sockets; using System.Text; class TcpClientDemo { static void Main() { string serverIp 127.0.0.1; int port 9000; using (TcpClient client new TcpClient()) { client.Connect(serverIp, port); Console.WriteLine(已连接到服务端。); NetworkStream stream client.GetStream(); while (true) { Console.Write(请输入要发送的内容exit退出); string input Console.ReadLine(); if (input exit) break; byte[] data Encoding.UTF8.GetBytes(input); stream.Write(data, 0, data.Length); byte[] buffer new byte[1024]; int bytesRead stream.Read(buffer, 0, buffer.Length); string response Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine($服务端返回{response}); } } } }Connect是同步连接连不上会抛异常。实际项目里建议先用client.ConnectAsync()配合超时控制避免界面卡死。同步版本会按操作系统默认超时时间一直等这个时间往往长得离谱不是我们想要的。另外注意using关键字TcpClient实现了IDisposable用using包起来能确保退出时自动释放资源。TCP连接一旦忘了关闭最直接的后果就是端口被占用服务端重启时报地址已在用的错误。3.3 WinForm里怎么接入TCP逻辑控制台跑通之后很多人会想把它挪进WinForm或者WPF上位机里。这时候有一个核心问题必须解决不能在UI线程里做阻塞操作。我的做法是服务端用Task.Run把监听循环丢到后台收到消息后通过Invoke或BeginInvoke把内容更新到UI控件上。客户端同理收发消息都放到后台任务里。Task.Run(() { TcpListener listener new TcpListener(IPAddress.Any, 9000); listener.Start(); while (true) { TcpClient client listener.AcceptTcpClient(); Task.Run(() HandleClient(client)); } });注意C#里通过lambda表达式访问外部变量是安全的但如果你在HandleClient里访问UI控件必须用Invoke。这个规矩不守程序就会随机崩给你看。4. 常见问题、避坑经验与排查技巧这部分是真正的干货。我搞了这么多年的TCP通讯这些坑一个不落地全踩过写下来帮你省点学费。4.1 地址被占用问题服务端程序跑起来之后如果没关干净又跑了一次经常会报SocketException: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。原因就是上一次的进程没释放端口。排查分三步第一步找出占用端口的进程ID netstat -ano | findstr 9000 第二步根据PID查进程名 tasklist | findstr 1234换成你自己的PID 第三步确认是残留进程后结束它 taskkill /F /PID 1234另外一个治本的方法是在代码里设置端口复用listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);不过要注意这个设置要放在Start()之前才生效。4.2 局域网连不上但本机能通这个坑出现频率极高。代码在本机用127.0.0.1测得好好的一放到局域网里别的电脑就是连不上。原因基本就三种服务端监听的IP不对。你用了IPAddress.Loopback127.0.0.1那就只能本机访问改成IPAddress.Any才能被局域网访问。Windows防火墙拦截了。最省事的做法是开发阶段直接关闭防火墙仅限内网测试环境正式部署时加一条入站规则放开对应端口。不在同一个网段。两台电脑IP要在同一网段或者中间路由器配置正确。我在现场调试的时候最常用的排查命令是ping和telnet IP 端口。尤其telnet它是测试TCP端口通不通的神器。如果telnet能通但程序连不上问题基本出在代码如果telnet都不通问题在网络或防火墙。4.3 接收数据不完整或者粘在一起这是TCP新手最容易绝望的问题。你发了100个字节对端有时候收到100个有时候收到50个有时候收到200个数据还串台。究其根本TCP是流式协议它只保证字节按顺序到达不保证每次Read拿到的都恰好是一条完整消息。处理办法业界已经有了成熟方案常用的是长度前缀法和结束符法。长度前缀法发送前先算好消息体的字节数作为固定长度的头比如前4个字节表示长度后面跟着消息体。接收端先读4个字节得到长度再按长度读够消息体。这种方式效率高、不依赖特定字符更稳妥。结束符法每条消息以\r\n结尾接收端边收边判断。简单但需要注意消息内容里不能包含这个结束符否则就提前截断了。如果你的设备协议里没有定义这些你在写上位机的时候就要自己定一套。我个人的经验是能用长度前缀就优先用长度前缀它最无脑最稳定。4.4 服务端能接多个客户端吗上面的例程里我用了来一个客户端开一个线程的方式所以当然可以接多个客户端。但要注意每开一个线程就有线程开销几千个并发连接的时候线程调度就会成为瓶颈。实际工程中高并发场景一般会用到SocketAsyncEventArgs或者async/await配合完成核心思路是让线程在等待I/O时去干别的活而不是死等。这个话题能写一整篇这里先不展开。对于接几个设备的工控现场来一个连接开一个线程的方案完全够用。4.5 心跳机制要不要做如果你的客户端和服务端之间有长时间不通讯的情况建议做心跳。原因很简单TCP连接断开时如果有一方没有正常发FIN包比如网线断了、设备直接断电另一端是感知不到的。它以为连接还活着实际早就断了。心跳做法不复杂客户端每隔几秒发一个约定的心跳包比如ping服务端收到后回一个pong。如果连续多次没有收到心跳服务端就把这个连接标记为断开并回收资源。这样做虽然会增加一点通讯量但换来的是连接状态的确定性很划算。我在做设备通讯时一般把心跳设3秒一次连续3次没收到就判定断开。5. 一些关于进阶方向的个人看法写到这里最简单例程的目标已经完成了。但我不太建议你就此打住——既然已经摸到TCP的门后面有两条路非常值得深入。第一条路是协议设计。你迟早会发现裸的TcpClient收发字符串只是起点实际项目里都是自定义协议有帧头、帧尾、长度字段、校验位。学习协议设计的时候可以先从抄Modbus协议开始看它怎么组织报文、怎么做CRC校验然后再尝试自己定义一套简版协议。这个过程会反过来加深你对TCP流传输特性的理解。第二条路是异步编程。async/await在C#里相当成熟用好了代码又简洁又能扛并发。建议你把同步版的例程改写成异步版用AcceptTcpClientAsync、ReadAsync、WriteAsync替换掉同步方法你会发现原来写起来异常痛苦的并发代码变得非常顺手。搞完这两个方向你在C# TCP通讯这块就算入门到家了。最后再分享个经验调试TCP程序一定要善用日志。我在工业现场就吃过大亏——程序跑得好好的一到现场就出问题最后靠加日志定位到是设备主动断开连接导致服务端异常退出。TCP这东西本机测试通过只是第一步网络环境一变什么怪问题都可能冒出来。所以在代码的关键节点连接成功、收到消息、连接断开、异常都要打日志这习惯一定要养成。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门