使用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接口实现》。
协议口语化描述
在项目中我们可能遇到的服务端同学对协议的描述:
- 你可以对http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2发送Get请求,参数的Key是userkey,Value是uservalue。
- 你可以对http://xxx.yyy.zzz:8324/urlpath?pk1=pv1&pk2=pk2发送Post请求,参数的Key是Data,Value是一个很长的数据。
- 你可以向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接口实现》。
相关文章:
收藏 | 一文带你总览知识蒸馏,详解经典论文
「免费学习 60 节公开课:投票页面,点击讲师头像」作者:凉爽的安迪来源 | 深度传送门(ID:deep_deliver)【导读】这是一篇关于【知识蒸馏】简述的文章,目的是想对自己对于知识蒸馏学习的内容和问题…

[工具推荐]用了TrueCrypt 再无难掩之隐
缘起:混在网络n多年了,手头总有些东西不想被别人看到的东西,由于小弟人品好,相貌佳,总有很多朋友喜欢用我的电脑玩啊玩啊……。 近日,冠希、柏芝等前辈以身示法,为我等上了很好一堂关于隐私保护…

利用phpmailer类邮件发送
<?phprequire("class.phpmailer.php"); //下载的文件必须放在该文件所在目录$mail new PHPMailer(); //建立邮件发送类$address "接收方邮箱"; //接收方地址$mail->IsSMTP(); //使用SMTP方式发送$…
据说这是大多数人【减肥】的真实写照
有句诗说得好 “冬天不减肥,夏天徒伤悲” 在这个人人储存脂肪的季节绝对是你甩掉脂肪的好时机(毕竟这是一个拼颜值的时代颜值是天生的,可是身材绝不能输)但是 据说大多数人的减肥经历其实是这样的减肥第一步管住嘴,迈开…
PE文件和COFF文件格式分析——导出表的应用——一种摘掉Inline钩子(Unhook)的方法
在日常应用中,某些程序往往会被第三方程序下钩子(hook)。如果被下钩子的进程是我们的进程,并且第三方钩子严重影响了我们的逻辑和流程,我们就需要把这些钩子摘掉(Unhook)。本件讲述一种在32位系统上,如何摘掉API钩子的思路和方法。…

设置列表字段为主键
转贴:Sample event handler to set a field as a pr imary key (enforce no duplicates) Got this as a request from a reader- how to prevent users from adding items with same titles as ones that already exist in the list. Codeusing System;using System.Collectio…

谁登录了你的linux
最近有一台数据库服务器自动重启。查了一下相关登录信息:查看linux下的用户登录日志,包括用户登录时所用的主机的ip:more /var/log/secure who /var/log/wtmp干了些什么? root账户下输入su - username 切换到username下输入 histo…
一种使用GDI+对图片尺寸和质量的压缩方法
今天同事向我询问图片压缩的算法,我想起大概两三年前做过的一个项目。其中包含了尺寸和质量两种压缩算法,并且支持JPEG、bmp、PNG等格式。今天把这段逻辑贴出来,供大家参考。(转载请指明来源于breaksoftware的CSDN博客)…

.NET企业级应用架构设计系列之应用服务器
本文属spanzhang(张友邦)原创,发布地址为:http://blog.csdn.net/spanzhang。转载或引用请注明原文之出处,谢谢! .NET企业级应用架构设计系列之开场白 .NET企业级应用架构设计系列之技术选型 这里要说到的…
编程语言发展70年,用50种不同语言输出「Hello World」
「免费学习 60 节公开课,投票页面,点击讲师头像」作者 | Sylvain Saurel译者 | 风车云马责编 | 屠敏【导读】历经 70 年,不断出现的编程语言为开发者解决了哪些难题?其存在又有怎样的特性?本文将以「Hello World」为例…

函数循环的理解
2019独角兽企业重金招聘Python工程师标准>>> var ulObjdocument.getElementById("box"); var lisObjulObj.getElementsTagname("li"); for(var i0;i<lisObj.length;i) { lisObj[i].οnclickfunction()//循环时对应节点绑定事件,事…
从LeNet到GoogLeNet:逐层详解,看卷积神经网络的进化
「免费学习 60 节公开课:投票页面,点击讲师头像」作者 | MrCharles来源 | CSDN原力计划获奖作品(*点击阅读原文,查看作者更多文章)前言深度学习的兴起使卷积神经网络在计算机视觉方面大放异彩,本文将按时间…

Windows客户端C/C++编程规范“建议”——前言
前言 工作中接触了很多编程规范。其中最有意思的是,公司最近发布了一版C/C编程规范,然后我看到该规范的最后一段时,有这么一句:“该规范不适用于Windows平台开发”。看来这份规范是由做其他平台开发的同学制定的。那么做Windows开…

storm入门教程 第一章 前言[转]
1.1 实时流计算 互联网从诞生的第一时间起,对世界的最大的改变就是让信息能够实时交互,从而大大加速了各个环节的效率。正因为大家对信息实时响应、实时交互的需求,软件行业除了个人操作系统之外,数据库(更精确的说是…

Windows客户端C/C++编程规范“建议”——函数
1 函数 1.1 代码行数控制在80行及以内 等级:【要求】 说明:每个函数的代码行数控制应该控制在80行以内。如果超过这个限制函数内部逻辑一般可以拆分。如果试图超过这个标准,请列出理由。但理由不包含如下: 无法拆分。流程内部…
把自己朝九晚五的工作自动化了,有错吗?
作者 | Brian Merchant译者 | 谭开朗编辑 | 屠敏来源 | CSDN(ID:CSDNnews)【导读】用代码让自己工作开启自动化之际,是否意味着自己将面临被解雇的风险?2016年,Reddit上出现了一个匿名的帖子,内…

COM组件转换成.NET组件[转]
利用类型库导入器(Type Library Importer)可以将其包装成一个.NET组件,然后就可以像使用.NET组件一样使用它了。 .NET框架只是提供了一个包装,并没有真正改变原有的对象 1.找到要转换的.dll文件 2.在命令提示符窗口中输入.dll文件的文件路径,…
干货:Android 源码使用心得分享
我相信很多初学者会和我一样经常在网上去找Android开发源码,但是往往因为运行不起来非常的懊恼!在做爱开发网站的时候,收集App代码时就遇到了这种困难,我相信网络上面的源码大部分在发布前都会人工测试,能够正常运行才…
60分钟入门深度学习工具PyTorch
「免费学习 60 节公开课:投票页面,点击讲师头像」作者 | Soumith Chintala中文翻译、注释制作 | 黄海广配置环境:PyTorch 1.3,python 3.7,主机:显卡:一块1080ti;内存:32g…

Windows客户端C/C++编程规范“建议”——指针
2 指针 2.1 尽量使用智能指针 等级: 【推荐】说明:正确使用智能指针可以省去指针管理的工作。2.2 类成员变量指针释放后一定要置空 等级: 【必须】说明:如果类成员变量指针在释放后没有置空,将出现如下问题࿱…

Bug tracker .net 部署经验(完善中)
Bug tracker .net 部署经验1. 软件要求windows 2003 serverSP1IISSQL sever 2005Net framework2. 安装和配置Bug tracker .net 提供了一个readme 文件。解压安装文件至某一个目录。在SQL SERVER 2005中新建一个数据库。确保IIS工作正常。定义一个虚拟目录,将其指向安…

Windows客户端C/C++编程规范“建议”——函数调用
3 函数调用 3.1 谨慎使用递归方法 等级: 【推荐】说明:递归方式控制不当,可能会导致栈空间不够而崩溃。一般的递归都可以使用循环代替。3.2 不要使用using namespace 等级: 【必须】说明:这是曾经教科书上的一种写法&a…
“一百万行Python代码对任何人都足够了”
作者 | Jake Edge译者 | Kolen出品 | AI科技大本营(ID: rgznai100)编程语言通常对其操作过程的各个方面都有或明或暗的限制。诸如标识符的最大长度或变量可以存储的值的范围之类的事情,这些是相当明显的例子,但是还有其他一些例子…

网吧电影客户端Realplayer的装配问题
网吧电影客户端Realplayer的装配问题 作者: 出处:网吧联盟 ( 20 ) 砖 ( 22 ) 好 评论 ( 1 ) 条 进入论坛 更新时间:2005-11-24 16:54关 键 词:网吧阅读提示:一般有过开网吧经验的朋友,应当知道如何让自己的realplayer播放器支…

基于Servlet+JDBC+Bootstrap+MySQL+AJAX权限管理系统项目实战教程
项目简介 权限系统一直以来是我们应用系统不可缺少的一个部分,若每个应用系统都重新对系统的权限进行设计,以满足不同系统用户的需求,将会浪费我们不少宝贵时间,所以花时间来设计一个相对通用的权限系统是很有意义的。 本系…

Windows客户端C/C++编程规范“建议”——表达式和运算
4 表达式和运算 4.1 比较操作中将常量设置为左值 等级: 【推荐】说明:编写代码时,如果将常量设置为右值。可能因马虎将“”写成“”导致逻辑错误。这种场景下,编译器是不会报错的,代码检查也比较容易被忽视。例子&…
GitHub标星1.5w+,从此我只用这款全能高速下载工具
「免费学习 60 节公开课:投票页面,点击讲师头像」作者 | Rocky0429来源 | Python空间(ID:Devtogether )大家好,我是 Rocky0429,一个喜欢在网上收集各种资源的蒟蒻...网上资源眼花缭乱,下载的方…

CSS text-transform 属性
定义 text-transform 对元素中的字母进行控制。 继承性:Yes 说明 这个属性会改变元素中的字母大小写,而不论源文档中文本的大小写。如果值为 capitalize,则要对某些字母大写,但是并没有明确定义如何确定哪些字母要大写,…

SeaJS基本开发原则
SeaJS基本开发原则 在讨论SeaJS的具体使用前,先介绍一下SeaJS的模块化理念和开发原则。使用SeaJS开发JavaScript的基本原则就是:一切皆为模块。引入SeaJS后,编写JavaScript代码就变成了编写一个又一个模块,SeaJS中模块的概念有点类…