异步WinHTTP

 

  图 1 会话、连接和请求

 

由于分布式编程的发展,大多数基于 Windows ® 的现今的应用程序必须能够执行 HTTP 请求。虽然 HTTP 相对简单,但现今的 HTTP 的处理却未必简单。异步处理需要缓冲大量的请求和响应、身份验证、自动代理服务器检测、持久连接等操作。当然,您可以忽略其中的许多问题,但这会影响应用程序的质量,而且模拟 HTTP 不像模拟 TCP 套接字那样简单。那么,C++ 开发人员的任务是什么?
一个常见的误解是,如果希望应用程序访问 Web,则需要使用 Microsoft ® .NET Framework。事实上,使用托管代码的开发人员仍然必须处理许多我刚刚提到的问题,而且具象状态传输 (REST) 等许多新式 Web 服务使用本机代码进行编程也很容易。
在本月的专栏中,我将介绍如何使用 Windows HTTP 服务(又称 WinHTTP)API 实现 HTTP 客户端。此外,在 Windows Vista ® 和 Windows Server ® 2008 中,WinHTTP 支持上载大于 4GB 的文件、改进了基于证书的身份验证,并且还提供了检索源 IP 地址和目标 IP 地址的功能。
WinHTTP 既可提供 C API,又可提供 COM API。最初的 C API 自然较难使用,但是稍微借助一下 C++,它就可以为构建 HTTP 客户端提供功能强大且灵活的 API。此外,它还同时提供同步编程模型和异步编程模型。我将重点介绍异步模型,原因如下:首先,在当今的并发感知领域中,并行编程非常重要。其次,在文档和培训中,通常会过多强调单线程编程,而忽略了对并行编程的介绍。或许有人认为并行编程太简单,不需要专门提供说明性指导。虽然许多开发人员不愿意使用并行编程,但是您很快就会发现它相当自然,甚至很有趣(哦,您对有趣的定义可能与我不同,但不要泄气)!

 

WinHTTP 概述
虽然在 C API 中并不明显,但 WinHTTP API 实际是在逻辑上作为三个独立对象进行建模:会话、连接和请求。初始化 WinHTTP 需要会话对象。每个应用程序只需要一个会话。会话有助于创建连接对象。您希望与之进行通信的每个 HTTP 服务器均需要一个连接对象,而连接对象也有助于创建单个请求对象。第一次发送请求后就会建立实际的网络连接。 图 1 中描述了这些对象之间的关系。一个会话可能具有多个活动连接,并且一个连接可能包含多个并发请求。
图 1 会话、连接和请求(单击图像可查看大图)
会话对象、连接对象和请求对象可由 HINTERNET 句柄表示。虽然创建这些对象时使用的函数不同,但是如果将各个对象的相应句柄传递给 WinHttpCloseHandle 函数,这些对象都会被损坏。此外,一些函数(如 WinHttpQueryOption 和 WinHttpSetOption)使用句柄查询和设置 WinHTTP 对象支持的各种不同选项。
图 2 列出了 WinHttpHandle 类,可将其用于管理 WinHTTP 句柄中所有三种类型的生存期,以及查询和设置这些句柄上的选项。因此,如果给定一个请求对象,您就可以使用 WINHTTP_OPTION_URL 选项检索其完整 URL:
DWORD length = 0;

request.QueryOption(WINHTTP_OPTION_URL, 0, length);

ASSERT(ERROR_INSUFFICIENT_BUFFER == ::GetLastError());
CString url;

COM_VERIFY(request.QueryOption(WINHTTP_OPTION_URL,
   url.GetBufferSetLength(length / sizeof(WCHAR)), length));
url.ReleaseBuffer();
class WinHttpHandle
{
public:
    WinHttpHandle() :
        m_handle(0)
    {}

    ~WinHttpHandle()
    {
        Close();
    }

    bool Attach(HINTERNET handle)
    {
        ASSERT(0 == m_h);
        m_handle = handle;
        return 0 != m_handle;
    }

    HINTERNET Detach()
    {
        HANDLE handle = m_handle;
        m_handle = 0;
        return handle;
    }

    void Close()
    {
        if (0 != m_handle)
        {   
            VERIFY(::WinHttpCloseHandle(m_handle));
            m_handle = 0;
        }
    }

    HRESULT SetOption(DWORD option,
                      const void* value,
                      DWORD length)
    {
        if (!::WinHttpSetOption(m_handle,
                                option,
                                const_cast(value),
                                length))
        {
            return HRESULT_FROM_WIN32(::GetLastError());
        }

        return S_OK;
    }

    HRESULT QueryOption(DWORD option,
                        void* value,
                        DWORD& length) const
    {
        if (!::WinHttpQueryOption(m_handle,
                                  option,
                                  value,
                                  &length))
        {
            return HRESULT_FROM_WIN32(::GetLastError());
        }

        return S_OK;
    }

    HINTERNET m_handle;
};
我在代码段中使用 COM_VERIFY 宏来明确指出函数返回必须要检查的 HRESULT 的位置。您可以使用相应的错误处理代替此操作,不论它是引发异常还是您自行返回 HRESULT 都可以。

 

会话对象
会话对象是使用 WinHttpOpen 函数创建的。第一个参数指定应用程序的可选代理字符串。接下来的三个参数指定 WinHTTP 如何解析服务器名称。这在控制是直接访问服务器还是通过代理服务器访问服务器时很有用(有关处理代理的详细信息,请参阅以下标题为“ 确定代理设置”的侧栏中的内容)。最后一个参数中包含标记,但当前只定义了其中一个标记。WINHTTP_FLAG_ASYNC 指示 WinHTTP 函数将异步执行:
HINTERNET session = ::WinHttpOpen(0, // no agent string
                                  WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                                  WINHTTP_NO_PROXY_NAME,
                                  WINHTTP_NO_PROXY_BYPASS,
                                  WINHTTP_FLAG_ASYNC);
图 3 列出了从 图 2 中的 WinHttpHandle 派生的 WinHttpSession 类,WinHttpSession 类可以简化会话对象的创建过程。WinHttpSession 使用某些默认值创建会话,在大多数情况下,这些默认值就足够了。
class WinHttpSession : public WinHttpHandle
{
public:
    HRESULT Initialize()
    {
        if (!Attach(::WinHttpOpen(0, // no agent string
                                  WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                                  WINHTTP_NO_PROXY_NAME,
                                  WINHTTP_NO_PROXY_BYPASS,
                                  WINHTTP_FLAG_ASYNC)))
        {
            return HRESULT_FROM_WIN32(::GetLastError());
        }

        return S_OK;
    }
};

 

连接对象
连接对象是使用 WinHttpConnect 函数创建的。第一个参数指定新连接所属的会话。第二个参数列出服务器的名称或 IP 地址。第三个参数指定服务器的端口号。对于此参数,您还可以使用 INTERNET_DEFAULT_PORT,这样,WinHTTP 会将端口 80 用于常规 HTTP 请求,而将端口 443 用于安全 HTTP 请求 (HTTPS):
HINTERNET connection = ::WinHttpConnect(session,
                                        L"example.com",
                                        INTERNET_DEFAULT_PORT,
                                        0); // reserved

if (0 == connection)
{
    // Call GetLastError for error information
}
图 4 中的代码显示了 WinHttpConnection 类,该类也是从 WinHttpHandle 派生而来,可以简化连接对象的创建过程。
class WinHttpConnection : public WinHttpHandle
{
public:
    HRESULT Initialize(PCWSTR serverName,
                       INTERNET_PORT portNumber,
                       const WinHttpSession& session)
    {
        if (!Attach(::WinHttpConnect(session.m_handle,
                                     serverName,
                                     portNumber,
                                     0))) // reserved
        {
            return HRESULT_FROM_WIN32(::GetLastError());
        }

        return S_OK;
    }
};
请求对象
从请求对象开始,事情就变得有趣了。请求对象是使用 WinHttpOpenRequest 函数创建的:
HINTERNET request = ::WinHttpOpenRequest(connection,
                                         0, // use GET as the verb
                                         L"/developers/",
                                         0, // use HTTP version 1.1
                                         WINHTTP_NO_REFERRER,
                                         WINHTTP_DEFAULT_ACCEPT_TYPES,
                                         0); // flags
第一个参数指定请求所属的连接。第二个参数指定请求中使用的 HTTP 谓词;如果该参数为 0,则会假定 GET 请求。第三个参数指定正在请求的资源的名称或相对路径。第四个参数指定要使用的 HTTP 协议版本;如果该参数为 0,则会假定 HTTP 版本 1.1。第五个参数指定任何引用的 URL。该参数通常为 WINHTTP_NO_REFERRER,表示无引用。倒数第二个参数指定您作为客户端将接受的媒体类型。如果该参数为 WINHTTP_DEFAULT_ACCEPT_TYPES,则不指定任何类型。通常,服务器利用此参数来表示客户端只接受文本响应。最后一个参数指定可以用来控制请求行为的标记。例如,您可能希望指定 WINHTTP_FLAG_SECURE 标记用来发出 SSL 请求。
接下来介绍同步编程模型和异步编程模型的不同之处。同步模型先调用一个函数来发送请求,然后在接收到响应后再调用一个函数,而异步模型使用回调函数,允许异步执行其余函数调用,因而并不会阻止调用线程。
使用 WinHttpSetStatusCallback 函数可将回调函数与请求对象相关联。
if (WINHTTP_INVALID_STATUS_CALLBACK == ::WinHttpSetStatusCallback(request,
    Callback, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS,
    0)) // reserved
{
    // Call GetLastError for error information
}
第一个 WinHttpSetStatusCallback 参数指定关于您希望通知其进程的请求。第二个参数指定回调函数的地址。第三个参数指定您希望收到的通知。WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS 只是一个包含所有可能通知的位掩码。
回调函数的原型应该如下所示:
void CALLBACK Callback(HINTERNET handle,
                       DWORD_PTR context,
                       DWORD code,
                       void* info,
                       DWORD length);
第一个参数提供回调与之相关的对象的句柄。从技术上讲,您可以使用回调处理会话对象和连接对象的通知,但这通常用于请求对象。第二个参数是应用程序定义的值,该值与发送时的请求相关联。第三个参数提供表示回调原因的代码。最后两个参数指定一个缓冲区,根据前一个参数提供的代码,该缓冲区可能包含其他信息。例如,如果接收到 WINHTTP_CALLBACK_STATUS_REQUEST_ERROR 通知代码,则该信息参数将指向 WINHTTP_ASYNC_RESULT 结构。
使用 WinHttpSendRequest 函数发送请求:
if (!::WinHttpSendRequest(request,
                          WINHTTP_NO_ADDITIONAL_HEADERS,
                          0, // headers length
                          WINHTTP_NO_REQUEST_DATA,
                          0, // request data length
                          0, // total length
                          0)) // context
{
    // Call GetLastError for error information
}
第一个参数确定要发送的请求。第二个参数和第三个参数指定一个字符串,其中可能包含任何其他标头以包含该请求。如果指定 WINHTTP_NO_ADDITIONAL_HEADERS,则最初不会包含任何其他标头。也可以使用 WinHttpAddRequestHeaders 函数在发送请求之前向其中添加标头。第四个参数和第五个参数指定可选缓冲区,其中包含用作请求的内容或正文的所有数据。这通常用于 POST 请求。倒数第二个参数指定请求内容的总长度。此值用于创建此请求中包含的 Content-Length 标头。如果此值大于前面参数提供的数据的长度,则使用 WinHttpWriteData 函数编写其他数据来完成该请求。
因为我使用的是异步编程模型,所以函数无需等待发送请求就可以返回,而且 WinHTTP 可以通过回调函数返回 WinHttpSendRequest 的最后一个参数中指定的上下文值来提供状态更新。
现在暂停一下,想想如何使用 C++ 将到目前为止所介绍的内容联系在一起。在前几部分中,我介绍了相当简单的 WinHttpSession 类和 WinHttpConnection 类,用于建立会话和连接模型。我需要的 WinHttpRequest 类不仅要包含与请求相关的各种函数,而且还要能够为编写应用程序的特定类型的请求提供了一种简单方法。原因是该类可有效地管理缓冲区,该缓冲区可以在数据传输过程中存储请求数据和响应数据。 图 5 简要显示了我将在本文的其余部分中构建的 WinHttpRequest 类模板。OnCallback 成员函数的内部存在着许多有趣的逻辑。不过,我首先要介绍如何设计 WinHttpRequest 类模板。
template 
class WinHttpRequest : public WinHttpHandle
{
public:
    HRESULT Initialize(PCWSTR path,
                       __in_opt PCWSTR verb,
                       const WinHttpConnection& connection)
    {
        HR(m_buffer.Initialize(8 * 1024));

        // Call WinHttpOpenRequest and WinHttpSetStatusCallback.
    }

    HRESULT SendRequest(__in_opt PCWSTR headers,
                        DWORD headersLength,
                        __in_opt const void* optional,
                        DWORD optionalLength,
                        DWORD totalLength)
    {
        T* pT = static_cast(this);

        // Call WinHttpSendRequest with pT as the context value.
    }

protected:
    static void CALLBACK Callback(HINTERNET handle,
                                  DWORD_PTR context,
                                  DWORD code,
                                  void* info,
                                  DWORD length)
    {
        if (0 != context)
        {
            T* pT = reinterpret_cast(context);

            HRESULT result = pT->OnCallback(code,
                                            info,
                                            length);

            if (FAILED(result))
            {
                pT->OnResponseComplete(result);
            }
        }
    }

    HRESULT OnCallback(DWORD code,
                       const void* info,
                       DWORD length)
    {
         // Handle notifications here.
    }

    SimpleBuffer m_buffer;
};
图 6 列出了 DownloadFileRequest 类。该类从 WinHttpRequest 派生并将其自身用作模板参数。Visual C++ ® 库使用这一常用技术来实现有效编译时虚拟函数调用。类似于 WinHttpSession 类和 WinHttpConnection 类,WinHttpRequest 可提供用于初始化 WinHTTP 请求对象的 Initialize 成员函数。若要初始化 WinHTTP 请求对象,首先要创建请求数据和响应数据所要使用的缓冲区。该缓冲区的实现并不重要。它只需管理 BYTE 数组的生存期。接着,Initialize 调用 WinHttpOpenRequest 来创建 WinHTTP 请求对象,并调用 WinHttpSetStatusCallback 来将其与 WinHttpRequest 类内部的静态 Callback 成员函数关联起来。
SendRequest 成员函数只包含 WinHttpSendRequest 函数,并且将其“this”指针作为请求的上下文值进行传递。请注意,此值将传递到回调函数中,并可以确定通知所针对的请求对象。SendRequest 使用 static_cast 将“this”指针调整为指向由模板参数指定的派生类。这就是在这种情况下编译时多形性的实现过程。
最后,静态 Callback 成员函数将上下文值转换回指向请求对象的指针,并调用 OnCallback 成员函数来处理通知。既然网络错误是不可避免的,那么自然需要一些相应的处理方法。派生类可以实现采用 HRESULT 的 OnResponseComplete 成员函数。如果结果为 S_OK,则表示请求成功完成。但是如果回调中出现错误,则相应的 HRESULT 会指示请求已失败。
在介绍 OnCallback 成员函数的实现过程之前,让我们首先看一下 图 6 中所示的 DownloadFileRequest 具体类以全面了解 WinHttpRequest。这样做的好处是 DownloadFileRequest 可以重点了解下载文件涉及的细节,而不会陷入 HTTP 请求管理的复杂性问题中。其 Initialize 成员函数可以接受要下载的源文件的路径以及要将该文件编写到的目标流。每次接收到响应块时都会调用 OnReadComplete 成员函数;请求完成时则调用 OnResponseComplete。
class DownloadFileRequest : public WinHttpRequest
{
public:
    HRESULT Initialize(PCWSTR source,
                       IStream* destination,
                       const WinHttpConnection& connection)
    {
        m_destination = destination;

        return __super::Initialize(source,
                                   0, // GET
                                     connection);
    }

    HRESULT OnReadComplete(const void* buffer,
                           DWORD bytesRead)
    {
        return m_destination->Write(buffer,
                                    bytesRead,
                                    0); // ignored
    }

    void OnResponseComplete(HRESULT result)
    {
        if (S_OK == result)
        {
            // Download succeeded
        }
    }

private:
    CComPtr m_destination;
};

 

请求通知
现在,我要讨论如何实现 OnCallback 成员函数。在 图 7 中您可以看到,OnCallback 再次使用 static_cast 来将“this”指针调整为指向派生类。然后,使用 switch 语句来处理各种通知。如果存在尚未处理的特定通知,则返回 S_FALSE。如果您希望覆盖派生类中的 OnCallback 成员函数,这可能会派上用场。
HRESULT OnCallback(DWORD code,
                   const void* info,
                   DWORD length)
{
    T* pT = static_cast(this);

    switch (code)
    {
        case :
        {
            // Handle specific notification here.

            break;
        }
        default:
        {
            return S_FALSE;
        }
    }

    return S_OK;
}
在服务器可以访问的情况下,发送请求时第一个要注意的通知是 WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE。这表示 WinHttpSendRequest 函数调用已成功完成。假设您正在发送一个简单的 GET 请求,则可以立即调用 WinHttpReceiveResponse 函数来指示 WinHTTP 开始读取响应:
case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
{
    if (!::WinHttpReceiveResponse(m_handle,
                                  0)) // reserved
    {
        return HRESULT_FROM_WIN32(::GetLastError());
    }

    break;
}
:
如果接收到响应,则会收到 WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE 通知,指示可以读取响应标头。您可以查询可能与应用程序相关的各种标头。至少,您应该查询服务器返回的 HTTP 状态代码。可以使用 WinHttpQueryHeaders 函数检索标头信息。简单的实现可能只检查 HTTP_STATUS_OK 状态代码,指示请求成功(请参见 图 8)。
case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
{
    DWORD statusCode = 0;
    DWORD statusCodeSize = sizeof(DWORD);

    if (!::WinHttpQueryHeaders(m_handle,
                   WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
                   WINHTTP_HEADER_NAME_BY_INDEX,
                   &statusCode,
                   &statusCodeSize,
                   WINHTTP_NO_HEADER_INDEX))
    {
        return HRESULT_FROM_WIN32(::GetLastError());
    }

    if (HTTP_STATUS_OK != statusCode)
    {
        return E_FAIL;
    }

    if (!::WinHttpReadData(m_handle,
                   m_buffer.GetData(),
                   m_buffer.GetCount(),
                   0)) // async result
    {
        return HRESULT_FROM_WIN32(::GetLastError());
    }

    break;
}
WinHttpQueryHeaders 函数功能非常强大。它提供了一组在大多数情况下可以映射到标头的信息标记,而不是仅仅返回特定命名的标头的文本值。这些标记还提供了 HTTP 请求/响应状态的其他方面的访问权限。
图 8 中,为了检索服务器返回的状态代码,我使用了 WINHTTP_QUERY_STATUS_CODE 标记,还使用了 WINHTTP_QUERY_FLAG_NUMBER 修饰符,以告知 WinHttpQueryHeaders 我希望返回的值是 32 位数字而不是字符串。
第三个参数指定要查询的标头的名称。如果此参数为 WINHTTP_HEADER_NAME_BY_INDEX,则传递给上一个参数的标记便可以识别要返回的信息。接下来的两个参数指定接收该信息的缓冲区。最后一个参数用于读取名称相同的多个标头。如果此参数为 WINHTTP_NO_HEADER_INDEX,则只返回第一个标头的值。
如果对成功完成的请求满意,则可以调用 WinHttpReadData 函数开始读取响应。第二个参数和第三个参数指定接收数据的缓冲区。在这种特殊情况下,我会使用 WinHttpRequest 类模板提供的缓冲区。使用异步编程模型时,最后一个参数必须为零。
接收到足够的响应数据后,会收到 WINHTTP_CALLBACK_STATUS_READ_COMPLETE 通知,指示数据现已存储到了缓冲区中。读取的数据量由 OnCallback 的长度参数表示。该值可以为零,表示没有其他数据可用,也可以是任意一个小于 WinHttpReadData 函数调用中指定的缓冲区大小的值(请参见 图 9)。
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
{
    if (0 < length)
    {
        HR(pT->OnReadComplete(m_buffer.GetData(),
                              length));

        if (!::WinHttpReadData(m_handle,
                               m_buffer.GetData(),
                               m_buffer.GetCount(),
                               0)) // async result
        {
            return HRESULT_FROM_WIN32(::GetLastError());
        }
    }
    else
    {
        pT->OnResponseComplete(S_OK);
    }

    break;
}
现在,您应该能够清楚地看到 图 6 中 DownloadFileRequest 类中的“事件”的来源。每次接收到数据时,WINHTTP_CALLBACK_STATUS_READ_COMPLETE 处理程序都会调用派生类中的 OnReadComplete 方法。然后,调用 WinHttpReadData 来读取下一个数据块。最后,没有其他数据可用时,将调用派生类中的 OnResponseComplete 方法,如果出现 S_OK,则表示已成功读取响应。

 

请求取消
操作的完成情况会始终通过回调函数通知给应用程序,因此,WinHTTP 提供的异步完成模型相对比较容易出错。但由于工作线程用于执行回调函数,取消请求时确实需要注意一些细节。
只需使用 WinHttpCloseHandle 函数关闭请求句柄便可取消请求,但关闭请求句柄后,您需要准备即将进行的后续回调。您可以调用 WinHttpSetStatusCallback 函数来指示在关闭请求句柄之前不再进行任何回调,但 WinHTTP 不会使此设置与其工作线程保持同步。
因此,您应确保适当保证所有共享状态的安全。特别是,必须在 WINHTTP_STATUS_CALLBACK_HANDLE_CLOSING 通知到达之后才能释放正与该回调共享的所有不受保护的数据。
此外,还需要使对 WinHttpCloseHandle 的调用与其他函数保持同步,这些函数可能正在处理请求对象,如 WinHttpReadData、WinHttpWriteData 和 WinHttpSendRequest。请求取消是异步 WinHTTP 中问题较多的领域之一,所以如果您的代码与取消有关,请务必要仔细检查。

 

发送请求数据
什么是 POST 请求?除了使用其他 HTTP 谓词外,这类请求还包含作为请求主体中的内容的其他数据。 图 10 介绍了更新后的 WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE 处理程序,它可以轻松处理 GET 和 POST 这两个请求。
case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
{
    HRESULT result = pT->OnWriteData();

    if (FAILED(result))
    {
        return result;
    }

    if (S_FALSE == result)
    {
        if (!::WinHttpReceiveResponse(m_handle,
                                      0)) // reserved
        {
            return HRESULT_FROM_WIN32(::GetLastError());
        }
    }

    break;
}
当 WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE 通知到达时,它赋予派生类一个机会,允许其通过调用附带的 OnWriteData 成员函数将数据写入为请求的一部分。WinHttpRequest 类模板为仅返回 S_FALSE 的 GET 请求提供一个默认实现。可以使用 WinHttpWriteData 函数写入数据:
if (!::WinHttpWriteData(request,
                       buffer,
                       count,
                       0)) // async result
{
    // Call GetLastError for error information
}
第二个参数和第三个参数指定包含要写入数据的缓冲区。写入数据后,会立即收到 WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE。然后,再次调用派生类中的 OnWriteData 成员函数以写入下一个数据块。写入完成后立即返回 S_FALSE,而且基类处理程序继续调用 WinHttpReceiveResponse,正常接收响应。

 

其他功能
当然,有关如何使用 C++ 编写 HTTP 客户端应用程序的内容远不止这些,但很遗憾这里篇幅有限。但是,我希望本文中的详细讲解为您入门提供了足够的信息。
您可以看到,WinHTTP 为编写 HTTP 客户端应用程序提供了一个先进且功能强大的 API。除了我在本文前面介绍的所有功能外,WinHTTP 还提供了其他一些重要的功能,包括支持各种身份验证方案以及提供和验证 SSL 证书。此外,它还提供了构建和分析 URL 的功能。如果您正在寻找一个内容丰富的平台以构建高效且可缩放的 HTTP 客户端应用程序,WinHTTP 可能正是您所需要的。
确定代理设置
编写可靠的 HTTP 客户端应用程序时,其中一个难题是缺少一种一致的方法来检索代理设置。如果无法连接到服务器,会出现什么状况?如何确定使用哪个代理服务器?虽然没有适当的统一标准,但稍微努力一下,通常就可以确定使用哪个代理服务器。遗憾的是,很多应用程序只提示用户输入代理服务器的名称。我认为软件应该有足够的智能,如果软件能够自行找出信息,就不需要使用者提供信息了。
有许多方法可以查找代理设置。我先介绍最直接的方法。在介绍会话对象时,我已经介绍了用于创建新会话对象的 WinHttpOpen 函数:
HINTERNET session = ::WinHttpOpen(0, // no agent string
                                  WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                                  WINHTTP_NO_PROXY_NAME,
                                  WINHTTP_NO_PROXY_BYPASS,
                                  WINHTTP_FLAG_ASYNC);
                                  
这里有一些 Proxy 设定参数,我先前没仔细讲解。现在我来介绍一下它们的功能。第二个参数指定访问类型。 图 A 列出了可能使用的值。
会话对象参数 描述
WINHTTP_ACCESS_TYPE_NO_PROXY 不使用默认代理服务器
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY 使用注册表中存储的 WinHTTP 代理设置。
WINHTTP_ACCESS_TYPE_NAMED_PROXY 使用指定代理设置。
WINHTTP_ACCESS_TYPE_NO_PROXY 指示不会检索或使用代理设置。您可以在创建会话后覆盖它,我一会儿会介绍此操作的方法。
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY 指示应该使用 WinHTTP 在注册表中存储的所有代理设置。此参数允许管理员控制代理设置,而且还独立于 Internet Explorer 的代理设置,因此是理想的服务器方案。在 Windows XP 和 Windows Server 2003 中,可以使用 proxycfg.exe 实用工具来设置和查询 WinHTTP 代理设置。在 Windows Vista 和 Windows Server 2008 中,netsh.exe 实用工具已替代 proxycfg.exe 实用工具提供相应的功能。您还可以使用 WinHttpSetDefaultProxyConfiguration 函数直接设置 WinHTTP 代理设置,并使用 WinHttpGetDefaultProxyConfiguration 函数进行查询。这些设置在重新启动后仍然有效,并由所有登录会话共用。这两个函数均使用 WINHTTP_PROXY_INFO 结构,该结构直接映射到 WinHttpOpen 函数中与代理相关的三个参数。
WINHTTP_ACCESS_TYPE_NAMED_PROXY 指示 WinHTTP 应该在接下来的两个参数中使用代理设置。WinHttpOpen 的第三个参数指定代理服务器的名称,第四个参数指定一个 HTTP 服务器可选列表,其中的服务器不能通过上一个参数指定的代理服务器进行路由。
如果您编写的是 Windows 服务或服务器应用程序,了解这些内容就应景足够了。不过从最终用户的角度来说,我们需要再多花一点心思让结果更完美,因为最终用户通常不知道也不想配置代理设置。另一种方法是检索 Internet Explorer 的代理设置,因为大多数用户的浏览器已配置为通过 Web 代理来访问 Web。单击“Internet Explorer 选项”窗口中“连接”选项卡上的“局域网(LAN)设置”按钮,便可找到配置代理设置的 Internet Explorer 对话框。因为这是一个常见的要求,所以 WinHTTP 提供了 WinHttpGetIEProxyConfigForCurrentUser 函数,可以直接为当前用户检索下列 Internet Explorer 代理设置:
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieProxyConfig = { 0 };

if (!::WinHttpGetIEProxyConfigForCurrentUser(&ieProxyConfig))
{
    // Call GetLastError for error information.
}
Here's what WINHTTP_CURRENT_USER_IE_PROXY_CONFIG looks like:
struct WINHTTP_CURRENT_USER_IE_PROXY_CONFIG
{
    BOOL    fAutoDetect;
    LPWSTR  lpszAutoConfigUrl;
    LPWSTR  lpszProxy;
    LPWSTR  lpszProxyBypass;
};
必须使用 GlobalFree 函数释放非 Null 字符串成员变量。fAutoDetect 成员映射到“局域网(LAN)设置”对话框中的“自动检测设置”选项。lpszAutoConfigUrl 成员映射到“使用自动配置脚本”地址,而且只有选中此选项才会填充该成员。lpszProxy 成员映射到“使用代理服务器...”地址和端口,而且只有选中此选项才会填充该成员。最后,lpszProxyBypass 映射到“异常”区域(位于单击“高级”按钮时出现的窗口中)中指定的服务器列表,只有选中“跳过本地地址的代理服务器”选项才会填充该成员。
如果填充的是 lpszProxy,则您很幸运,因为可以直接将它用作代理服务器。如果选中的是其他选项,则要确定应执行的操作就困难了一些。
如果填充的是 lpszAutoConfigUrl,则表示 Internet Explorer 下载代理自动配置 (PAC) 文件来确定特定连接的代理服务器。重现此行为将是一个挑战,因为下载的 PAC 文件通常包含 JavaScript,而 JavaScript 可能用于将客户端指向特定代理服务器,具体取决于目标服务器。幸运的是,WinHTTP 提供了 WinHttpGetProxyForUrl 函数,为您简化了这一步骤:
WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions = { 0 };
autoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL;
autoProxyOptions.lpszAutoConfigUrl = ieProxyConfig.lpszAutoConfigUrl;

WINHTTP_PROXY_INFO proxyInfo = { 0 };

if (!::WinHttpGetProxyForUrl(session,
                             L"http://example.com/path",
                             &autoProxyOptions,
                             &proxyInfo))
{
    // Call GetLastError for error information.
}
同样,必须使用 GlobalFree 函数释放 WINHTTP_PROXY_INFO 结构的非 Null 字符串成员变量。第一个 WinHttpGetProxyForUrl 参数指定 WinHTTP 会话句柄。第二个参数指定您希望发送的请求的 URL。第三个参数是一个输入参数,指定控制 WinHttpGetProxyForUrl 函数行为的 WINHTTP_AUTOPROXY_OPTIONS 结构。您将在此处指定从 Internet Explorer 代理设置检索到的 PAC 文件的 URL。最后一个参数是一个输出参数,指定接收结果的 WINHTTP_PROXY_INFO 结构。接着,可以将收到的结果与 WinHttpSetOption 函数和 WINHTTP_OPTION_PROXY 选项结合使用,以设置特定请求对象的代理设置。
最后,如果 WINHTTP_CURRENT_USER_IE_PROXY_CONFIG 的 fAutoDetect 成员变量为 true,则意味着 Internet Explorer 使用 Web 代理自动发现(Web Proxy Auto-Discovery,WPAD)协议来查找代理设置。WPAD 根据网络的 DNS 或 DHCP 服务器的更新来提供 PAC 脚本的 URL,因此客户端不必预先配置特定的 PAC 脚本。这与静态 IP 地址和动态 IP 地址之间的关系类似。通过按以下方式更改 WINHTTP_AUTOPROXY_OPTIONS 初始化设置,可以指示 WinHttpGetProxyForUrl 使用 WPAD 来自动查找 PAC 文件的 URL:
WINHTTP_AUTOPROXY_OPTIONS autoProxyOptions = { 0 };
autoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT;
autoProxyOptions.dwAutoDetectFlags = WINHTTP_AUTO_DETECT_TYPE_DHCP 
  | WINHTTP_AUTO_DETECT_TYPE_DNS_A;
当然,可以使用 WinHTTP 的“AutoProxy”功能,无需依赖 Internet Explore。在 WPAD 在公司网络环境中获得更普遍的推广之前,使用 Internet Explorer 设置主要是一种权宜之计。如果您的网络不提供 WPAD,则可能需等待几秒钟才能返回 WinHttpGetProxyForUrl,而且 GetLastError 会返回 ERROR_WINHTTP_AUTODETECTION_FAILED。

 

 

 

你可能感兴趣的:(技术文摘)