AHK v1 做网络请求时,很多脚本会直接写一段 WinHttp.WinHttpRequest.5.1,简单 GET 确实够用,但一碰到请求头、POST、超时、代理、二进制保存或异步进度,代码很快就会散得到处都是。
我整理这个 WinHttpRequest_v1 封装,主要是把这些常用能力收进同一个类里。它面向 AutoHotkey v1,底层仍然使用 Windows 自带的 WinHTTP COM 组件,不需要另外安装浏览器驱动,也不依赖第三方 DLL。
它能处理哪些网络任务
基础请求支持 GET、HEAD、POST 和自定义方法,可以设置请求头、超时、代理、凭据、客户端证书与 WinHTTP 选项。响应部分可以读取状态码、状态文本、响应头、文本正文和二进制正文,也可以直接把 ResponseBody 保存成文件。
库还封装了 OnResponseStart、OnResponseDataAvailable、OnResponseFinished 和 OnError 四个异步事件。下载较大文件时,可以在数据到达时分块写入,而不是等全部响应进入内存后再保存。
同步请求怎么写
同步请求适合接口数据、小文件和必须拿到结果后才能继续的流程。创建对象后调用 Open()、设置请求头,再执行 Send() 即可;也可以使用库中的 request() 快捷方法。网络请求一定要配超时和异常处理,不能默认远端服务永远可用。
如果返回的是文本,就读取 ResponseText;图片、压缩包和安装包这类二进制内容,应使用 ResponseBody 或 SaveResponseBody(),不要把二进制数据当字符串处理。
异步事件解决什么问题
同步下载期间,当前线程会一直等待。异步模式通过 Open("GET", url, true) 启动请求,数据分块到达时触发回调,脚本可以更新 GUI、进度条或 ToolTip,也可以保留中止按钮。
异步不是只把第三个参数改成 true 就结束了。文件句柄要在完成或失败时关闭,失败和中止后要删除未完成文件,事件回调也要清空,避免对象之间互相引用。下面两个示例都把这部分收尾流程写完整了。
代理、证书和安全选项
SetProxy() 可以切换系统预配置、直连或指定代理;SetCredentials() 用于服务器或代理凭据;客户端证书可以通过 SetClientCertificate() 设置。库也保留了 WinHTTP 的 Option、SecureProtocol 和 SslErrorFlag 常量。
证书错误忽略选项只适合受控测试环境。我不建议为了让请求“先跑起来”就长期忽略未知 CA、域名不匹配或证书过期。正式脚本还应检查 HTTP 状态码、响应类型和保存文件大小,再决定是否继续处理。
示例代码:WinHttpRequest_v1_异步下载_节流ToolTip进度示例.ahk
这个示例把异步下载单独抽出来,并对进度提示做了节流。素材中的默认 URL 指向 QQ 安装包,文件较大;实际测试前建议先替换成自己信任的小文件 URL,同时修改保存文件名,确认流程无误后再用于正式下载。
示例代码:WinHttpRequest_v1_综合功能示例.ahk
第二个 GUI 示例覆盖 GET、HEAD、POST JSON、同步保存、异步保存和中止请求。默认测试地址使用 httpbin.org,运行时需要正常联网;公共测试服务偶尔会限流或不可用,失败时先换成自己确认可访问的接口。
库源码:WinHttpRequest_v1.ahk
#Requires AutoHotkey v1.1
; @file: WinHttpRequest.ahk
; @description: 网络请求库
; @author thqby (Original v2) / dbgba (Ported to v1)
; @date 2021/08/01
; @version 0.0.18
; @note Converted for AutoHotkey v1 compatibility by dbgba.
; Original source: https://github.com/thqby/ahk2_lib
WinHttpRequest_v1_State() {
static state
if (!IsObject(state)) {
state := Object()
state.EventVTable := 0
state.EventCallbacks := Object()
state.EventRequests := Object()
}
return state
}
WinHttpRequest_v1_ObjPtr(obj) {
return &obj
}
WinHttpRequest_v1_PtrKey(ptr) {
ptr += 0
return ptr ? "p:" ptr : ""
}
WinHttpRequest_v1_RegisterSinkRequest(pThis, req) {
local table, key
if (!pThis || !IsObject(req))
return 0
table := WinHttpRequest_v1_State().EventRequests
key := WinHttpRequest_v1_PtrKey(pThis)
table[key] := req
return 1
}
WinHttpRequest_v1_UnregisterSinkRequest(pThis) {
local table, key
if (!pThis)
return 0
table := WinHttpRequest_v1_State().EventRequests
key := WinHttpRequest_v1_PtrKey(pThis)
if (table.HasKey(key))
table.Delete(key)
return 1
}
WinHttpRequest_v1_RequestFromSink(pThis) {
local table, key
if (!pThis)
return ""
table := WinHttpRequest_v1_State().EventRequests
key := WinHttpRequest_v1_PtrKey(pThis)
if (table.HasKey(key))
return table[key]
return ""
}
WinHttpRequest_v1_QueryInterface(pThis, riid, ppvObject) {
pHolder := NumGet(pThis + 0, A_PtrSize * 2, "Ptr")
NumPut(pThis, ppvObject + 0, 0, "Ptr")
return ObjAddRef(pHolder), 0
}
WinHttpRequest_v1_AddRef(pThis) {
pHolder := NumGet(pThis + 0, A_PtrSize * 2, "Ptr")
return ObjAddRef(pHolder)
}
WinHttpRequest_v1_Release(pThis) {
pHolder := NumGet(pThis + 0, A_PtrSize * 2, "Ptr")
return ObjRelease(pHolder)
}
WinHttpRequest_v1_OnResponseStart(pThis, status, pContentType) {
req := WinHttpRequest_v1_RequestFromSink(pThis)
if (!IsObject(req))
return 0
req.readyState := 2
try req._CallEvent("OnResponseStart", status, StrGet(pContentType, "UTF-16"))
return 0
}
WinHttpRequest_v1_OnResponseDataAvailable(pThis, pData) {
req := WinHttpRequest_v1_RequestFromSink(pThis)
if (!IsObject(req))
return 0
req.readyState := 3
pSafeArray := NumGet(pData + 0, 0, "Ptr")
pvData := NumGet(pSafeArray + 8 + A_PtrSize, 0, "Ptr")
cbElements := NumGet(pSafeArray + 8 + A_PtrSize * 2, 0, "UInt")
try req._CallEvent("OnResponseDataAvailable", pvData, cbElements)
return 0
}
WinHttpRequest_v1_OnResponseFinished(pThis) {
req := WinHttpRequest_v1_RequestFromSink(pThis)
if (!IsObject(req))
return 0
req.readyState := 4
WinHttpRequest_v1_UnregisterSinkRequest(pThis)
try
req._CallEvent("OnResponseFinished")
catch e
{
}
req._ref := 0
return 0
}
WinHttpRequest_v1_OnError(pThis, errCode, pErrDesc) {
req := WinHttpRequest_v1_RequestFromSink(pThis)
if (!IsObject(req))
return 0
req.readyState := 0
WinHttpRequest_v1_UnregisterSinkRequest(pThis)
try
req._CallEvent("OnError", errCode, StrGet(pErrDesc, "UTF-16"))
catch e
{
}
req._ref := 0
return 0
}
class WinHttpRequest {
static AutoLogonPolicy := {Always: 0, OnlyIfBypassProxy: 1, Never: 2}
static Option := {UserAgentString: 0, URL: 1, URLCodePage: 2, EscapePercentInURL: 3, SslErrorIgnoreFlags: 4, SelectCertificate: 5, EnableRedirects: 6, UrlEscapeDisable: 7, UrlEscapeDisableQuery: 8, SecureProtocols: 9, EnableTracing: 10, RevertImpersonationOverSsl: 11, EnableHttpsToHttpRedirects: 12, EnablePassportAuthentication: 13, MaxAutomaticRedirects: 14, MaxResponseHeaderSize: 15, MaxResponseDrainSize: 16, EnableHttp1_1: 17, EnableCertificateRevocationCheck: 18, RejectUserpwd: 19}
static PROXYSETTING := {PRECONFIG: 0, DIRECT: 1, PROXY: 2}
static SETCREDENTIALSFLAG := {SERVER: 0, PROXY: 1}
static SecureProtocol := {SSL2: 0x08, SSL3: 0x20, TLS1: 0x80, TLS1_1: 0x200, TLS1_2: 0x800, All: 0xA8}
static SslErrorFlag := {UnknownCA: 0x0100, CertWrongUsage: 0x0200, CertCNInvalid: 0x1000, CertDateInvalid: 0x2000, Ignore_All: 0x3300}
__New(UserAgent := "") {
this.whr := ComObjCreate("WinHttp.WinHttpRequest.5.1")
if (UserAgent = "")
UserAgent := "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36 Edg/89.0.774.68"
this.whr.Option[0] := UserAgent
this.readyState := 0
this.OnError := 0
this.OnResponseDataAvailable := 0
this.OnResponseStart := 0
this.OnResponseFinished := 0
this._ievents := 0
this._ref := 0
this.Option := new WinHttpRequest.OptionAccessor(this)
}
__Delete() {
this.enableRequestEvents(false)
}
request(url, method := "GET", post_data := "", headers := "") {
this.Open(method, url)
if IsObject(headers) {
for k, v in headers
this.SetRequestHeader(k, v)
}
if (post_data = "")
this.Send()
else
this.Send(post_data)
return this.ResponseText
}
enableRequestEvents(Enable := true) {
if (!Enable) {
if IsObject(this._ievents)
this._ievents.Disconnect()
this._ievents := 0
this._ref := 0
return
}
if IsObject(this._ievents)
return
this._ievents := new WinHttpRequest.EventSink(this)
this._ievents.Connect(this.whr)
}
SetProxy(ProxySetting, ProxyServer := "", BypassList := "") {
return this.whr.SetProxy(ProxySetting, ProxyServer, BypassList)
}
SetCredentials(UserName, Password, Flags) {
return this.whr.SetCredentials(UserName, Password, Flags)
}
SetRequestHeader(Header, Value) {
return this.whr.SetRequestHeader(Header, Value)
}
GetResponseHeader(Header) {
return this.whr.GetResponseHeader(Header)
}
GetAllResponseHeaders() {
return this.whr.GetAllResponseHeaders()
}
Send(Body*) {
if (this._HasEventHandlers())
this.enableRequestEvents(true)
if IsObject(this._ievents)
{
this._ievents.RegisterRequest(this)
this._ref := this
}
if (Body.MaxIndex())
return this.whr.Send(Body[1])
return this.whr.Send()
}
Open(verb, url, async := false) {
this.readyState := 0
this.whr.Open(verb, url, async)
this.readyState := 1
}
WaitForResponse(Timeout := -1) {
return this.whr.WaitForResponse(Timeout)
}
Abort() {
local ret, err
this.readyState := 0
try
ret := this.whr.Abort()
catch e
err := e
this.enableRequestEvents(false)
if (IsObject(err))
throw err
return ret
}
SetTimeouts(ResolveTimeout := 0, ConnectTimeout := 60000, SendTimeout := 30000, ReceiveTimeout := 30000) {
return this.whr.SetTimeouts(ResolveTimeout, ConnectTimeout, SendTimeout, ReceiveTimeout)
}
SetClientCertificate(ClientCertificate) {
return this.whr.SetClientCertificate(ClientCertificate)
}
SetAutoLogonPolicy(AutoLogonPolicy) {
return this.whr.SetAutoLogonPolicy(AutoLogonPolicy)
}
GetResponseBodyInfo(ByRef pvData, ByRef cbElements) {
body := this.whr.ResponseBody
this._lastResponseBody := body
return WinHttpRequest._SafeArrayInfo(body, pvData, cbElements)
}
SaveResponseBody(Filename, Overwrite := true) {
if (Overwrite && FileExist(Filename))
FileDelete, %Filename%
if !(file := FileOpen(Filename, "w"))
throw Exception("Failed to open file for writing.", -1, Filename)
try {
this.GetResponseBodyInfo(pvData, cbElements)
if (cbElements)
file.RawWrite(pvData + 0, cbElements)
} catch e {
file.Close()
throw e
}
file.Close()
return cbElements
}
_CallEvent(name, args*) {
handler := this[name]
if IsObject(handler)
return handler.Call(this, args*)
if (handler != "" && IsFunc(handler))
return %handler%(this, args*)
}
_HasEventHandlers() {
return IsObject(this.OnError) || IsObject(this.OnResponseDataAvailable) || IsObject(this.OnResponseStart) || IsObject(this.OnResponseFinished)
|| (this.OnError != "" && IsFunc(this.OnError))
|| (this.OnResponseDataAvailable != "" && IsFunc(this.OnResponseDataAvailable))
|| (this.OnResponseStart != "" && IsFunc(this.OnResponseStart))
|| (this.OnResponseFinished != "" && IsFunc(this.OnResponseFinished))
}
Status {
get {
return this.whr.Status
}
}
StatusText {
get {
return this.whr.StatusText
}
}
ResponseText {
get {
return this.whr.ResponseText
}
}
ResponseBody {
get {
return this.whr.ResponseBody
}
}
ResponseStream {
get {
return this.whr.ResponseStream
}
}
Headers {
get {
m := {}
all := this.GetAllResponseHeaders()
Loop, Parse, all, `n, `r
{
if (p := InStr(A_LoopField, ":")) {
key := SubStr(A_LoopField, 1, p - 1)
m[key] .= LTrim(SubStr(A_LoopField, p + 1))
}
}
return m
}
}
_SafeArrayInfo(ByRef body, ByRef pvData, ByRef cbElements) {
pSafeArray := ComObjValue(body)
pvData := NumGet(pSafeArray + 8 + A_PtrSize, 0, "Ptr")
cbElements := NumGet(pSafeArray + 8 + A_PtrSize * 2, 0, "UInt")
return cbElements
}
_VTable() {
state := WinHttpRequest_v1_State()
if (state.EventVTable)
return state.EventVTable
cb := []
cb.Push(RegisterCallback("WinHttpRequest_v1_QueryInterface", "", 3))
cb.Push(RegisterCallback("WinHttpRequest_v1_AddRef", "", 1))
cb.Push(RegisterCallback("WinHttpRequest_v1_Release", "", 1))
cb.Push(RegisterCallback("WinHttpRequest_v1_OnResponseStart", "", 3))
cb.Push(RegisterCallback("WinHttpRequest_v1_OnResponseDataAvailable", "", 2))
cb.Push(RegisterCallback("WinHttpRequest_v1_OnResponseFinished", "", 1))
cb.Push(RegisterCallback("WinHttpRequest_v1_OnError", "", 3))
vt := DllCall("GlobalAlloc", "UInt", 0x40, "UPtr", A_PtrSize * cb.MaxIndex(), "Ptr")
if (!vt)
throw Exception("GlobalAlloc failed for WinHttpRequest event vtable.", -1)
Loop, % cb.MaxIndex()
NumPut(cb[A_Index], vt + 0, (A_Index - 1) * A_PtrSize, "Ptr")
state.EventCallbacks := cb
state.EventVTable := vt
return state.EventVTable
}
_ThrowHR(hr, where := "") {
if (hr < 0)
throw Exception("COM call failed: " where, -1, Format("0x{:08X}", hr & 0xFFFFFFFF))
return hr
}
class EventSink {
__New(req) {
this.SetCapacity("iface", A_PtrSize * 3)
pThis := this.GetAddress("iface")
NumPut(WinHttpRequest._VTable(), pThis + 0, 0, "Ptr")
NumPut(WinHttpRequest_v1_ObjPtr(req), pThis + 0, A_PtrSize, "Ptr")
NumPut(WinHttpRequest_v1_ObjPtr(this), pThis + 0, A_PtrSize * 2, "Ptr")
this.ptr := pThis
this.pConnectionPoint := 0
this.cookie := 0
}
__Delete() {
this.Disconnect()
}
Connect(whr) {
static IID_IConnectionPointContainer := "{B196B284-BAB4-101A-B69C-00AA00341D07}"
static IID_IWinHttpRequestEvents := "{F97F4E15-B787-4212-80D1-D380CBBF982E}"
pCPC := ComObjQuery(whr, IID_IConnectionPointContainer)
if (!pCPC)
throw Exception("Failed to query IConnectionPointContainer.")
VarSetCapacity(iidEvents, 16, 0)
DllCall("ole32\CLSIDFromString", "WStr", IID_IWinHttpRequestEvents, "Ptr", &iidEvents)
pCP := 0
fn := NumGet(NumGet(pCPC + 0, 0, "Ptr") + 4 * A_PtrSize, 0, "Ptr")
hr := DllCall(fn, "Ptr", pCPC, "Ptr", &iidEvents, "Ptr*", pCP, "Int")
ObjRelease(pCPC)
WinHttpRequest._ThrowHR(hr, "FindConnectionPoint")
cookie := 0
fn := NumGet(NumGet(pCP + 0, 0, "Ptr") + 5 * A_PtrSize, 0, "Ptr")
hr := DllCall(fn, "Ptr", pCP, "Ptr", this.ptr, "UInt*", cookie, "Int")
if (hr < 0) {
ObjRelease(pCP)
WinHttpRequest._ThrowHR(hr, "Advise")
}
this.pConnectionPoint := pCP
this.cookie := cookie
}
RegisterRequest(req) {
return WinHttpRequest_v1_RegisterSinkRequest(this.ptr, req)
}
Disconnect() {
WinHttpRequest_v1_UnregisterSinkRequest(this.ptr)
if (this.pConnectionPoint) {
if (this.cookie) {
fn := NumGet(NumGet(this.pConnectionPoint + 0, 0, "Ptr") + 6 * A_PtrSize, 0, "Ptr")
DllCall(fn, "Ptr", this.pConnectionPoint, "UInt", this.cookie, "Int")
}
ObjRelease(this.pConnectionPoint)
this.pConnectionPoint := 0
this.cookie := 0
}
}
}
class OptionAccessor {
__New(owner) {
ObjRawSet(this, "whr", owner.whr)
}
__Get(Opt) {
whr := ObjRawGet(this, "whr")
return whr.Option[Opt]
}
__Set(Opt, value) {
whr := ObjRawGet(this, "whr")
return whr.Option[Opt] := value
}
}
}
使用时要留意的边界
WinHTTP 更适合接口调用、文件下载和明确的 HTTP 通信,不负责执行网页 JavaScript,也不能代替浏览器处理验证码、复杂登录状态和页面交互。遇到这种需求,应改用浏览器控制方案,而不是不断给 HTTP 请求补模拟浏览器行为。
下载脚本不要直接信任外部提供的 URL、文件名或响应头。保存前限制目标目录,完成后校验状态码、文件大小或哈希;异步失败、中止和脚本退出时,都要关闭文件、解除回调并清理未完成文件。网络自动化是否稳定,很多时候就取决于这些收尾代码有没有认真写。

评论(0)