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

来源:互联网 时间:1970-01-01


在《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》一文中,我已经比较详细地讲解了如何使用WinHttp接口实现各种协议。在最近的代码梳理中,我觉得Post和文件上传模块可以得到简化,于是几乎重写了这两个功能的代码。因为Get、Post和文件上传功能的基础(父)类基本没有改动,函数调用的流程也基本没有变化,所以本文我将重点讲解修改点。(转载请指明出于breaksoftware的csdn博客)

        首先我修改了接口的字符集。之前我都是使用UNICODE作为接口参数类型,其中一个原因是Windows提倡UNICODE编码,其次是因为WinHttp接口只提供了UNICODE接口函数。而我在本次修改中,将字符集改成UTF8。因为在网络传输方便,UTF8格式才是主流。于是为了使用WinHttp接口,我提供了一个A版本的转换层——工程中WinhttpA.h。

        其次,我增强了Post接口。《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》的读者和我讨论了很多Post协议,让我感觉非常有必要重视起该功能。本文我们将着重讲解Post的实现和测试。

        再次,我将Post的实现和文件上传功能的实现合二为一。因为两者代码非常相似,其实在原理方面也是很相似的。

        最后,我使用前一篇博文中介绍的IMemFileOperation接口,重新定义了Post和文件上传功能的参数定义。因为IMemFileOperation的特性,我们可以上传文件,或者上传一片内存值,或者上传文件中的内容,而这些操作是相同的。

        Get请求没什么好说的了,我们主要关注Post和文件上传。

        一般情况下,我们遇到的是“我们需要向http://www.xxx.com:8080/yyyy/zzz地址Post数据”。其中的“数据”是我们问题的重点。可能很多人认为Post请求就是将所有参数都Post到服务器,其实不然。打个比方,比如我们要求对http://www.xxxx.com/post?a=b&c=d地址Post一个数据e=f,我们并不是将"a=b&c=d&e=f"Post到服务器,而只是"e=f"Post过去,"a=b&c=d"还是按Get的方式发送。于是我对上一版的设计做了改良,废掉了ParseParams函数,简化了设计,但是要求用户传进来的URL中不包含需要Post过去的数据——需要Post的数据通过SetPostParam方法传递进来。我们想把重点发到这种发送分离的实现上:

[cpp] view plaincopy
  1. if ( !WinHttpCrackUrlA_( m_strUrl, strHost, strPath, strExt, nPort ) ) {  
  2.     break;  
  3. }  
  4.   
  5. m_hSession = WinHttpOpenA( m_strAgent.empty() ? NULL : m_strAgent.c_str(), WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0 );   
  6. if ( NULL == m_hSession ) {  
  7.     break;  
  8. }  
  9.   
  10. if ( FALSE == WinHttpSetTimeouts(m_hSession, m_nResolveTimeout, m_nConnectTimeout, m_nSendTimeout, m_nSendTimeout) ) {  
  11.     break;  
  12. }  
  13.   
  14. m_hConnect = WinHttpConnectA( m_hSession, strHost.c_str(), nPort, 0 );  
  15. if ( NULL == m_hConnect ) {  
  16.     break;  
  17. }  
  18.   
  19. m_strRequestData = strPath + strExt;  

        主要关注最后一行,我将URL路径和URL参数放到m_strRequestData里。之后

[cpp] view plaincopy
  1. VOID CHttpRequestByWinHttp::TransmiteDataToServerByPost()  
  2. {  
  3.     BOOL bSuc = FALSE;  
  4.     do {  
  5.         m_hRequest = WinHttpOpenRequestA(m_hConnect, "Post",  
  6.                 m_strRequestData.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);  
  7.         if ( NULL == m_hRequest ) {  
  8.             break;  
  9.         }  

        这样,我们便将不需要Post的数据发送了过去。

        现在我们再探讨下需要Post过去的数据。首先我们需要明确下数据的来源:

  • 内存中的数据
  • 文件中的数据

        不管数据来源于何处,它都可以成为待Post过去的数据或者待上传的文件的内容。于是我们借用上一篇博文中的IMemFileOperation接口,定义Post的数据的格式。

[cpp] view plaincopy
  1. typedef struct _FMParam_ {  
  2.     std::string strkey;  
  3.     ToolsInterface::LPIMemFileOperation value;  
  4.     bool postasfile;  
  5.     struct FileInfo {  
  6.         char szfilename[128];  
  7.     };  
  8.   
  9.     struct MemInfo{  
  10.         bool bMulti;  
  11.     };  
  12.   
  13.     union {  
  14.         FileInfo fileinfo;  
  15.         MemInfo meminfo;  
  16.     };  
  17. }FMParam, *PFMParam;  
  18.   
  19. typedef std::vector<FMParam> FMParams;  
  20. typedef FMParams::iterator FMParamsIter;  
  21. typedef FMParams::const_iterator FMParamsCIter;  
        不管是Post数据还是要上传文件,协议中都需要key的存在。strkey是数据的key。value字段只是一个指针,它是指向一个文件还是内存。已经没有关系了,因为之后我们将使用统一的接口去访问它。postasfile字段是标志该参数是否以文件内容的形式Post上去。这儿需要特别说明下,postasfile和value是内存还是文件是没有关系的。因为value只是指向了数据内容,至于内容上传到服务器是作为文件的内容还是只是普通Post的数据值是由postasfile决定的。如果postasfile为真,则FileInfo将被利用到。因为它标记了内容上传到服务器后,服务器上保存的文件名。如果postasfile为假,则我们需要考虑下数据是作为普通数据post,还是作为MultiPart数据Post。这个就取决于MemInfo中的字段了。至于什么是MultiPart类型,可以简单参考《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》后半部分关于文件上传的讨论。

        对于待上传的数据,之前设计改框架时,框架提供了GetData方法,让继承类提供数据。因为数据存在延续性,所以导致继承类的书写很麻烦——需要记录已经上传了哪些数据。这个版本我将这个设计做了修改,基类暴露一个发送方法,让继承类在需要的时候调用基类的方法,从而不需要基类记录过程的状态。于是以前一大坨代码被简化到如下几行:

[cpp] view plaincopy
  1. DWORD dwUserDataLength = GetUserDataSize();  
  2. if ( FALSE == WinHttpSendRequest( m_hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, dwUserDataLength, 0)) {  
  3.     break;  
  4. }  
  5.   
  6. DWORD dwSendUserDataLength = SendUserData();  
  7. bSuc = (dwUserDataLength == dwSendUserDataLength) ? TRUE : FALSE;  

        通过GetUserDataSize我们将获得待Post过去的数据的大小。然后调用SendUserData发送数据,返回发送了的数据的大小。通过对比两者大小得知是否整个操作是否成功。

       现在我们再看下发送数据的具体实现,首先我们看下一些固定要写死的字段的申明

[cpp] view plaincopy
  1. #define BOUNDARYPART "--MULTI-PARTS-FORM-DATA-BOUNDARY"  
  2.   
  3. #define PARTRETURN  "/r/n"  
  4. #define PARTDISPFD  "Content-Disposition:form-data;"  
  5. #define PARTNAME    "name"  
  6. #define PARTEQUATE  "="  
  7. #define PARTQUOTES  "/""  
  8. #define PARTSPLIT   "&"  
  9. #define PARTSEMICOLON   ";"  
  10. #define PARTFILENAME    "filename"  
  11. #define PARTTYPEOCT "Content-Type:application/octet-stream"  
        读过《使用WinHttp接口实现HTTP协议Get、Post和文件上传功能》的朋友应该记得其中有很多繁杂的数据格式化。之前我们讲过,我们需要先获得待Post的数据大小,再发送数据。这意味着繁杂的数据格式化需要做两次。如果以后需要对其中某个发送数据格式化做修改,那么相应的计算数据长度的方法也要做修改。这是非常不利于维护的。于是,我将两者合为一个函数,通过参数判断是需要计算还是需要发送。这样以后修改发送数据时,只要修改一处,降低了维护的成本和难度。

[cpp] view plaincopy
  1. DWORD CHttpTransByPost::SendUserData() {  
  2.     return SendOrCalcData();  
  3. }  
  4.   
  5. DWORD CHttpTransByPost::GetUserDataSize() {  
  6.     return SendOrCalcData(FALSE);  
  7. }  
  8.   
  9. DWORD CHttpTransByPost::SendOrCalcData( BOOL bSend /*= TRUE*/ ) {  
  10.     DWORD dwsize = 0;  
  11.     for (FMParamsCIter it = m_PostParam.begin(); it != m_PostParam.end(); it++) {  
  12.         dwsize += SendData(*it, bSend);  
  13.     }  
  14.     if (!m_strBlockEnd.empty()) {  
  15.         dwsize += DataToServer(m_strBlockEnd.c_str(), m_strBlockEnd.length(), bSend);  
  16.     }     
  17.     return dwsize;  
  18. }  

        在SendOrCalcData的最后,我们判断m_strBlockEnd是否为空,如果不为空,则我们将BlockEnd格式化数据发送过去,告诉服务器MultiPart数据发送结束。如果为空,则代表此次发送数据不需要按MultiPart形式发送。至于是否需要MultiPart,以及其各种格式化则是在下面的代码中判断

[cpp] view plaincopy
  1. BOOL CHttpTransByPost::ModifyRequestHeader( HINTERNET hRequest ) {  
  2.         bool bMulti = false;  
  3.         for (FMParamsCIter it = m_PostParam.begin(); it != m_PostParam.end(); it++) {  
  4.             if (it->postasfile) {  
  5.                 bMulti = true;  
  6.                 break;  
  7.             }  
  8.             else {  
  9.                 bMulti = it->meminfo.bMulti;  
  10.                 if (bMulti) {  
  11.                     break;  
  12.                 }  
  13.             }  
  14.         }  
  15.   
  16.         if (bMulti) {  
  17.             m_strBlockStart = "--";  
  18.             m_strBlockStart += BOUNDARYPART;  
  19.             m_strBlockStart += "/r/n";  
  20.   
  21.             m_strBlockEnd =  "/r/n--";  
  22.             m_strBlockEnd += BOUNDARYPART;  
  23.             m_strBlockEnd +=  "--/r/n";  
  24.   
  25.             m_strNewHeader = "Content-Type: multipart/form-data; boundary=";  
  26.             m_strNewHeader += BOUNDARYPART;  
  27.             m_strNewHeader += "/r/n";  
  28.         }  
  29.         else {  
  30.             m_strNewHeader = "Content-Type:application/x-www-form-urlencoded";  
  31.             m_strNewHeader += "/r/n";  
  32.         }  
  33.   
  34.         ::WinHttpAddRequestHeadersA(hRequest, m_strNewHeader.c_str(),   
  35.             m_strNewHeader.length(), WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE) ;  
  36.         return AddUserRequestHeader(hRequest);  
  37.     }  
        最后我们将注意力集中到发送(计算)数据的函数SendData上。
[cpp] view plaincopy
  1. DWORD CHttpTransByPost::SendData(const FMParam& postparam, BOOL bSend /*= TRUE*/ ) {  
  2.     DWORD dwsize = 0;  
  3.     postparam.value->MFSeek(0, SEEK_SET);  
  4.     if (postparam.postasfile) {  
  5.         dwsize = SendFileData(postparam, bSend);  
  6.     }  
  7.     else {  
  8.         dwsize = SendMemData(postparam, bSend);  
  9.     }  
  10.     return dwsize;  
  11. }  

        首先,我们使用MFSeek将文件(内存)的指针置到起始处。然后再通过postasfile决定是按文件的形式发送还是按内存的形式发送。

[cpp] view plaincopy
  1. DWORD CHttpTransByPost::SendFileData(const FMParam& postparam, BOOL bSend) {  
  2.     DWORD dwsize = 0;  
  3.     dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);  
  4.     dwsize += DataToServer(m_strBlockStart.c_str(), m_strBlockStart.length(), bSend);  
  5.     dwsize += DataToServer(PARTDISPFD, strlen(PARTDISPFD), bSend);  
  6.     dwsize += DataToServer(PARTNAME, strlen(PARTNAME), bSend);  
  7.     dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);  
  8.     dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);  
  9.     dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);  
  10.     dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);  
  11.     dwsize += DataToServer(PARTSEMICOLON, strlen(PARTSEMICOLON), bSend);  
  12.     dwsize += DataToServer(PARTFILENAME, strlen(PARTFILENAME), bSend);  
  13.     dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);  
  14.     dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);  
  15.     dwsize += DataToServer(postparam.fileinfo.szfilename, strlen(postparam.fileinfo.szfilename), bSend);  
  16.     dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);  
  17.     dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);  
  18.     dwsize += DataToServer(PARTTYPEOCT, strlen(PARTTYPEOCT), bSend);  
  19.     dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);  
  20.     dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);  
  21.     while(!postparam.value->MFEof()) {  
  22.         char buffer[1024] = {0};  
  23.         size_t size = postparam.value->MFRead(buffer, 1, 1024);  
  24.         dwsize += DataToServer(buffer, size, bSend);  
  25.     }  
  26.     return dwsize;  
  27. }  
        以文件内容形式发送的代码如上。我们关注下最后几行,MFRead读取内容,然后发送(计算)数据。
[cpp] view plaincopy
  1. DWORD CHttpTransByPost::SendMemData(const FMParam& postparam, BOOL bSend) {  
  2.     DWORD dwsize = 0;  
  3.     if (postparam.meminfo.bMulti) {  
  4.         dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);  
  5.         dwsize += DataToServer(m_strBlockStart.c_str(), m_strBlockStart.length(), bSend);  
  6.         dwsize += DataToServer(PARTDISPFD, strlen(PARTDISPFD), bSend);  
  7.         dwsize += DataToServer(PARTNAME, strlen(PARTNAME), bSend);  
  8.         dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);  
  9.         dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);  
  10.         dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);  
  11.         dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);  
  12.         dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);  
  13.      while(!postparam.value->MFEof()) {  
  14.       char buffer[1024] = {0};  
  15.       size_t size = postparam.value->MFRead(buffer, 1, 1024);  
  16.             dwsize += DataToServer(buffer, size, bSend);  
  17.      }  
  18.     }  
  19.     else {  
  20.         dwsize += DataToServer(PARTSPLIT, strlen(PARTSPLIT), bSend);  
  21.         dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);  
  22.         dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);  
  23.         while(!postparam.value->MFEof()) {  
  24.             char buffer[1024] = {0};  
  25.             size_t size = postparam.value->MFRead(buffer, 1, 1024);  
  26.             dwsize += DataToServer(buffer, size, bSend);  
  27.         }  
  28.     }  
  29.   
  30.     return dwsize;  
  31. }  
        以上是发送普通Post数据的方法。其中分为是否需要以MultiiPart形式发送,还是以普通形式发送。MultiPart形式之前已经说过,而普通Post数据形式则是无约束的。我将该数据时拼装成Name1=Value1&Name2=Value2的形式发送的。

        对于MultiParg类型的Post,我们使用WireShark截取发送包


        发送普通Post数据的WireShark截包为

        最后我们看下使用的例子
[cpp] view plaincopy
  1. HttpRequestFM::CHttpTransByPost* p = new HttpRequestFM::CHttpTransByPost();  
  2. ToolsInterface::IMemFileOperation* pMemOp = new MemFileOperation::CMemOperation();  
  3.   
  4. p->SetOperation(pMemOp);  
  5. p->SetProcessCallBack(ProcssCallback);  
  6. p->SetUrl(BIGFILEURL);  
  7.   
  8. FMParams params;  
  9.   
  10. FMParam param1;  
  11. param1.postasfile = false;  
  12. param1.strkey = "key1";  
  13. param1.meminfo.bMulti = false;  
  14. MemFileOperation::CMemOperation mem1("value1", strlen("value1"));  
  15. param1.value = &mem1;  
  16. params.push_back(param1);  
  17.   
  18. FMParam param2;  
  19. param2.postasfile = false;  
  20. param2.strkey = "key2";  
  21. param2.meminfo.bMulti = true;  
  22. //sprintf_s(param2.fileinfo.szfilename, sizeof(param2.fileinfo.szfilename), "2.bin");  
  23. MemFileOperation::CFileOperation file2("F:/2.bin");  
  24. param2.value = &file2;  
  25. params.push_back(param2);  
  26.   
  27. FMParam param3;  
  28. param3.strkey = "key3";  
  29. //param3.meminfo.bMulti = true;  
  30. param3.postasfile = true;  
  31. sprintf_s(param3.fileinfo.szfilename, sizeof(param3.fileinfo.szfilename), "3.bin");  
  32. MemFileOperation::CFileOperation file3("F:/1.bin");  
  33. param3.value = &file3;  
  34. params.push_back(param3);  
  35.   
  36. p->SetPostParam(params);  
  37. p->Start();  

        param1是以普通Post数据格式传输的参数;param2的value是从F:/2.bin文件中读取的,但是其只是MultiPart形式上传的数据,而非上传文件。param3是要求上传文件F:/1.bin文件到服务器上为3.bin。

        通过不同的组合,我们可以同时上传多个文件。比如我们将上例中的param2做稍微的修改,即可以将其对应的文件上传至服务器,实现同时上传多个文件的功能。


上文来自:http://blog.csdn.net/breaksoftware/article/details/45874189



相关阅读:
Top