我实现了一个最新版本的接口,详见《实现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接口实现》。
在项目中我们可能遇到的服务端同学对协议的描述:
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; }
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);
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);在本文的后面部分,我会给出各继承类对该方法的实现。
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; }
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方式要设置的头信息。
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中的描述:
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; } }这儿做了一个偷懒的处理,我将数据一次性写入。当然比较严谨的做法是根据每次成功的长度递减数据发送。
// 静态分配一个数组 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地址空间中。
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)
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三种方式讲解结束。 附上对应的代码。