概述
By Fanxiushu 2014 转载或引用请注明原作者
在Windows平台做IE内核浏览器,可以非常简单,拖拖控件就能形成一个简单的”浏览器“。
这顶多算是一个嵌入在应用程序中的一个COM控件而已,他不支持标签浏览,没处理弹出页面等等,压根算不上浏览器。
似乎解决多标签浏览,弹出页面处理这些问题不算太难,是不是这样呢? 还是从IE的基本(WebBrowser控件)说起。
浏览器内核:
其实最核心的是他的排版引擎,简单的说就是当HTML等众多文档被下载下来之后,如何在程序窗口上布局并显示出来。
世界上处于垄断地位的无非就几个排版引擎: Trident(IE内核),Gecko(火狐),WebKit(谷歌)。
这么说不大准确,只有Trident是微软为IE浏览器开发的排版引擎,
像Gecko和WebKit本身就是跨平台的开源项目,只是被火狐和谷歌采用。
其次就是脚本引擎,用于渲染执行javascript脚本。IE9以后微软使用的是一个叫chakra的脚本引擎,谷歌使用的是V8引擎。
最后就是下载引擎,简单的说就是通过HTTP协议下载各种HTML文档。
在Windows平台, wininet.dll动态库是各种HTTP下载的核心通讯库。
排版引擎,脚本引擎,下载引擎三大部件就凑成一个浏览器的核心部分。
比如Windows平台下,谷歌的chrome的下载使用的是wininet.dll, 脚本引擎是他自己开发的V8, 排版引擎使用的是开源的WebKit。
而微软的IE浏览器清一色是自己开发的,他的功能也是最强大的。
他提供的WebBrowser的COM控件,把IE的三大核心部件集成到COM控件里,
这样只要在自己开发的程序里,使用这个COM控件,就能非常方便的实现网页浏览功能。
现在要讨论的就是基于WebBrowser控件,使用C++语言,在VS2013环境下开发自己的IE内核的浏览器,
这里假设你已经非常熟悉Windows平台的COM组件技术和WIN32 API以及ATL的开发。
这里采用WIN32 API和ATL组合来开发这个浏览器的。ATL只是为了使用WebBrowser控件和COM接口的方便,
其他部分全是清一色的使用WIN32 API,放弃使用MFC,主要是觉得MFC会越用越乱,而且还要携带庞大的MFC库。
万事皆有开头,首先必须引入WebBrowser。
从ATL中引入,可以申明一个class继承自ATL库,大致伪代码如下:
///
#include <atlbase.h>
#include <ExDispid.h>
#import "c:windowssystem32ieframe.dll"
#include <atlwin.h>
#include <process.h>
#include <set>
#include <ShlObj.h>
#define IDC_IEXPLORE 0
class CWebBrowser :
public CWindowImpl<CWebBrowser, CAxWindow> ,
public IDispEventImpl< IDC_IEXPLORE, CWebBrowser, &DIID_DWebBrowserEvents2 ,&LIBID_SHDocVw, 1, 0>
{
public:
CComPtr<IWebBrowser2> m_pWebBrowser; // WebBrowser控件接口,这个很重要,操作网页,都必须通过他来完成
BEGIN_SINK_MAP(CWebBrowser)
SINK_ENTRY_EX( IDC_IEXPLORE, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, OnDocumentComplete )
....
SINK_ENTRY_EX( IDC_IEXPLORE, DIID_DWebBrowserEvents2, DISPID_NEWWINDOW3, OnNewWindow3)
.....
END_SINK_MAP()
//定义一些事件,比如其中DISPID_NEWWINDOW3表示有新窗口弹出,DISPID_DOCUMENTCOMPLETE代表文档下载完成等。
BEGIN_MSG_MAP(CWebBrowser)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
....
MESSAGE_HANDLER(WM_TIMER, OnTimer)
END_MSG_MAP()
//定义窗口事件,比如在OnCreate中初始化获得m_pWebBrowser接口等等。
。。。。
。。。。。
};
在我们的工程中,只要使用 CWebBrowser 类,就可以使用WebBrowser控件,
(以下描述都使用 CWebBrowser来代表做了扩展的WebBrowser控件)
比如这样使用 :
CWebBrowser* page = new CWebBrowser;
page->Create( parentWnd, ....); / / 创建WebBrowser控件子窗口
page->m_pWebBrowser->Navigate(CComBSTR("http://www.baidu.com"),....); / / 导航到指定URL
大家可以通过google或百度搜索到如何使用ATL中的WebBrowser控件这方面内容的大量资料。
当一个页面呈现在我们面前,点里边的超级链接,有两种跳转方式,
一种是直接在当前页面跳转,一种是弹出新窗口,如果我们不在 CWebBrowser里做处理,弹出新窗口的时候,
就会直接弹出新的微软默认的IE窗口了,
因此必须截获WebBrowser的这个默认行为,只要处理上边提到的 DISPID_NEWWINDOW3 事件即可(这个事件在WINXP SP2以上有效)。
对应的事件函数如下:
void OnNewWindow3(LPDISPATCH* ppDisp, VARIANT_BOOL * Cancel, DWORD dwFlags, BSTR bstrUrlContext, BSTR bstrUrl);
Cancel参数表示是否取消弹出新的窗口,设置为TRUE,表示不再弹出窗口,bstrUrl是要跳转的URL,
一般能想到的做法是,在这个回调函数中,把Cancel设置成TRUE,
然后新开一个自己的CWebBrowser窗口,再调用IWebBrowser接口的Navigate导航到 新的URL,
这个做法是不对的,这样做起码会丢失 Referer信息,正确的做法是 填充 ppDisp,然后直接返回。
就是自己新开一个CWebBrowser窗口,获得IWebBrowser接口,查询到IDispatch接口并填充到 ppDisp,然后直接返回。
Windows自身没有提供现代浏览器的那种标签控件,所以必须自己去绘画实现。
像firefox,chrome等各种浏览器的标签,都是自己实现的,功能大同小异。
我是从网上下载了一个别人做MFC浏览器标签控件,然后自己做了些修改移植成纯WIN32 API的格式。
一是因为懒惰,二是因为自绘繁琐,等以后闲的没事再自己重写这个Tab。
有了这些准备,现在可以开始做多标签页的浏览器了。
首先得布局程序的结构,在这里也走了弯路,因为一开始想得挺简单的(以前也一向不大重视WebBrowser这块内容):
就一个进程,创建一个主窗口,主窗口上边布局标签页控件,一些功能按钮,一个导航的编辑框等。
其余的留给 CWebBrowser子窗口用来呈现网页。
导航到新的Web页或者在OnNewWindow3中,new一个CWebBrowser,创建Web子窗口。
把他挂到一个全局队列,然后把IWebBrowser接口返回。
因为这些都是在同一个线程里操作,所以各种COM接口都可以直接使用。
因此本来想到这样就非常轻易的完成了一个具备现代功能需求的浏览器的开发。
等按照上边的想法开发完成,正在兴奋的时候。
不幸的事发生了,Web页面开多了之后,速度很慢,而且很不稳定。
在WIN7下,当Web页面达到10多个之后,程序崩溃,报告说什么资源不足。
在WINXP的情况更加糟糕,开不了几个Web页面,程序就崩溃。
忙乎了几天,等把程序做完了,才发现这么糟糕的问题。
然后禁不住开骂了:非常糟糕的WebBrowser控件,简直比firefox差太多了!
firefox在内存占用方面是做的非常优秀,开了50,60个页面,也才占用700-800M内存,有时还更少。
而WebBrowser开10多个页面,就能占用700-800M内存。
像我平时上网的习惯基本上都是只打开网页不关,所以很容易就达到40-50个页面以上。
(注: 测试的是32位WebBrowser,64位WebBrowser未测试过,
可能微软在64位WebBrowser控件中做了优化,能在单进程里可以打开非常多的PAGE也未可知,
但是有点可以肯定,内存浪费依然是非常高)
我所知道的是谷歌的chrome采用多进程,IE也是多进程+多线程方式,
基于上面的糟糕的结果,只要使用IE内核的其他浏览器,都会被迫使用多进程+多线程方式来处理。
就firefox牛XX,一个进程处理所有的页面,所以这里也要对firefox称赞一下。
没办法得推倒重来,重新布局程序的结构。基本思想是采用多进程显示网页。
主窗口单独在一个进程里,叫主进程,其他子进程专门管理CWebBrowser,显示网页。
每个子进程的主线程创建一个隐藏窗口用来跟主进程通讯,开辟新线程来创建CWebBrowser,
每个子进程管理3-5个Web页面,具体多少页面,是通过主进程获得子进程的内存占用情况来分配。
正是采用多进程,接下来的问题,一个比一个麻烦;这个才叫做从解决一个问题引入了多个其他问题。
首先是主进程和子进程之间需要进行通讯,需要随时传递数据:
创建子进程的时候,传递两个参数给子进程,一个是主进程的主窗口HWND句柄,一个是唯一字符串,
这个字符串用来在子进程创建隐藏窗口标题,在子进程建好之后,主进程通过FindWondow查找这个窗口,
就能确定子进程的用来通讯的窗口。这样,父子进程通过窗口来通讯就建立好了基本的联系。
多进程的之间的较大的数据传输是不可避免的,为了简单,统一使用 WM_COPYDATA消息来传递数据。
各个进程之间的通知消息,都是通过PostMessage和SendMessage来通知。
这样进程之间的通讯办法算是解决了,虽然从效率上讲是马马虎虎,但是贵在简单方便。
接着就是在 OnNewWindow3回调函数中解决弹出新窗口问题:
因为是在不同的线程,甚至是不同的进程中的不同线程,COM接口指针不能互相传递,
必须采用一种叫列集和散集的办法才能在多线程多进程之间传递COM接口指针。
如果只在 OnNewWindow3 回调函数中,简单打开新子进程,然后把URL作为参数传递过去,这倒是简单,
但是这种做法正如上边所说,会破坏网页浏览的连贯性,造成至少Referer的丢失。
因此,必须要在多线程之间传递COM接口指针,列集和散集的办法是不可避免的。
列集的时候使用CoMarshalInterface 函数,散集使用CoUnmarshalInterface 函数,
由于是在多进程传递,因此还必须创建一块共享内存来传递列集之后的IStream数据流。
列集简单流程如下:
调用CoGetMarshalSizeMax 获得需要列集的IWebBrowser接口列集之后需要的数据流大小,
调用 GlobalAlloc和CreateStreamOnHGlobal创建IStream数据流接口,
调用CoMarshalInterface把IWebBrowser列集到 IStream,这样GlobalAlloc分配出来的内存就是列集之后的数据。
调用CreateFileMaping等函数传教共享内存,把数据copy到共享内存。
散集简单流程如下:
调用OpenFileMaping等函数打开共享内存,调用GlobalAlloc等分配内存,把共享内存的数据copy到GlobalAlloc分配的内存中,
调用 CreateStreamOnHGlobal 在GlobalAlloc内存中创建 IStream流,
调用 CoUnmarshalInterface 散集,散集之后的 IwebBrowser接口就可以在当前的线程中正常使用了。
现在就得处理 OnNewWindow3中的实际弹窗问题,
由于OnNewWindow3是在某个 CWebBrowser的子进程中被调用的,因此他必须调用SendMessage发送一个消息到主进程,
告诉主进程我需要打开一个新的WebPage页面, 主进程获得这个消息之后,开始在自己维护的所有子进程中查找合适子进程。
主要是查看是否打开的WebPage超过一定得限制。
我的策略是如果某个子进程的WebPage小于3个,就直接把这个子进程的通讯窗口HWND句柄返回,
如果大于3个小于5个,就检查这个子进程占用的内存情况,如果没超过限制,也把这个子进程的通讯窗口HWND句柄返回,
如果当前的子进程池里没有符合要求的,就重新打开一个子进程,并把新子进程的通讯窗口的HWND句柄返回。
当在OnNewWindow3获得适当的子进程的通讯窗口HWND句柄之后,接着调用SendMessage发送一个消息到这个子进程,
告诉他我需要列集IWebBrowser接口,接受到这个消息的子进程开始列集IWebBrowser接口,
它首先在他的主线程(就是通讯窗口所在的线程)新开一个线程,
在新的线程里new 一个 CWebBrowser对象,创建一个空白的WebBrowser控件,建立起消息循环。
主线程然后发送消息到这个新线程,于是新线程列集IWebBrowser接口,列集成功之后返回。
这样调用 OnNewWindow3的子进程于是获得列集结果,他接着打开共享内存,开始散集接口,成功之后,就把接口传递给
OnNewWindow3的返回参数。
够头大的过程吧。反正是没想到什么简单的办法来解决这个弹出新窗口的问题。
接下来还有一个大麻烦,就是多进程共享Cookie的问题:
先简单介绍一下cookie这个玩意,是HTTP服务器生成的一块小数据,
Web服务器通过HTTP协议的Set-Cookie字段或者通过在网页中的js脚本中设置,浏览器负责保存这些cookie数据,
当下次请求的时候,浏览器原封不动的把这些cookie数据传回给HTTP服务器。
按照生存周期来区分的话,cookie可以分为内存cookie(或者叫会话cookie)和文件cookie。
内存cookie是保存在浏览器内存中,随着浏览器程序结束,也就消失了。
文件cookie是保存到文件中,不会消失,除非超时。
同时IE内核对第三方cookie限制的比较严格,这个不像firefox和chrome,他们都是默认允许第三方cookie的。
不过可以通过P3P头来改变这种限制,因此处理cookie的时候,还得考虑P3P头。
文件cookie因为是保存到文件中,能被多个进程共享,所以默认情况下,文件cookie是被IE内核共享的,
但是内存cookie,不同进程有不同的内存空间,这个是肯定不能共享的。
多进程cookie共享的问题,就是为了解决内存cookie共享的问题。
如果不解决这个问题,就会出现当你登录到一个网站,而在新窗口打开这个网站时候,显示的却不是登录状态。
因此应该解决这个问题。
如何解决呢?
首先需要获得每个进程的所有cookie,但是可惜没有这样的现成的函数。
上边说过了,cookie不是凭空产生的,他是HTTP服务传递给给浏览器的,
一种是通过HTTP通讯时候传递,一种是在网页的js脚本传递(不知道还有没有其他传递方式,暂时就只发现这两种传递方式)。
不管如何传递,Windows系统最终都会调用某个函数来设置到cookie内存池中去。
当初以为是InternetSetCookieEx函数,测试后发现js脚本确实是调用这个函数,但是在HTTP通讯中,调用的是更底层的函数。
而这个底层的函数并没有公布出来。
不管哪种设置方式,widows系统如果能提供一个回调函数指示某个cookie将要被设置,事情就变得简单多了。
可惜没有这样的回调函数(也许我还没发现这样的函数)。
幸好在查看wininet的资料的时候,发现InternetSetStatusCallback设置的回调函数中,有指示接收到COOKIE的状态,
而IE内核的下载引擎全是通过 wininet.dll来进行通讯的,所以HTTP通讯设置cookie就有办法获得。
如何获得呢? 自然是HOOK win32 API的办法来解决,就是挂钩本进程的 windows系统函数。
至于如何挂钩系统函数,解决办法很多,这里使用的是微软自己的detours库(1.5的版本)。
wininet导出的函数很多,不过无论如何变化,
HTTP请求最终都会调用HttpOpenRequestW(A) (W,A对应的是Unicode和ANSI版本) 打开请求回话句柄。
因此主要挂钩这个函数,在HttpOpenRequest的挂钩函数中,
使用InternetSetStatusCallback 设置我们自己定义的回调函数,同时要保存原来的通知回调函数和请求回话句柄的关联。
在我们自定义的回调函数中,当有INTERNET_STATUS_COOKIE_RECEIVED 通知的时候,
就表示接受到HTTP服务发来的Set-Cookie数据。
同时还要挂钩 InternetCloseHandle,主要目的是为了清除HttpOpenRequest中为每个请求回话句柄申请的资源。
还要挂钩 InternetSetCookieW ,是为了获得在js脚本设置的cookie。
完成以上步骤,就能实时的获得Cookie的变化通知。
获得实时的cookie并分析哪些是内存cookie之后,需要发送到到其他进程,这样才能达到同步内存Cookie的目的。
首先发送到主进程,主进程查找是否存在其他主进程(通过FindWindow查找指定的主进程窗口),是的话,就发送到其他主进程,
然后就是分发到主进程管理的其他子进程, (整个过程都是调用 SendMessage( ... WM_COPYDATA..) ; 完成数据传递)
最后还得保存一份到一个临时文件里,为了在新建子进程的时候,保证新子进程的内存cookie一致)
多么蛋疼的内存cookie分享过程(不知道哪位高人有没更简单的内存cookie分享办法)。
解决了以上麻烦,终于算是把为了解决一个问题而引入的其他问题处理了。
还有点小麻烦,WebBrowser控件在高版本的IE浏览器的工作模式是IE7的兼容模式,因此需要修改注册表改变这种模式,
让他达到跟当前windows系统安装的IE处于同样的工作模式下。这方面的资料,可以通过查Google等找到,就不赘述了。
注(有人询问P3P的问题,P3P的策略文件非常复杂看得叫人生怕,这里说的都是P3P Header):
多进程的 P3P共享也麻烦,当然可以采用避开P3P问题的办法,就是打开IE的Internet选项,然后在”隐私“设置里,选择接受所有Cookies。可是如果这么设置,不是所有用户都愿意或者懂得这么设置。
上边说到设置cookie有两种途径,一个是js脚本里最终系统会调用 InternetSetCookieEx函数设置,在hook到这个函数之后,就能通过第3个参数知道js脚本里有没有设置P3P头。
另外一个再HTTP通讯中,在回调函数里获得 Set-Cookie通知后,通过调用系统函数 HttpQueryInfo 传递 HTTP_QUERY_P3P(80)参数进去,就能查询到response里是否包含P3P头。
获得这些P3P头之后,在我们的程序里调用 InternetSetCookieEx 设置cookie的时候,原封不动的把获得的P3P Header设置进去,这样就能保持 P3P共享了。
但是我简单测试过,好像有些网站会失效,原因不得而知,也懒得去管了,
当然就是还有一个更加简单的办法:就是通过两个途径获得要设置的Cookie之后,每个Cookie都设置成 P3P可访问的。类似如下
InternetSetCookieEx( url, NULL, cookie, flags | INTERNET_COOKIE_EVALUATE_P3P, (DWORD_PTR)“CP=CAO PSA OUR” );
意思我管你以前是设置的啥P3P或者没设置P3P,我总是对每个Cookie都设置一个可访问权限的P3P,这个效果就类似在 Internet选项里设置隐私接受所有cookie。
本文章以及这文章所讲述的这个浏览器都会出现在 本人的 CSDN博客中,有兴趣的朋友可关注本人BLOG:
http://blog.csdn.net/fanxiushu/article/details/21837859
浏览器界面如下,纯 WIN32 API开发,浏览器程序只有300多K。
源代码下载地址:
http://download.csdn.net/detail/fanxiushu/7583355
应用程序下载地址:
http://download.csdn.net/detail/fanxiushu/7083905
最后
以上就是粗犷项链为你收集整理的基于IE内核的多标签浏览器开发过程的全部内容,希望文章能够帮你解决基于IE内核的多标签浏览器开发过程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复