☰
C# TCP通信实战:从粘包处理到生产级双端例程
2026/10/12 2:35:48 网站建设 项目流程

简介:本资源是一套面向C#初学者与网络编程入门者的TCP/IP通信实战例程,聚焦客户端与服务端双向通信的核心实现,帮助开发者快速掌握Socket编程基础。压缩包共55个文件,包含12个核心C#源码文件(如Program.cs、ClientFrm.cs、ServerFrm.cs)、8个可直接运行的exe程序(客户端.exe、服务端.exe等)、2个Visual Studio解决方案(.sln)及配套项目文件(.csproj、.resx、.settings等),另有文本说明与资源文件辅助理解,整体仅450KB,轻量易解压、即开即用。已有206人学习下载,适合在Windows平台快速验证TCP连接、数据收发、多实例交互等典型场景。读者可直接编译运行源码,观察控制台通信过程,结合代码注释理解Bind/Listen/Accept、Connect/Write/Read等关键流程,并参考目录中分离的ClientFile与ServerFile双工程结构,建立清晰的客户端-服务端开发范式。

1. 为什么“TCP/IP C#最简单例程”不是入门捷径,而是避坑起点?

很多刚接触网络编程的开发者,一搜“TCP/IP C#最简单例程”,就急着复制粘贴两段代码:一个TcpClient连接,一个TcpListener监听,跑通了“Hello World”式收发,就以为吃透了 TCP 通信。结果一到真实场景——比如客户端断网重连、服务端并发百连接、消息粘包拆包、超时控制、异常中断恢复——立刻翻车。这不是代码写得不够“简单”,而是“最简单例程”常隐去最关键的边界逻辑:它没告诉你NetworkStream.Read()可能只读到部分数据,没提醒你TcpClient.Close()不等于连接立即消失,更不会在注释里写“此处不加try-catch,生产环境必崩”。本文不讲抽象协议栈分层,也不堆 RFC 文档,就用一个可直接编译运行、带完整异常防护、含真实调试日志、适配 .NET 6+ 的最小可行双端例程,把 C# 中 TCP 客户端与服务端从“能通”做到“稳通”。适合正在写毕业设计通信模块、接手老旧工控上位机、或为物联网边缘设备开发轻量通信层的开发者——你要的不是教科书定义,是今天下午就能跑起来、明天上线不掉链子的那套东西。


2. 从零构建可落地的 TCP 双端骨架:基于System.Net.Sockets的现代写法

C# 中实现 TCP 通信,核心是System.Net.Sockets命名空间下的类型。.NET Core 3.0+和.NET 5/6/7/8已全面支持async/await异步模型,绝不要用BeginAccept/EndAccept这类已标记[Obsolete]的 APM 模式——它们不仅代码冗长,更难处理取消、超时和上下文捕获。本节构建的服务端与客户端均采用Task-based 异步 API,并严格遵循IDisposable模式管理资源生命周期。

2.1 服务端:TcpListener+AcceptAsync实现高响应监听

服务端需持续监听端口,接受新连接,并为每个连接启动独立处理任务。关键点在于:不能阻塞主线程,不能让单个连接故障拖垮整个服务,且必须支持优雅关闭。

// Server.cs using System.Net; using System.Net.Sockets; using System.Text; public class TcpEchoServer : IDisposable { private readonly TcpListener _listener; private bool _isRunning = false; private readonly CancellationTokenSource _cts = new(); public TcpEchoServer(IPAddress address, int port) { _listener = new TcpListener(address, port); } public async Task StartAsync() { _listener.Start(); _isRunning = true; Console.WriteLine($"[Server] Started on {(_listener.LocalEndpoint as IPEndPoint)?.ToString()}"); while (_isRunning) { try { // AcceptAsync 返回 Task<TcpClient>,非阻塞等待新连接 var client = await _listener.AcceptTcpClientAsync(_cts.Token); // 为每个连接启动独立任务,避免串行处理 _ = HandleClientAsync(client); } catch (OperationCanceledException) { break; // 关闭信号触发 } catch (ObjectDisposedException) { break; // listener 已释放 } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted) { // 系统中断(如 Ctrl+C),忽略 continue; } catch (Exception ex) { Console.WriteLine($"[Server] Accept error: {ex.Message}"); } } } private async Task HandleClientAsync(TcpClient client) { var endpoint = client.Client.RemoteEndPoint?.ToString() ?? "unknown"; Console.WriteLine($"[Server] New connection from {endpoint}"); try { using var stream = client.GetStream(); var buffer = new byte[1024]; while (client.Connected) { int bytesRead; try { // ReadAsync 可能返回 0(对端关闭)或部分数据(粘包) bytesRead = await stream.ReadAsync(buffer, _cts.Token); } catch (IOException ex) when (ex.InnerException is SocketException se && se.SocketErrorCode == SocketError.ConnectionReset) { Console.WriteLine($"[Server] Client {endpoint} reset connection"); break; } catch (OperationCanceledException) { break; } if (bytesRead == 0) { Console.WriteLine($"[Server] Client {endpoint} disconnected gracefully"); break; } // 回显收到的数据(UTF-8 编码) string message = Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine($"[Server] Received from {endpoint}: '{message.Trim()}'"); // 发送回显(注意:WriteAsync 不保证一次发完,但本例小数据可忽略) await stream.WriteAsync(Encoding.UTF8.GetBytes($"ECHO: {message}"), _cts.Token); } } catch (Exception ex) { Console.WriteLine($"[Server] Handle client error: {ex.Message}"); } finally { client.Close(); // 显式关闭,释放底层 socket Console.WriteLine($"[Server] Connection with {endpoint} closed"); } } public void Dispose() { _cts.Cancel(); _listener.Stop(); _cts.Dispose(); _listener?.Dispose(); } }

逻辑说明:

  • StartAsync()使用while(_isRunning)循环 +AcceptTcpClientAsync()实现非阻塞监听;
  • HandleClientAsync()为每个连接创建独立Task,避免一个连接卡死影响其他连接;
  • stream.ReadAsync()是核心:它返回实际读取字节数,必须检查bytesRead == 0判断对端是否关闭,这是粘包/半包处理的起点;
  • 所有await操作均传入_cts.Token,确保关闭时能及时中止异步操作;
  • finally块中调用client.Close()是硬性要求,否则 socket 资源泄漏,Windows 下表现为TIME_WAIT状态堆积。

2.2 客户端:TcpClient+ConnectAsync实现健壮连接与重试

客户端难点不在连接,而在如何应对网络抖动、服务端未启动、DNS 解析失败等现实问题。一个“最简单”的new TcpClient().Connect()在生产环境毫无容错能力。

// Client.cs using System.Net; using System.Net.Sockets; using System.Text; public class TcpEchoClient : IDisposable { private readonly string _host; private readonly int _port; private readonly TimeSpan _connectTimeout = TimeSpan.FromSeconds(5); private readonly TimeSpan _readWriteTimeout = TimeSpan.FromSeconds(10); private TcpClient? _client; private NetworkStream? _stream; public TcpEchoClient(string host, int port) { _host = host; _port = port; } public async Task<bool> ConnectAsync() { try { _client = new TcpClient(); // 设置连接超时(.NET 6+ 支持) var connectTask = _client.ConnectAsync(_host, _port); var timeoutTask = Task.Delay(_connectTimeout); var completedTask = await Task.WhenAny(connectTask, timeoutTask); if (completedTask == timeoutTask) { throw new TimeoutException($"Connection to {_host}:{_port} timed out after {_connectTimeout.TotalSeconds}s"); } await connectTask; // 确保连接完成,抛出可能异常 _stream = _client.GetStream(); _stream.ReadTimeout = (int)_readWriteTimeout.TotalMilliseconds; _stream.WriteTimeout = (int)_readWriteTimeout.TotalMilliseconds; Console.WriteLine($"[Client] Connected to {_host}:{_port}"); return true; } catch (Exception ex) { Console.WriteLine($"[Client] Connect failed: {ex.GetType().Name} - {ex.Message}"); Disconnect(); return false; } } public async Task<string?> SendAndReceiveAsync(string message) { if (_stream == null || !_client!.Connected) return null; try { var data = Encoding.UTF8.GetBytes(message + "\n"); // 添加换行符便于服务端识别消息边界 await _stream.WriteAsync(data, CancellationToken.None); // 读取响应(简单起见,读到换行或超时) var responseBuffer = new List<byte>(); int b; while ((b = await _stream.ReadByteAsync()) != '\n' && b != -1) { responseBuffer.Add((byte)b); if (responseBuffer.Count > 4096) // 防止无限读 break; } return responseBuffer.Count > 0 ? Encoding.UTF8.GetString(responseBuffer.ToArray()).Trim() : null; } catch (Exception ex) { Console.WriteLine($"[Client] Send/Receive error: {ex.GetType().Name} - {ex.Message}"); Disconnect(); return null; } } public void Disconnect() { _stream?.Dispose(); _client?.Close(); _stream = null; _client = null; } public void Dispose() { Disconnect(); } }

参数说明:

  • _connectTimeout:连接阶段超时,防止ConnectAsync卡死(如目标端口无监听);
  • _readWriteTimeout:设置NetworkStream的读写超时,这是防止ReadAsync永久挂起的关键;
  • SendAndReceiveAsync中添加\n作为消息分隔符,是解决粘包问题的最简方案(后续章节详解);
  • ByteReadAsync()逐字节读取直到\n,虽效率不高,但逻辑清晰、无缓冲区溢出风险,适合教学与轻量场景;
  • 所有异常分支均调用Disconnect(),确保资源清理。

2.3 主程序:组合双端并加入基础日志与控制流

将服务端与客户端实例化,通过命令行参数控制角色,并加入Console.CancelKeyPress支持 Ctrl+C 优雅退出。

// Program.cs using System; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { if (args.Length < 1) { Console.WriteLine("Usage: tcpdemo server|client [host] [port]"); return; } var role = args[0].ToLower(); var host = args.Length > 1 ? args[1] : "127.0.0.1"; var port = args.Length > 2 ? int.Parse(args[2]) : 8080; // 注册 Ctrl+C 事件,触发优雅关闭 Console.CancelKeyPress += (sender, e) => { e.Cancel = true; // 阻止进程立即终止 Console.WriteLine("\n[Main] Shutdown signal received..."); }; try { if (role == "server") { using var server = new TcpEchoServer(IPAddress.Any, port); Console.WriteLine($"Starting TCP Echo Server on port {port}..."); await server.StartAsync(); } else if (role == "client") { using var client = new TcpEchoClient(host, port); Console.WriteLine($"Starting TCP Echo Client to {host}:{port}..."); // 尝试连接,失败则退出 if (!await client.ConnectAsync()) return; // 发送几条测试消息 var testMessages = new[] { "Hello Server!", "How are you?", "Goodbye!" }; foreach (var msg in testMessages) { Console.WriteLine($"[Client] Sending: '{msg}'"); var response = await client.SendAndReceiveAsync(msg); Console.WriteLine($"[Client] Received: '{response}'"); await Task.Delay(500); // 人为延时,便于观察 } } else { Console.WriteLine("Unknown role. Use 'server' or 'client'"); } } catch (Exception ex) { Console.WriteLine($"[Main] Fatal error: {ex}"); } finally { Console.WriteLine("[Main] Exiting..."); } } }

关键设计点:

  • Console.CancelKeyPress事件中设置e.Cancel = true,使主程序能捕获中断信号并执行Dispose();
  • using语句确保TcpEchoServer和TcpEchoClient在作用域结束时自动释放资源;
  • 客户端测试循环中加入Task.Delay(500),避免快速发送导致服务端来不及处理(尤其在调试模式下);
  • 所有Console.WriteLine均带[Role]前缀,方便区分服务端/客户端日志流。

3. 粘包与半包:TCP 流式传输的“玄学”问题与三招落地解法

TCP 是面向字节流的协议,它不保证应用层消息边界。你调用WriteAsync发送"A"和"B"两次,对端ReadAsync可能一次性读到"AB"(粘包),也可能第一次读到"A"、第二次读到"B"(半包)。这是所有 TCP 应用绕不开的“玄学”门槛。新手常误以为“只要发一次、收一次就行”,结果在压力测试或跨网段通信时数据错乱。本节不讲理论推导,只给三种可直接抄作业的工程解法,按复杂度递进。

3.1 方案一:消息定界符(Delimiter-Based)——最简可用,适合文本协议

在每条消息末尾添加唯一分隔符(如\n,\r\n,|),接收方持续读取直到遇到该符号。优点是实现极简、人类可读;缺点是分隔符不能出现在消息体中(需转义)。

// 服务端接收逻辑片段(替换原 HandleClientAsync 中的读取部分) private async Task<string?> ReadMessageWithDelimiterAsync(NetworkStream stream, char delimiter = '\n') { var buffer = new List<byte>(); int b; while ((b = await stream.ReadByteAsync()) != delimiter && b != -1) { buffer.Add((byte)b); if (buffer.Count > 8192) // 限制最大消息长度,防内存耗尽 throw new InvalidOperationException("Message too long, possible delimiter missing"); } return buffer.Count > 0 ? Encoding.UTF8.GetString(buffer.ToArray()) : null; } // 客户端发送时确保结尾有分隔符 await _stream.WriteAsync(Encoding.UTF8.GetBytes(message + "\n"), CancellationToken.None);

适用场景:日志上报、配置同步、简单指令交互(如"SET TEMP=25");
血泪经验:分隔符选\n最安全,避免\r\n在跨平台时歧义;务必加长度限制,否则恶意客户端发巨长无分隔符数据可致服务端 OOM。

3.2 方案二:消息头+长度前缀(Length-Prefixed)——二进制友好,工业级首选

在每条消息前添加固定字节数的长度字段(如 4 字节int32),接收方先读长度,再按长度读取正文。无转义负担,高效稳定。

// 服务端读取完整消息(支持任意长度) private async Task<byte[]> ReadFullMessageAsync(NetworkStream stream) { // 先读 4 字节长度 var lenBuffer = new byte[4]; int totalRead = 0; while (totalRead < 4) { int read = await stream.ReadAsync(lenBuffer, totalRead, 4 - totalRead, CancellationToken.None); if (read == 0) throw new IOException("Connection closed while reading length"); totalRead += read; } int messageLength = BitConverter.ToInt32(lenBuffer, 0); // 再读 messageLength 字节正文 var msgBuffer = new byte[messageLength]; totalRead = 0; while (totalRead < messageLength) { int read = await stream.ReadAsync(msgBuffer, totalRead, messageLength - totalRead, CancellationToken.None); if (read == 0) throw new IOException("Connection closed while reading message body"); totalRead += read; } return msgBuffer; } // 客户端发送(需先序列化消息为字节数组) public async Task SendBinaryMessageAsync(byte[] payload) { var lenBytes = BitConverter.GetBytes(payload.Length); await _stream.WriteAsync(lenBytes, CancellationToken.None); await _stream.WriteAsync(payload, CancellationToken.None); }

参数说明:

  • BitConverter.ToInt32()默认使用小端序(Little-Endian),.NET 标准,无需额外处理;
  • ReadAsync循环读取确保获取完整长度和正文,这是处理半包的核心;
  • 长度字段大小可选 2 字节(UInt16,最大 64KB)、4 字节(Int32,最大 2GB),根据业务消息大小选择;
  • 此方案天然支持二进制数据(图片、音频片段),无编码/转义开销。

3.3 方案三:Protocol Buffers + gRPC(进阶)——当你的“简单例程”开始长出肌肉

当项目规模扩大,需要多语言互通、强类型契约、流式 RPC、内置超时与重试时,“手写 TCP”应让位于成熟框架。protobuf-net.Grpc是 C# 生态最轻量的 gRPC 实现,仅需定义.proto文件,自动生成强类型客户端/服务端。

// echo.proto syntax = "proto3"; package echo; service EchoService { rpc Echo (EchoRequest) returns (EchoResponse); } message EchoRequest { string message = 1; } message EchoResponse { string echoed_message = 1; }
// 服务端注册(Program.cs 中) var builder = WebApplication.CreateBuilder(args); builder.Services.AddCodeFirstGrpc(); // 启用 gRPC var app = builder.Build(); app.MapGrpcService<EchoService>(); // 注册服务实现 // 客户端调用(比原始 TCP 更简洁) var channel = GrpcChannel.ForAddress("http://localhost:5000"); var client = new Echo.EchoService.EchoServiceClient(channel); var reply = await client.EchoAsync(new EchoRequest { Message = "Hello gRPC!" }); Console.WriteLine(reply.EchoedMessage);

价值点:

  • 自动生成序列化/反序列化代码,零粘包风险;
  • 天然支持流式调用(IAsyncEnumerable<T>)、截止时间(Deadline)、取消令牌;
  • 服务发现、负载均衡、TLS 加密等企业级能力开箱即用;
  • 不是替代 TCP,而是构建在 TCP 之上的更高层抽象——它帮你把“粘包”这个脏活彻底封装掉了。

4. 避坑指南:C# TCP 开发中 5 个高频翻车现场与后悔药

写 TCP 代码最怕的不是功能做不出来,而是线上跑着跑着突然不收消息、连接数暴涨、CPU 100%、日志一片空白。这些往往源于几个经典误区。以下是我在线上系统维护中踩过的坑,按“现象→原因→解决”结构整理,每一条都附带可验证的复现方式。

4.1 现象:客户端ConnectAsync永远不返回,程序假死

原因:未设置连接超时,且目标 IP 存在但端口无监听(如防火墙拦截、服务未启动),ConnectAsync在 Windows 上默认等待约 21 秒才超时。
解决:必须用Task.WhenAny()包裹ConnectAsync并设置明确超时,如 2~5 秒。.NET 6+虽支持TcpClient.ConnectAsync(host, port, timeout),但内部仍建议用Task.WhenAny更可控。
验证:启动服务端前运行客户端,观察控制台输出是否在设定秒数后报错。

4.2 现象:服务端ReadAsync读到 0 字节后,连接未关闭,while(client.Connected)无限循环

原因:TcpClient.Connected属性仅反映上次 I/O 操作后的状态,不是实时连接检测。对端静默断开(如拔网线)时,该属性仍为true,导致空循环消耗 CPU。
解决:永远不要依赖client.Connected作为循环条件。正确做法是检查ReadAsync返回值:bytesRead == 0表示对端已关闭连接,应跳出循环并client.Close()。
验证:服务端运行中,用任务管理器结束客户端进程,观察服务端日志是否打印 “disconnected gracefully”。

4.3 现象:发送大文件时,客户端WriteAsync报IOException: An existing connection was forcibly closed by the remote host

原因:服务端未及时读取数据,TCP 接收窗口填满,内核丢弃后续数据包,最终触发 RST。常见于服务端ReadAsync逻辑过重(如同步文件写入)或未启用NoDelay。
解决:服务端TcpClient创建后立即设置client.NoDelay = true(禁用 Nagle 算法),并确保ReadAsync循环不阻塞;客户端大文件发送需分块(如 8KB/次)并 await 每次写入。
验证:服务端HandleClientAsync中Thread.Sleep(1000)模拟慢处理,客户端发 1MB 数据,观察是否报错。

4.4 现象:多客户端并发连接,服务端AcceptTcpClientAsync抛SocketException: Only one usage of each socket address is normally permitted

原因:服务端频繁启停,TIME_WAIT状态 socket 未释放完毕,新绑定端口失败。
解决:服务端TcpListener创建时,设置TcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);或改用IPAddress.Any绑定,避免端口冲突。
验证:快速连续启动/关闭服务端多次,观察是否报地址占用错误。

4.5 现象:客户端断网重连后,服务端HandleClientAsync任务仍在运行,但ReadAsync抛ObjectDisposedException

原因:TcpClient或NetworkStream被 dispose 后,其关联的Task仍在await,导致异常未被捕获而终止任务。
解决:在HandleClientAsync的catch块中捕获ObjectDisposedException和InvalidOperationException,并确保finally中client.Close();更佳实践是使用CancellationToken控制整个任务生命周期。
验证:客户端运行中,禁用本地网络适配器,观察服务端日志是否出现未处理异常。


5. 生产就绪 checklist:从“能跑”到“敢上”的 7 个硬性动作

写完一个能dotnet run的 TCP 例程,离部署到树莓派采集终端、工厂 PLC 上位机、或车载诊断盒子,中间隔着七道关。我经手的十几个嵌入式通信项目,凡是跳过下面任何一项的,上线后都付出了额外 2~3 人日的排查成本。这不是最佳实践清单,是血换来的上线前必检项。

5.1 必做:启用TcpClient.NoDelay = true(禁用 Nagle 算法)

Nagle 算法会合并小包以提升吞吐,但牺牲延迟。工业控制、实时指令、心跳包等场景,毫秒级延迟不可接受。C# 中必须显式关闭:

// 服务端接受连接后 var client = await _listener.AcceptTcpClientAsync(); client.NoDelay = true; // 关键!默认为 false // 客户端连接后 _client = new TcpClient(); await _client.ConnectAsync(host, port); _client.NoDelay = true; // 同样关键

为什么重要:默认开启时,若你连续发送"CMD1"、"CMD2"两个短指令,TCP 可能合并为一个包发出,服务端ReadAsync一次读到"CMD1CMD2",导致解析失败。NoDelay=true强制立即发送。

5.2 必做:为所有ReadAsync/WriteAsync设置CancellationToken

CancellationToken不是锦上添花,是防止“幽灵任务”的救命绳。服务端关闭时,若某个HandleClientAsync正在await stream.ReadAsync(),它会永远挂起,无法释放内存和 socket。

// 错误:无 token int bytesRead = await stream.ReadAsync(buffer); // 正确:传入取消令牌 int bytesRead = await stream.ReadAsync(buffer, _cts.Token);

实操技巧:将CancellationTokenSource作为服务端/客户端类的私有字段,在Dispose()中调用_cts.Cancel();所有await操作均传入_cts.Token。这样Ctrl+C或Dispose()时,所有挂起的异步操作会立即抛OperationCanceledException,被catch块捕获并安全退出。

5.3 必做:记录连接生命周期日志(含 IP、端口、时长)

没有日志的网络服务如同黑匣子。必须记录:谁连了、何时连、何时断、连了多久、收发多少字节。

// 服务端 HandleClientAsync 开头 var remoteIp = client.Client.RemoteEndPoint as IPEndPoint; Console.WriteLine($"[CONN] {remoteIp?.Address}:{remoteIp?.Port} connected at {DateTime.Now:HH:mm:ss}"); // 结尾 var duration = DateTime.Now - startTime; Console.WriteLine($"[CONN] {remoteIp?.Address}:{remoteIp?.Port} closed after {duration.TotalSeconds:F1}s, rx={_rxBytes}, tx={_txBytes}");

价值:某次客户现场问题,日志显示某 IP 连接后 3 秒即断开,且无任何收发记录——直指客户端 DNS 解析失败,而非服务端问题。

5.4 必做:客户端实现指数退避重连(Exponential Backoff)

网络抖动是常态。裸写while(!connected) { ConnectAsync(); await Task.Delay(1000); }会引发雪崩式重连请求。

public async Task<bool> ConnectWithRetryAsync(int maxAttempts = 5) { TimeSpan delay = TimeSpan.FromSeconds(1); for (int i = 0; i < maxAttempts; i++) { if (await ConnectAsync()) return true; if (i < maxAttempts - 1) { Console.WriteLine($"[Client] Retry {i+1}/{maxAttempts} in {delay.TotalSeconds}s..."); await Task.Delay(delay); delay = delay.Add(delay); // 指数增长:1s, 2s, 4s, 8s... } } return false; }

参数建议:maxAttempts=5,初始delay=1s,最大间隔约 16s,既避免冲击服务端,又保证 30 秒内恢复。

5.5 必做:服务端限制最大并发连接数

无限制AcceptTcpClientAsync会导致内存耗尽、文件描述符耗尽(Linux)、或OutOfMemoryException(Windows)。

private readonly SemaphoreSlim _connectionLimiter = new(100, 100); // 最多 100 并发 private async Task HandleClientAsync(TcpClient client) { if (!await _connectionLimiter.WaitAsync(TimeSpan.FromSeconds(5))) { Console.WriteLine("[Server] Rejecting connection: limit reached"); client.Close(); return; } try { // ... 正常处理逻辑 } finally { _connectionLimiter.Release(); } }

经验值:树莓派 4B(4GB RAM)建议设为 50;x64 服务器(16GB+)可设 500;超过此数,应考虑连接池或消息队列削峰。

5.6 必做:用Wireshark抓包验证关键路径

代码逻辑再完美,也需用真实字节流验证。重点抓三处:

  • 客户端Connect是否发出 SYN,服务端是否回 SYN-ACK;
  • 消息发送时,TCP 包是否含预期数据(验证粘包/分包);
  • 断网时,是否发出 FIN 或 RST 包(验证优雅关闭)。

快捷命令:tshark -i "Ethernet" -f "tcp port 8080" -w tcp-debug.pcap,用 Wireshark 打开分析。别信日志,信字节。

5.7 必做:编写最小化单元测试(非集成测试)

用System.Net.Sockets.Socket构造内存中管道,隔离测试编解码逻辑:

[Test] public void ReadMessageWithDelimiter_ShouldParseTwoMessages() { // 模拟服务端发送两条带 \n 的消息 var testData = Encoding.UTF8.GetBytes("Hello\nWorld\n"); // 创建内存管道 var (clientStream, serverStream) = CreatePipeStreams(); // 启动服务端读取任务 var task = Task.Run(() => ReadMessageWithDelimiterAsync(serverStream)); // 客户端写入 clientStream.Write(testData, 0, testData.Length); clientStream.Close(); // 验证结果 var result1 = task.Result; // "Hello" var result2 = ReadMessageWithDelimiterAsync(serverStream).Result; // "World" Assert.AreEqual("Hello", result1); Assert.AreEqual("World", result2); }

为什么有效:绕过网络、DNS、防火墙干扰,专注验证你的粘包处理逻辑是否鲁棒。每次重构ReadMessage函数,运行此测试 0.1 秒即知对错。

最后说一句:我见过太多项目,因为省略了其中一两项,上线后半夜被报警电话叫醒。这些不是“高级技巧”,而是 C# TCP 开发的呼吸和心跳。把它们刻进肌肉记忆,下次写TcpClient时,手指会自动敲出NoDelay = true和await ...(_cts.Token)。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询