news 2026/10/6 10:31:20

C#高性能TCP服务器客户端实战:异步Socket、粘包拆包与心跳重连全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#高性能TCP服务器客户端实战:异步Socket、粘包拆包与心跳重连全解析

简介:面向高并发网络通信场景,一套基于完成端口(IOCP)的C#高性能TCP服务器与客户端源码包,实现高速报文收发并保持较低CPU占用,适合需要快速搭建稳定网络服务或研究IOCP机制的.NET开发者。压缩包共59个文件,整体约122KB,核心为13个.cs源码、可直接运行的exe、config配置以及csproj/sln工程文件,另有dll与pdb供调试参考,目录按IocpClient与IocpServer两端分置,便于对照阅读。作者在双核2G内存环境下验证,服务器可稳定承载5000以上客户端连接,默认监听9900端口;自带客户端与服务器完整通讯代码,可直接进行高并发数据收发性能测试。资源内封装的网络通信类可引入项目复用,附带“源码必读.txt”说明开发环境与常见问题处理,对希望掌握IOCP高并发编程的中高级C#开发者有较高参考价值。目前已有1302人学习下载。

1. 用 C# 写高性能 TCP 服务器和客户端,最值钱的不是 Socket 语法

拿 C# 搭 TCP 服务器和客户端,很多人第一个想法是网上搜一段 Socket 代码跑通就完事,但真正做上位机、设备数据采集、长连接推送的人很快就会撞上一堵墙:单客户端连接好好的,十几个客户端同时上来就开始丢包、端口占用、界面卡死,甚至服务器直接假死。这套 C# 完全端口高性能网络 TCP 服务器和客户端源码,解决的就是这一整条链路,而不是给你一段能 ping 通的 Demo。它把 listen、accept、收发缓存、粘包拆包、心跳重连这些 TCP 工程里绕不开的环节都做成了可以直接改的类,适合两类人:一类是做上位机需要和仪器、PLC、第三方服务对接的 C# 工程师,另一类是刚开始写网络服务、想搞清楚高性能服务器内部到底该怎么组织的后端新手。你能照着它把服务器起起来、把客户端连上去,然后在真实压力下看到哪些参数决定生死。

2. 先看架构再动手:异步模型、缓冲池和连接上下文是高性能的三大支柱

2.1 从同步阻塞到异步 Socket:这套源码的选型逻辑

TCP 服务端最原始的写法是Accept()一个连接就开一个Thread,在里面Receive()收数据。连接少的时候没问题,连接一多,线程数量膨胀,上下文切换开销直接压垮 CPU,这是同步阻塞模型的硬伤。这套源码里能看到的标准做法是:监听 Socket 用异步AcceptAsync,收发用基于SocketAsyncEventArgs的异步操作,回调由操作系统 IO 线程池承载,业务逻辑再投递到托管线程池。

两者的差别不只是“写法更高级”,而是线程模型的本质区别。同步模型里,一个线程在Receive()上阻塞时什么也干不了;异步模型里,SocketAsyncEventArgs在操作完成前不占用托管线程,由 IOCP 完成端口在数据到达时回调。高并发连接场景下,异步模型可以用少量线程撑起上千个连接,这套源码也是这么组织的。

实际改的时候你只需要关心中间几层,不需要重新发明一遍。核心链路是:

监听 Socket(异步 Accept) → 每个接入连接封装成一个 ConnectionContext → 分配 SocketAsyncEventArgs(收发复用同一个对象) → 收到数据进入缓冲区 → 解析包头 → 交给业务回调

2.2 连接上下文与缓冲区池:高性能的另一半不在 Socket

源码中你会看到一个很关键的类,通常叫ConnectionContext或Session,它把每个 TCP 连接的“家底”都存在一起。我拆过类似项目,这类上下文至少要盖住下面这些字段:

字段类型作用
Idlong连接唯一编号,用于日志追踪
SocketSocket当前连接的原生 Socket 引用
ReceiveSAEASocketAsyncEventArgs接收用的异步参数对象
SendSAEASocketAsyncEventArgs发送用的异步参数对象
ReceiveBufferbyte[]接收缓冲区,默认 8KB 或 64KB
SendQueueQueue<byte[]>发送队列,防止写半包
LastActiveTimeDateTime最后活动时间,用于心跳判断

一个很容易被忽略的细节是:SocketAsyncEventArgs对象应该池化复用,而不是每个连接每次都 new。在 .NET Framework 时代,频繁创建 SAEA 会造成 GC 压力;即使 .NET Core / .NET 5+ 做了很多优化,习惯上仍然保留 SAEA 池,源码里一般会有个SocketAsyncEventArgsPool,按“先取后还”的方式循环使用。我之前一个数据采集服务,从每连接 new SAEA 改成从池里取,GC 频率肉眼可见地降下来。

public SocketAsyncEventArgs Pop() { lock (_pool) { return _pool.Count > 0 ? _pool.Pop() : new SocketAsyncEventArgs(); } } public void Push(SocketAsyncEventArgs item) { item.SetBuffer(null, 0, 0); lock (_pool) { _pool.Push(item); } }

这段代码的逻辑很简单:取的时候先看池子里有没有空闲对象,有就复用,没有才创建;归还时清掉 Buffer 引用,避免上个连接的数据残留在内存里。这里有个参数细节值得注意:SetBuffer(null, 0, 0)是归还前必须做的清理,否则下一个连接用这个 SAEA 时可能读到上一个连接的脏数据。池的初始容量我一般会配成预期并发连接数的 1.5 倍,不要和最大连接数完全相同,否则峰值瞬间会频繁 new 对象。

2.3 拆包与粘包:没有协议边界,高性能就是空中楼阁

TCP 是流协议,业务上一个完整的数据包到了内核缓冲区可能被切一刀变成两块,也可能被粘成一个大数据包一起到达,这就是俗称的粘包和半包。源码能高性能工作的前提是它定义了一个明确的协议边界。

最常见的做法是长度前缀协议,即每个包先发 4 字节包头,表示后面的负载长度。收到数据后先攒够 4 字节解析出长度,再等负载收满一包。源码里一般会有一个PacketParser或MessageDecoder,内部维护一个临时缓冲区和当前包状态机。

public class LengthPrefixDecoder { private byte[] _buffer = new byte[1024]; private int _offset; private int _payloadLength; public List<byte[]> Decode(byte[] data, int length) { var packets = new List<byte[]>(); if (_offset + length > _buffer.Length) { Array.Resize(ref _buffer, Math.Max(_buffer.Length * 2, _offset + length)); } Array.Copy(data, 0, _buffer, _offset, length); _offset += length; while (true) { if (_offset < 4) break; _payloadLength = BitConverter.ToInt32(_buffer, 0); if (_offset < 4 + _payloadLength) break; var payload = new byte[_payloadLength]; Array.Copy(_buffer, 4, payload, 0, _payloadLength); packets.Add(payload); _offset -= 4 + _payloadLength; Array.Copy(_buffer, 4 + _payloadLength, _buffer, 0, _offset); } return packets; } }

这段解码器的逻辑是:先把新收到的数据拼到内部缓冲区,然后循环检查是否有完整包。第一个判断是“连 4 字节头都没凑够,不处理”;第二个判断是“头部有了但负载还没收完,等下一次 Receive”;两个条件都满足才切出一个完整负载。Array.Copy把剩余数据搬到缓冲区头部,是保证下一次拼接不会越界的关键。

用这个类时要额外留神BitConverter.ToInt32读取的字节序。默认是小端序,如果你的服务端要对接 C++ 或 Java 写的设备,它们可能用大端序,解析出来的长度会变成天文数字,直接导致缓冲区无限扩容。源码里通常会在协议初始化时指定Endianness,建议你改动前先确认对端协议。

3. 服务端落地:监听参数、连接管理和业务分发这样接才稳

3.1 监听 Socket 初始化:bind 之前要决定的三件事

服务端第一步是创建监听 Socket,这一步的参数一般人抄代码不会多想,但恰恰是高并发下翻车的高发区。

var listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); listenSocket.Bind(new IPEndPoint(IPAddress.Any, 9000)); listenSocket.Listen(1024);

这里三个关键参数分别解决三个问题:ReuseAddress让服务器重启时能立刻重新绑定同一端口,否则你在开发环境调试时杀进程后再启动,会经常碰到“地址已在使用”的报错;IPAddress.Any表示监听本机所有网卡地址,如果只想让内网特定网卡访问,就改成对应的IPAddress.Parse("192.168.1.10");Listen(1024)是内核监听队列长度,表示还没来得及Accept的连接可以排多长的队。

按我自己的经验,Listen的 backlog 在客户端连接数几百这个量级时设置成 512 到 1024 就够了,超过 5000 并发时再往上调,同时要配合net.core.somaxconn的系统参数一起改,只改 C# 这一层没意义。

3.2 Accept 循环:用异步接入避免连接堆积

建立监听后就是 Accept 循环。同步Accept()在监听 Socket 上阻塞,每个连接到来才能继续,处理不过来时 backlog 满了,新连接直接被内核拒绝,这在压力测试里表现为客户端连接超时。源码里的异步 Accept 循环一般长这样:

private void StartAccept(SocketAsyncEventArgs acceptEventArgs) { bool pending = _listenSocket.AcceptAsync(acceptEventArgs); if (!pending) { ProcessAccept(acceptEventArgs); } } private void ProcessAccept(SocketAsyncEventArgs e) { if (e.SocketError == SocketError.Success) { var clientSocket = e.AcceptSocket; var connection = new ConnectionContext(clientSocket); _connections.TryAdd(connection.Id, connection); StartReceive(connection); } StartAccept(e); }

这段代码里有个关键点:AcceptAsync返回bool表示操作是否同步完成。返回true代表操作还在内核中异步进行,完成后会触发Completed事件;返回false代表数据已经就绪,不用等回调,直接处理即可。两种情况都要走到ProcessAccept,很多新手只处理了回调分支,导致高并发时偶发丢连接。e.AcceptSocket拿到新连接后,立刻创建ConnectionContext并注册到ConcurrentDictionary,后续所有查找都是 O(1)。

我在自己项目里会额外加一步:ProcessAccept里先判断当前总连接数是否达到上限,超过就直接把AcceptSocket关掉。这是保护服务器不被恶意连接打满的常用兜底,源码里如果有类似配置项,上线前要按业务上限核对一遍。

3.3 接收数据与业务分发:回调里不要碰业务逻辑

接收过程是整条链路最容易被写坏的地方。正确的姿势是:SocketAsyncEventArgs完成回调只做数据搬运——把字节交给解码器,解析出完整包后投递给业务线程池,回调立即返回。

private void OnReceiveCompleted(object sender, SocketAsyncEventArgs e) { if (e.SocketError != SocketError.Success || e.BytesTransferred == 0) { CloseConnection(e); return; } var packets = _decoder.Decode(e.Buffer, e.BytesTransferred); foreach (var packet in packets) { ThreadPool.QueueUserWorkItem(_ => HandleMessage(e.UserToken as ConnectionContext, packet)); } _receiveSAEA.SetBuffer(_receiveSAEA.Offset, _receiveSAEA.Buffer.Length); bool pending = (e.UserToken as ConnectionContext).Socket.ReceiveAsync(e); if (!pending) { OnReceiveCompleted(this, e); } }

两个细节必须说明。第一,BytesTransferred == 0表示对端关闭,这是 TCP 半关闭的标准信号,必须在这里处理清理逻辑,否则连接永远不会释放。第二,SetBuffer(Offset, Length)用来重置接收偏移,如果不重置,第二次接收的数据会从上次的偏移位置开始写,收到的内容就是错位的。这个问题排查起来很隐蔽,现象是第一个包正常,第二个包开始全部乱码。

业务分发用ThreadPool.QueueUserWorkItem是常见做法,但如果你要保证同一连接的消息按顺序处理,就不能无脑丢线程池,否则多线程同时处理同一个连接的包会乱序。我一般的做法是每个ConnectionContext里维护一个按 FIFO 顺序执行的消息队列,或者用SemaphoreSlim把同一连接的处理逻辑串行化。

3.4 Idle 清理与心跳检测:不活跃连接必须定期收尸

如果没有心跳和空闲检测,客户端拔网线、断电,服务器端的连接会一直挂着,最终把连接数拖满。源码里通常有一个后台定时器,每 10 秒扫描一次所有连接,找出超过 N 秒没有数据活动的连接强制关闭。

private void CheckIdleConnections(object state) { var threshold = DateTime.Now.AddSeconds(-_idleTimeoutSeconds); foreach (var kvp in _connections) { if (kvp.Value.LastActiveTime < threshold) { CloseConnection(kvp.Value); } } }

LastActiveTime的更新时机是:收到任何字节时更新,包括心跳包。这里有个容易被忽略的坑,有些设备断电后 TCP 连接不会立刻断开,也没有 FIN 报文送到服务器,只能靠超时清理。_idleTimeoutSeconds我一般设成心跳间隔的 3 倍,比如设备每 10 秒发一次心跳,超时设为 30 秒,太短会导致正常连接被误杀,太长则收尸不及时。

4. 客户端实现:连接、心跳和重连要按对端协议定制

4.1 客户端 Socket 初始化:NoDelay 和连接超时是必须显式控制的

客户端代码在源码里同样是一等公民,它是和服务端配合的调试利器。客户端连接服务器,最常踩的坑是 Nagle 算法:小数据包被内核缓存一段时间再发出去,交互式请求明显延迟。

var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); clientSocket.NoDelay = true; clientSocket.SendTimeout = 3000; clientSocket.ReceiveTimeout = 3000; var task = clientSocket.ConnectAsync(new IPEndPoint(IPAddress.Parse("192.168.1.100"), 9000)); if (!task.Wait(5000)) { throw new TimeoutException("连接超时:5秒内未完成 TCP 三次握手"); }

NoDelay = true表示禁用 Nagle,每条小消息立即发送,适合需要低延迟的实时控制;如果业务是批量上传大文件,把它设为 false 反而能提高吞吐。ConnectAsync返回的Task在超时控制上要注意:很多封装直接把ConnectAsyncawait,导致对端 IP 不可达时卡十几秒甚至几十秒才抛异常。Task.Wait(5000)是我惯用的后备方案,超过 5 秒直接判超时,做上位机的人会很有共鸣——连不上设备时早点报错比卡死强得多。

4.2 心跳与断线重连:要在业务静默时维持通路

很多协议栈设备一两个小时不发一包业务数据,如果中间网络设备把空闲连接清了,你需要感知到并自动重连。客户端的标准做法是后台定时发心跳帧,同时记录最后一次收到服务端数据的时间,连续超时则重启连接逻辑。

private async void HeartbeatLoop(CancellationToken token) { while (!token.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(10), token); var lastReceived = Interlocked.Read(ref _lastReceiveTicks); if (Environment.TickCount64 - lastReceived > 30000) { Reconnect(); return; } SendHeartbeatPacket(); } }

心跳包的格式必须严格跟着协议走,不能自己发明。源码里通常留了一个BuildHeartbeatPacket方法让你按业务重写,有的设备心跳是 1 字节的0x00,有的要求带固定魔数,有的用业务层的特定命令。30000这个阈值取的是“心跳间隔 × 3”,太敏感会在服务端 GC 停顿或网络抖动时频繁误判重连。重连逻辑里还要注意退避:连续失败时按 1、2、4、8 秒递增,不能死循环每 3 秒重连一次,否则服务器刚恢复就会被你的客户端连接风暴冲垮。

4.3 收发缓冲区的容量匹配:客户端不是越简单越好

客户端收到的数据包大小通常比服务端小,但缓冲区设计同样不能省。很多新手在客户端把ReceiveBuffer设成 1024 字节,以为够了,结果对端发来一个 2KB 的响应就直接截断。源码里服务端和客户端的ReceiveBuffer往往是可配置的,我在实际项目中客户端收到数据后还要做一次合法性校验,比如魔数不对、长度超上限就断开连接,防止对端异常数据把客户端内存撑爆。这个策略在对接第三方厂商设备时尤其重要,设备逻辑有 bug 发来大包,不能让它把上位机拖死。

4.4 联调时的抓包验证:用数据而不是猜

客户端写完后第一步不是连真实服务器,而是先用本地回环测试,再用 Wireshark 抓包验证。重点关注三点:三次握手的 SYN/SYN-ACK/ACK 是否完整;数据包是否有粘包现象;关闭连接时 FIN/ACK 是否正确交互。源码里如果有自带的 TestClient 或 Debug 模式,保留它是很有价值的,它能帮你确认对端协议差异到底在包头还是负载。

5. 避坑排查:端口、缓冲区、粘包与连接管理的五个常见翻车点

5.1 服务器重启后报“地址已在使用”

现象:开发调试时关闭服务器进程,立刻重新启动服务,Bind抛SocketException: Address already in use。

原因:主动关闭的 TCP 连接会进入 TIME_WAIT 状态,持续 2MSL(约 60 秒),期间端口处于半释放状态。没设置ReuseAddress时,新进程绑定同一端口会被拒绝。

解决:创建监听 Socket 后立即执行SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true)。注意这条选项必须写在Bind之前,写在后面无效。Windows 和 Linux 行为有细微差异,Linux 上ReuseAddress加上ReusePort才能在多进程监听同一端口时生效,单进程服务用前者就够。

5.2 第一个包正常,第二个包开始乱码

现象:客户端正常发送第一包数据,服务器正确解析;紧接着发第二包,服务器解析出来的内容错位、长度异常。

原因:这是典型的接收缓冲区没有重置偏移。SocketAsyncEventArgs.Buffer是复用对象,上一次接收后内部偏移停留在已读位置,第二次直接写入导致数据从错误位置开始拼接。

解决:每次完成回调准备下一次ReceiveAsync前,调用SetBuffer(e.Offset, e.Buffer.Length)复位。同时把解码器的合并逻辑做成无状态增量式,不要每次新包从头初始化一次。

5.3 客户端偶发报错 10054 / 连接被重置

现象:客户端长时间运行后,某个时刻Receive抛SocketException: 远程主机强迫关闭了一个现有的连接,错误码 10054。

原因:服务端主动关闭了连接——可能是超时收尸,可能是异常崩溃,也可能是对端网络设备发 RST。客户端感知到 RST 后就会抛这个异常。对某些协议设备来说,出现 10054 是正常的运维事件,不是代码 bug。

解决:在客户端把 10054 当成重连触发条件之一,不要当成致命异常往上抛。处理顺序是:捕获异常 → 记录日志和当前连接状态 → 清理旧 Socket → 走断线重连流程。注意日志不要打太多刷屏,用LogLevel.Information记录一次即可。

5.4 连接数一多,接收回调迟迟不触发

现象:本地测试 10 个客户端都正常,压测升到 500 连接后,部分连接收数据明显延迟,或者完全不回调。

原因:回调查里做了耗时操作——比如直接在回调里解析 JSON、写数据库、锁竞争,把 IOCP 线程池阻塞了。回调线程被阻塞,后续所有这个线程承载的 IO 完成通知全部排队。

解决:回调里只做字节搬运和包解析,业务处理全部丢业务线程池;实在要做同步耗时操作,单独开消费者队列。如果用了锁,检查锁粒度,不要锁整段解析过程。这个问题的排查方法是抓 dump 看线程栈,看到OnReceiveCompleted卡在DbCommand.Execute上就说明业务太重了。

5.5 Nagle 与延迟 ACK 相互作用导致写口卡顿

现象:客户端发送小包后被服务器延迟几十毫秒才响应,不影响正确性但交互明显变慢。

原因:TCP 的 Nagle 算法会攒小包,延迟 ACK 机制会等待 40ms 再回 ACK,两者叠加造成“写完等 ACK”的假阻塞。

解决:低延迟交互场景在双方 Socket 上都设置NoDelay = true。但如果传输大量文件、追求吞吐最大化,关掉 Nagle 不一定更好,要按业务特征选。这个现象在 Modbus TCP、自定义短指令协议里很典型,很多设备厂商的 SDK 默认关了 Nagle,你复用它时保持一致就行。

6. 进阶验证:用并发压测把边界试出来,别等上线再翻车

拿到这套源码后,最值得做的一步是写一个压测脚本,把服务器和客户端都推到真实业务量级以上,确认瓶颈在哪儿。我通常的做法是写一个小控制台程序,同时建立 500 个 TcpClient 连接,每个连接循环发送业务包,统计服务器处理后的回包数量和延迟分布。

var clients = new List<TcpClient>(); for (int i = 0; i < 500; i++) { var client = new TcpClient(); await client.ConnectAsync("127.0.0.1", 9000); clients.Add(client); } var stopwatch = Stopwatch.StartNew(); await Parallel.ForEachAsync(clients, async (client, _) => { var stream = client.GetStream(); var payload = Encoding.UTF8.GetBytes("hello"); var header = BitConverter.GetBytes(payload.Length); await stream.WriteAsync(header.Concat(payload).ToArray()); await stream.ReadAsync(new byte[1024], 0, 1024); }); Console.WriteLine($"500 连接并发耗时: {stopwatch.ElapsedMilliseconds} ms");

压测时我只会看三个指标:500 连接全量握手耗时、并发回包成功率、服务端 CPU 和内存是否有持续增长。先跑 1 分钟看 GC 频率,再跑 30 分钟看内存曲线,最后把客户端数量翻一倍看系统熔断前崩溃的形态——主动拒绝还是卡死。这一步做下来,比读十遍源码都清楚线程池配置和缓冲区大小该怎么调。从那以后我每次写 TCP 服务,落地前都强制把压测脚本跑一遍,连带着把客户端的超时参数、重连退避一起调了,省得上线后半夜被叫起来查 10054。这套源码的价值也在这里,它给你的是一个能验证、能改、能跑的起点,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 10:31:05

FastAPI+Vue3实战:从零搭建网上书店系统全解析

去年下半年&#xff0c;我接了个活儿&#xff1a;给一个做二手教材生意的朋友搭一套网上书店系统。需求并不复杂——能展示图书、能搜索、能加购物车、能下单&#xff0c;最好还能有个简单的后台管理库存。我当时的想法很直接&#xff0c;后端用Python把最核心的业务逻辑跑通&a…

作者头像 李华
网站建设 2026/10/6 10:29:53

S7-200 PLC与组态王的水箱液位控制系统设计与调试

“毕业设计做了个水箱液位控制系统&#xff0c;S7-200 PLC加组态王&#xff0c;梯形图也都是自己写的”——这应该是很多自动化、电气、机电专业的学生都绕不开的一个题目。说实话&#xff0c;虽然现在S7-1200/1500和触摸屏组合越来越主流&#xff0c;但在教学和毕设场景里&…

作者头像 李华
网站建设 2026/10/6 10:29:35

OpenShell:让Shell脚本从命令堆砌升级为可维护的自动化框架

不知道你是不是跟我一样&#xff0c;最初接触Shell时觉得它就是个"敲命令的黑框框"&#xff0c;直到被一个又一个零散脚本、一堆搞不清含义的$1 $2、还有深夜跑挂了没人发现的定时任务折磨过之后&#xff0c;才真正意识到&#xff1a;Shell脚本要想工程化&#xff0c…

作者头像 李华
网站建设 2026/10/6 10:29:17

基于DDD的API接口优雅重构实践:从上帝接口到业务语言

重构API这件事&#xff0c;我干了快十年&#xff0c;见过太多服务从两三个接口膨胀到一两百个&#xff0c;最后变成谁都不敢改、改了必出事故的雷区。去年帮一个创业团队做技术评审&#xff0c;他们有一个下单接口&#xff0c;参数是一坨三百多行的JSON&#xff0c;里面既有用户…

作者头像 李华
网站建设 2026/10/6 10:29:13

OpenShell终极指南:找回Windows经典开始菜单与高效资源管理器

如果你在Windows 7时代用惯了开始菜单&#xff0c;突然被丢到Windows 8/10/11面前&#xff0c;大概率会有一个共同的念头&#xff1a;原来的开始菜单到底去哪了。OpenShell就是我这几年来一直留在每台Windows机器上的答案。这个开源项目的前身叫Classic Shell&#xff0c;后来因…

作者头像 李华
网站建设 2026/10/6 10:28:46

微网动态经济调度中的场景生成与削减实战指南

做微网动态经济调度的人&#xff0c;几乎都要和“场景”这两个字打交道。注意&#xff0c;这里说的场景&#xff0c;跟最近很火的那种“上传一段视频生成对应的三维场景”完全不是一回事。在电力系统的语境里&#xff0c;场景是指对风电、光伏、负荷这些不确定量未来可能呈现的…

作者头像 李华