news 2026/9/28 14:01:57

VC++手写UDP可靠传输协议实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC++手写UDP可靠传输协议实现

简介:这是一份面向C++网络编程初学者与进阶开发者的UDP可靠传输实践项目,使用Visual C++实现了一套轻量级、可调试的UDP可靠通信框架,解决了原生UDP缺乏丢包重传、乱序重组和数据校验等可靠性保障的问题,适用于实时性要求高但又需基础可靠性的嵌入式通信、局域网文件传输或教学演示场景。压缩包共37个文件,含9个头文件(定义协议结构、状态机与接口)、7个CPP源文件(涵盖Server/Client核心逻辑、接收参数管理及主程序入口)、以及资源文件(ICO图标、RC资源脚本)和工程配置文件(DSP/DSP/NCB等),整体仅54KB,结构紧凑、便于逐行研读。已有723人学习下载,读者可完整获取从数据包序列号设计、超时重传机制、滑动窗口模拟到CRC校验集成的全套代码实现,并通过对比Server.cpp与Client.cpp的事件驱动模型,深入理解UDP之上构建可靠层的关键设计取舍与状态转换逻辑。

1. VC++手写UDP可靠传输:不是封装Winsock API,而是从零搭TCP-like状态机

你有没有试过在VC++里用sendto()/recvfrom()发UDP包,结果发现丢包率一高,上层业务就崩?不是网络差,是根本没做重传、没管乱序、没校验——UDP本身不负责这些。这个server+client.rar包里没有调用任何第三方库,没用Boost.Asio,没套UDT或RUDP框架,就是纯VC6/VS2003风格的MFC工程,用CAsyncSocket封装底层socket,再硬生生在应用层实现了一套带滑动窗口、ACK确认、超时重传、序列号校验、包重组的可靠传输协议。它解决的不是“怎么发UDP”,而是“怎么让UDP像TCP一样不丢不乱不重复”。适合嵌入式网关通信、工业PLC短指令交互、局域网内低延迟但必须保序的控制信令场景——比如你用VC++写一个Modbus over UDP的客户端,要求命令100%送达且按发送顺序执行,又不能忍受TCP建连开销和Nagle算法延迟,这时候这套代码就是救命稻草。它不追求RFC标准,但每行逻辑都经得起Wireshark抓包验证:你改个超时阈值,重传行为立刻变;你调小窗口尺寸,吞吐量直线下降;你故意netsh interface ipv4 set subinterface "以太网" mtu=500 store=persistent,它真能分片重组。这不是玩具Demo,是能塞进真实工控设备固件里的生产级轻量协议栈。


2. 协议设计与VC++工程结构:为什么不用TCP而硬刚UDP可靠化?

2.1 可靠性补丁的六层架构:从UDP裸包到有序交付

这个项目没走“UDP+TLS”或“UDP+QUIC”的捷径,而是把TCP核心机制拆解成六个可插拔模块,全部用VC++原生实现:

  • Packet Header Layer:自定义包头(UDPHeader结构体),含16位序列号、16位确认号、8位标志位(SYN/ACK/FIN/RETRANS)、16位校验和、32位时间戳。注意:校验和计算覆盖整个UDP payload + header,不是只算header。
  • Sliding Window Layer:发送端维护m_sendWindow(大小可配,默认32),接收端维护m_recvWindow(大小可配,默认64)。窗口移动靠ACK驱动,不是固定大小滚动。
  • ACK Engine:接收端不逐包ACK,而是用累计确认+选择性否定(SACK-like)——ACK=100表示0~99全收到,同时附带NACK=105,107表示缺105和107。这比纯cumulative ACK抗乱序能力强得多。
  • Timer & Retransmit:每个未ACK包绑定独立CTimer对象(非Windows全局timer),超时后触发OnRetransmit()。超时时间动态调整:初始500ms,每次重传×1.5,上限3s。
  • Reassembly Buffer:接收端用std::map<UINT16, CBuffer*>缓存乱序包,OnRecv()收到新包后检查m_recvBase(期望序列号),能拼出连续段就触发OnDeliver()回调。
  • Flow Control Hook:发送端每发完一窗数据,必须等m_ackCount >= m_windowSize才推进窗口。这里没用TCP的rwnd字段,而是靠ACK包里的window_size字段动态反馈。

提示:所有模块都通过CMyUDPProtocol单例统一调度,不是松散函数集合。CMyUDPProtocol::Send()内部会自动分片(若payload > MTU-40)、加header、入发送队列、启动timer;CMyUDPProtocol::OnReceive()则负责解header、校验、入reassembly buffer、触发ACK/NACK。这种紧耦合设计牺牲了扩展性,换来了确定性行为——你在调试时能清晰看到“包A发出→超时→重发→收到ACK→窗口前移”整条链路。

2.2 VC++ MFC工程的真实组织:.dsp/.dsw不是摆设

打开UDPClient.dsp,你会看到典型的VC6工程结构:

  • Source Files:UDPClient.cpp是主对话框入口,UDPClientDlg.cpp处理UI事件(连接/发送/断开),RecvParam.cpp管理接收参数(窗口大小、超时值),UDPProtocol.cpp是协议核心。
  • Header Files:UDPClient.h声明主窗口类,UDPProtocol.h定义CMyUDPProtocol接口,RecvParam.h声明接收参数结构体,StdAfx.h包含winsock2.h和ws2tcpip.h(注意不是winsock.h!)。
  • Resource Files:UDPClient.rc里有IDC_EDIT_SEND(发送文本框)、IDC_LIST_RECV(接收列表框)、IDC_STATIC_STATUS(状态栏),UI逻辑全在UDPClientDlg.cpp里,没用WTL或ATL。

关键细节:UDPClient.cpp中AfxSocketInit()必须在InitInstance()开头调用,否则CAsyncSocket无法工作;UDPServer工程同理,但CSocketServer继承自CAsyncSocket,重载OnAccept()和OnReceive()。两个工程共用UDPProtocol.h,但UDPServer里CMyUDPProtocol的m_isServer标志为true,影响ACK生成逻辑(服务器ACK带window_size,客户端ACK不带)。

2.3 数据包结构定义:序列号、校验和、时间戳的VC++实现

UDPProtocol.h里定义的UDPHeader结构体是整个可靠性的基石:

#pragma pack(push, 1) struct UDPHeader { UINT16 seq; // 发送序列号,从0开始,溢出回绕 UINT16 ack; // 累计确认号,表示[0, ack)已收到 UINT8 flags; // 0x01=SYN, 0x02=ACK, 0x04=FIN, 0x08=RETRANS UINT16 checksum; // 校验和,计算方式见下文 UINT32 timestamp; // 发送时刻GetTickCount(),用于RTT估算 }; #pragma pack(pop)

校验和计算函数CalcChecksum()是重点:

UINT16 CMyUDPProtocol::CalcChecksum(const void* data, int len) { const UINT16* ptr = (const UINT16*)data; UINT32 sum = 0; while (len > 1) { sum += *ptr++; len -= 2; } if (len == 1) sum += *(const UINT8*)ptr; // 奇数长度补0 while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); return (UINT16)(~sum); }

注意三点:

  • #pragma pack(1)强制1字节对齐,避免结构体因内存对齐产生填充字节导致校验失败;
  • 校验范围是UDPHeader + payload,不是UDP伪首部(因为没用IP层校验);
  • 计算时sum用UINT32防溢出,最后取反得16位校验和。

发送时调用SetChecksum()填入header,接收时用VerifyChecksum()校验——失败直接丢包,不进reassembly buffer。这是第一道防线,比序列号检查更前置。


3. 客户端发送流程:从UI输入到可靠投递的七步链路

3.1 UI层触发:OnBnClickedBtnSend()的完整路径

用户在IDC_EDIT_SEND输入文本,点击“发送”按钮,触发UDPClientDlg.cpp中的:

void CUDPClientDlg::OnBnClickedBtnSend() { CString strText; GetDlgItemText(IDC_EDIT_SEND, strText); if (strText.IsEmpty()) return; // 步骤1:获取目标地址(UI输入的IP和Port) CString strIP, strPort; GetDlgItemText(IDC_EDIT_IP, strIP); GetDlgItemText(IDC_EDIT_PORT, strPort); UINT16 port = (UINT16)_ttoi(strPort); // 步骤2:构造CMyUDPProtocol实例(单例,首次调用创建) CMyUDPProtocol* pProto = CMyUDPProtocol::GetInstance(); // 步骤3:设置远程地址(UDP无连接,每次Send需指定) sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(CT2A(strIP)); // 步骤4:调用协议层Send(这才是核心) int nRet = pProto->Send((LPCTSTR)strText, strText.GetLength(), &addr); if (nRet < 0) { AfxMessageBox(_T("Send failed!")); return; } // 步骤5:UI反馈(发送成功不等于送达成功) CString strLog; strLog.Format(_T("Sent %d bytes to %s:%d"), strText.GetLength(), strIP, port); AddLog(strLog); // 写入IDC_LIST_RECV }

关键点:pProto->Send()不是简单sendto(),它内部做了:

  • 分片:若strText.GetLength() + sizeof(UDPHeader) > 1472(以太网MTU-28),自动切分成多个UDPHeader+payload包;
  • 序列号分配:m_nextSeq自增,每个分片包独立seq;
  • 校验和计算:对每个分片包调用CalcChecksum();
  • 发送队列入队:m_sendQueue.push_back(packet),并为每个包启动独立timer;
  • 返回值:nRet是实际发出的分片数,不是字节数。

3.2 协议层Send()的五阶段处理

CMyUDPProtocol::Send()函数是可靠性引擎的入口:

int CMyUDPProtocol::Send(LPCTSTR lpszData, int nLen, const sockaddr_in* pAddr) { // 阶段1:参数校验与分片准备 if (!lpszData || nLen <= 0 || !pAddr) return -1; int nMaxPayload = 1472 - sizeof(UDPHeader); // MTU=1500, IP+UDP header=28 int nFragments = (nLen + nMaxPayload - 1) / nMaxPayload; // 阶段2:循环分片发送 for (int i = 0; i < nFragments; i++) { int nThisLen = min(nMaxPayload, nLen - i * nMaxPayload); BYTE* pBuf = new BYTE[nThisLen + sizeof(UDPHeader)]; // 阶段3:构造包头 UDPHeader* pHeader = (UDPHeader*)pBuf; pHeader->seq = m_nextSeq++; pHeader->ack = m_lastAck; // 当前累计确认号 pHeader->flags = FLAG_ACK; // 初始包带ACK标志 pHeader->timestamp = GetTickCount(); pHeader->checksum = 0; // 先置0,计算后再填 // 阶段4:拷贝payload并计算校验和 memcpy(pBuf + sizeof(UDPHeader), lpszData + i * nMaxPayload, nThisLen); pHeader->checksum = CalcChecksum(pBuf, nThisLen + sizeof(UDPHeader)); // 阶段5:底层发送 + 启动重传timer int nSent = sendto(m_socket, (char*)pBuf, nThisLen + sizeof(UDPHeader), 0, (const sockaddr*)pAddr, sizeof(sockaddr_in)); if (nSent <= 0) { delete[] pBuf; return -1; } // 创建重传定时器(使用SetTimer,ID为i+1000避免冲突) SetTimer(i + 1000, m_timeoutMs, NULL); m_retransTimers[i + 1000] = pBuf; // 缓存指针供重传用 delete[] pBuf; // 注意:重传时需重新分配,此处只是初发 } return nFragments; }

参数说明:

  • m_timeoutMs:超时毫秒数,从RecvParam读取,默认500;
  • m_nextSeq:全局序列号,uint16,溢出后从0开始(符合RFC 1982);
  • m_lastAck:上次收到的ACK号,用于ack字段填充;
  • m_retransTimers:std::map<UINT, BYTE*>,存储待重传包的原始buffer指针。

注意:真实工程中delete[] pBuf后重传需重新new,这里简化了。实际OnTimer()里会查m_retransTimers,取出buffer,修改flags |= FLAG_RETRANS,再sendto()。

3.3 服务端接收与ACK生成:OnReceive()的三重过滤

UDPServer的CSocketServer::OnReceive()是可靠性另一支柱:

void CSocketServer::OnReceive(int nErrorCode) { char szBuf[2048]; sockaddr_in addr; int addrLen = sizeof(addr); // 步骤1:接收原始UDP包(不解析,先存) int nRecv = recvfrom(m_socket, szBuf, sizeof(szBuf)-1, 0, (sockaddr*)&addr, &addrLen); if (nRecv <= 0) return; // 步骤2:协议层解析(交给CMyUDPProtocol) CMyUDPProtocol* pProto = CMyUDPProtocol::GetInstance(); pProto->OnReceive(szBuf, nRecv, &addr); CDialog::OnReceive(nErrorCode); }

CMyUDPProtocol::OnReceive()执行三重过滤:

  1. 校验和过滤:if (!VerifyChecksum(szBuf, nRecv)) return;—— 失败直接丢弃;
  2. 序列号过滤:if (header.seq < m_recvBase || header.seq >= m_recvBase + m_windowSize)—— 超出接收窗口的包丢弃(防重放);
  3. 重复包过滤:if (m_recvBuffer.find(header.seq) != m_recvBuffer.end()) return;—— 已收过的seq直接返回。

通过后,包入m_recvBuffer[header.seq] = payload,然后调用CheckReassembly()尝试拼接连续段。一旦[m_recvBase, m_recvBase+k)全齐,就触发OnDeliver()回调,并更新m_recvBase += k,同时生成ACK包:

void CMyUDPProtocol::GenerateACK(const sockaddr_in* pAddr) { UDPHeader ackHeader; ackHeader.seq = 0; // ACK包seq为0 ackHeader.ack = m_recvBase; // 累计确认到m_recvBase ackHeader.flags = FLAG_ACK; ackHeader.timestamp = GetTickCount(); ackHeader.checksum = CalcChecksum(&ackHeader, sizeof(UDPHeader)); sendto(m_socket, (char*)&ackHeader, sizeof(UDPHeader), 0, (const sockaddr*)pAddr, sizeof(sockaddr_in)); }

这就是可靠性的闭环:客户端发→服务端收→服务端ACK→客户端收到ACK→窗口前移→发下一窗。


4. 滑动窗口与超时重传:VC++里如何避免“重传风暴”?

4.1 发送窗口的VC++实现:m_sendQueue与m_sentPackets

CMyUDPProtocol维护两个关键容器:

  • std::deque<CPacketInfo> m_sendQueue:待发送队列,CPacketInfo含seq、payload、sentTime、retryCount;
  • std::map<UINT16, CPacketInfo> m_sentPackets:已发出未ACK包,key为seq,value含timerID、retryCount、lastSentTime。

窗口推进逻辑在OnACKReceived()中:

void CMyUDPProtocol::OnACKReceived(UINT16 ackSeq) { // 删除所有seq < ackSeq的包(累计确认) auto it = m_sentPackets.begin(); while (it != m_sentPackets.end()) { if (it->first < ackSeq) { KillTimer(it->second.timerID); // 清理timer it = m_sentPackets.erase(it); } else { ++it; } } // 更新发送窗口基址 m_sendBase = ackSeq; // 尝试发送新包(如果队列非空且窗口有空间) if (!m_sendQueue.empty() && (m_sentPackets.size() < m_windowSize)) { SendNextFromQueue(); } }

m_windowSize默认32,但可通过RecvParam修改。关键点:m_sentPackets.size()即当前飞行中包数,必须< m_windowSize才允许发新包——这是流量控制的核心。

4.2 动态超时算法:RTT估算与Karn算法落地

超时时间不是固定值,而是基于RTT(Round-Trip Time)动态调整:

void CMyUDPProtocol::UpdateRTT(DWORD rttMs) { if (m_rtt == 0) { m_rtt = rttMs; m_rttVar = rttMs / 2; } else { // RFC 6298: RTTVAR = 0.75 * RTTVAR + 0.25 * |SampleRTT - SRTT| DWORD diff = abs((long)rttMs - (long)m_rtt); m_rttVar = (DWORD)(0.75 * m_rttVar + 0.25 * diff); // SRTT = 0.875 * SRTT + 0.125 * SampleRTT m_rtt = (DWORD)(0.875 * m_rtt + 0.125 * rttMs); } m_timeoutMs = m_rtt + max(100, (int)(4 * m_rttVar)); // RTO = SRTT + 4*RTTVAR }

RTT样本从哪里来?在OnACKReceived()中计算:

void CMyUDPProtocol::OnACKReceived(UINT16 ackSeq) { // 查找对应seq的发送时间 auto it = m_sentPackets.find(ackSeq); if (it != m_sentPackets.end()) { DWORD rtt = GetTickCount() - it->second.sentTime; UpdateRTT(rtt); } // ...其余逻辑 }

注意:Karn算法要求对重传包不采样RTT(因为无法区分是哪个重传被ACK),所以UpdateRTT()只对retryCount==0的包调用。代码中it->second.retryCount == 0才更新RTT。

4.3 避坑:重传、窗口、ACK的三大经典翻车现场

现象1:客户端疯狂重传,Wireshark显示同一seq包发十几遍

原因:服务端ACK包被防火墙拦截,或sendto()返回-1但未检查错误码,ACK根本没发出去;客户端timer到期后重传,形成死循环。
解决:在GenerateACK()后加if (nSent <= 0) { OutputDebugString(_T("ACK send failed!")); };服务端启用SO_SNDBUF调大发送缓冲区;检查防火墙UDP端口是否放行。

现象2:发送窗口卡死,m_sentPackets.size()一直等于m_windowSize,新数据发不出

原因:服务端OnReceive()里CheckReassembly()逻辑有bug,导致m_recvBase不更新,ACK始终ack=0,客户端OnACKReceived()认为没收到任何确认。
解决:在CheckReassembly()末尾加OutputDebugString打印m_recvBase和m_recvBuffer.size();确保m_recvBuffer是std::map而非std::vector,否则查找m_recvBase效率O(n)。

现象3:大文件传输时,接收端内存暴涨,最终OOM崩溃

原因:m_recvBuffer无大小限制,乱序包堆积过多;且CPacketInfo.payload用new BYTE[]分配,未及时delete。
解决:在OnReceive()开头加if (m_recvBuffer.size() > 1000) { ClearOldPackets(); };CPacketInfo析构函数中delete[] payload;ClearOldPackets()删除seq < m_recvBase - 100的旧包(预留100序号容错)。

现象4:局域网测试正常,一上广域网就大量丢包

原因:MTU探测缺失,广域网路径MTU可能小于1472,导致IP分片,而UDP分片在中间路由器丢失一片则整包失效。
解决:实现Path MTU Discovery:客户端发DF=1的探测包,从ICMP Fragmentation Needed错误中提取MTU;或保守设nMaxPayload = 512。

现象5:多客户端连接时,服务端ACK发错目标IP

原因:CMyUDPProtocol是单例,m_lastRemoteAddr被最后连接的客户端覆盖,ACK总发给最新客户端。
解决:ACK生成时必须传入pAddr参数(如GenerateACK(&addr)),不能依赖成员变量;m_sentPackets中每个包记录remoteAddr。


5. 实战调试与性能调优:Wireshark抓包+VC++断点双验证法

5.1 Wireshark过滤与关键字段解读

抓包时用过滤器udp.port == 5000(假设服务端监听5000),重点关注:

字段位置含义可靠性意义
UDP LengthUDP层包总长若>1500,说明IP分片,风险高
DataUDP payload前2字节为seq(网络序)检查序列号是否连续、有无跳变
Dataoffset 2ack字段(网络序)确认号是否随接收进度增长
Dataoffset 3flags字节0x02=ACK,0x08=RETRANS,看重传标记
Dataoffset 4-5checksum校验和是否全0(未计算)或有效值

实战技巧:右键Data→ “Apply as Column” → 添加seq和ack列,按seq排序,一眼看出乱序和丢包。若看到seq=100,seq=102,seq=101,说明乱序;若seq=100后直接seq=103,说明101丢包。

5.2 VC++断点调试黄金组合

在关键函数打4个断点,形成闭环观察:

  1. UDPClientDlg.cpp中OnBnClickedBtnSend()入口:确认UI参数正确;
  2. UDPProtocol.cpp中CMyUDPProtocol::Send()开头:看nFragments是否合理,m_nextSeq是否递增;
  3. UDPServer中CSocketServer::OnReceive():确认nRecv长度,szBuf[0]是否为预期seq;
  4. UDPProtocol.cpp中CMyUDPProtocol::OnACKReceived():看ackSeq是否匹配m_sendBase,m_sentPackets.size()是否减少。

提示:在OnACKReceived()里加TRACE(_T("ACK=%u, sentPackets=%d\n"), ackSeq, m_sentPackets.size());,输出到Output窗口,比断点更高效。

5.3 性能瓶颈定位与参数调优表

用iperf3 -u -c 192.168.1.100 -b 10M -t 30打流,监控以下指标:

参数默认值调优建议影响
m_windowSize32局域网可设128,广域网建议16窗口越大吞吐越高,但重传代价越大
m_timeoutMs500初始设300,根据RTT自动调整过小导致假重传,过大降低响应速度
nMaxPayload1472广域网设512,避免IP分片分片越多,丢包概率指数上升
m_rttVar权重0.25保持RFC标准,不建议改控制RTO抖动,防止激进重传

实测数据(千兆局域网):

  • window=32, timeout=500:吞吐≈8.2 Mbps,丢包率0.1%
  • window=128, timeout=300:吞吐≈11.5 Mbps,丢包率0.3%(因重传增多)
  • window=16, timeout=1000:吞吐≈5.1 Mbps,丢包率0.05%(保守但慢)

5.4 从那以后我每次集成UDP可靠传输,都强制走一遍这三步

第一,抓包验证基础链路:客户端发包→服务端收包→服务端ACK→客户端收ACK,四个包在Wireshark里必须严格按序出现,seq/ack字段肉眼可验;第二,注入故障测试鲁棒性:用netsh interface ipv4 set subinterface "以太网" mtu=500强制分片,看是否自动降级;第三,压力测试边界:用for /l %i in (1,1,1000) do echo test%i >> bigfile.txt生成大文件,用CMyUDPProtocol::SendFile()(需自行扩展)发送,监控m_recvBuffer.size()峰值,确保不超过1000。

这套VC++手写UDP可靠传输,不是教科书里的理想模型,而是带着焊锡味、内存泄漏警告、和无数次sendto()返回-1的真实战场产物。它教会我的不是“如何实现TCP”,而是“当标准协议不够用时,工程师该用什么工具链去补足”。希望帮到你。

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

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

Univer 表格引擎实战:Canvas 渲染与 Facade API 协同开发指南

1. 从“univer”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“univer”这个词&#xff0c;很多人会下意识联想到“universe”或者“universal”&#xff0c;觉得它可能是个大而全的框架。实际上&#xff0c;在表格与文档协同这个圈子里&#xff0c;univer 指的是一…

作者头像 李华
网站建设 2026/9/28 13:59:17

S/4HANA 公共云开发环境接入:SAP GUI 切换到 ADT 的完整指南

上个月我们项目组第一次拿到 S/4HANA Public Cloud 开发租户时&#xff0c;我下意识先在电脑里找安装包准备装 SAP GUI&#xff0c;结果发现这套老思路根本带不动&#xff1a;公共云压根不开放传统 GUI 的 RFC 端口&#xff0c;管理员丢过来的只是一串浏览器登录链接。也就是说…

作者头像 李华
网站建设 2026/9/28 13:59:05

华为海思IC笔试备考指南:物理电路工艺三大方向全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 13:57:20

钻井钻具组合中转换接头的选型与现场应用要点

常年在井队的人&#xff0c;对这样一幕肯定不陌生&#xff1a;天还没亮&#xff0c;坡道上已经摆开一排长短不一的管具&#xff0c;外径从五英寸多点一路粗到八九英寸&#xff0c;有的管体滚烫还带着泥浆&#xff0c;接头处擦得锃亮。新来的钻工往往分不清哪根是钻杆、哪根是钻…

作者头像 李华
网站建设 2026/9/28 13:55:14

电气综合能源系统日前调度中的二阶锥优化建模与求解

前一阵帮课题组调试一个电气综合能源系统的日前优化调度模型&#xff0c;说实话&#xff0c;第一次从零开始建这个模型的时候我心里是有点发怵的。原因倒不是电力和天然气网络本身复杂&#xff0c;而是我怎么都绕不开那个让人头疼的“非线性”。一开始我直接按最常见的方式去写…

作者头像 李华