1. 程式人生 > >windows學習記錄之MFC通過URL上傳下載檔案

windows學習記錄之MFC通過URL上傳下載檔案

**

HTTP四種常見的POST提交資料方式:

**

HTTP/1.1 協議規定的 HTTP 請求方法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 這幾種。其中 POST 一般用來向服務端提交資料,本文主要討論 POST 提交資料的幾種方式。
我們知道,HTTP 協議是以 ASCII 碼傳輸,建立在 TCP/IP 協議之上的應用層規範。規範把 HTTP 請求分為三個部分:狀態行、請求頭、訊息主體。類似於下面這樣:

<method> <request-url> <version>
<headers
>
<entity-body></entity-body></headers></version></request-url></method>

協議規定 POST 提交的資料必須放在訊息主體(entity-body)中,但協議並沒有規定資料必須使用什麼編碼方式。實際上,開發者完全可以自己決定訊息主體的格式,只要最後傳送的 HTTP 請求滿足上面的格式就可以。

但是,資料傳送出去,還要服務端解析成功才有意義。一般服務端語言如 php、python 等,以及它們的 framework,都內建了自動解析常見資料格式的功能。服務端通常是根據請求頭(headers)中的 Content-Type 欄位來獲知請求中的訊息主體是用何種方式編碼,再對主體進行解析。所以說到 POST 提交資料方案,包含了 Content-Type 和訊息主體編碼方式兩部分。下面就正式開始介紹它們。

application/x-www-form-urlencoded:
這應該是最常見的 POST 提交資料的方式了。瀏覽器的原生 form 表單,如果不設定 enctype 屬性,那麼最終就會以 application/x-www-form-urlencoded 方式提交資料。請求類似於下面這樣(無關的請求頭在本文中都省略掉了):

POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3

首先,Content-Type 被指定為 application/x-www-form-urlencoded;其次,提交的資料按照 key1=val1&key2=val2 的方式進行編碼,key 和 val 都進行了 URL 轉碼。大部分服務端語言都對這種方式有很好的支援。例如 PHP 中,POST[title]title_POST[‘sub’] 可以得到 sub 陣列。

很多時候,我們用 Ajax 提交資料時,也是使用這種方式。例如 JQuery 和 QWrap 的 Ajax,Content-Type 預設值都是「application/x-www-form-urlencoded;charset=utf-8」。

multipart/form-data:
這又是一個常見的 POST 資料提交的方式。我們使用表單上傳檔案時,必須讓 form 的 enctyped 等於這個值。直接來看一個請求示例:
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=—-WebKitFormBoundaryrGKCBY7qhFd3TrwA

——WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name=”text”

title
——WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name=”file”; filename=”chrome.png”
Content-Type: image/png

PNG … content of chrome.png …
——WebKitFormBoundaryrGKCBY7qhFd3TrwA–

這個例子稍微複雜點。首先生成了一個 boundary 用於分割不同的欄位,為了避免與正文內容重複,boundary 很長很複雜。然後 Content-Type 裡指明瞭資料是以 mutipart/form-data 來編碼,本次請求的 boundary 是什麼內容。訊息主體裡按照欄位個數又分為多個結構類似的部分,每部分都是以 –boundary 開始,緊接著內容描述資訊,然後是回車,最後是欄位具體內容(文字或二進位制)。如果傳輸的是檔案,還要包含檔名和檔案型別資訊。訊息主體最後以 –boundary– 標示結束。關於 mutipart/form-data 的詳細定義,請前往 rfc1867 檢視。

這種方式一般用來上傳檔案,各大服務端語言對它也有著良好的支援。

上面提到的這兩種 POST 資料的方式,都是瀏覽器原生支援的,而且現階段原生 form 表單也只支援這兩種方式。但是隨著越來越多的 Web 站點,尤其是 WebApp,全部使用 Ajax 進行資料互動之後,我們完全可以定義新的資料提交方式,給開發帶來更多便利。

application/json
application/json 這個 Content-Type 作為響應頭大家肯定不陌生。實際上,現在越來越多的人把它作為請求頭,用來告訴服務端訊息主體是序列化後的 JSON 字串。由於 JSON 規範的流行,除了低版本 IE 之外的各大瀏覽器都原生支援 JSON.stringify,服務端語言也都有處理 JSON 的函式,使用 JSON 不會遇上什麼麻煩。
JSON 格式支援比鍵值對複雜得多的結構化資料,這一點也很有用。記得我幾年前做一個專案時,需要提交的資料層次非常深,我就是把資料 JSON 序列化之後來提交的。不過當時我是把 JSON 字串作為 val,仍然放在鍵值對裡,以 x-www-form-urlencoded 方式提交。
Google 的 AngularJS 中的 Ajax 功能,預設就是提交 JSON 字串。例如下面這段程式碼:
var data = {‘title’:’test’, ‘sub’ : [1,2,3]};
$http.post(url, data).success(function(result) {

});
最終傳送的請求是:
POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8

{“title”:”test”,”sub”:[1,2,3]}
這種方案,可以方便的提交複雜的結構化資料,特別適合 RESTful 的介面。各大抓包工具如 Chrome 自帶的開發者工具、Firebug、Fiddler,都會以樹形結構展示 JSON 資料,非常友好。但也有些服務端語言還沒有支援這種方式,例如 php 就無法通過 $_POST 物件從上面的請求中獲得內容。這時候,需要自己動手處理下:在請求頭中 Content-Type 為 application/json 時,從 php://input 裡獲得原始輸入流,再 json_decode 成物件。一些 php 框架已經開始這麼做了。
當然 AngularJS 也可以配置為使用 x-www-form-urlencoded 方式提交資料。如有需要,可以參考這篇文章。

text/xml
我之前提到過 XML-RPC(XML Remote Procedure Call)。它是一種使用 HTTP 作為傳輸協議,XML 作為編碼方式的遠端呼叫規範。典型的 XML-RPC 請求是這樣的:
POST http://www.example.com HTTP/1.1
Content-Type: text/xml


examples.getStateName


41

</params>


XML-RPC 協議簡單、功能夠用,各種語言的實現都有。它的使用也很廣泛,如 WordPress 的 XML-RPC Api,搜尋引擎的 ping 服務等等。JavaScript 中,也有現成的庫支援以這種方式進行資料互動,能很好的支援已有的 XML-RPC 服務。不過,我個人覺得 XML 結構還是過於臃腫,一般場景用 JSON 會更靈活方便。

**

MFC下載上傳檔案

**

我們知道,windows有關網路連線的API在wininet.h裡,而在MFC裡,這些API被封裝成了類。
CInternetSession類:直接繼承自CObject類,該類用來建立與某個Internet伺服器的會話
CInternetConnection類:幫助使用者管理與Internet伺服器的連線,同時還提供一些函式完成和響應伺服器的通訊
CInternetConnection類又派生出三個類:
CHttpConnection類:管理與HTTP伺服器的連線
CFtpConnection類:用於管理與FTP伺服器的連線
CGopherConnection類:管理與Gopher伺服器的連線

我們要實現根據URL下載並儲存檔案,就需要CInternetSession、CHttpConnection、CHttpFile。
CHttpFile用來做什麼呢?它可以關聯起URL指向的一個WEB物件,由於CHttpFile繼承與CFile,我們可以像讀取一個CFile檔案一樣,把它從HTTP伺服器上下載下來。
MFC就是通過對CHttpFile物件的讀寫來完成與HTTP伺服器的對話的,包括GET/POST提交資料,接收資料。

下面就是我自己寫的一個根據URL下載並儲存檔案的函式,注意使用時需要在MFC專案的預編譯標頭檔案里加入#include <afxinet.h>,因為上面我提到的那些類全部在這個標頭檔案裡。
第一引數為URL,第二個引數為儲存路徑。
返回值:
0:成功下載儲存
-1:URL不正確
-2:建立網路連線失敗
-3:向伺服器發起GET請求失敗
-4:伺服器不接受請求
-5:引數一不能為空
-6:引數二不能為空
-7:儲存檔案時建立或寫入檔案失敗
程式碼如下:
.h檔案:

#ifndef CHTTPIMAGE_H
#define CHTTPIMAGE_H
#include <WinInet.h>
#include <afxinet.h>

class CHttpImage
{
public:
    CHttpImage();
    ~CHttpImage();
    bool HttpUpLoading(const char *phostName,const char *postData,int iLen,char *pResult,int *iResultLen);//上傳檔案
    bool HttpDownload(const char *phostName,const char *pFileName);//下載檔案
    bool HttpGetLogoURL(const char *phostName,const char *postData,int iLen,char *pResult,int *iResultLen);//獲取玩家影象URL地址
};
#endif

.cpp檔案:

#include "StdAfx.h"
#include "CHttpImage.h"

CHttpImage::CHttpImage()
{
}
CHttpImage::~CHttpImage()
{
}
bool CHttpImage::HttpUpLoading(const char *phostName,const char *postData,int iLen,char *pResult,int *iResultLen)//上傳檔案
{
    unsigned short nPort;       //用於儲存目標HTTP服務埠  
    CString strServer, strObject;   //strServer用於儲存伺服器地址,strObject用於儲存檔案物件名稱  
    DWORD dwServiceType;      //dwServiceType用於儲存服務型別,dwRet用於儲存提交GET請求返回的狀態號  
    //解析URL,獲取資訊  
    if(!AfxParseURL(phostName, dwServiceType, strServer, strObject, nPort))  
    {  
        //解析失敗,該Url不正確  
        return false;  
    }  
    CInternetSession session;
    DWORD dwRet = 0;
    //建立網路連線  
    CHttpConnection *pHtCon = session.GetHttpConnection(strServer,nPort);  
    if(pHtCon == NULL)  
    {  
        //建立網路連線失敗  
        session.Close();  
        return false;  
    }  
    //發起GET請求  
    CHttpFile* pFile=pHtCon->OpenRequest(CHttpConnection::HTTP_VERB_GET,strObject);  
    if(pFile == NULL)  
    {  
        //發起GET請求失敗  
        session.Close();  
        delete pHtCon;
        pHtCon = NULL;  
        return false;  
    }  
    CString strHeaders = "Content-Type:application/x-www-form-urlencoded;charset=gbk"; // 請求頭
    //開始傳送請求
    pFile->SendRequest(strHeaders,strHeaders.GetLength(),(LPVOID)postData,iLen);
    pFile->QueryInfoStatusCode(dwRet);
    int index=0;
    if (dwRet == HTTP_STATUS_OK)
    {
        CString  newline;
        while(pFile->ReadString(newline))
        {//迴圈讀取每行內容
            memcpy(pResult+index,newline.GetBuffer(),sizeof(char)*newline.GetLength());
            index+=newline.GetLength();
        }
    }
    else
    {
        session.Close();
        if(pHtCon)
        {
            delete pHtCon;
            pHtCon=NULL;
        }
        if(pFile)
        {
            delete pFile;
            pFile=NULL;
        }
        return false;
    }
    *iResultLen=index;
    session.Close();
    if(pHtCon)
    {
        delete pHtCon;
        pHtCon=NULL;
    }
    if(pFile)
    {
        delete pFile;
        pFile=NULL;
    }
    std::string _cs(pResult);
    if(-1!=_cs.find(std::string("status"),0))
    {
        return false;
    }
    return true;
}
bool CHttpImage::HttpDownload(const char *phostName,const char *pFileName)//下載檔案
{
    unsigned short nPort;       //用於儲存目標HTTP服務埠  
    CString strServer, strObject;   //strServer用於儲存伺服器地址,strObject用於儲存檔案物件名稱  
    DWORD dwServiceType,dwRet;      //dwServiceType用於儲存服務型別,dwRet用於儲存提交GET請求返回的狀態號  
    //解析URL,獲取資訊  
    if(!AfxParseURL(phostName, dwServiceType, strServer, strObject, nPort))  
    {  
        //解析失敗,該Url不正確  
        return false;  
    }  
    //建立網路連線物件,HTTP連線物件指標和用於該連線的HttpFile檔案物件指標,注意delete  
    CInternetSession intsess;  
    CHttpFile *pHtFile = NULL;  
    //建立網路連線  
    CHttpConnection *pHtCon = intsess.GetHttpConnection(strServer,nPort);  
    if(pHtCon == NULL)  
    {  
        //建立網路連線失敗  
        intsess.Close();  
        return false;  
    }  
    //發起GET請求  
    pHtFile =pHtCon->OpenRequest(CHttpConnection::HTTP_VERB_GET,strObject);  
    if(pHtFile == NULL)  
    {  
        //發起GET請求失敗  
        intsess.Close();  
        delete pHtCon;
        pHtCon = NULL;  
        return false;  
    }  
    //提交請求  
    pHtFile->SendRequest();  
    //獲取伺服器返回的狀態號  
    pHtFile->QueryInfoStatusCode(dwRet);  
    if (dwRet != HTTP_STATUS_OK)  
    {  
        //伺服器不接受請求  
        intsess.Close();  
        delete pHtCon;
        pHtCon = NULL;  
        delete pHtFile;
        pHtFile = NULL;  
        return false;  
    }  
    //獲取檔案大小  
    long long i64FileLen = pHtFile->GetLength();  
    int iRead = 0;       //用於標識讀了多少,為1是為了進入迴圈  
    int iWriteSize=1024*1024;//1M
    //建立緩衝區  
    char *szBuffer = new char[iWriteSize];  
    try  
    {  
        HANDLE hWriteFileHandle=::CreateFile(pFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
        if(INVALID_HANDLE_VALUE==hWriteFileHandle)
        {
            return false;
        }
        unsigned long iReadNums=0;
        unsigned long iWriteNums=0;
        do
        {    
            //清空緩衝區  
            memset(szBuffer,0,sizeof(char)*(iWriteSize));  
            //讀取到緩衝區  
            iRead = pHtFile->Read(szBuffer,iWriteSize);   
            ::FlushFileBuffers(hWriteFileHandle);
            bool bWriteSuccess=WriteFile(hWriteFileHandle,szBuffer,iRead,&iWriteNums,NULL);
            if(!bWriteSuccess)
            {
                break;
            }
        }while(iRead>0);
        CloseHandle(hWriteFileHandle);
        //釋放記憶體  
        delete[] szBuffer;  
        delete pHtFile;  
        pHtFile=NULL;
        delete pHtCon;  
        pHtCon=NULL;
        //關閉網路連線  
        intsess.Close();  
        return true;
    }  
    catch(CFileException e)  
    {  
        //釋放記憶體  
        delete []szBuffer;  
        delete pHtFile; 
        pHtFile=NULL;
        delete pHtCon;  
        pHtCon=NULL;
        //關閉網路連線  
        intsess.Close();  
        return false;          //讀寫檔案異常  
    }  
    return false;  
}
bool CHttpImage::HttpGetLogoURL(const char *phostName,const char *postData,int iLen,char *pResult,int *iResultLen)//獲取玩家影象URL地址
{
    unsigned short nPort;       //用於儲存目標HTTP服務埠  
    CString strServer, strObject;   //strServer用於儲存伺服器地址,strObject用於儲存檔案物件名稱  
    DWORD dwServiceType;      //dwServiceType用於儲存服務型別,dwRet用於儲存提交GET請求返回的狀態號  
    //解析URL,獲取資訊  
    if(!AfxParseURL(phostName, dwServiceType, strServer, strObject, nPort))  
    {  
        //解析失敗,該Url不正確  
        return false;  
    }  
    CInternetSession session;
    DWORD dwRet = 0;
    CHttpFile* pFile =(CHttpFile *)session.OpenURL(phostName);
    CString strHeaders = "Content-Type:application/x-www-form-urlencoded;charset=gbk"; // 請求頭
    //開始傳送請求
    pFile->SendRequest(strHeaders,strHeaders.GetLength(),(LPVOID)postData,iLen);
    pFile->QueryInfoStatusCode(dwRet);
    int index=0;
    if (dwRet == HTTP_STATUS_OK)
    {
        CString  newline;
        while(pFile->ReadString(newline))
        {//迴圈讀取每行內容
            memcpy(pResult+index,newline.GetBuffer(),sizeof(char)*newline.GetLength());
            index+=newline.GetLength();
        }
    }
    else
    {
        session.Close();
        if(pFile)
        {
            delete pFile;
            pFile=NULL;
        }
        return false;
    }
    *iResultLen=index;
    session.Close();
    if(pFile)
    {
        delete pFile;
        pFile=NULL;
    }
    std::string _cs(pResult);
    if(-1!=_cs.find(std::string("status"),0))
    {
        return false;
    }
    return true;
}