使用WinHttp接口实现HTTP协议Get、Post和文件上传功能
? ? ? ? 我實現(xiàn)了一個最新版本的接口,詳見《實現(xiàn)HTTP協(xié)議Get、Post和文件上傳功能——使用WinHttp接口實現(xiàn)》。還有基于libcurl實現(xiàn)的版本《實現(xiàn)HTTP協(xié)議Get、Post和文件上傳功能——使用libcurl接口實現(xiàn)》。以下是原博文:
? ? ? ? 我們在做項目開發(fā)時,往往會涉及到和服務器通信。對于安全性要求不高的情況,一般我們采用HTTP通信協(xié)議。對于喜歡挑戰(zhàn)底層技術的同學,可能希望使用winsocket去完成通信過程。對于希望快速開發(fā)的同學,可能希望引入諸如CURL這類的第三方庫。而本文將介紹使用WinHttp接口實現(xiàn)Http協(xié)議的Get、Post和文件上傳的功能。為了保證我們代碼的精簡性和易擴展性,我并不打算做的很全面——比如我不考慮HTTPS和SSL以及轉碼等。我只是希望提供一個一目了然的結構,用于指出三種功能在代碼實現(xiàn)上的異同點。當然在這套代碼上增加HTTPS和SSL,以及用戶名\密碼機制也是非常簡單的。(轉載請指明出于breaksoftware的csdn博客)——新版本參閱《實現(xiàn)HTTP協(xié)議Get、Post和文件上傳功能——使用WinHttp接口實現(xiàn)》。
協(xié)議口語化描述
? ? ? ? 在項目中我們可能遇到的服務端同學對協(xié)議的描述:
- 你可以對http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2發(fā)送Get請求,參數(shù)的Key是userkey,Value是uservalue。
- 你可以對http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2發(fā)送Post請求,參數(shù)的Key是Data,Value是一個很長的數(shù)據(jù)。
- 你可以向http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2上傳一個文件,文件的Key是Data,Value是文件的內容。哦!別忘了,還要傳文件的MD5給我們,這個MD5的參數(shù)的Key是hash,Value是文件內容的MD5值。
? ? ? ? 在上述的描述中,你可能會遇到曾經(jīng)和服務端同學溝通的影子。一般來說,對于簡單協(xié)議的描述,上述基本可以涵蓋。我提煉出如下實現(xiàn),來實現(xiàn)相關功能,具體的函數(shù)說明會在之后給出
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方式發(fā)送,一個是使用Post方式。那就是說,除了發(fā)送方式不同,我們其他的設計“基本”可以認為是統(tǒng)一的。那么我們就先分析下URL及追加的參數(shù)。在討論這個之前,我先引進一個結構體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。我們可以這樣調用函數(shù),以解析出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指向一段用于保存協(xié)議類型的內存空間,dwSchemeLength用于描述傳入空間的大小(以TCHARS為單位的大小,下面其他空間大小描述字段都是以TCHARS單位)。對應于我們的例子,該空間將保存的結果是:http,dwSchemeLength的值是4(執(zhí)行后被修改)。
- 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中參數(shù)信息的一段內容空間。對應于我們的例子,該空間的值是?pk1=pv1&pk2=pk2
? ? ? ? 完整的實現(xiàn)代碼是
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。在我們口頭描述的協(xié)議中,還要增加一個參數(shù),即userkey=uservalue。那么完整的參數(shù)將是:?pk1=pv1&pk2=pk2&userkey=uservalue。為了讓這種參數(shù)的拼接具有易擴展性,我將參數(shù)信息分拆并保存到一個Map中。然后繼承于我們基類的派生類,可以根據(jù)自己的業(yè)務特點,向我們這個Map中新增其他Key-Value對,最后我們統(tǒng)一生成參數(shù)串。這兒需要指出的是,這種方法只是針對GET協(xié)議,因為GET協(xié)議發(fā)送參數(shù)的方法是一致的。而POST和文件上傳協(xié)議都不需要對lpszExtraInfo解析參數(shù),它將作為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);
}
? ? ? ? 同時,我們的基類提供一個純虛函數(shù),讓繼承類去自由增加參數(shù)
virtual VOID AddExtInfo(VecStParam& VecExtInfo) = 0;
? ? ? ? 于是在CHttpClientSyn::InitializeHttp函數(shù)中,執(zhí)行
std::wstring wstrExtraInfo = urlCom.lpszExtraInfo;ParseParams(wstrExtraInfo);AddExtInfo(m_VecExtInfo);
? ? ? ? 在本文的后面部分,我會給出各繼承類對該方法的實現(xiàn)。
? ? ? ? 至此,各種該準備的數(shù)據(jù)已經(jīng)OK了。現(xiàn)在,我們要初始化Get、Post和上傳結構都要環(huán)境——打開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;
}
? ? ? ? ? 至此,三種方式相同的執(zhí)行路徑已經(jīng)結束。我們要依據(jù)繼承類的調用方式,決定走三種方式中的哪個
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方式發(fā)送數(shù)據(jù)
? ? ? ?Get方式是最常用的HTTP方式。它的實現(xiàn)也很簡單,只要將除了Host和Port部分(上例中/urlpath?pk1=pv1&pk2=pk2&userkey=uservalue,注意那個?號)一次性發(fā)給服務器即可。注意這個發(fā)送要使用WinHttpOpenRequest來完成。
BOOL CHttpClientSyn::TransmiteDataToServerByGet()
{BOOL bSuc = FALSE;do {std::wstring wstrUrlPathAppend = m_wstrUrlPath;// 采用Get方式時,要將參數(shù)放在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;}
? ? ? ? 在請求打開后,我們還要設置頭信息。我這兒將設置頭信息的函數(shù)設置為純虛函數(shù),這樣繼承類就要自己實現(xiàn)這個函數(shù),并設置自己的頭信息。
ModifyRequestHeader(m_hRequest);
? ? ? ? 頭信息設置好后,我們就可以發(fā)送請求了
if ( FALSE == WinHttpSendRequest( m_hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) ){break;}bSuc = TRUE;} while (0);return bSuc;}
? ? ? ? 過程就是如此簡單。
? ? ? ? 我們再看下繼承類的相關實現(xiàn)
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方式發(fā)送數(shù)據(jù)
? ? ? ? Post方式和Get方式的有若干實現(xiàn)的區(qū)別。首先,我們在打開Request的時候,要設置Post方式,同時要設置打開的是UrlPath,而不是攜帶參數(shù)的部分(即上例中的/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);
? ? ? ? 最后便是數(shù)據(jù)發(fā)送。我們回顧下2中的描述:
? ? ? ? 你可以對http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2發(fā)送Post請求,參數(shù)的Key是Data,Value是一個很長的數(shù)據(jù)。
? ? ? ? 可以看出,我們要發(fā)送兩批數(shù)據(jù):一個是固有參數(shù)pk1=pv2&pk2=pv2;一個是不確定的參數(shù)“參數(shù)的Key是Data,Value是一個很長的數(shù)據(jù)”。我也是按這種描述設計的:
? ? ? ? 先將容易確定的固定參數(shù)發(fā)送出去
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;}}
? ? ? ? 這兒做了一個偷懶的處理,我將數(shù)據(jù)一次性寫入。當然比較嚴謹?shù)淖龇ㄊ歉鶕?jù)每次成功的長度遞減數(shù)據(jù)發(fā)送。
? ? ? ? 為了支持這種可能是Data對應的不確定數(shù)據(jù)的發(fā)送,我在基類中暴露了一個接口,供繼承函數(shù)類以向基類邏輯提供數(shù)據(jù)。我這兒分而治之,是為了區(qū)分這些數(shù)據(jù)和之前的固有數(shù)據(jù)的區(qū)別——固有數(shù)據(jù)是字符串,而自定義數(shù)據(jù)可能是2進制流。
// 靜態(tài)分配一個數(shù)組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字節(jié)的空間。通過繼承類(或基類,基類直接返回False)GetData函數(shù)不停填充數(shù)據(jù),并調用WinHttpWriteData發(fā)送數(shù)據(jù)。我們看下繼承類的實現(xiàn)
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用于標記當前已經(jīng)讀取到哪個位置。這樣這些函數(shù)將保證,基類將可以將數(shù)據(jù)讀取完畢。這兒可能有個要注意的就是:要將“&Data=”傳入lpData地址空間中。
向服務器上傳文件
? ? ? ? 向服務器上傳文件,可能是使用的頻率僅次于Get的一種方式。在編寫上傳功能時,我還是踩中了不少坑,這也是我決心將這些整理出來分享的一個很重要原因。
? ? ? ? 最開始時,我以為上傳文件無非就是一個Post請求。后來經(jīng)過一些磨難后,發(fā)現(xiàn)事實僅非如此。
? ? ? ? 首先我們看下和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;}
? ? ? ? 這僅僅是調用流程的相同,而不同點,我都將其“埋伏”在繼承類中。我們先看繼承類中頭設置的實現(xiàn)
#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的,即多部分組成。那么如何分隔各部分數(shù)據(jù)呢?我們使用一個分隔符,該分隔符就是上面代碼中的"--h1o9n8e6y6k6k"。我們還要在頭中告訴服務器:我們要用什么來做分隔符。于是你看到這個頭的完整信息是:
Content-Type: multipart/form-data; boundary=--h1o9n8e6y6k6k
? ? ? ? 在之后,我們還會陸續(xù)提到這個分隔字段。它將貫穿整個Post過程。
? ? ? ? 頭信息設置好后,我將發(fā)送文件
// 靜態(tài)分配一個數(shù)組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 );
? ? ? ? 文件發(fā)送好之后,我們再將URL中帶的pk1=pv1&pk2=pv2信息發(fā)送出去。
if ( 0 != strExtInfo.length() ) {if ( FALSE == WinHttpWriteData(m_hRequest, strExtInfo.c_str(), strExtInfo.length(), NULL ) ) {break;}}bSuc = bSendOK;} while (0);return bSuc;
}
? ? ? ? 我之所以如此快速的將這個流程過掉,而沒細分講解,是希望大家避免一個坑——發(fā)送順序問題。如果這兩個順序反了,服務器可能接收不到文件。原因是在文件段(之后會介紹文件段是什么,這個名字是我臨時起意)之后,我們還要向服務器發(fā)一個普通數(shù)據(jù)段(之后會介紹普通數(shù)據(jù)段,這個名字也是我臨時起意)。否則服務器會一直等待,認為我們文件沒傳完,哪怕我們在WinHttpSendRequest設置了正確的大小。當然這個順序也不是一定要如此,我們可以將普通數(shù)據(jù)(pk1=pv1&pk2=pv2)先發(fā)送,再發(fā)送文件段,最后再發(fā)送一個無用的數(shù)據(jù)段。
? ? ? ? 我們先關注一下這段代碼
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這個字段我們已經(jīng)在之前講解過,它是需要使用WinHttpAddRequestHeaders設置的頭信息。m_wstrBlockStart 是我們整個大的數(shù)據(jù)塊(包括文件段和數(shù)據(jù)段)的一開始的標識符,即它是要“最”先傳送給服務器。m_wstrBlockEnd應該可以猜出來了——它是整個大數(shù)據(jù)塊的結尾符。即我們整個數(shù)據(jù)將要被m_wstrBlockStart和m_wstrBlockEnd包含。
----h1o9n8e6y6k6k(用\r\n)
數(shù)據(jù)
----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)
? ? ? ? 我們再看下文件發(fā)送的流程,其實就是數(shù)據(jù)填充的過程
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;
}
? ? ? ? 最后我們看下數(shù)據(jù)段的發(fā)送
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;
}
? ? ? ? 數(shù)據(jù)段也要使用分隔符分隔。并用固定的格式傳送參數(shù)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之前,我們要獲取正確的發(fā)送包的大小。
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
? ? ? ? 再次強烈建議,請看新版本《實現(xiàn)HTTP協(xié)議Get、Post和文件上傳功能——使用WinHttp接口實現(xiàn)》《實現(xiàn)HTTP協(xié)議Get、Post和文件上傳功能——使用libcurl接口實現(xiàn)》。
總結
以上是生活随笔為你收集整理的使用WinHttp接口实现HTTP协议Get、Post和文件上传功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一种将快捷方式从开始菜单“常用应用”的中
- 下一篇: PE文件和COFF文件格式分析——导出表