ARTICLE DETAIL

资讯详情

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

VC6老工程实战:MFC连接MySQL与cURL调用Google接口全解析

VC6老工程实战:MFC连接MySQL与cURL调用Google接口全解析 简介面向 VC 开发者的 MySQL 接入与外网服务调用实践示例聚焦 Windows 环境下使用 Visual C 连接 MySQL、执行 SQL 查询以及借助 HTTP 调用 Google 开放接口时常见的环境配置、库链接和错误处理问题。压缩包共 41 个文件约 3.89MB除 h/cpp 工程源码外还包含 MFC 界面相关的资源文件ico/rc/bmp、可执行文件与动态库/静态库libmySQL.dll/lib以及 obj/pdb/ilk 等调试中间文件源码、配置与编译产物齐全适合直接打开工程对照学习或重新编译验证。目前已有 312 人学习下载适合具备一定 C 基础、正在尝试将 MySQL 与外部 Web API 整合到桌面应用的开发者对于理解 Windows 下桌面端如何与数据库、外部网络服务协同工作也有直接帮助。读者可从项目中看到 mysql_init、mysql_real_connect、mysql_store_result 等 API 的完整调用流程也能从 Google Geocoding 接口示例中理解 curl_easy_init、curl_easy_setopt、curl_easy_perform 的 HTTPS 请求写法MFC 界面与后台数据库操作分离的工程结构还能帮助减少在 libmysqlclient、cURL 配置及编译链接环节的重复排错时间。1. 这份 VC6 工程包解决的是两件最磨人的事老编译器调 MySQL、老代码连 Google先说结论Google.zip不是一份能双击就跑的成品软件而是一份 VC 6.0 时代的 MFC 单文档工程源码包。拆开Google.dsw你能看到GoogleView.cpp、GoogleDoc.cpp、MainFrm.cpp这套标准 MFC 框架同时也躺着libmySQL.lib、libmySQL.dll和googleearth.h——它把两件很多初学者卡到怀疑人生的需求做到了一起VC 里通过官方 C API 调 MySQL 数据库以及用 HTTP 请求去访问 Google 的开放接口。适合接手老项目的人、拿它做课程设计参考的学生以及想把 VC6 老代码迁移到新工程的从业者。我也是在维护一个 2008 年的遗留系统时翻到这份源码发现里面的连接方式和踩坑点放到今天依然能用。接下来按我拆包时的顺序从工程结构、数据库调用、外网请求到雷区逐层讲透。2. 先拆工程从 Google.dsw 到 libmySQL.lib一条完整的调用链2.1 MFC 单文档骨架里文件是怎么各司其职的打开Google.dsw这个工作区文件你会看到这是一个典型的 MFC AppWizard 生成单文档工程运行起来是一个带主窗口的 Windows 桌面程序。GoogleView.cpp和GoogleDoc.cpp构成视图-文档对GoogleDoc负责数据存取GoogleView负责把查到的数据库内容画在窗口上MainFrm.cpp是主框架窗口负责菜单和工具栏。这套结构与业务逻辑本身无关但决定了你编译后拿到的是一个带 GUI 的.exe而不是控制台程序——调试时要留意printf的内容不会直接出现在黑窗口里得用 OutputDebugString 或写日志文件来看。googleearth.h这个文件值得单独说。它封装的是与 Google Earth / Google Maps 类服务通信的接口声明配合googleearth.cpp一起使用。本质上是把 HTTP 请求、URL 拼装、响应解析这几件事包成函数方便在 MFC 的视图类里直接调用。Google.clw、.ncb、.opt是 VC6 的辅助文件分别存类向导信息、智能感知缓存和工程选项新版 Visual Studio 打开时通常会忽略它们不用管。2.2 链接库的选择逻辑为什么用 libmysqlclient 而不是 Connector/C工程里出现的是libmySQL.lib和libmySQL.dll这是 MySQL 官方 C API 客户端库的经典形态。很多人会问为什么不用更新的 Connector/C因为在 VC6 编译器下新版 Connector/C 对 C11 特性有依赖VC6 对模板和标准库的支持跟不上老工程选 C API 是兼容性最优解。C API 是纯 C 接口头文件mysql.h里全是函数声明和结构体VC6 编译毫无压力。这里的调用链要理清楚你的工程代码#include mysql.h编译时只认头文件链接时依赖libmySQL.lib这个导入库它不包含真正的实现只是告诉你函数符号在哪个 DLL 里运行的时候才真正加载libmySQL.dll。所以三个东西一个都不能少少了任何一个报错阶段完全不同——少了头文件是编译错 C1083少了 lib 是链接错 LNK2001少了 dll 是运行错“找不到 libmySQL.dll”。我一般会建议把这三个东西放同一个目录然后在 VC6 的 Tools - Options - Directories 里分别配置 Include files 和 Library files 路径省得每次新建工程都要找。2.3 资源文件与图标不影响逻辑但影响你替换成自己项目Google.rc2、Toolbar.bmp、GoogleDoc.ico、Google.ico这些是工程资源。Google.rc是资源脚本的文本入口resource.h是资源 ID 的头文件VC6 的资源编辑器会同步维护这两个文件。Google.aps是资源文件的缓存版本如果手改.rc后编译出现资源相关诡异报错删掉.aps让它重新生成是常见解法。这些资源在此工程里只影响界面外观不影响数据库和网络逻辑。但如果你想把这套代码改成自己的项目名要同步改的是.dsp、.dsw、.rc、resource.h里所有Google字样漏改.rc会导致资源编译报错。文件作用缺失时的报错mysql.h编译期函数声明fatal error C1083: 无法打开包含文件libmySQL.lib链接期导入库error LNK2001: 无法解析的外部符号libmySQL.dll运行期实现找不到 libmySQL.dll无法继续执行代码googleearth.hGoogle 接口封装声明工程内源码无法编译3. 把 VC 和 MySQL 接起来从 mysql_init 到事务控制的完整代码3.1 最简可用的连接与查询代码逐行讲透在 VC6 里写 MySQL 调用核心就是摘要里那段代码的完整版。我把工程里实际能通过编译的版本写出来并补上字符集和资源释放的处理#include mysql.h #include stdio.h #include string.h #pragma comment(lib, libmySQL.lib) int main() { MYSQL* conn; MYSQL_RES* res; MYSQL_ROW row; const char* server localhost; const char* user root; const char* password 123456; const char* database test_db; conn mysql_init(NULL); if (conn NULL) { fprintf(stderr, mysql_init failed\n); return -1; } if (mysql_real_connect(conn, server, user, password, database, 0, NULL, 0) NULL) { fprintf(stderr, connect error: %s\n, mysql_error(conn)); mysql_close(conn); return -1; } mysql_set_character_set(conn, utf8); if (mysql_query(conn, SELECT id, name FROM users WHERE age 18) ! 0) { fprintf(stderr, query error: %s\n, mysql_error(conn)); mysql_close(conn); return -1; } res mysql_store_result(conn); if (res NULL) { fprintf(stderr, store result error: %s\n, mysql_error(conn)); mysql_close(conn); return -1; } unsigned int num_fields mysql_num_fields(res); while ((row mysql_fetch_row(res)) ! NULL) { for (unsigned int i 0; i num_fields; i) { printf(%s\t, row[i] ? row[i] : NULL); } printf(\n); } mysql_free_result(res); mysql_close(conn); return 0; }第 3 行用#pragma comment(lib, ...)直接指定链接库省去在工程设置里手动添加依赖的步骤。这段代码里最容易翻车的是mysql_real_connect的参数第五个参数是数据库名如果填 NULL 表示不指定默认库之后每次查询都要写全库名.表名第六个参数是端口号填 0 表示用默认 3306第七个参数是 socket 路径Windows 下填 NULL第八个参数是连接标志填 0 表示不加任何特殊行为。mysql_store_result把整个结果集拉到客户端内存适合数据量小的场景数据量大时应该用mysql_use_result一行一行取但用那个函数时不能在取完前执行其他查询。mysql_set_character_set(conn, utf8)这行是我强烈建议保留的没有它你插入的中文在库里会变成乱码查出来也是乱码现象极其诡异。这行必须在连接成功后、执行任何查询前调用。还有一点mysql_fetch_row返回的row[i]类型是char*如果是 NULL 字段它返回的是 NULL 而不是字符串 NULL所以代码里做了三目判断否则printf直接崩溃。3.2 查询参数拼接与 SQL 注入老代码最常见的隐患工程里实际使用时查询条件往往来自界面输入框于是有人直接这么写char sql[512]; sprintf(sql, SELECT * FROM users WHERE name %s, input_name); mysql_query(conn, sql);这会存在注入问题英文单引号或--注释符能直接改变 SQL 语义。VC6 下面没有现成的mysql_real_escape_string的 C 封装但 C API 提供了正确做法是先转义再拼char escaped[256]; mysql_real_escape_string(conn, escaped, input_name, strlen(input_name)); sprintf(sql, SELECT * FROM users WHERE name %s, escaped);mysql_real_escape_string的第二个参数是输出缓冲区长度必须至少为输入长度的两倍加一因为每个特殊字符都要转成两个字节。这个函数是连接相关的因为它按当前连接字符集处理转义所以必须在连接成功后调用。我看到很多老代码没做这一步后来被注入拖库的新闻看多了自己写的时候才老实了。3.3 事务控制与多语句mysql_autocommit 的正确用法如果你要执行的是转账、批量更新这类多语句操作就必须用事务。C API 的方式很直观mysql_query(conn, START TRANSACTION); // 执行多条 UPDATE if (mysql_query(conn, UPDATE account SET balance balance - 100 WHERE id 1) ! 0) { mysql_query(conn, ROLLBACK); fprintf(stderr, rollback, error: %s\n, mysql_error(conn)); return -1; } if (mysql_query(conn, UPDATE account SET balance balance 100 WHERE id 2) ! 0) { mysql_query(conn, ROLLBACK); return -1; } mysql_query(conn, COMMIT);也可以用mysql_autocommit(conn, 0)关掉自动提交然后手动mysql_commit(conn)和mysql_rollback(conn)。注意mysql_commit成功后事务已经结束下一条查询自动开启新事务。我在实际调试时发现很多人把ROLLBACK写在mysql_commit之后那等于白写事务都已经提交了。判断语句成功与否要检查mysql_query的返回值不是看它有没有报错输出——这个函数返回 0 表示成功非 0 表示失败mysql_error(conn)只负责告诉你失败原因。4. 连接外网 Google 接口cURL 封装与 HTTPS 请求的落地写法4.1 工程里 googleearth.h 封装的是什么本质是一次 HTTP GETgoogleearth.h和googleearth.cpp在这份工程里封装的正是一次向 Google 服务发起请求并接收响应的完整过程。摘要里给的是 Google Maps Geocoding API 的示例这个 API 的作用是把“1600 Amphitheatre Parkway, Mountain View, CA”这种地址文本转成经纬度坐标返回的是 JSON。整个请求本质就是一个 HTTPS GET区别只在于 URL 里带不带参数。工程里把这个封装成函数后MFC 的视图类只需要调用GetGeocode(address, lat, lng)不需要关心底层 socket 和 HTTP 头。为什么用 cURL 而不是 WinINet 或 WinHTTPVC6 自带的 WinINet 也能发 HTTP 请求但处理 HTTPS 证书、代理、重定向这些细节时 API 设计更繁琐而 libcurl 把这些都收拢成curl_easy_setopt的选项语义清晰。老工程里只要目标机器能装上 cURL 的开发库优先用它没毛病。4.2 cURL 请求的完整代码与回调函数陷阱我整理一份能在 VC6 下编译的 cURL 调用代码包含响应接收的完整闭环#include curl/curl.h #include string // 回调函数libcurl 会把响应内容分块传入 size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) { size_t total_size size * nmemb; output-append((char*)contents, total_size); return total_size; } std::string request_google_api(const char* url) { CURL* curl; CURLcode res; std::string response; curl_global_init(CURL_GLOBAL_DEFAULT); curl curl_easy_init(); if (curl) { curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // 调试阶段可设为 0生产环境建议保留证书校验 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); res curl_easy_perform(curl); if (res ! CURLE_OK) { fprintf(stderr, curl error: %s\n, curl_easy_strerror(res)); } curl_easy_cleanup(curl); } curl_global_cleanup(); return response; }CURLOPT_WRITEFUNCTION是最容易理解错的地方。libcurl 默认把响应写到标准输出你要截获内容就得给它一个回调函数这个回调的返回值和size * nmemb完全相等才算完整接收返回少了 libcurl 会认为传输异常。回调函数声明里std::string*是用户自定义指针通过CURLOPT_WRITEDATA传进去CURLOPT_WRITEFUNCTION不直接捕获上下文两个选项要配对使用。CURLOPT_FOLLOWLOCATION设 1 是允许自动跟随 301/302 重定向Google 的部分接口会先返回重定向地址这个不设的话请求结果可能是一段空的跳转提示。CURLOPT_SSL_VERIFYPEER和CURLOPT_SSL_VERIFYHOST在调试阶段设 0 能避开证书链校验失败的坑但生产环境我是会设回 1 的不然有中间人攻击风险。4.3 URL 编码地址带空格和中文时的必踩点调用 Geocoding API 时 URL 里的地址参数必须做 URL 编码空格要变成或%20中文要转成 UTF-8 百分号编码。直接拼接原始字符串发出去服务端会返回 400 或找不到结果。工程里需要自行实现一个简单的编码函数因为 VC6 环境不一定有现成的 URL 编码库std::string url_encode(const std::string input) { std::string output; char hex[4]; for (size_t i 0; i input.length(); i) { unsigned char c (unsigned char)input[i]; if (isalnum(c) || c - || c _ || c . || c ~) { output c; } else if (c ) { output ; } else { sprintf(hex, %%%02X, c); output hex; } } return output; }函数里%%%02X是printf格式化的转义%%输出一个百分号%02X输出字节的大写十六进制。调用时先url_encode(address)再拼 URL。这个函数只适用于 ASCII 字符的编码如果输入是 GBK 编码的中文需要先把std::string里的 GBK 字节转成 UTF-8 再编码否则 Google 解析不了。我处理过的一个案例就是界面输入的地址是 GBK直接编码发出去返回ZERO_RESULTS用MultiByteToWideChar转成 UTF-8 后再编码就好了。别小看这个细节Google 接口对编码格式极其敏感错一个字节都不认。5. 避坑指南VC6 MySQL cURL 组合下的七个经典雷区5.1 “找不到 mysql.h”——头文件路径与 lib 路径配错现象新建工程包含mysql.h时报fatal error C1083: Cannot open include file: mysql.h: No such file or directory。原因VC6 的默认 include 目录里没有 MySQL 的头文件路径。解决把mysql.h所在目录加入 Tools - Options - Directories - Include files把libmySQL.lib所在目录加入 Library files两个都要配。注意 MySQL 安装目录里include下的是头文件lib下的是导入库别指错层级。我记得还有一次是 64 位的libmySQL.lib拿到 32 位工程里链接报错直接是LNK2001 unresolved external symbol换回 32 位库才通过。5.2 libmySQL.dll 版本和 lib 版本不一致导致运行期崩溃现象编译链接全过一运行弹窗“无法定位程序输入点于 libmySQL.dll”或者查询时直接内存访问违例。原因程序运行时加载的libmySQL.dll和链接时的libmySQL.lib来自不同 MySQL 版本导出的函数符号对不上mysql_real_connect的底层实现已经变了。解决把libmySQL.lib和libmySQL.dll放到同一个目录版本必须严格对应最好直接用 MySQL 官方安装目录里的lib和bin下同名文件并把libmySQL.dll拷贝到 exe 输出目录。用dumpbin /dependents 你的exe可以查看实际加载的 DLL 路径确认是不是被别的旧版 DLL 抢先加载了。5.3 mysql_query 执行中文 SQL 后查不到数据或报语法错误现象SQL 语句里带中文条件在 MySQL 命令行能查出来通过mysql_query执行却报错或返回空。原因mysql_real_connect后没有设置字符集连接默认用 latin1 或旧版配置中文字符被误解码。解决连接后立即执行mysql_set_character_set(conn, utf8)同时确认数据表本身是 utf8 字符集。如果表是 utf8mb4 且里面有 emoji把这里改成utf8mb4。代码和数据库两边字符集对齐后这个问题基本绝迹。5.4 cURL 回调函数不生效响应内容为空现象请求执行成功但返回的std::string是空的。原因CURLOPT_WRITEFUNCTION设了但CURLOPT_WRITEDATA没传指针或传的指针在回调函数执行前已失效。解决确认两个选项成对出现response的生命周期覆盖curl_easy_perform全程。我在调试时还碰过一次回调函数声明为static但类成员函数编译报类型不匹配改成自由函数或静态成员函数解决。VC6 对函数指针类型检查严格回调签名必须是size_t (*)(void*, size_t, size_t, void*)多一个修饰符都可能编译不过。5.5 HTTPS 证书校验失败curl_easy_perform 返回 60现象访问https://开头的 Google API 时返回CURLE_PEER_FAILED_VERIFICATION。原因libcurl 默认校验服务器证书链但本机没有安装 CA 证书或系统时间不对导致证书有效期判断失败。解决先检查系统时间是否准确这个坑我踩过一次时间差一天全是证书报错然后调试阶段设CURLOPT_SSL_VERIFYPEER, 0L绕过校验最终方案是配置 CA 证书路径CURLOPT_CAINFO指向一份 ca-bundle.crt。要分清这是调试策略和上线配置的区别不能为了省事长期关校验。5.6 mysql_close 之后还在用之前取到的结果集现象程序退出时偶发崩溃或者第二次查询时返回乱码。原因mysql_close(conn)释放了连接资源但代码里还保留着之前mysql_store_result拿到的指针。解决方案所有结果集用完后先mysql_free_result(res)再mysql_close(conn)顺序不能反。另外mysql_store_result的返回值可能为空但该为空有两种情况查询本身出错和查询无结果。判断方式是用mysql_field_count(conn)检查为 0 表示无结果非 0 才是错误。很多老代码只判断了空指针导致空结果集的查询被误判为失败。5.7 VC6 的 std::string 和 C API 的 char* 混用翻车现象把std::string直接传给mysql_query的第二个参数编译偶尔警告运行结果随机。原因mysql_query(conn, sql.c_str())这里的c_str()返回指针在语句执行期间有效但如果sql在mysql_query执行前被重新赋值或销毁指针就悬空了。解决用一个局部char数组固定保存 SQL 文本或者确保std::string的生命周期覆盖整个查询过程。VC6 的 STL 实现比较老c_str()返回的缓冲区在字符串重新分配时会变地址所以不能先取地址存着后面再用。6. 验证技巧把每次网络请求和 SQL 查询都写成带时间戳的日志接手这份工程后的第一件事我会先给它加一套日志函数因为 MFC 程序看不到控制台输出出问题全靠日志定位。核心做法是把 DebugView 能捕获的OutputDebugString和文件日志结合每条日志带毫秒级时间戳和调用位置void LogTrace(const char* func, const char* msg) { SYSTEMTIME st; GetLocalTime(st); char line[512]; sprintf(line, [%02d:%02d:%02d.%03d] %s: %s\n, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, func, msg); OutputDebugString(line); FILE* fp fopen(debug.log, a); if (fp) { fputs(line, fp); fclose(fp); } }GetLocalTime提供的毫秒字段能让你看清两次操作之间到底卡在哪里OutputDebugString配合 DebugView 实时看fopen追加写文件作为永久留痕。调用点放在数据库连接成功、查询开始前、查询返回后、HTTP 请求发出前、响应接收完这五个关键节点。我在实际追踪一个“偶发查询超时”的 bug 时就是靠日志发现mysql_real_connect之后有一句 DNS 解析卡了 5 秒——不是 MySQL 的问题是本机 hosts 没配好。自从那以后我每次接手老工程都强制先走一遍这套日志接入再谈改业务逻辑。这套方法对你的意义在于数据库报错和网络超时是两类完全不同的问题日志能把它们精确分开省得来回猜。希望帮到你。本文还有配套的精品资源点击获取
返回列表