ARTICLE DETAIL

资讯详情

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

C#高并发Socket服务器与客户端完整工程实例源码解析

C#高并发Socket服务器与客户端完整工程实例源码解析 简介这份C#高并发SOCKET服务器与客户端完整工程实例源码面向希望深入理解网络通信底层机制、提升并发处理能力的.NET开发者尤其适合刚接触Socket编程的新手与需要优化网络服务性能的进阶工程师。压缩包共431个文件约4.1MB以cs源码、pas单元、res资源、dll动态库、dfm窗体文件及csproj工程配置为主涵盖服务器监听、连接处理、数据收发与异常捕获等核心模块同时包含客户端连接管理、异步收发与界面集成逻辑并附带解决方案与项目配置文件便于直接编译运行。目前已有987人学习下载。读者可通过阅读源码、调试运行对比多线程与async/await异步模型在高并发场景下的差异掌握Socket监听、线程调度、断线重连及错误处理等关键实现是学习C#网络编程与并发优化的实用参考。1. C#高并发SOCKET服务器从一份完整工程源码里能拆出什么很多人第一次搜「C#高并发SOCKET服务器和客户端完整工程实例源码.zip」心里想的其实不是「我要读代码」而是「我要一套能扛住几千连接、能直接改吧改吧上生产的骨架」。这个诉求非常具体服务端要能同时接住大量客户端客户端要能稳定收发最好还带心跳、断线重连、粘包处理这些真实项目里绕不开的东西。C# 做 Socket 高并发核心不是把Socket.Accept写出来而是把「连接管理、IO 模型、消息边界、资源释放」这四件事同时做对。这份工程实例的价值就在于它把服务端和客户端放在同一个解决方案里让你能对照着看一条消息从发出到被处理的全过程。适合谁适合已经会写基础 C# 控制台程序、但一上真实并发就翻车的上位机开发者、IM 后端初学者以及需要给设备做长连接网关的工程师。2. 高并发 SOCKET 的 IO 模型选型为什么不是每个连接开一个线程2.1 同步阻塞、异步回调、SocketAsyncEventArgs 三条路C# 里做 Socket 服务端绕不开三种模型。第一种是同步阻塞Accept一个连接就new Thread去Receive。这种写法在几十个连接时没问题一旦到几百上千线程上下文切换和内存占用就会把 CPU 吃光而且线程栈默认 1MB1000 个线程就是 1GB 内存还没算业务对象。第二种是BeginReceive/EndReceive异步回调也就是热搜里提到的「c# socket bigging receive 回调」它把 IO 交给线程池比同步阻塞省线程但每次收发都要分配IAsyncResult高频小包场景下 GC 压力明显。第三种是SocketAsyncEventArgs它把异步操作对象池化复用事件参数避免每次收发都分配新对象是 .NET 里做高并发 Socket 的推荐路径。选型理由很直接如果你的连接数预期在 500 以内、消息频率低同步阻塞加线程池也能跑如果预期 1000 以上、且消息频繁直接上SocketAsyncEventArgs。这份工程实例源码通常采用的就是第三种因为标题里「高并发」三个字已经把场景定死了。下面给一个最小可复现的服务端骨架用SocketAsyncEventArgs池化思路先跑通再谈优化。using System; using System.Net; using System.Net.Sockets; using System.Collections.Concurrent; public class HighConcurrencyServer { private Socket _listenSocket; // 复用 SAEA 对象避免每次 Accept/Receive 都 new private readonly ConcurrentStackSocketAsyncEventArgs _acceptPool new(); private readonly ConcurrentStackSocketAsyncEventArgs _receivePool new(); public void Start(int port) { _listenSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listenSocket.Bind(new IPEndPoint(IPAddress.Any, port)); _listenSocket.Listen(512); // backlog 设大防止瞬时连接被拒 for (int i 0; i 10; i) { var saea new SocketAsyncEventArgs(); saea.Completed OnAcceptCompleted; _acceptPool.Push(saea); StartAccept(saea); } } private void StartAccept(SocketAsyncEventArgs saea) { saea.AcceptSocket null; if (!_listenSocket.AcceptAsync(saea)) OnAcceptCompleted(null, saea); // 同步完成要手动回调 } private void OnAcceptCompleted(object sender, SocketAsyncEventArgs saea) { if (saea.SocketError SocketError.Success) { var client saea.AcceptSocket; client.NoDelay true; // 关闭 Nagle降低小包延迟 var recvArgs new SocketAsyncEventArgs(); recvArgs.SetBuffer(new byte[4096], 0, 4096); recvArgs.UserToken client; recvArgs.Completed OnReceiveCompleted; StartReceive(recvArgs); } StartAccept(saea); // 继续接下一个 } private void StartReceive(SocketAsyncEventArgs saea) { if (!((Socket)saea.UserToken).ReceiveAsync(saea)) OnReceiveCompleted(null, saea); } private void OnReceiveCompleted(object sender, SocketAsyncEventArgs saea) { if (saea.BytesTransferred 0 saea.SocketError SocketError.Success) { // 这里只做演示真实项目要进缓冲区做粘包处理 var data new byte[saea.BytesTransferred]; Buffer.BlockCopy(saea.Buffer, saea.Offset, data, 0, saea.BytesTransferred); // TODO: 投递到业务队列 StartReceive(saea); // 继续收 } else { ((Socket)saea.UserToken)?.Close(); } } }逻辑说明Listen(512)的 backlog 参数决定内核等待队列长度设太小会在压测时出现连接被拒。NoDelay true关闭 Nagle 算法对实时性要求高的上位机场景很关键。AcceptAsync和ReceiveAsync返回false表示操作同步完成必须手动调用回调否则会丢事件这是最常见的翻车点之一。参数上接收缓冲区 4096 是折中值消息体大就调到 8192 或 16384但别超过 64KB否则大对象堆分配会拖慢 GC。2.2 连接数、缓冲区、backlog 三个参数怎么定连接数不是拍脑袋定的。Windows 下单进程默认可用端口约 16 位但服务端监听端口只有一个限制在文件句柄和内存。每个SocketAsyncEventArgs加缓冲区大约占 4KB 到 16KB10000 连接按 8KB 算就是 80MB可接受。backlog 一般设为预期瞬时峰值的 1.5 倍比如压测 2000 并发就设 3000。缓冲区大小按你协议里最大包来定如果最大包 2KB缓冲区 4KB 足够如果走文件传输另开大缓冲区通道不要和心跳包混用同一个 SAEA。提示SocketAsyncEventArgs用完必须重置SetBuffer和UserToken再放回池否则下次复用会带着旧数据出现「收到上次残留包」的玄学问题。3. 客户端与服务端消息边界粘包、半包和心跳的工程解法3.1 长度前缀协议四字节头加包体TCP 是字节流没有消息边界。你发两次 100 字节对端可能一次收到 200 字节也可能分三次收到。工程实例里最稳的做法是「长度前缀」包头固定 4 字节 int表示包体长度收满包头再收包体。下面给一个可复用的拆包器服务端客户端都能用。using System; using System.Collections.Generic; using System.IO; public class LengthPrefixedDecoder { private readonly MemoryStream _buffer new(); // 喂入原始字节返回已解析出的完整包 public Listbyte[] Feed(byte[] data, int offset, int count) { _buffer.Write(data, offset, count); var packets new Listbyte[](); while (true) { var raw _buffer.ToArray(); if (raw.Length 4) break; // 包头都不够等下次 int bodyLen BitConverter.ToInt32(raw, 0); if (bodyLen 0 || bodyLen 1024 * 1024) throw new InvalidDataException(包体长度非法可能协议错位); if (raw.Length 4 bodyLen) break; // 半包继续等 var body new byte[bodyLen]; Buffer.BlockCopy(raw, 4, body, 0, bodyLen); packets.Add(body); // 移除已消费数据 _buffer.SetLength(0); _buffer.Write(raw, 4 bodyLen, raw.Length - 4 - bodyLen); } return packets; } }逻辑说明Feed方法可以反复调用内部MemoryStream累积不完整数据。bodyLen上限设 1MB 是防御性编程防止对端发恶意长度导致内存暴涨。每次解析完要重建MemoryStream这里为了可读性用了ToArray生产环境建议用环形缓冲区避免频繁分配。参数上包头 4 字节支持单包最大 2GB实际业务很少超过 64KB可以改成 2 字节包头省流量。3.2 心跳与断线重连别等 TCP 自己发现TCP 的KeepAlive默认两小时才探测客户端拔网线服务端可能一直挂着死连接。工程实例里通常自己实现应用层心跳客户端每 5 秒发一个空包服务端 15 秒没收到就判定掉线并关闭。客户端侧用定时器检测发送失败或超时触发重连重连间隔做指数退避从 1 秒逐步到 30 秒避免服务端刚重启就被重连风暴打垮。// 客户端心跳与重连核心逻辑 private async Task HeartbeatLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) { try { if (_socket null || !_socket.Connected) { await ReconnectAsync(token); // 内部指数退避 continue; } var heartbeat BuildPacket(Array.Emptybyte()); // 空包体 await _socket.SendAsync(heartbeat, SocketFlags.None); await Task.Delay(5000, token); } catch { _socket?.Close(); _socket null; } } }逻辑说明心跳包体为空靠包头长度 0 区分业务包。ReconnectAsync里维护一个retryCount每次失败delay Math.Min(30000, 1000 * (int)Math.Pow(2, retryCount))。注意SendAsync在连接断开时会抛异常必须捕获后置空_socket否则下一轮循环会一直用死连接。注意心跳间隔要小于服务端判定超时时间的一半比如服务端 15 秒超时客户端就 5 秒发一次留两次容错。4. 避坑与排查高并发 SOCKET 工程里最容易翻车的 5 个点4.1 端口只允许使用一次AddressAlreadyInUse现象服务端重启时报Windows socket error: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。原因上一次进程退出后监听端口处于TIME_WAIT状态默认持续 2 到 4 分钟。解决在Bind之前设置_listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true)让新进程可以复用端口。注意这个选项要在Bind前调用顺序错了不生效。4.2 回调里做业务导致收包停滞现象压测时 QPS 上不去延迟越来越高。原因在OnReceiveCompleted里直接做数据库写入或复杂计算阻塞了 IO 回调线程后续收包排不上队。解决回调里只做拆包把完整包投递到BlockingCollection或Channel由独立业务线程池消费。这样 IO 线程始终轻量收包不会停。4.3 客户端关闭后服务端没释放 Socket现象连接数只增不减内存缓慢上涨。原因客户端Close后服务端ReceiveAsync返回 0 字节代码里只return没Close和Dispose。解决BytesTransferred 0或SocketError ! Success时必须Close客户端 Socket并把SocketAsyncEventArgs放回池。最好用try/finally保证释放。4.4 大包导致内存碎片和 GC 卡顿现象传输 1MB 以上文件时服务端周期性卡顿。原因每次收包都new byte[bodyLen]大对象进入 LOHGC 回收代价高。解决大文件走独立通道用固定大小的池化缓冲区分片传输或者直接用ArrayPoolbyte.Shared.Rent租借缓冲区用完归还。业务包尽量控制在 64KB 以内。4.5 心跳包和业务包共用缓冲区导致协议错位现象偶尔解析出乱码包长度字段是天文数字。原因心跳包和业务包用了同一个SocketAsyncEventArgs但心跳包没走长度前缀接收端按长度前缀解析就错位了。解决所有包统一走长度前缀协议心跳包就是长度为 0 的包。协议统一是避免玄学问题的根本办法。5. 压测验证与进阶用最小工具确认你的服务端到底能扛多少5.1 用自带客户端做 1000 并发连接压测工程实例里通常带一个测试客户端但很多人只开一个实例手动点。要验证高并发得写一个压测端开 1000 个Task每个建一个 Socket 连上服务端然后按固定频率发心跳包统计发送成功率和往返延迟。下面给一个最小压测脚本。using System; using System.Diagnostics; using System.Net.Sockets; using System.Threading.Tasks; public class LoadTester { public static async Task RunAsync(string host, int port, int connections) { var tasks new Task[connections]; var success 0; var sw Stopwatch.StartNew(); for (int i 0; i connections; i) { tasks[i] Task.Run(async () { using var socket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); await socket.ConnectAsync(host, port); var packet new byte[] { 0, 0, 0, 0 }; // 长度 0 的心跳 for (int j 0; j 100; j) { await socket.SendAsync(packet, SocketFlags.None); await Task.Delay(100); } System.Threading.Interlocked.Increment(ref success); }); } await Task.WhenAll(tasks); sw.Stop(); Console.WriteLine($连接数 {connections}完成 {success}耗时 {sw.ElapsedMilliseconds}ms); } }逻辑说明每个连接发 100 个心跳间隔 100ms总共 10 秒。观察服务端 CPU 和内存如果 CPU 单核跑满但连接没掉说明 IO 模型没问题瓶颈在业务如果连接大量超时检查 backlog 和Listen参数。参数上connections从 100 开始逐步加到 5000每次观察服务端句柄数和 GC 次数。5.2 用性能计数器定位瓶颈Windows 下用perfmon加三个计数器Process\Handle Count看句柄泄漏.NET CLR Memory\Gen 2 Collections看 GC 压力Network Interface\Bytes Total/sec看带宽是否打满。如果句柄数只涨不跌就是 Socket 没释放如果 Gen2 频繁就是大对象分配太多如果带宽打满就该考虑压缩或分片。我自己的习惯是每次改完代码先跑 10 分钟压测看这三个曲线稳不稳稳了再上真实设备。这套工程实例源码最大的价值不是代码本身而是让你有一个能反复改、反复压的基线改坏了能回退改好了能对比。希望帮到你。本文还有配套的精品资源点击获取
返回列表