ubuntu 16.04系统LimeSDR使用srsLTE搭建LTE实验环境

操作系统调整
参考ubuntu 16.04低延迟内核安装低延时内核

CPU调整为最大性能模式,并且不允许降低频率

$ sudo apt-get install cpufrequtils
 
$ sudo touch /etc/default/cpufrequtils
 
$ sudo sed -i "/GOVERNOR.*/d" /etc/default/cpufrequtils
 
$ test -s /etc/default/cpufrequtils && sudo sed -i '$a\GOVERNOR=\"performance\"' /etc/default/cpufrequtils || echo "GOVERNOR=\"performance\"" | sudo tee /etc/default/cpufrequtils
 
$ sudo update-rc.d ondemand disable
 
$ sudo reboot

安装依赖包

$ sudo apt-get install cmake g++ libpython-dev python-numpy swig git libsqlite3-dev libi2c-dev libusb-1.0-0-dev libwxgtk3.0-dev freeglut3-dev

编译安装SoapySDR

$ cd ~ 

$ git clone https://github.com/pothosware/SoapySDR.git

$ cd SoapySDR

$ git pull origin master

$ mkdir build && cd build

$ cmake ..

$ make -j4

$ sudo make install

$ sudo ldconfig

编译安装LimeSDR

$ cd ~

$ git clone https://github.com/myriadrf/LimeSuite.git

$ cd LimeSuite

# 再次更新一下,确保代码到最新
$ git pull

# 不可删除build目录,清理build目录后要还原被误删除的文件,
# 原因在于build目录下存在mcu程序,默认应用启动后从这个目录提取mcu程序刷新到设备

$ mkdir build ; cd build

# cmake -DCMAKE_BUILD_TYPE=Debug ..
$ cmake ..

$ make -j4

$ sudo make install

$ sudo ldconfig

$ cd ../udev-rules/

$ sudo ./install.sh

# Download board firmware
$ sudo LimeUtil --update

编译安装srsGUI

$ cd ~

# srsGUI提供了一个软件示波器的功能,我们可以比较直观的观察到波形信息

$ git clone https://github.com/srsLTE/srsGUI.git

$ cd srsGUI

# 再次更新一下,确保代码到最新
$ git pull

$ mkdir build

$ cd build

$ cmake ..

$ make

$ sudo make install

$ sudo ldconfig

编译安装srsLTE

$ sudo apt-get install git cmake libfftw3-dev libmbedtls-dev libboost-program-options-dev libboost-thread-dev libconfig++-dev libsctp-dev
$ cd ~

$ git clone https://github.com/srsLTE/srsLTE.git

$ cd srsLTE

# 再次更新一下,确保代码到最新
$ git pull

# 目前(2018.05.04)以及之前的代码中srsenb/src/upper/rrc.cc文件中
# uint32_t rrc::generate_sibs() 函数中存在一个数组越界问题,
# 已经提交patch ,具体修改
# 参看https://github.com/srsLTE/srsLTE/pull/173

# 调用LimeSDR设备的代码在 lib/src/phy/rf/rf_soapy_imp.c 文件中
# 这个文件中默认的接收天线是"LNAH",默认的发送天线是"BAND1",需要根据实际情况
# 也就是自己天线接入的接口是哪个,就修改成哪个。
# 调整这里的参数,目前是直接修改代码。比如我这边的发送天线就修改为"BAND2"

$ mkdir build

$ cd build

# cmake -DCMAKE_BUILD_TYPE=Debug ..
$ cmake ../

$ make

$ make test

测试

$ cd ~/srsLTE

$ cd build/lib/examples

#以20M带宽的形式进行基站的扫描,LTE基站带宽20M,目前版本测试不支持,无法扫描到基站
# ./cell_search -b 20

#目前测试仅支持此命令的运行
$ ./pdsch_enodeb

搭建LTE测试环境

$ cd ~/srsLTE

$ mkdir lteCell

$ cd lteCell


# 生成配置文件
$ mkdir lteENB

$ cp ../srsenb/enb.conf.example lteENB/enb.conf

$ cp ../srsenb/rr.conf.example lteENB/rr.conf

$ cp ../srsenb/sib.conf.example lteENB/sib.conf

$ cp ../srsenb/drb.conf.example lteENB/drb.conf

$ mkdir lteEPC

$ cp ../srsepc/epc.conf.example lteEPC/epc.conf

$ cp ../srsepc/user_db.csv.example lteEPC/user_db.csv


# 生成运行脚本
$ echo "echo -ne \"\033]0;ENB\007\"" >> run_enb.sh

$ echo "cd lteENB" >> run_enb.sh


# 需要gdb的话使用如下
#echo "#gdb -args ../../build/srsenb/src/srsenb enb.conf" >> run_enb.sh

$ echo "../../build/srsenb/src/srsenb enb.conf" >> run_enb.sh

$ echo "echo -ne \"\033]0;EPC\007\"" >> run_epc.sh

$ echo "cd lteEPC" >> run_epc.sh

# 需要配置如下转发规则,否则不能正常工作,配置信息参考openair-cn的SPGW中的代码
# sudo sysctl -w net.ipv4.ip_forward=1
# sudo sync
# sudo iptables -t mangle -F FORWARD
# sudo iptables -t nat -F POSTROUTING
# export LANG=C
# 如果没有修改过配置文件,则默认使用如下配置即可,
# 如果修改过sgi_if_addr的地址(默认sgi_if_addr=172.16.0.1),需要更改这个字段
# export FORDING_IPs=172.16.0.0/12
# 有线网卡一般以"en"开头,比如"enp3s0",此处需要根据自身机器上的网卡进行设置,修改
# export NIC_NAME=`ls /sys/class/net | grep en`
# export NIC_IP=`ifconfig $NIC_NAME | grep 'inet addr:' | awk '{print $2}' | cut -c 6-`
# sudo iptables -t nat -I POSTROUTING -s $FORDING_IPs -o  $NIC_NAME ! --protocol sctp -j SNAT --to-source $NIC_IP

$ echo "sysctl -w net.ipv4.ip_forward=1" >> run_epc.sh

$ echo "sync" >> run_epc.sh

$ echo "iptables -t mangle -F FORWARD" >> run_epc.sh

$ echo "iptables -t nat -F POSTROUTING" >> run_epc.sh

$ echo "export LANG=C" >> run_epc.sh

$ echo 'export FORDING_IPs=172.16.0.0/12' >> run_epc.sh

$ echo 'export NIC_NAME=`ls /sys/class/net | grep en`' >> run_epc.sh

$ echo "export NIC_IP=\\`ifconfig \$NIC_NAME | grep 'inet addr:' | awk '{print \$2}' | cut -c 6-\\`" >> run_epc.sh

$ echo 'iptables -t nat -I POSTROUTING -s $FORDING_IPs -o  $NIC_NAME ! --protocol sctp -j SNAT --to-source $NIC_IP' >> run_epc.sh

$ echo "../../build/srsepc/src/srsepc epc.conf" >> run_epc.sh

$ echo "gnome-terminal -e \"bash run_epc.sh\"" >> run.sh

$ echo "sleep 2" >> run.sh

$ echo "gnome-terminal -e \"bash run_enb.sh\"" >> run.sh


# 运行测试
$ sudo bash run.sh

#日志查看 /tmp/enb.log, /tmp/epc.log

如果设备(比如手机)的设置是按照ubuntu 16.04系统LimeSDR V1.4使用OpenAirInterface搭建LTE实验环境里面的设置的,设备参数如下图:
注意上图与OpenAirInterface中设置的不同之处,差别就是一个选中OPC,一个选中OP

则需要对配置文件进行如下调整:

$ cd ~/srsLTE/lteCell

# 修改MNC,MCC
$ sed -i -r "s/^mnc[ \t]*=[ \t0-9]*/mnc = 92/g" lteENB/enb.conf
$ sed -i -r "s/^mcc[ \t]*=[ \t0-9]*/mcc = 208/g" lteENB/enb.conf
$ sed -i -r "s/^tac[ \t]*=[ \t]*0x[0-9]*/tac = 0x0001/g" lteENB/enb.conf

$ sed -i -r "s/^mnc[ \t]*=[ \t0-9]*/mnc = 92/g" lteEPC/epc.conf 
$ sed -i -r "s/^mcc[ \t]*=[ \t0-9]*/mcc = 208/g" lteEPC/epc.conf
$ sed -i -r "s/^tac[ \t]*=[ \t]*0x[0-9]*/tac = 0x0001/g" lteEPC/epc.conf

# 修改认证加密算法
$ sed -i -r "s/^auth_algo[ \t]*=[ \ta-Z]*/auth_algo = milenage/g" lteEPC/epc.conf


# 需要软件示波器界面的话,修改 lteEPC/epc.conf 里面的 [gui]部分的enable = true
# 但是打开示波器会导致CPU开销加大,降低实时性,稳定性变低

# 认证数据库中增加Name,IMSI,Key,OP,AMF,SQN 这里主要的就是IMSI,Key,OP这几个参数
# OPc=AES128(Ki,OP) XOR OP 因此需要反算 OP 
# openair-cn需要OPc,而 srsLTE 的epc需要 OP 这是两者的主要区别
# OPc = 504f20634f6320504f50206363500a4f 
# Ki = 6874736969202073796d4b2079650a73
# OP = 11111111111111111111111111111111
# 最后的 SQN 目前测试发现如果一直不能正常注册,并且提示
# "Sequence number synch failure" 则会观察到,当EPC退出时候,应用一定回写 
# 成000000001b02 ,一旦出现这个数字,我们就没办法注册设备了。应该是个BUG
# http://www.mobibrw.com/?p=12688 中我们持续跟进这个问题,目前猜测是要比设备最后
# 一次通信记录的数据号大就可以了,如果要复位可以让设备关机,应该就从0开始了。
 
$ sed -i '$a\ue3,208920100001100,6874736969202073796d4b2079650a73,11111111111111111111111111111111,8000,000000001b03' lteEPC/user_db.csv

注意目前srsLTE的CPU开销远远高于OpenAirInterface,差不多一个内核满负载,以及稳定性是低于OpenAirInterface的,经常出现连接困难以及中途掉线,这部分需要后续的持续修改。目前测试发现,在"lteEPC/epc.conf"中关闭GUI的显示,可以显著减低CPU开销,并增加稳定性,但是还是比OpenAirInterface要多消耗CPU。另外在"lteEPC/epc.conf"中日志设置成"all_level = none"也可以降低CPU开销,并且增加稳定性。

另外,注意"/tmp/enb.log",这个日志文件默认情况下,写入的比较多,文件大小增长很快,注意磁盘占用情况。

目前测试发现,使用Intel MKL加速的情况下,CPU降低并不明显,而稳定性下降非常多应该是代码的适配问题。因此,暂时不要使用Intel MKL。

目前测试发现,使用最新的FFTW3版本,使用AVX,AVX2加速的情况下,CPU开销更高,性能更差,应该是代码存在BUG。因此,暂时不要使用自己编译的FFTW3,使用系统自带的版本即可。

手机等设备的设置参考ubuntu 16.04系统LimeSDR V1.4使用OpenAirInterface搭建LTE实验环境最后的介绍。

上述的代码如果下载困难,可以从本站下载一份拷贝。

SoapySDR源代码下载
LimeSuite源代码下载
srsLTE源代码下载
srsGUI源代码下载
fftw3源代码下载

参考链接


ubuntu 16.04低延迟内核

在某些特殊环境中,比如音视频实时处理(比如MIDI),无线电数据的编解码(比如 OpenAirInterface 明确要求使用低延时内核)等情况下,我们希望系统尽可能的实时,同时又不会降低太多的性能(实时性越高,性能,功耗等的损失越大)。这种情况下,我们可以尝试使用低延时内核。

Linux提供了五种内核类型,对于内核类型的选择,可以参考如下的解释:

*******************************************

  • If you do not require low latency for your system then please use the -generic kernel.
  • If you need a low latency system (e.g. for recording audio) then please use the -preempt kernel as a fist choice. This reduces latency but doesn't sacrifice power saving features. It is available only for 64 bit systems (also called amd64).
  • If the -preempt kernel does not provide enough low latency for your needs (or you have an 32 bit system) then you should try the -lowlatency kernel.
  • If the -lowlatency kernel isn't enough then you should try the -rt kernel
  • If the -rt kernel isn't enough stable for you then you should try the -realtime kernel

*******************************************

目前(2018.02)为止,ubuntu官方库中提供前四种内核,我们一般建议使用lowlatency版本。可以使用如下命令安装:

$ sudo apt-get install linux-lowlatency

$ sudo apt-get install linux-image-`uname -r | cut -d- -f1-2`-lowlatency

$ sudo apt-get install linux-headers-`uname -r | cut -d- -f1-2`-lowlatency

# 重启加载新内核
$ sudo reboot

目前,如果机器上使用了nvidia显卡,并且使用nvidia的闭源驱动的情况下,重启机器会无法进入图形界面,查看系统日志,出现如下错误信息:

$ cat /var/log/syslog | grep dkms
Feb 28 16:23:01 xx-System-Product-Name gpu-manager[831]: Error: can't open /lib/modules/4.13.0-36-lowlatency/updates/dkms

解决方法如下:

#目前最新版本驱动是384版本,如果使用的其他版本,请配置使用的版本

$ sudo dpkg-reconfigure nvidia-384

注意,使用sudo apt-get dist-upgrade升级内核的时候,内核会被替换成generic版本,低延时内核需要手工重新安装一遍。

参考链接


ubuntu 16.04修复固件刷新失败的LimeSDR-USB V1.4

最近在使用LimeSDR捣鼓软件定义无线电,结果在刷新固件的过程中,莫名失败,导致无法识别硬件。

使用LimeUtil检查硬件,出现如下结果:

$ sudo ./LimeUtil --find
  * [WestBridge , media=USB 2.0, module=STREAM, addr=04b4:00f3, serial=0000000004BE]

之后就再也无法通过

$ sudo ./LimeUtil --update

更新固件了,一直失败。

继续阅读ubuntu 16.04修复固件刷新失败的LimeSDR-USB V1.4

泰勒公式

泰勒公式是将一个在x=x0处具有n阶导数的函数f(x)利用关于(x-x0)n次多项式来逼近函数的方法。

若函数f(x)在包含x0的某个闭区间[a,b]上具有n阶导数,且在开区间(a,b)上具有(n+1)阶导数,则对闭区间[a,b]上任意一点x,成立下式:

其中,表示f(x)n阶导数,等号后的多项式称为函数f(x)x0处的泰勒展开式,剩余的Rn(x)是泰勒公式的余项,是(x-x0)n的高阶无穷小。

这里需要注意的是,我们规定0的阶乘 " 0!=1 "

参考链接


Chrome扩展:内容脚本(ContentScript)修改控件时未触发事件

在编写Chrome扩展的时候,我们需要修改一个input控件的内容,并且在内容修改完成后,需要触发控件的onFocus,onBlur事件,在这两个事件中,网页有相关的处理逻辑。

使用如下代码修改input控件的内容的时候,没有正确的触发焦点变化事件:

var inputElement = document.getElementsByTagName("inputTagName");
inputElement.focus();
inputElement.value="123456";
inputElement.blur();

经过Google发现,原来不能直接调用控件的事件函数,直接调用控件事件函数会导致事件流程不正确,尤其是网页使用Kissy,JQuery框架的时候,可能导致框架内部流程异常。

应当通过发送事件的方式,驱动整个框架的运行,代码如下:

var inputElement = document.getElementsByTagName("inputTagName");
var focusEvent = document.createEvent("HTMLEvents");
focusEvent.initEvent("focus", true, true);
inputElement.dispatchEvent(focusEvent);
inputElement.value="123456";
var blurEvent = document.createEvent("HTMLEvents");
blurEvent.initEvent("blur", true, true);
inputElement.dispatchEvent(blurEvent);

同样的道理适用于change事件触发的情况:

var inputElement = document.getElementsByTagName("inputTagName");
var changeEvent = document.createEvent("HTMLEvents");
changeEvent.initEvent("change", true, true);
inputElement.dispatchEvent(changeEvent);

参考链接


Fire “onchange” event on page from google chrome extension

ubuntu 16.04上调整使用Innodb存储引擎的MySQL性能

最近WordPress上使用的WP Statistics打开的时候,整个网站都几乎处于卡顿的状态,无法正常访问。

使用top命令发现主要是mysqldCPU占用很高。于是使用mytop分析当前执行的查询语句,发现主要是在查询wp_statistics_visitor表导致的问题(我设置了不要删除浏览记录,因此会导致表内数据偏多)。

大致估计是由于分配给MySQL的内存不足导致频繁的磁盘交换引起的性能问题。

解决方法比较简单,就是增大MySQL可以使用的内存即可。

首先确认服务器上的MySQL使用的存储引擎是否为Innodb(缺省情况下已经是Innodb了):

$ mysql -p -e "show variables like '%storage_engine%';"

如果确定是Innodb,则查询Innodb被限制使用的内存大小,如下:

$ mysql -p -e "SELECT @@innodb_buffer_pool_size;"

默认情况下是128MB,鉴于内存已经不够了,我们扩大到256MB,这部分内存原则上越大越好,越大与磁盘的交互越少,性能越高。

如下命令调整Innodb的内存:

$ sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

[mysqld]部分增加(如果存在则修改):

innodb_buffer_pool_size = 256M

然后重启MySQL服务:

$ sudo service mysql restart

上面的调整之后,性能提升还是比较明显的。

参考链接


使用chrome.runtime.sendMessage向内容脚本(ContentScript)发送消息无效

参考Chrome插件(Extensions)开发攻略调试Chrome扩展的时候,发现从内容脚本(ContentScript)向后台脚本(BackgroundScript)使用chrome.runtime.sendMessage发送消息的时候,后台脚本(BackgroundScript)可以接收到来自内容脚本(ContentScript)的消息。

但是从后台脚本(BackgroundScript)向内容脚本(ContentScript)发送消息的时候,内容脚本(ContentScript)无法接收到消息。

原来的发送命令函数如下:

function sendMessageToContentUrl(){
	var msg = {
		type: "req"
	};
	// to send back your response  to the current tab
	chrome.runtime.sendMessage(msg, function (response) { });
}

根据Google了解到,需要指定tab发送,主要原因是当多个页面同时加载了内容脚本(ContentScript)的情况下,直接发送消息,会导致无法区分到底是发送给哪个页面的。
正确的发送消息函数如下:

function sendMessageToContentUrl(){
	var msg = {
		type: "req"
	};
	// to send back your response  to the current tab
	chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
		 chrome.tabs.sendMessage(tabs[0].id, msg, function(response) { });
		 });
}

参考链接


chrome.runtime.sendMessage not working in Chrome Extension

Javascript:如何循环遍历页面上的所有DOM元素?

传递一个*getElementsByTagName(),以便它将返回页面中的所有元素:

var all = document.getElementsByTagName("*");

for (var i=0, max=all.length; i < max; i++) {
	// Do something with the element here
	var node = all[i];
	/*以获取所有label标签的内容为例*/
	if(node.tagName && (node.tagName.toLowerCase() == "label")){
		console.log("innerText:" + node.innerText);
	}
}

参考链接


Chrome扩展:消息传递

由于内容脚本在网页而不是扩展程序的环境中运行,它们通常需要某种方式与扩展程序的其余部分通信。例如,RSS 阅读器扩展程序可能使用内容脚本检测页面上是否存在 RSS 供稿,然后通知后台页面,为当前页面显示页面按钮图标。

扩展程序和内容脚本间的通信使用消息传递的方式。两边均可以监听另一边发来的消息,并通过同样的通道回应。消息可以包含任何有效的 JSON 对象(null、boolean、number、string、array 或 object)。对于一次性的请求有一个简单的 API,同时也有更复杂的 API,允许您通过长时间的连接与共享的上下文交换多个消息。另外您也可以向另一个扩展程序发送消息,只要您知道它的标识符,这将在跨扩展程序消息传递部分介绍。

如果您只需要向您的扩展程序的另一部分发送一个简单消息(以及可选地获得回应),您应该使用比较简单的 runtime.sendMessage 方法。这些方法允许您从内容脚本向扩展程序发送可通过 JSON 序列化的消息,可选的 callback 参数允许您在需要的时候从另一边处理回应。

如下列代码所示从内容脚本中发送请求:

chrome.runtime.sendMessage({greeting: "您好"}, function(response) {
  console.log(response.farewell);
});

从扩展程序向内容脚本发送请求与上面类似,唯一的区别是您需要指定发送至哪一个标签页。这一例子演示如何向选定标签页中的内容脚本发送消息。

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "您好"}, function(response) {
    console.log(response.farewell);
  });
});

在接收端,您需要设置一个 runtime.onMessage 事件监听器来处理消息。

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "来自内容脚本:" + sender.tab.url :
                "来自扩展程序");
    if (request.greeting == "您好")
      sendResponse({farewell: "再见"});
  });

注意: 如果多个页面都监听 onMessage 事件,对于某一次事件只有第一次调用 sendResponse() 能成功发出回应,所有其他回应将被忽略。

有时候需要长时间的对话,而不是一次请求和回应。在这种情况下,您可以使用runtime.connecttabs.connect 从您的内容脚本建立到扩展程序的长时间连接。建立的通道可以有一个可选的名称,让您区分不同类型的连接。

使用长时间连接的一种可能的情形为自动填充表单的扩展程序。对于一次登录操作,内容脚本可以连接到扩展程序页面,每次页面上的输入元素需要填写表单数据时向扩展程序发送消息。共享的连接允许扩展程序保留来自内容脚本的不同消息之间的状态联系。

建立连接时,两端都将获得一个 runtime.Port 对象,用来通过建立的连接发送和接收消息。

如下代码演示如何从内容脚本中建立连接,发送并监听消息:

var port = chrome.runtime.connect({name: "敲门"});
port.postMessage({joke: "敲门"});
port.onMessage.addListener(function(msg) {
  if (msg.question == "是谁?")
    port.postMessage({answer: "女士"});
  else if (msg.question == "哪位女士?")
    port.postMessage({answer: "Bovary 女士"});
});

为了处理传入连接,您需要设置一个 runtime.onConnect 事件监听器。这一步无论在内容脚本还是扩展程序页面中都是一样的。当您的扩展程序的另一部分调用 connect() 时,会产生这一事件,同时传递您可以通过建立的连接发送和接受消息的 runtime.Port 对象。如下代码演示如何回应传入连接:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name == "敲门");
  port.onMessage.addListener(function(msg) {
    if (msg.joke == "敲门")
      port.postMessage({question: "是谁?"});
    else if (msg.answer == "女士")
      port.postMessage({question: "哪位女士?"});
    else if (msg.answer == "Bovary 女士")
      port.postMessage({question: "我没听清楚。"});
  });
});

您可能想知道连接何时关闭,例如您需要为每一个打开的端口单独保留状态。这种情况下您可以监听 runtime.Port.onDisconnect 事件,当连接的另一端调用 runtime.Port.disconnect 或包含该端口的页面已结束(例如标签页转到了另一个页面)时,对于每一个端口确保都会发生一次该事件。

除了在您的扩展程序的不同组成部分间发送消息以外,您也可以使用消息传递 API 与其他扩展程序通信。这样您可以提供一个公共的 API,让其他扩展程序使用。

监听传入的请求和连接与处理内部的消息类似,唯一的区别是您分别使用runtime.onMessageExternalruntime.onConnectExternal 事件。如下是分别处理这两个事件的例子:

// 用于简单的请求:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id == blacklistedExtension)
      return;  // 不允许这一扩展程序访问
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// 用于长时间的连接:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // 有关处理 onMessage 事件的示例请参见其他例子
  });
});

同样,向另一个扩展程序发送消息与在您的扩展程序中发送消息类似,唯一的区别是您必须传递您需要与之通信的扩展程序的标识符。例如:

// 我们需要与之通信的扩展程序的标识符。
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// 发出一个简单请求:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  });

// 建立一个长时间的连接:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

跨扩展程序消息传递类似,您的应用或扩展程序可以接受并响应来自普通网页的消息。要使用该特性,您必须首先在您的 manifest.json 中指定您希望与之通信的网站,例如:

"externally_connectable": {
  "matches": ["*://*.example.com/*"]
}

这样会将消息传递 API 提供给所有匹配您指定的 URL 表达式的网页。URL 表达式必须至少包含一个二级域名,也就是说禁止使用类似于“*”、“*.com”、“*.co.uk”和“*.appspot.com”之类的主机名。在网页中,使用 runtime.sendMessageruntime.connect API 向指定应用或扩展程序发送消息。例如:

// 我们希望与之通信的扩展程序标识符。
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// 发送一个简单的请求:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

在您的应用或扩展程序中,您可以通过 runtime.onMessageExternalruntime.onConnectExternal API 监听来自网页的消息,与跨扩展程序消息传递类似。只有网页才能发起连接。如下是一个例子:

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url == blacklistedWebsite)
      return;  // 不允许该网页访问
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

扩展程序可以与原生应用程序交换消息。支持该特性的原生应用程序必须注册一个了解如何与扩展程序通信的原生消息宿主,Chrome 浏览器将在单独的进程中启动宿主,并通过标准输入和标准输出流与之通信。

为了注册一个原生消息通信宿主,应用程序必须安装一个清单文件,定义原生消息通信宿主的配置。如下是这一清单文件的例子:

{
  "name": "com.my_company.my_application",
  "description": "我的应用程序",
  "path": "C:\\Program Files\\My Application\\chrome_native_messaging_host.exe",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"
  ]
}

消息通信宿主清单文件包含如下字段:

名称 描述
name 原生消息通信宿主的名称,客户端需要将该字符串传递给runtime.connectNativeruntime.sendNativeMessage
description 应用程序的简短描述。
path 原生消息通信宿主的二进制文件路径。在 Linux 和 OSX 上必须使用绝对路径,在 Windows 上可以使用相对于清单文件所在目录的路径。
type 与原生消息通信宿主交流时所使用的接口类型。目前该参数只有一种可能的值:stdio,它表示 Chrome 浏览器应该使用stdin(标准输入)和 stdout(标准输出)与宿主通信。
allowed_origins 允许访问原生消息通信宿主的扩展程序列表。

清单文件的类型取决与平台:

Windows:
清单文件可以在文件系统中的任意位置,应用程序的安装程序必须创建如下注册表键HKEY_LOCAL_MACHINE\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.my_company.my_applicationHKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\com.my_company.my_application,并将键的默认值设置为清单文件的完整路径。
OSX:
清单文件必须位于/Library/Google/Chrome/NativeMessagingHosts/com.my_company.my_application.json,对于在用户级别上安装的应用程序则是 ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.my_company.my_application.json
Linux:
清单文件必须位于 /etc/opt/chrome/native-messaging-hosts/com.my_company.my_application.json,对于在用户级别上安装的应用程序则是 ~/.config/google-chrome/NativeMessagingHosts/com.my_company.my_application.json

Chrome 浏览器在单独的进程中启动每一个原生消息通信宿主,并使用标准输入(stdin)与标准输出(stdout)与之通信。向两个方向发送消息时使用相同的格式:每一条消息使用 JSON 序列化,以 UTF-8 编码,并在前面附加 32 位的消息长度(使用本机字节顺序)。

使用 runtime.connectNative 创建消息传递端口时,Chrome 浏览器会启动原生消息传递宿主进程,并让它一直运行,直到端口释放。如果消息是使用 runtime.sendNativeMessage 发送,没有创建消息传递端口,Chrome 浏览器会为每一条消息创建一个新的原生消息传递宿主进程。在这种情况下,宿主进程产生的第一条消息作为原始请求的响应处理,也就是说,Chrome 浏览器会将它传递给调用 runtime.sendNativeMessage 时指定的回调函数,而原生消息传递宿主产生的所有其他消息则会忽略。

向原生应用程序发送和接收消息类似与跨扩展程序消息传递,主要的区别是用runtime.connectNative 代替 runtime.connect,用 runtime.sendNativeMessage 代替 runtime.sendMessage

以下例子创建一个 runtime.Port 对象,连接到原生消息通信宿主com.my_company.my_application,开始监听来自该端口的消息,并发送一条消息:

var port = chrome.runtime.connectNative('com.my_company.my_application');
port.onMessage.addListener(function(msg) {
  console.log("收到 " + msg);
});
port.onDisconnect.addListener(function() {
  console.log("已断开");
});
port.postMessage({ text: "我的应用程序,您好!" });

runtime.sendNativeMessage 可以用来向原生应用程序发送消息,而不用创建端口。例如:

chrome.runtime.sendNativeMessage('com.my_company.my_application',
  { text: "您好" },
  function(response) {
    console.log("收到 " + response);
  });

当您从内容脚本或另一个扩展程序接收消息时,您的后台网页应该小心,以免遭到跨站脚本攻击。特别地,避免使用下面这些危险的 API:

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // 警告!可能会执行恶意脚本!
  var resp = eval("(" + response.farewell + ")");
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // 警告!可能会插入恶意脚本!
  document.getElementById("resp").innerHTML = response.farewell;
});

您应该首选不运行脚本的更安全的API:

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse 不会执行攻击者的脚本。
  var resp = JSON.parse(response.farewell);
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText 不会让攻击者插入 HTML 元素。
  document.getElementById("resp").innerText = response.farewell;
});

您可以在 examples/api/messaging -->目录中找到使用消息通信的简单例子,examples/api/nativeMessaging 包含使用原生消息通信的示例应用程序,另外请参见contentscript_xhr 例子,在这个例子中内容脚本与所属扩展程序交换消息,以便扩展程序可以代表内容脚本发出跨站请求。有关更多例子以及查看源代码的帮助,请参见示例

参考链接


消息传递