1. 程式人生 > >實現HTTP協議Get、Post和檔案上傳功能——使用WinHttp介面實現

實現HTTP協議Get、Post和檔案上傳功能——使用WinHttp介面實現

        在《使用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方法傳遞進來。我們想把重點發到這種傳送分離的實現上:

	if ( !WinHttpCrackUrlA_( m_strUrl, strHost, strPath, strExt, nPort ) ) {
		break;
	}

	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 ); 
	if ( NULL == m_hSession ) {
		break;
	}

	if ( FALSE == WinHttpSetTimeouts(m_hSession, m_nResolveTimeout, m_nConnectTimeout, m_nSendTimeout, m_nSendTimeout) ) {
		break;
	}

	m_hConnect = WinHttpConnectA( m_hSession, strHost.c_str(), nPort, 0 );
	if ( NULL == m_hConnect ) {
		break;
	}

	m_strRequestData = strPath + strExt;

        主要關注最後一行,我將URL路徑和URL引數放到m_strRequestData裡。之後

VOID CHttpRequestByWinHttp::TransmiteDataToServerByPost()
{
	BOOL bSuc = FALSE;
	do {
		m_hRequest = WinHttpOpenRequestA(m_hConnect, "Post",
				m_strRequestData.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
		if ( NULL == m_hRequest ) {
			break;
		}

        這樣,我們便將不需要Post的資料傳送了過去。

        現在我們再探討下需要Post過去的資料。首先我們需要明確下資料的來源:

  • 記憶體中的資料
  • 檔案中的資料

        不管資料來源於何處,它都可以成為待Post過去的資料或者待上傳的檔案的內容。於是我們借用上一篇博文中的IMemFileOperation介面,定義Post的資料的格式。

typedef struct _FMParam_ {
    std::string strkey;
    ToolsInterface::LPIMemFileOperation value;
    bool postasfile;
    struct FileInfo {
        char szfilename[128];
    };

    struct MemInfo{
        bool bMulti;
    };

    union {
        FileInfo fileinfo;
        MemInfo meminfo;
    };
}FMParam, *PFMParam;

typedef std::vector<FMParam> FMParams;
typedef FMParams::iterator FMParamsIter;
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方法,讓繼承類提供資料。因為資料存在延續性,所以導致繼承類的書寫很麻煩——需要記錄已經上傳了哪些資料。這個版本我將這個設計做了修改,基類暴露一個傳送方法,讓繼承類在需要的時候呼叫基類的方法,從而不需要基類記錄過程的狀態。於是以前一大坨程式碼被簡化到如下幾行:

DWORD dwUserDataLength = GetUserDataSize();
if ( FALSE == WinHttpSendRequest( m_hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, dwUserDataLength, 0)) {
	break;
}

DWORD dwSendUserDataLength = SendUserData();
bSuc = (dwUserDataLength == dwSendUserDataLength) ? TRUE : FALSE;

        通過GetUserDataSize我們將獲得待Post過去的資料的大小。然後呼叫SendUserData傳送資料,返回傳送了的資料的大小。通過對比兩者大小得知是否整個操作是否成功。

       現在我們再看下發送資料的具體實現,首先我們看下一些固定要寫死的欄位的申明

#define BOUNDARYPART "--MULTI-PARTS-FORM-DATA-BOUNDARY"

#define PARTRETURN  "\r\n"
#define PARTDISPFD  "Content-Disposition:form-data;"
#define PARTNAME    "name"
#define PARTEQUATE  "="
#define PARTQUOTES  "\""
#define PARTSPLIT   "&"
#define PARTSEMICOLON   ";"
#define PARTFILENAME    "filename"
#define PARTTYPEOCT "Content-Type:application/octet-stream"

        讀過《使用WinHttp介面實現HTTP協議Get、Post和檔案上傳功能》的朋友應該記得其中有很多繁雜的資料格式化。之前我們講過,我們需要先獲得待Post的資料大小,再發送資料。這意味著繁雜的資料格式化需要做兩次。如果以後需要對其中某個傳送資料格式化做修改,那麼相應的計算資料長度的方法也要做修改。這是非常不利於維護的。於是,我將兩者合為一個函式,通過引數判斷是需要計算還是需要傳送。這樣以後修改傳送資料時,只要修改一處,降低了維護的成本和難度。

    DWORD CHttpTransByPost::SendUserData() {
        return SendOrCalcData();
    }

    DWORD CHttpTransByPost::GetUserDataSize() {
        return SendOrCalcData(FALSE);
    }

    DWORD CHttpTransByPost::SendOrCalcData( BOOL bSend /*= TRUE*/ ) {
        DWORD dwsize = 0;
        for (FMParamsCIter it = m_PostParam.begin(); it != m_PostParam.end(); it++) {
            dwsize += SendData(*it, bSend);
        }
        if (!m_strBlockEnd.empty()) {
            dwsize += DataToServer(m_strBlockEnd.c_str(), m_strBlockEnd.length(), bSend);
        }   
        return dwsize;
    }

        在SendOrCalcData的最後,我們判斷m_strBlockEnd是否為空,如果不為空,則我們將BlockEnd格式化資料傳送過去,告訴伺服器MultiPart資料傳送結束。如果為空,則代表此次傳送資料不需要按MultiPart形式傳送。至於是否需要MultiPart,以及其各種格式化則是在下面的程式碼中判斷

BOOL CHttpTransByPost::ModifyRequestHeader( HINTERNET hRequest ) {
        bool bMulti = false;
        for (FMParamsCIter it = m_PostParam.begin(); it != m_PostParam.end(); it++) {
            if (it->postasfile) {
                bMulti = true;
                break;
            }
            else {
                bMulti = it->meminfo.bMulti;
                if (bMulti) {
                    break;
                }
            }
        }

		if (bMulti) {
			m_strBlockStart = "--";
			m_strBlockStart += BOUNDARYPART;
			m_strBlockStart += "\r\n";

			m_strBlockEnd =  "\r\n--";
			m_strBlockEnd += BOUNDARYPART;
			m_strBlockEnd +=  "--\r\n";

			m_strNewHeader = "Content-Type: multipart/form-data; boundary=";
			m_strNewHeader += BOUNDARYPART;
			m_strNewHeader += "\r\n";
		}
		else {
			m_strNewHeader = "Content-Type:application/x-www-form-urlencoded";
			m_strNewHeader += "\r\n";
		}

		::WinHttpAddRequestHeadersA(hRequest, m_strNewHeader.c_str(), 
			m_strNewHeader.length(), WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE) ;
		return AddUserRequestHeader(hRequest);
	}

        最後我們將注意力集中到傳送(計算)資料的函式SendData上。

    DWORD CHttpTransByPost::SendData(const FMParam& postparam, BOOL bSend /*= TRUE*/ ) {
        DWORD dwsize = 0;
        postparam.value->MFSeek(0, SEEK_SET);
        if (postparam.postasfile) {
            dwsize = SendFileData(postparam, bSend);
        }
        else {
            dwsize = SendMemData(postparam, bSend);
        }
        return dwsize;
    }

        首先,我們使用MFSeek將檔案(記憶體)的指標置到起始處。然後再通過postasfile決定是按檔案的形式傳送還是按記憶體的形式傳送。

    DWORD CHttpTransByPost::SendFileData(const FMParam& postparam, BOOL bSend) {
        DWORD dwsize = 0;
        dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
        dwsize += DataToServer(m_strBlockStart.c_str(), m_strBlockStart.length(), bSend);
        dwsize += DataToServer(PARTDISPFD, strlen(PARTDISPFD), bSend);
        dwsize += DataToServer(PARTNAME, strlen(PARTNAME), bSend);
        dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);
        dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
        dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);
        dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
        dwsize += DataToServer(PARTSEMICOLON, strlen(PARTSEMICOLON), bSend);
        dwsize += DataToServer(PARTFILENAME, strlen(PARTFILENAME), bSend);
        dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);
        dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
        dwsize += DataToServer(postparam.fileinfo.szfilename, strlen(postparam.fileinfo.szfilename), bSend);
        dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
        dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
        dwsize += DataToServer(PARTTYPEOCT, strlen(PARTTYPEOCT), bSend);
        dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
        dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
        while(!postparam.value->MFEof()) {
            char buffer[1024] = {0};
            size_t size = postparam.value->MFRead(buffer, 1, 1024);
            dwsize += DataToServer(buffer, size, bSend);
        }
        return dwsize;
    }

        以檔案內容形式傳送的程式碼如上。我們關注下最後幾行,MFRead讀取內容,然後傳送(計算)資料。

    DWORD CHttpTransByPost::SendMemData(const FMParam& postparam, BOOL bSend) {
        DWORD dwsize = 0;
        if (postparam.meminfo.bMulti) {
            dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
            dwsize += DataToServer(m_strBlockStart.c_str(), m_strBlockStart.length(), bSend);
            dwsize += DataToServer(PARTDISPFD, strlen(PARTDISPFD), bSend);
            dwsize += DataToServer(PARTNAME, strlen(PARTNAME), bSend);
            dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);
            dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
            dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);
            dwsize += DataToServer(PARTQUOTES, strlen(PARTQUOTES), bSend);
            dwsize += DataToServer(PARTRETURN, strlen(PARTRETURN), bSend);
	        while(!postparam.value->MFEof()) {
		        char buffer[1024] = {0};
		        size_t size = postparam.value->MFRead(buffer, 1, 1024);
                dwsize += DataToServer(buffer, size, bSend);
	        }
        }
        else {
            dwsize += DataToServer(PARTSPLIT, strlen(PARTSPLIT), bSend);
            dwsize += DataToServer(postparam.strkey.c_str(), postparam.strkey.length(), bSend);
            dwsize += DataToServer(PARTEQUATE, strlen(PARTEQUATE), bSend);
            while(!postparam.value->MFEof()) {
                char buffer[1024] = {0};
                size_t size = postparam.value->MFRead(buffer, 1, 1024);
                dwsize += DataToServer(buffer, size, bSend);
            }
        }

        return dwsize;
    }

        以上是傳送普通Post資料的方法。其中分為是否需要以MultiiPart形式傳送,還是以普通形式傳送。MultiPart形式之前已經說過,而普通Post資料形式則是無約束的。我將該資料時拼裝成Name1=Value1&Name2=Value2的形式傳送的。

        對於MultiParg型別的Post,我們使用WireShark擷取傳送包

        傳送普通Post資料的WireShark截包為

        最後我們看下使用的例子

    HttpRequestFM::CHttpTransByPost* p = new HttpRequestFM::CHttpTransByPost();
    ToolsInterface::IMemFileOperation* pMemOp = new MemFileOperation::CMemOperation();
	
    p->SetOperation(pMemOp);
    p->SetProcessCallBack(ProcssCallback);
    p->SetUrl(BIGFILEURL);
    
    FMParams params;

    FMParam param1;
    param1.postasfile = false;
    param1.strkey = "key1";
    param1.meminfo.bMulti = false;
    MemFileOperation::CMemOperation mem1("value1", strlen("value1"));
    param1.value = &mem1;
    params.push_back(param1);

    FMParam param2;
    param2.postasfile = false;
    param2.strkey = "key2";
    param2.meminfo.bMulti = true;
    //sprintf_s(param2.fileinfo.szfilename, sizeof(param2.fileinfo.szfilename), "2.bin");
    MemFileOperation::CFileOperation file2("F:/2.bin");
    param2.value = &file2;
    params.push_back(param2);
    
    FMParam param3;
    param3.strkey = "key3";
    //param3.meminfo.bMulti = true;
    param3.postasfile = true;
    sprintf_s(param3.fileinfo.szfilename, sizeof(param3.fileinfo.szfilename), "3.bin");
    MemFileOperation::CFileOperation file3("F:/1.bin");
    param3.value = &file3;
    params.push_back(param3);

    p->SetPostParam(params);
    p->Start();

        param1是以普通Post資料格式傳輸的引數;param2的value是從F:/2.bin檔案中讀取的,但是其只是MultiPart形式上傳的資料,而非上傳檔案。param3是要求上傳檔案F:/1.bin檔案到伺服器上為3.bin。

        通過不同的組合,我們可以同時上傳多個檔案。比如我們將上例中的param2做稍微的修改,即可以將其對應的檔案上傳至伺服器,實現同時上傳多個檔案的功能。

        工程原始碼連結:http://pan.baidu.com/s/1i3eUnMt 密碼:hfro