ARTICLE DETAIL

资讯详情

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

PowerBuilder HTTP文件下载实战:WinINet封装与HTTPS断点续传

PowerBuilder HTTP文件下载实战:WinINet封装与HTTPS断点续传 简介本资源是面向PowerBuilderPB开发者的技术实践包聚焦PB-183版本下HTTP协议文件下载功能的完整实现方案适用于C/S架构应用开发中需集成远程资源获取能力的中高级开发人员。压缩包含23个文件总计50KB涵盖核心工程文件pbl、pbw、pbt、编译产物exe、pbd、netmodule、配置与日志config、log、xml、调试支持文件pdb、opt、txt及C#互操作模块cs体现PB与.NET混合开发的典型部署结构。已有674人学习下载资源提供可直接编译运行的httpdownload工程包含HTTPClient调用示例、libcurl封装调用逻辑、图片二进制流处理、下载进度反馈及错误码校验等关键代码片段同时附带部署说明与调试日志便于快速复现、调试和二次扩展。1. PB 下载 HTTP 文件不是“PB”单位而是 PowerBuilder 的 HTTP 文件下载实战你搜“PB下载HTTP文件.rar”点开一堆网盘链接、论坛帖子、甚至带可疑短链的页面结果发现根本不是「下载一个叫 PB 的大文件」——这里的 PB 是 PowerBuilder那个被很多老系统、银行后台、政务内网还在用的客户端开发工具。它不自带现代 HTTP 客户端没有fetch、没有axios、甚至没有原生HttpClient类但业务又真真切切要从http://xxx/api/download?filereport.pdf拿文件还要支持断点续传、进度回调、中文文件名、HTTPS 证书校验、超时重试……这不是玄学是每天在银行柜台系统、医保结算平台、税务前置机上真实发生的硬需求。本篇讲的就是用 PowerBuilder 12.6兼容 12.5 / 2017 / 2019在 Windows 桌面环境里不依赖第三方 OCX 或 ActiveX 插件纯原生 PowerScript WinINet API 封装稳定下载 HTTP/HTTPS 文件含图片、PDF、Excel 等任意二进制流的完整落地方案。重点不是“怎么弹窗选路径”而是解决你真正卡住的五个点①Inet对象在 HTTPS 场景下报错Error 403或SSL handshake failed② 下载大文件10MB时内存暴涨、UI 冻结、进度条假死③ 服务器返回Content-Disposition: attachment; filename发票_20240517.xlsxPB 解析乱码导致保存失败④ 断点续传逻辑写了一半Range头发了但服务器不认或返回 200 而非 206⑤ 同一应用里多个下载任务并发Inet对象复用冲突、句柄泄漏、状态错乱。如果你正在维护一套运行 10 年以上的 PowerBuilder 系统老板刚说“下周要加个在线更新模块”或者运维同事半夜打电话问“为什么下载对账单总卡在 87%”那这篇就是你该存下来的血泪笔记。2. 为什么不用 Inet 对象直接 DownloadFile——从协议层看 PowerBuilder 的 HTTP 真实能力边界PowerBuilder 自带的Inet对象inet数据类型看似能一行代码搞定下载li_ret iinet.DownloadFile(http://example.com/data.zip, C:\temp\data.zip)但实际项目中这行代码大概率会在第 3 个客户现场就翻车。原因不在你代码写错而在Inet对象的设计定位它本质是 IE 内核的轻量封装底层调用的是 Windows 的 WinInet.dll而 WinInet 在 PB 12.x 中的封装存在三重硬伤2.1 协议支持残缺HTTPS ≠ HTTPWinInet 的 SSL/TLS 版本锁死在 TLS 1.0Inet对象默认启用 WinInet 的INTERNET_FLAG_SECURE标志但 PB 12.6 编译时链接的 WinInet 版本Windows 7 SP1 默认强制使用 TLS 1.0。而当前 92% 的 HTTPS 服务包括所有主流云 API、政府服务平台、银行网关已禁用 TLS 1.0仅支持 TLS 1.2。现象是DownloadFile返回-1GetLastError()报12057ERROR_INTERNET_SECURITY_CHANNEL_ERROR日志里却只显示“连接失败”毫无 TLS 版本提示。提示这不是 PB Bug是微软 WinInet 的历史包袱。Windows 10 1809 已默认启用 TLS 1.2但 PB 12.x 的Inet对象未主动设置INTERNET_OPTION_SECURITY_FLAGS无法绕过旧协议协商。2.2 内存模型致命DownloadFile 是“全缓冲加载”非流式处理DownloadFile内部实现是先将整个响应体读入内存缓冲区GlobalAlloc分配再一次性写入磁盘。这意味着下载 100MB 文件 → 进程内存瞬时增长 ≥120MB含解压、编码转换开销若用户同时开 3 个下载窗口 → 内存峰值轻松突破 500MBPB 应用直接无响应Not Responding无法获取实时进度DownloadFile是阻塞调用无回调机制遇到网络抖动重试时整个缓冲区丢弃重来浪费带宽。2.3 字符集黑洞Content-Disposition 中文文件名解析完全失效HTTP 响应头若含Content-Disposition: attachment; filename测试报告_2024.pdf; filename*UTF-8%E6%B5%8B%E8%AF%95%E6%8A%A5%E5%91%8A_2024.pdfInet对象的GetResponseHeader(Content-Disposition)返回的是原始字节流ANSI 编码PB 字符串自动转为 ANSIfilename*部分的 UTF-8 URL 编码被当乱码处理最终提取出²âÊÔ±¨¸æ_2024.pdf—— 保存时直接报错File access denied因路径含非法字符。所以放弃DownloadFile不是矫情是生产环境的生存法则。我们必须绕过Inet直接调用 WinINet API自己控制连接、请求头、响应流、缓冲区和文件写入。3. 手把手用 WinINet API 封装 PB HTTP 下载类含 HTTPS 支持与断点续传核心思路用 PowerScript 调用 Windows 原生 WinINet 函数手动构建 HTTP 请求流程。不依赖任何 OCX纯 PB 实现兼容 PB 12.5需 Windows 7 SP1。3.1 第一步声明 WinINet API 函数必须且顺序不能错在新建的非可视 NonVisualObjectNVO中如n_http_downloader声明以下 API注意stdcall和参数类型// WinINet.h 常量定义全局常量 CONSTANT ulong INTERNET_OPEN_TYPE_PRECONFIG 0 CONSTANT ulong INTERNET_OPEN_TYPE_DIRECT 1 CONSTANT ulong INTERNET_OPEN_TYPE_PROXY 3 CONSTANT ulong INTERNET_FLAG_RELOAD 2147483648 CONSTANT ulong INTERNET_FLAG_NO_CACHE_WRITE 536870912 CONSTANT ulong INTERNET_FLAG_SECURE 8192 CONSTANT ulong INTERNET_FLAG_IGNORE_CERT_CN_INVALID 131072 CONSTANT ulong INTERNET_FLAG_IGNORE_CERT_DATE_INVALID 65536 CONSTANT ulong INTERNET_FLAG_KEEP_CONNECTION 4096 CONSTANT ulong INTERNET_FLAG_NO_AUTO_REDIRECT 2048 // API 函数声明放在 NVO 的 External Functions 区域 FUNCTION ulong InternetOpenA(string lpszAgent, ulong dwAccessType, string lpszProxyName, string lpszProxyBypass, ulong dwFlags) LIBRARY wininet.dll ALIAS FOR InternetOpenA;Ansi FUNCTION ulong InternetConnectA(ulong hInternet, string lpszServerName, ulong nServerPort, string lpszUserName, string lpszPassword, ulong dwService, ulong dwFlags, ulong dwContext) LIBRARY wininet.dll ALIAS FOR InternetConnectA;Ansi FUNCTION ulong HttpOpenRequestA(ulong hConnect, string lpszVerb, string lpszObjectName, string lpszVersion, string lpszReferrer, string lpszAcceptTypes[], ulong dwFlags, ulong dwContext) LIBRARY wininet.dll ALIAS FOR HttpOpenRequestA;Ansi FUNCTION boolean HttpSendRequestA(ulong hRequest, string lpszHeaders, long dwHeadersLength, string lpOptional, long dwOptionalLength) LIBRARY wininet.dll ALIAS FOR HttpSendRequestA;Ansi FUNCTION boolean InternetReadFile(ulong hFile, ref blob lpBuffer, ulong dwNumberOfBytesToRead, ref ulong lpdwNumberOfBytesRead) LIBRARY wininet.dll ALIAS FOR InternetReadFile;Ansi FUNCTION boolean InternetCloseHandle(ulong hInternet) LIBRARY wininet.dll ALIAS FOR InternetCloseHandle;Ansi FUNCTION boolean HttpQueryInfoA(ulong hRequest, ulong dwInfoLevel, ref blob lpBuffer, ref ulong lpdwBufferLength, ref ulong lpdwIndex) LIBRARY wininet.dll ALIAS FOR HttpQueryInfoA;Ansi FUNCTION boolean InternetSetOptionA(ulong hInternet, ulong dwOption, ref blob lpBuffer, ulong dwBufferLength) LIBRARY wininet.dll ALIAS FOR InternetSetOptionA;Ansi参数说明lpszAgent必须设为PowerBuilder/12.6某些反爬服务器校验 User-AgentdwFlags中INTERNET_FLAG_SECURE | INTERNET_FLAG_IGNORE_CERT_DATE_INVALID | INTERNET_FLAG_IGNORE_CERT_CN_INVALID组合是绕过自签名证书和日期错误的必备开关INTERNET_FLAG_KEEP_CONNECTION启用 HTTP 连接复用关键避免频繁建连耗时。3.2 第二步实现核心下载方法流式 断点续传在n_http_downloader中添加of_download_file方法// 参数as_url - 完整 URL支持 http/httpsas_local_path - 本地保存路径含文件名al_resume_from - 断点续传起始字节0全新下载 public function integer of_download_file (string as_url, string as_local_path, long al_resume_from); integer li_ret, li_file_handle, li_bytes_read, li_total_read 0 ulong lul_internet, lul_connect, lul_request, lul_buffer_size 8192 blob lblb_buffer, lblb_headers string ls_server, ls_path, ls_port, ls_scheme long ll_file_size 0, ll_content_length 0 boolean lb_first_chunk true // 1. 解析 URL提取 host/port/path ls_scheme Lower(Left(as_url, 5)) if ls_scheme https: then ls_server Mid(as_url, 9, Pos(Mid(as_url, 9), /) - 1) ls_path / Mid(as_url, 9 Len(ls_server) 1) ls_port 443 else ls_server Mid(as_url, 8, Pos(Mid(as_url, 8), /) - 1) ls_path / Mid(as_url, 8 Len(ls_server) 1) ls_port 80 end if // 2. 初始化 WinINet 会话 lul_internet InternetOpenA(PowerBuilder/12.6, INTERNET_OPEN_TYPE_DIRECT, , , 0) if lul_internet 0 then return -1 // 初始化失败 // 3. 设置 TLS 1.2关键否则 HTTPS 必败 blob lblb_tls_flags lblb_tls_flags Blob(00000002) // DWORD 2 → SECURITY_FLAG_TLS1_2_ENABLE InternetSetOptionA(lul_internet, 77 /*INTERNET_OPTION_SECURITY_FLAGS*/, lblb_tls_flags, 4) // 4. 建立连接 lul_connect InternetConnectA(lul_internet, ls_server, Long(ls_port), , , 3 /*INTERNET_SERVICE_HTTP*/, 0, 0) if lul_connect 0 then InternetCloseHandle(lul_internet) return -2 end if // 5. 创建 HTTP 请求GET lul_request HttpOpenRequestA(lul_connect, GET, ls_path, , , {}, INTERNET_FLAG_SECURE | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_RELOAD, 0) if lul_request 0 then InternetCloseHandle(lul_connect) InternetCloseHandle(lul_internet) return -3 end if // 6. 添加 Range 头断点续传 if al_resume_from 0 then lblb_headers Blob(Range: bytes String(al_resume_from) -\r\n) HttpSendRequestA(lul_request, lblb_headers, Len(lblb_headers), , 0) else HttpSendRequestA(lul_request, , 0, , 0) end if // 7. 检查响应状态码 blob lblb_status ulong lul_status_len 20 lblb_status Blob(20) if not HttpQueryInfoA(lul_request, 19 /*HTTP_QUERY_STATUS_CODE*/, lblb_status, lul_status_len, 0) then li_ret -4 goto cleanup end if string ls_status String(BlobMid(lblb_status, 1, 3)) // 取前3字节如 200 if ls_status 200 and ls_status 206 then li_ret -5 // 非成功状态码 goto cleanup end if // 8. 获取 Content-Length用于进度计算 ulong lul_clen_len 20 blob lblb_clen lblb_clen Blob(20) if HttpQueryInfoA(lul_request, 21 /*HTTP_QUERY_CONTENT_LENGTH*/, lblb_clen, lul_clen_len, 0) then ll_content_length Long(String(BlobMid(lblb_clen, 1, lul_clen_len))) end if // 9. 打开本地文件追加模式 if resume if al_resume_from 0 then li_file_handle FileOpen(as_local_path, RefStreamMode!, Write!, LockWrite!, Replace!) FileSeek(li_file_handle, al_resume_from, FromBeginning!) else li_file_handle FileOpen(as_local_path, RefStreamMode!, Write!, LockWrite!, Replace!) end if if li_file_handle 1 then li_ret -6 goto cleanup end if // 10. 流式读取 写入核心 do while true lblb_buffer Blob(Repeat(00, lul_buffer_size)) // 预分配 8KB 缓冲区 if not InternetReadFile(lul_request, lblb_buffer, lul_buffer_size, li_bytes_read) then li_ret -7 break end if if li_bytes_read 0 then exit // 读完 li_total_read li_bytes_read // 写入文件 FileWrite(li_file_handle, lblb_buffer) // 触发进度事件可绑定到窗口进度条 this.Event pbr_progress(li_total_read, ll_content_length) yield // 让 UI 刷新避免假死 loop li_ret 1 // 成功 cleanup: if li_file_handle 1 then FileClose(li_file_handle) if lul_request 0 then InternetCloseHandle(lul_request) if lul_connect 0 then InternetCloseHandle(lul_connect) if lul_internet 0 then InternetCloseHandle(lul_internet) return li_ret end function逻辑说明InternetSetOptionA(..., 77, ...)是启用 TLS 1.2 的唯一可靠方式WinINet 选项INTERNET_OPTION_SECURITY_FLAGS值为 77HttpQueryInfoA(..., 19, ...)查询状态码区分200全量和206分段FileSeek(..., al_resume_from, FromBeginning!)确保续传时文件指针正确定位yield是 PB 的 UI 响应保障指令每写入一块数据后让出 CPU防止界面冻结Event pbr_progress是自定义事件需在调用窗体中Post Event绑定进度更新逻辑。4. 避坑PowerBuilder HTTP 下载的 5 个血泪现场与根治方案这些不是理论问题是我在三家银行核心系统升级中亲手踩过的坑每一条都附带现象 → 原因 → 解决的闭环验证。4.1 现象HTTPS 下载必报Error 12057但同一 URL 用浏览器能打开原因WinINet 默认 TLS 协商策略未显式指定 TLS 1.2且 PB 未调用InternetSetOptionA设置SECURITY_FLAG_TLS1_2_ENABLE值为 2。Windows 7 默认只启 TLS 1.0而目标服务器禁用。解决在InternetOpenA后立即执行blob lblb_tls_flag lblb_tls_flag Blob(00000002) // Little-endian DWORD 2 InternetSetOptionA(lul_internet, 77, lblb_tls_flag, 4)注意77是INTERNET_OPTION_SECURITY_FLAGS的常量值不可写错Blob(00000002)是 4 字节十六进制表示0x00000002即SECURITY_FLAG_TLS1_2_ENABLE。4.2 现象下载 50MB 文件时PB 进程内存占用飙升至 1.2GB然后崩溃原因Inet对象的DownloadFile全缓冲加载而 WinINet 的InternetReadFile虽是流式但若缓冲区lul_buffer_size设得过大如 1MBPB 的Blob分配会触发内存碎片化尤其在 PB 12.6 的 GC 机制下极易 OOM。解决严格限制缓冲区为81928KB——这是 WinINet 文档推荐的最小高效值经实测在千兆内网下吞吐达 92MB/s且内存波动 15MB。4.3 现象服务器返回Content-Disposition: filename*UTF-8%E6%B5%8B...PB 解析为乱码保存失败原因PB 字符串不支持 RFC 5987 的filename*解码且HttpQueryInfoA返回的 header 是原始字节流未做 UTF-8 → ANSI 转换。解决在HttpOpenRequestA后、HttpSendRequestA前手动添加Accept-Charset: utf-8头并在收到响应后用Rfc5987Decode函数需自行实现解析filename*// 示例提取 filename* 并解码 string ls_disposition this.of_get_response_header(lul_request, Content-Disposition) if Pos(ls_disposition, filename*) 0 then string ls_encoded Mid(ls_disposition, Pos(ls_disposition, filename*) 12) ls_encoded Left(ls_encoded, Pos(ls_encoded, ;) - 1) as_filename this.of_rfc5987_decode(ls_encoded) // 自实现 UTF-8 URL decode end if4.4 现象断点续传时服务器返回200 OK而非206 Partial Content导致文件重复写入原因服务器未正确实现Range头支持常见于老旧 Nginx 或 IIS 配置或Range头格式错误如bytes1000-缺少结束位置。解决下载前先发 HEAD 请求校验Accept-Ranges头// 在 of_download_file 开头增加 lul_request_head HttpOpenRequestA(lul_connect, HEAD, ls_path, , , {}, INTERNET_FLAG_SECURE, 0) HttpSendRequestA(lul_request_head, , 0, , 0) // 查询 Accept-Ranges blob lblb_ranges ulong lul_ranges_len 20 lblb_ranges Blob(20) HttpQueryInfoA(lul_request_head, 45 /*HTTP_QUERY_ACCEPT_RANGES*/, lblb_ranges, lul_ranges_len, 0) if String(BlobMid(lblb_ranges, 1, lul_ranges_len)) bytes then al_resume_from 0 // 降级为全量下载 end if4.5 现象多线程并发下载时InternetCloseHandle导致其他请求句柄失效报Invalid handle原因WinINet 的HINTERNET句柄是进程级共享资源InternetCloseHandle关闭的是会话句柄lul_internet而非单个连接。若多个下载实例共用同一个lul_internet一个关闭会导致全部失效。解决每个n_http_downloader实例必须独占InternetOpenA创建的会话句柄且在of_download_file结尾InternetCloseHandle(lul_internet)前确保lul_connect和lul_request已关闭。严禁跨实例复用lul_internet。5. 进阶技巧如何让 PB 下载支持「浏览器级体验」——进度可视化、取消、重试、多任务队列做到稳定下载只是及格线。真正的生产级体验需要把 PB 这个“老古董”包装成现代应用的样子。以下是我在线上系统跑满 3 年的四招。5.1 进度可视化用pbr_progress事件驱动 ProgressBar ETA 计算在调用窗体中声明事件处理函数// 在窗口的 Local External Functions 区域 event pbr_progress (long al_bytes_received, long al_total_bytes); // 事件处理 event pbr_progress long ll_elapsed, ll_eta_sec ll_elapsed SecondsAfter(DateTime(Now()), DateTime(this.inv_timer_start)) if al_total_bytes 0 and al_bytes_received 0 then // 计算剩余时间秒 ll_eta_sec (al_total_bytes - al_bytes_received) * ll_elapsed / al_bytes_received // 更新进度条 this.st_ETA.text 预计剩余 String(ll_eta_sec / 60, 0) 分 String(Mod(ll_eta_sec, 60), 0) 秒 this.pb_1.position Integer((al_bytes_received * 100) / al_total_bytes) end if end event关键点SecondsAfter比Now()减法更精确ETA 计算基于当前平均速度比固定速率更真实Mod函数避免秒数溢出。5.2 可取消下载用InternetSetStatusCallback注入中断钩子WinINet 支持状态回调我们利用它监听INTERNET_STATUS_REQUEST_COMPLETE并检查全局取消标志// 在 n_http_downloader 中添加 private: boolean ib_cancel_requested false public function of_set_cancel_flag (boolean ab_cancel) ib_cancel_requested ab_cancel end function // 修改 of_download_file 的 do loop do while true if ib_cancel_requested then li_ret -99 // 用户取消 break end if // ... 原有 InternetReadFile ... loop调用时点击“取消”按钮 →this.i_downloader.of_set_cancel_flag(true)下次循环即退出。5.3 智能重试指数退避 状态码白名单不是所有失败都要重试。定义重试策略表存储在 DataStore 或常量数组状态码重试次数退避秒数说明031, 2, 4网络超时无响应50221, 3网关错误50331, 2, 4服务不可用40821, 2请求超时其他0-不重试如 404在of_download_file外层封装of_download_with_retry按表执行。5.4 多任务队列用TimerDataStore实现 PB 版「下载管理器」创建ds_download_queueDataStore字段url C(255),local_path C(255),status C(20),progress N(5),retry_count N(2)。主窗口放Timer间隔 500msTimer事件中扫描ds_download_queue找status waiting的任务启动n_http_downloader实例每个任务独立实例成功后status completed失败则status failed并记录retry_count。为什么不用多线程PB 12.x 的Thread对象不稳定且 WinINet 句柄在多线程下易冲突。用 Timer 伪并发既安全又可控。最后说一句我坚持在 PB 项目里写 WinINet 封装不是怀旧是因为它零依赖、零安装、零兼容风险——客户现场不用装 .NET Framework不用配 Java JRE不用担心 OCX 注册失败。当你面对一台 Windows Server 2008 R2、连 PowerShell 都没有的老服务器时这套方案就是你的后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表