Chrome Native Messaging技术示例

来源:互联网 发布:联通软件研究院 亦庄 编辑:程序博客网 时间:2024/06/10 18:11
Title:Chrome Native Messaging技术示例
Author: kagula
Revison: 1
First publish Date:2015-10-23
Last modify date:2015-10-23


环境:
 [1]Visaul Stuio 2013 Update4
 [2]Google chrome 45.0.2454.101 
 [3]Windows7 SP1 64bits
 
正文
    在Chrome浏览器中,Native Messaging是唯一能让web page调用Native API的技术,这里记录要达到这个目的所要涉及到的知识。

    web page、extension和native messaging host三者之间用UTF-8编码的json字符串通讯。


    web page同extension之间进行message exchange,extension同native messaging host之间message exchange.
    最后由native messaging host access native OS API.
    本文主体由native messaging host、extension、web page三部份组成。
    这里基于google echo示例代码做了大量修改,用于实现web page同native application的通讯。
    
第一部份 native messaging host
    native messaging host从stdin读取消息从stdout中返回消息,通过stderr返回错误消息给chrome。
    一次最多能向native messaging host发送4GB字节的请求,native messaging host一次最多能返回1M字节的消息。
    这部份由C++源文件和manifest文件两部份组成:
    
在VisualStudio中新建chrome_native_messaging_host win32 console project,把下面的文件加进去并编译。
下面是native messaing host源文件清单   
    
#include "stdafx.h"/*Title: native messaging c++ programAuthor: kagulaDate: 2015-10-22*/#include <stdio.h>#include <fcntl.h>#include <io.h>   #include "SimpleLog.h"#include <iostream>#include <string>#include <sstream>using namespace std;void sendMessage(const string &strMsg){// We need to send the 4 bytes of length informationunsigned int len = strMsg.length();std::cout << char(((len >> 0) & 0xFF))<< char(((len >> 8) & 0xFF))<< char(((len >> 16) & 0xFF))<< char(((len >> 24) & 0xFF));//output integer value directly will lead byte order problem.// Now we can output our messagecout << strMsg;cout.flush();}simpleLogClass logger;int _tmain(int argc, _TCHAR* argv[]){logger.fileMaxSize = 64*1024;logger.fileName = "e:\\a.log";logger.fileOldName = "e:\\a.old.log";_setmode(_fileno(stdin), O_BINARY);_setmode(_fileno(stdout), O_BINARY);_setmode(_fileno(stderr), O_BINARY);int bufSize;do {bufSize = 0;//unlike >> operator and scanf function, //read function will wait until read all 4 bytes!cin.read((char*)&bufSize, 4);stringstream ss;ss << "bufSize = " << bufSize << endl;logger.info(ss.str());if (bufSize> 0){char *pData = new char[bufSize+1];memset(pData, 0, bufSize + 1);cin.read(pData, bufSize);string response = "{\"echo\":";response.append(pData);response.append("}");sendMessage(response);logger.info(response);delete pData;} else{break; }} while (true);return 0;}


下面是com.google.chrome.example.echo-win.json源文清单

{  "name": "com.google.chrome.example.echo",  "description": "Chrome Native Messaging API Example Host",  "path": "chrome_native_messaging_host.exe",  "type": "stdio",  "allowed_origins": [    "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"  ]}


    manifest文件和C++源码编译后的EXE文件放在同一个目录,chrome extension通过注册表“HKCU\Software\Google\Chrome\NativeMessagingHosts”节点查找manifest文件
manifest文件告诉Extension要启动哪个应用程序,允许哪些extension访问当前native messaging host。
    通过下面的命令向Windows注册native messaging host程序
    假设两个文件均存放在“E:\workspace\chrome_native_messaging_host\Debug”路径下
REG ADD "HKCU\Software\Google\Chrome\NativeMessagingHosts\com.google.chrome.example.echo" /ve /t REG_SZ /d "E:\workspace\chrome_native_messaging_host\Debug\com.google.chrome.example.echo-win.json" /f
    HKCU指的是HKEY_CURRENT_USER,你也可以使用HKLM即HKEY_LOCAL_MACHINE代替。    
    通过下面的命令卸载native messaging host程序
REG DELETE "HKCU\Software\Google\Chrome\NativeMessagingHosts\com.google.chrome.example.echo" /f

   

注意host name中不能出现大写字母。

第二部份 chrome extension(这是一个browser类型的谷歌扩展)
    用于通讯中继的extension比较复杂,由七个文件组成。
    background.js content.js icon-128.png icon-16.png main.html main.js manifest.json
    [a]main.html和main.js文件用来测试extension是否能正常工作。
      可以把extension的内部看成由三个sandbox组成,这是第一个sandbox。
main.html源码清单      
<!DOCTYPE html><!-- * Copyright 2013 The Chromium Authors. All rights reserved.  Use of this * source code is governed by a BSD-style license that can be found in the * LICENSE file.--><html>  <head>    <script src='./main.js'></script>  </head>  <body>    <button id='connect-button'>Connect</button>    <input id='input-text' type='text' />    <button id='send-message-button'>Send</button>    <div id='response'></div>  </body></html>


main.js源码清单
// Copyright 2013 The Chromium Authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.var port = null;var getKeys = function(obj){   var keys = [];   for(var key in obj){      keys.push(key);   }   return keys;}function appendMessage(text) {  document.getElementById('response').innerHTML += "<p>" + text + "</p>";}function updateUiState() {  if (port) {    document.getElementById('connect-button').style.display = 'none';    document.getElementById('input-text').style.display = 'block';    document.getElementById('send-message-button').style.display = 'block';  } else {    document.getElementById('connect-button').style.display = 'block';    document.getElementById('input-text').style.display = 'none';    document.getElementById('send-message-button').style.display = 'none';  }}function sendNativeMessage() {  message = {"text": document.getElementById('input-text').value};  port.postMessage(message);  appendMessage("Sent message: <b>" + JSON.stringify(message) + "</b>");}function onNativeMessage(message) {  appendMessage("Received message: <b>" + JSON.stringify(message) + "</b>");}function onDisconnected() {  appendMessage("Failed to connect: " + chrome.runtime.lastError.message);  port = null;  updateUiState();}function connect() {  var hostName = "com.google.chrome.example.echo";  appendMessage("Connecting to native messaging host <b>" + hostName + "</b>")  port = chrome.runtime.connectNative(hostName);  port.onMessage.addListener(onNativeMessage);  port.onDisconnect.addListener(onDisconnected);  updateUiState();}document.addEventListener('DOMContentLoaded', function () {  document.getElementById('connect-button').addEventListener(      'click', connect);  document.getElementById('send-message-button').addEventListener(      'click', sendNativeMessage);  updateUiState();});


    [b]background.js 第二个sandbox。
       [b-1]负责用户点击chrome browser上的extension按钮后,启动extension测试页。
       [b-2]负责建立native messaging host之间的通讯。
       [b-3]负责接收来自web page的message并转发给native messaging host。
background.js源码清单      
 
function focusOrCreateTab(url) {  chrome.windows.getAll({"populate":true}, function(windows) {    var existing_tab = null;    for (var i in windows) {      var tabs = windows[i].tabs;      for (var j in tabs) {        var tab = tabs[j];        if (tab.url == url) {          existing_tab = tab;          break;        }      }    }    if (existing_tab) {      chrome.tabs.update(existing_tab.id, {"selected":true});    } else {      chrome.tabs.create({"url":url, "selected":true});    }  });}chrome.browserAction.onClicked.addListener(function(tab) {  var manager_url = chrome.extension.getURL("main.html");  focusOrCreateTab(manager_url);});document.addEventListener("DOMContentLoaded", function () {});var port = null;var hostName = "com.google.chrome.example.echo";function connect() {  port = chrome.runtime.connectNative(hostName);  port.onMessage.addListener(onNativeMessage);  port.onDisconnect.addListener(onDisconnected);}function onNativeMessage(message) {console.log("onNativeMessage=>"+JSON.stringify(message));chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {  chrome.tabs.sendMessage(tabs[0].id, {data: JSON.stringify(message)}, function(response) {    console.log(JSON.stringify(message));  });});}function onDisconnected() {  port = null;}chrome.runtime.onMessageExternal.addListener(  function(request, sender, sendResponse) {    console.log("chrome.runtime.onMessageExternal.addListener in background.js");    if (request.data)            var data = request.data;      if(data=="connect")      {      connect();      }      else      {      if(port==null)      {      console.log("disconnect with"+hostName);      return;      }            console.log("Hi, there is message ["+data+"]from the website");      var message = {"text": request.data};      port.postMessage(message);      }  });


    [c]contenet.js 第三个sandbox。
       background.js在收到native messaging host的反馈后,会把反馈信息转给content.js,由content.js负责转发给web page。
content.js源码清单       
function appendMessage(text) {document.getElementById('response_div_id').innerHTML += "<p>" + text + "</p>";}      chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {if (request.data != null){console.log("get response from extension" + request.data);appendMessage("get response from extension" + request.data);console.log("create custom event");var evt = new CustomEvent('Event');evt.initCustomEvent('customEvent', true, true, {'data': request.data});console.log("fire custom event");document.getElementById('response_div_id').dispatchEvent(evt)}});



    [d]manifest.json文件,用来保存Extension的信息
       比如告诉chrome browser, 我们的extension程序由哪些文件组成,需要哪些权限,在browser上的按钮图片是哪一个,
       content scripts是哪一个js文件,background scripts是哪一个js文件等,
       允许使用extension的web page它的域是什么,等等一大堆。

manifest.json源码清单

{  "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcBHwzDvyBQ6bDppkIs9MP4ksKqCMyXQ/A52JivHZKh4YO/9vJsT3oaYhSpDCE9RPocOEQvwsHsFReW2nUEc6OLLyoCFFxIb7KkLGsmfakkut/fFdNJYh0xOTbSN8YvLWcqph09XAY2Y/f0AL7vfO1cuCqtkMt8hFrBGWxDdf9CQIDAQAB",  "name": "Native Messaging Example",  "version": "1.0",  "manifest_version": 2,  "description": "Send a message to a native application.",  "browser_action": {    "default_icon": "icon-16.png"  },  "icons": {    "16": "icon-16.png",    "48": "icon-128.png",    "128": "icon-128.png"  },  "permissions": [    "nativeMessaging", "tabs", "http://*/*", "https://*/*"  ],  "content_scripts": [    {      "matches": ["http://*/*"],      "js": ["content.js"]    }  ],  "externally_connectable": {"matches": ["*://*.example.com/*"]},  "background" : { "scripts": ["background.js"] }}


    [e]icon-128.png,icon-16.png两个背景透明的图片文件
       我们的extension在chrome上的按钮图片,外观。


    在chrome中输入chrome://extensions打开“开发者模式”,点击“加载正在开发的扩展程序...”按钮,拾取extension根目录,就可以成功装载我们的extension。
你也可以把正在开发的extension的根目录直接拖到chrome://extensions页面。


extension的相关知识
 [1]extension装载好后,chrome就会调用background.js。background.js可以侦听browser上extension按钮的单击事件。
    background.js不会加载到main.html中,也不会加载到普通页面中。
 [2]chrome中的tab每完成一个页面的加载,就会调用extension里的content.js一次。
   chrome规范中的content scripts不一定要叫content.js这个名字,而且这些js可以有多个。
   同理background我们为了容易记忆所以起名叫background.js,但是可以是任何名字而且可以是多个js,而且还可以是html文件。
 [3]打包extension
   在chrome中输入chrome://extensions打开“开发者模式”,点击“打包扩展程序...”按钮,选择extension所在的目录打包。
   在extension的上级目录生成crx和pem两个文件,其中pem文件里存放了私匙,更新extension需要私匙。
 [4]更新extension
  [Step1]在manifest.json中修改版本号。
  [Step2]在Chrome中输入“chrome://extensions”,点击“打包扩展程序...”按钮,弹出对话框后选择extension的根目录。
  [FinalStep]对话框中拾取上次打extension包生成的pem文件,点击确定即可。更多关于extension packaging的信息参考资料[4]。
 [5]扩展为crx后缀的文件,通过两种方式分发:
  第一种是通过chrome web store分发。
  第二种是直接让用户下载crx文件,让用户自己手动去安装。
 [5]extension使用chrome打包后extension id可能会变,所以在其它计算机上使用我们的echo这个例子
  得修改“com.google.chrome.example.echo-win.json”和“test.html”文件的extension id。
  extension id不正确会导致extension无权访问native messaging host,webpage不能正确访问extension。
 
  
第三部份 web page
  web page是普通的页面,被上面的manifest文件要求只能放在www.example.com上才能有权访问Extension。
  可以通过修改extension中的manifest.json文件授权哪些域名,比如说把example.com改为kagula.com,可以访问我们的extension。  
  所以我们需要修改C:\Windows\System32\drivers\etc\hosts文件,把www.example.com域名定位到当前计算机中,
  在当前计算机中启动一个web server,把web page的文件放到html root路径下。
  为了示例清晰,我们的web page只有一个test.html文件。
下面是test.html文件清单。
<html><head><Script type="text/javascript">window.onload=function(){//after html loadedvar obj = document.getElementById('response_div_id');obj.addEventListener('customEvent', function(e){ console.log('customEvent 事件触发了=>'+e.detail.data);}, false);}  // The ID of the extension we want to talk to.var kagulaExtensionId = "pldlmdbmiepkhemhgofdefdomcmamcfk";function requestConnect(){console.log("connect");chrome.runtime.sendMessage(kagulaExtensionId, {data: "connect"});}function requestEcho(){console.log("Make a simple request");var reqData =   document.getElementById('input-text').value;chrome.runtime.sendMessage(kagulaExtensionId, {data: reqData},  function(response) {  alert(response);});}</Script></head><body>  <button id='connect-button' OnClick="javascript:requestConnect()">连接</button>    <input id='input-text' type='text' value='fromKagula'/>  <button id='send-message-button' OnClick="javascript:requestEcho()">发送消息</button>    <div id="response_div_id"></div></body></html>



  确认native messaging host和extension正确安装后,启动chrome输入“http://www.example.com:8080/test.html”打开测试页。
测试方式,先点击“连接”连上native messaging host程序,然后再发送消息就可以了。


备注
[1]为chrome程序添加“--enable-logging”命令行参数,重新启动chrome,chrome就会输出错误日志。


    
参考资料
[1]《Native Messaging》
https://developer.chrome.com/extensions/nativeMessaging#examples    
[2]《Alternative Extension Distribution Options》
https://developer.chrome.com/extensions/external_extensions
[3]Chrome 插件: 起动本地应用 (Native messaging) 
http://blog.csdn.net/talking12391239/article/details/38498557
[4]extension Packaging
https://developer.chrome.com/extensions/packaging
[5]extensions的示例代码
https://developer.chrome.com/extensions/samples#search:
[6]html和js的学习资源
http://www.w3school.com.cn/tags/tag_input.asp
[7]分享Chrome Extension(扩展程序/插件)开发的一些小经验 
http://www.cnblogs.com/dudu/archive/2012/11/22/chrome_extension.html
[8]sending message to chrome extension from a web page
http://stackoverflow.com/questions/11431337/sending-message-to-chrome-extension-from-a-web-page
[9]Chrome Extension how to send data from content script to popup.html
http://stackoverflow.com/questions/20019958/chrome-extension-how-to-send-data-from-content-script-to-popup-html
[10]如何开发extension的官方文档
https://developer.chrome.com/extensions
[11]JavaScript 自定义事件
http://kayosite.com/javascript-custom-event.html
[12]《如何在Chrome浏览器安装第三方扩展》
http://www.williamlong.info/archives/3160.html
3 0