Skip to content

浏览器插件与原生应用的通讯

  • 参考示例

https://github.com/mdn/webextensions-examples/tree/main/native-messaging

  • chrome 插件文档

https://developer.chrome.com/docs/extensions/reference/

关于nativeMessaging与注册表

这里主要以mac/win系统chrome为主

  • 原生应用的manifest
json
{
  "name": "com.my_company.my_application",
  "description": "My Application",
  "path": "C:\\Program Files\\My Application\\chrome_native_messaging_host.exe",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"
  ]
}
1
  • mac manifest的位置 2

  • win manifest的位置

在Windows上,清单文件可以位于文件系统中的任何位置。应用程序安装程序必须创建注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Google\Chrome\NativeMessagingHosts\_com.my_company.my_application_HKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\_com.my_company.my_application_,并将该注册表项的默认值设置为清单文件的完整路径。例如,使用以下命令:

sh
REG ADD "HKCU\Software\Google\Chrome\NativeMessagingHosts\com.my_company.my_application" /ve /t REG_SZ /d "C:\path\to\nmh-manifest.json" /f

或使用以下.reg文件:

sh
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts\com.my_company.my_application]
@="C:\\path\\to\\nmh-manifest.json"

当 Chrome 查找本机消息传递主机时,首先查询 32 位注册表,然后查询 64 位注册表。

自我尝试步骤

权限

插件manifest.json

json
{
  "name": "My extension",
  ...
  "browser_specific_settings": {
      "gecko": {
        "id": "ping_pong@example.org",
        "strict_min_version": "50.0",
      },
    },
  "permissions": [
    "nativeMessaging"
  ],
  ...
}

向原生发收消息

background.js

json
// 循环给native发送消息
const port = browser.runtime.connectNative('ping_pong')

/*
Listen for messages from the app.
*/
port.onMessage.addListener((response) => {
  console.log(`Received: ${response}`)
})
setInterval(() => {
  port.postMessage('ping')
}, 1000)

原生应用的manifest.json

json
{
  "name": "ping_pong",
  "description": "My Application",
  "path": "/Users/tanjie/Desktop/ping_pong.app/Contents/MacOS/native-message-chrome-ext",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://ebnbccpndlpnbgdohobmhkfgnpfhkenl/"
  ]
}

path要指向到具体的可自行文件

原生app的接收 Unity c#代码

csharp

using System;
using System.IO;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.UI;
public class ChromeCommunicator : MonoBehaviour
{
    private Thread workThread;
    private SynchronizationContext mainThreadSynContext;
    public Text text;
    private void Start()
    {
        
        mainThreadSynContext = SynchronizationContext.Current;
        workThread = new Thread(new ThreadStart(DoWork));
        workThread.Start();
    }
    private void DoWork()//这个是workThread线程执行的
    {
        //这儿做些事(连接什么的。。。)
        while (OpenStandardStreamIn() != null || OpenStandardStreamIn() != "")
        {
            mainThreadSynContext.Post(new SendOrPostCallback(OnConnected), OpenStandardStreamIn());//通知主线程
            // FIXME: 这里的输出似乎没效果
            // OpenStandardStreamOut("Received to Native App: " + OpenStandardStreamIn());
            // OpenStandardStreamOut("Recieved: " + OpenStandardStreamIn());
        }
    }
    public static void OpenStandardStreamOut(string stringData)
    {
        string outString = "\"" + stringData + "\"";
        int dataLength = outString.Length;
        byte[] bytes = BitConverter.GetBytes(dataLength);
        Stream stdout = Console.OpenStandardOutput();
        for (int i = 0; i < 4; i++) stdout.WriteByte(bytes[i]);
        Console.Write(outString);
    }
    static string OpenStandardStreamIn()
    {
        // FIXME: 指定输入输出格式不行 所以采用 DecString 方法
        // Console.OutputEncoding = System.Text.Encoding.UTF8;
        // Console.InputEncoding = System.Text.Encoding.UTF8;
        //// We need to read first 4 bytes for length information
        Stream stdin = Console.OpenStandardInput();
        int length = 0;
        byte[] bytes = new byte[4];
        stdin.Read(bytes, 0, 4);
        length = System.BitConverter.ToInt32(bytes, 0);
        string input = "";
        for (int i = 0; i < length; i++)
        {
            input += (char)stdin.ReadByte();
        }
        return DecString(input);
    }
    private void OnConnected(object state)//由于是主线程的同步对象Post调用,这个是在主线程中执行的
    {
        //这里就回到了主线程里面了
        var json = (string)state;
        Danmu dm = JsonConvert.DeserializeObject<Danmu>(json);
        if (dm.data.message!="")
        {
            text.text = dm.data.message;
        }
    }

    public static string DecString(string str)
    {
        Encoding utf8=Encoding.GetEncoding("iso-8859-1");
        // Encoding utf8=Encoding.UTF8;
        // Encoding utf8=Encoding.UTF32;
        byte[] btArr = utf8.GetBytes(str);
        return Encoding.UTF8.GetString(btArr);
    }
    private void OnDestroy()
    {
        if (workThread != null)
        {
            workThread.Abort();
        }
    }

    private void OnApplicationPause(bool pauseStatus)
    {
        workThread?.Abort();
    }
}
[Serializable]
public class Danmu
{
    public string roomId;
    public Data data;
}
[Serializable]
public class Data
{
    public int type;
    public string typeText;
    public string message;
    public string face;
    public string uname;
    public string uid;
    public string fans;
    public string platform;
}

将manifest移动到~/Library/Application Support/Google/Chrome/NativeMessagingHosts/ping-pong.json

启动浏览器插件,unity 页面展示了浏览器推送过来的弹幕

1

将应用程序打包成安装包dmg

参考:https://zhuanlan.zhihu.com/p/56864296

本想打包成安装程序pkg,安装的过程中直接写入注册表,还没发现比较好用的软件 所以目前的简单处理方式是第一次打开程序的用程序写入注册表

nodejs

给权限 chmod +x ./ping_pong.js

js
#!/usr/local/bin/node
(() => {
    // console.log('Hello from Node.js');
    let payloadSize = null;

    // A queue to store the chunks as we read them from stdin.
    // This queue can be flushed when `payloadSize` data has been read
    let chunks = [];

    // Only read the size once for each payload
    const sizeHasBeenRead = () => Boolean(payloadSize);

    // All the data has been read, reset everything for the next message
    const flushChunksQueue = () => {
        payloadSize = null;
        chunks.splice(0);
    };

    const processData = () => {
        // Create one big buffer with all the chunks
        const stringData = Buffer.concat(chunks);

        // The browser will emit the size as a header of the payload,
        // if it hasn't been read yet, do it.
        // The next time we'll need to read the payload size is when all of the data
        // of the current payload has been read (i.e. data.length >= payloadSize + 4)
        if (!sizeHasBeenRead()) {
            payloadSize = stringData.readUInt32LE(0);
        }

        // If the data we have read so far is >= to the size advertised in the header,
        // it means we have all of the data sent.
        // We add 4 here because that's the size of the bytes that hold the payloadSize
        if (stringData.length >= (payloadSize + 4)) {
            // Remove the header
            const contentWithoutSize = stringData.slice(4, (payloadSize + 4));

            // Reset the read size and the queued chunks
            flushChunksQueue();

            const json = JSON.parse(contentWithoutSize);
            // console.log(json);

            // process.stdout.write(JSON.stringify({message:'pong'}))
            // Do something with the data…
            // const message = { message: 'Hello Chrome' };
            // const jsonMessage = JSON.stringify(message,null,0);
            // const utf8Message = jsonMessage.toString('utf8');
            // console.log(utf8Message);
            // process.stdout.write(utf8Message);
        }
    };

    process.stdin.on('readable', () => {
        // A temporary variable holding the nodejs.Buffer of each
        // chunk of data read off stdin
        let chunk = null;

        // Read all of the available data
        while ((chunk = process.stdin.read()) !== null) {
            chunks.push(chunk);
        }

        processData();

    });

    process.stdin.on('end', () => {
        // console.log('end');
        const message = { message: 'Hello Chrome' };
        const jsonMessage = JSON.stringify(message);
        const utf8Message = jsonMessage.toString('utf8');
        process.stdout.write(utf8Message);
    })

    process.stdin.on('error', (err) => {
        // console.log('error', err);
    })

})();

nodejs 使用第三方库

给权限 chmod +x ./ping_pong.js 确保程序文件可直接在命令行执行

js
#!/usr/local/bin/node

// Might be good to use an explicit path to node on the shebang line in case
// it isn't in PATH when launched by Chrome.

var fs = require('fs');

// var nativeMessage = require('../index');
var nativeMessage = require('chrome-native-messaging');

var input = new nativeMessage.Input();
var transform = new nativeMessage.Transform(messageHandler);
var output = new nativeMessage.Output();

process.stdin
    .pipe(input)
    .pipe(transform)
    .pipe(output)
    .pipe(process.stdout)
;

var subscriptions = {};

var timer = setInterval(function() {
    if (subscriptions.time) {
        output.write({ time: new Date().toISOString() });
    }
}, 1000);

input.on('end', function() {
    clearInterval(timer);
});

function messageHandler(msg, push, done) {
    if (msg.readdir) {
        fs.readdir(msg.readdir, function(err, files) {
            if (err) {
                push({ error: err.message || err });
            } else {
                files.forEach(function(file) {
                    push({ file: file });
                });
            }

            done();
        });
    } else if (msg.subscribe) {
        subscriptions[msg.subscribe] = true;
        push({ subscribed: msg.subscribe });
        done();
    } else if (msg.unsubscribe) {
        delete subscriptions[msg.unsubscribe];
        push({ unsubscribed: msg.unsubscribe });
        done();
    } else {
        // Just echo the message:
        push(msg);
        done();
    }
}