AHK v1 做网络请求时,很多脚本会直接写一段 WinHttp.WinHttpRequest.5.1,简单 GET 确实够用,但一碰到请求头、POST、超时、代理、二进制保存或异步进度,代码很快就会散得到处都是。

我整理这个 WinHttpRequest_v1 封装,主要是把这些常用能力收进同一个类里。它面向 AutoHotkey v1,底层仍然使用 Windows 自带的 WinHTTP COM 组件,不需要另外安装浏览器驱动,也不依赖第三方 DLL。

 

它能处理哪些网络任务

基础请求支持 GET、HEAD、POST 和自定义方法,可以设置请求头、超时、代理、凭据、客户端证书与 WinHTTP 选项。响应部分可以读取状态码、状态文本、响应头、文本正文和二进制正文,也可以直接把 ResponseBody 保存成文件。

库还封装了 OnResponseStartOnResponseDataAvailableOnResponseFinished 和 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、文件名或响应头。保存前限制目标目录,完成后校验状态码、文件大小或哈希;异步失败、中止和脚本退出时,都要关闭文件、解除回调并清理未完成文件。网络自动化是否稳定,很多时候就取决于这些收尾代码有没有认真写。

声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。