网页和原生程序的交互方案
1 ActiveX和BHO是微软开发且闭源的,仅适用于IE
这里就不讨论了,这种方式会给用户带来很大的安全风险。而且也不符合html5标准,现在已经被市场抛弃。
2 搜索挂接(URL SEARCHHOOK)
在window系统中,通过在注册表中,写入相应的键值信息,来实现通过在浏览器中,通过url来打开本地原生程序的目的。此方法实现很简单,但是缺点也是很明显的,第一、每次调用都会弹出询问是否打开当前的可执行程序,如下图所示:
第二、不能实现实时的交互,只能通过参数将外部进行带入一次;
具体的示例代码如下,网上到处都是这种代码,如果不够清除,可以自己去检索;
int RegWebProtocol ( LPCTSTR lpszProtocolName, LPCTSTR lpszAssociatedApp, int nIconIndex/*=0*/ )
{
if ( !lpszProtocolName ||
lstrlen(lpszProtocolName) < 1 ||
!lpszAssociatedApp ||
lstrlen(lpszAssociatedApp) < 1 )
return 0;
CString csSubKey;
DWORD dwBufSize = 0;
// 如果该该协议已经存在
HKEY hKey = NULL;
if ( RegOpenKeyEx ( HKEY_CLASSES_ROOT,
lpszProtocolName,
0,
KEY_ALL_ACCESS,
&hKey ) == ERROR_SUCCESS )
{
RegCloseKey(hKey);
csSubKey.Format ( _T("%s\\shell\\open\\command"), lpszProtocolName );
CString csCommand; csCommand.Format ( _T("\"%s\" \"%%1\""), lpszAssociatedApp );
dwBufSize = csCommand.GetLength();
if ( !WriteRegister ( HKEY_CLASSES_ROOT, csSubKey,
_T(""), REG_SZ, (PUCHAR)csCommand.GetBuffer(0),&dwBufSize) )
return 0;
return 2;
}
else
hKey = NULL;
// 创建协议子键
if (!CreateRegisterSubKey ( HKEY_CLASSES_ROOT, lpszProtocolName ))
return 0;
// 设置协议描述字符串
CString csProtocolDesc;
csProtocolDesc.Format ( _T("%sProtocol"), lpszProtocolName );
dwBufSize = csProtocolDesc.GetLength();
if ( !WriteRegister ( HKEY_CLASSES_ROOT, lpszProtocolName,
_T(""), REG_SZ, (PUCHAR)csProtocolDesc.GetBuffer(0),&dwBufSize) )
return 0;
CString csAppFile; csAppFile.Format ( _T("%s"), lpszAssociatedApp );
dwBufSize = csAppFile.GetLength();
if ( !WriteRegister ( HKEY_CLASSES_ROOT, lpszProtocolName,
_T("URL Protocol"), REG_SZ, (PUCHAR)csAppFile.GetBuffer(0),&dwBufSize) )
return 0;
// DefaultIcon 子键
csSubKey.Format ( _T("%s\\DefaultIcon"), lpszProtocolName );
if ( !CreateRegisterSubKey ( HKEY_CLASSES_ROOT, csSubKey ) )
return 0;
CString csIconParameter; csIconParameter.Format ( _T("%s,%d"), lpszAssociatedApp, nIconIndex );
dwBufSize = csIconParameter.GetLength();
if ( !WriteRegister ( HKEY_CLASSES_ROOT, csSubKey,
_T(""), REG_SZ, (PUCHAR)csIconParameter.GetBuffer(0),&dwBufSize) )
return 0;
// shell\open\command 子键
csSubKey.Format ( _T("%s\\shell\\open\\command"), lpszProtocolName );
if ( !CreateRegisterSubKey ( HKEY_CLASSES_ROOT, csSubKey ) )
return 0;
CString csCommand; csCommand.Format ( _T("\"%s\" \"%%1\""), lpszAssociatedApp );
dwBufSize = csCommand.GetLength();
if ( !WriteRegister ( HKEY_CLASSES_ROOT, csSubKey,
_T(""), REG_SZ, (PUCHAR)csCommand.GetBuffer(0),&dwBufSize) )
return 0;
return 1;
}
其中,HKEY_CLASSES_ROOT和HKEY_LOCAL_MACHINE\SOFTWARE\Classes是一样的。具体的原理是,The IURLSearchHook interface is used by the browser to translate the address of an unknown URL protocol.
When attempting to browse to a URL address that does not contain a protocol,
the browser will first attempt to determine the correct protocol from the address.
If this is not successful, the browser will create URL Search Hook objects and call each object's
Translate method until the address is translated or all of the hooks have been queried.
IURLSearchHook接口被浏览器用来转换一个未知的URL协议地址。
当浏览器企图去打开一个未知协议的URL地址时,浏览器首先尝试从这个地址得到当前的协议,如果不成功,浏览器将创建在系统中注册的URL Search Hook对象并调用每一个对象的Translate方法,直到地址被转换或所有的URL Search Hook都尝试过。 也就是说,我们可以注册一种目前不存在的协议(类似HTTP),当浏览器遇到新的协议时会自动调用Translate方法来翻译我们的协议, 甚至激活我们自己的程序。这里注意,改动注册表的时候,必须以管理员身份来运行;
以下是调用的代码
RegWebProtocol("YYP", "D:\\xxx\\测试源码\\SurfaceRender\\UrlCall\\Debug\\UrlCall.exe", 1);
程序就会自动运行起来,运行的结果如下
3. Chromium Embedded Framework (CEF)
其实,此方案也是用来解决BS和CS架构目前最优的方案,既可以利用BS中的所有优势,升级快,部署方便,另外,界面的开发速度也快,还能有界面的高还原度;又能对程序需要控制系统资源的需求得到满足。Adobe和微软的几款产品都是基于CEF开发的,足见此方案的分量。
英文帮助的文档 https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-entry-point-function
3.1 同源(origin)
origin = scheme(协议)+ domain(域名)+port(端口);例如,
url= "http://baidu.com:80/pub/new";
origin = "http://baidu.com:80";
CEF3 runs using multiple processes. The main process which handles window creation, painting and network access is called the “browser” process. This is generally the same process as the host application and the majority of the application logic will run in the browser process. Blink rendering and JavaScript execution occur in a separate “render” process. Some application logic, such as JavaScript bindings and DOM access, will also run in the render process. The default process model will spawn a new render process for each unique origin (scheme + domain). Other processes will be spawned as needed, such as “plugin” processes to handle plugins like Flash and “gpu” processes to handle accelerated compositing.
3.2 浏览器进程 Browser Process和渲染进程 Render Process
目前我的认知内,一个实例只有一个浏览器进程。它的作用有二。第一、负责通过CefBrowserHost::CreateBrowser函数来创建页面窗口,以及,对应的渲染进程和其他进程(插件进程和GPU进程等进程);第二、其他的进程向浏览器进程发送消息,进行通讯;我们可以定制不一样的回调句柄,来处理renderer process发送的信息;使用CefBrowserHost::CreateBrowser函数,为新的原点(origin)创建新的html的浏览器(CefBrowserView)和当前原生窗口绑定到一起(Windows系统,就是一个对话框或者Frame),另外,浏览器进程会接受子进程的消息回调,用来处理这些子进程的发出消息。回调的类,需要开发者自定义一个ClientHandler类用来继承一些Cef的接口,来处理渲染进程或其他进程发送来的标准回调消息;例如,处理子进程生命周期的回调函数,下载的回调等,例如,CefLifeSpanHandler,表示的是Implement this interface to handle events related to browser life span. The methods of this class will be called on the UI thread unless otherwise indicated. 当使用函数CefBrowserHost::CreateBrowser(info, m_handler.get(), strUrl, settings, NULL);来创建浏览器的时候,会调用CefLifeSpanHandler接口中的OnAfterCreated函数关闭的时候,会调用 OnBeforeClose函数
class ClientHandler
: public CefClient,
public CefDownloadHandler,
public CefLoadHandler,
public CefLifeSpanHandler,
public CefRenderHandler
{
// Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(ClientHandler);
public:
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE
{
}
virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE
{
}
virtual CefRefPtr<CefDownloadHandler> GetDownloadHandler() OVERRIDE
{return this;
}
virtual CefRefPtr<CefLoadHandler> GetLoadHandler() OVERRIDE
{return this;
}
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) OVERRIDE
{
ClientApp::RenderDelegateSet::iterator it = render_delegates_.begin();
for (; it != render_delegates_.end(); ++it)
(*it)->OnProcessMessageReceived(NULL, browser, source_process, message);
return true;
}
virtual void OnPaint(CefRefPtr<CefBrowser> browser,
PaintElementType type,
const RectList& dirtyRects,
const void* buffer,
int width, int height)
{
}
};
3.3 CefFrame 和 CefBrowser
没一个CefBrowser都有一个主的CefFrame和0和多个子CefFrame,例如一个网页包含两个iFarme标签,就是一个主的CefFrame,两个子CefFrame对象;Each CefBrowser object will have a single main CefFrame object representing the top-level frame and zero or more CefFrame objects representing sub-frames. For example, a browser that loads two iframes will have three CefFrame objects (the top-level frame and the two iframes).
3.4 动态交互,进程间的通讯 Inter-Process Communication (IPC)
由于CEF3在多个进程中运行,因此有必要提供用于在这些进程之间进行通信的机制。CefBrowser和CefFrame对象同时存在于浏览器和渲染过程中,这样设计有助于进程间通讯。每个CefBrowser和CefFrame对象也有一个与其关联的唯一ID值,该值将在流程边界的两侧匹配。Since CEF3 runs in multiple processes it is necessary to provide mechanisms for communicating between those processes. CefBrowser and CefFrame objects exist in both the browser and render processes which helps to facilitate this process. Each CefBrowser and CefFrame object also has a unique ID value associated with it that will match on both sides of the process boundary. 进程间的消息可以通过CefProcessMessage来实现,示例如下
// Create the message object.
CefRefPtr<CefProcessMessage> msg= CefProcessMessage::Create(“my_message”);
// Retrieve the argument list object.
CefRefPtr<CefListValue> args = msg>GetArgumentList();
// Populate the argument values.
args->SetString(0, “my string”);
args->SetInt(0, 10);
// Send the process message to the main frame in the render process.
// Use PID_BROWSER instead when sending a message to the browser process.
browser->GetMainFrame()->SendProcessMessage(PID_RENDERER, msg);
一个从Browser进程发送到渲染进程的消息,是通过CefRenderProcessHandler::OnProcessMessageReceived()来实现的;反过来,从渲染进行到浏览器进程是通过CefClient::OnProcessMessageReceived().函数来实现回调的。A message sent from the browser process to the render process will arrive in CefRenderProcessHandler::OnProcessMessageReceived(). A message sent from the render process to the browser process will arrive in CefClient::OnProcessMessageReceived().
4 通过websocket的方式
4.1 websocket原理介绍
通过CEF的方式,实现js和原生程序交互,是一个绝佳的方案。但同样具有缺点,即不能和通用浏览器(Chrome,Edge)中的页面(js),实现实时的交互。可以实时交互,都是基于页面在二次开发的程序中进行打开的前提下,才能成立。此时WebSocket就是另外一个绝佳的方案,可以用它来实现浏览器中的js和原生程序之间的通讯,且没有http跨域访问的问题。
实际上,websocket和socket是一样的,只是在accept后,需要去验证,当前链接的socket收到的请求信息(请求头),是否符合websocket协议。客户端发送来的链接信息头,如下格式所示,以下代码中的换行都是用\r\n隔开,整个头结束是\r\n\r\n来结束。所有的信息格式的验证都是通过自己写代码来实现的。
GET / HTTP/1.1
Host: localhost:10010
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.64
Upgrade: websocket
Origin: null
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Sec-WebSocket-Key: Nehn10clcDKqzQ69hrsxOw==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
返回信息头
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: fmZxN7gu1bI/ZfLrHITOcm33JJQ=
4.2 websoket示例代码
这里代码很多,就不单独用代码块来列举了。这里的代码是vs2012的工程。其中最重要的两个函数是ListeningSocketMethod,在此函数中,是监听socket不断的监听前来链接的socket;EstablishSocketMethod,在此函数中,是已经处理链接完成的socket信息函数;