一、目標
在這篇文章中,我們要通過對URL Moniker的封裝,實現以下幾個功能:
- 支持URL的“GET”和“POST”兩種操作。
- 支持同步和異步調用。
二、約定
我們建立一個類CCuteMoniker,通過向外部提供一個Request方法,從而實現以上兩個功能。
HRESULT CCuteMoniker::Request(LPCTSTR szMethod,LPCTSTR szURL,VARIANT_BOOL bAsync,
LPCTSTR szHeaders,LPCTSTR szPostData,
CuteHTTPResponseProc pProc,void* pParam1,CParam_Http_Base* pParam2,IStream** ppResponse,
DWORD nBindFlags)
參數說明:
szMethod:“GET”或“POST”
szURL:要訪問的URL
bAsync:同步還是異步
szHeaders:Request的頭部信息
szPostData:Request的POST提交數據
pProc:異步調用時的回調函數,當數據成功返回時,調用此函數
pParam1:與本次訪問相關的參數1
pParam2:與本次訪問相關的參數2
ppResponse:當同步調用時,返回數據流
nBindFlags:訪問時指定的綁定標誌,如可以指定BINDF_NOWRITECACHE,即“禁止將返回的數據寫入緩存”。這對於“CSDN助手”的緩存優化很重要。相關信息請參見文章CSDN助手源碼剖析(一)--緩存優化
特別說明:
參數pParam2類型爲CParam_Http_Base* 。CParam_Http_Base是參數基類。外部在訪問URL資源時,經常要分配一些資源。這時,可以從CParam_Http_Base派生一個類,將所有分配的資源放入其中。這樣,CCuteMoniker就會在合適的時候自動釋放這些資源。
三、外部調用舉例
下面舉一個回覆帖子的例子,主要有兩個方法。
CCSDNTools::topicReply:用於啓動向服務器提交數據;
CCSDNTools::func_topicReplyResponseProc:當回覆成功時,調用此回調函數。
1、CCSDNTools::topicReply
//回覆帖子
//bstrTopicID:帖子ID
//bstrContent:回覆正文
//pDispCallback:採用異步操作,當回覆成功時,調用這個回調接口
STDMETHODIMP CCSDNTools::topicReply(BSTR bstrTopicID, BSTR bstrContent, IDispatch* pDispCallback)
{
// TODO: 在此添加實現代碼
USES_CONVERSION;
//構造異步參數,CParam_Execute_TopicView派生自CParam_Http_Base
CParam_Execute_TopicView* pParamExecute=new CParam_Execute_TopicView();
pParamExecute->m_bstrUrl=::SysAllocString(bstrTopicID);
pParamExecute->m_pDispCallback=pDispCallback;
if(pDispCallback!=NULL)
pDispCallback->AddRef();//增加引用
//構造URL
LPCTSTR szTopicID=W2A(bstrTopicID);
CString sUrl;
sUrl.Format("http://community.csdn.net/Expert/reply.asp?Topicid=%s",szTopicID);
//構造Headers
CString sHeaders;
sHeaders.Format("Content-Type:application/x-www-form-urlencoded/r/nReferer:http://community.csdn.net/Expert/xsl/Reply_Xml.asp?Topicid=%s/r/n",szTopicID);
//對回覆的文本進行編碼
CString sContent;
EscapeToCString(sContent,W2A(bstrContent));
//構造Post Data
CString sPostData;
sPostData.Format("Topicid=%s&xmlReply=aaaaa&csdnname=&csdnpassword=&ReplyContent=%s",
szTopicID,sContent);
//新建一個CCuteMoniker對象
CComPtr<IUnknown> pUnkThis;
this->QueryInterface(IID_IUnknown,(void**)&pUnkThis);
CCuteMoniker* pHttp=new CCuteMoniker(pUnkThis);
//調用Request方法
HRESULT hr=pHttp->Request("POST",sUrl,VARIANT_TRUE,sHeaders,sPostData,
func_topicReplyResponseProc,NULL,pParamExecute,NULL,BINDF_GETNEWESTVERSION);
if(FAILED(hr))
{
return hr;
}
return S_OK;
}
2、CCSDNTools::func_topicReplyResponseProc
//回覆返回
//pParam1:參數1
//pParam2:參數2
//pHttpBase:這是CCuteMoniker的基類。
//pStream:這是返回的數據,用於指定回覆是否成功,及後續的操作指令
void CCSDNTools::func_topicReplyResponseProc(void* pParam1,CParam_Http_Base* pParam2,CCuteHttpBase* pHttpBase,IStream* pStream)
{
USES_CONVERSION;
//強制轉換參數2
CParam_Execute_TopicView* pParamExecute=(CParam_Execute_TopicView*)pParam2;
//構造URL,準備緩存結果
LPCTSTR szTopicID=W2A(pParamExecute->m_bstrUrl);
CString sUrl;
sUrl.Format("http://community.csdn.net/Expert/reply.asp?Topicid=%s",szTopicID);
//將數據緩存至Internet臨時目錄,爲的是讓瀏覽器轉向這個頁面,從而自動執行後續的操作指令。
char szFileName[MAX_PATH];
CCuteToolsB::SavetoCache(pStream,sUrl,szFileName,1,"htm",NULL);
//回調,將緩存得到的臨時文件名回調給外部調用者,以便瀏覽器轉向這個頁面。
CComVariant vParam1=szTopicID;
CComVariant vParam2=szFileName;
CCuteTools::AutoWrap(
DISPATCH_METHOD,NULL,pParamExecute->m_pDispCallback,NULL,2,vParam2,vParam1);
}
四、創建URL Moniker,啓動訪問過程
接下來,我們看看在CCuteMoniker::Request方法中如何創建URL Moniker對象,並啓動訪問過程。
CComPtr<IMoniker> m_spMoniker; //URL Moniker對象
CComPtr<IBindCtx> m_spBindCtx; //綁定環境,通過向綁定環境註冊一個回調接口,我們可以控制URL傳輸的過程,並得到反饋信息。
CComPtr<IStream> spStream; //如果是同步調用,可在綁定返回時,直接得到數據流
//創建一個URL Moniker對象
hr = CreateURLMoniker(NULL, A2W(szURL), &m_spMoniker);
//創建一個綁定環境
hr = CreateBindCtx(0, &m_spBindCtx);
//向綁定環境註冊一個回調接口IBindStatusCallback,CCuteMoniker派生自接口IBindStatusCallback。
hr = RegisterBindStatusCallback(m_spBindCtx, static_cast<IBindStatusCallback*>(this), 0, 0L);
//執行綁定,啓動實際的URL訪問及數據傳輸過程。
hr = m_spMoniker->BindToStorage(m_spBindCtx, 0, __uuidof(IStream), (void**)&spStream);
//如果是同步操作,則直接返回數據流
if(!bAsync)
{
ATLASSERT(ppResponse!=NULL);
//複製數據流,並返回。
return CCuteTools::CopyStream(spStream,ppResponse);
}
五、綁定狀態回調接口IBindStatusCallback
CCuteMoniker派生自接口IBindStatusCallback,在實際的訪問及數據傳輸過程中,Moniker對象會通過接口IBindStatusCallback取得相關的綁定信息,如綁定標誌、訪問方法、Post Data,也可以通過它反饋當前的進度,彙報返回的數據。
1、IBindStatusCallback::OnStartBinding方法。
將傳入的參數IBinding *pBinding保存下來。通過IBinding ,我們可以暫停、重啓、中止綁定過程。
STDMETHOD(OnStartBinding)(DWORD /*dwReserved*/, IBinding *pBinding)
{
ATLTRACE(atlTraceControls,2,_T("CBindStatusCallback::OnStartBinding/n"));
m_spBinding = pBinding;
return S_OK;
}
2、IBindStatusCallback::GetBindInfo方法。
通過這個方法,我們可以指定綁定標誌、訪問方法、Post Data。
STDMETHOD(GetBindInfo)(DWORD *pgrfBINDF, BINDINFO *pbindInfo)
{
ATLTRACE(atlTraceControls,2,_T("CBindStatusCallback::GetBindInfo/n"));
if (pbindInfo==NULL || pbindInfo->cbSize==0 || pgrfBINDF==NULL)
return E_INVALIDARG;
//綁定標誌,
//默認爲 (BINDF_ASYNCHRONOUS | BINDF_ASYNCSTORAGE |BINDF_GETNEWESTVERSION | BINDF_NOWRITECACHE)
*pgrfBINDF = m_dwBindFlags;
//初始化結構體
ULONG cbSize = pbindInfo->cbSize; // remember incoming cbSize
memset(pbindInfo, 0, cbSize); // zero out structure
pbindInfo->cbSize = cbSize; // restore cbSize
//指定訪問方法
if(this->m_sMethod=="POST")
pbindInfo->dwBindVerb = BINDVERB_POST;
else
pbindInfo->dwBindVerb = BINDVERB_GET;
//指定post data
pbindInfo->cbstgmedData=this->m_dwPostSize;
pbindInfo->stgmedData.tymed=TYMED_HGLOBAL;
pbindInfo->stgmedData.hGlobal=this->m_hGlobalPost;
return S_OK;
}
3、IBindStatusCallback::OnDataAvailable方法
當有數據返回(可能分多次返回)時,調用此方法。我們可以得到返回的數據流對象及數據大小
STDMETHOD(OnDataAvailable)(DWORD grfBSCF, DWORD dwSize, FORMATETC * /*pformatetc*/, STGMEDIUM *pstgmed)
{
ATLTRACE(atlTraceControls,2,_T("CBindStatusCallback::OnDataAvailable/n"));
HRESULT hr = S_OK;
//當第一次返回數據時,設置標誌BSCF_FIRSTDATANOTIFICATION
//這時,我們取得數據流對象
if (BSCF_FIRSTDATANOTIFICATION & grfBSCF)
{
if (!m_spStream && pstgmed->tymed == TYMED_ISTREAM)
m_spStream = pstgmed->pstm;
}
//當最後一次返回數據時,設置標誌BSCF_LASTDATANOTIFICATION
//這時,我們取得數據的完整大小
if (BSCF_LASTDATANOTIFICATION & grfBSCF)
{
this->m_dwTotalRead=dwSize;
}
return hr;
}
4、IBindStatusCallback::OnStopBinding方法
在綁定結束的時候,最後執行這個方法。這時,如果是異步調用,我們就可以把得到的數據通過回調函數返回給外部。
STDMETHOD(OnStopBinding)(HRESULT hresult, LPCWSTR /*szError*/)
{
ATLTRACE(atlTraceControls,2,_T("CBindStatusCallback::OnStopBinding/n"));
//清理對象
if(m_spBinding!=NULL)
m_spBinding.Release();
if(m_spBindCtx!=NULL)
m_spBindCtx.Release();
if(m_spMoniker!=NULL)
m_spMoniker.Release();
//如果是異步調用,則執行回調
if(this->m_bAsync)
{
ATLASSERT(m_pProc!=NULL);
//回調
try
{
//複製數據流
CComPtr<IStream> pStream;
CCuteTools::CopyStream(m_spStream,&pStream);
//調用回調函數
this->m_pProc(this->m_pParam1,this->m_pParam2,this,pStream);
}
catch(_com_error& e)
{}
}
//釋放數據流
if(m_spStream!=NULL)
m_spStream.Release();
//如果是異步,釋放自身
if(this->m_bAsync)
{
this->Release();
}
return S_OK;
}
5、IBindStatusCallback還有其他幾個方法,由於在"CSDN助手"中沒有用到,所以簡單的返回S_OK。
六、接口IHttpNegotiate,處理Headers信息
至此,一個基本的URL訪問框架已經成形了。但還沒有解決在Request時發送Headers,當Response時得到Headers的問題。
接口IHttpNegotiate可以幫助我們解決這個問題。在MSDN中,有這樣一句話來描述IHttpNegotiate:“Urlmon.dll uses the QueryInterface method on your implementation of IBindStatusCallback to obtain a pointer to your IHttpNegotiate interface.”。由於類CCuteMoniker派生自接口IBindStatusCallback,顯然我們還要讓類CCuteMoniker派生自接口IHttpNegotiate。這樣,Moniker對象才能通過已註冊的接口IBindStatusCallback得到接口IHttpNegotiate。
1、IHttpNegotiate::BeginningTransaction方法,提供Request時的Headers信息
virtual HRESULT STDMETHODCALLTYPE BeginningTransaction(
/* [in] */ LPCWSTR szURL,
/* [unique][in] */ LPCWSTR szHeaders,
/* [in] */ DWORD dwReserved,
/* [out] */ LPWSTR *pszAdditionalHeaders)
{
USES_CONVERSION;
if(this->m_sHeaders!="")
{
LPCWSTR swzHeaders=A2W(this->m_sHeaders);
int nSize=(wcslen(swzHeaders)+1)*2;
//必須用CoTaskMemAlloc分配內存,因爲Monker對象用CoTaskMemFree進行釋放。
LPWSTR pszHeaders=(LPWSTR)CoTaskMemAlloc(nSize);
memcpy(pszHeaders,swzHeaders,nSize);
*pszAdditionalHeaders=pszHeaders;
}
return S_OK;
}
2、IHttpNegotiate::OnResponse方法,在這裏我們可以得到Response的Headers信息及響應碼。
virtual HRESULT STDMETHODCALLTYPE OnResponse(
/* [in] */ DWORD dwResponseCode,
/* [unique][in] */ LPCWSTR szResponseHeaders,
/* [unique][in] */ LPCWSTR szRequestHeaders,
/* [out] */ LPWSTR *pszAdditionalRequestHeaders)
{
this->m_nResponseStatus=dwResponseCode;
this->m_sResponseHeaders=szResponseHeaders;
return S_OK;
}
七、其他參考文章
- 關於CCuteTools::CopyStream方法,相關信息請參見爲何有些IStream不能得到HGlobal句柄
- 關於CCuteTools::AutoWrap方法,相關信息請參見如何調用IDispatch接口的方法和屬性
- “CSDN助手”源代碼下載,請轉到http://blog.csdn.net/seasol/archive/2006/07/04/873747.aspx