简介:这份资源面向具备一定C++基础的开发者,聚焦VC++环境下调用MySQL数据库并访问外网Google服务的实现方案,可用于数据库管理、网络通信及Web服务集成等场景的学习与参考。压缩包共41个文件,约3.89MB,以h头文件、cpp源文件、obj编译中间文件为主,另含exe可执行程序、dll与lib动态及静态库、ico图标、rc资源脚本和dsp、dsw工程文件,覆盖从源码到可运行工程的完整结构。资源中提供了mysql.h头文件引用、libmysqlclient库链接以及mysql_real_connect建立连接、执行SQL查询并处理结果集的示例代码,同时演示了借助cURL向Google Maps Geocoding API等外网服务发送HTTP请求并解析响应的思路,有助于读者理解VC++与MySQL交互及外网API调用的关键环节。目前已有312人学习下载,适合需要动手实践数据库连接与外部服务集成的开发者参考借鉴。
1. 从一份 2003 年的 VC6 工程说起:它到底解决了什么问题
翻到Google.zip这个包的时候,我第一反应是「这名字起得真敢」。解压出来一看,Google.dsp、Google.dsw、vc60.idb、vc60.pdb全在,典型的 Visual C++ 6.0 工程结构,编译产物Google.exe和libmySQL.dll也一并躺在目录里。这不是什么新项目,而是一份把「VC++ 通过 MySQL C API 读写数据库」和「VC++ 发起 HTTP 请求访问外部服务」两件事揉在一个 MFC 单文档程序里的老工程。标题里那个「连接外网 Google」,落到代码层面其实就是用 WinINet 或 socket 去请求 Google 的 HTTP 接口,跟今天说的 RESTful 调用是一回事,只是年代早、封装薄。
它适合谁?适合两类人:一类是还在维护 VC6 遗留系统、需要把本地数据同步到远端服务的工程师;另一类是想搞明白「MFC 程序怎么同时挂数据库和网络请求」的学生或转行者。你不需要把它当成生产级模板,但它是一份能跑通的、结构完整的最小闭环,比看十篇割裂的 API 文档管用。下面我按「先跑起来、再拆原理、最后说坑」的顺序,把这份工程拆开讲。
2. 把工程跑起来:VC6 环境、libmySQL 链接与 MFC 骨架
2.1 先确认你手里有什么
解压Google.zip,根目录下能看到的文件大致分四类,先认清楚再动手,不然编译报错你都不知道缺哪块。
| 文件/目录 | 作用 | 是否必须 |
|---|---|---|
Google.dsw/Google.dsp | VC6 工作区与工程文件 | 必须,双击 dsw 打开 |
GoogleView.cpp/.h、MainFrm.cpp/.h、GoogleDoc.cpp/.h | MFC 单文档视图框架 | 必须,程序主体 |
googleearth.cpp/.h | 业务逻辑,网络请求相关 | 必须 |
libmySQL.lib/libmySQL.dll | MySQL C API 的导入库与运行时 | 必须,缺一编译或运行失败 |
res/、Toolbar.bmp、Google.ico | 资源文件 | 必须,否则资源编译报错 |
Debug/ | 已有编译产物 | 可删,建议重新编译 |
ReadMe.txt里通常写着原始作者的环境说明,先读一遍,里面往往有数据库版本和连接参数的线索。
2.2 配置 VC6 的包含目录与库目录
VC6 默认不认识 MySQL 的头文件和库,得手动告诉它。打开Tools → Options → Directories,在Include files里加上 MySQL 安装目录下的include,在Library files里加上lib目录。这一步是血泪经验:很多人直接把libmySQL.lib拷到工程目录就以为完事,结果链接阶段报一堆unresolved external symbol,就是因为库搜索路径没配。
配好之后,在Project → Settings → Link的Object/library modules里补上libmySQL.lib,多个库用空格隔开。如果你用的是 MySQL 5.x 以上版本,注意libmySQL.lib可能是 32 位还是 64 位的问题——VC6 只能编 32 位,所以必须用 32 位的库,拿 64 位的来链接会直接失败。
2.3 让程序先连上本地 MySQL
工程里连接数据库的核心代码在googleearth.cpp或GoogleDoc.cpp里,逻辑跟下面这段基本一致。先确保本地 MySQL 服务在跑,建好库和表,再改连接参数。
#include <mysql.h> #include <iostream> MYSQL* conn; MYSQL_RES* res; MYSQL_ROW row; // 初始化连接句柄 conn = mysql_init(NULL); if (conn == NULL) { std::cerr << "mysql_init failed" << std::endl; return; } // 建立连接:主机、用户、密码、库名、端口、socket、flag if (!mysql_real_connect(conn, "127.0.0.1", "root", "your_password", "test_db", 3306, NULL, 0)) { std::cerr << "connect error: " << mysql_error(conn) << std::endl; mysql_close(conn); return; } // 设置字符集,避免中文乱码 mysql_query(conn, "SET NAMES utf8"); // 执行查询 if (mysql_query(conn, "SELECT id, name FROM users")) { std::cerr << "query error: " << mysql_error(conn) << std::endl; mysql_close(conn); return; } res = mysql_store_result(conn); if (res == NULL) { std::cerr << "store result error: " << mysql_error(conn) << std::endl; mysql_close(conn); return; } // 逐行取数据 while ((row = mysql_fetch_row(res)) != NULL) { std::cout << "id=" << row[0] << " name=" << row[1] << std::endl; } mysql_free_result(res); mysql_close(conn);这段代码有几个参数值得说清楚。mysql_real_connect的第五个参数是端口,写 0 表示用默认 3306;第六个是 unix socket,Windows 下传 NULL;第七个是客户端标志位,一般填 0,需要压缩协议或 SSL 时才改。mysql_store_result会把整个结果集拉到客户端内存,数据量大时改用mysql_use_result逐行读,但那样就不能在读完前发新查询。SET NAMES utf8这行别省,VC6 默认是 GBK 环境,不设字符集,中文进去就是乱码,这是最常见的翻车点之一。
2.4 编译时最容易卡住的两个地方
第一个是mysql.h找不到。除了目录没配,还有一种情况是 MySQL 安装包只带了 Connector/C++ 而没带 C API 头文件,那就得单独装 MySQL Server 或下载对应版本的开发包。第二个是libmySQL.dll运行时找不到。编译通过不代表能跑,双击 exe 报「无法启动此程序,因为计算机中丢失 libmySQL.dll」,把 dll 拷到 exe 同目录或C:\Windows\System32即可。Debug 目录里原本就带着一份 dll,说明原作者也是这么干的。
3. 从本地到外网:VC++ 里发起 HTTP 请求的三种做法
3.1 为什么这份工程选了 WinINet 而不是 cURL
标题里「连接外网 Google」听着玄乎,本质就是发 HTTP 请求。VC6 时代能用的方案有三类:WinINet、WinSock 手写 HTTP、第三方库如 libcurl。这份工程大概率用的是 WinINet,因为它是 Windows 自带、不用额外链接库、MFC 里调用最省事。libcurl 功能强但要自己编译或找预编译包,在 VC6 下配起来麻烦。WinSock 手写则要自己拼请求头、处理分块传输,容易出错。
选型逻辑很简单:如果你只是发个 GET 拿 JSON,WinINet 够用;如果要处理 HTTPS、重定向、超时重试、上传大文件,libcurl 更稳。这份工程属于前者,所以别指望它有多强的网络健壮性。
3.2 用 WinINet 发一个 GET 请求
下面这段是 WinINet 的标准调用流程,放在googleearth.cpp里就能跑。注意它请求的是 Google 的某个 HTTP 接口,实际使用时把 URL 换成你要访问的服务地址。
#include <windows.h> #include <wininet.h> #include <string> #include <iostream> #pragma comment(lib, "wininet.lib") void HttpGet(const std::string& host, const std::string& path) { HINTERNET hInternet = InternetOpenA("VCApp", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (!hInternet) { std::cerr << "InternetOpen failed: " << GetLastError() << std::endl; return; } // 建立到目标主机的连接,端口 80 为 HTTP HINTERNET hConnect = InternetConnectA(hInternet, host.c_str(), 80, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); if (!hConnect) { std::cerr << "InternetConnect failed: " << GetLastError() << std::endl; InternetCloseHandle(hInternet); return; } // 打开请求,flag 里 INTERNET_FLAG_RELOAD 表示不走缓存 HINTERNET hRequest = HttpOpenRequestA(hConnect, "GET", path.c_str(), NULL, NULL, NULL, INTERNET_FLAG_RELOAD, 0); if (!hRequest) { std::cerr << "HttpOpenRequest failed: " << GetLastError() << std::endl; InternetCloseHandle(hConnect); InternetCloseHandle(hInternet); return; } // 发送请求 if (!HttpSendRequestA(hRequest, NULL, 0, NULL, 0)) { std::cerr << "HttpSendRequest failed: " << GetLastError() << std::endl; } else { // 循环读取响应体 char buffer[4096]; DWORD bytesRead = 0; std::string response; while (InternetReadFile(hRequest, buffer, sizeof(buffer) - 1, &bytesRead) && bytesRead > 0) { buffer[bytesRead] = '\0'; response += buffer; } std::cout << "response length: " << response.size() << std::endl; std::cout << response.substr(0, 500) << std::endl; } InternetCloseHandle(hRequest); InternetCloseHandle(hConnect); InternetCloseHandle(hInternet); }参数逐个说:InternetOpenA的第一个参数是 User-Agent,有些服务会校验,别乱填;第二个INTERNET_OPEN_TYPE_PRECONFIG表示用系统已有的网络配置,这是最省事的方式。InternetConnectA的端口参数,HTTP 是 80,HTTPS 是 443,但 WinINet 走 HTTPS 还需要在HttpOpenRequestA的 flag 里加INTERNET_FLAG_SECURE,并且 VC6 自带的 wininet 对现代 TLS 支持有限,访问 HTTPS 站点经常握手失败,这是老工程的硬伤。HttpSendRequestA的第三、四参数用于 POST 时传 body,GET 传 NULL 和 0 即可。
3.3 如果换成 libcurl 会怎样
libcurl 的写法更简洁,跨平台也好,但在 VC6 下要先解决库的问题。核心调用是curl_easy_init→curl_easy_setopt设 URL →curl_easy_perform→curl_easy_cleanup。它自带 HTTPS 支持(取决于编译时的 SSL 后端),超时、重定向、代理设置都是一行setopt的事。如果你要长期维护这个工程,我一般会建议把 WinINet 那部分替换成 libcurl,代价是引入一个 dll,收益是网络层少踩很多坑。
3.4 把数据库数据和网络请求串起来
这份工程真正的价值在于它把两件事连起来了:先从 MySQL 读出数据,再把数据作为参数发到远端服务。典型流程是查出一批待同步记录,拼成 URL 或 POST body,逐条发送,根据响应更新本地状态字段。这里要注意的是,VC6 的 MFC 界面线程里直接做网络请求会卡死 UI,正确做法是开工作线程,用AfxBeginThread起一个线程跑网络逻辑,主线程只负责刷新界面。工程里如果没做这一步,数据量一大界面就假死,这是遗留系统里最常见的体验问题。
4. 避坑与排查:这份老工程最容易翻车的五个地方
4.1 编译报 unresolved external symbol _mysql_init@4
现象是链接阶段一堆_mysql_xxx找不到。原因通常是libmySQL.lib没加进链接选项,或者加的是 64 位版本。VC6 只能链接 32 位库,去 MySQL 安装目录确认lib下是libmySQL.lib还是libmysql.lib,名字大小写在不同版本里不一样,工程设置里要跟实际文件名完全一致。解决方式是在Project Settings → Link → Object/library modules里补上正确路径和文件名。
4.2 程序启动就报缺少 libmySQL.dll
现象是编译通过,双击 exe 弹窗提示缺 dll。原因是动态链接的库在运行时找不到。解决方式是把libmySQL.dll放到 exe 同目录,或者放进系统 PATH 覆盖的目录。注意 dll 的位数要和 exe 一致,32 位 exe 配 32 位 dll,混了会报「不是有效的 Win32 程序」。
4.3 中文数据读出来是乱码
现象是数据库里明明是中文,程序里显示问号或方块。原因是连接字符集和客户端编码不一致。解决方式是在mysql_real_connect之后立刻执行mysql_query(conn, "SET NAMES utf8"),同时确认数据库表和字段的字符集也是 utf8。如果 VC6 源文件本身是 GBK 编码,界面显示时还要做一次转换,MFC 里用MultiByteToWideChar或CString的转换宏处理。
4.4 访问 HTTPS 接口时 HttpSendRequest 返回失败
现象是请求 Google 的 https 地址时GetLastError返回 12029 或 12157。原因是 VC6 自带的 WinINet 版本太老,不支持现代 TLS 协议。解决方式有两条:一是改用 libcurl 并带上 SSL 后端;二是如果只是内部服务,退回 HTTP。硬要在老工程里支持 HTTPS,基本是给自己找麻烦,不如把网络层单独抽出来用新工具编译成 dll 再调用。
4.5 查询结果集没释放导致内存持续增长
现象是程序跑久了内存越占越大。原因是mysql_store_result拿到的MYSQL_RES*用完没调mysql_free_result,或者连接断开前没mysql_close。解决方式是把资源释放写进固定的清理流程,每次查询配对释放,连接用完即关。在 MFC 里可以封装一个 RAII 类,构造时连接、析构时释放,避免手动漏掉。
5. 进阶用法:把这份工程改造成可维护的数据库+网络模块
5.1 用 RAII 封装 MySQL 连接
老工程里连接、查询、释放散在各处,改起来容易漏。我一般会封一个MySqlConn类,构造时mysql_init和mysql_real_connect,析构时mysql_close,查询方法返回结果集并负责释放。这样调用方只管业务,不用记 API 的配对关系。
class MySqlConn { public: MySqlConn(const char* host, const char* user, const char* pwd, const char* db, unsigned int port = 3306) { conn_ = mysql_init(NULL); if (!mysql_real_connect(conn_, host, user, pwd, db, port, NULL, 0)) { throw std::runtime_error(mysql_error(conn_)); } mysql_query(conn_, "SET NAMES utf8"); } ~MySqlConn() { if (conn_) mysql_close(conn_); } MYSQL* raw() { return conn_; } private: MYSQL* conn_; };这个类把连接生命周期绑在对象上,异常安全,也不会忘记关连接。参数默认端口 3306,需要改就传参。注意构造函数里抛异常时,mysql_init已经分配的资源要确保释放,严格写法是在抛之前先mysql_close。
5.2 网络请求加超时和重试
WinINet 默认超时很长,网络不通时界面会卡很久。可以在InternetOpenA之后用InternetSetOption设置超时:
DWORD timeout = 5000; // 5 秒 InternetSetOptionA(hInternet, INTERNET_OPTION_CONNECT_TIMEOUT, &timeout, sizeof(timeout)); InternetSetOptionA(hInternet, INTERNET_OPTION_RECEIVE_TIMEOUT, &timeout, sizeof(timeout));重试逻辑放在调用层,失败后 sleep 一小段时间再试,最多三次。别在底层无限重试,那样出问题时更难定位。
5.3 验证改造是否成功
改完之后怎么确认没退化?我一般做三件事:一是用同一批数据跑一遍,对比改造前后数据库里的记录数和内容是否一致;二是断网测试,看超时和重试是否按预期触发,程序是否还能正常退出;三是用任务管理器观察内存,连续跑几十次查询和请求,内存曲线应该是平的,不该持续上升。这三点过了,基本可以认为改造是稳的。
从那以后我每次接手这类老工程,都强制先把数据库连接和网络请求各自封成独立模块,再谈业务逻辑。不然改一处崩三处,后悔药都没得吃。希望帮到你。
本文还有配套的精品资源,点击获取