VB.NET에서 HTTP 요청을 보낼 때는 보통 HttpClient나 WebClient를 쓰지만, VBA에서 쓰던 코드를 옮겨 오거나 쿠키·리다이렉트를 세밀하게 제어해야 할 때는 WinHttp를 그대로 가져다 쓰는 경우가 많습니다. 참조 추가에서 COM의 "Microsoft WinHTTP Services, version 5.1"을 넣으면 Interop.WinHttp 어셈블리가 생기고, 그 안의 WinHttpRequest를 쓸 수 있습니다. 개체 브라우저에서 정의로 이동하면 나오는 선언을 옮겨 두고, 자주 쓰는 멤버를 정리했습니다.
인터페이스 선언 (Interop.WinHttp, Version=5.1.0.0)
<Guid("016FE2EC-B2C8-45F8-B23B-39E53A75396B")> <TypeLibTypeAttribute(4288)>
Public Interface IWinHttpRequest
<DispId(6)>
Property [Option]([Option] As WinHttpRequestOption) As Object
<DispId(10)>
ReadOnly Property ResponseBody As Object
<DispId(9)>
ReadOnly Property ResponseText As String
<DispId(8)>
ReadOnly Property StatusText As String
<DispId(7)>
ReadOnly Property Status As Integer
<DispId(11)>
ReadOnly Property ResponseStream As Object
<DispId(18)>
Sub SetAutoLogonPolicy(AutoLogonPolicy As WinHttpRequestAutoLogonPolicy)
<DispId(17)>
Sub SetClientCertificate(ClientCertificate As String)
<DispId(16)>
Sub SetTimeouts(ResolveTimeout As Integer, ConnectTimeout As Integer, SendTimeout As Integer, ReceiveTimeout As Integer)
<DispId(12)>
Sub Abort()
<DispId(5)>
Sub Send(Optional Body As Object = Nothing)
<DispId(2)>
Sub SetRequestHeader(Header As String, Value As String)
<DispId(1)>
Sub Open(Method As String, Url As String, Optional Async As Object = Nothing)
<DispId(14)>
Sub SetCredentials(UserName As String, Password As String, <ComAliasName("WinHttp.HTTPREQUEST_SETCREDENTIALS_FLAGS")> Flags As Integer)
<DispId(13)>
Sub SetProxy(<ComAliasName("WinHttp.HTTPREQUEST_PROXY_SETTING")> ProxySetting As Integer, Optional ProxyServer As Object = Nothing, Optional BypassList As Object = Nothing)
<DispId(4)>
Function GetAllResponseHeaders() As String
<DispId(3)>
Function GetResponseHeader(Header As String) As String
<DispId(15)>
Function WaitForResponse(Optional Timeout As Object = Nothing) As Boolean
End Interface
Option 속성에 넘기는 값
[Option] 속성은 아래 열거형 값을 키로 받아 여러 동작을 켜고 끕니다. VB에서 Option이 예약어라 대괄호로 감싸 쓰는 점이 처음에 낯섭니다.
<Guid("12782009-FE90-4877-9730-E5E183669B19")>
Public Enum WinHttpRequestOption
WinHttpRequestOption_UserAgentString = 0
WinHttpRequestOption_URL = 1
WinHttpRequestOption_URLCodePage = 2
WinHttpRequestOption_EscapePercentInURL = 3
WinHttpRequestOption_SslErrorIgnoreFlags = 4
WinHttpRequestOption_SelectCertificate = 5
WinHttpRequestOption_EnableRedirects = 6
WinHttpRequestOption_UrlEscapeDisable = 7
WinHttpRequestOption_UrlEscapeDisableQuery = 8
WinHttpRequestOption_SecureProtocols = 9
WinHttpRequestOption_EnableTracing = 10
WinHttpRequestOption_RevertImpersonationOverSsl = 11
WinHttpRequestOption_EnableHttpsToHttpRedirects = 12
WinHttpRequestOption_EnablePassportAuthentication = 13
WinHttpRequestOption_MaxAutomaticRedirects = 14
WinHttpRequestOption_MaxResponseHeaderSize = 15
WinHttpRequestOption_MaxResponseDrainSize = 16
WinHttpRequestOption_EnableHttp1_1 = 17
WinHttpRequestOption_EnableCertificateRevocationCheck = 18
WinHttpRequestOption_RejectUserpwd = 19
End Enum
제가 가장 자주 만지는 건 EnableRedirects(6)입니다. 로그인 후 302로 넘어가는 응답에서 쿠키를 가로채야 할 때 이 값을 False로 꺼 두고 GetResponseHeader("Location")이나 Set-Cookie를 직접 읽습니다. UserAgentString(0)은 서버가 브라우저가 아니라고 응답을 거부할 때 바꿔 주고, SslErrorIgnoreFlags(4)는 사내 테스트 서버처럼 인증서가 엉성한 곳에 붙을 때 씁니다.
메서드별로 알아 둘 점
| 멤버 | 하는 일 |
|---|---|
Open |
메서드(GET/POST 등)와 URL을 정합니다. 세 번째 인자에 True를 주면 비동기 모드 |
SetTimeouts |
이름 해석·연결·전송·수신 시간 제한을 차례로 지정. 네 값 모두 밀리초 단위 |
SetRequestHeader |
요청 헤더 이름과 값을 지정. Open 뒤, Send 앞에 호출 |
Send |
실제로 요청을 보냅니다. 인자는 선택이지만 POST라면 본문을 넣어야 함 |
WaitForResponse |
비동기로 열었을 때 응답을 다 받을 때까지 기다림 |
SetProxy |
통신에 쓸 프록시 지정: (설정 값, "주소:포트", 우회 목록) |
ResponseText |
받은 응답을 문자열로 가져옴 |
ResponseBody |
받은 응답을 바이트 배열로 가져옴 |
Abort |
진행 중인 요청을 중단 |
GetResponseHeader |
지정한 응답 헤더 하나를 가져옴. 해당 헤더가 없으면 오류 발생 |
GetAllResponseHeaders |
응답 헤더 전체를 문자열로 가져옴 |
SetTimeouts는 예를 들어 WinHttp.SetTimeouts(3000, 3000, 3000, 3000)처럼 씁니다. 순서가 꼭 Open 전이어야 하는 건 아니지만 Send 전에는 불러야 적용됩니다. 기본 수신 제한이 꽤 길어서, 응답 없는 서버에 붙으면 프로그램이 한참 멈춘 것처럼 보이니 명시적으로 줄여 두는 게 좋습니다.
GetResponseHeader가 헤더가 없을 때 빈 문자열이 아니라 예외를 던진다는 점은 꼭 기억해 두세요. 쿠키나 Location처럼 있을 수도 없을 수도 있는 헤더는 Try로 감싸거나, GetAllResponseHeaders로 전체를 받아서 문자열에서 찾는 편이 안전합니다.
SetProxy의 첫 번째 인자 값은 다음과 같습니다.
HTTPREQUEST_PROXYSETTING_DEFAULT = 0
HTTPREQUEST_PROXYSETTING_PRECONFIG = 0
HTTPREQUEST_PROXYSETTING_DIRECT = 1
HTTPREQUEST_PROXYSETTING_PROXY = 2
DEFAULT와 PRECONFIG는 같은 값(0)입니다. 프록시를 직접 지정하려면 2를 주고 두 번째 인자에 "주소:포트"를 넣으면 됩니다. Fiddler 같은 도구로 요청을 들여다볼 때 127.0.0.1:8888을 넣어 두면 디버깅이 편합니다.
간단한 사용 예
Dim req As New WinHttp.WinHttpRequest
req.Open("GET", "https://example.com/", False)
req.SetTimeouts(3000, 3000, 3000, 3000)
req.SetRequestHeader("User-Agent", "Mozilla/5.0")
req.Send()
If req.Status = 200 Then
Console.WriteLine(req.ResponseText)
Else
Console.WriteLine(req.Status & " " & req.StatusText)
End If
ResponseText는 응답 헤더의 charset을 참고해 문자열로 바꿔 주는데, 헤더에 charset이 없는 EUC-KR 페이지는 한글이 깨져 나옵니다. 그럴 때는 ResponseBody로 바이트를 받아 Encoding.GetEncoding(51949)처럼 직접 디코딩하면 됩니다(.NET Core 이상이라면 인코딩 공급자 등록이 먼저 필요합니다).
이벤트 받기
비동기로 진행 상황을 받고 싶다면 이벤트를 쓸 수 있습니다. 단, 이벤트 프로시저를 쓰려면 변수가 WithEvents로 선언되어 있어야 합니다.
Private Sub WinHttp_OnError(ByVal ErrorNumber As Long, ByVal ErrorDescription As String)
' Winhttp 통신 중 에러가 발생할 경우 발생합니다.
' ErrorNumber : 에러 번호, ErrorDescription : 에러 String
End Sub
Private Sub WinHttp_OnResponseDataAvailable(Data() As Byte)
' 데이터를 받을 때 발생합니다.
' Data() : 받은 데이터
End Sub
Private Sub WinHttp_OnResponseFinished()
' 통신이 끝날 경우 발생합니다.
End Sub
Private Sub WinHttp_OnResponseStart(ByVal Status As Long, ByVal ContentType As String)
' 통신이 시작될 경우 발생합니다.
' Status : 상태, ContentType : 콘텐츠 타입
End Sub
위 시그니처는 VBA·VB6 스타일(WinHttp_이벤트명 이름 규칙)입니다. VB.NET에서는 이름이 아니라 Handles WinHttp.OnResponseFinished처럼 Handles 절로 연결해야 이벤트가 들어오고, 이벤트는 UI 스레드가 아닌 곳에서 올라올 수 있으니 폼 컨트롤을 건드릴 때는 Invoke를 거치는 편이 안전합니다. 대량 파일을 받으면서 진행률을 보여 주는 정도가 아니라면, 동기 모드로 Send 한 번 하고 결과를 읽는 쪽이 코드도 짧고 디버깅도 쉽습니다.