有人研究过凤之焚的MimeFilter例子吗 或者熟悉网页内容过滤技术的请进。MimeFilter Bu

时间:2022-01-15 18:28:07

我想实现的效果是对于指定Web站点上的所有JPG图像在浏览器中显示的同时,也将图片的数据保存到硬盘上。
为了实现对图片数据的过滤,参考了凤之焚的《HTML代码过滤技术》一文,在此表示感谢!
http://www.cppblog.com/phenix-burn/archive/2006/08/29/11824.html (文章)
http://download..net/source/158515 (对应的例子)

整体的实现思路是这样子的:
1. 在Start函数中获得目标URL
2. 在ReportData函数中调用UrlMonProtocol的Read方法获得目标URL的数据,并存放入DataStream中
同时将目标数据写入本地图片中
3. 在Read函数中将已经读取完的数据交给上层显示。

基本流程和原来的例子一致,但在使用中碰到了几个问题,提出来恳请大家指点。

1、 在MimeFilter.cpp文件的145行, 原本是
HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
但在实际使用中发现,假如直接执行的话,程序无法启动,在VS的调试窗口内有以下信息输出:
Warning: OleInitialize returned scode = RPC_E_CHANGED_MODE ($80010106).

我把这条语句修改为 HRESULT hRes = CoInitialize(NULL); 就没有这个问题了,程序可以正常启动了
不知道这样修改对不对?

2、 通过实际的调试发现:对于一个指定的目标URL,会产生一个对应的MimeFilter过滤器实例对象。
其中,过滤器实例的Start方法仅会被调用一次,但ReportData方法可能会在不同的进程中被并发调用。
ReportData并发调用的情况并不是每次都会出现,假如监控的是文字的话,很少会出现,但假如监控的是图片的话,出现的概率就比较大。
PS1: 凤之焚例子中的DataStream的初始化应该放在Start函数中执行,不然的话,在并发调用ReportData的情况下,可能会冲掉以前接收好的数据的。
所以最好将以下二行代码放到Start函数中:
DataStream = NULL;
CreateStreamOnHGlobal(0, true, &DataStream);
PS2: 一旦出现ReportData并发调用的情况,就会出现死循环的情况。主要是由于UrlMonProtocol->Read在经过几次有数据的成功调用之后,之后的调用总是返回E_PENDING,具体请参考下面的LOG内容

测试代码如下:
do
{
memset(p,0,sizeof(p));
hr = UrlMonProtocol->Read(p, sizeof(p)-1, &Readtotal);

if (Readtotal > 0){
DataStream->Write(p,Readtotal,&cbWritten);
TotalSize = Readtotal;
}
TRACE(TEXT("this=0xX ThreadID=%d hr=0xX Readtotal=%d TotalSize=%d/r/n"), this, GetCurrentThreadId(), hr, Readtotal, TotalSize);
}while((hr != S_FALSE) && (hr != INET_E_DOWNLOAD_FAILURE) && (hr != INET_E_DATA_NOT_AVAILABLE));

LOG内容如下:
this=0x00377B18 ThreadID=3304 <== Start函数中的输出
this=0x00377B18 ThreadID=3304 hr=0x00000000 Readtotal=1023 TotalSize=1023 <== ReportData函数中的输出(进程ID:3304)
this=0x00377B18 ThreadID=3304 hr=0x00000000 Readtotal=122 TotalSize=1145 <== ReportData函数中的输出(进程ID:3304)
this=0x00377B18 ThreadID=3304 hr=0x8000000A Readtotal=0 TotalSize=1145 <== ReportData函数中的输出(进程ID:3304)
this=0x00377B18 ThreadID=2228 hr=0x00000000 Readtotal=1023 TotalSize=2168 <== ReportData函数中的输出(进程ID:2228)
this=0x00377B18 ThreadID=2228 hr=0x00000000 Readtotal=1023 TotalSize=3191 <== ReportData函数中的输出(进程ID:2228)
this=0x00377B18 ThreadID=2228 hr=0x00000000 Readtotal=850 TotalSize=4041 <== ReportData函数中的输出(进程ID:2228)
this=0x00377B18 ThreadID=2228 hr=0x8000000A Readtotal=0 TotalSize=4041 <== 后面调用UrlMonProtocol->Read的返回值都是E_PENDING(0x8000000A)
this=0x00377B18 ThreadID=3304 hr=0x8000000A Readtotal=0 TotalSize=4041
this=0x00377B18 ThreadID=2228 hr=0x8000000A Readtotal=0 TotalSize=4041
this=0x00377B18 ThreadID=3304 hr=0x8000000A Readtotal=0 TotalSize=4041
this=0x00377B18 ThreadID=2228 hr=0x8000000A Readtotal=0 TotalSize=4041
this=0x00377B18 ThreadID=3304 hr=0x8000000A Readtotal=0 TotalSize=4041
...... 无限循环下去.....

这个问题搞了很久了,一直想不明白,求高人指点一下如何解决,谢谢啦。
分数就剩40分了, 大家见谅,以后有分了再补。 ^-^!

网友回复:1、无需再调用CoInitialize或CoInitializeEx,因为线程已经被初始化为STA了。
2、ReportData用法错误。一个文件的下载只会对应一个mimefilter,出现多个的原因是因为第一个filter没有正确向浏览器报告下载结果,导致浏览器以为它出错,从而重启新的filter来执行下载。
假如对文件的下载是同步的,执行一次ReportData就够了,假如下载是异步的,应该分多次调用ReportData,调用时需要使用不同的标志,有 三个标志一定要用到BSCF_FIRSTDATANOTIFICATION、BSCF_LASTDATANOTIFICATION、 BSCF_DATAFULLYAVAILABLE,同步调用时这三个标志可以或到一起。
在最后一次ReportData之后要调用ReportResult(S_OK, 0, NULL);
同理,在Read方法读完最后的数据后也需要调用ReportResult(S_OK, 0, NULL)并返回S_FALSE。
网友回复:十分感谢胡兄,不过我还是有点不明白 -_-!

第一点没什么问题
第二点:
一个文件的下载只会对应一个mimefilter,这个没错,但是我这边并不是出现了多个filter,只是原来的那个filter的ReportData被并发调用。
怀疑和对文件的下载是否同步有关系,但我不知道如何控制使的下载变成同步
网友回复:下面是ReportData的具体实现,麻烦帮我看一下

STDMETHODIMP CHTMLFilter::ReportData( DWORD grfBSCF, ULONG ulProgress, ULONG ulProgressMax)
{
USES_CONVERSION;
//存储网页代码
char p[1024];
HRESULT hr;
ULONG Readtotal;
ULONG cbWritten=0;

do{
memset(p,0,sizeof(p));
hr = UrlMonProtocol->Read(p, sizeof(p)-1, &Readtotal);

if (Readtotal > 0){
DataStream->Write(p,Readtotal,&cbWritten);
TotalSize = Readtotal;
}
TRACE(TEXT("this=0xX ThreadID=%d hr=0xX Readtotal=%d TotalSize=%d/r/n"), this, GetCurrentThreadId(), hr, Readtotal, TotalSize);

}while((hr != S_FALSE) && (hr != INET_E_DOWNLOAD_FAILURE) && (hr != INET_E_DATA_NOT_AVAILABLE));

if(hr == S_FALSE)
{
ULARGE_INTEGER Dummy;
_LARGE_INTEGER zero;
zero.QuadPart =0;
DataStream->Seek ( zero, STREAM_SEEK_SET, &Dummy);

UrlMonProtocolSink->ReportData(BSCF_FIRSTDATANOTIFICATION ¦ BSCF_LASTDATANOTIFICATION ¦ BSCF_DATAFULLYAVAILABLE, TotalSize, TotalSize);
UrlMonProtocolSink->ReportResult(S_OK, S_OK, NULL);
}
else
{
Abort(hr, 0);
}
return S_OK;
}

网友回复:这个是Read的实现
STDMETHODIMP CHTMLFilter::Read(void *pv, ULONG cb, ULONG *pcbRead)
{
DataStream->Read(pv, cb, pcbRead);
Written =*pcbRead;
if (Written == TotalSize)
{
ReportResult(S_OK, 0, NULL);
return S_FALSE;
}
else
{
return S_OK;
}
}


网友回复:我似乎回答过类似的问题。
在Read实现中把 if (Written == TotalSize) 改成 if (Written >= TotalSize) 这个很重要。

另外,CHTMLFilter::ReportData是被谁调用的?这个是你的自定义函数,应该是你自己调用的,为什么会被并发调用?
网友回复:CHTMLFilter::ReportData实现的是IInternetProtocolSink接口的ReportData方法,
他不是我的自定义函数,是被系统回调的,我似乎没法控制的。

下面是ReportData函数在被并发调用时分别对应的堆栈情况:
堆栈1:
> Mimefilter.exe!CHTMLFilter::ReportData(unsigned long grfBSCF=1, unsigned long ulProgress=2594, unsigned long ulProgressMax=98590) Line 119 C
urlmon.dll!42cf83b7()
urlmon.dll!42d31db1()
urlmon.dll!42d0c844()
urlmon.dll!42d209b5()
ntdll.dll!7c96d886()
ntdll.dll!7c949d18()
ntdll.dll!7c91b686()
oleaut32.dll!771215f8()
oleaut32.dll!77121629()
ntdll.dll!7c949b34()
ntdll.dll!7c926a44()
ntdll.dll!7c926abe()
urlmon.dll!42cf192a()
urlmon.dll!42d20d52()
urlmon.dll!42d10eb8()
urlmon.dll!42d10e8e()
urlmon.dll!42d10cc2()
urlmon.dll!42d10c9f()
urlmon.dll!42d0f128()
Mimefilter.exe!CHTMLFilter::Continue(_tagPROTOCOLDATA * pProtocolData=0x0321fd34) Line 40 0x29 C
urlmon.dll!42d317d2()
urlmon.dll!42d0f75f()
urlmon.dll!42d20d47()
urlmon.dll!42d20d26()
wininet.dll!42c1eb3d()
mswsock.dll!71a544b0()
ws2_32.dll!71ab93c2()
mswsock.dll!71a544b0()
ntdll.dll!7c91056d()
kernel32.dll!7c80995a()
kernel32.dll!7c80996d()
wininet.dll!42c4355d()
kernel32.dll!7c80996d()
wininet.dll!42c14507()
wininet.dll!42c20ba5()
wininet.dll!42c1eeb7()
wininet.dll!42c207c4()
shlwapi.dll!77f69548()
ntdll.dll!7c927545()
ntdll.dll!7c927583()
ntdll.dll!7c927645()
ntdll.dll!7c92761c()
kernel32.dll!7c80b683()
ntdll.dll!7c910760()

堆栈2:
Mimefilter.exe!CHTMLFilter::ReportData(unsigned long grfBSCF=1, unsigned long ulProgress=1, unsigned long ulProgressMax=98590) Line 119 C
urlmon.dll!42cf83b7()
urlmon.dll!42d31db1()
urlmon.dll!42d0c844()
urlmon.dll!42d209b5()
网友回复:无需实现IInternetProtocolSink这个接口,这个接口指针系统已经实现了,在Start方法里面会传给你的,你只需保存下来即可,需要的时候就调用它的ReportXXX方法
网友回复:因为我是在IInternetProtocolSink::ReportData中实现目标数据过滤的,
假如我不实现这个方法的话,那么我应该在哪个函数中实现目标数据过滤功能?
网友回复:目标数据过滤是什么意思?我没看到帖子中有过滤的需求,只有同时保存至文件的功能。
网友回复:是这样的,我希望实现的是对于指定类型的资源能够先经过我的MimeFilter,然后再由MimeFilter来交给上层。
保存文件是针对图片而言的,目标数据过滤是针对文本数据而言的。

但现在在ReportData这个函数处碰壁了,因为经常会碰到二个并发的ReportData从而造成死循环。

刚才我试了一下,将利用UrlMonProtocol->Read循环读取网络上的数据这段代码放到Start函数中,结果发现Start函数并发了,昏过去了。

网友回复:要实现过滤也无需实现IInternetProtocolSink。

在Start方法里,对于任何URL请求,假如你想做额外的处理,就按照上面的做法自己下载获得数据;
假如自己不想做非凡处理,直接返回INET_E_USE_DEFAULT_PROTOCOLHANDLER;
假如想禁止此URL下载,可以直接返回多种错误码,比如INET_E_DATA_NOT_AVAILABLE、INET_E_DOWNLOAD_FAILURE、INET_E_INVALID_URL等等都可以。
网友回复:我似乎找到并发启动的原因了,
我来验证一下,稍等
网友回复:并发的问题解决了,以下代码:
STDMETHODIMP CHTMLFilter::Continue(PROTOCOLDATA *pProtocolData)
{
UrlMonProtocol->Continue(pProtocolData);
return S_OK;
}
修正如下:
STDMETHODIMP CHTMLFilter::Continue(PROTOCOLDATA *pProtocolData)
{
//UrlMonProtocol->Continue(pProtocolData);
return E_NOTIMPL;
}
================================================================
总算搞清楚ReportData为什么会并发了,当UrlMonProtocol->Read的返回值hr=E_PENDING的时候,
UrlMon会调用扩展的Continue方法,假如在扩展的Continue方法中再调用UrlMonProtocol->Continue的话,就会产生并发问题了。

就着一个小小的地方,搞了整整三天,郁闷死了,不过幸好最终还是解决了。
凤之焚兄的例子中似乎还有一些小BUG,只能慢慢解决啦。
万分感谢胡柏华老兄的支持!!!