简介面向VC6.0开发者的沪深股市五档行情获取与显示实例压缩包内含完整MFC工程源码可直接编译运行配套的工程配置和界面资源也让环境搭建更为省心。项目演示了从网络会话初始化到界面刷新的完整链路通过CInternetSession建立HTTP连接向行情接口发送查询请求并接收响应再对XML或JSON格式的返回结果做字段映射界面侧利用对话框和列表控件展示股票代码、最新价、涨跌幅以及买一至卖三六档报价同时借助CWinThread子线程处理网络请求避免阻塞用户界面兼顾了定时刷新与异常恢复的基本策略。资源共18个文件以h/cpp源文件为主涵盖网络请求封装、对话框逻辑与主程序框架配合aps、dsw、dsp、rc等MFC工程与界面资源文件另有ico图标和txt说明压缩包仅33KB。已有578人学习适合具备基础网络编程知识、希望用老技术栈快速实现行情客户端的开发者参考也可作为课程设计或股票软件入门练手项目。1. 五档行情与 VC 6.0这套老组合为什么还没退场第一次听到“用 VC 6.0 接沪深五档行情”这个需求我也愣了一下。但接过的单子多了就发现券商周边工具、老柜台系统的行情展示模块、还有那些跑在 Windows XP 工控机上没法升级的存量程序里这套组合一直没退场。五档行情指盘口上的买一至买五、卖一至卖五的价与量属于 Level-1 快照数据拿来做盯盘、涨幅榜和价格告警完全够用但它不包含逐笔委托明细和 Level-2 不是一回事。下面按我实际接行情源的流程来讲先梳理行情协议和线程模型再给 VC6 能直接编译的 socket 与界面代码最后列几个踩过的坑。适合谁维护老代码的、或者被要求“就用 VC6 写个小行情工具”的开发者。2. 先搞清楚五档行情的网络模型推送、字节序与线程选型2.1 五档行情是 Level-1 快照不是逐笔流水做这个标题之前先得把“五档行情”四个字的数据形态说清楚。沪深股市的五档行情是交易所下发的 Level-1 快照行情源会按固定周期常见是 1 到 3 秒一帧或价格变动触发的方式把一只股票的买一至买五、卖一至卖五的价和量推给客户端。它的本质是“快照”不是你每次看到的价格跳动的流水记录。两者的区别很实际快照丢了中间过程但拿来刷新界面、计算涨跌幅、做简单策略完全够用想要逐笔成交明细那是 Level-2 的活协议复杂度、授权费用和带宽消耗都不是一个量级。所以做这个项目时心里要有一个明确的边界核心逻辑就是“反复接收同一个股票池的快照按股票代码索引更新界面上的档位价格和手数”。不要在设计阶段就想着去拼逐笔委托、还原盘口变化过程那会把一个本来一周能交付的小工具拖成无底洞。我一般会在需求确认时直接问行情源提供方快照是全量推送还是只推变化档位。这个问题的答案直接决定解析逻辑怎么写。全量推送最简单收到一帧就用整包覆盖本地缓存只推变化档位则要自己维护一个“上一帧”做合并坑多不少。2.2 设计消息头包长、字节序与结构体对齐先解决两个最基础也最容易出错的问题TCP 粘包和结构体对齐。行情源很少给你一个“一整包就是一条行情”的干净 TCP 流——TCP 是字节流没有消息边界。所以能用协议普遍会带消息头把包长放进去。下面这张表是我自己常用来和行情源对字段的结构你可以拿去做原型验证真实接入时按对方文档替换对应字段即可。字节偏移字段名类型说明0headunsigned short消息头魔数例如 0xAA55用于失步检测2lenunsigned short包体总长度含头4timeunsigned int行情时间戳格式 HHMMSSmmm8codechar[8]股票代码例如 60000016priceunsigned int最新价按最小变动价位缩放后的整数20preCloseunsigned int昨收价24bid[5]unsigned int买一至买五价格44bidVol[5]unsigned int买一至买五量64ask[5]unsigned int卖一至卖五价格84askVol[5]unsigned int卖一至卖五量这里有两个参数别忽略。第一是价格缩放。沪深 A 股最小变动价位是 0.01 元行情源为了避开浮点误差普遍把价格放大了 100 倍传整数收到后自己再除以 100 显示。第二是字节序。x86 是 little-endian网络传输常按 big-endian但不少行情商为了性能会直接按 x86 的小端传。正确做法是先确认一个已知包打印前 4 字节 hexdump看 0xAA55 到底是55 AA还是AA 55。这个搞反了后面所有价格都会变成天文数字。结构体对齐是一个藏在代码里的暗雷。写解析结构体时不加#pragma pack(push, 1)编译器会在char code[8]后面按 4 字节对齐插入填充字节后续所有字段全部错位。我见过不止一次“价格对、代码错、量对、买卖方向反”的诡异现象最后定位下来全是对齐问题。定义结构体时把#pragma pack(push, 1)和#pragma pack(pop)包好错位问题就从根上没了。2.3 线程模型为什么是“阻塞 socket 工作线程 PostMessage”线程模型是 VC6 做行情显示绕不开的决定。常见做法有两个CAsyncSocket 的消息驱动或者阻塞 socket 加独立工作线程。我用的是后者。CAsyncSocket 底层依赖WSAAsyncSelect把网络事件转成窗口消息在高频行情推送下FD_READ 消息会在 UI 线程排队一次来几十只股票的行情时窗口消息可能积压界面拖动都会卡。而且 CAsyncSocket 把连接状态、重连的细节包在黑匣子里出了错定位困难。阻塞 socket 加一个收包线程代码虽然“原始”但每个 socket 错误都能直接看到返回值超时、断线、重连全部自己控制。工作线程里的核心是一个循环select判断可读、recv收数据、缓存拼接、按消息头取出完整包、PostMessage通知界面。注意收包线程不要直接调用 ListView 的SetItemText跨线程操作控件有时能跑通但会造成界面刷新偶发崩溃。正确姿势是把解析好的快照数据通过PostMessage投递到窗口过程在 UI 线程里做显示更新。这套模型在 VC6 下的另一个好处是断线重连逻辑可以集中在工作线程里做用 1 秒、2 秒、4 秒的指数退避重试不会卡住界面。3. VC 6.0 跑通五档行情从 connect 到 ListView3.1 先跑最小 socket连接、收包与 hexdump我拿到新行情源的第一件事不是写界面而是写一个最小 Win32 控制台程序把 socket 连接和收包跑通打印 hexdump。MFC 调试跨界太麻烦控制台里能确认“连接成功、能收到数据、能按包长拆出完整包”再进入界面部分就踏实了。// 最小 socket 验证VC6 环境Winsock 1.1 足够 #include winsock2.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsa; WSAStartup(MAKEWORD(1, 1), wsa); // 请求 Winsock 1.1VC6 下兼容性最好 SOCKET s socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN addr {0}; addr.sin_family AF_INET; addr.sin_port htons(5000); // 示例端口换成行情源文档里的实际值 addr.sin_addr.s_addr inet_addr(127.0.0.1); // 示例 IP真实环境写行情服务器地址 if (SOCKET_ERROR connect(s, (SOCKADDR*)addr, sizeof(addr))) { int err WSAGetLastError(); // 10060 超时10061 拒绝10049 地址无效 printf(connect failed: %d\n, err); return 1; } char buf[2048]; int n recv(s, buf, sizeof(buf), 0); if (n 0) { for (int i 0; i n i 64; i) // 只打前 64 字节避免刷屏 printf(%02X , (unsigned char)buf[i]); printf(\n); } closesocket(s); WSACleanup(); return 0; }这段代码的关键参数是MAKEWORD(1, 1)和htons(5000)。VC6 年代WSAStartup请求 1.1 版就够用不要在这里写MAKEWORD(2, 2)老系统上没必要引入 2.2 的依赖虽然大多数现代 Windows 都支持。端口和 IP 是示例务必替换成行情源提供的手册里的真实值。连接失败时WSAGetLastError()的三个典型值要背下来10060 是连接超时说明防火墙或地址不对10061 是拒绝说明端口没人监听或 IP 错10049 是地址无效检查inet_addr是否返回了INADDR_NONE。连不通时先对照这三个值定位别急着怀疑协议。收到数据后打印 hexdump 的意义在于确认三件事数据真的来了、长度不为 0、开头两个字节能和协议文档里的消息头对上。如果连这一步都过不去后面解析全是白做。3.2 登录鉴权与心跳保活间隔、超时与 select 判活行情服务器一般不会让你裸连收数据多数协议要求先发一个登录包里面有用户名、密码、协议版本号。登录包的细节每家不同但流程一致发登录包、等登录应答包、应答成功后进入行情推送阶段。// 登录包示例字段顺序以行情源文档为准 // 这里演示的是“长度 类型 用户名 密码”的常见布局 char login[128] {0}; unsigned short len 24; unsigned char type 0x01; // 0x01 表示登录请求 memcpy(login, len, 2); login[2] type; strcpy(login 3, user01); // 用户名按实际账号写 strcpy(login 19, pwd123); // 密码生产环境别用明文 int sent send(s, login, len, 0); if (sent SOCKET_ERROR) { int err WSAGetLastError(); // 10054 表示对端已重置连接通常是没登录先发币了 printf(send login failed: %d\n, err); }登录应答通常是一个固定长度的包里面可能有心跳协商字段。比如有些源要求客户端每 3 秒发一个心跳字节有的要求 5 秒。我的做法是把行情源限定的心跳超时除以 2作为本地发送间隔。比如服务端 3 秒判活我就 1.5 秒发一次心跳留足网络抖动的余量。// 心跳发送单独定时器或独立小线程不要和 recv 放在同一个阻塞循环里 static char hb 0x00; // 心跳内容按协议文档有些源是固定 0x00有些是时间戳 SetTimer(g_hWnd, 1001, 1500, NULL); // 1.5 秒触发一次对应服务端 3 秒超时的一半 // OnTimer 里 case 1001: send(s, hb, 1, 0); break;判活不能只靠recv因为行情清淡时段快照间隔可能大于你的超时窗口recv阻塞在那里并不代表连接死了。正确做法是用select设置 200ms 超时做“可读性判断”能读到数据就调用recv超时无数据视为正常返回错误或 0 字节才判定断线。fd_set rd; timeval tv {0, 200000}; // 200ms单位是秒 微秒 FD_ZERO(rd); FD_SET(s, rd); int ret select(0, rd, NULL, NULL, tv); if (ret 0) { int n recv(s, buf, sizeof(buf), 0); // 可读才 recv不会无限阻塞 if (n 0) { /* 断线进入重连逻辑 */ } } else if (ret 0) { /* 超时连接仍正常继续循环 */ } else { int err WSAGetLastError(); // 10004 是被 WSAInterrupt 打断可忽略重试 }注意select的第一个参数在 Windows 下会被忽略填 0 就行不用像 Linux 那样写s 1。timeval的两个字段分别是秒和微秒{0, 200000}是 200ms。这个值不是随便定的太短会增加空转 CPU 占用太长会让断线发现变慢。我一般控制在 100ms 到 500ms 之间行情源对断线重连的要求没那么苛刻时直接取 200ms。3.3 五档快照拆包解析从 TCP 字节流到 ListView 刷新行情推送一上来TCP 粘包和半包是必然的。一次recv里可能包含多条完整行情也可能只包含半条。处理粘包的通用做法是维护一个接收缓冲区循环做“取包头 - 计算包长 - 判断缓冲区是否凑够一个完整包 - 取出 - 移动缓冲区残余”这个过程通常叫拆包。// 从缓冲区里尝试取出一个完整包 // 返回值包长度0 表示取到了0 表示数据不够-1 表示失步 int TryGetPacket(char* buf, int bufLen) { if (bufLen 4) return 0; // 连消息头都不够继续收 unsigned short head *(unsigned short*)buf; if (head ! 0xAA55) return -1; // 头不对MACD 都别算了直接失步 unsigned short pktLen *(unsigned short*)(buf 2); if (pktLen 4 || pktLen 1024) return -1; // 包长越界协议帧损坏 return (bufLen pktLen) ? pktLen : 0; // 凑够就返回包长否则等下一次 recv }拆包回调拿到完整包后按结构体直接映射。前面提过的#pragma pack(push, 1)在这里真正发挥作用否则code[8]后面的偏移全错取出来的买一价其实是卖一价。#pragma pack(push, 1) // 1 字节对齐禁止编译器插填充字节 typedef struct tagL1Quote { unsigned short head; // 0xAA55 unsigned short len; // 包长 unsigned int time; // HHMMSSmmm char code[8]; // 股票代码 unsigned int price; // 最新价真实价 x100 unsigned int preClose; // 昨收 x100 unsigned int bid[5]; // 买一至买五价 unsigned int bidVol[5]; // 买一至买五量 unsigned int ask[5]; // 卖一至卖五价 unsigned int askVol[5]; // 卖一至卖五量 } L1Quote; #pragma pack(pop)工作线程解析完一包不做界面操作直接把结构体拷贝一份通过PostMessage丢给主窗口。注意PostMessage的LPARAM传的是指针要new一块堆内存窗口过程用完后记得delete否则内存泄漏。这是 VC6 老程序员之间的约定俗成。// 工作线程里每解析出一个完整 L1Quote 就往 UI 投递一份 L1Quote* pQuote new L1Quote; memcpy(pQuote, parsed, sizeof(L1Quote)); PostMessage(g_hWnd, WM_USER 100, 0, (LPARAM)pQuote); // 主窗口消息映射里 LRESULT OnQuote(WPARAM wParam, LPARAM lParam) { L1Quote* q (L1Quote*)lParam; int row FindRowByCode(q-code); // 按代码找到列表行号找不到就加行 if (row 0) { char tmp[32]; wsprintf(tmp, %d.%02d, q-bid[0] / 100, q-bid[0] % 100); m_list.SetItemText(row, 1, tmp); // 更新买一价下标按你的列顺序定 // 其余档位、量、最新价同理只更新变化的单元格 } delete q; return 0; }界面上把 ListView 设成LVS_REPORT风格每一行是一只股票每一列是一个字段代码、最新价、买一价、买一量、卖一价、卖一量……按顺序排即可。刷新时不要DeleteAllItems再InsertItem重建整行那会明显闪烁。只对价格或量变化的单元格调用SetItemText刷新频率高时观感差别非常大。4. 五档行情接入的 5 个常见问题排查现象、定位与对策4.1 VC 6.0 SP6 在 Win10 上安装假死现象双击 VC6 安装程序后进度条走一会儿就卡住不动任务管理器里能看到一个_INS5576_MP.EXE进程挂着点界面没反应强制结束整个安装程序又提示安装未完成。 原因VC6 的 Acme Setup 安装器是上个世纪的东西和 Win10 的进程、文件权限机制不兼容。官方 SP6 补丁解决的是编译器本身对新系统的适配安装器这个壳它管不了。 解决别跟它硬扛。安装卡住时用任务管理器只结束_INS5576_MP.EXEAcme Setup 的机制是这个子进程退掉后安装器会继续往下走如果还不行就在 Win7 或 XP 虚拟机里装好 VC6 SP6装完把整个安装目录拷出来到 Win10 上用。注意装完必须打 SP6否则在 Win10 上编译出来的程序调试时符号表经常对不上点击“运行”直接报奇奇怪怪的地址错误。4.2 程序跑几分钟就被行情源踢下线现象程序运行 3 到 5 分钟后行情推送中断工作线程日志显示recv返回 0随后进入重连循环换个网络环境复现稳定。 原因登录时协商的心跳超时到了客户端没按时发心跳。常见诱因是心跳发送代码被并进了recv的阻塞循环——行情清淡时recv一直阻塞在那里心跳根本没机会发出去。 解决把心跳发送拆到独立定时器或独立小线程里心跳间隔设为服务端超时的一半或更小。比如服务端 3 秒判活客户端 1.5 秒发一次。另外排查一下登录包里是否提前协商了心跳周期有些协议允许客户端请求更长的超时但必须写进登录包不写就按默认值。4.3 现代 C 写好的解析代码复制进 VC6 编译一片红现象从新工程里拷贝来一个解析类auto、lambda、std::thread、std::to_string全部报错报错信息还没头没尾看起来像函数不匹配。 原因VC6 的编译器停在 1998 年的 C98 早期实现这些在今天看来稀松平常的语法全是 C11 甚至 C17 的东西。VC6 不是“不擅长”这些特性而是根本不认识。 解决把解析层按 C 风格重写结构体加数组加普通函数不要用 STL 容器不要用模板线程用CreateThread数字转字符串用wsprintf。一开始就别写std::string直接char[32]。这样写出来的代码在 VC6 下编译干净以后想迁到新编译器也容易。4.4 五档中文乱码行情源给的是 GBK现象界面上的股票名称或拼音字段显示成一串乱码类似“鍏”这种完全不认识的汉字但价格和量都对。 原因行情源按 GBK/GB2312 编码传中文字段VC6 工程默认代码页也是 GBK但 MFC 控件在 Unicode 字符集下直接赋值两边没对齐就乱码。 解决收到字节后按CP_ACP用MultiByteToWideChar转成宽字符再喂给控件。在 VC6 下最简单的是把工程字符集设成多字节非 Unicode直接塞const char*给控件。这里不要偷懒宁可统一封装一个ToWide函数不要把SetWindowText调用散得到处都是后续换 Unicode 工程时会省大力气。4.5 涨跌停和停牌时五档“变稀”不是 bug现象一字跌停的股票卖一那侧全是 0或者停牌股整包价格字段全为 0新人开发会当成解析错误花一整天对协议文档。 原因Level-1 五档行情在涨跌停时只推有效价位对手盘的档位用 0 填充停牌股票则可能干脆不发快照。这不是协议损坏是市场状态的正常表达。 解决解析时增加一个行情状态字段的判断或者用成交量是否为 0 做兜底。UI 显示上价格为 0 的档位显示“--”不要显示 0.00否则第一眼会以为价格崩了。计算涨跌幅时要先判断昨收是否为 0避免除零崩溃。行情源如果提供了状态字优先用状态字没提供就用“五档全部为 0 且成交量固定不变”做启发式判断。5. 把五档刷新做到 500ms 观感的三个技巧局部刷新、粘包边界与本地回放行情工具的“快”本质上是观感问题。数据链路再通畅界面每隔几秒整块闪烁一下用户就觉得卡。三个技巧能把观感做到 500ms 级别。第一个技巧是 ListView 局部刷新加双缓冲。前面提过不要DeleteAllItems重建列表这里再进一步连整行都别刷只刷变化的单元格。价格没动的档位不碰它操作系统就不会重绘那一格。如果有闪烁残留处理WM_ERASEBKGND直接返回TRUE把擦除背景的操作去掉配合ListView_SetItemState禁用选中高亮的闪烁。方法简单点击就能看到明显改善。第二个技巧是把粘包拆到彻底的“剩余缓冲区”这是最容易留隐患的地方。有些人处理完一个完整包后直接把缓冲区memcpy到开头但忘了把“剩余未消费字节”一并搬走导致丢包错位。正确的边界做法是维护一个readPos每次消费完把未消费部分移动到底部int remain bufLen - consumed; // 剩余未消费的字节 memmove(buf, buf consumed, remain); // 搬到头部下次 recv 接着写 recv(s, buf remain, BUF_SIZE - remain, 0);memmove而不是memcpy的原因是两个区间可能重叠memcpy在这种场景是未定义行为老 VC6 编译器不会提醒你但运行到特定数据长度时会莫名丢字节。第三个技巧是做本地回放器验证。真实行情源联调是有窗口期的收盘后就没了。我习惯自己写一个回放工具把一天的行情快照存成文件工具用 socket 按抓包时的节奏把包重发出去客户端把回放地址当行情服务器连。这样解析代码的验证可以随时做测边界情况也不用等开盘。验证清单一般是三条第一每只股票的五档价和量是否和源数据一致第二把客户端显示的时间戳和回放包里的时间戳逐帧比对第三故意把回放速度调成 5 倍看会不会卡界面或者内存涨。把这个回放器加进去之后接入成本明显下降至少不用半夜守在电脑前等行情源出包。我自己当年第一版就是没做回放器把recv直接放在 UI 线程里开盘时拖动窗口直接卡成白板重启了三次才意识到问题。后来给自己立了个规矩任何行情接入先本地回放验证三天再做真源联调这个小习惯帮我省了不少半夜查日志的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?