有人研究过凤之焚的MimeFilter例子吗? 或者熟悉网页内容过滤技术的请进。(MimeFilter Bug修正&&问题求解)

时间:2021-03-14 19:35:08
我想实现的效果是对于指定Web站点上的所有JPG图像在浏览器中显示的同时,也将图片的数据保存到硬盘上。
为了实现对图片数据的过滤,参考了凤之焚的《HTML代码过滤技术》一文,在此表示感谢!
http://www.cppblog.com/phenix-burn/archive/2006/08/29/11824.html   (文章)
http://download.csdn.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=0x%08X ThreadID=%d  hr=0x%08X     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分了, 大家见谅,以后有分了再补。 ^-^!

17 个解决方案

#1


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。

#2


十分感谢胡兄,不过我还是有点不明白 -_-!

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

#3


下面是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=0x%08X ThreadID=%d  hr=0x%08X     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;
}

#4


这个是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;
    }
}

#5


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

另外,CHTMLFilter::ReportData是被谁调用的?这个是你的自定义函数,应该是你自己调用的,为什么会被并发调用?

#6


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() 

#7


无需实现IInternetProtocolSink这个接口,这个接口指针系统已经实现了,在Start方法里面会传给你的,你只需保存下来即可,需要的时候就调用它的ReportXXX方法

#8


因为我是在IInternetProtocolSink::ReportData中实现目标数据过滤的, 
如果我不实现这个方法的话,那么我应该在哪个函数中实现目标数据过滤功能?

#9


目标数据过滤是什么意思?我没看到帖子中有过滤的需求,只有同时保存至文件的功能。

#10


是这样的,我希望实现的是对于指定类型的资源能够先经过我的MimeFilter,然后再由MimeFilter来交给上层。
保存文件是针对图片而言的,目标数据过滤是针对文本数据而言的。

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

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

#11


要实现过滤也无需实现IInternetProtocolSink。

在Start方法里,对于任何URL请求,如果你想做额外的处理,就按照上面的做法自己下载获得数据;
如果自己不想做特别处理,直接返回INET_E_USE_DEFAULT_PROTOCOLHANDLER;
如果想禁止此URL下载,可以直接返回多种错误码,比如INET_E_DATA_NOT_AVAILABLE、INET_E_DOWNLOAD_FAILURE、INET_E_INVALID_URL等等都可以。

#12


我好像找到并发启动的原因了,
我来验证一下,稍等

#13


并发的问题解决了,以下代码:
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,只能慢慢解决啦。
万分感谢胡柏华老兄的支持!!!

#14


......

#15


楼主  问你一个问题

我下载msdn上  xmlmimefilter 的例子
修改了一下mime对象为 image/gif
发现只有当右键选图片 然后点属性的时候 才能截获
这是为什么啊

#16


xmlmimefilter 的例子,为什么在我这里会死掉?

#17


此法似乎不行。
引用 11 楼  的回复:
要实现过滤也无需实现IInternetProtocolSink。

在Start方法里,对于任何URL请求,如果你想做额外的处理,就按照上面的做法自己下载获得数据;
如果自己不想做特别处理,直接返回INET_E_USE_DEFAULT_PROTOCOLHANDLER;
如果想禁止此URL下载,可以直接返回多种错误码,比如INET_E_DATA_NOT_AVAILABLE、INET_E_D……

#1


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。

#2


十分感谢胡兄,不过我还是有点不明白 -_-!

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

#3


下面是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=0x%08X ThreadID=%d  hr=0x%08X     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;
}

#4


这个是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;
    }
}

#5


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

另外,CHTMLFilter::ReportData是被谁调用的?这个是你的自定义函数,应该是你自己调用的,为什么会被并发调用?

#6


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() 

#7


无需实现IInternetProtocolSink这个接口,这个接口指针系统已经实现了,在Start方法里面会传给你的,你只需保存下来即可,需要的时候就调用它的ReportXXX方法

#8


因为我是在IInternetProtocolSink::ReportData中实现目标数据过滤的, 
如果我不实现这个方法的话,那么我应该在哪个函数中实现目标数据过滤功能?

#9


目标数据过滤是什么意思?我没看到帖子中有过滤的需求,只有同时保存至文件的功能。

#10


是这样的,我希望实现的是对于指定类型的资源能够先经过我的MimeFilter,然后再由MimeFilter来交给上层。
保存文件是针对图片而言的,目标数据过滤是针对文本数据而言的。

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

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

#11


要实现过滤也无需实现IInternetProtocolSink。

在Start方法里,对于任何URL请求,如果你想做额外的处理,就按照上面的做法自己下载获得数据;
如果自己不想做特别处理,直接返回INET_E_USE_DEFAULT_PROTOCOLHANDLER;
如果想禁止此URL下载,可以直接返回多种错误码,比如INET_E_DATA_NOT_AVAILABLE、INET_E_DOWNLOAD_FAILURE、INET_E_INVALID_URL等等都可以。

#12


我好像找到并发启动的原因了,
我来验证一下,稍等

#13


并发的问题解决了,以下代码:
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,只能慢慢解决啦。
万分感谢胡柏华老兄的支持!!!

#14


......

#15


楼主  问你一个问题

我下载msdn上  xmlmimefilter 的例子
修改了一下mime对象为 image/gif
发现只有当右键选图片 然后点属性的时候 才能截获
这是为什么啊

#16


xmlmimefilter 的例子,为什么在我这里会死掉?

#17


此法似乎不行。
引用 11 楼  的回复:
要实现过滤也无需实现IInternetProtocolSink。

在Start方法里,对于任何URL请求,如果你想做额外的处理,就按照上面的做法自己下载获得数据;
如果自己不想做特别处理,直接返回INET_E_USE_DEFAULT_PROTOCOLHANDLER;
如果想禁止此URL下载,可以直接返回多种错误码,比如INET_E_DATA_NOT_AVAILABLE、INET_E_D……