简介面向PowerBuilder PB-183版本开发者的HTTP下载示例包聚焦C/S程序中通过内置Internet Toolkit或第三方库实现文件与图片的远程获取覆盖状态码校验、超时设置、请求头配置、异常处理、下载进度与本地落盘等完整链路。压缩包共23个文件大小仅50KB包含pbt/pbl/pbw工程与源码、pbd/exe编译产物、cs辅助脚本、config配置、log日志等其中工程与源码便于直接打开修改exe可快速验证效果日志则记录了构建与迁移过程整体目录结构清晰。已有674人浏览学习。借助该示例可掌握HTTPClient对象与DLL调用两种下载方式理解将响应流保存为本地文件或直接加载到Image控件显示图片的差异并能参考其错误处理和断点续传等扩展思路适合需要为PB应用增添在线资源获取能力的初中级开发人员直接借鉴与二次开发。1. PowerBuilder 的 HTTP 下载为什么一个老需求能难住一批人先说明一下标题里的 PB 是 PowerBuilder不是 TensorFlow 的 pb 模型文件。搜索“PB下载HTTP文件”“pb http下载图片”的人十有八九是在维护老系统EAS 上跑着 PB 9 或 PB 12.5 写的数据采集程序要从内网服务器拉报表、拉图片、拉加密包或者对接一个只给 HTTP 接口的新平台。PowerBuilder 这套老框架里没有内置的 HttpClient很多版本连 URLDownloadToFile 都调不明白于是大家只能去网上找现成的 rar 工具包比如标题里这种“PB-183下载”的资源包。这类包解压后通常是几个 DLL 加一段示例代码版本不匹配时照样跑不起来。这篇把一条我自己验证过、能直接照抄的路径讲透先用 WinHttp 组件做 OLE 调用配合 ADODB.Stream 处理二进制把文本、图片、压缩包的下载都跑通再把超时、文件名、HTTPS 证书这些高频坑一个个排掉。适合正在维护老 PB 项目、或者要把 PowerBuilder 接进 HTTP 接口体系的工程师照步骤走完你可以在自己代码里写一个稳定的下载函数不再依赖来路不明的第三方包。2. 三条路先想清楚PB 里调 HTTP 的选型决定了你后面少踩多少坑2.1 OLE 组件、API 直调、.NET 封装各自适用什么人PowerBuilder 里做 HTTP 下载常见做法无非三条路用 OLEObject 调 WinHttp 组件、用外部函数直接声明 WinHTTP API、以及 PB 12.2 以上调用 .NET WebClient。我给老项目做技术方案时默认优先 OLE 方案理由很直白代码量少运行依赖少Windows 7 及以上系统都自带 WinHttp.dll不需要安装额外运行时。直接声明 WinHTTP API 的路线适合对请求过程控制要求极高的场景比如要自己处理连接复用、要拿到 TLS 握手细节、要做底层流量统计。代价是代码量大PB 里声明 ref char、ref long 这类参数很容易出类型错配调试起来像在跟黑匣子搏斗。.NET WebClient 的方案在新版本 PB 里确实一行代码就能下载但有个现实问题老补丁包环境下运行时加载程序集经常失败。我手头一个客户环境就是 PB-183 这种老 build跑 WebClient 时断时续地报 System.IO.FileNotFoundException最后统一退回 WinHttp世界安静了。所以选型结论不复杂如果你的 PB 版本在 9 到 12.5 之间直接走 OLE WinHttp如果版本新、运行环境干净、且你能确认 .NET 运行时没问题再考虑 WebClient。新手别一上来就抄 API 直调的代码很容易被指针和缓冲区绕晕。2.2 为什么不推荐 MSXML2.XMLHTTP以及 WinHttp 的优势在搜索“pb http”的时候很多老帖会推荐用 MSXML2.XMLHTTP 组件。这个组件确实能做 HTTP 请求但实际用下来有两个烦心事第一MSXML 在系统里有 3.0、4.0、6.0 多个版本共存Office 或系统更新可能改变默认版本代码里写死 “MSXML2.XMLHTTP” 可能在升级后连不上第二有些精简版系统或离线内网机器没有正确注册 MSXMLConnectToNewObject 直接返回 -1。WinHttp.WinHttpRequest.5.1 没这么多幺蛾子。它从 Windows 7 开始就是系统组件不依赖 Office也不会被其他软件随意改版本行为上接近 WinINet但更适合服务端和后台任务重定向、代理、HTTPS 证书都有独立可控的属性。用它做 PB 下载遇到最多的问题反而是 PB 自身对 VARIANT 字节数组的处理限制这个下文细说。顺带提一句很多朋友搜索时看到“winform之http客户端实现”其实思路是相通的不管外面包的是 WinForm 还是 PB底层都是同一个 WinHTTP 协议栈理解了组件调用方式换语言只是换壳。2.3 动手之前先分清两件事文本和二进制、GET 和 POST这是很多人翻车的第一步。HTTP 下载文本和下载图片、压缩包看似都是“发请求、收数据”代码结构却完全不同。文本可以走 ResponseText拿到的是字符串图片和 zip 必须走 ResponseBody拿到的是字节数组PB 里不能直接把 ResponseBody 赋给 blob 变量常见做法是把它作为参数传给 ADODB.Stream 的 Write 方法由 Stream 完成二进制落地。这个坑在下面章节的具体代码里会反复强调。另外要分清楚请求方法。下载文件 90% 是 GET但有些内部系统把下载地址包装成了 POST 接口参数放在请求体里响应体里返回文件流。WinHttp 的 Open 方法第一个参数就是方法名写 GET 还是 POST会直接影响服务端的处理逻辑。实现下载函数时最好把请求方法也做成参数别写死。3. 用 WinHttp 跑通最小下载文本文件、图片的完整代码3.1 先验证组件ConnectToNewObject 的返回值不是摆设写代码之前先确认运行环境能创建 WinHttp 对象。这个检查不是走形式我遇到过不止一次程序在一台机器上跑得好好的换到离线内网机就报错排查半天才发现是组件创建失败。下面的代码放在下载函数入口处作为第一道防线// 创建 OLEObject 对象 OLEObject lole_http lole_http CREATE OLEObject // 连接 WinHttp 组件返回值为 0 表示成功 IF lole_http.ConnectToNewObject(WinHttp.WinHttpRequest.5.1) 0 THEN DESTROY lole_http MessageBox(提示, 当前系统不支持 WinHttp 组件无法执行下载) RETURN false END IF这里的关键是 ConnectToNewObject 的返回值0 代表成功非 0 代表创建失败。失败可能的原因包括系统精简掉了 WinHttp.dll、组件未注册、或者当前进程是 64 位而组件注册在 32 位分支里。判断失败后要立即释放对象避免内存占用。这一步过了后面的事才谈得上。3.2 文本文件下载ResponseText 到 UTF-8 文件文本下载的最小完整函数如下方代码。这个函数接收 URL 和保存路径成功返回文件内容字符串失败返回空串// 函数of_download_text // 入参as_url 完整HTTP地址as_save_path 本地保存路径 // 返回文件内容字符串失败为空串 string ls_text OLEObject lole_http, lole_stream lole_http CREATE OLEObject IF lole_http.ConnectToNewObject(WinHttp.WinHttpRequest.5.1) 0 THEN DESTROY lole_http RETURN END IF // SetTimeouts 四个参数依次是解析超时、连接超时、发送超时、接收超时单位毫秒 lole_http.SetTimeouts(5000, 5000, 10000, 30000) lole_http.Open(GET, as_url, false) // 部分服务器对空 User-Agent 直接返回 403这里伪装成浏览器 lole_http.SetRequestHeader(User-Agent, Mozilla/5.0) lole_http.Send() // Status 为 HTTP 状态码200 才继续302 由 WinHttp 自动跟随 IF lole_http.Status 200 THEN DESTROY lole_http RETURN END IF // 取文本内容 ls_text lole_http.ResponseText DESTROY lole_http // 保存为 UTF-8 文件用 ADODB.Stream 保持编码一致 lole_stream CREATE OLEObject IF lole_stream.ConnectToNewObject(ADODB.Stream) 0 THEN lole_stream.Type 2 // adTypeText文本模式 lole_stream.Charset UTF-8 // 指定编码 lole_stream.Open() lole_stream.WriteText(ls_text) lole_stream.SaveToFile(as_save_path, 2) // 2 adSaveCreateOverWrite lole_stream.Close() END IF DESTROY lole_stream RETURN ls_text逻辑说明很直接先创建 WinHttp设置超时发 GET校验状态码再取文本。这里要注意两个参数SetTimeouts 的第二个值 5000 是连接超时如果服务器在内网但路由不通这个值决定了程序卡多久才报错SaveToFile 的第二个参数 2 表示覆盖写如果你希望文件已存在时不覆盖要改成 1adSaveCreateNotExist。还有一个容易忽略的点直接 FileWrite 保存文本会丢失编码信息尤其服务端返回 UTF-8 而 PB 字符串内部是 ANSI 时写出来的文件在浏览器里打开就是乱码。用 ADODB.Stream 指定 Charset 再落盘是目前我试过最稳的做法。3.3 图片下载ResponseBody 配合 ADODB.Stream 落盘下载图片或二进制文件的函数如下。核心差异在第 20 行附近不再取 ResponseText而是把 ResponseBody 交给 Stream 的 Write 方法// 函数of_download_binary // 入参as_url 文件地址as_save_path 保存路径 // 返回boolean是否成功 boolean lb_ok OLEObject lole_http, lole_stream long ll_status lole_http CREATE OLEObject lb_ok (lole_http.ConnectToNewObject(WinHttp.WinHttpRequest.5.1) 0) IF NOT lb_ok THEN DESTROY lole_http RETURN false END IF lole_http.SetTimeouts(5000, 5000, 10000, 60000) lole_http.Open(GET, as_url, false) lole_http.SetRequestHeader(User-Agent, Mozilla/5.0) lole_http.Send() ll_status lole_http.Status IF ll_status 200 THEN DESTROY lole_http RETURN false END IF lole_stream CREATE OLEObject lb_ok (lole_stream.ConnectToNewObject(ADODB.Stream) 0) IF NOT lb_ok THEN DESTROY lole_http DESTROY lole_stream RETURN false END IF // Type1 表示二进制模式Mode3 表示可读可写 lole_stream.Type 1 lole_stream.Mode 3 lole_stream.Open() // ResponseBody 是 VARIANT 字节数组不要赋给 blob直接交给 Stream lole_stream.Write(lole_http.ResponseBody) // 保存到文件2 表示覆盖已有文件 lole_stream.SaveToFile(as_save_path, 2) lole_stream.Close() DESTROY lole_http DESTROY lole_stream RETURN true参数说明lole_stream.Type 1 是关键这是二进制流如果漏了这句图片写出来会变成文本内容。Mode 3 表示读和写都允许有的环境默认 Mode 不对写入时会报“对象关闭时不允许操作”。ResponseBody 的处理逻辑要重点理解PB 的 OLEObject 对 VARIANT 类型的字节数组支持有限直接写blob lblb lole_http.ResponseBody在 PB 9 到 12.5 上大概率报类型转换错误。绕开的方法是让 Stream 的 Write 方法来接收它本身就是设计来吃 VARIANT 的。图片下载成功后建议立刻做一次文件头校验比如 JPEG 的 FF D8 FFPNG 的 89 50 4E 47。这个校验可以帮助区分“下载成功”和“下载成功但是个错误页”后面避坑章会细说。4. 下载场景里最容易看走眼的细节ContentType、文件名和超时4.1 ContentType 不等于文件类型HTTP 下载的响应头别乱信很多人在写下载逻辑时喜欢先读 ContentType 再决定怎么保存比如看到image/jpeg就加 .jpg 后缀。这个思路在浏览器里没问题在 PB 下载场景里经常翻车。原因很简单第一部分内网服务器是拿静态文件服务软件搭的会把所有文件都标成application/octet-stream你根本从 ContentType 判断不出真实类型第二有些接口是程序生成的下载流比如报表系统导出 PDF 时ContentType 写的是text/html但内容实际上是个 PDF 二进制流。正确做法是不要依赖 ContentType 决定文件类型而是依赖 URL 后缀和 Content-Disposition 头的 filename 字段。ContentType 可以打印到日志里做参考但不能作为唯一判断依据。在 PB 里获取响应头的代码是string ls_content_type ls_content_type lole_http.GetResponseHeader(Content-Type) // 只做日志记录用不要据此判断文件格式另一点要注意如果服务端返回的是application/json; charsetutf-8这种带参数的 ContentTypePB 字符串解析时别只找application/json用 Pos 函数匹配到;之前的部分再判断。4.2 文件名怎么取Content-Disposition 和 URL 的兜底解析下载保存到本地时文件名来源有三个优先级Content-Disposition 的 filename 字段最权威其次 URL 最后一段最后是调用方显式传入。Content-Disposition 常见格式有两种attachment; filenamereport.pdf attachment; filename*UTF-8%E6%9C%88%E6%8A%A5.pdf第一种是普通 ASCII 文件名第二种是 RFC 5987 的编码格式其中%E6%9C%88%E6%8A%A5是 UTF-8 编码后的“月报”。PB 里没有内置的 URLDecode 函数需要自己写解析先用Pos找到filename*再找到之后的内容然后循环把%xx转成字符。伪代码如下// 取 Content-Disposition 头 string ls_disposition ls_disposition lole_http.GetResponseHeader(Content-Disposition) // 若包含 filename*取单引号后面的部分 IF Pos(ls_disposition, filename*) 0 THEN ls_disposition Mid(ls_disposition, Pos(ls_disposition, ) 2) // 此处还需要将 %E6%9C%88 之类的编码转成真实字符 END IF实操里有个捷径如果下载地址是内网系统生成的文件URL 里通常已经带了原始文件名比如/download?id123namereport.pdf直接用正则从 URL 截取 name 参数最省事。我的习惯是优先取 Content-Disposition取不到就取 URL 截取都取不到才落到一层默认命名规则。文件名解析失败时千万别用时间戳命名否则用户根本不知道下载的是什么。4.3 超时参数怎么设以及 HTTP 连接复用为什么影响下载速度SetTimeouts 四个参数里接收超时是下载大文件的关键。默认 30 秒对几 MB 的图片够用下载几十 MB 的压缩包时如果接收超时设得太短下载中途会报 12002 超时错误。我的经验值文本类接口连接超时 5 秒、接收超时 30 秒图片和压缩包下载接收超时至少放宽到 60 秒甚至 120 秒。网络不好时单位时间下载量小超时时间要按文件大小除以最低期望速率来估算。HTTP 连接复用是另一个影响下载速度的隐藏点。WinHttp.WinHttpRequest 每次新建组件实例、执行完 Send 后连接默认是断开的。如果循环下载 100 个小文件每次都重新创建对象抓包看就是持续不断的三次握手和挥手整体时间会被连接建立耗掉一大截。优化做法是把 WinHttp 对象做成窗口级实例在 Main 窗口定义的实例变量循环里重复使用只改变 URL减少连接重建的开销。能显著改善批量下载场景的耗时特别是下载小图标、小附件这种高频低数据量的任务。5. 这几个坑我基本都踩过PB 下载 HTTP 文件的常见问题与排查5.1 现象ConnectToNewObject 返回 -1WinHttp 组件在离线内网机上失效程序在开发机正常部署到客户内网机器后第一次调用下载就弹“当前系统不支持 WinHttp 组件”。原因多半不是系统精简掉了 WinHttp而是目标机器是 Windows Server Core 或国产化系统定制版缺少组件注册信息。解决先检查C:\Windows\SysWOW64\winhttp.dll是否存在确认文件在的情况下不要尝试regsvr32 winhttp.dllWinHttp 不是可注册的 COM 组件。更快的兜底方案是改用 MSXML6.0 创建对象把组件名换成MSXML2.ServerXMLHTTP.6.0代码其余部分不用动。我一般会在公共函数里做两层降级先连 WinHttp失败自动尝试 ServerXMLHTTP再不行才报错。5.2 现象图片下载下来无法打开文件大小比浏览器里看到的大用浏览器访问同一张图片只有 20KBPB 下载保存后却有 80KB而且画图工具打不开。原因大概率是服务端返回了错误页或登录跳转页HTTP 状态码虽然 200但内容不是图片。另一个常见情况是把 ResponseText 硬转成 blob 写文件导致 UTF-8 文本被按 ANSI 存储文件字节数膨胀。解决下载后立刻比对 Content-Length 与本地文件大小不相等就删除重下同时对文件头做魔数校验JPEG 应以FF D8 FF开头。我惯用的写法是下载后读文件前两个字节用FileRead或者 API 读入判断魔数。校验失败时建议打印响应头全文到日志能快速看出服务端返回的到底是什么。5.3 现象HTTPS 地址报 12175 证书错误而浏览器能正常打开HTTP 下载正常换 HTTPS 就报 12175 或 12038 证书错误。原因通常是服务器证书链不完整或者内部系统用自签名证书WinHttp 组件默认要求完整证书链校验不信任自签名证书。处理时不要为了省事关闭证书校验尤其是生产环境。正确的做法有两步第一步把服务器根证书导入 Windows 受信任证书存储区用certmgr.msc操作这个最干净第二步如果系统里有多级证书链且中间证书缺失找网络管理员补全。只在联调阶段可以临时用 WinHttp.SetOption 调整安全选项跳过校验上线前必须改回。5.4 现象中文文件名保存后全是乱码保存文件名里带中文落盘后变成“锟斤拷”或问号。原因在 URL 编码和本地编码不一致HTTP 路径中的中文通常按 UTF-8 编码PB 字符串内部是 GBK 或 ANSI直接拼接后文件系统按 ANSI 解析乱码是必然。解决思路文件名拆成编码前和解码后两步URL 里的%xx先还原成 UTF-8 字节再转成 GBKContent-Disposition 里的filename*UTF-8也按同样方式处理。转码函数在 PB 里可以利用System.Text.Encoding相关调用老版本 PB 则借助 API 或 ADODB.Stream 做中转。我的习惯是尽量在服务端把文件名改成 ASCII实在要支持中文文件名解析逻辑里就做两层兼容先试 UTF-8 解码失败再试 GBK。5.5 现象大批量下载越跑越慢抓包发现连接一直在重建下载 100 个文件时前 10 个速度正常后面每个都要等 1 到 2 秒才开始。用抓包工具看每个文件请求前都有完整的 TCP 握手和挥手。原因就是前面讲的连接复用没做每次下载都CREATE新的 OLEObject下载完就DESTROY连接自然无法复用。解决把 WinHttp 对象提升为窗口或应用的实例变量下载循环里只调用Open和Send不要反复创建销毁如果目标服务器支持 HTTP/1.1 keep-alive这样做了之后批量下载耗时能下降一半。另一个关联问题并发下载时开太多线程会触发服务器连接数限制表现为部分请求返回 503建议控制并发数在 5 以内。6. 再进一步断点续传、完整性校验和验证下载结果的三个手段6.1 Range 头实现断点续传的代码骨架大文件下载中断后重新下载整个文件浪费带宽。WinHttp 支持 Range 头可以让服务端只返回文件剩余部分// in 参数 ll_start 表示已下载的字节数 lole_http.Open(GET, as_url, false) lole_http.SetRequestHeader(Range, bytes String(ll_start) -) lole_http.Send() // 服务端返回 206 Partial Content 才说明续传生效 IF lole_http.Status 206 THEN // 打开本地文件流把位置定位到末尾再写入后续内容 END IF这里注意两点一是断点续传要配合文件追加写入用 ADODB.Stream 打开文件后要调整 Position 到文件末尾二是服务端不支持 Range 时返回 200 和完整内容代码要有兜底逻辑直接覆盖写。6.2 用 Content-Length 校验下载完整性下载完成后从响应头取 Content-Length和本地文件实际大小对比。一致基本说明数据完整如果不一致说明传输中途出了问题但 WinHttp 的流式接口可能没报错。校验逻辑放下载函数内部失败时自动重试一次避免调用方在不知道的情况下拿到残缺文件。6.3 收尾建议封装、日志与抓包验证落地时把下载函数统一封装成of_http_download(url, save_path, method, timeout, ref file_size)的结构返回成功失败之外把文件大小、状态码、耗时都填出来。日志里记录 URL、HTTP 状态码、Content-Type、Content-Length、耗时这五个字段问题发生后基本不用猜。调通之后再用 Fiddler 或 Wireshark 抓一次包确认请求头里有预期的 User-Agent 和 Range响应码符合预期。多唠叨一句我曾经在下载函数里图省事跳过校验结果生产环境把一份残缺的 Excel 报表写进了业务库事后排查了两天才定位到是传输不完整。从那以后我的下载函数里永远把完整性和状态码校验放在第一步。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?