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

来源:互联网 发布:众筹系统源码 编辑:程序博客网 时间:2024/06/05 09:05

http://blog.csdn.net/breaksoftware/article/details/17232483


我实现了一个最新版本的接口,详见《实现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值。
        在上述的描述中,你可能会遇到曾经和服务端同学沟通的影子。一般来说,对于简单协议的描述,上述基本可以涵盖。我提炼出如下实现,来实现相关功能,具体的函数说明会在之后给出
[cpp] view plain copy
  1. BOOL CHttpClientSyn::TransmiteData( const std::wstring& wstrUrl, EType eType, DWORD dwTimeout)  
  2. {  
  3.     BOOL bSuc = FALSE;  
  4.     do {  
  5.         if ( FALSE == InitializeHttp(wstrUrl, dwTimeout)) {  
  6.             break;  
  7.         }  
  8.         if ( FALSE == TransmiteData(eType) ) {  
  9.             break;  
  10.         }  
  11.         ReceiveData();  
  12.         UninitializeHttp();  
  13.         bSuc = TRUE;  
  14.     } while (0);  
  15.     return bSuc;  
  16. }  

信息准备

        我们看一下1和2描述内容。可以看出,其主要差别就是一个是使用Get方式发送,一个是使用Post方式。那就是说,除了发送方式不同,我们其他的设计“基本”可以认为是统一的。那么我们就先分析下URL及追加的参数。在讨论这个之前,我先引进一个结构体URL_COMPONENTS
[cpp] view plain copy
  1. typedef struct {  
  2.   DWORD           dwStructSize;  
  3.   LPTSTR          lpszScheme;  
  4.   DWORD           dwSchemeLength;  
  5.   INTERNET_SCHEME nScheme;  
  6.   LPTSTR          lpszHostName;  
  7.   DWORD           dwHostNameLength;  
  8.   INTERNET_PORT   nPort;  
  9.   LPTSTR          lpszUserName;  
  10.   DWORD           dwUserNameLength;  
  11.   LPTSTR          lpszPassword;  
  12.   DWORD           dwPasswordLength;  
  13.   LPTSTR          lpszUrlPath;  
  14.   DWORD           dwUrlPathLength;  
  15.   LPTSTR          lpszExtraInfo;  
  16.   DWORD           dwExtraInfoLength;  
  17. } URL_COMPONENTS, *LPURL_COMPONENTS;  
        详细的说明,可以查看MSDN。我们可以这样调用函数,以解析出URL中包含的信息
[cpp] view plain copy
  1. URL_COMPONENTS urlCom;  
  2. ……  
  3. 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
        完整的实现代码是
[cpp] view plain copy
  1. BOOL CHttpClientSyn::InitializeHttp( const std::wstring& wstrUrl, DWORD dwTimeout)  
  2. {  
  3.     BOOL bSuc = FALSE;  
  4.     do {  
  5.         URL_COMPONENTS urlCom;  
  6.         memset(&urlCom, 0, sizeof(urlCom));  
  7.         urlCom.dwStructSize = sizeof(urlCom);  
  8.         WCHAR wchScheme[64] = {0};  
  9.         urlCom.lpszScheme = wchScheme;  
  10.         urlCom.dwSchemeLength = ARRAYSIZE(wchScheme);  
  11.         WCHAR wchHostName[1024] = {0};  
  12.         urlCom.lpszHostName = wchHostName;  
  13.         urlCom.dwHostNameLength = ARRAYSIZE(wchHostName);  
  14.         WCHAR wchUrlPath[1024] = {0};  
  15.         urlCom.lpszUrlPath = wchUrlPath;  
  16.         urlCom.dwUrlPathLength = ARRAYSIZE(wchUrlPath);  
  17.         WCHAR wchExtraInfo[1024] = {0};  
  18.         urlCom.lpszExtraInfo = wchExtraInfo;  
  19.         urlCom.dwExtraInfoLength = ARRAYSIZE(wchExtraInfo);  
  20.   
  21.         if ( FALSE == WinHttpCrackUrl( wstrUrl.c_str(), wstrUrl.length(), ICU_ESCAPE, &urlCom) ) {  
  22.             break;  
  23.         }  
  24.   
  25.         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的一部分在之后的操作中被使用。
[cpp] view plain copy
  1. VOID CHttpClientSyn::ParseParams(const std::wstring& wstrExtraInfo)  
  2. {  
  3.     int nPos = 0;  
  4.     nPos = wstrExtraInfo.find('?');  
  5.     if ( -1 == nPos ) {  
  6.         return;  
  7.     }  
  8.     std::wstring wstrParam = wstrExtraInfo;  
  9.     int nStaticMaxParamCount = MAXSTATICPARAMCOUNT;  
  10.     do{  
  11.         wstrParam = wstrParam.substr(nPos + 1, wstrExtraInfo.length() - nPos - 1);  
  12.         nPos = wstrParam.find('&', nPos);  
  13.         std::wstring wstrKeyValuePair;  
  14.   
  15.         if ( -1 == nPos ) {  
  16.             wstrKeyValuePair = wstrParam;  
  17.         }  
  18.         else {  
  19.             wstrKeyValuePair = wstrParam.substr(0, nPos);  
  20.         }  
  21.           
  22.         int nSp = wstrKeyValuePair.find('=');  
  23.         if ( -1 != nSp ) {  
  24.             StParam stParam;  
  25.             stParam.wstrKey = wstrKeyValuePair.substr(0, nSp);  
  26.             stParam.wstrValue = wstrKeyValuePair.substr( nSp + 1, wstrKeyValuePair.length() - nSp - 1);  
  27.             m_VecExtInfo.push_back(stParam);  
  28.         }  
  29.     }while(-1 != nPos && nStaticMaxParamCount > 0);  
  30. }  
        同时,我们的基类提供一个纯虚函数,让继承类去自由增加参数
[cpp] view plain copy
  1. virtual VOID AddExtInfo(VecStParam& VecExtInfo) = 0;  
        于是在CHttpClientSyn::InitializeHttp函数中,执行
[cpp] view plain copy
  1. std::wstring wstrExtraInfo = urlCom.lpszExtraInfo;  
  2. ParseParams(wstrExtraInfo);  
  3. AddExtInfo(m_VecExtInfo);  
        在本文的后面部分,我会给出各继承类对该方法的实现。
        至此,各种该准备的数据已经OK了。现在,我们要初始化Get、Post和上传结构都要环境——打开Session并连接服务器

打开Session并连接服务器

        这部分的代码,三种方式是一致的。具体也没什么好说明的,直接上代码(还是之前的CHttpClientSyn::InitializeHttp中)
[cpp] view plain copy
  1.         m_hSession = WinHttpOpen(NULL, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0 );   
  2.         if ( NULL == m_hSession ) {  
  3.             break;  
  4.         }  
  5.   
  6.         m_hConnect = WinHttpConnect( m_hSession, urlCom.lpszHostName, urlCom.nPort, 0 );  
  7.         if ( NULL == m_hConnect ) {  
  8.             break;  
  9.         }  
  10.   
  11.         m_wstrUrlPath = urlCom.lpszUrlPath;  
  12.   
  13.         bSuc = TRUE;  
  14.     } while (0);  
  15.     return bSuc;  
  16. }  
          至此,三种方式相同的执行路径已经结束。我们要依据继承类的调用方式,决定走三种方式中的哪个
[cpp] view plain copy
  1. BOOL CHttpClientSyn::TransmiteData(EType eType)  
  2. {  
  3.     BOOL bSuc = FALSE;  
  4.     switch (eType) {  
  5.     case eGet:{  
  6.         bSuc = TransmiteDataToServerByGet();  
  7.         }break;  
  8.     case ePost:{  
  9.         bSuc = TransmiteDataToServerByPost();  
  10.         }break;  
  11.     case eUpload:{  
  12.         bSuc = TransmiteDataToServerByUpload();  
  13.         }break;  
  14.     defaultbreak;  
  15.     }  
  16.     return bSuc;  
  17. }  

使用Get方式发送数据

       Get方式是最常用的HTTP方式。它的实现也很简单,只要将除了Host和Port部分(上例中/urlpath?pk1=pv1&pk2=pk2&userkey=uservalue,注意那个?号)一次性发给服务器即可。注意这个发送要使用WinHttpOpenRequest来完成。
[cpp] view plain copy
  1. BOOL CHttpClientSyn::TransmiteDataToServerByGet()  
  2. {  
  3.     BOOL bSuc = FALSE;  
  4.     do {  
  5.   
  6.         std::wstring wstrUrlPathAppend = m_wstrUrlPath;  
  7.         // 采用Get方式时,要将参数放在OpenRequest中  
  8.         if ( false == wstrUrlPathAppend.empty() ) {  
  9.             wstrUrlPathAppend += L"?";  
  10.         }  
  11.         wstrUrlPathAppend += GenerateExtInfo(m_VecExtInfo);  
  12.   
  13.         m_hRequest = WinHttpOpenRequest(m_hConnect, L"Get",  
  14.             wstrUrlPathAppend.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);  
  15.         if ( NULL == m_hRequest ) {  
  16.             break;  
  17.         }  
        在请求打开后,我们还要设置头信息。我这儿将设置头信息的函数设置为纯虚函数,这样继承类就要自己实现这个函数,并设置自己的头信息。
[cpp] view plain copy
  1. ModifyRequestHeader(m_hRequest);  
        头信息设置好后,我们就可以发送请求了
[cpp] view plain copy
  1.         if ( FALSE == WinHttpSendRequest( m_hRequest,   
  2.             WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) )  
  3.         {  
  4.             break;  
  5.         }  
  6.   
  7.         bSuc = TRUE;  
  8.     } while (0);  
  9.     return bSuc;  
  10.   
  11. }  
        过程就是如此简单。
        我们再看下继承类的相关实现
[cpp] view plain copy
  1. std::wstring CHttpTransByGet::GenerateExtInfo( const VecStParam& VecExtInfo )  
  2. {  
  3.     std::wstring wstrExtInf;  
  4.     for ( VecStParamCIter it = VecExtInfo.begin(); it != VecExtInfo.end(); it++ ) {  
  5.         if ( false == wstrExtInf.empty() ) {  
  6.             wstrExtInf += L"&";  
  7.         }  
  8.         wstrExtInf += it->wstrKey;  
  9.         wstrExtInf += L"=";  
  10.         wstrExtInf += it->wstrValue;  
  11.     }  
  12.     return wstrExtInf;  
  13. }  
  14.   
  15. BOOL CHttpTransByGet::ModifyRequestHeader( HINTERNET hRequest )  
  16. {  
  17.     std::wstring wstrHeader[] = { L"Content-type: application/x-www-form-urlencoded\r\n"};  
  18.     for ( size_t i = 0; i < ARRAYSIZE(wstrHeader); i++ ) {  
  19.         WinHttpAddRequestHeaders(hRequest, wstrHeader[i].c_str(), wstrHeader[i].length(), WINHTTP_ADDREQ_FLAG_ADD);  
  20.     }  
  21.     return TRUE;  
  22. }  
  23.   
  24. VOID CHttpTransByGet::AddExtInfo( VecStParam& VecExtInfo )  
  25. {  
  26.     for ( VecStParamCIter it = m_vecParam.begin(); it != m_vecParam.end(); it++ ) {  
  27.         VecExtInfo.push_back(*it);  
  28.     }  
  29. }  
        这段代码,没有多少要注意的,只要注意下Get方式要设置的头信息。

使用Post方式发送数据

        Post方式和Get方式的有若干实现的区别。首先,我们在打开Request的时候,要设置Post方式,同时要设置打开的是UrlPath,而不是携带参数的部分(即上例中的/urlpath)。
[cpp] view plain copy
  1. BOOL CHttpClientSyn::TransmiteDataToServerByPost()  
  2. {  
  3.     BOOL bSuc = FALSE;  
  4.     do {  
  5.         m_hRequest = WinHttpOpenRequest(m_hConnect, L"Post",  
  6.             m_wstrUrlPath.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);  
  7.         if ( NULL == m_hRequest ) {  
  8.             break;  
  9.         }  
       之后,我们也是要设置头信息。这儿我们可以和上面Get方式一样设置
[cpp] view plain copy
  1. 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是一个很长的数据”。我也是按这种描述设计的:
        先将容易确定的固定参数发送出去
[cpp] view plain copy
  1. std::wstring wstrExtInfo = GenerateExtInfo(m_VecExtInfo);  
  2. std::string strExtInfo = CW2A(wstrExtInfo.c_str(), CP_UTF8);  
  3.   
  4. DWORD dwTotal = strExtInfo.length();  
  5. dwTotal += GetDataSize();  
  6.   
  7. if ( FALSE == WinHttpSendRequest( m_hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, dwTotal, 0)) {  
  8.     break;  
  9. }  
  10.   
  11. if ( 0 != strExtInfo.length() ) {  
  12.     // 默认可以一次全部写完  
  13.     if ( FALSE == WinHttpWriteData(m_hRequest, strExtInfo.c_str(), strExtInfo.length(), NULL ) ) {  
  14.         break;  
  15.     }  
  16. }  
        这儿做了一个偷懒的处理,我将数据一次性写入。当然比较严谨的做法是根据每次成功的长度递减数据发送。
        为了支持这种可能是Data对应的不确定数据的发送,我在基类中暴露了一个接口,供继承函数类以向基类逻辑提供数据。我这儿分而治之,是为了区分这些数据和之前的固有数据的区别——固有数据是字符串,而自定义数据可能是2进制流。
[cpp] view plain copy
  1.     // 静态分配一个数组  
  2.     BYTE buffer[1024]= {0};  
  3.     BOOL bContinue = FALSE;  
  4.     BOOL bSendOK = FALSE;  
  5.   
  6.     do {  
  7.         DWORD dwBufferLength = sizeof(buffer);  
  8.         SecureZeroMemory(buffer, dwBufferLength);  
  9.         DWORD dwWriteSize = 0;  
  10.         bContinue = GetData(buffer, dwBufferLength, dwWriteSize);  
  11.         if ( 0 != dwWriteSize ) {  
  12.             bSendOK= WinHttpWriteData( m_hRequest, buffer, dwWriteSize, NULL);  
  13.         }  
  14.         else {  
  15.             bSendOK = TRUE;  
  16.         }  
  17.     } while ( bContinue && bSendOK );  
  18.   
  19.     bSuc = bSendOK;  
  20.   
  21. while (0);  
  22. return bSuc;  
        这个逻辑,分配了一个1024字节的空间。通过继承类(或基类,基类直接返回False)GetData函数不停填充数据,并调用WinHttpWriteData发送数据。我们看下继承类的实现
[cpp] view plain copy
  1. DWORD CHttpTransByPost::GetDataSize()  
  2. {  
  3.     return m_dwDataSize;  
  4. }  
  5.   
  6. BOOL CHttpTransByPost::GetData( LPVOID lpBuffer, DWORD dwBufferSize, DWORD& dwWrite )  
  7. {  
  8.     BOOL bContinue = TRUE;  
  9.     dwWrite = 0;  
  10.     if ( m_dwDataSize > m_dwWriteIndex + dwBufferSize ) {  
  11.         dwWrite = dwBufferSize;  
  12.     }  
  13.     else {  
  14.         dwWrite = m_dwDataSize - m_dwWriteIndex;  
  15.         bContinue = FALSE;  
  16.     }  
  17.   
  18.     if ( 0 != memcpy_s(lpBuffer, dwBufferSize, (LPBYTE)m_lpData + m_dwWriteIndex, dwWrite) ){  
  19.         bContinue = FALSE;  
  20.     }  
  21.   
  22.     return bContinue;  
  23. }  
  24.   
  25. BOOL CHttpTransByPost::TransDataToServer( const std::wstring& wstrUrl, DWORD dwTimeout,   
  26.     VecStParam& vecParam, LPVOID lpData, DWORD dwDataLenInBytes )  
  27. {  
  28.     m_lpData = lpData;  
  29.     m_dwDataSize = dwDataLenInBytes;  
  30.     m_vecParam.assign(vecParam.begin(), vecParam.end());  
  31.     m_dwWriteIndex = 0;  
  32.     return TransmiteData(wstrUrl, eGet, dwTimeout);  
  33. }  
        m_dwWriteIndex用于标记当前已经读取到哪个位置。这样这些函数将保证,基类将可以将数据读取完毕。这儿可能有个要注意的就是:要将“&Data=”传入lpData地址空间中。

向服务器上传文件

        向服务器上传文件,可能是使用的频率仅次于Get的一种方式。在编写上传功能时,我还是踩中了不少坑,这也是我决心将这些整理出来分享的一个很重要原因。
        最开始时,我以为上传文件无非就是一个Post请求。后来经过一些磨难后,发现事实仅非如此。
        首先我们看下和Post调用相同的地方
[cpp] view plain copy
  1. BOOL CHttpClientSyn::TransmiteDataToServerByUpload()  
  2. {  
  3.     BOOL bSuc = FALSE;  
  4.     do {  
  5.         m_hRequest = WinHttpOpenRequest(m_hConnect, L"Post",  
  6.             m_wstrUrlPath.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);  
  7.         if ( NULL == m_hRequest ) {  
  8.             break;  
  9.         }  
  10.   
  11.         ModifyRequestHeader(m_hRequest);  
  12.   
  13.         std::wstring wstrExtInfo = GenerateExtInfo(m_VecExtInfo);  
  14.         std::string strExtInfo = CW2A(wstrExtInfo.c_str(), CP_UTF8);  
  15.   
  16.         DWORD dwTotal = strExtInfo.length();  
  17.         dwTotal += GetDataSize();  
  18.   
  19.         if ( FALSE == WinHttpSendRequest( m_hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, dwTotal, 0)) {  
  20.             break;  
  21.         }  
        这仅仅是调用流程的相同,而不同点,我都将其“埋伏”在继承类中。我们先看继承类中头设置的实现
[cpp] view plain copy
  1. #define BOUNDARYPART L"--h1o9n8e6y6k6k"  
  2. ……  
  3.     m_wstrNewHeader = L"Content-Type: multipart/form-data; boundary=";  
  4.     m_wstrNewHeader += BOUNDARYPART;  
  5.     m_wstrNewHeader += L"\r\n";  
  6. ……  
  7. BOOL CHttpUploadFiles::ModifyRequestHeader( HINTERNET hRequest )  
  8. {  
  9.     return ::WinHttpAddRequestHeaders(hRequest, m_wstrNewHeader.c_str(),   
  10.         m_wstrNewHeader.length(), WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE);  
  11. }  
        Content-Type: multipart/form-data;相关说明可以参看rfc2388,至于更详细的文件上传的rfc可以参看rfc1867。本文只从使用的角度去讲解,所以不会去分析RFC文档。读者只要知道我们要设置这个头即可。从这个头可以看出来,我们这次请求是一个MultiPart的,即多部分组成。那么如何分隔各部分数据呢?我们使用一个分隔符,该分隔符就是上面代码中的"--h1o9n8e6y6k6k"。我们还要在头中告诉服务器:我们要用什么来做分隔符。于是你看到这个头的完整信息是:
[plain] view plain copy
  1. Content-Type: multipart/form-data; boundary=--h1o9n8e6y6k6k  
        在之后,我们还会陆续提到这个分隔字段。它将贯穿整个Post过程。
        头信息设置好后,我将发送文件
[cpp] view plain copy
  1. // 静态分配一个数组  
  2. BYTE buffer[1024]= {0};  
  3. BOOL bContinue = FALSE;  
  4. BOOL bSendOK = FALSE;  
  5.   
  6. do {  
  7.     DWORD dwBufferLength = sizeof(buffer);  
  8.     SecureZeroMemory(buffer, dwBufferLength);  
  9.     DWORD dwWriteSize = 0;  
  10.     bContinue = GetData(buffer, dwBufferLength, dwWriteSize);  
  11.     if ( 0 != dwWriteSize ) {  
  12.         bSendOK= WinHttpWriteData( m_hRequest, buffer, dwWriteSize, NULL);  
  13.     }  
  14.     else {  
  15.         bSendOK = TRUE;  
  16.     }  
  17. while ( bContinue && bSendOK );  
        文件发送好之后,我们再将URL中带的pk1=pv1&pk2=pv2信息发送出去。
[cpp] view plain copy
  1. if ( 0 != strExtInfo.length() ) {  
  2.             if ( FALSE == WinHttpWriteData(m_hRequest, strExtInfo.c_str(), strExtInfo.length(), NULL ) ) {  
  3.                 break;  
  4.             }  
  5.         }  
  6.         bSuc = bSendOK;  
  7.     } while (0);  
  8.     return bSuc;  
  9. }  
        我之所以如此快速的将这个流程过掉,而没细分讲解,是希望大家避免一个坑——发送顺序问题。如果这两个顺序反了,服务器可能接收不到文件。原因是在文件段(之后会介绍文件段是什么,这个名字是我临时起意)之后,我们还要向服务器发一个普通数据段(之后会介绍普通数据段,这个名字也是我临时起意)。否则服务器会一直等待,认为我们文件没传完,哪怕我们在WinHttpSendRequest设置了正确的大小。当然这个顺序也不是一定要如此,我们可以将普通数据(pk1=pv1&pk2=pv2)先发送,再发送文件段,最后再发送一个无用的数据段。
        我们先关注一下这段代码
[cpp] view plain copy
  1. BOOL CHttpUploadFiles::TransDataToServer( const std::wstring wstrUrl, VecStParam& VecExtInfo,   
  2.     const std::wstring& wstrFilePath,  const std::wstring& wstrFileKey)  
  3. {  
  4.     m_wstrBlockStart = L"--";  
  5.     m_wstrBlockStart += BOUNDARYPART;  
  6.     m_wstrBlockStart += L"\r\n";  
  7.   
  8.     m_strBlockStartUTF8 = CW2A(m_wstrBlockStart.c_str(), CP_UTF8);  
  9.   
  10.     m_wstrBlockEnd =  L"\r\n--";  
  11.     m_wstrBlockEnd += BOUNDARYPART;  
  12.     m_wstrBlockEnd +=  L"--\r\n";  
  13.   
  14.     m_wstrNewHeader = L"Content-Type: multipart/form-data; boundary=";  
  15.     m_wstrNewHeader += BOUNDARYPART;  
  16.     m_wstrNewHeader += L"\r\n";  
        m_wstrNewHeader这个字段我们已经在之前讲解过,它是需要使用WinHttpAddRequestHeaders设置的头信息。m_wstrBlockStart 是我们整个大的数据块(包括文件段和数据段)的一开始的标识符,即它是要“最”先传送给服务器。m_wstrBlockEnd应该可以猜出来了——它是整个大数据块的结尾符。即我们整个数据将要被m_wstrBlockStart和m_wstrBlockEnd包含。
[plain] view plain copy
  1. ----h1o9n8e6y6k6k(用\r\n)  
  2. 数据  
  3. ----h1o9n8e6y6k6k--(用\r\n)  
        然后我们看下文件段。文件段一开始是有这样的一个头
[cpp] view plain copy
  1. std::wstring wstrUploadFileHeader;  
  2. wstrUploadFileHeader = m_wstrBlockStart;  
  3. wstrUploadFileHeader += L"Content-Disposition: form-data; name=\"";  
  4. wstrUploadFileHeader += wstrFileKey;  
  5. wstrUploadFileHeader += L"\";";  
  6. wstrUploadFileHeader += L"filename=\"";  
  7. wstrUploadFileHeader += wstrFileName;  
  8. wstrUploadFileHeader += L"\"\r\n";  
  9. wstrUploadFileHeader += L"Content-Type:application/octet-stream\r\n\r\n";  
  10.   
  11. m_strUploadFileHeaderUTF8 = CW2A(wstrUploadFileHeader.c_str(), CP_UTF8);  
        这个头包含了文件名和文件内容对应的Key。以描述3 为例,这个Key就是name的值,就是Data。
[plain] view plain copy
  1. ----h1o9n8e6y6k6k(用\r\n)  
  2. Content-Disposition: form-data; name="Data";filename="uploadfilename.bin"(用\r\n)  
  3. Content-Type:application/octet-stream(用\r\n\r\n)  
  4.   
  5. 文件内容  
  6. ----h1o9n8e6y6k6k--(用\r\n)  
        我们再看下文件发送的流程,其实就是数据填充的过程
[cpp] view plain copy
  1. BOOL CHttpUploadFiles::GetData( LPVOID lpBuffer, DWORD dwBufferSize, DWORD& dwWrite )  
  2. {  
  3.     if ( m_strUploadFileHeaderUTF8.empty() ) {  
  4.         return FALSE;  
  5.     }  
  6.   
  7.     if ( EHeader == m_ReadInfo.eType ) {  
  8.         if ( FALSE == ReadFromString(m_strUploadFileHeaderUTF8, lpBuffer, dwBufferSize, m_ReadInfo.dwReadIndex, dwWrite ) ) {  
  9.             return FALSE;  
  10.         }  
  11.         m_ReadInfo.dwReadIndex += dwWrite;  
  12.         if ( m_ReadInfo.dwReadIndex == m_strUploadFileHeaderUTF8.length() ) {  
  13.             m_ReadInfo.eType = EFile;  
  14.             m_ReadInfo.dwReadIndex = 0;  
  15.             return TRUE;  
  16.         }  
  17.     }  
  18.     else if ( EFile == m_ReadInfo.eType ){  
  19.         OVERLAPPED ov;  
  20.         memset(&ov, 0, sizeof(ov));  
  21.         ov.Offset = m_ReadInfo.dwReadIndex;  
  22.   
  23.         HANDLE hFile = CreateFile( m_wstrFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL );  
  24.         BOOL bContinue = FALSE;  
  25.         DWORD dwFileSize = 0;  
  26.         do {  
  27.             if ( INVALID_HANDLE_VALUE == hFile ) {  
  28.                 dwWrite = 0;  
  29.                 break;  
  30.             }  
  31.   
  32.             LARGE_INTEGER lgFileSize = {0};  
  33.             if ( FALSE == GetFileSizeEx(hFile, &lgFileSize) ) {  
  34.                 break;  
  35.             }  
  36.   
  37.             if ( FALSE == ReadFile(hFile, lpBuffer, dwBufferSize, &dwWrite, &ov)) {  
  38.                 break;  
  39.             }  
  40.             dwFileSize = lgFileSize.LowPart;  
  41.             bContinue = TRUE;  
  42.         } while (0);  
  43.           
  44.         if ( INVALID_HANDLE_VALUE != hFile ) {  
  45.             CloseHandle(hFile);  
  46.             hFile = NULL;  
  47.         }  
  48.         m_ReadInfo.dwReadIndex += dwWrite;  
  49.         if ( m_ReadInfo.dwReadIndex == dwFileSize ) {  
  50.             m_ReadInfo.dwReadIndex = 0;  
  51.             bContinue = FALSE;  
  52.         }  
  53.   
  54.         return bContinue;  
  55.     }  
  56.   
  57.     return TRUE;  
  58. }  
        最后我们看下数据段的发送
[cpp] view plain copy
  1. std::wstring CHttpUploadFiles::GenerateExtInfo( const VecStParam& VecExtInfo )  
  2. {  
  3.     std::wstring wstrInfo = L"\r\n";  
  4.     for ( VecStParamCIter it = VecExtInfo.begin(); it != VecExtInfo.end(); it++ ) {  
  5.         wstrInfo += m_wstrBlockStart;  
  6.         wstrInfo += L"Content-Disposition:form-data;";  
  7.         wstrInfo += L"name=";  
  8.         wstrInfo += L"\"";  
  9.         wstrInfo += it->wstrKey;  
  10.         wstrInfo += L"\"";  
  11.         wstrInfo += L"\r\n\r\n";  
  12.         wstrInfo += it->wstrValue;  
  13.         wstrInfo += L"\r\n";  
  14.     }  
  15.     wstrInfo += m_wstrBlockEnd;  
  16.     return wstrInfo;  
  17. }  
        数据段也要使用分隔符分隔。并用固定的格式传送参数pk1=pv1&pk2=pk2
[plain] view plain copy
  1. ----h1o9n8e6y6k6k(用\r\n)  
  2. Content-Disposition: form-data; name="Data";filename="uploadfilename.bin"(用\r\n)  
  3. Content-Type:application/octet-stream(用\r\n\r\n)  
  4.   
  5. 文件内容  
  6. ----h1o9n8e6y6k6k(用\r\n)  
  7. Content-Disposition:form-data;name="pk1"(用\r\n\r\n)  
  8.   
  9. pv1  
  10. ----h1o9n8e6y6k6k(用\r\n)  
  11. Content-Disposition:form-data;name="pk2"(用\r\n\r\n)  
  12.   
  13. pv2  
  14. ----h1o9n8e6y6k6k--(用\r\n)  
        至此,文件传输主要流程讲完了,最后还要提一句,就是在Post之前,我们要获取正确的发送包的大小。
[cpp] view plain copy
  1. DWORD CHttpUploadFiles::GetDataSize()  
  2. {  
  3.     if ( m_strUploadFileHeaderUTF8.empty() ) {  
  4.         return 0;  
  5.     }  
  6.   
  7.     DWORD dwFileSize = 0;  
  8.     HANDLE hFile = CreateFile( m_wstrFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL );  
  9.     do {  
  10.         if ( INVALID_HANDLE_VALUE == hFile ) {  
  11.             break;  
  12.         }  
  13.   
  14.         LARGE_INTEGER lgFileSize = {0};  
  15.         if ( FALSE == GetFileSizeEx(hFile, &lgFileSize) ) {  
  16.             break;  
  17.         }  
  18.   
  19.         if ( lgFileSize.HighPart > 0 || lgFileSize.LowPart > 0x00FFFFFF) {  
  20.             // 限制大小  
  21.             break;  
  22.         }  
  23.         dwFileSize = lgFileSize.LowPart;  
  24.     }while(0);  
  25.     if ( INVALID_HANDLE_VALUE != hFile ) {  
  26.         CloseHandle(hFile);  
  27.         hFile = NULL;  
  28.     }  
  29.   
  30.     DWORD dwDataSize = 0;  
  31.     if ( 0 != dwFileSize ) {  
  32.         dwDataSize = dwFileSize + m_strUploadFileHeaderUTF8.length();  
  33.     }  
  34.   
  35.     return dwDataSize;  
  36. }  
        HTTP三种方式讲解结束。附上对应的代码。
        在百度云盘上的代码的链接:http://pan.baidu.com/s/1i3DZEol 密码:2em8

阅读全文
0 0
原创粉丝点击