一、目标
在这篇文章中,我们要通过对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方法,相关信息请参见
“CSDN助手”源代码下载,请转到http://blog.csdn.net/seasol/archive/2006/07/04/873747.aspx