ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C# TCP客户端开发实战:从Socket原理到断线重连全解析

C# TCP客户端开发实战:从Socket原理到断线重连全解析 在嵌入式、上位机、工业互联这些场景里泡久了你会发现“TCP客户端”这个看似基础的东西其实是整个通讯链路里最容易出幺蛾子的一环。很多人上手第一个网络程序就是TCP客户端写出来能连上、能收发就算完事但真正到了现场对接设备、对接服务器的时候才发现一堆隐性问题连不上、连上就断、收发数据错位、粘包、半包、缓冲区溢出……这篇文章我就以C#写一个简单TCP客户端的完整过程为例把从socket原理、代码实现到踩坑排查的所有细节一次说清楚。不管你是刚接触网络编程的新手还是被各种设备通讯协议折磨的老手这篇文章都能给你一些可以直接抄作业的参考。1. 客户端要做的那些事先搞清楚TCP通讯的本质1.1 一个TCP客户端到底在干什么很多人一上来就写代码结果连“客户端”这个概念都没完全吃透。简单说TCP通讯里永远有两个角色主动发起连接的一方叫客户端被动监听、等待连接的一方叫服务端。我们做的这个“简单的TCP通讯客户端”就是那个主动去敲门的人。从代码视角看客户端要做的事情非常明确知道服务器的IP地址和端口号这是前提一个都不能少主动发起TCP连接这一步会触发TCP三次握手内核帮你完成了不需要你手动管连接建立后可以发送数据给服务端可以接收服务端返回的数据连接断开后能感知到并且能按需重连。就这么点事但每一步都有坑。尤其是连接断开后的处理很多初学代码里压根没有这个逻辑导致程序跑一段时间就“死”了——其实不是死是socket已经断了你还拿着一个无效的连接在读写。1.2 为什么客户端比服务端“好写”但“难调”服务端要处理并发、多连接、半包粘包、优雅退出听着很难客户端看起来只要连一个目标简单多了。但实际工作中你会发现客户端的事故率一点不比服务端低。原因在于客户端往往要对接的是“别人家的服务端”——可能是PLC的TCP Server、可能是某台设备自带的网络模块、可能是一个自定义协议的网关。你没法控制对端的行为只能去适配。这就意味着客户端的代码必须足够稳健能把各种异常情况都兜住比如服务端不在线连接超时服务端主动断开你读数据时读到0字节网络中间设备重置连接收到RST读或写时抛异常服务端只发不答你的接收线程一直在等服务端应答格式不固定粘包、拆包、字节序问题。这些场景如果不在客户端代码里做防御性处理调试的时候就会非常痛苦。我的经验是客户端代码宁可多写几行异常分支也不要在现场抓瞎。1.3 什么时候真正需要自己写TCP客户端不少朋友会问现在各种高级语言、第三方库、现成软件一大堆为什么还要自己写一个TCP客户端这个问题得分情况看。像调试工具类的场景——用现成的串口助手、网络调试助手就能应付不需要写代码。但如果你要做一个自动化测试工具、一个设备对接的上位机、一个集成到业务系统里的通讯模块那就必须自己写客户端。特别是以下几种情况需要把TCP收到的数据直接解析到业务逻辑层中间不能有冗余环节需要把多个TCP连接统一管理比如同时连多台设备需要嵌入到现有程序里比如WinForm、WPF、服务里跑一个后台通讯线程需要实现私有协议、加密传输、特殊断线重连策略现成工具根本做不到。所以自己写一个TCP客户端不是为了重复造轮子而是为了把通讯链路完全掌握在自己手里。平时哪怕有更好的库我也建议你至少亲手从socket层面实现一遍这个底子打好了后面遇到任何通讯问题心里都有数。2. 动手前的技术准备理解TCP连接的“脾气”2.1 三次握手和四次挥手你要知道连接不是“瞬开瞬关”的TCP三次握手的原理网上讲得很多我这里用大白话点一下重点。客户端调用Connect()的那一刻内核会帮你发一个SYN包然后进入SYN_SENT状态等服务端回SYNACK再回一个ACK连接就建立了。这个过程不是瞬时完成的多少会有几十毫秒到几百毫秒的开销取决于网络质量和服务端处理能力。这个原理告诉我们一件事Connect()是有可能卡住的。如果对端IP根本不通或者服务端没启动SYN包发出去没人理客户端就会一直等。好在Socket自带的Connect超时机制可以设置下文代码里会讲把主动权拿回来别让界面卡死。四次挥手同理主动断开的一方会先发FIN然后走一个状态机。但客户端代码里一般不需要关心这个流程只需要知道你Close()之后端口不会立刻释放会进入TIME_WAIT状态如果高频重连可能会遇到端口占用问题。这就是为什么很多老手会告诉你客户端不要反复new Socket又Close能复用就复用。2.2 字节流的概念没有“消息边界”这件事初学者最容易掉的坑是把TCP当成“发一条收一条”的管道。实际不是。TCP是字节流协议它只保证你发出去的字节序列和收到的字节序列顺序一致但不保证每次Recv到的正好是一次Send的数据。比如你调用Send发了10个字节对端可能一次收到10个也可能分两次收到64。反过来你连续Send两次对端可能一次就把两段数据全收走了。这就是常说的粘包和拆包。理解这点是写客户端的基础不然你写个接收循环按固定长度去解析数据遇到拆包就会漏数据遇到粘包就会错位。那怎么办传统的做法是应用层自己约定边界常见三种思路固定长度每个数据包都是N字节不够就补齐收满N字节再解析分隔符每个包以特定符号结尾比如\r\n收到分隔符再处理长度头包头固定几个字节表示后面包体长度先收包头再收包体。工业设备通讯里用长度头的非常多比如Modbus TCP用的就是类似思路MBAP头里带了长度字段。我们的客户端代码里也要预留这种拆包逻辑的扩展位。2.3 大端小端通讯协议里的“字节序”细节TCP传的是字节流但你在代码里操作的是整型、浮点型这些多字节数据。这里就牵涉到字节序。C#在Windows平台上默认是小端序但很多网络协议尤其是Modbus TCP、GB/T协议、电力系统规约这些用的是大端序。如果你把一个int直接BitConverter.GetBytes之后塞进TCP流里发给一个按大端解析的服务端对方读出来的数完全不对。解决办法是用IPAddress.HostToNetworkOrder转换或者手动反转字节数组。这是做客户端通讯必会的细节我第一次对接设备时在这个问题上折腾了半天后来学乖了每对接一个协议都先确认字节序。3. 核心代码实现C# TcpClient最简示例与要点拆解3.1 最小可用版本连接、发送、接收一次跑通用C#的TcpClient封装类来做客户端是Windows下最省力的方式。它内部包装了Socket帮你省去了不少底层的痛苦。先给一个最小可用版本我习惯把连接、发送、接收都写在同一个类里职责清晰一点。using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; public class SimpleTcpClient { private TcpClient _tcpClient; private NetworkStream _stream; private readonly object _sendLock new object(); public bool IsConnected { get { if (_tcpClient null) return false; return _tcpClient.Connected _tcpClient.Client.Poll(0, SelectMode.SelectRead) false; } } public bool Connect(string ip, int port, int timeoutMs 3000) { try { _tcpClient new TcpClient(); // 异步方式连接再用Task等待超时实现可控的连接超时 var task _tcpClient.ConnectAsync(IPAddress.Parse(ip), port); if (!task.Wait(timeoutMs)) { _tcpClient.Close(); _tcpClient null; return false; } _stream _tcpClient.GetStream(); // 这两项分别控制发送和接收的缓冲策略看3.2节详解 _tcpClient.NoDelay true; _tcpClient.ReceiveBufferSize 8192; _tcpClient.SendBufferSize 8192; return true; } catch { _tcpClient?.Close(); _tcpClient null; return false; } } public void Send(byte[] data) { if (data null || data.Length 0) return; if (!IsConnected) throw new InvalidOperationException(连接已断开无法发送数据); lock (_sendLock) { _stream.Write(data, 0, data.Length); _stream.Flush(); } } public void Send(string text) { Send(Encoding.UTF8.GetBytes(text)); } public void Disconnect() { try { _stream?.Close(); _tcpClient?.Close(); } catch { } finally { _stream null; _tcpClient null; } } }这段代码麻雀虽小但几个细节是专门处理过坑的下面逐个说。3.2 为什么连接超时要这么写TcpClient.Connect没有原生超时初学的时候我直接用_tcpClient.Connect(ip, port)结果对方设备没开机程序在Connect这一步卡了几十秒才弹异常。界面直接假死用户体验极差。原因很简单TcpClient.Connect()的默认超时时间由操作系统决定一般是几十秒甚至更久。所以上面代码里用了ConnectAsync加Task.Wait(timeoutMs)的组合拳ConnectAsync返回一个Task不会阻塞调用线程Task.Wait(3000)最多等3秒超过3秒没完成说明连接失败或网络不通直接关闭客户端并返回false。这个写法比外部开一个定时器轮询要干净很多而且不会泄漏Task。注意一下调用Wait的时候如果超时Task的状态是RanToCompletion还是Faulted都不重要了直接把TcpClient关了就行不用担心后续的异步回调再来操作已经释放的资源。3.3 NoDelay、缓冲区大小影响实时性的两个小开关TcpClient.NoDelay true这行很有讲究。TCP底层有个Nagle算法它会把你连续几次小数据的发送合并成一个包减少网络报文数量。这在传大文件时是好事但在设备通讯这种“一发一答”的场景里是致命的——对方等你完整指令你的数据却被Nagle算法在缓冲区里聚着延迟变大响应变慢。把NoDelay设为true就是禁用Nagle算法每个Send尽量立刻发出去。实测在PLC通讯、传感器采集这种小包高频场景里能明显感受到响应变快。ReceiveBufferSize和SendBufferSize设置的是TcpClient内部缓冲区的建议值。调大一点有助于吞吐但也不是越大越好。对于普通的设备通讯8KB足够了。如果后面要传图片、传文件再按需调大也不迟。这两个值必须在连接建立后设置而且在内核层面只是“建议值”实际缓冲区大小以操作系统为准。3.4 断线判断的正确姿势别信Connected属性TcpClient.Connected是个非常坑的属性。它检测的不是对端是否还活着而是“本端socket是否处于连接状态”。只要本端没有感知到断开即使对端早就断电了这个属性照样返回true。所以我在代码里加了这么一段_tcpClient.Connected _tcpClient.Client.Poll(0, SelectMode.SelectRead) falseSocket.Poll(0, SelectMode.SelectRead)第一个参数填0表示非阻塞检查的返回值含义很微妙返回true说明socket有数据可读或者连接被重置/关闭了。这时候再用_tcpClient.Client.Available 0兜底就能区分“有数据可读”和“连接已断开”两种情况。上面这个表达式用 false配合Connected等于做了一次双重检查既要求TCP握手状态还保持着又要求没有收到对端的断开通知。实测下来服务端程序正常退出后客户端能很快感知到断开但对端直接断电的情况TCP要等TCP KeepAlive周期或者发送数据时才会发现这个没办法TCP协议就是这样设计的指望它实时感知物理链路断了是不现实的。3.5 Send为什么要加锁多线程下的写入安全我写的Send方法里加了一个lock (_sendLock)。为什么因为实际项目里界面线程要发送心跳后台逻辑线程可能也要发数据两个线程同时调用NetworkStream.Write是线程不安全的轻则数据交错重则直接抛异常。NetworkStream.Write内部虽然做了同步但它不能保证多次独立调用的原子性。加锁之后可以保证同一时刻只有一个线程在写流避免了数据帧交错和socket层面的线程竞争。那接收呢接收一般就一个线程循环读不太会有并发写的问题。但如果你的程序里有多个线程同时复用一个TcpClient强烈建议收、发各持一把锁或者干脆用一个管道队列串行化所有读写操作这样最稳。4. 接收数据的处理从裸字节流到可用的消息4.1 接收循环与粘包拆包处理一个可复用的模板服务端发来的数据客户端怎么收最基础的做法是起一个后台线程死循环里Read。但光Read还不够得处理粘包和拆包。这里给一个简单的接收模板按“包头长度包体内容”的常见协议来处理private async Task ReceiveLoop() { byte[] buffer new byte[4096]; MemoryStream ms new MemoryStream(); try { while (IsConnected) { int bytesRead await _stream.ReadAsync(buffer, 0, buffer.Length); if (bytesRead 0) { // 对端正常关闭退出循环 break; } // 把读取到的内容暂存到内存流里 ms.Write(buffer, 0, bytesRead); // 尝试从暂存区解析出完整的数据包 while (TryParsePacket(ms, out byte[] packet)) { ProcessPacket(packet); } // 防止内存流无限增长超过一定大小直接清空保护性措施 if (ms.Length 1024 * 1024) { ms.SetLength(0); } } } catch (Exception ex) { Console.WriteLine($接收异常: {ex.Message}); } finally { Disconnect(); } }这里注意几个细节ReadAsync会阻塞等待数据返回0表示对端优雅关闭了连接这时候要主动退出循环并清理资源如果对端是异常断开通常会抛IOException或者ObjectDisposedException被catch兜住也算退出循环。4.2 TryParsePacket的思路按协议解析一条完整报文真正解析报文的地方在TryParsePacket里。这里我按最常见的“2字节长度头包体”的协议举例子。假设协议规定前2字节大端序表示后面包体的长度也就是length 包体字节数那一帧数据的完整长度就是2 length。private bool TryParsePacket(MemoryStream ms, out byte[] packet) { packet null; byte[] buffer ms.GetBuffer(); int validLen (int)ms.Length; int startIndex 0; if (validLen 2) { return false; // 连长度头都没凑齐 } int bodyLen (buffer[0] 8) | buffer[1]; int totalLen 2 bodyLen; if (validLen totalLen) { return false; // 数据不够一帧继续等 } packet new byte[totalLen]; Array.Copy(buffer, startIndex, packet, 0, totalLen); // 移除已解析的数据把剩余数据往前挪 byte[] remaining new byte[validLen - totalLen]; Array.Copy(buffer, totalLen, remaining, 0, remaining.Length); ms.SetLength(0); ms.Write(remaining, 0, remaining.Length); return true; }这个逻辑看起来简单却是客户端最核心的骨架。实际项目的协议可能会复杂得多——可能长度头里包含协议类型、消息ID、CRC校验可能还有多个长度字段。但套路是不变的先暂存、再尝试解析、解析失败就继续等数据、解析成功就处理、处理完继续解析剩余数据。4.3 收到数据之后的处理线程别在接收线程里做重活记住了接收线程里拿到一个完整包之后不要直接在同一个线程里做耗时操作——比如写数据库、调用远程API、处理大文件、驱动界面控件。这样会阻塞接收循环导致后续数据迟迟得不到处理。正确的做法是接收线程只负责把数据包交给一个“处理队列”再由一个独立的工作线程或者线程池去消费队列。最简单的实现就是ConcurrentQueuebyte[]加一个后台消费线程。这种“生产者-消费者”模型在通讯软件里是标配刚开始写客户端可能会有侥幸心理到数据量一大、处理一慢就暴露问题了。4.4 关于Encoding不要迷信UTF8很多初学者收发字符串时无脑用Encoding.UTF8在两台Windows机器之间通讯没问题但一旦对接工业设备、老系统就经常对不上。原因在于很多设备默认发的是ASCII、GB2312在中文字段里或者干脆是纯Byte的十六进制表示。我的习惯是发送和接收都先以byte[]为单位处理用到字符串时再显式指定编码。如果对接的文档里写了“ANSI编码”在Windows上就用Encoding.Default注意.NET Core里Default不再是系统ANSI码页要用Encoding.GetEncoding(0)如果文档写的是UTF8才用UTF8。宁可多一步字节转换也不要让编码问题成为排查黑盒。5. 实操实录用最小客户端对接一个模拟服务端5.1 场景设定与工具准备纸上谈兵没意思这里我们实际搭一个能跑的实验环境。用C#写一个极简的监听服务端再拿上面写的客户端去连接整个过程走一遍。为了排除外部环境干扰我用的工具是:服务端C# TcpListener监听本机127.0.0.1的8899端口收到数据后原样返回也就是echo模式客户端上面写的SimpleTcpClient加一个简单的手动输入。这个echo服务端的作用是让我们能验证客户端的发送、接收、拆包逻辑不会因为对端行为复杂而混淆问题。5.2 服务端代码仅用于实验using System; using System.Net; using System.Net.Sockets; using System.Text; class EchoServer { static void Main() { TcpListener listener new TcpListener(IPAddress.Loopback, 8899); listener.Start(); Console.WriteLine(服务端已启动监听127.0.0.1:8899); while (true) { TcpClient client listener.AcceptTcpClient(); Console.WriteLine(有客户端连入); NetworkStream stream client.GetStream(); byte[] buffer new byte[1024]; try { while (true) { int len stream.Read(buffer, 0, buffer.Length); if (len 0) break; Console.WriteLine($收到 {len} 字节: {BitConverter.ToString(buffer, 0, len)}); stream.Write(buffer, 0, len); // 原样返回 stream.Flush(); } } catch (Exception ex) { Console.WriteLine($连接异常: {ex.Message}); } finally { client.Close(); Console.WriteLine(客户端断开); } } } }这个服务端每收到一段数据就原样返回正好用来测试客户端各种行为。启动服务端后它就开始监听8899端口任何客户端连上它都会被记录和回应。5.3 客户端调试实录连通性、收发、断线检测我现在开一个控制台程序用SimpleTcpClient连上echo服务端然后手动输入字符串发给服务端观察返回情况。第一步连接SimpleTcpClient client new SimpleTcpClient(); bool ok client.Connect(127.0.0.1, 8899, 3000); Console.WriteLine(ok ? 连接成功 : 连接失败);这一步会经历三次握手如果服务端是开的ok为true。把服务端关掉再测试3秒后返回false程序不会被卡住。第二步发送一条指令并接收client.Send(ping); byte[] resp client.Receive(); // 需要自行实现一个简易接收方法 Console.WriteLine(Encoding.UTF8.GetString(resp));为了演示我临时在客户端里加了一个阻塞接收的方法读一次流里的数据并返回。echo服务端收到“ping”后会原样返回客户端收到“ping”输出正常。第三步重点来了直接强制关闭服务端程序不是优雅退出而是杀进程观察客户端状态。IsConnected属性会在一小段时间内仍然返回true直到TCP协议栈感知到连接异常。如果是正常关闭服务端程序Exit会触发四次挥手客户端会在下一次读操作时收到0字节从而进入断开流程。这个差异一定要心里有数前者考验的是你的心跳机制后者是你标准的断开路径。5.4 断线重连的设计心跳与自动重连既然聊到断线干脆把重连策略也一并给了。现场对接设备网络抖动、设备重启是家常便饭客户端必须能自动恢复连接。我常用的套路是启动时连接失败则每隔N秒重试连接成功后开启一个心跳线程每30秒发一条心跳指令如果连续几次心跳都没有应答主动断开并进入重连流程接收循环意外退出时触发重连。一个简化版本的长这样while (true) { if (client null || !client.IsConnected) { client new SimpleTcpClient(); bool ok client.Connect(ip, port, 3000); if (!ok) { Console.WriteLine(连接失败5秒后重试); Thread.Sleep(5000); continue; } // 启动接收线程 _ Task.Run(() client.ReceiveLoop()); } Thread.Sleep(1000); }这里要注意的是心跳指令不能干扰业务指令。有些设备协议里明确规定了心跳报文格式有些没有那就自己约定一个不和业务冲突的报文或者干脆依靠TCP保活机制设置TcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true)。不过TCP keepalive默认2小时才探一次对实时性要求高的场景还是要应用层心跳。6. 常见问题与排查技巧实录6.1 连不上服务端的几种典型原因有丰富的实战经验的人不会满足于多试几次下面这些我都酒过放个清单看得快一点。现象可能原因排查方法连接超时IP不通、对方防火墙拦了、服务端没启动先ping通IP再用telnet ip port测试端口立刻拒绝服务端没监听该端口、端口被占检查服务端监听地址是否是0.0.0.0检查端口占用连上了但立即断开服务端accept后主动关闭、白名单限制抓包看是否有RST查服务端日志局域网可连跨网段不行防火墙、路由ACL、NAT问题检查防火墙入站规则和路由器端口映射这里头最常见的就是服务端监听在127.0.0.1而客户端连的是局域网IP。TcpListener监听127.0.0.1只接受本机回环连接要接受外部连接必须监听0.0.0.0或具体网卡IP。6.2 粘包/拆包问题现象、判断与应对如果你发现客户端收到的数据偶尔对不上、偶尔丢字、最后一个包不完整、解析经常错位基本就是粘包或拆包。判断方法在接收日志里打上每次收到多少字节如果出现“一次收到两次发送的数据”或者“一次发送的数据分两次收到”就确认问题了。应对思路上面讲过固定长度、分隔符、长度头三选一。设备通讯最稳的是长度头因为二进制协议里分隔符容易和数据本身冲突固定长度又浪费带宽。6.3 为什么发送100字节对端只收到几十字节这个坑比粘包更隐蔽。有些朋友在循环里直接stream.Write(buffer, 0, bytesToSend.Length)但Write并不保证一次写完虽然TcpClient内部做了缓冲大多数情况能写完但理论上有部分写入的可能。严谨的写法是循环写入int offset 0; int len data.Length; while (offset len) { int written _stream.Write(data, offset, len - offset); offset written; }不过NetworkStream.Write在.NET里通常一次性写完我标这段话的目的是提醒如果你将来直接用底层Socket.Send或者对接的是其他语言的socket实现就要警惕“短写”问题。6.4 客户端偶发闪退报错0000005开头热词列表里有一条“qt写的关于can通讯的软件很容易闪退报0000005”这个错误是Windows下的访问违规Access Violation本质是程序访问了非法内存地址。虽然说的是CAN通讯的Qt程序但TCP客户端同样可能踩这个雷——C#里通常表现为ObjectDisposedException、NullReferenceExceptionC/Qt里就直接崩了。这类崩溃最常见的几个源头接收线程还在阻塞Read另一个线程把socket关了导致Read操作对象被释放事件回调里访问了已被回收的控件或对象多线程访问同一个非线程安全的集合比如List 没加锁被并行修改。应对手段就一句话socket的关闭和所有读写操作必须在同一个串行上下文里协调或者用CancellationToken、标志位通知接收线程退出不要直接无脑Close。这个教训我从多次崩溃中总结出来的多看两眼不亏。6.5 关于“心跳”和“超时重传”别把小问题拖成大问题TCP协议自身有超时重传机制发送的数据如果丢了会重传。但如果对端网络不可达重传次数到上限后连接就会被内核标记为断开。这个过程可能持续几分钟对业务是致命的。所以应用层心跳做得越早恢复越快。实际操作中我习惯把心跳周期和服务端业务超时时间对齐心跳周期 服务端超时时间 / 3。比如服务端30秒没收到业务数据就断开心跳就10秒发一次这样服务端永远不会因为空闲而断开你。7. 代码之外的思考一个优秀TCP客户端应该具备的体面7.1 日志系统通讯程序必须有“黑匣子”我现在写的每一个通讯模块第一件事就是加日志。不是用Console.WriteLine糊弄而是接入到日志框架里记录下每次连接的建立、断开、收发方向、字节长度、内容涉敏的脱敏、异常堆栈。为什么因为现场排查问题时如果没有日志你连“数据到底是没发出去还是对方没收到”都说不清。日志分级上我会重点关注连接成功、连接失败、重连发送了哪条指令、收到了什么响应异常解析时原始字节内容断开原因正常关闭、超时、异常。有了一套好日志你自己和对接方都能省很多时间。很多时候设备厂商想甩锅你把时间戳对齐的日志拍过去谁的问题一看便知。7.2 配置化IP、端口、超时不要写死在代码里这个踩过坑的人太多了——程序写死了IP和端口拿到现场发现IP不同只能重新编译。一个体面的客户端IP、端口、连接超时、心跳间隔、重连间隔都应该放在配置文件里至少也得放在程序入口的参数里。我用的是比较朴素的做法一个Config类从INI、JSON或者appsettings里读配置然后注入到客户端。这样现场改一下配置文件重启就能换设备不用动代码。7.3 多连接管理一个进程连N台设备怎么办实际项目里一个上位机同时连多台设备是常态。这时候不要new N份代码复制粘贴应该把TcpClient封装成一个可复用的组件每个设备一个实例用名称或设备ID做Key存到字典里统一管理。我习惯额外包一层DeviceSession类里面除了TcpClient还持有设备状态、命令队列、重连次数、最近心跳时间这些状态字段。逻辑复杂时光有一个socket是远远不够的。最后再分享一个小技巧如果你用C#写TCP客户端调试阶段可以把TcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true)加上这个选项允许你在TIME_WAIT状态下快速重新绑定同一端口。做高频重连测试时能少踩很多“Address already in use”的坑。另外再说一句TCP客户端的代码一定要把“连接不成功时怎么办”“连接中途断了怎么办”“收到了垃圾数据怎么办”这三种情况考虑清楚再收工这比多写几个炫技的封装方法实在得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进