CSDN助手源碼剖析(二)--URL Moniker的封裝

一、目標

  在這篇文章中,我們要通過對URL Moniker的封裝,實現以下幾個功能:

  1. 支持URL的“GET”和“POST”兩種操作。
  2. 支持同步和異步調用。

二、約定

  我們建立一個類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;
 }

七、其他參考文章

  1. 關於CCuteTools::CopyStream方法,相關信息請參見爲何有些IStream不能得到HGlobal句柄 
  2. 關於CCuteTools::AutoWrap方法,相關信息請參見如何調用IDispatch接口的方法和屬性  
  3. “CSDN助手”源代碼下載,請轉到http://blog.csdn.net/seasol/archive/2006/07/04/873747.aspx
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章