做 C# Socket 开发这么多年我手头上最常被同事讨要的一份代码就是一套既支持 TCP 又支持 UDP服务器和客户端都能直接落地还能扛住高并发场景的通信源码。市面上很多教程给的是单连接聊天室或者阻塞式收发写起来简单放到生产环境一压测就露馅。这篇文章不吹框架直接讲我在维护这套 C# 高并发高性能 Socket 源代码时的核心设计思路从线程模型、缓冲区管理、SocketAsyncEventArgs 的使用到 TCP 粘包处理、UDP 收包并发、以及压测时踩过的那些坑尽量一次讲透。这套代码适合几类人做即时通讯服务端的写 ERP 库存网关的用 C# 写上位机采集设备数据的以及准备从“阻塞式 Socket 多线程”转型到异步高并发模型但还没想明白从哪下手的人。我会把项目骨架、每个模块为什么这么写、哪里最容易出错按我实际开发时的顺序串起来。跟着这篇文章走完你至少能对自己的 socket 框架有一个完整判断而不是继续在网上拼碎片代码。1. 先想清楚再写代码高性能 Socket 到底在解决什么问题1.1 为什么不能直接用 TcpClient 和同步收发很多新手喜欢用 TcpClient因为封装度够高几行代码就能连上。但 TcpClient 内部的高层封装到了高并发场景反而成了限制。你可以打开 TcpClient 源码看看它本质上是包了一层 Socket而真正决定性能的是你如何驱动那个底层 Socket。最大的问题出在“阻塞式同步收发”上。一个线程在那里Receive()死等如果有 1 万个客户端连接你就得准备 1 万个线程。操作系统不是不能开这么多线程但线程上下文切换会吃掉大量 CPU而且每个线程默认栈空间 1MB1 万连接光栈内存就接近 10GB。这不是性能问题是资源爆炸问题。高并发下正确方向是异步 I/O。异步收发不需要“一个连接占一个线程”而是把 I/O 完成事件交给操作系统完成后回调到线程池继续处理。C# 里最底层、最稳的这个机制就是 SocketAsyncEventArgs它在 Windows 上映射到 IOCP在 Linux 上映射到 epoll。我写的这套源码里TCP 服务器端、TCP 客户端、UDP 服务器端核心收发逻辑全是基于 SocketAsyncEventArgs 实现的。1.2 SocketAsyncEventArgs 和 async/await 怎么选现在写 C# 网络代码很多人第一反应是await socket.ReceiveAsync(buffer, ...)这个写法本身没问题.NET 5 之后性能也不错。但如果你需要兼容 .NET Framework 4.8 或者老项目或者面对几万连接这种量级直接用 SocketAsyncEventArgs 仍然是更稳妥的选择。SocketAsyncEventArgs 的核心优势是“复用”。它允许你把操作要用的缓冲区和 Socket 状态打包成一个对象反复投递给操作系统尽量避免在每次收发时都分配新对象。异步回调触发时系统会调用Completed事件。这里有一个很多初学者必踩的坑如果AcceptAsync或ReceiveAsync返回true说明操作被挂起等完成事件如果返回false说明操作已经同步完成此时事件不会另外触发你必须自己立刻处理回调逻辑。这也意味着 Complected 事件处理函数里面必须假设“当前回调可能在任意线程”所有共享状态要加锁或做成无锁队列。我见过不少代码异步回调里直接访问同一个 List 结果偶发崩溃查半天都找不到原因其实就是多线程并发写。1.3 TCP 和 UDP 在高并发维度上完全是两回事TCP 是面向连接的字节流协议。它可靠、有序、但默认有 Nagle 算法和 ACK 延迟等机制吞吐并不总能跑满。高并发 TCP 要解决的问题主要是连接管理、粘包半包、慢启动、断开检测。你的服务器每接受一个连接都要为它单独维护一个会话对象这个会话里有缓冲区、协议解码器、心跳状态、发送队列。连接数一上来会话管理本身就是一门学问。UDP 是无连接的数据报协议。它保留消息边界本身就天然解决粘包问题但不可靠、可能乱序。UDP 的高并发维度和 TCP 完全不同你不需要为每个客户端维护复杂状态但你需要处理“一个 socket 上有无数来源的包”这种情况需要快速地把每个包分发到对应的业务逻辑里。还要面对全链路丢包、接收缓冲区溢出、Windows 上莫名奇妙的 ConnectionReset 错误。这不是比 TCP 简单是“简单的地方不用管复杂的地方特别隐蔽”。所以不要指望一套代码同时解决 TCP 和 UDP 的一切问题。TCP 代码解决“连接与流”UDP 代码解决“数据报与分发”。下面我按模块拆开讲。2. 工程结构与线程模型这套源码是怎么组织的2.1 建议的项目目录结构很多团队写 socket 通信喜欢把所有代码堆在一个文件里。开始 500 行还好连接类型一多、协议一复杂后面根本没法维护。我现在的工程结构是这样NetEngine/ ├─ BufferManager.cs # 共享缓冲区管理 ├─ SocketAsyncEventArgsPool.cs # SAEA 对象池 ├─ Tcp/ │ ├─ TcpServer.cs # TCP 服务器端 │ ├─ TcpSession.cs # 单连接会话 │ └─ TcpClient.cs # TCP 客户端 ├─ Udp/ │ ├─ UdpServer.cs # UDP 服务器端 │ └─ UdpChannel.cs # UDP 客户端封装 └─ Protocol/ ├─ LengthPrefixDecoder.cs # 长度前缀拆包器 ├─ Packet.cs # 消息包 └─ PacketDispatcher.cs # 消息分发这个分层思路是IO 层只负责把字节搬进缓冲区协议层负责把字节解析成消息业务层只处理消息。如果你有一个业务逻辑要跟 Socket 收发混在一起说明分层出了问题。我在做 ERP 库存网关时曾经把库存预占逻辑直接写在 ReceiveCompleted 回调里结果 IO 线程被数据库查询堵死后面所有连接的读取都延迟了这就是架构级的错误。2.2 Accept 循环和连接会话要分离TCP 服务器最忌讳的写法是“每来一个连接就线程池丢一个 Task 去 while 循环接收”。这种写法等于把阻塞模型换了一层皮。高并发服务器的正确结构应该是一个监听 Socket 负责 Accept每 accept 出一个新 Socket立刻封装成独立的 TcpSession 对象然后由这个 Session 自行投递 ReceiveAsync 接收请求。Accept 本身不需要多线程。大多数场景维护一个 pending 的 AcceptAsync 就够用但我在源码里默认开了 4 个并发 AcceptAsync主要是防止 accept 突发瞬间处理不过来。每个 AcceptAsync 必须用独立的 SocketAsyncEventArgs因为同一个实例同时只能服务一个异步操作。Accept 完成后立刻再次调用 AcceptAsync形成 Accept 循环。这里还要注意第一次调用AcceptAsync时如果返回 false也是同步完成要马上处理。很多网上代码没判断返回值只在 Completed 事件里处理结果偶发丢失连接。我遇到过一次线上服务“部分客户端连不上”的诡异问题最后排查下来就是同步完成场景没处理。2.3 业务消息处理不能卡住 IO 线程IO 完成回调里的代码执行时间直接决定这个连接的“下一个包什么时候能收到”。如果回调里做数据库操作、调用第三方 HTTP、处理复杂 JSON网络吞吐会被这种 CPU/IO 混合任务拖垮。正确做法是让 IO 回调只做“搬运”把收到的字节写入协议解码器解析出消息把消息投递到一个有界队列里然后立刻回到ReceiveAsync。业务侧由专门的 Consumer 线程从队列取消息处理。C# 里可以用ChannelT这是一个线程安全、支持背压的队列。背压这个点特别重要。如果业务处理不过来队列会无限增长最后内存爆掉。有界 Channel 写满时TryWrite会返回 false此时你应该选择丢弃或断开连接。很多 IM 场景愿意丢弃非关键消息而 ERP 库存场景宁可断连重推也不丢数据。这是我做 IM 和 ERP 网关时最核心的取舍没有统一答案但架构必须支持这种取舍。3. TCP 服务器端与客户端实现细节3.1 TCP 服务器Accept 与 Session 核心代码下面是一段简化但可运行的服务器端核心框架。监听 Socket 初始化时先设置 ReuseAddress避免服务重启时 “Address already in use” 的尴尬。public sealed class TcpServer { private readonly Socket _listenSocket; private const int AcceptConcurrency 4; public TcpServer(int port, int backlog 1024) { _listenSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); _listenSocket.Bind(new IPEndPoint(IPAddress.Any, port)); _listenSocket.Listen(backlog); } public void Start() { for (int i 0; i AcceptConcurrency; i) { var acceptArgs new SocketAsyncEventArgs(); acceptArgs.Completed AcceptCompleted; AcceptAsync(acceptArgs); } } private void AcceptAsync(SocketAsyncEventArgs args) { args.AcceptSocket null; bool pending _listenSocket.AcceptAsync(args); if (!pending) { AcceptCompleted(this, args); } } private void AcceptCompleted(object? sender, SocketAsyncEventArgs e) { if (e.SocketError SocketError.Success e.AcceptSocket ! null) { var session new TcpSession(e.AcceptSocket); session.Start(); } else { // 这里要记录 SocketError不要只是吞掉 } AcceptAsync(e); } }这块代码里有几个细节经常被忽略。一是backlog也就是 listen 队列长度一般压测场景 1024 够用如果压力特别猛要结合操作系统参数一起调。二是 Accept 失败后不能直接继续盲目 Accept如果因为 fd 耗尽或者瞬时错误至少延迟几十毫秒再恢复否则会出现疯狂空转。三是 AcceptSocket 拿到之后单独立刻设置参数比如NoDelay true、KeepAlive 等再封装成 Session。每个 Session 的核心接收代码如下。注意接收缓冲区是每个连接一份大小为 8KB 到 16KB 在多数业务场景覆盖了大量短消息。大消息靠协议层累积而不是靠调大这块缓冲区。public sealed class TcpSession { private readonly Socket _socket; private readonly byte[] _receiveBuffer; private readonly SocketAsyncEventArgs _receiveArgs; private readonly LengthPrefixDecoder _decoder; public TcpSession(Socket socket) { _socket socket; _socket.NoDelay true; _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); _receiveBuffer new byte[8192]; _receiveArgs new SocketAsyncEventArgs(); _receiveArgs.SetBuffer(_receiveBuffer, 0, _receiveBuffer.Length); _receiveArgs.Completed ReceiveCompleted; } public void Start() { ReceiveAsync(); } private void ReceiveAsync() { bool pending _socket.ReceiveAsync(_receiveArgs); if (!pending) { ReceiveCompleted(this, _receiveArgs); } } private void ReceiveCompleted(object? sender, SocketAsyncEventArgs e) { if (e.SocketError ! SocketError.Success || e.BytesTransferred 0) { Dispose(); return; } _decoder.Feed(e.Buffer, e.Offset, e.BytesTransferred); foreach (var packet in _decoder.TryDecodePackets()) { PacketDispatcher.Enqueue(this, packet); } ReceiveAsync(); } }当BytesTransferred 0说明对端正常关闭一定要把会话从全局连接字典里移除并释放 Socket 和 SAEA。如果忘了释放每断开一个连接就泄漏一个对象长期运行 GC 涨到爆。3.2 粘包半包处理长度前缀是最稳的方案TCP 是字节流不是消息流。对方一帧消息可能是 1 个字节也可能跨多个 TCP 段接收方必须自己“划界”。我在源码里统一使用“4 字节 header body”的长度前缀方案[4字节 int32 大端 body长度] [body]这个方案的好处是协议简单解析快也不需要转义。解码器的核心逻辑是先累积 4 字节的头部从头部里读出 body 长度再继续累积 body直到凑够一个完整消息。一旦一个消息解析完成把剩余的字节当作下一个消息的开始继续解析。这里有个很容易搞错的地方头部和 body 都可能跨ReceiveAsync的多次回调到达。所以解码器内部必须维护状态不能每次收到数据都当成一个完整消息。我见过很多新手写if (count 4) return;然后直接丢弃数据这是不对的。正确的做法是把剩余字节暂存起来等待后续数据填满。如果你传输的是二进制协议大端还是小端也要提前规划。.NET 默认小端而很多硬件设备、嵌入式端用的是大端。我自己做上位机采集时和 C 端设备联调最常见的问题就是字节序对不上。建议在协议文档里明确标注字节序并在解码器里统一用BinaryPrimitives.ReadInt32BigEndian这种语义明确的 API。3.3 TCP 客户端连接池、超时与重连TCP 客户端表面简单就是 Connect、Send、Receive但高并发下客户端同样不能乱来。最常见的问题是“每次发一条消息就 new 一个 Socket”。如果业务 QPS 是每秒一万那就意味着每秒有一万次 TCP 握手和四次挥手服务器还没处理业务先被 SYN 包打懵了。正确做法是维护连接池。按目标服务器地址维护一组 TcpSession 连接空闲时复用不用时放回池子定期心跳。连接池的核心参数有三个最小连接数、最大连接数、空闲回收时间。最小连接数保证突发流量下有存量连接可用最大连接数避免资源耗尽空闲回收时间避免大量半死不活的长连接占着内存。连接超时和重连也要单独设计。下面的伪代码方式可以表达重点private async ValueTaskTcpSession ConnectWithRetryAsync(IPEndPoint remote, int retryCount) { var socket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.NoDelay true; socket.SendBufferSize 64 * 1024; socket.ReceiveBufferSize 64 * 1024; for (int i 0; i retryCount; i) { try { await socket.ConnectAsync(remote, CancellationToken.None); return new TcpSession(socket); } catch (SocketException) { if (i retryCount) { socket.Dispose(); throw; } await Task.Delay(500 * (i 1)); } } throw new InvalidOperationException(unreachable); }重连退避很重要。不要失败后立刻无限重连那会导致“连接风暴”尤其服务器刚重启时几千个客户端同时疯狂重连很容易把服务打崩。我现在的默认方案是第一次延迟 500ms随后指数递增到最大 15 秒并加随机抖动让重连请求均匀散开。4. UDP 服务器端与客户端实现细节4.1 UDP 服务器的异步接收循环UDP 服务器不需要 Accept只需要创建一个 SocketBind 到一个端口然后持续调用 ReceiveFromAsync。听起来比 TCP 简单但它有个“单接收循环性能瓶颈”如果你只维持一个 ReceiveFromAsync 在飞行处理一个包期间新到的包只能等内核缓冲区排队。极端情况下内核缓冲区溢出UDP 直接丢包。所以在 UDP 高并发场景我的服务器端默认维持多个接收通道。每个通道一个独立的 SocketAsyncEventArgs一个独立缓冲区一个独立的“当前投递中”状态。收到包后复制到接收队列就立刻再次投递 ReceiveFromAsync。接收通道数量一般取 CPU 核数最常见的是 4 到 8 个。简化版核心代码public sealed class UdpServer { private readonly Socket _udpSocket; private readonly int _port; private readonly ListSocketAsyncEventArgs _receiveArgsList new(); public UdpServer(int port) { _port port; _udpSocket new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); _udpSocket.Bind(new IPEndPoint(IPAddress.Any, port)); } public void Start(int receiveChannelCount) { for (int i 0; i receiveChannelCount; i) { var args new SocketAsyncEventArgs(); args.SetBuffer(new byte[65507], 0, 65507); args.RemoteEndPoint new IPEndPoint(IPAddress.Any, 0); args.Completed ReceiveCompleted; _receiveArgsList.Add(args); ReceiveFromAsync(args); } } private void ReceiveFromAsync(SocketAsyncEventArgs args) { bool pending _udpSocket.ReceiveFromAsync(args); if (!pending) { ReceiveCompleted(this, args); } } private void ReceiveCompleted(object? sender, SocketAsyncEventArgs e) { if (e.SocketError ! SocketError.Success || e.BytesTransferred 0) { e.RemoteEndPoint new IPEndPoint(IPAddress.Any, 0); ReceiveFromAsync(e); return; } OnUdpPacketReceived(e.RemoteEndPoint!, e.Buffer, e.Offset, e.BytesTransferred); e.RemoteEndPoint new IPEndPoint(IPAddress.Any, 0); ReceiveFromAsync(e); } }UDP 数据报最大长度是 65507 字节所以缓冲区直接按上限申请也没问题。但注意如此大的 UDP 包几乎不可能在不分片的情况下穿过公网所以生产环境更多是处理 1400 字节以内的小包。ReceiveFromAsync 返回的 BytesTransferred 表示当前数据报的实际长度不要把它当成缓冲区已填充的连续字节流。OnUdpPacketReceived 里不要做重逻辑。我的源码在这里只做一件事通过 RemoteEndPoint 找到对应的业务处理器把数据包投递到 Channel 然后马上返回。如果收到包后要花 10ms 去解析复杂结构丢包率会不可控。4.2 UDP 客户端发送并发与缓冲区UDP 客户端和 TCP 客户端不同通常不需要连接池。因为 UDP 本身无连接一个 Socket 可以往任意地址发送而且 SendToAsync 是线程安全的多个线程同时调用不会导致数据错乱。问题在于发送方也会遇到内核缓冲区紧张如果发送速度超过对端接收速度或者网络拥堵发送缓冲区满了之后SendTo 会返回错误。高并发 UDP 客户端的关键是要控制发送速率和内存占用。不要因为 UDP 不收 ACK就一口气把大量数据丢进 Socket。很多 IM 消息推送服务用 UDP 做秒级定位消息一但瞬间涌入客户端在网络特差时会有大量 UDP 包在发送缓冲区排队内存压力非常大。我的做法是应用层维护一个有界发送队列队列满时直接丢弃最新不重要的包或者对重要包降级为 TCP 通道补发。UDP 客户端的发送代码用起来很直白public async Task SendAsync(IPEndPoint remote, ReadOnlyMemorybyte payload) { await _socket.SendToAsync(payload, SocketFlags.None, remote); }如果你要兼容 .NET Framework则要回到 SocketAsyncEventArgs 那套写法。但无论哪种方式发送之前都建议检查payload.LengthUDP 超过 65507 会直接报错超过链路 MTU 则可能分片性能大幅下降。本地回环测试可以发大包公网场景尽量控制在 1472 字节以内也就是以太网 MTU 1500 减去 IP 头 20 字节和 UDP 头 8 字节。4.3 丢包、乱序和 Windows 上的 ConnectionResetUDP 最大的特点是“没有保证”。做 UDP 服务器你必须接受两件事第一包可能丢第二到达顺序可能不同。尤其是多通道接收的情况下CPU 多线程并行处理如果后续业务依赖顺序就得在包体里加序号用接收端排序。我的做法是在业务包头部带一个 4 字节的自增序列号接收端收到后不直接处理而是先放进滑动窗口排序窗口。窗口大小按业务容忍度设计比如容忍 100 个包乱序超过就丢弃整段或者请求重发。这个逻辑虽然增加内存但它让 UDP 层具备从“裸数据报”升级为“可靠消息”的能力。如果你只是做简单传感器数据可能不需要排序直接解析即可。还有一个 Windows 特有的坑我必须单独讲。当你用 UDP Socket 向一个未启动服务的端口发送数据该主机会回一个 ICMP Port Unreachable。Windows 在收到这个 ICMP 后会让你下一次ReceiveFromAsync抛出一个 SocketException错误码是 10054 / ConnectionReset。这在 Linux 上通常不会这么直接暴露。处理办法是创建 UDP Socket 后执行一次IOControl_socket.IOControl(-1744830452, new byte[] { 0 }, null);这个数值对应的就是 Windows 的SIO_UDP_CONNRESET作用是让系统禁止把 UDP 的 ConnectionReset 错误传给你的应用。在 Windows 下做 UDP 收包时这道设置我建议从 Socket 初始化后立刻调用。否则一个健康服务会收到莫名其妙的异常基至导致服务器停止接收。5. 让性能再上一个台阶对象池、Buffer 管理与内核参数5.1 内存与对象分配是隐形杀手高并发 Socket 性能和 GC 压力强相关。典型的反面写法是每次 Receive 都 new 一个 byte[]每次收到包都 new 一个 SocketAsyncEventArgs。连接数少时感觉不到连接数上万时每秒几千次分配会让 GC 频繁触发造成明显的卡顿。SocketAsyncEventArgs 最好做成对象池。一个连接从池子里借 SAEA断开连接时归还。缓冲区也不建议每个连接都 new 独立大数组更优的做法是提前从ArrayPoolbyte.Shared租一段用完归还。下面是我在 UDP 服务器里替换“new byte[]”的方式byte[] rented ArrayPoolbyte.Shared.Rent(e.BytesTransferred); Buffer.BlockCopy(e.Buffer!, e.Offset, rented, 0, e.BytesTransferred); // 业务消费完这个包之后务必归还 ArrayPoolbyte.Shared.Return(rented);这里要注意一个细节从 ArrayPool 租到的数组可能比实际请求的大使用长度必须用e.BytesTransferred而不是rented.Length。归还后不能再被业务侧持有。如果你的业务需要异步长时间持有这个包就得复制到自己的缓冲区或者等处理完再归还。GC 不是恶魔但高并发下“无谓分配”是恶魔。我见过一些人为了追求“高性能”把 SocketAsyncEventArgs 池写得特别复杂实际上池子本身如果管理不当反而引入锁竞争。目前我的建议是预创建 100 到 200 个 SAEA 进池子高峰期动态补充空闲时裁剪不要一上来就搞几万个对象。5.2 Socket 参数优化明细参数这东西不能直接抄网上要理解每个参数在业务里起什么作用。下面是我测试中比较常用的一套配置括号里是原因参数推荐值说明NoDelaytrue关闭 Nagle 算法适合请求/响应频繁的场景大文件流传输时可以再评估KeepAlivetrue用于清理长时间无数据连接但应用层心跳才是主角ReceiveBufferSize32KB~128KB不是越大越好内核有自动调优机制SendBufferSize32KB~128KB过大反而导致延迟Listen backlog1024高并发压测时结合系统 somaxconn 调整ReuseAddresstrue服务重启时避免 TIME_WAIT 导致绑定失败NoDelay 这个要单独说。默认情况下 TCP 会启用 Nagle 算法把小的数据包攒在一起发送节省小包开销但对即时交互是灾难。比如发一条 20 字节的实时指令如果被 Nagle 等 200ms用户感官极差。做 IM、游戏、上位机指令下发基本都是 NoDelaytrue。但如果你在传输大文件NoDelaytrue 可能导致大量小包突刺也不代表最快。所以要从实际业务判断。5.3 .NET 环境配置和线程池参数代码层面优化完之后环境参数也要跟上。压测时最容易踩的坑是 .NET 线程池慢启动。异步 Socket 完成回调最终要在线程池线程上执行线程池会根据负载逐步增加线程压测刚开始时可能只有几个线程导致现象是一开始性能很低跑几十秒才上去。这不是程序写得不行是线程池还没热。我习惯在服务启动时显式预热线程池ThreadPool.SetMinThreads(64, 64);不要设置成几千否则线程上下文切换会成为新的瓶颈。64 或者 128 对多数场景足够。关键是它会告诉线程池别吝啬立刻给我足够的执行线程不要等队列堆满才加线程。Linux 部署时还要注意两个系统参数文件描述符限制和somaxconn。ulimit -n如果只有 1024就算程序写再好开到第 1025 个连接也会直接报 “Too many open files”。生产环境我一般调到 100 万。/proc/sys/net/core/somaxconn限制 listen 队列最大值如果你的 backlog 设置超过它最终会被它截断。这些不是 C# 代码问题但高并发排查时经常遇到。6. 高频问题排查与压测实录6.1 本地 10 万连接压测怎么做压测高并发 Socket 服务器很容易走入“客户端成了瓶颈”的误区。本地起几个 Task 连几千个连接发现服务端 CPU 没跑满就不敢继续加其实往往是客户端先扛不住了。我的建议是不要用一个进程模拟几万客户端尽量用多台机器或多个进程每个进程只模拟 5000 到 10000 个连接。最简单的压测方式是写一个客户端压力工具每个连接建立一个 TcpSession循环发送固定大小的请求。你要统计几个指标连接数峰值每秒处理消息数QPSP50、P95、P99 延迟服务端 CPU、内存GC 频率和单次 GC 暂停在 Linux 上可以用dotnet-counters观察内存和线程池数据用perfview或者dotnet-trace抓热点。如果延迟 P99 突然上不去优先怀疑三件事线程池线程不够、锁竞争、GC 分配过多。在这三种原因里锁竞争最常见比如每个连接一个锁、全局消息队列加锁、连接字典加锁都能把并发拖垮。6.2 错误速查表现象原因处理方法Accept 报 Too many open filesLinux 文件描述符上限不够调大 ulimit -n服务重启 Bind 报 Address already in use大量 TIME_WAIT 连接绑定前设置 ReuseAddress客户端连上后偶尔收不到首个数据包AcceptAsync 同步完成分支没处理判断 AcceptAsync 返回值同步完成直接回调TCP 消息解析出乱码粘包半包没处理使用长度前缀拆包器UDP 收包时抛 10054Windows 的 UDP ConnectionReset初始化 Socket 后执行 IOControl(-1744830452...)ReceiveCompleted 里出现 ObjectDisposedException连接被主动释放后异步完成事件仍然回调用 int 状态位控制在 Dispose 后忽略回调高并发下 GC 频繁每包 new byte[] / new SAEA改用 ArrayPool 和对象池客户端大量连接建立失败客户端频繁 new Socket 导致握手风暴接入连接池和退避重连压测性能先低后高.NET 线程池慢启动启动时 ThreadPool.SetMinThreads6.3 几个我后来才改掉的坏习惯第一个坏习惯是在async void事件处理函数里直接await。这个方法表面能跑但一旦内部抛出异常会导致整个进程崩溃而且很难追踪。所有 SocketAsyncEventArgs 的 Completed 事件我后来都改成“同步逻辑 明确调用异步方法”结构禁止把业务 await 放在事件处理链里。第二个坏习惯是连接关闭时只调 Socket.Close。Close 默认会立即返回但底层可能还有未发送的数据或者对端还在读取。社区里常见的做法是Socket.Shutdown(SocketShutdown.Both)之后再 Close但 Shutdown 本身也可能阻塞。我现在的策略是先停止接受新读写把发送队列里的剩余数据尽量 Flush再关闭。到了高并发场景“如何优雅退场”跟“如何高效进场”同样重要。第三个坏习惯是过度优化。为了省一两次new很多人把缓冲区和 SAEA 复用得过于极端导致调试时根本无法定位数据污染。我后来把代码分成两档默认档用 SocketAsyncEventArgs ArrayPool够稳妥极限档才上无锁队列和内存池。做 Socket 高并发先保证正确性和可排查性再谈压榨性能。这个顺序不能反。按我这套架构从零搭建TCP 服务器端、TCP 客户端、UDP 服务器端、UDP 客户端四块代码是可以解耦的。你如果不做 UDP完全不需要为了“完整性”硬塞一个收到 10054 就崩的模块进去。根据业务需要选型比照抄一份庞大源码更重要。如果要拿这套思路改造成私有协议建议先把长度前缀解码器和会话生命周期管理写好这两块稳了上层业务基本不会出大问题。
企业数字化 ERP 产品动态
相关推荐
降AIGC率新思路:千笔智能体如何改写论文表达 近半年,越来越多人开始找我问一个问题:“论文写完了,但一检测AIGC率超标,怎么办?” 网上搜出来一堆五花八门的“降AIGC”服务,有改词的、有重写的,还有号称搭载“黑科技算法”的,但大… · 2026/9/26 17:11:43
SpringBoot校园出入管理系统毕设实战:从数据库设计到部署排错 每年毕业季,我都能收到一堆私信,问同一个类型的问题:“SpringBoot做的校园出入管理系统”“XX管理系统带源码数据库”“有没有能直接跑的Java毕设项目”。问的人基本都是同一个画像:课程设计或毕业设计选了管理类系统,… · 2026/9/26 17:11:43
AI如何生成GPU算子:从CUDA手写到Triton+LLM自动编译 1. 项目概述:这不是一篇关于“才华埋葬”的伤感散文,而是一份GPU算子开发前线的战地笔记 你点开这个标题,大概率不是来听文艺批评的——你真正想搞清楚的是:当AI开始写CUDA Kernel,我们这些天天和 __syncthreads() 、… · 2026/9/26 17:11:43
IEC 60068-2-14:2023温度冲击测试实战指南 简介:本资源为IEC官方发布的最新版环境可靠性测试标准IEC 60068-2-14:2023(第7版),聚焦电子产品与电气设备的温度变化试验,面向硬件研发工程师、质量检测人员、第三方实验室技术人员及高校可靠性工程研究者,… · 2026/9/26 17:38:55
基于神经网络的无人机姿态自适应控制仿真:从PID到RBF自适应律 简介:这份PDF面向无人机控制、自动化与机器学习方向的学习者和研究者,聚焦四旋翼无人机姿态控制中模型不完整、参数与扰动不确定的难题。资源以RBF神经网络学习姿态动力学中的未知动态,结合类反步法设计包含反馈控制与神经网络控制的自适应控… · 2026/9/26 17:38:55
MapReduce源码深度解析:从作业提交到Shuffle的核心机制 1. 为什么逼自己啃一遍MapReduce源码,而不是继续当黑盒用户很多写了好几年Java的大数据工程师,对MapReduce的认知还停在“写个Mapper、写个Reducer、丢给集群跑”这一步。能用,但一旦遇到数据倾斜、任务卡死、OOM、输出结果对不上,… · 2026/9/26 17:38:55
深入理解LLVM编译器优化:从IR到Pass管道与-O3优化 1. 为什么优化发生在编译器后端,而不是只在写代码时很多开发者第一次接触 LLVM 的时候,脑子里浮现的还是《编译原理》课上的老三段:词法分析、语法分析、生成目标代码。可等你真的拿 clang 编一个项目、再打开优化选项后,会发现事… · 2026/9/26 17:38:55
Mask R-CNN猫脸像素级分割实战:从数据准备到边缘优化 简介:本资源是一套基于Mask R-CNN实现猫脸图像实例分割的完整项目,面向计算机、人工智能、数据科学等专业的在校学生与初学者,适用于课程设计、大作业及毕业设计选题,兼顾入门学习与二次开发需求。压缩包共22个文件,包… · 2026/9/26 17:38:55
Ubuntu 22.04 NVIDIA驱动安装全指南:黑屏、循环登录与CUDA失效排查 1. 为什么Ubuntu 22.04装NVIDIA驱动像走钢丝?——从黑屏、循环登录到CUDA失效的底层逻辑你刚装好Ubuntu 22.04 LTS,兴冲冲插上RTX 4060笔记本显卡,想跑个Stable Diffusion或者训练个小模型,结果系统一重启——黑屏、光标不动、TTY… · 2026/9/26 17:38:49
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46