ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TcpListener与TcpClient实战:构建稳定的局域网通信与断线重连

TcpListener与TcpClient实战:构建稳定的局域网通信与断线重连 简介这份资源面向具备一定C#基础的开发者与网络编程初学者聚焦在WinForm环境下实现TCP服务端与客户端通信这一典型场景。包内以两个独立窗体项目分别演示TcpListener与TcpClient的完整用法涵盖监听启动、连接建立、NetworkStream读写、后台线程处理与UI交互整合等关键环节帮助读者理解可靠连接式传输协议的实际落地方式。资源共61个文件以cs源码、config配置、resx与resources资源、exe可执行程序及pdb调试文件为主另有sln解决方案与csproj工程文件压缩包约104KB结构完整可直接编译运行。目前已有963人学习下载。通过对照服务端与客户端两套窗体代码读者可掌握套接字创建、连接管理、数据收发与资源释放的排错思路并快速迁移到自己的桌面通信应用中。1. 从 TcpListener 与 TcpClient 说起一个压缩包标题背后的局域网通信最小闭环很多人第一次看到TcpListenrAndTcpClient.rar这种命名第一反应是「这不就是个 TCP 聊天室 Demo 吗」。但真正在工控、医疗设备、自助终端这些场景里滚过几年的人会告诉你这个标题背后藏着的是一套最朴素的点对点通信骨架一端用TcpListener死守端口等连接另一端用TcpClient主动发起握手连上之后双方靠字节流对话。它不依赖 HTTP、不依赖消息队列、不依赖任何中间件两台机器插上网线就能跑。适合谁适合需要快速验证设备间指令下发与状态回传的嵌入式上位机开发者、需要给产线写一个轻量级心跳上报工具的 .NET 工程师以及所有被「为什么我的客户端连不上」折磨过、想彻底搞懂 TCP 连接生命周期的人。这个压缩包如果解压出来大概率是一份 C# 控制台或 WinForm 工程核心就是两个类System.Net.Sockets.TcpListener和System.Net.Sockets.TcpClient。把这两个类用对局域网通信的底子就打牢了。2. TcpListener 与 TcpClient 的角色分工谁先动谁在等2.1 服务端为什么必须用 TcpListener 而不是裸 SocketTcpListener本质上是对Socket的一层封装专门用于监听模式。它替你做了三件容易出错的事绑定本机 IP 与端口、把 Socket 置为监听状态、用AcceptTcpClient()阻塞等待入站连接。如果你直接用new Socket(...)需要手动调Bind、Listen、Accept参数顺序和SocketType选错一个就抛异常。常见做法是服务端启动时指定IPAddress.Any和端口让操作系统决定用哪块网卡接收。这里有个细节IPAddress.Any对应0.0.0.0意味着监听所有网卡如果只想监听内网就换成具体的内网 IP比如192.168.1.100。端口选择上避开 0 到 1023 的知名端口也避开 1433、3306 这类数据库端口我一般从 8000 往上找比如 8899、9527 这种不容易撞车的。2.2 TcpClient 发起连接时到底发生了什么TcpClient的Connect方法触发的是标准 TCP 三次握手。客户端发出 SYN服务端回 SYN-ACK客户端再回 ACK连接建立。这个过程在代码里就是一行client.Connect(192.168.1.100, 8899)但背后涉及路由表查询、ARP 寻址、防火墙过滤。如果服务端没监听客户端会收到 RST 包表现为SocketException错误码通常是ConnectionRefused。如果服务端 IP 不对或不在同一网段客户端会卡在 SYN 重传直到超时默认大约 20 秒以上。所以调试时第一步永远是ping通不通第二步用telnet 目标IP 端口看端口开没开。TcpClient还支持构造函数里直接传IPEndPoint也支持异步ConnectAsync在 UI 线程里用异步可以避免界面卡死。2.3 一个最小可运行的服务端与客户端代码骨架下面这段代码可以直接复制到两个控制台工程里跑通。服务端先启动客户端后启动。// 服务端TcpListener 监听 8899 端口 using System; using System.Net; using System.Net.Sockets; using System.Text; class TcpServer { static void Main() { // 监听本机所有网卡的 8899 端口 TcpListener listener new TcpListener(IPAddress.Any, 8899); listener.Start(); Console.WriteLine(服务端已启动等待连接...); // AcceptTcpClient 会阻塞直到有客户端连入 TcpClient client listener.AcceptTcpClient(); Console.WriteLine(客户端已连接 client.Client.RemoteEndPoint); // 获取网络流用于收发字节 NetworkStream stream client.GetStream(); byte[] buffer new byte[1024]; int bytesRead stream.Read(buffer, 0, buffer.Length); string msg Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine(收到 msg); // 回一条消息 byte[] reply Encoding.UTF8.GetBytes(服务端已收到); stream.Write(reply, 0, reply.Length); client.Close(); listener.Stop(); } }// 客户端TcpClient 连接服务端并发送消息 using System; using System.Net.Sockets; using System.Text; class TcpClientDemo { static void Main() { TcpClient client new TcpClient(); // 连接服务端 IP 和端口超时由系统控制 client.Connect(127.0.0.1, 8899); NetworkStream stream client.GetStream(); byte[] data Encoding.UTF8.GetBytes(你好我是客户端); stream.Write(data, 0, data.Length); byte[] buffer new byte[1024]; int bytesRead stream.Read(buffer, 0, buffer.Length); Console.WriteLine(收到回复 Encoding.UTF8.GetString(buffer, 0, bytesRead)); client.Close(); } }逻辑说明服务端listener.Start()之后进入AcceptTcpClient()阻塞此时端口处于LISTEN状态。客户端Connect成功后双方各自拿到NetworkStream这个流是全双工的读写可以同时进行。参数说明IPAddress.Any表示监听所有网卡端口 8899 是自定义的只要两端一致且未被占用即可buffer大小 1024 是保守值实际按协议长度调整一般不超过 8KB 以免拆包复杂。注意stream.Read返回的是实际读到的字节数不能假设一次就读满。3. 把连接做稳超时、心跳与断线重连的工程化处理3.1 连接超时为什么不能只靠默认值TcpClient.Connect的默认超时由操作系统决定Windows 下大约 21 秒。在产线设备上这个等待时间不可接受。常见做法是用ConnectAsync配合Task.Wait指定毫秒数或者用BeginConnect加WaitHandle。下面是一个带 3 秒超时的连接封装using System; using System.Net.Sockets; using System.Threading.Tasks; static bool ConnectWithTimeout(TcpClient client, string ip, int port, int timeoutMs) { var task client.ConnectAsync(ip, port); // 等待连接完成或超时 bool completed task.Wait(timeoutMs); if (!completed) { // 超时后关闭客户端释放资源 client.Close(); return false; } return true; }逻辑说明ConnectAsync返回TaskWait(timeoutMs)在指定毫秒内等待。如果超时task仍在后台尝试必须Close掉客户端避免资源泄漏。参数说明timeoutMs建议设为 3000 到 5000局域网内超过 5 秒基本可以判定网络不通。注意Wait会阻塞当前线程在 UI 线程里应改用await。3.2 心跳包的设计为什么不能只靠 TCP KeepAliveTCP 自带的 KeepAlive 默认两小时才发一次探测对实时性要求高的场景形同虚设。我一般会在应用层自己做心跳客户端每隔 5 秒发一个固定字节序列比如0x01服务端收到后回0x02。如果服务端连续 3 个周期没收到心跳就判定客户端掉线主动Close连接。客户端如果连续 3 次没收到回复就触发重连。心跳包要尽量小一个字节足够不要发 JSON 字符串浪费带宽还增加解析开销。3.3 断线重连的退避策略与线程安全断线重连不能死循环猛连否则服务端刚重启就被打满。常见做法是指数退避第一次等 1 秒第二次 2 秒第三次 4 秒上限 30 秒。重连成功后重置计数。另外TcpClient不是线程安全的多个线程同时读写同一个NetworkStream会出乱序。我一般把发送和接收放在两个独立线程发送线程只写接收线程只读用lock保护TcpClient的关闭操作。下面是一个简单的退避重连片段int retryDelay 1000; // 初始 1 秒 while (true) { try { client new TcpClient(); if (ConnectWithTimeout(client, 192.168.1.100, 8899, 3000)) { retryDelay 1000; // 重连成功重置退避 break; } } catch { } System.Threading.Thread.Sleep(retryDelay); retryDelay Math.Min(retryDelay * 2, 30000); // 翻倍上限 30 秒 }逻辑说明每次失败后休眠当前退避时长然后翻倍直到 30 秒封顶。参数说明初始 1 秒适合局域网广域网可以设 2 秒上限 30 秒是经验值再长会影响恢复速度。注意Sleep会阻塞线程在异步环境里用Task.Delay。4. 避坑与排查TcpListener 和 TcpClient 最容易翻车的 5 个地方4.1 现象客户端报「目标计算机积极拒绝」原因服务端根本没启动或者端口被防火墙拦截。解决先在服务端机器上netstat -ano | findstr 8899确认端口处于LISTENING状态。如果没有检查listener.Start()是否执行如果有但客户端仍连不上检查 Windows 防火墙入站规则临时关闭防火墙测试确认后添加入站端口放行规则。4.2 现象连接成功但收不到数据Read 一直阻塞原因NetworkStream.Read是阻塞的如果对方没发数据也没关闭连接它就永远等下去。解决给Read设置读超时stream.ReadTimeout 5000超时抛IOException捕获后决定重试还是断开。注意ReadTimeout只对同步Read有效异步ReadAsync要用CancellationToken。4.3 现象中文消息变成乱码原因发送端用Encoding.UTF8接收端用Encoding.Default编码不一致。解决两端统一用Encoding.UTF8并且发送时不要带 BOM。如果协议里混用二进制和文本建议在消息头里加一个字节标识消息类型文本部分固定用 UTF-8。4.4 现象服务端 Accept 一次之后就再也连不上原因AcceptTcpClient只接受一个连接处理完就Stop了。解决把AcceptTcpClient放在while(true)循环里每个连接开一个独立线程或Task处理主线程继续 Accept。注意每个TcpClient用完必须Close否则句柄泄漏连到一定数量后报「无法分配请求的地址」。4.5 现象程序退出后端口仍被占用重启报「地址已在使用」原因TcpListener没有调用Stop()或者 Socket 处于TIME_WAIT状态。解决在finally块里确保listener.Stop()和client.Close()执行。如果频繁重启可以在Start()之前设置listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true)允许端口复用。5. 进阶技巧用 TcpListener 和 TcpClient 做一套可验证的指令应答协议5.1 给字节流加一个 4 字节长度头裸 TCP 是字节流没有消息边界。如果客户端连发两条指令服务端一次Read可能全收到也可能只收到半条。我一般会在每条消息前面加 4 字节的int长度头表示后续正文的字节数。接收端先读 4 字节解析出长度 N再循环读满 N 字节。这样无论怎么拆包粘包都能还原出完整消息。下面是一个带长度头的发送封装void SendMessage(NetworkStream stream, string text) { byte[] body Encoding.UTF8.GetBytes(text); byte[] header BitConverter.GetBytes(body.Length); // 4 字节长度头 stream.Write(header, 0, 4); stream.Write(body, 0, body.Length); }逻辑说明先写长度头再写正文。接收端对应先读 4 字节BitConverter.ToInt32得到长度再读正文。参数说明BitConverter默认小端序两端机器架构一致时没问题如果跨大端小端平台需要手动统一字节序。注意stream.Write不保证一次写完循环写直到全部发出。5.2 用 Wireshark 验证握手与心跳是否按预期发出代码写完了不代表协议跑对了。我习惯在服务端机器上开 Wireshark过滤tcp.port 8899然后启动客户端。正常应该看到三次握手SYN、SYN-ACK、ACK。之后每隔 5 秒看到一对小包那就是心跳。如果只看到 SYN 没有 SYN-ACK说明服务端没监听或防火墙拦截。如果看到 RST说明端口没开。Wireshark 是排查 TCP 问题的黑匣子比看日志快得多。5.3 一个可复用的连接状态机把连接状态抽象成Disconnected、Connecting、Connected、Reconnecting四个状态用一个枚举变量记录。每次操作前检查状态避免在未连接时调Write抛异常。状态切换时打日志方便回溯。我一般会在Connected状态下才启动心跳线程断开时先停心跳再关流。这套状态机在多个项目里复用血泪经验是不要省这个枚举省了后面全是玄学 bug。5.4 压力测试用 100 个 TcpClient 并发连接看服务端表现服务端写好后用循环开 100 个TcpClient同时连接每个发一条消息就关。观察服务端内存和句柄数。如果句柄数持续上涨不降说明Close没调或没调对。如果连接数到 50 就报错检查listener的 backlog 参数默认是int.MaxValue但操作系统有上限。Windows 下可以用netstat -ano | findstr 8899 | find /c ESTABLISHED统计当前连接数。这个测试能提前暴露资源泄漏比上线后半夜被告警叫醒强。我自己的习惯是每次写完 TCP 通信代码先在本机用127.0.0.1跑通再用两台虚拟机跨网段跑最后用 Wireshark 抓一次包确认心跳和长度头都对。这套流程走下来基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表