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