ARTICLE DETAIL

资讯详情

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

VC++6.0实战:TCP长连接接收五档行情并MFC实时显示

VC++6.0实战:TCP长连接接收五档行情并MFC实时显示 简介面向VC 6.0开发者的股票行情示例工程讲解如何借助微软基础类库获取并显示沪深股市五档实时行情。工程围绕网络通信建立数据通路涵盖请求封装、多线程取数、界面绑定与异常处理等关键模块适合需要了解早期开发环境下股票桌面软件实现思路的读者学习。压缩包仅33KB共18个文件以头文件与源文件为主另有工程配置、界面资源和说明文档结构清晰便于直接编译查看。已有五百七十八人学习属于轻量级入门参考。读者可从中学到利用网络会话类发起请求、解析行情响应数据并通过列表控件刷新买卖盘口信息同时掌握工作线程与主界面交互的常用做法。对想在VC 6.0中构建股票看盘工具或梳理五档行情获取流程的开发者这份资料提供了可直接对照的代码骨架。1. 五档行情还在用 VC 6.0这不是情怀是务实看到“VC 6.0”这个前缀很多人第一反应是“古董级开发环境”。但如果你真在券商、期货资管或量化私募的柜台系统周边做过开发就会明白市场上大量存量行情网关、柜台 SDK、DLL 接口仍然是二十年前那套技术栈尤其是沪深 Level-1 五档快照推送很多行情服务商的接入 SDK 头文件还带着#include afxwin.h的痕迹。用 VC 6.0 接收并显示沪深股市五档实时行情不是学不到新东西而是你在和真实生产环境对齐——它没有 .NET 的 GC 拖累、没有 Python 解释器开销一个 300KB 的 exe 就能长连行情服务器、解包五档数据、塞进 ListView 显示。这篇东西适合谁适合要用 VC 6.0 对接行情接口、做行情展示工具或给老交易系统补一个实时窗口的开发者。你不需要懂金融衍生品但需要懂 Socket、多线程和一点十六进制字节的耐心。做完这套方案你手上的成果是一个能连接行情服务器、订阅股票代码、接收五档快照、在 MFC 窗口里刷新显示买一至买五、卖一至卖五价、量、笔数的完整 Win32 程序。中间会遇到粘包、拆包、字节序、UI 刷新抖动这些坑全部有对应解法。下面直接开干。2. 行情接入那点事TCP 长连接推送才是五档行情的正确打开方式2.1 为什么不用 HTTP 轮询拿五档先回答一个绕不开的问题为什么不用 HTTP 接口每 3 秒拉一次五档免费行情 HTTP 接口确实存在也能返回五档快照但做实时行情展示有硬伤频率受限严重。五档快照有日内刷新频率限制公开 HTTP 接口对单 IP 的请求频率压得很死做秒级轮询大概率被限流或封禁。时延不可控。HTTP 短连接的握手开销在行情剧烈波动时是完全不可接受的买一卖一跳变时轮询周期内你看到的永远是上一帧。数据完整性差。HTTP 接口通常拿不到逐笔成交和十档盘口而行情服务器的 TCP 推送会按你自己的订阅清单连续推送快照和逐笔顺序有保证。所以股票行情接入特别是 Level-1、Level-2 这类需要严格时序和全量数据的场景行业通行做法是走行情服务器提供的 TCP 长连接。VC 6.0 里做这件事没有任何障碍——Winsock 在 Windows 95 时代就是成熟的东西了。2.2 五档快照协议先搞懂服务器推送的是什么东西沪深两所 Level-1 五档快照数据结构核心是20 个价格/量字段 行情时间 股票代码。深市 Level-1 目前是五档盘口沪市老协议也支持五档具体看接入服务商的协议版本字段结构差异主要在字段顺序和单位字段含义常见单位/精度股票代码6 位代码字符串如600519买一价~买五价五档买单价格价格乘以 10000黄马甲单位或乘以 1000看协议买一量~买五量五档买单挂单量单位通常为股也有协议用手卖一价~卖五价五档卖单价格同上卖一量~卖五量五档卖单挂单量同上最新价、昨收、开盘等快照辅助字段不一定每个协议都带看接入方行情时间HHMMSSmmm 格式整数或 BCD 编码这里最关键的一点是价格几乎不会用浮点数传输而是放大后的整数。浮点在传输和比较上有精度风险而整数价格既省带宽又能精确表达最小变动价位0.001 或 0.01取决于协议实现。我在实际接入时遇到的协议价格字段用int、short、甚至 BCD 都是常见的。所以解包的时候第一步就是要确认协议文档里的单位映射不然读出来价格多了两位小数核对行情软件时会怀疑人生。2.3 建立连接与订阅清单发一个订阅包过去常见行情接入协议流程是TCP 连接行情服务器的行情端口。发登录包有些公开行情服务商不需要登录直接订阅。发订阅请求指定股票代码列表。服务器开始推送快照数据客户端只负责收。心跳包必须按协议要求定时发送防止被服务器踢下线。因为我这里没有具体服务商的协议原文按行业常见做法设计一个极简演示协议作为我们学习 Socket 收发和协议解析的载体。假设协议封包格式typedef struct _DEMO_HEADER { WORD wMagic; // 魔数 0x5A01固定值 WORD wCmd; // 命令字1登录 2订阅 3心跳 0x8101行情推送 DWORD dwLen; // 后续包体长度小端 } DEMO_HEADER; // 总共 8 字节订阅请求的包体是股票代码列表每只股票 6 字节如 “600519”。#pragma pack(push, 1) typedef struct _DEMO_HEADER { WORD wMagic; WORD wCmd; DWORD dwLen; } DEMO_HEADER; #pragma pack(pop) BOOL SendSubscribePacket(SOCKET s, char szStocks[][6], int nCount) { // 包体nCount 只股票每只 6 字节 int nBodyLen nCount * 6; int nTotalLen sizeof(DEMO_HEADER) nBodyLen; char* pBuf new char[nTotalLen]; DEMO_HEADER* pHeader (DEMO_HEADER*)pBuf; pHeader-wMagic 0x015A; // 小端模式下 0x5A01 存入WORD pHeader-wCmd 2; // 命令字订阅 pHeader-dwLen nBodyLen; memcpy(pBuf sizeof(DEMO_HEADER), szStocks, nBodyLen); int nSent send(s, pBuf, nTotalLen, 0); delete[] pBuf; return (nSent nTotalLen); }这段代码的逻辑很简单拼一个 8 字节头加包体一次性send出去。注意wMagic为什么写成0x015A而不是0x5A01——因为 x86 是小端字节序WORD在内存中低位在前。如果协议文档写的是“包头魔数 0x5A01”那么内存布局就是0x01 0x5A代码里赋0x015A才对。很多初学者在这里翻车魔数读不出来后面全乱。send返回值必须等于nTotalLen。TCP 下send可能只发送了部分字节虽然小包 1.5KB在大多数情况下能一次发完严谨的写法要循环发送直到发完。这里为了演示简洁先不展开后面避坑章节会专门讲。2.4 行情服务器的地址和端口配置成可改的外部输入不要硬编码行情服务器的 IP 和端口。我见过很多内部工具的行情地址写死在源码里服务器切换后还得重新编译分发 exe非常被动。正确做法在配置文件.ini或程序目录下的config.ini里写[SERVER] IP... PORT...启动时用GetPrivateProfileString读进来。VC 6.0 对 INI 的支持很好读配置不用额外库这也是生产环境里仍然常见的做法// 读取配置文件VC6 下用 GetPrivateProfileString 即可 char szIp[64] {0}; int nPort 0; GetPrivateProfileString(SERVER, IP, 127.0.0.1, szIp, sizeof(szIp), .\\config.ini); nPort GetPrivateProfileInt(SERVER, PORT, 8080, .\\config.ini);这样换行情服务器地址时不用动代码。实盘环境的服务器 IP 和端口通常会同时给主备两套配置文件里把 IP 字段做成逗号分隔也好启动时优先连第一个失败尝试第二个这是值得一开始就做好的后期保障。3. VC 6.0 下把行情收下来Socket、缓冲区与拆包解包3.1 接收线程收数据不能堵住 UI行情推送是持续不断的数据量在开盘时非常大尤其订阅多只股票时每秒可能进来几百个包。如果把这些recv逻辑放在 UI 线程里界面必卡死。正确结构如下工作线程专门调recv循环收数把收到的裸字节追加到内存缓冲区。主线程UI用定时器或消息触发解析和显示。AfxBeginThread在 VC 6.0 的 MFC 程序里用起来很方便一个静态函数作为线程入口即可// 工作线程入口 UINT RecvThreadProc(LPVOID pParam) { CMyDlg* pDlg (CMyDlg*)pParam; char byBuf[4096]; int nRecv; while (!pDlg-m_bStop) { // 用 select 做超时避免 recv 永远阻塞导致线程无法退出 fd_set fdRead; FD_ZERO(fdRead); FD_SET(pDlg-m_sock, fdRead); timeval tv {1, 0}; // 1 秒超时 int nRet select(0, fdRead, NULL, NULL, tv); if (nRet 0) continue; // 超时或出错再循环 if (!FD_ISSET(pDlg-m_sock, fdRead)) continue; nRecv recv(pDlg-m_sock, byBuf, sizeof(byBuf), 0); if (nRecv 0) { pDlg-AppendBuffer(byBuf, nRecv); // 追加到拆包缓冲区 } else if (nRecv 0) { // 服务器关闭连接 break; } else { // recv 出错看 WSAGetLastError int nErr WSAGetLastError(); if (nErr ! WSAEWOULDBLOCK) break; } } return 0; }这段代码的要点是select加超时而不是让recv死等。原因是用recv阻塞时设置SO_RCVTIMEO在部分 Windows 版本上对套接字的生效情况不稳定而select是标准行为。1 秒超时让工作线程每秒钟至少醒来检查一次退出标志m_bStop程序退出时才不会卡死。recv一次收到 4096 字节但包含了多少个行情包可不一定。这就是 TCP 的流特性导致的粘包拆包问题。下一节处理。3.2 自定义拆包缓冲区粘包和半包一起解行情推送是 TCP 流不是消息边界。服务器可能一秒钟推送 10 个包合并发送也可能一个包分两次到达这就是粘包和半包。解决思路是维护一个字节数组做累积缓冲区每有新数据到达就检查能否解析出一个完整包头和完整包体能解析就取走不能解析就留下等待下一个recv。// 在对话框类中维护一个可变长缓冲区 class CMyDlg : public CDialog { public: // 累积缓冲 CByteArray m_byBuf; // 原始字节 // 处理一个完整包后的剩余数据起始位置 int m_nBufPos; }; void CMyDlg::AppendBuffer(char* pData, int nLen) { // 追加到 m_byBuf 尾部 int nOldSize m_byBuf.GetSize(); m_byBuf.SetSize(nOldSize nLen); memcpy(m_byBuf.GetData() nOldSize, pData, nLen); // 循环拆包 ParseBuffer(); } void CMyDlg::ParseBuffer() { int nSize m_byBuf.GetSize(); while (nSize - m_nBufPos 8) // 至少够一个包头 { DEMO_HEADER* pHdr (DEMO_HEADER*)(m_byBuf.GetData() m_nBufPos); // 魔数校验防止错位解析 if (pHdr-wMagic ! 0x015A) { // 找一个字节一个字节找魔数而不是简单丢弃 // 因为服务器可能在任何边界发包 m_nBufPos; continue; } int nTotal sizeof(DEMO_HEADER) pHdr-dwLen; if (nSize - m_nBufPos nTotal) { // 半包数据还没到齐留下等待后续 break; } // 一个完整包按命令字分发处理 if (pHdr-wCmd 0x8101) { HandleQuotePacket(m_nBufPos sizeof(DEMO_HEADER), pHdr-dwLen); } // 这个包处理完了游标前移 m_nBufPos nTotal; } // 把已处理完的数据从缓冲区移除防止无限膨胀 if (m_nBufPos 0) { int nRemain nSize - m_nBufPos; if (nRemain 0) { memmove(m_byBuf.GetData(), m_byBuf.GetData() m_nBufPos, nRemain); } m_byBuf.SetSize(nRemain); m_nBufPos 0; } }缓冲区的核心思路是m_nBufPos游标加memmove回收。性能上memmove有 O(n) 的成本但行情包通常都很小几百字节量级这个开销完全可以忽略。如果追求极致可以换环形缓冲区但在行情客户端这种量级下普通字节数组配合游标已经足够代码可读性还更好。拆包时魔数校验是重点。如果解析出错魔数对不上用逐字节推进的方式找下一个魔数位置。不能整包丢弃因为 TCP 流数据没有边界一丢就可能丢了之后所有包的对齐位置。3.3 解析五档快照字段一位一位掰开现在进入干货核心把五档快照包体内的字段解出来。假设快照包体结构如下常见协议风格字段为小端整数#pragma pack(push, 1) typedef struct _QUOTE_SNAPSHOT { char szCode[6]; // 股票代码 6 字节 int nTime; // 时间 HHMMSSmm int nPreClose; // 昨收 int nOpen; // 今开 int nLast; // 最新价 int nBidPrice[5]; // 买一~买五价 int nBidVolume[5]; // 买一~买五量 int nAskPrice[5]; // 卖一~卖五价 int nAskVolume[5]; // 卖一~卖五量 } QUOTE_SNAPSHOT; #pragma pack(pop) // 缓冲区中的快照包体起始地址 pBody长度为 nBodyLen void CMyDlg::HandleQuotePacket(int nOffset, DWORD nBodyLen) { if (nBodyLen sizeof(QUOTE_SNAPSHOT)) return; // 长度校验 QUOTE_SNAPSHOT* pQ (QUOTE_SNAPSHOT*)(m_byBuf.GetData() nOffset); // 价格字段是放大的整数假设单位是 1000即 0.001 元 double dBid1 (double)pQ-nBidPrice[0] / 1000.0; double dAsk1 (double)pQ-nAskPrice[0] / 1000.0; CString strInfo; strInfo.Format(600519 买一价%.3f 买一量%d 卖一价%.3f 卖一量%d, dBid1, pQ-nBidVolume[0], dAsk1, pQ-nAskVolume[0]); // 交给 UI 刷新函数这里只做简单示意 SetDlgItemText(IDC_STATIC_QUOTE, strInfo); }字段解析的注意点字节序int按小端读取时x86 直接强转即可。但服务器如果是大端传输部分行情服务商是就要手动ntohl。这一点必须以协议文档为准不同服务商差别很大。单位放大系数常见的有 1000 和 10000 两种需要仔细核对。如果读出来价格是 167800 而不是 167.80那就是少除了 100 或者多除了 10。这种错误在实盘里是致命的。量有的协议用的是股有的协议用的是手1手100股。显示的时候想清楚按哪个口径。BCD 编码有些老协议时间字段是 BCD 而不是普通 int比如 093025 用 BCD 存储需要按字节拆出 0x09、0x30、0x25 再各自转换。3.4 把解析到的五档数据放进 ListView接收线程拿到行情后下一步是在 MFC 的CListCtrl中显示。五档数据是典型的表格结构用 ListView 比用静态文本框直观得多。行结构建议这样组织买五 价格 数量 买四 价格 数量 买三 价格 数量 买二 价格 数量 买一 价格 数量 卖一 价格 数量 卖二 价格 数量 卖三 价格 数量 卖四 价格 数量 卖五 价格 数量买一在卖一上面买一到买五是买盘卖一到卖五是卖盘这是行情软件通用布局。每行 3 列档位、价格、数量。创建 ListView 并设置列然后填充数据// 初始化 ListView在 OnInitDialog 中调用一次 BOOL CMyDlg::InitQuoteList() { DWORD dwStyle m_listQuote.GetExtendedStyle(); m_listQuote.SetExtendedStyle(dwStyle | LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT); m_listQuote.InsertColumn(0, 档位, LVCFMT_LEFT, 60); m_listQuote.InsertColumn(1, 价格, LVCFMT_RIGHT, 100); m_listQuote.InsertColumn(2, 数量, LVCFMT_RIGHT, 100); return TRUE; }刷新时的核心技巧是复用行而不要删除重插。DeleteAllItems再InsertItem会导致列表闪烁和滚动位置丢失。正确做法是第一次把所有行插入之后每次刷新只调用SetItemText修改变化行。// 刷新函数nRows 固定为 10 void CMyDlg::FlushQuoteList(QUOTE_SNAPSHOT* pQ) { // 第一次创建行 if (m_bListInited FALSE) { for (int i 0; i 10; i) { CString strLevel; strLevel.Format(%d, i 1); m_listQuote.InsertItem(i, strLevel); } m_bListInited TRUE; } CString strTxt; // 买五到买一行 0~4 for (int i 0; i 5; i) { strTxt.Format(%.3f, (double)pQ-nBidPrice[4 - i] / 1000.0); m_listQuote.SetItemText(i, 1, strTxt); strTxt.Format(%d, pQ-nBidVolume[4 - i]); m_listQuote.SetItemText(i, 2, strTxt); } // 卖一到卖五行 5~9 for (int i 0; i 5; i) { strTxt.Format(%.3f, (double)pQ-nAskPrice[i] / 1000.0); m_listQuote.SetItemText(i 5, 1, strTxt); strTxt.Format(%d, pQ-nAskVolume[i]); m_listQuote.SetItemText(i 5, 2, strTxt); } }这里买盘为什么是4 - i因为pQ-nBidPrice[0]是买一但显示时买五在最上面所以第 0 行要显示的是买五即数组下标 4。这个顺序搞反了显示出来的盘口会让人以为买卖颠倒实盘里出这种错会非常尴尬。4. 显示刷新与线程同步别让界面拖垮行情接收4.1 UI 刷新节奏定时器驱动而不是来一个刷一个行情推送频率很高如果每收到一个快照就刷一次 ListViewUI 会被刷疯掉SetItemText本身消耗不少时间尤其是Format做了格式化之后。更严重的是一边后台线程在改数据一边主线程在读数据会读到半新半旧的状态。常见做法是双缓存加定时器工作线程解析后把最新快照写入一个QUOTE_SNAPSHOT副本临界区或简单原子变量保护。主线程用SetTimer(1, 200, NULL)每 200ms 读一次副本并刷 UI。200ms 是经验值行情五档灵敏度在 200ms 级别对肉眼完全够用还兼顾了系统负载。如果你做高频盯盘可以调 100ms但不建议再低因为 MFC 的SetTimer在 100ms 以下精度和稳定性都开始变差。// 在 OnInitDialog 中启动定时器 SetTimer(1, 200, NULL); // 定时器响应 void CMyDlg::OnTimer(UINT nIDEvent) { if (nIDEvent 1) { // 加锁取最新快照 CSingleLock lock(m_csQuote, TRUE); QUOTE_SNAPSHOT qCopy m_qLatest; lock.Unlock(); if (qCopy.nLast 0) { // 用最新价判断是否有效数据 FlushQuoteList(qCopy); } } CDialog::OnTimer(nIDEvent); } // 工作线程解析回调用它更新最新快照 void CMyDlg::UpdateLatestQuote(QUOTE_SNAPSHOT* pQ) { CSingleLock lock(m_csQuote, TRUE); m_qLatest *pQ; lock.Unlock(); }这 200ms 定时器的存在让接收线程和 UI 线程解耦了。接收线程只管收、解、存不碰 UI主线程只管读副本、显示不存在跨线程直接操作控件的问题。MFC 中跨线程直接调SetDlgItemText是危险的没准就崩溃或闪烁这里有经验之谈可循——用过 PostMessage 一族消息的都知道这套更稳妥。4.2 正确处理刷新闪烁WM_SETREDRAW 与 ListView 的配合ListView 每 200ms 刷新一次如果数据量大或数据抖动频繁列表会闪。单个SetItemText调用本身在刷新时会触发控件重绘十行连续修改会闪烁十次。解决方法是暂停控件重绘全部更新完再恢复void CMyDlg::FlushQuoteList(QUOTE_SNAPSHOT* pQ) { // 暂停绘制 m_listQuote.SendMessage(WM_SETREDRAW, FALSE, 0); // ... 原有的 SetItemText 逻辑 ... // 恢复绘制并强制立即重绘 m_listQuote.SendMessage(WM_SETREDRAW, TRUE, 0); m_listQuote.Invalidate(FALSE); }这个技巧用的人很多但要注意开启LVS_EX_DOUBLEBUFFER样式在 VC 6.0 时代不一定可用跟操作系统版本有关。而WM_SETREDRAW是标准消息所有 Win32 版本通用。每次刷新前后各发一次消息能明显减少闪烁代价是刷新期间控件不响应鼠标交互但 200ms 一次很快用户无感知。字体方面行情数字建议用等宽字体或Lucida Console不然价格从 12.30 变成 1234.5 时列宽会跳动。SetFont在OnInitDialog里设置一次用CFont成员变量存起来避免局部变量被销毁。老 MFC 程序里CFont对象的生命周期是个坑局部CFont font;赋值给控件后局部变量析构字体就失效了通常是“把CFont声明为对话框类的成员变量”来解决。5. 五档行情程序的常见坑与排查全是拿血换来的经验5.1 坑一订阅后收不到任何行情数据现象程序连上服务器发订阅包也没报错但 UI 上就是没有行情。原因八成是订阅包格式不对或者登录流程没走。第一种情况是魔数或命令字错误导致服务器直接丢弃第二种情况是某些服务商要求先登录认证后才能在订阅中间还有一个握手超时窗口不做这一步服务器根本不会推送。还有一个非常隐蔽的原因——订阅的股票代码带了交易所前缀或没有补齐大写比如“sh600519”和“600519”不是同一个东西不能想当然。解决用抓包工具比如 Wireshark对比自己能收到的字节流和协议文档里的示例报文检查魔数、长度、命令字、代码格式。如果没有现成报文就找行情服务商的技术支持要一份抓包样例这个比调代码快得多。我一般会在程序里加一个调试开关把send和recv的原始字节按十六进制写日志文件连收不到数据时直接看日志一目了然。5.2 坑二价格频繁对不上总有 10 倍或 100 倍误差现象能收到数据界面也刷新但价格和行情软件上显示的对不上要么大十倍要么大一百倍。原因价格放大系数不对。有的是int单位元有的是short单位分有的是除以 10000有的是除以 1000。最可气的是同一个协议里昨收、最新价、买一价这几个字段可能单位还不一样昨收用分、最新价用厘。查协议文档时必须要看清每个字段单独的精度说明不要假设“统一都是一个单位”。解决找到协议文档中的“字段说明”表格逐字段核对每个价格的单位把单位写成一个统一的宏或常量。然后用一只价格稳定、五档明显的权重股比如“600519”做校准对照行情软件上的数值反向验证。这个技巧很实用——如果读出来是 1678000行情软件显示 167.80那么放大系数就是 10000代码改成/ 10000.0即可。5.3 坑三程序跑一两个小时后面板卡死内存飙升现象刚启动一切正常跑久了 UI 越来越迟钝任务管理器里内存涨到几百兆。原因缓冲区没做裁剪。ParseBuffer里如果半包一直凑不齐m_byBuf只增不减或者服务器持续推送无法解析的包游标一直推进但memmove条件不成立缓冲就膨胀。另外如果HandleQuotePacket里做了new但没delete内存泄漏也会表现为这个现象。解决在AppendBuffer里加一个缓冲区上限阈值比如超过 1MB 就丢弃并重新初始化同时日志记录丢弃事件。内存泄漏用 VC 6.0 自带的 Debug 模式检测CRTDBG_LEAK_CHECK_ALWAYS可以抓出来。缓冲区裁剪逻辑放在ParseBuffer末尾执行完后立刻检查m_byBuf.GetSize()和m_nBufPos的关系分红线处理GetSize() - m_nBufPos超过上限就把剩余数据搬到开头直接SetSize缩容。5.4 坑四UI 每个几百毫秒卡一下行情多时更严重现象订阅股票数量多的时候界面每隔几秒明显卡顿ListVeiw 闪烁严重。原因SetItemText调用次数太多每次都会触发控件消息循环。10 行 × 2 列 20 次SetItemText如果订阅 50 只股票一次全刷要 1000 次调用这个开销在当前 200ms 周期内可能超过 100ms卡顿就这么来的。解决双重方案。第一用WM_SETREDRAW在批量刷新时禁绘前面已经讲过。第二数据量大的时候不要把所有股票都刷新在同一个 ListView 里考虑多 ListView 或只显示当前选中股票的五档。真正的生产程序里自选股列表和五档详情是两个独立的列表五档详情只刷当前选中的那一只自选股列表只刷价格变化。这是很成熟的做法不要在一个控件里塞太多东西。5.5 坑五编译器玄学——VC 6.0 的 char 符号性和指针优化现象同样的代码在 VC 6.0 下编译运行价格偶尔错乱换其他编译器就没问题或者#pragma pack指定的结构体大小不对。原因VC 6.0 默认char是signed char当你用char*去遍历字节流并做位运算时如果字节最高位是 1符号扩展会把高位带入int导致数值异常。另一个是 VC 6.0 对pack的支持在所有场景下不是严格一致的涉及double或__int64字段时可能出现对齐和文档不一致的情况。解决解析字节流时一律用unsigned char*或者把char强转为BYTE再取位。结构体定义里禁止double字段行情价格全用整型解析时才转浮点。写完结构体后加一个ASSERT(sizeof(QUOTE_SNAPSHOT) 期望值);在调试模式下验证对齐这一步能救你无数次。6. 没有行情服务器时怎么验证做一个数据源模拟器就够用了最后这个技巧值得实操在接入真实行情服务器之前先做一个本地模拟推送器。这个模拟器的作用是模拟服务端行为持续向客户端发五档快照包让你的客户端逻辑不需要等待真实行情就能验证。做法很简单用ServerSocket在本地监听一个端口客户端连接后定时向客户端发送构造好的五档包。模拟器只做一件事每个包填入假数据按与真实协议相同的封包格式发出来。// 模拟器核心逻辑伪代码放控制台程序里跑 SOCKET sServer socket(AF_INET, SOCK_STREAM, 0); bind(sServer, ...); // 绑定 127.0.0.1:8080 listen(sServer, 1); SOCKET sClient accept(sServer, NULL, NULL); while (TRUE) { // 构造一个假的五档数据价格随心跳递增 QUOTE_SNAPSHOT q; memset(q, 0, sizeof(q)); memcpy(q.szCode, 600519, 6); static int nTick 0; nTick; q.nLast 167000 nTick * 10; // 最新价缓慢上涨 for (int i 0; i 5; i) { q.nBidPrice[i] q.nLast - (i 1) * 20; // 买盘低挂 q.nAskPrice[i] q.nLast (i 1) * 20; // 卖盘高挂 q.nBidVolume[i] 100 * (i 1); q.nAskVolume[i] 100 * (i 1); } // 封包8 字节头 包体 DEMO_HEADER hdr; hdr.wMagic 0x015A; hdr.wCmd 0x8101; hdr.dwLen sizeof(QUOTE_SNAPSHOT); send(sClient, (char*)hdr, sizeof(hdr), 0); send(sClient, (char*)q, sizeof(q), 0); Sleep(500); // 每 500ms 推一次 }有了模拟器后验证就变得快起来。启动模拟器再启动客户端可以看到界面按预期刷新。这个验证思路还有一个好处你可以用模拟器测试异常情况。比如连续发送两个完整包模拟粘包把一个包拆成两次send模拟半包故意发一个错误魔数看客户端是否卡死或者断线重连确认客户端能恢复。这些测试在真实服务器上做会很麻烦模拟器里全都能快速复现。当初接手一个行情展示模块时真实行情服务器只在交易时段开放非交易时间代码改完根本没法验证。后来用这个方式把整个客户端逻辑全部验证完开盘后接真实数据一次过。在这里把话说完整五档行情这个方向难的不是代码逻辑而是封包细节——字段单位、字节序、对齐、粘包拆包每一项都值得通过模拟器和日志去验证。你在调试日志里花的每一分钟最后都会在接真实行情时省回来。这套经验我用了很久希望帮到你。本文还有配套的精品资源点击获取
返回列表