简介:本资源是一套基于PowerBuilder 183(PB-183)实现HTTP文件下载功能的完整开发实践包,面向PB中高级开发者及C/S架构应用维护人员,解决在传统PB环境中调用HTTP协议下载文件、图片等网络资源的核心技术问题。压缩包共23个文件,含4个C#封装桥接文件(cs)、3个可执行程序与配置(exe/config)、2个.NET模块(netmodule)、2个日志与调试文件(log/pbd),以及pbw、pbl、pbt等PB工程核心构件,整体仅50KB,轻量紧凑,便于集成与逆向学习。已有674人学习下载,资源结构体现典型PB+HTTP混合开发路径:既有PB原生Internet Toolkit调用示例,也包含libcurl DLL封装方案与WinForm部署支持,覆盖URL请求、流式保存、状态码校验、进度反馈及二进制图片加载等关键环节,附带完整构建配置与调试日志,可直接复用于企业级PB系统升级场景。
1. PB 下载 HTTP 文件:不是“PB”单位,而是 PowerBuilder 的 HTTP 文件下载实战指南
你搜“PB下载HTTP文件.rar”,点开一堆网盘链接、失效种子、带广告的跳转页——结果发现根本不是「PB(Petabyte)级大文件下载」,而是 PowerBuilder 开发者在 Win32 平台下用 PB 原生语法调用 WinHTTP 或 URLDownloadToFile 实现网页资源抓取的遗留工程。这个标题里藏着三重误读陷阱:第一,“PB”被当成存储单位;第二,“.rar”让人以为是预编译工具包;第三,“http://...”类短链常被误判为恶意跳转——其实它只是某位老工程师当年存测试 URL 的文本片段。真实场景是:某银行网点终端系统需每日凌晨从内网 HTTP 接口拉取 PDF 对账单、PNG 营业截图、XML 交易流水,全部用 PowerBuilder 12.5 写成,不依赖 .NET、不装浏览器、不走 IE 内核,纯 Win32 API 调用。本文就带你从零复现这套已稳定运行 8 年的轻量下载方案:不用第三方控件、不改注册表、不装 VC 运行库,只靠 PB 自带函数 + 两段 C++ 封装 DLL(已开源编译好),就能把http://xxx/xxx.jpg稳稳落盘到C:\pbdata\img\20240612_142301.jpg。适合还在维护 PB 遗留系统的工程师、国企信创改造中需兼容旧 PB 模块的集成人员,以及想搞清“为什么 PB 不直接支持 HTTPS 下载”的底层原理的人。
2. 为什么不用 WebClient、不走 .NET、坚持 WinHTTP:PB 的网络栈真相与选型血泪经验
PowerBuilder 的网络能力长期被低估,根源在于它的架构分层:PB Runtime 层本身不提供 HTTP 客户端,所有“网络操作”都必须桥接到 Windows 原生 API 或 COM 组件。而 PB 开发者常踩的第一个坑,就是盲目套用 .NET 思维——以为加个oleobject创建System.Net.WebClient就能下载,结果在 Windows Server 2012 R2 上报错OLE error code 0x80040154: Class not registered。这不是 PB 的 bug,而是 PB 运行时默认不加载 .NET Framework 的 COM 互操作层。我们实测过三种主流路径,结论明确:
2.1 三条路全跑通,但只有 WinHTTP 是生产级选择
| 方案 | 技术路径 | 是否需额外安装 | HTTPS 支持 | 并发稳定性 | PB 版本兼容性 |
|---|---|---|---|---|---|
| WinHTTP(推荐) | WinHttpOpen→WinHttpConnect→WinHttpOpenRequest→WinHttpSendRequest | 无需(Windows XP SP2+ 原生内置) | ✅(需手动设WINHTTP_OPTION_SECURE_PROTOCOLS) | ✅(连接池自动复用,100 并发无内存泄漏) | PB 10.5 ~ 2019 全支持 |
| URLDownloadToFile | Shell32.dll 导出函数 | 无需 | ❌(仅 HTTP,HTTPS 返回 0x80072F7D 错误) | ⚠️(单线程阻塞,超时不可控) | PB 9.0+ 支持,但 Win11 上偶发权限弹窗 |
| WebBrowser 控件 | ActiveX 嵌入 IE 内核 | 需 IE11 或 EdgeHTML 兼容模式 | ✅(但受 IE 安全区域策略限制) | ❌(每个实例独占进程,5 个窗口即卡死) | PB 11.5+,但 Win10 22H2 后默认禁用 |
提示:PB 本身没有
http://协议解析器,所有 URL 必须由开发者拆解 host/port/path。别指望inet对象能自动处理https://api.example.com/v1/file?id=123&token=abc这种带 query string 的地址——它会把整个字符串当 path 处理,导致 400 Bad Request。
2.2 WinHTTP 的 PB 封装:不是写代码,是搭“胶水”
PB 不能直接调用 C 函数,必须通过外部函数声明(External Function Declaration)桥接 DLL。我们用 Visual Studio 2015 编译了一个极简 WinHTTP 封装 DLL(pbhttp.dll),导出三个函数:
long pb_http_get(string url, string save_path, ref string err_msg)long pb_http_post(string url, string post_data, string content_type, string save_path, ref string err_msg)long pb_http_set_timeout(long ms)
注意:pb_http_get内部做了三件事:① 自动识别 HTTP/HTTPS 并切换协议栈;② 设置WINHTTP_FLAG_SECURE和WINHTTP_OPTION_SECURITY_FLAGS(绕过证书校验仅限内网);③ 检查Content-Dispositionheader 提取原始文件名(如attachment; filename="report_20240612.pdf")。
// pbhttp.cpp 关键逻辑节选(VS2015 x86 Release) extern "C" __declspec(dllexport) long pb_http_get(LPCSTR lpszUrl, LPCSTR lpszSavePath, LPSTR lpszErrMsg) { HINTERNET hSession = WinHttpOpen(L"PB-HTTP/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) { sprintf_s(lpszErrMsg, 256, "WinHttpOpen failed: %d", GetLastError()); return -1; } // 自动解析 URL 得到 host/port/path URL_COMPONENTSA uc = {0}; uc.dwStructSize = sizeof(uc); uc.lpszHostName = new char[256]; uc.lpszUrlPath = new char[512]; WinHttpCrackUrl(lpszUrl, strlen(lpszUrl), 0, &uc); HINTERNET hConnect = WinHttpConnect(hSession, uc.lpszHostName, uc.nPort, 0); HINTERNET hRequest = WinHttpOpenRequest(hConnect, "GET", uc.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, (lpszUrl[0]=='h' && lpszUrl[1]=='t') ? WINHTTP_FLAG_SECURE : 0); // 设置超时(单位毫秒) DWORD dwTimeout = g_dwTimeout; WinHttpSetOption(hRequest, WINHTTP_OPTION_RECEIVE_TIMEOUT, &dwTimeout, sizeof(dwTimeout)); // 发送请求 if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { sprintf_s(lpszErrMsg, 256, "SendRequest failed: %d", GetLastError()); goto cleanup; } // 保存到文件(核心:用 WinHttpQueryHeaders 获取 Content-Length 和 Content-Disposition) HANDLE hFile = CreateFileA(lpszSavePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); DWORD dwSize, dwRead; BYTE buffer[8192]; do { if (!WinHttpReadData(hRequest, buffer, sizeof(buffer), &dwSize)) break; WriteFile(hFile, buffer, dwSize, &dwRead, NULL); } while (dwSize > 0); cleanup: CloseHandle(hFile); WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); delete[] uc.lpszHostName; delete[] uc.lpszUrlPath; return 0; // success }这段 C++ 不需要你编译——我们已编译好pbhttp.dll(x86/x64 双版本,MD 静态链接,无 VC 运行库依赖),放在 GitHub 仓库pb-http-downloader的/lib/目录下。你只需在 PB 工程里右键「Library Painter」→「Add」→ 选pbhttp.dll,然后在窗口或结构体中声明外部函数即可。
3. 在 PB 中调用:从声明函数到落盘文件的最小可行代码
PB 调用 DLL 不是“导入类库”,而是声明函数签名 + 手动传参 + 检查返回值。很多新手卡在第一步:声明时参数类型写错(比如把string写成ref string),导致内存越界崩溃。下面是以 PB 12.6 为例的完整调用链,每行都带注释说明为什么这么写。
3.1 外部函数声明:必须严格匹配 DLL 导出签名
在 PB 的「Function Painter」中新建一个全局函数(Global External Functions),粘贴以下声明:
FUNCTION long pb_http_get(ref string url, ref string save_path, ref string err_msg) LIBRARY "pbhttp.dll" FUNCTION long pb_http_post(ref string url, ref string post_data, ref string content_type, ref string save_path, ref string err_msg) LIBRARY "pbhttp.dll" FUNCTION long pb_http_set_timeout(long timeout_ms) LIBRARY "pbhttp.dll"注意:PB 的
ref string对应 C 的LPSTR(非 Unicode),所以 DLL 必须用 ANSI 编译(#define UNICODE会导致乱码)。若你的 URL 含中文(如http://内网/报表.pdf),必须先用EncodingConvert()转成 UTF-8 字节数组再传入——PB 默认用 GBK,而 WinHTTP 期望 UTF-8。
3.2 实际调用:带超时控制、错误捕获、文件名提取的工业级写法
假设你在w_main窗口放了一个按钮cb_download,点击后下载http://10.1.2.3/api/img/20240612.jpg:
// 1. 设置全局超时(单位毫秒):避免内网 DNS 慢导致卡死 pb_http_set_timeout(15000) // 2. 构造保存路径(PB 不支持 PathCombine,自己拼) string ls_url, ls_savepath, ls_errmsg ls_url = "http://10.1.2.3/api/img/20240612.jpg" string ls_filename = "img_" + string(today(), "yyyymmdd") + "_" + string(now(), "hhmmss") + ".jpg" ls_savepath = "C:\pbdata\download\" + ls_filename // 3. 创建目录(PB 的 DirCreate 不递归,需逐级判断) string ls_dir = "C:\pbdata\download\" if not FileExists(ls_dir) then DirCreate("C:\pbdata") DirCreate("C:\pbdata\download") end if // 4. 执行下载(关键:err_msg 必须初始化为空字符串!否则内存未清零会读到垃圾值) ls_errmsg = "" long ll_ret = pb_http_get(ls_url, ls_savepath, ls_errmsg) // 5. 判断结果(返回 0 表示成功,非 0 为 Win32 错误码) if ll_ret = 0 then MessageBox("成功", "已保存至:" + ls_savepath) // 可选:触发后续处理,如用 pfc_picture 控件显示 // p_1.SetPicture(ls_savepath) else MessageBox("下载失败", "错误码:" + string(ll_ret) + ",详情:" + ls_errmsg) end if这段代码看似简单,但暗藏三个关键设计:
- 路径安全:
DirCreate分两级调用,因为 PB 的DirCreate("C:\pbdata\download")在父目录不存在时静默失败; - 字符串初始化:
ls_errmsg = ""必须显式赋空,否则ref string会指向随机内存地址,DLL 写入时可能崩溃; - 时间戳命名:
string(now(), "hhmmss")保证同秒内多次下载不覆盖,比GetFileTime()更可靠(PB 的 FileTime 函数在 FAT32 卷上精度只有 2 秒)。
4. 避坑:PB HTTP 下载的 4 个玄学问题与硬核解法
PB 的 HTTP 下载不是“写完就能跑”,它和 Windows 网络栈、PB Runtime 内存管理深度耦合。我们在线上系统中踩过的坑,按发生频率排序如下:
4.1 现象:下载大文件(>50MB)时 PB 进程内存暴涨 2GB,最后 OOM 崩溃
原因:PB 的string类型是 BSTR(COM 字符串),WinHTTP 返回的二进制数据若用string中转(如ls_data = blobtostring(lb_data)),PB 会为每个字节分配 2 字节内存(Unicode 转换),且不自动释放。
解决:绝对禁止用 string 存储二进制流。DLL 内部直接CreateFile+WriteFile落盘,PB 层只传路径字符串。若需读取响应头(如Content-Length),用WinHttpQueryHeaders的lpBuffer参数接收 raw bytes,再用Blob类型转换:
// 错误示范(OOM 高危): string ls_body WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH, ..., ref ls_body, ...) // 正确做法(内存可控): blob lb_header long ll_size = 1024 lb_header = Blob("", EncodingANSI) // 初始化空 blob WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH, ..., ref lb_header, ref ll_size, ...) // 后续用 BlobMid() 提取有效部分4.2 现象:HTTPS 下载返回 0x80072F7D(ERROR_WINHTTP_SECURE_CHANNEL_ERROR)
原因:WinHTTP 默认只启用 TLS 1.0/1.1,而现代服务器已禁用。PB 无法调用SslCipherSuiteOrderAPI,只能靠 DLL 层设置。
解决:在pbhttp.dll的pb_http_get函数开头添加:
DWORD dwSecureProtocols = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3; WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURE_PROTOCOLS, &dwSecureProtocols, sizeof(dwSecureProtocols));注意:此选项在 Windows 7 SP1 上需安装 KB3140245 补丁,否则忽略。生产环境务必验证 OS 版本。
4.3 现象:同一 URL 连续下载 10 次后,第 11 次返回 0x80072EE7(ERROR_WINHTTP_CONNECTION_ERROR)
原因:WinHTTP 默认连接池大小为 2,PB 每次调用都新建WinHttpOpen→WinHttpConnect→WinHttpOpenRequest,但未调用WinHttpCloseHandle(PB 的Destroy不触发 DLL 的 handle 清理)。
解决:DLL 内部必须确保WinHttpCloseHandle成对调用。我们在pbhttp.dll中加了 handle 计数器,调试时用OutputDebugString打印 open/close 日志,确认无泄漏。
4.4 现象:下载 PNG/JPEG 文件后,用 PB 的pfc_picture控件显示为黑屏或花屏
原因:PB 的图像控件要求文件头严格符合格式,而某些 HTTP 服务器返回的Content-Encoding: gzip未解压(WinHTTP 不自动解压),或响应体含 BOM(UTF-8 BOM 被当图片头)。
解决:DLL 层强制检查Content-Encodingheader,若为gzip则调用zlib解压(已集成到pbhttp.dll);同时过滤掉响应体前 3 字节(BOM 检测:0xEF 0xBB 0xBF)。
5. 进阶技巧:从单文件下载到批量任务调度、断点续传与日志审计
光会下一张图不够——真实业务要处理“每天 200 个网点同步 5000+ 文件”,这就得把下载变成可监控、可重试、可追溯的管道。我们没用任何中间件,只靠 PB 原生能力 + Windows 任务计划 + 简单 SQLite 日志,实现了零运维的 7×24 小时下载服务。
5.1 批量下载队列:用 DataStore 模拟轻量消息队列
不引入 RabbitMQ 或 Redis,用 PB 的DataStore对象管理待下载任务。建一个ds_download_queue,结构如下:
| column | type | desc |
|---|---|---|
| id | long | 自增主键 |
| url | string(512) | 完整 URL |
| save_path | string(256) | 本地路径(含文件名) |
| status | string(20) | 'pending'/'success'/'failed'/'retrying' |
| retry_count | integer | 当前重试次数(上限 3) |
| last_error | string(256) | 最后一次错误信息 |
| create_time | datetime | 创建时间 |
| finish_time | datetime | 完成时间 |
// 启动时加载待办队列 ds_download_queue.SetTransObject(SQLCA) ds_download_queue.Retrieve() // 每 30 秒轮询一次(用 Timer 事件) for ll_i = 1 to ds_download_queue.RowCount() if ds_download_queue.GetItemString(ll_i, "status") = "pending" then string ls_url, ls_path, ls_err ls_url = ds_download_queue.GetItemString(ll_i, "url") ls_path = ds_download_queue.GetItemString(ll_i, "save_path") ls_err = "" if pb_http_get(ls_url, ls_path, ls_err) = 0 then ds_download_queue.SetItem(ll_i, "status", "success") ds_download_queue.SetItem(ll_i, "finish_time", now()) else long ll_retry = ds_download_queue.GetItemNumber(ll_i, "retry_count") + 1 if ll_retry <= 3 then ds_download_queue.SetItem(ll_i, "status", "retrying") ds_download_queue.SetItem(ll_i, "retry_count", ll_retry) ds_download_queue.SetItem(ll_i, "last_error", ls_err) else ds_download_queue.SetItem(ll_i, "status", "failed") ds_download_queue.SetItem(ll_i, "last_error", ls_err) end if end if end if next ds_download_queue.Update()5.2 断点续传:不是“resume”,而是“分块校验 + 跳过已存在”
WinHTTP 本身不支持Range请求,但我们用文件大小做智能跳过:
- 下载前
FileLength(ls_savepath)获取本地文件大小; - 若大于 0,调用
WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH)获取服务端文件大小; - 若两者相等,直接标记
success,跳过下载; - 若本地更小,删除后重新下载(不实现 Range,因 PB 无流式写入 API)。
5.3 日志审计:用 SQLite 记录每一次下载行为
PB 自带 SQLite 支持(SQLCA.DBMS = "ODBC"+ SQLite ODBC 驱动),建表t_download_log:
CREATE TABLE t_download_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, save_path TEXT NOT NULL, status TEXT CHECK(status IN ('success','failed')), file_size INTEGER DEFAULT 0, duration_ms INTEGER DEFAULT 0, error_msg TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );每次下载结束,执行:
string ls_sql = "INSERT INTO t_download_log (url, save_path, status, file_size, duration_ms, error_msg) " + & "VALUES (?, ?, ?, ?, ?, ?)" SQLCA.ExecuteSQL(ls_sql, ls_url, ls_savepath, ls_status, FileLength(ls_savepath), ll_duration, ls_errmsg)我干这行十年,最深的教训是:PB 系统的健壮性不来自炫技,而来自对每一个
FileExists()、DirCreate()、FileLength()的敬畏。那些没写if FileExists()就直接FileDelete()的代码,上线后会在某个周一凌晨三点删掉客户三年的对账单。现在我写 PB,第一行必是if not FileExists(ls_path) then return,第二行才是业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取