使用WinHttp接口实现HTTP协议Get、Post和文件上传功能

        我实现了一个最新版本的接口,详见《实现HTTP协议Get、Post和文件上传功能——使用WinHttp接口实现》。还有基于libcurl实现的版本《实现HTTP协议Get、Post和文件上传功能——使用libcurl接口实现》。以下是原博文:

        我们在做项目开发时,往往会涉及到和服务器通信。对于安全性要求不高的情况,一般我们采用HTTP通信协议。对于喜欢挑战底层技术的同学,可能希望使用winsocket去完成通信过程。对于希望快速开发的同学,可能希望引入诸如CURL这类的第三方库。而本文将介绍使用WinHttp接口实现Http协议的Get、Post和文件上传的功能。为了保证我们代码的精简性和易扩展性,我并不打算做的很全面——比如我不考虑HTTPS和SSL以及转码等。我只是希望提供一个一目了然的结构,用于指出三种功能在代码实现上的异同点。当然在这套代码上增加HTTPS和SSL,以及用户名\密码机制也是非常简单的。(转载请指明出于breaksoftware的csdn博客)——新版本参阅《实现HTTP协议Get、Post和文件上传功能——使用WinHttp接口实现》

协议口语化描述

        在项目中我们可能遇到的服务端同学对协议的描述:

  1. 你可以对http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2发送Get请求,参数的Key是userkey,Value是uservalue。
  2. 你可以对http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2发送Post请求,参数的Key是Data,Value是一个很长的数据。
  3. 你可以向http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2上传一个文件,文件的Key是Data,Value是文件的内容。哦!别忘了,还要传文件的MD5给我们,这个MD5的参数的Key是hash,Value是文件内容的MD5值。

        在上述的描述中,你可能会遇到曾经和服务端同学沟通的影子。一般来说,对于简单协议的描述,上述基本可以涵盖。我提炼出如下实现,来实现相关功能,具体的函数说明会在之后给出

BOOL CHttpClientSyn::TransmiteData( const std::wstring& wstrUrl, EType eType, DWORD dwTimeout)
{
    BOOL bSuc = FALSE;
    do {
        if ( FALSE == InitializeHttp(wstrUrl, dwTimeout)) {
            break;
        }
        if ( FALSE == TransmiteData(eType) ) {
            break;
        }
        ReceiveData();
        UninitializeHttp();
        bSuc = TRUE;
    } while (0);
    return bSuc;
}

信息准备

        我们看一下1和2描述内容。可以看出,其主要差别就是一个是使用Get方式发送,一个是使用Post方式。那就是说,除了发送方式不同,我们其他的设计“基本”可以认为是统一的。那么我们就先分析下URL及追加的参数。在讨论这个之前,我先引进一个结构体URL_COMPONENTS

typedef struct {
  DWORD           dwStructSize;
  LPTSTR          lpszScheme;
  DWORD           dwSchemeLength;
  INTERNET_SCHEME nScheme;
  LPTSTR          lpszHostName;
  DWORD           dwHostNameLength;
  INTERNET_PORT   nPort;
  LPTSTR          lpszUserName;
  DWORD           dwUserNameLength;
  LPTSTR          lpszPassword;
  DWORD           dwPasswordLength;
  LPTSTR          lpszUrlPath;
  DWORD           dwUrlPathLength;
  LPTSTR          lpszExtraInfo;
  DWORD           dwExtraInfoLength;
} URL_COMPONENTS, *LPURL_COMPONENTS;

        详细的说明,可以查看MSDN。我们可以这样调用函数,以解析出URL中包含的信息

URL_COMPONENTS urlCom;
……
WinHttpCrackUrl( wstrUrl.c_str(), wstrUrl.length(), ICU_ESCAPE, &urlCom);

        我在此,以http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2为例,做个简要的说明:

  • dwStructSize用于表明该结构体大小,一般我们都是传递sizeof(URL_COMPONENTS)。
  • lpszSheme指向一段用于保存协议类型的内存空间,dwSchemeLength用于描述传入空间的大小(以TCHARS为单位的大小,下面其他空间大小描述字段都是以TCHARS单位)。对应于我们的例子,该空间将保存的结果是:http,dwSchemeLength的值是4(执行后被修改)。
  • lpHostName指向一段用于保存域名信息的内存空间,dwHostNameLength;用于描述传入空间的大小。对应于我们的例子,lpHostName指向的空间信息是:xxx.yyy.zzz。dwHostNameLength返回11。
  • nPort用于接收端口号。我们例子中的端口号是8324。
  • lpszUserName和lpszPassword分别用于保存URL中携带的用户名和密码。我们例子中没有这些信息(包含密码的格式是http://name:password@xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pv2),所以我们不需要使用这些空间,自然不必分配相应的空间。
  • lpszUrlPath指向保存URL的路径——不包含域名的一段内存空间。对应于我们的例子,该空间的值是:/urlpath。
  • lpszExtraInfo指向保存URL中参数信息的一段内容空间。对应于我们的例子,该空间的值是?pk1=pv1&pk2=pk2

        完整的实现代码是

BOOL CHttpClientSyn::InitializeHttp( const std::wstring& wstrUrl, DWORD dwTimeout)
{
    BOOL bSuc = FALSE;
    do {
        URL_COMPONENTS urlCom;
        memset(&urlCom, 0, sizeof(urlCom));
        urlCom.dwStructSize = sizeof(urlCom);
        WCHAR wchScheme[64] = {0};
        urlCom.lpszScheme = wchScheme;
        urlCom.dwSchemeLength = ARRAYSIZE(wchScheme);
        WCHAR wchHostName[1024] = {0};
        urlCom.lpszHostName = wchHostName;
        urlCom.dwHostNameLength = ARRAYSIZE(wchHostName);
        WCHAR wchUrlPath[1024] = {0};
        urlCom.lpszUrlPath = wchUrlPath;
        urlCom.dwUrlPathLength = ARRAYSIZE(wchUrlPath);
        WCHAR wchExtraInfo[1024] = {0};
        urlCom.lpszExtraInfo = wchExtraInfo;
        urlCom.dwExtraInfoLength = ARRAYSIZE(wchExtraInfo);

        if ( FALSE == WinHttpCrackUrl( wstrUrl.c_str(), wstrUrl.length(), ICU_ESCAPE, &urlCom) ) {
            break;
        }

        std::wstring wstrExtraInfo = urlCom.lpszExtraInfo;

        我们通过这个结构体,可以拆解开URL。这儿我们需要特别注意的是lpszExtraInfo保存的信息:?pk1=pv1&pk2=pk2。在我们口头描述的协议中,还要增加一个参数,即userkey=uservalue。那么完整的参数将是:?pk1=pv1&pk2=pk2&userkey=uservalue。为了让这种参数的拼接具有易扩展性,我将参数信息分拆并保存到一个Map中。然后继承于我们基类的派生类,可以根据自己的业务特点,向我们这个Map中新增其他Key-Value对,最后我们统一生成参数串。这儿需要指出的是,这种方法只是针对GET协议,因为GET协议发送参数的方法是一致的。而POST和文件上传协议都不需要对lpszExtraInfo解析参数,它将作为UrlPath的一部分在之后的操作中被使用。

VOID CHttpClientSyn::ParseParams(const std::wstring& wstrExtraInfo)
{
    int nPos = 0;
    nPos = wstrExtraInfo.find('?');
    if ( -1 == nPos ) {
        return;
    }
    std::wstring wstrParam = wstrExtraInfo;
    int nStaticMaxParamCount = MAXSTATICPARAMCOUNT;
    do{
        wstrParam = wstrParam.substr(nPos + 1, wstrExtraInfo.length() - nPos - 1);
        nPos = wstrParam.find('&', nPos);
        std::wstring wstrKeyValuePair;

        if ( -1 == nPos ) {
            wstrKeyValuePair = wstrParam;
        }
        else {
            wstrKeyValuePair = wstrParam.substr(0, nPos);
        }
        
        int nSp = wstrKeyValuePair.find('=');
        if ( -1 != nSp ) {
            StParam stParam;
            stParam.wstrKey = wstrKeyValuePair.substr(0, nSp);
            stParam.wstrValue = wstrKeyValuePair.substr( nSp + 1, wstrKeyValuePair.length() - nSp - 1);
            m_VecExtInfo.push_back(stParam);
        }
    }while(-1 != nPos && nStaticMaxParamCount > 0);
}

        同时,我们的基类提供一个纯虚函数,让继承类去自由增加参数

virtual VOID AddExtInfo(VecStParam& VecExtInfo) = 0;

        于是在CHttpClientSyn::InitializeHttp函数中,执行

        std::wstring wstrExtraInfo = urlCom.lpszExtraInfo;
        ParseParams(wstrExtraInfo);
        AddExtInfo(m_VecExtInfo);

        在本文的后面部分,我会给出各继承类对该方法的实现。
        至此,各种该准备的数据已经OK了。现在,我们要初始化Get、Post和上传结构都要环境——打开Session并连接服务器

打开Session并连接服务器

        这部分的代码,三种方式是一致的。具体也没什么好说明的,直接上代码(还是之前的CHttpClientSyn::InitializeHttp中)

        m_hSession = WinHttpOpen(NULL, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0 ); 
        if ( NULL == m_hSession ) {
            break;
        }

        m_hConnect = WinHttpConnect( m_hSession, urlCom.lpszHostName, urlCom.nPort, 0 );
        if ( NULL == m_hConnect ) {
            break;
        }

        m_wstrUrlPath = urlCom.lpszUrlPath;

        bSuc = TRUE;
    } while (0);
    return bSuc;
}

          至此,三种方式相同的执行路径已经结束。我们要依据继承类的调用方式,决定走三种方式中的哪个

BOOL CHttpClientSyn::TransmiteData(EType eType)
{
    BOOL bSuc = FALSE;
    switch (eType) {
    case eGet:{
        bSuc = TransmiteDataToServerByGet();
        }break;
    case ePost:{
        bSuc = TransmiteDataToServerByPost();
        }break;
    case eUpload:{
        bSuc = TransmiteDataToServerByUpload();
        }break;
    default: break;
    }
    return bSuc;
}

使用Get方式发送数据

       Get方式是最常用的HTTP方式。它的实现也很简单,只要将除了Host和Port部分(上例中/urlpath?pk1=pv1&pk2=pk2&userkey=uservalue,注意那个?号)一次性发给服务器即可。注意这个发送要使用WinHttpOpenRequest来完成。

BOOL CHttpClientSyn::TransmiteDataToServerByGet()
{
    BOOL bSuc = FALSE;
    do {

        std::wstring wstrUrlPathAppend = m_wstrUrlPath;
        // 采用Get方式时,要将参数放在OpenRequest中
        if ( false == wstrUrlPathAppend.empty() ) {
            wstrUrlPathAppend += L"?";
        }
        wstrUrlPathAppend += GenerateExtInfo(m_VecExtInfo);

        m_hRequest = WinHttpOpenRequest(m_hConnect, L"Get",
            wstrUrlPathAppend.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
        if ( NULL == m_hRequest ) {
            break;
        }

        在请求打开后,我们还要设置头信息。我这儿将设置头信息的函数设置为纯虚函数,这样继承类就要自己实现这个函数,并设置自己的头信息。

        ModifyRequestHeader(m_hRequest);

        头信息设置好后,我们就可以发送请求了

        if ( FALSE == WinHttpSendRequest( m_hRequest, 
            WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) )
        {
            break;
        }

        bSuc = TRUE;
    } while (0);
    return bSuc;

}

        过程就是如此简单。

        我们再看下继承类的相关实现

std::wstring CHttpTransByGet::GenerateExtInfo( const VecStParam& VecExtInfo )
{
    std::wstring wstrExtInf;
    for ( VecStParamCIter it = VecExtInfo.begin(); it != VecExtInfo.end(); it++ ) {
        if ( false == wstrExtInf.empty() ) {
            wstrExtInf += L"&";
        }
        wstrExtInf += it->wstrKey;
        wstrExtInf += L"=";
        wstrExtInf += it->wstrValue;
    }
    return wstrExtInf;
}

BOOL CHttpTransByGet::ModifyRequestHeader( HINTERNET hRequest )
{
    std::wstring wstrHeader[] = { L"Content-type: application/x-www-form-urlencoded\r\n"};
    for ( size_t i = 0; i < ARRAYSIZE(wstrHeader); i++ ) {
        WinHttpAddRequestHeaders(hRequest, wstrHeader[i].c_str(), wstrHeader[i].length(), WINHTTP_ADDREQ_FLAG_ADD);
    }
    return TRUE;
}

VOID CHttpTransByGet::AddExtInfo( VecStParam& VecExtInfo )
{
    for ( VecStParamCIter it = m_vecParam.begin(); it != m_vecParam.end(); it++ ) {
        VecExtInfo.push_back(*it);
    }
}

        这段代码,没有多少要注意的,只要注意下Get方式要设置的头信息。

使用Post方式发送数据

        Post方式和Get方式的有若干实现的区别。首先,我们在打开Request的时候,要设置Post方式,同时要设置打开的是UrlPath,而不是携带参数的部分(即上例中的/urlpath)。

BOOL CHttpClientSyn::TransmiteDataToServerByPost()
{
    BOOL bSuc = FALSE;
    do {
        m_hRequest = WinHttpOpenRequest(m_hConnect, L"Post",
            m_wstrUrlPath.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
        if ( NULL == m_hRequest ) {
            break;
        }

       之后,我们也是要设置头信息。这儿我们可以和上面Get方式一样设置

        ModifyRequestHeader(m_hRequest);

        最后便是数据发送。我们回顾下2中的描述:

        你可以对http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2发送Post请求,参数的Key是Data,Value是一个很长的数据。

        可以看出,我们要发送两批数据:一个是固有参数pk1=pv2&pk2=pv2;一个是不确定的参数“参数的Key是Data,Value是一个很长的数据”。我也是按这种描述设计的:

        先将容易确定的固定参数发送出去

        std::wstring wstrExtInfo = GenerateExtInfo(m_VecExtInfo);
        std::string strExtInfo = CW2A(wstrExtInfo.c_str(), CP_UTF8);

        DWORD dwTotal = strExtInfo.length();
        dwTotal += GetDataSize();

        if ( FALSE == WinHttpSendRequest( m_hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, dwTotal, 0)) {
            break;
        }

        if ( 0 != strExtInfo.length() ) {
            // 默认可以一次全部写完
            if ( FALSE == WinHttpWriteData(m_hRequest, strExtInfo.c_str(), strExtInfo.length(), NULL ) ) {
                break;
            }
        }

        这儿做了一个偷懒的处理,我将数据一次性写入。当然比较严谨的做法是根据每次成功的长度递减数据发送。

        为了支持这种可能是Data对应的不确定数据的发送,我在基类中暴露了一个接口,供继承函数类以向基类逻辑提供数据。我这儿分而治之,是为了区分这些数据和之前的固有数据的区别——固有数据是字符串,而自定义数据可能是2进制流。

        // 静态分配一个数组
        BYTE buffer[1024]= {0};
        BOOL bContinue = FALSE;
        BOOL bSendOK = FALSE;

        do {
            DWORD dwBufferLength = sizeof(buffer);
            SecureZeroMemory(buffer, dwBufferLength);
            DWORD dwWriteSize = 0;
            bContinue = GetData(buffer, dwBufferLength, dwWriteSize);
            if ( 0 != dwWriteSize ) {
                bSendOK= WinHttpWriteData( m_hRequest, buffer, dwWriteSize, NULL);
            }
            else {
                bSendOK = TRUE;
            }
        } while ( bContinue && bSendOK );

        bSuc = bSendOK;

    } while (0);
    return bSuc;
}

        这个逻辑,分配了一个1024字节的空间。通过继承类(或基类,基类直接返回False)GetData函数不停填充数据,并调用WinHttpWriteData发送数据。我们看下继承类的实现

DWORD CHttpTransByPost::GetDataSize()
{
    return m_dwDataSize;
}

BOOL CHttpTransByPost::GetData( LPVOID lpBuffer, DWORD dwBufferSize, DWORD& dwWrite )
{
    BOOL bContinue = TRUE;
    dwWrite = 0;
    if ( m_dwDataSize > m_dwWriteIndex + dwBufferSize ) {
        dwWrite = dwBufferSize;
    }
    else {
        dwWrite = m_dwDataSize - m_dwWriteIndex;
        bContinue = FALSE;
    }

    if ( 0 != memcpy_s(lpBuffer, dwBufferSize, (LPBYTE)m_lpData + m_dwWriteIndex, dwWrite) ){
        bContinue = FALSE;
    }

    return bContinue;
}

BOOL CHttpTransByPost::TransDataToServer( const std::wstring& wstrUrl, DWORD dwTimeout, 
    VecStParam& vecParam, LPVOID lpData, DWORD dwDataLenInBytes )
{
    m_lpData = lpData;
    m_dwDataSize = dwDataLenInBytes;
    m_vecParam.assign(vecParam.begin(), vecParam.end());
    m_dwWriteIndex = 0;
    return TransmiteData(wstrUrl, eGet, dwTimeout);
}

        m_dwWriteIndex用于标记当前已经读取到哪个位置。这样这些函数将保证,基类将可以将数据读取完毕。这儿可能有个要注意的就是:要将“&Data=”传入lpData地址空间中。

向服务器上传文件

        向服务器上传文件,可能是使用的频率仅次于Get的一种方式。在编写上传功能时,我还是踩中了不少坑,这也是我决心将这些整理出来分享的一个很重要原因。

        最开始时,我以为上传文件无非就是一个Post请求。后来经过一些磨难后,发现事实仅非如此。

        首先我们看下和Post调用相同的地方

BOOL CHttpClientSyn::TransmiteDataToServerByUpload()
{
    BOOL bSuc = FALSE;
    do {
        m_hRequest = WinHttpOpenRequest(m_hConnect, L"Post",
            m_wstrUrlPath.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
        if ( NULL == m_hRequest ) {
            break;
        }

        ModifyRequestHeader(m_hRequest);

        std::wstring wstrExtInfo = GenerateExtInfo(m_VecExtInfo);
        std::string strExtInfo = CW2A(wstrExtInfo.c_str(), CP_UTF8);

        DWORD dwTotal = strExtInfo.length();
        dwTotal += GetDataSize();

        if ( FALSE == WinHttpSendRequest( m_hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, dwTotal, 0)) {
            break;
        }

        这仅仅是调用流程的相同,而不同点,我都将其“埋伏”在继承类中。我们先看继承类中头设置的实现

#define BOUNDARYPART L"--h1o9n8e6y6k6k"
……
    m_wstrNewHeader = L"Content-Type: multipart/form-data; boundary=";
    m_wstrNewHeader += BOUNDARYPART;
    m_wstrNewHeader += L"\r\n";
……
BOOL CHttpUploadFiles::ModifyRequestHeader( HINTERNET hRequest )
{
    return ::WinHttpAddRequestHeaders(hRequest, m_wstrNewHeader.c_str(), 
        m_wstrNewHeader.length(), WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE);
}

        Content-Type: multipart/form-data;相关说明可以参看rfc2388,至于更详细的文件上传的rfc可以参看rfc1867。本文只从使用的角度去讲解,所以不会去分析RFC文档。读者只要知道我们要设置这个头即可。从这个头可以看出来,我们这次请求是一个MultiPart的,即多部分组成。那么如何分隔各部分数据呢?我们使用一个分隔符,该分隔符就是上面代码中的"--h1o9n8e6y6k6k"。我们还要在头中告诉服务器:我们要用什么来做分隔符。于是你看到这个头的完整信息是:

Content-Type: multipart/form-data; boundary=--h1o9n8e6y6k6k

        在之后,我们还会陆续提到这个分隔字段。它将贯穿整个Post过程。

        头信息设置好后,我将发送文件

        // 静态分配一个数组
        BYTE buffer[1024]= {0};
        BOOL bContinue = FALSE;
        BOOL bSendOK = FALSE;

        do {
            DWORD dwBufferLength = sizeof(buffer);
            SecureZeroMemory(buffer, dwBufferLength);
            DWORD dwWriteSize = 0;
            bContinue = GetData(buffer, dwBufferLength, dwWriteSize);
            if ( 0 != dwWriteSize ) {
                bSendOK= WinHttpWriteData( m_hRequest, buffer, dwWriteSize, NULL);
            }
            else {
                bSendOK = TRUE;
            }
        } while ( bContinue && bSendOK );

        文件发送好之后,我们再将URL中带的pk1=pv1&pk2=pv2信息发送出去。

if ( 0 != strExtInfo.length() ) {
            if ( FALSE == WinHttpWriteData(m_hRequest, strExtInfo.c_str(), strExtInfo.length(), NULL ) ) {
                break;
            }
        }
        bSuc = bSendOK;
    } while (0);
    return bSuc;
}

        我之所以如此快速的将这个流程过掉,而没细分讲解,是希望大家避免一个坑——发送顺序问题。如果这两个顺序反了,服务器可能接收不到文件。原因是在文件段(之后会介绍文件段是什么,这个名字是我临时起意)之后,我们还要向服务器发一个普通数据段(之后会介绍普通数据段,这个名字也是我临时起意)。否则服务器会一直等待,认为我们文件没传完,哪怕我们在WinHttpSendRequest设置了正确的大小。当然这个顺序也不是一定要如此,我们可以将普通数据(pk1=pv1&pk2=pv2)先发送,再发送文件段,最后再发送一个无用的数据段。

        我们先关注一下这段代码

BOOL CHttpUploadFiles::TransDataToServer( const std::wstring wstrUrl, VecStParam& VecExtInfo, 
    const std::wstring& wstrFilePath,  const std::wstring& wstrFileKey)
{
    m_wstrBlockStart = L"--";
    m_wstrBlockStart += BOUNDARYPART;
    m_wstrBlockStart += L"\r\n";

    m_strBlockStartUTF8 = CW2A(m_wstrBlockStart.c_str(), CP_UTF8);

    m_wstrBlockEnd =  L"\r\n--";
    m_wstrBlockEnd += BOUNDARYPART;
    m_wstrBlockEnd +=  L"--\r\n";

    m_wstrNewHeader = L"Content-Type: multipart/form-data; boundary=";
    m_wstrNewHeader += BOUNDARYPART;
    m_wstrNewHeader += L"\r\n";

        m_wstrNewHeader这个字段我们已经在之前讲解过,它是需要使用WinHttpAddRequestHeaders设置的头信息。m_wstrBlockStart 是我们整个大的数据块(包括文件段和数据段)的一开始的标识符,即它是要“最”先传送给服务器。m_wstrBlockEnd应该可以猜出来了——它是整个大数据块的结尾符。即我们整个数据将要被m_wstrBlockStart和m_wstrBlockEnd包含。

----h1o9n8e6y6k6k(用\r\n)
数据
----h1o9n8e6y6k6k--(用\r\n)

        然后我们看下文件段。文件段一开始是有这样的一个头

    std::wstring wstrUploadFileHeader;
    wstrUploadFileHeader = m_wstrBlockStart;
    wstrUploadFileHeader += L"Content-Disposition: form-data; name=\"";
    wstrUploadFileHeader += wstrFileKey;
    wstrUploadFileHeader += L"\";";
    wstrUploadFileHeader += L"filename=\"";
    wstrUploadFileHeader += wstrFileName;
    wstrUploadFileHeader += L"\"\r\n";
    wstrUploadFileHeader += L"Content-Type:application/octet-stream\r\n\r\n";

    m_strUploadFileHeaderUTF8 = CW2A(wstrUploadFileHeader.c_str(), CP_UTF8);

        这个头包含了文件名和文件内容对应的Key。以描述3 为例,这个Key就是name的值,就是Data。

----h1o9n8e6y6k6k(用\r\n)
Content-Disposition: form-data; name="Data";filename="uploadfilename.bin"(用\r\n)
Content-Type:application/octet-stream(用\r\n\r\n)

文件内容
----h1o9n8e6y6k6k--(用\r\n)

        我们再看下文件发送的流程,其实就是数据填充的过程

BOOL CHttpUploadFiles::GetData( LPVOID lpBuffer, DWORD dwBufferSize, DWORD& dwWrite )
{
    if ( m_strUploadFileHeaderUTF8.empty() ) {
        return FALSE;
    }

    if ( EHeader == m_ReadInfo.eType ) {
        if ( FALSE == ReadFromString(m_strUploadFileHeaderUTF8, lpBuffer, dwBufferSize, m_ReadInfo.dwReadIndex, dwWrite ) ) {
            return FALSE;
        }
        m_ReadInfo.dwReadIndex += dwWrite;
        if ( m_ReadInfo.dwReadIndex == m_strUploadFileHeaderUTF8.length() ) {
            m_ReadInfo.eType = EFile;
            m_ReadInfo.dwReadIndex = 0;
            return TRUE;
        }
    }
    else if ( EFile == m_ReadInfo.eType ){
        OVERLAPPED ov;
        memset(&ov, 0, sizeof(ov));
        ov.Offset = m_ReadInfo.dwReadIndex;

        HANDLE hFile = CreateFile( m_wstrFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL );
        BOOL bContinue = FALSE;
        DWORD dwFileSize = 0;
        do {
            if ( INVALID_HANDLE_VALUE == hFile ) {
                dwWrite = 0;
                break;
            }

            LARGE_INTEGER lgFileSize = {0};
            if ( FALSE == GetFileSizeEx(hFile, &lgFileSize) ) {
                break;
            }

            if ( FALSE == ReadFile(hFile, lpBuffer, dwBufferSize, &dwWrite, &ov)) {
                break;
            }
            dwFileSize = lgFileSize.LowPart;
            bContinue = TRUE;
        } while (0);
        
        if ( INVALID_HANDLE_VALUE != hFile ) {
            CloseHandle(hFile);
            hFile = NULL;
        }
        m_ReadInfo.dwReadIndex += dwWrite;
        if ( m_ReadInfo.dwReadIndex == dwFileSize ) {
            m_ReadInfo.dwReadIndex = 0;
            bContinue = FALSE;
        }

        return bContinue;
    }

    return TRUE;
}

        最后我们看下数据段的发送

std::wstring CHttpUploadFiles::GenerateExtInfo( const VecStParam& VecExtInfo )
{
    std::wstring wstrInfo = L"\r\n";
    for ( VecStParamCIter it = VecExtInfo.begin(); it != VecExtInfo.end(); it++ ) {
        wstrInfo += m_wstrBlockStart;
        wstrInfo += L"Content-Disposition:form-data;";
        wstrInfo += L"name=";
        wstrInfo += L"\"";
        wstrInfo += it->wstrKey;
        wstrInfo += L"\"";
        wstrInfo += L"\r\n\r\n";
        wstrInfo += it->wstrValue;
        wstrInfo += L"\r\n";
    }
    wstrInfo += m_wstrBlockEnd;
    return wstrInfo;
}

        数据段也要使用分隔符分隔。并用固定的格式传送参数pk1=pv1&pk2=pk2

----h1o9n8e6y6k6k(用\r\n)
Content-Disposition: form-data; name="Data";filename="uploadfilename.bin"(用\r\n)
Content-Type:application/octet-stream(用\r\n\r\n)

文件内容
----h1o9n8e6y6k6k(用\r\n)
Content-Disposition:form-data;name="pk1"(用\r\n\r\n)

pv1
----h1o9n8e6y6k6k(用\r\n)
Content-Disposition:form-data;name="pk2"(用\r\n\r\n)

pv2
----h1o9n8e6y6k6k--(用\r\n)

        至此,文件传输主要流程讲完了,最后还要提一句,就是在Post之前,我们要获取正确的发送包的大小。

DWORD CHttpUploadFiles::GetDataSize()
{
    if ( m_strUploadFileHeaderUTF8.empty() ) {
        return 0;
    }

    DWORD dwFileSize = 0;
    HANDLE hFile = CreateFile( m_wstrFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL );
    do {
        if ( INVALID_HANDLE_VALUE == hFile ) {
            break;
        }

        LARGE_INTEGER lgFileSize = {0};
        if ( FALSE == GetFileSizeEx(hFile, &lgFileSize) ) {
            break;
        }

        if ( lgFileSize.HighPart > 0 || lgFileSize.LowPart > 0x00FFFFFF) {
            // 限制大小
            break;
        }
        dwFileSize = lgFileSize.LowPart;
    }while(0);
    if ( INVALID_HANDLE_VALUE != hFile ) {
        CloseHandle(hFile);
        hFile = NULL;
    }

    DWORD dwDataSize = 0;
    if ( 0 != dwFileSize ) {
        dwDataSize = dwFileSize + m_strUploadFileHeaderUTF8.length();
    }

    return dwDataSize;
}

        HTTP三种方式讲解结束。附上对应的代码

        在百度云盘上的代码的链接:http://pan.baidu.com/s/1i3DZEol 密码:2em8

        再次强烈建议,请看新版本《实现HTTP协议Get、Post和文件上传功能——使用WinHttp接口实现》《实现HTTP协议Get、Post和文件上传功能——使用libcurl接口实现》

WinHTTP提供以下功能WinHttpAddRequestHeaders 向HTTP请求句柄添加一个或多个HTTP请求标头。 WinHttpCheckPlatform 确定WinHTTP是否支持当前平台。 WinHttpCloseHandle 关闭单个 HINTERNET句柄。 WinHttpConnect 指定HTTP请求的初始目标服务器。 WinHttpCrackUrl 将URL分为其组成部分,例如主机名和路径。 WinHttpCreateProxyResolver 创建WinHttpGetProxyForUrlEx使用的句柄。 WinHttpCreateUrl 从组件部分创建URL,例如主机名和路径。 WinHttpDetectAutoProxyConfigUrl 查找代理自动配置(PAC)文件的URL。此功能报告PAC文件的URL,但不下载该文件。 WinHttpFreeProxyResult 释放从以前的调用WinHttpGetProxyResult检索的数据。 WinHttpGetDefaultProxyConfiguration 从注册表中检索默认的WinHTTP代理配置。 WinHTTPGetIEProxyConfigForCurrentUser 获取当前用户的Internet Explorer(IE)代理配置。 WinHttpGetProxyForUrl 检索指定URL的代理信息。 WinHttpGetProxyForUrlEx 检索指定URL的代理信息。 WinHttpGetProxyResult 检索到调用的结果WinHttpGetProxyForUrlEx。 WinHttpOpen 初始化应用程序对WinHTTP功能使用WinHttpOpenRequest 创建HTTP请求句柄。 WinHttpQueryAuthSchemes 返回服务器支持的授权方案。 WinHttpQueryDataAvailable 返回可立即与读取数据的字节数 WinHttpReadData。 WinHttpQueryHeaders 检索与HTTP请求相关联的头信息。 WinHttpQueryOption 在指定的句柄上查询Internet选项。 WinHttpReadData 从WinHttpOpenRequest函数打开的句柄中读取数据 。 WinHttpReceiveResponse 结束由WinHttpSendRequest启动的HTTP请求 。 WinHttpResetAutoProxy 重置自动代理。 WinHttpSendRequest 将指定的请求发送到HTTP服务器。 WinHttpSetCredentials 将所需的授权凭证传递给服务器。 WinHttpSetDefaultProxyConfiguration 在注册表中设置默认的WinHTTP代理配置。 WinHttpSetOption 设置Internet选项。 WinHttpSetStatusCallback 设置WinHTTP可以在操作过程中进行调用的回调函数。 WinHttpSetTimeouts 设置涉及HTTP事务的各种超时。 WinHttpTimeFromSystemTime 根据HTTP版本1.0规范格式化日期和时间。 WinHttpTimeToSystemTime 获取HTTP时间/日期字符串并将其转换为 SYSTEMTIME结构。 WinHttpWriteData 将请求数据写入HTTP服务器。 WinHttpWebSocketClose 关闭WebSocket连接。 WinHttpWebSocketCompleteUpgrade 完成由WinHttpSendRequest启动的WebSocket握手。 WinHttpWebSocketQueryCloseStatus 获取服务器发送的关闭状态。 WinHttpWebSocketReceive 从WebSocket连接接收数据。 WinHttpWebSocketSend 通过WebSocket连接发送数据。 WinHttpWebSocketShutdown 向WebSocket连接发送关闭框架
### 回答1: VC是指Visual C++,而http协议是一种用于传输超文本的协议。在VC中使用http协议下载文件可以通过以下步骤实现。 首先,需要使用VC提供的网络编程库,如WinINet或WinHTTP。这些库提供了一些函数和类,用于与服务器进行http通信。 其次,需要创建一个http会话并与服务器建立连接。这可以通过调用相关函数来实现,如InternetOpen、InternetConnect等。 然后,要使用http协议发送下载文件的请求。可以使用HTTP GET方法向服务器发送请求,并指定要下载的文件的URL。这可以通过调用相关函数,如HttpOpenRequest、HttpSendRequest等来实现。 接下来,需要接收服务器的响应并获取待下载文件的信息,如文件大小、文件类型等。可以使用相关函数如HttpQueryInfo等来实现。 然后,创建本地文件并准备接收服务器发送的文件内容。可以使用相关函数如CreateFile、WriteFile等来实现。 最后,从服务器接收文件内容并将其写入本地文件中。这可以通过调用相关函数如InternetReadFile等来实现,直到接收完整个文件。 需要注意的是,在下载过程中需要严密地处理各种可能的错误和异常情况,以确保下载过程的稳定性和可靠性。 以上是使用VC中http协议下载文件的大致步骤,具体的实现细节可以根据具体情况和需求进行调整和完善。 ### 回答2: VC(Virtual Channel)是在计算机网络中用于传输数据的通道,它是一种在网络服务器和客户端之间建立连接并传输数据的协议。HTTP(Hypertext Transfer Protocol)是一种用于在网络上传输超文本的协议。 在使用VC协议下载文件时,通常会结合HTTP协议来完成文件的传输。具体的步骤如下: 1. 客户端向服务器发送HTTP请求,请求下载文件的URL。 2. 服务器接收到请求后,通过VC协议建立与客户端的连接,并开始传输文件。 3. 服务器根据请求的URL找到对应的文件,并以HTTP响应的形式返回给客户端。 4. 客户端接收到服务器的响应后,开始通过VC协议接收文件数据。 5. 服务器通过VC协议将文件数据分包发送给客户端,客户端通过接收这些包来获取完整的文件数据。 6. 当服务器传输完整个文件后,关闭与客户端的连接。 通过以上步骤,客户端就能够使用VC协议下载HTTP协议中请求的文件。VC协议通过在网络上建立可靠的连接来确保数据的完整性和准确性。而HTTP协议则负责处理客户端和服务器之间的通信,并提供对文件的请求和响应。 总结起来,通过VC和HTTP协议的结合,实现了在网络上下载文件的功能,使得文件的传输更加可靠和高效。 ### 回答3: VC是Visual C++的简称,是一种面向对象的编程语言。VC可以使用HTTP协议下载文件,以下是下载文件的步骤: 1. 创建一个Win32控制台应用程序的工程,打开VC开发环境。 2. 在代码中引入必要的头文件,如 <winhttp.h> 和 <iostream>。 3. 使用WinHTTP库进行HTTP请求,可以使用WinHttpOpen函数初始化HTTP会话,然后使用WinHttpOpenRequest函数创建HTTP请求。 4. 设置HTTP请求的参数,包括请求的method(GET或POST),URL、头部信息等。 5. 发送HTTP请求,使用WinHttpSendRequest函数发送请求到服务器。 6. 接收HTTP响应,使用WinHttpReceiveResponse函数接收服务器的响应。 7. 判断响应状态,使用WinHttpQueryHeaders函数查询响应头部信息,判断请求是否成功。 8. 创建本地文件,使用CreateFile函数创建保存文件的本地文件。 9. 读取响应数据,使用WinHttpReadData函数持续读取服务器返回的数据,并写入本地文件。 10. 关闭HTTP请求和会话,使用WinHttpCloseHandle函数关闭请求和会话句柄。 11. 关闭本地文件,使用CloseHandle函数关闭本地文件句柄。 以上是使用VC编写的HTTP协议下载文件的基本过程。在实际应用中,还可以添加错误处理、进度显示等功能,以提高用户体验。
评论 47
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值