目的
实现启动远程服务和关闭远程服务,通过connectAbility可以连接远程的服务,通过客户端将请求发送到远程服务,远程服务进行业务处理并把结果返回到请求端,实现连接远程服务的能力。
前置条件
环境
- 设备:DAYU200 开发板。
- 系统:OpenHarmony 3.1 release。
- IDE:DevEco Studio 3.0 Beta3。
项目实践
以下内容属于个人实践总结,在不同的系统版本、不同的SDK版本存在着一些差异,如果有描述错误的地方请留意修改,谢谢。
官方指导
创建一个项目
创建项目
坑点1、建议使用eTS语言开发服务模块。
理由:根据实践发现,通过JS开发的Service只能开启本地服务,无法连接远程服务,问题已在社区反馈,如果你需要使用JS开发服务,并且还需要实现连接远程服务,请关注社区的解决进度:OpenHarmony3.1release 连接服务失败。
创建service
这里重复的说明下服务的生命周期功能介绍。
接口名 | 描述 |
onStart | 该方法在创建Service的时候调用,用于Service的初始化。在Service的整个生命周期只会调用一次,调用时传入的Want应为空。 |
onCommand | 在Service创建完成之后调用,该方法在客户端每次启动该Service时都会调用,开发者可以在该方法中做一些调用统计、初始化类的操作。 |
onConnect | 在Ability和Service连接时调用。 |
onDisconnect | 在Ability与绑定的Service断开连接时调用。 |
onStop | 在Service销毁时调用。Service应通过实现此方法来清理任何资源,如关闭线程、注册的侦听器等。 |
从这里可以看出,IDE为我们自动创建的service生命周期函数中,还有两个重要的函数onConnect(want)、onDisconnect(want) 没有实现,你可以通过手动方式添加函数。这里顺便提一下,如果是使用 startAbility()的方式启动本地服务,则可以不实现onConnect(want)函数。
注册服务
Service也需要在应用配置文件config.json中进行注册,注册类型type需要设置为service。
{
"module": {
"abilities": [
{
"srcPath": "ServiceAbility",
"name": ".ServiceAbility",
"srcLanguage": "ets",
"icon": "$media:icon",
"description": "$string:ServiceAbility_desc",
"type": "service",
"visible": true
},
]
}
}
注意:注册服务时需要将 “visible”: true。
声明权限
因为本案例中需要使用到分布式相关的内容,所以在config.json配置文件中的module模块下添加DISTRIBUTED_DATASYNC 权限声明。
"reqPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC"
}
]
服务开发
Service与Page Ability或其他应用的Service Ability进行跨设备交互,则须创建用于连接的Connection。Service支持其他Ability通过connectAbility()方法与其进行跨设备连接。
在使用connectAbility()处理回调时,需要传入目标Service的Want与IAbilityConnection的实例。IAbilityConnection提供了以下方法供开发者实现:onConnect()是用来处理连接Service成功的回调,onDisconnect()是用来处理Service异常死亡的回调,onFailed()是用来处理连接Service失败的回调。
连接远程服务 connectService()
demo视频
连接远程服务流程图
连接远程服务的业务流程
1、启动应用,自动开启局域网内的设备扫描。
2、未认证的设备,显示在设备列表中,从设备列表中选择需要连接的远程设备,点击"连接"。
3、系统发起PIN码认证,根据提示输入PIN码认证成功。
4、在已认证设备列表中点击"操作"进入操作页面,选择服务模块进行操作。
5、为了方便查看本次操作是通过远端服务进行处理,可以点击"启动服务APP",远程调度服务FA,用于展示客户端的请求和服务端处理的结果。
6、使用远程服务能力之前,必须要先创建连接,可以点击:“连接服务”,如果连接成功则提示:“服务连接成功”。
7、输入需要排序的字符串。
8、点击"排序",此时如果远程服务的FA被启动,则在远程服务的FA上显示客户端请求的排序字符串。
9、服务端收到排序请求后进行排序处理,最终将结果返回给客户端显示,同时在服务端的FA上也显示计算的结果。
关键的代码
本案例中,由于客户端代码使用JS,服务端代码使用eTS,为什么服务端代码需要使用eTS开发呢?前面我们提过,因为JS开发的服务,无法连接JS开发的客户端,所以服务端的代码这里采用rTS。
(客户端)分布式设备连接处理器–RemoteDeviceModel.js
说明:RemoteDeviceModel主要负责扫描局域网内的设备、获取已认证的设备、具体设备的状态变化。
import deviceManager from '@ohos.distributedHardware.deviceManager';
var SUBSCRIBE_ID = 100;
const DEVICE_STATE_CHANGE = 'deviceStateChange';
const DEVICE_FOUND = "deviceFound";
const DISCOVER_FAIL = "discoverFail";
const SERVICE_DIE = "serviceDie";
export default class RemoteDeviceModel {
deviceList = [];// 可信设备列表
discoverList = [];// 发现设备列表
callback;
#deviceManager;
constructor() {
}
// 注册设备列表回调
registerDeviceListCallback(callback) {
console.info("[RemoteDeviceModel] registerDeviceListCallback")
if (typeof (this.#deviceManager) === 'undefined') {
// 获取设备管理对象
let self = this;
deviceManager.createDeviceManager('com.nlas.softbustools',
(error, value) => {
if (error) {
console.error('[RemoteDeviceModel] createDeviceManager failed.');
return;
}
self.#deviceManager = value;// 设备管理器对象实例
self.registerDeviceListCallback_(callback);
})
} else {
this.registerDeviceListCallback_(callback);
}
}
registerDeviceListCallback_(callback) {
console.info('[RemoteDeviceModel] registerDeviceListCallback_');
this.callback = callback;
if (typeof (this.#deviceManager) === 'undefined') {
console.error('[RemoteDeviceModel] deviceManager has not initialized');
this.callback();
return;
}
// 同步获取所有可信设备列表
var list = this.#deviceManager.getTrustedDeviceListSync();
console.info('[RemoteDeviceModel] deviceList=' + JSON.stringify(list));
if (typeof (list) != 'undefined' && typeof (list.length) != 'undefined') {
this.deviceList = list;// list包含:deviceId、deviceName、deviceType
}
this.callback();
let self = this;
// 注册分布式监听器--设备状态变化
// 可以参看API:https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis/js-apis-device-manager.md#devicemanager
this.#deviceManager.on(DEVICE_STATE_CHANGE, (data) => {
if (typeof (data) === 'undefined') {
console.error("[RemoteDeviceModel] data undefined");
return;
}
console.info('[RemoteDeviceModel] deviceStateChange data=' + JSON.stringify(data));
switch (data.action) {
case 0:
{
// 设备上线
console.info('[RemoteDeviceModel] online, updated device list=' + JSON.stringify(self.deviceList));
// 实际证明,返回的设备信息与原来的设备信息存在差异:deviceId、deviceName、deviceType、networkId
// 源数据:data={"subscribeId":20226,"device":{"deviceId":"2762998A243A9F2746FFF1FE7546736F7171C3FC089F39FD4BC987AD76379717","networkId":"","deviceName":"OpenHarmony","deviceType":175}}
// 返回:data={"action":0,"device":{"deviceId":"09f70e8e0e85d0523454ffe58d0a851dfe05c30073a1aa8a2306233e43750b30","networkId":"09f70e8e0e85d0523454ffe58d0a851dfe05c30073a1aa8a2306233e43750b30","deviceName":"OpenHarmony 3.1 Release","deviceType":0}}
// 去重处理
let deviceCount = self.deviceList.length;
if (deviceCount > 0) {
for(var i = 0; i < deviceCount; i++) {
if (self.deviceList[i].deviceId === data.device.deviceId) {
console.info("[RemoteDeviceModel] online, device already exist");
return;
}
}
}
self.deviceList[deviceCount] = data.device;
self.callback();
break;
}
case 2:
{
// 设备下线
console.info('[RemoteDeviceModel] offline, updated device list=' + JSON.stringify(data.device));
const count = self.deviceList.length;
if (count > 0) {
var list = [];
// 过滤出所有在线的设备
for (var i = 0; i < count; i++) {
if (self.deviceList[i].deviceId != data.device.deviceId) {
list[i] = data.device;
}
}
self.deviceList = list;
}
self.callback();
break;
}
case 3:
{
console.info('[RemoteDeviceModel] change, updated device list=' + JSON.stringify(self.deviceList));
if (self.deviceList.length > 0) {
// 修改设备状态
for (var i = 0; i < self.deviceList.length; i++) {
if (self.deviceList[i].deviceId === data.device.deviceId) {
self.deviceList[i] = data.device;
break;
}
}
}
self.callback();
break;
}
default:
break;
}
});
// 发现设备
this.#deviceManager.on(DEVICE_FOUND, (data) => {
console.info('[RemoteDeviceModel] deviceFound data=' + JSON.stringify(data));
if (data == null) {
return;
}
const count = self.discoverList.length;
for (var i = 0; i < count; i++) {
if (self.discoverList[i].deviceId === data.device.deviceId) {
// 设备已经在列表中
return;
}
}
// 新增发现的设备
self.discoverList[count] = data.device;
console.info('[RemoteDeviceModel] deviceFound finish');
self.callback();
});
this.#deviceManager.on(DISCOVER_FAIL, (data) => {
console.info('[RemoteDeviceModel] discoverFail data=' + JSON.stringify(data));
});
this.#deviceManager.on(SERVICE_DIE, () => {
console.error('[RemoteDeviceModel] serviceDie');
});
// 随机数
SUBSCRIBE_ID = Math.floor(65536 * Math.random());
var info = {
subscribeId: SUBSCRIBE_ID,// 服务订阅ID【0~65535】,对每个发现进程来说应该是唯一的
mode: 0xAA,//主动模式
medium: 2,//订阅媒介 2-wifi
freq: 2,// 订阅频率 高
isSameAccount: false,// 只能找到帐号相同的设备
isWakeRemote: true,// 找到睡眠设备
capability: 0
};
console.info('[RemoteDeviceModel] startDeviceDiscovery ' + SUBSCRIBE_ID);
this.#deviceManager.startDeviceDiscovery(info);// 发现设备
}
/**
* 设备认证
* @param deviceId 设备ID
* @param callback 回调
*/
authDevice(deviceId, callback) {
console.info('[RemoteDeviceModel] authDevice ' + deviceId);
for (var i = 0; i < this.discoverList.length; i++) {
if (this.discoverList[i].deviceId === deviceId) {
// 在线设备中存在此设备,创建扩展信息
let extraInfo = {
"targetPkgName": 'com.ohos.distributedRemoteStartFA',
"appName": 'demo',
"appDescription": 'demo application',
"business": '0'
};
let authParam = {
"authType": 1,// 认证类型,1为pin码。
"appIcon": '',
"appThumbnail": '',
"extraInfo": extraInfo // 扩展信息:key-value
};
console.info('[RemoteDeviceModel] authenticateDevice ' + JSON.stringify(this.discoverList[i]));
this.#deviceManager.authenticateDevice(this.discoverList[i], authParam, (err, data)=>{
console.info('[RemoteDeviceModel] authenticateDevice, data=' + JSON.stringify(data) + ";err=" + JSON.stringify(err));
let code = 0;
if (typeof(err) != 'undefined') {
code = err.code;
}
callback({
code: code
});
})
}
}
}
unregisterDeviceListCallback() {
console.info('[RemoteDeviceModel] stopDeviceDiscovery ' + SUBSCRIBE_ID);
this.#deviceManager.stopDeviceDiscovery(SUBSCRIBE_ID);
this.#deviceManager.off(DEVICE_STATE_CHANGE);
this.#deviceManager.off(DEVICE_FOUND);
this.#deviceManager.off(DISCOVER_FAIL);
this.#deviceManager.off(SERVICE_DIE);
this.deviceList = [];
this.discoverList = [];
}
}
(客户端)远程服务连接–ServiceModel.js
说明:ServiceModel 主要实现远程服务的连接、提供远程服务代理对象、断开远程服务能力。
import prompt from '@ohos.prompt';
import featureAbility from '@ohos.ability.featureAbility';
const TAG = "[ServiceModel]";
let mRemote = null; //rpc远程对象
let connection = -1; // 连接码
export class ServiceModel {
/**
* 连接服务回调
* @param element 元素名
* @param remote rpc远程对象 rpc.IRemoteObject
*/
onConnectCallback(element, remote) {
console.log(`${TAG}onConnectLocalService onConnectDone element:${element}`)
console.log(`${TAG}onConnectLocalService onConnectDone remote:${remote}`)
mRemote = remote;
if (mRemote === null) {
prompt.showToast({
message: '服务连接失败'
})
return;
}
prompt.showToast({
message: '服务连接成功',
})
}
onDisconnectCallback(element) {
console.log(`${TAG}onConnectLocalService onDisconnectDone element:${element}`)
prompt.showToast({
message: '服务断开',
})
}
onFailedCallback(code) {
console.log(`${TAG}onConnectLocalService onFailed errCode:${code}`)
prompt.showToast({
message: `连接失败 :${code}`
})
}
connectService(deviceId) {
console.log(`${TAG} onConnectService begin deviceId:${deviceId}`)
connection = featureAbility.connectAbility(
{
deviceId: deviceId,
// bundleName: 'com.nlas.softbustools',
// abilityName: 'com.example.entry.ServiceAbility',
bundleName: 'com.nlas.myapplication',
abilityName: 'com.example.entry.ServiceAbility',
},
{
onConnect: this.onConnectCallback,
onDisconnect: this.onDisconnectCallback,
onFailed: this.onFailedCallback,
},
)
}
disconnectService() {
console.log(`${TAG} onDisconnectService begin`)
mRemote = null
if (connection === -1) {
prompt.showToast({
message: 'onDisconnectService not connected yet'
})
return
}
featureAbility.disconnectAbility(connection)
connection = -1
prompt.showToast({
message: 'onDisconnectService disconnect done'
})
}
getRemoteObject() {
return mRemote
}
}
(服务端)服务–ServiceAbility
service.ts:此服务的目的是接收到客户端的请求,根据请求码进行相关的业务操作,本案例中主要完成字符串的排序功能,为了更方便查看客户端的请求数据,服务代理收到数据后,通过公共事件传输给页面显示。
import rpc from "@ohos.rpc"
import commonEvent from '@ohos.commonEvent'
const TAG: string = '[ConnectServiceAbility]'
class ServiceSubscriber extends rpc.RemoteObject{
constructor(des: any) {
if (typeof des === 'string') {
super(des)
} else {
return
}
}
onRemoteRequest(code: number, data: any, reply: any, option: any) {
console.log(`${TAG}onRemoteRequest called`)
if (code === 1) {
let string = data.readString();
this.initialPublish(102, string);
console.log(`${TAG} string=${string}`)
let result = Array.from(string).sort().join('')
console.log(`${TAG} result=${result}`)
reply.writeString(result);
this.initialPublish(103, result);
} else {
console.log(`${TAG} unknown request code`)
}
return true;
}
initialPublish(code, result) {
// CommonEvent related information
var options = {
code: code, // Initial code for CommonEvent
data: result, // Initial data for CommonEvent
}
// Publish CommonEvent
commonEvent.publish("sortResult", options, (err) => {
if (err.code) {
console.error(`[CommonEvent]PublishCallBack err = ${JSON.stringify(err)}`)
} else {
console.info("[CommonEvent]Publish2")
}
})
}
}
export default {
onStart() {
console.log(`${TAG} ServiceAbility onStart`);
},
onStop() {
console.log(`${TAG} ServiceAbility onStop`);
},
onCommand(want, startId) {
console.log(`${TAG} ServiceAbility onCommand`);
},
onConnect(want) {
console.log(`${TAG} ServiceAbility OnConnect`);
return new ServiceSubscriber("sort service");
},
onDisconnect(want) {
console.log(`${TAG} ServiceAbility OnDisConnect`);
}
};
注意:公共事件类似于全局广播,订阅者都能收到公共事件,所以建议非必要情况下业务相关的通知可以不适用公共事件进行通知,如果一定要使用公共事件通知业务,那么注册的事件名称也尽量与业务相关,避免出现错误。
客户端向服务端发送排序请求
onSortStr() {
this.log.logDef("wait Sort Str:" + this.waitSortStr);
let remote = this.serviceModel.getRemoteObject();
if (remote === null) {
this.showToast("请先连接服务");
return;
}
if (this.waitSortStr.length === 0) {
this.showToast("排序字符不能为空");
return;
}
let option = new rpc.MessageOption();// 消息对象,本次请求的同异步模式,默认同步调用
// MessageParcel 提供读写基础类型及数组、IPC对象、接口描述符和自定义序列化对象的方法。
let data = rpc.MessageParcel.create();// 保存待发送数据的 MessageParcel对象
let reply = rpc.MessageParcel.create();// 接收应答数据的MessageParcel对象
data.writeString(this.waitSortStr);
// 发起请求,参数1:业务码
let self = this;
let promise = remote.sendRequest(1,data, reply, option);
promise.then((result) => {
self.log.logDef("sendRequest result:" + result);
if (result.errCode === 0) {
// 获取发送给服务处理的结果
self.sortResult = reply.readString();
self.log.logDef("sort result:" + self.sortResult);
} else {
self.log.logDef("sendRequest fail errorCode:" + result.errCode);
}
}).catch((e) => {
self.log.logErr("sendRequest exception:" + e.message);
}).finally(() => {
self.log.logDef("sendRequest end");
data.reclaim();// 释放不再使用的MessageParcel对象
reply.reclaim();
})
}
至此调用远程服务处理业务的功能就介绍完成。