OpenHarmony ServiceAbility一本地服务

系统 OpenHarmony
以下内容属于个人实践总结,在不同的系统版本、不同的SDK版本存在着一些差异。

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​

目的

创建服务,支持两种方式启动:startAbility、connectAbility,本次只实现启动本地服务,后续再介绍启动远程服务的操作。

前置条件

环境

  • 设备:DAYU200 开发板。
  • 系统:OpenHarmony 3.1 release。
  • IDE:DevEco Studio 3.0 Beta3。

项目实践

以下内容属于个人实践总结,在不同的系统版本、不同的SDK版本存在着一些差异,如果有描述错误的地方请留意修改,谢谢。

创建一个项目

创建项目

OpenHarmony ServiceAbility(一)本地服务-开源基础软件社区

项目初始状态的目录

OpenHarmony ServiceAbility(一)本地服务-开源基础软件社区

创建service

OpenHarmony ServiceAbility(一)本地服务-开源基础软件社区

创建service后,IDE会自动创建service的回调函数。

OpenHarmony ServiceAbility(一)本地服务-开源基础软件社区

这里简单的说明下服务的生命周期功能介绍。

接口名

描述

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)函数。

OpenHarmony ServiceAbility(一)本地服务-开源基础软件社区

注册服务

Service也需要在应用配置文件config.json中进行注册,注册类型type需要设置为service。

{
"module": {
"abilities": [
{
"visible": true,
"srcPath": "StartServiceAbility",
"name": ".StartServiceAbility",
"srcLanguage": "ets",
"icon": "$media:icon",
"description": "$string:StartServiceAbility_desc",
"type": "service"
},
]
...
}
...
}

服务开发

基于Service模板的Ability(以下简称“Service”)主要用于后台运行任务(如执行音乐播放、文件下载等),但不提供用户交互界面。Service可由其他应用或Ability启动,即使用户切换到其他应用,Service仍将在后台继续运行。

启动本地服务 startAbility()

​官方文档​

OpenHarmony ServiceAbility(一)本地服务-开源基础软件社区

​demo视频​

启动本地服务业务流程

1、点击“绑定事件”,用于将服务端处理的结果通过公共事件通知页面显示。

2、点击“开启本地服务”,将本地服务启动。

3、服务启动后,执行累加操作,初始值为1每间隔1秒累加一次,每次增加1。

4、通过公共事件,将服务端处理的累加结果通知页面显示。

5、点击“断开本地服务”,通过公共事件通知服务端停止,累加计算也停止。

6、点击“解绑事件”,取消公共事件的观察者。

相关代码

index.ets

说明:页面,主要用于操作服务功能使用。

import { TitleBar } from '../component/TitleBar'
import commonEvent from '@ohos.commonEvent'
import { StartServiceModel } from '../model/StartServiceModel'
import { ConnectServiceModel } from '../model/ConnectServiceModel'
import { OperateView } from '../component/OperateView'
import prompt from '@ohos.prompt';
import rpc from '@ohos.rpc';
const TAG = "[StartService.index]";
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
private btnBindCommonEvent: Array<String> = ['绑定事件', '解绑事件'];
private btnFResources: Array<String> = ['开启本地服务', '断开本地服务'];
private mSubscriber = null;
@State calculateResult: string = '';
private startServiceModel = new StartServiceModel();
private connectServiceModel = new ConnectServiceModel();
@State startServiceTitle: string = 'Start Service Test';
createSubscriber() {
if (this.mSubscriber != null) {
return;
}
// Subscriber information
var subscribeInfo = {
events: ["calculateResult", // 计算结果
],
}
// Create subscriber callback
commonEvent.createSubscriber(subscribeInfo, (err, subscriber) => {
if (err.code) {
console.error(`${TAG} [CommonEvent]CreateSubscriberCallBack err = ${JSON.stringify(err)}`)
prompt.showToast({
message: '绑定失败',
duration: 2000
})
} else {
console.log(`${TAG} [CommonEvent]CreateSubscriber`)
this.mSubscriber = subscriber
this.subscribe();
}
})
}
subscribe() {
// Subscribe
if (this.mSubscriber != null) {
commonEvent.subscribe(this.mSubscriber, (err, data) => {
if (err.code) {
console.error(`[CommonEvent]SubscribeCallBack err= = ${JSON.stringify(err)}`)
} else {
console.info(`[CommonEvent]SubscribeCallBack data= = ${JSON.stringify(data)}`)
let code: number = data.code;
switch (code) {
case (101):
{
// 计算返回
this.calculateResult = data.data;
break;
}
default:
{
break;
}
}
}
})
console.log(`${TAG} [CommonEvent] Subscribe succeed`);
prompt.showToast({
message: '绑定成功',
duration: 2000
})
} else {
console.error(`${TAG} [CommonEvent] Need create subscriber`)
prompt.showToast({
message: '请先绑定事件',
duration: 2000
});
}
}
unsubscribe() {
// Unsubscribe CommonEvent
if (this.mSubscriber != null) {
commonEvent.unsubscribe(this.mSubscriber, (err) => {
if (err.code) {
console.error(`${TAG}[CommonEvent]UnsubscribeCallBack err= = ${JSON.stringify(err)}`);
prompt.showToast({
message: '事件解绑失败',
duration: 2000
});
} else {
console.info(`${TAG} [CommonEvent]Unsubscribe success`)
prompt.showToast({
message: '事件解绑成功',
duration: 2000
});
this.mSubscriber = null
}
})
}
}
build() {
Column() {
TitleBar({ title: $startServiceTitle })
Text(this.calculateResult)
.fontSize(22)
.width('94%')
.margin({ top: 10, bottom: 10 })
.constraintSize({ minHeight: 50 })
.padding(10)
.border({ width: 1, color: Color.Gray, radius: 20 })
Row() {
ForEach(this.btnBindCommonEvent, (item, index) => {
Button() {
Text(item)
.fontColor(Color.White)
.fontSize(20)
}
.type(ButtonType.Capsule)
.backgroundColor('#0D9FFB')
.width('40%')
.height(60)
.margin(10)
.onClick(() => {
console.log(`${TAG} button clicked,index=${index}`);
switch (index) {
case 0:
{
// 绑定事件监听器
this.createSubscriber();
break;
}
case 1:
{
// 解绑事件监听器
this.unsubscribe();
break;
}
default:
{
break;
}
}
})
}, item => JSON.stringify(item))
}
ForEach(this.btnFResources, (item, index) => {
Button() {
Text(item)
.fontColor(Color.White)
.fontSize(20)
}
.type(ButtonType.Capsule)
.backgroundColor('#0D9FFB')
.width('90%')
.height(60)
.margin(10)
.onClick(() => {
console.log(`${TAG} button clicked,index=${index}`);
switch (index) {
case 0:
{
// 点击启动服务
this.startServiceModel.startService("");
prompt.showToast({
message: '服务已启动',
duration: 2000
})
break;
}
case 1:
{
// 点击断开服务
this.startServiceModel.stopService();
prompt.showToast({
message: '服务已关闭',
duration: 2000
});
break;
}
default:
{
break;
}
}
})
}, item => JSON.stringify(item))
}
.width('100%')
.height('100%')
}
}

StartServiceAbility

service.ts:

说明:服务,启动后在后台进行业务处理。

import commonEvent from '@ohos.commonEvent'
import featureAbility from '@ohos.ability.featureAbility';
const TAG: string = '[StartServiceAbility]'
class ServiceSubscriber {
subscriber = null;
intervalID = -1;
isCalculating = false;
constructor() {
}
/**
* 开始累加
* @param data
*/
calculate(data: number) {
if (this.isCalculating) {
return;
}
this.isCalculating = true;
let self = this;
this.intervalID = setInterval(() => {
data++;
self.sendResult(data.toString());
}, 1000);
}
/**
* 停止累加
*/
stopCalculate() {
console.log(`${TAG} ServiceAbility stop Calculate`);
this.isCalculating = false;
if (this.intervalID != -1) {
clearInterval(this.intervalID);
this.intervalID = -1;
}
}
sendResult(result) {
var options = {
code: 101, // 返回消息结果
data: result
}
commonEvent.publish("calculateResult", options, (err) => {
if (err.code) {
console.error(`${TAG} [CommonEvent]PublishCallBack err = ${JSON.stringify(err)}`)
} else {
console.log(`${TAG} [CommonEvent] PublishCall success`)
}
})
}
createSubscriber() {
// Subscriber information
var subscribeInfo = {
events: ["closeService"],
}
// Create subscriber callback
commonEvent.createSubscriber(subscribeInfo, (err, subscriber) => {
if (err.code) {
console.error(`${TAG} [CommonEvent]CreateSubscriberCallBack err = ${JSON.stringify(err)}`)
} else {
console.log(`${TAG} [CommonEvent]CreateSubscriber`)
this.subscriber = subscriber
this.subscribe();
}
})
}
subscribe() {
// Subscribe
if (this.subscriber != null) {
commonEvent.subscribe(this.subscriber, (err, data) => {
if (err.code) {
console.error(`[CommonEvent]SubscribeCallBack err= = ${JSON.stringify(err)}`)
} else {
console.info(`[CommonEvent]SubscribeCallBack data= = ${JSON.stringify(data)}`)
// 关闭服务
featureAbility.terminateSelf(() => {
console.log(`${TAG} [CommonEvent] featureAbility terminateSelf`)
});
}
})
console.log(`${TAG} [CommonEvent] Subscribe succeed`)
} else {
console.error(`${TAG} [CommonEvent] Need create subscriber`)
}
}
unsubscribe() {
// Unsubscribe CommonEvent
if (this.subscriber != null) {
commonEvent.unsubscribe(this.subscriber, (err) => {
if (err.code) {
console.error(`${TAG}[CommonEvent]UnsubscribeCallBack err= = ${JSON.stringify(err)}`)
} else {
console.info(`${TAG} [CommonEvent]Unsubscribe success`)
this.subscriber = null
}
})
}
}
}
let serviceSubscriber: ServiceSubscriber = null;
export default {
onStart() {
console.log(`${TAG} ServiceAbility onStart`);
serviceSubscriber = new ServiceSubscriber();
serviceSubscriber.createSubscriber();
},
onConnect(want) {
console.log(`${TAG} ServiceAbility OnConnect`);
return null;
},
onCommand(want, startId) {
console.log(`${TAG} ServiceAbility onCommand`);
// 开始累加数值
if (serviceSubscriber != null) {
serviceSubscriber.calculate(0);
} else {
console.error(`${TAG} ServiceAbility onCommand serviceSubscriber is null`);
}
},
onDisconnect(want) {
console.log(`${TAG} ServiceAbility OnDisConnect`);
},

onStop() {
console.log(`${TAG} ServiceAbility onStop`);
// 关闭监听器
if (serviceSubscriber != null) {
serviceSubscriber.unsubscribe();
serviceSubscriber.stopCalculate();
serviceSubscriber = null;
}
},
onReconnect(want) {
console.log(`${TAG} ServiceAbility onReconnect`);
},
};

StartServiceModel

说明:启动本地服务处理类,用于控制启动、停止服务,关闭服务使用到了通过公共事件通知服务调用featureAbility.terminateSelf()实现停止服务。

import featureAbility from '@ohos.ability.featureAbility';
import commonEvent from '@ohos.commonEvent'
let TAG: string = '[StartService.StartServiceModel]'
/**
* 客户端启动服务
*/
export class StartServiceModel{
/**
* 启动服务
*/
startService(deviceId) {
console.log(`${TAG} startService begin`);
featureAbility.startAbility({
want: {
deviceId: deviceId,
bundleName: 'com.nlas.etsservice',
abilityName: 'com.example.entry.StartServiceAbility'
}
});
}
/**
* 断开服务
*/
stopService() {
var options = {
code: 103 // 返回消息结果
}
commonEvent.publish("closeService", options, (err) => {
if (err.code) {
console.error(`${TAG} [CommonEvent]PublishCallBack stopService err = ${JSON.stringify(err)}`)
} else {
console.log(`${TAG} [CommonEvent] PublishCall stopService success`)
}
})
}
}

至此,startAbility()的方式启动服务操作就完成了。

接下去我们来说说通过 connectAbility() 连接服务。

连接本地服务 connectAbility

​官方指导​

OpenHarmony ServiceAbility(一)本地服务-开源基础软件社区

​demo视频​

启动本地服务业务流程

1、点击 “连接本地服务”,执行服务连接,根据连接的结果进行toast提示,连接成功则提示:“服务已连接”。

2、服务连接成功状态下,在输入框中输入需要排序的字符串;

3、点击“排序”,服务端处理字符排序,并把排序的结果通过服务代理对象返回给客户端,客户端显示排序结果。

4、点击“断开本地服务”,如果服务断开成功,则提示:“服务已断开”。

5、服务未连接或者已断开状态下,点击排序,提示:“请连接服务”。

相关代码

index.ets

说明:页面,用于操作服务和显示服务端处理的结果。

import { TitleBar } from '../component/TitleBar'
import { ConnectServiceModel } from '../model/ConnectServiceModel'
import { OperateView } from '../component/OperateView'
import prompt from '@ohos.prompt';
import rpc from '@ohos.rpc';
const TAG = "[StartService.index]";
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
private btnSortResources: Array<String> = ['连接本地服务', '断开本地服务', '排序'];
private mSubscriber = null;
private connectServiceModel = new ConnectServiceModel();
@State beforeSortString: string = '';
@State afterSortString: string = '';
@State sourceString: string = '';
@State connectServiceTitle: string = 'Connect Service Test';
async sortString() {
console.log(`${TAG} sortString begin`)
let mRemote = this.connectServiceModel.getRemoteObject()
if (mRemote === null) {
prompt.showToast({
message: '请连接服务'
})
return;
}
if (this.beforeSortString.length === 0) {
prompt.showToast({
message: 'please input a string'
})
return;
}
let option: rpc.MessageOption = new rpc.MessageOption()
let data: rpc.MessageParcel = rpc.MessageParcel.create()
let reply: rpc.MessageParcel = rpc.MessageParcel.create()
data.writeString(this.beforeSortString)
await mRemote.sendRequest(1, data, reply, option)
let msg = reply.readString()
this.afterSortString = msg
}
build() {
Column() {
TitleBar({ title: $connectServiceTitle })
OperateView({ before: $beforeSortString, after: $afterSortString, source: $sourceString })
ForEach(this.btnSortResources, (item, index) => {
Button() {
Text(item)
.fontColor(Color.White)
.fontSize(20)
}
.type(ButtonType.Capsule)
.backgroundColor('#0D9FFB')
.width('94%')
.height(50)
.margin(10)
.onClick(() => {
console.info(`${TAG} button clicked,index=${index}`)
switch (index) {
case 0:
this.connectServiceModel.connectService("");
break;
case 1:
this.connectServiceModel.disconnectService();
break;
case 2:
this.sortString();
break;
default:
break;
}
})
}, item => JSON.stringify(item))
}
.width('100%')
.height('100%')
}
}

ConnectServiceAbility

service.ts:

说明:服务,服务连接后返回服务代理对象,客户端通过服务代理对象向服务端发起请求(sendRequest()),服务端根据客户端的请求进行处理,将结果通服务代理对象写给客户端。

import rpc from "@ohos.rpc"
const TAG: string = '[StartServiceAbility]'
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();
console.log(`${TAG} string=${string}`)
let result = Array.from(string).sort().join('')
console.log(`${TAG} result=${result}`)
reply.writeString(result);
} else {
console.log(`${TAG} unknown request code`)
}
return true;
}
}
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`);
}
};

ConnectServiceModel.ts

说明:用于连接服务、断开服务的操作处理类。

import prompt from '@ohos.prompt'
import featureAbility from '@ohos.ability.featureAbility'
import rpc from "@ohos.rpc"
let mRemote: rpc.IRemoteObject = null
let connection: number = -1
let TAG: string = '[ConnectServiceModel.ServiceModel]'
export class ConnectServiceModel {
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}`)
}
onFailedCallback(code) {
console.log(`${TAG}onConnectLocalService onFailed errCode:${code}`)
prompt.showToast({
message: `服务连接失败 errCode:${code}`
})
}
connectService(deviceId) {
console.log(`${TAG} onConnectService begin`)
connection = featureAbility.connectAbility(
{
deviceId: deviceId,
bundleName: 'com.nlas.etsservice',
abilityName: 'com.example.entry.ConnectServiceAbility'
},
{
onConnect: this.onConnectCallback,
onDisconnect: this.onDisconnectCallback,
onFailed: this.onFailedCallback,
},
)
}
disconnectService() {
console.log(`${TAG} onDisconnectService begin`)
mRemote = null
if (connection === -1) {
prompt.showToast({
message: '服务未连接'
})
return
}
featureAbility.disconnectAbility(connection)
connection = -1
prompt.showToast({
message: '服务已断开'
})
}
getRemoteObject() {
return mRemote
}
}

至此,通过connectAbility()连接本地服务的操作就完成了,主要注意两点:

1、连接服务时需要返回服务代理对象。

2、客户端向服务端发起请求时,服务端通过代理对象读取客户端数据的顺序必须与客户端写入的数据顺序一致,当然,服务端向客户端返回数据时代理对象写入的顺序与客户端读取的顺序必须相同。

错误示范

客户端代码:

async sortString() {
console.log(`${TAG} sortString begin`)
let mRemote = this.connectServiceModel.getRemoteObject()
if (mRemote === null) {
prompt.showToast({
message: '请连接服务'
})
return;
}
if (this.beforeSortString.length === 0) {
prompt.showToast({
message: 'please input a string'
})
return;
}
let option: rpc.MessageOption = new rpc.MessageOption()
let data: rpc.MessageParcel = rpc.MessageParcel.create()
let reply: rpc.MessageParcel = rpc.MessageParcel.create()
data.writeInt(11);// 客户端写入1
data.writeString("aaaaaaaaa");// 客户端写入2
data.writeString(this.beforeSortString);// 客户端写入3
data.writeString("eeeeeeeee");// 客户端写入4
data.writeInt(22);// 客户端写入5
await mRemote.sendRequest(1, data, reply, option);
let intS = reply.readInt();// 服务端返回数据 读取4
let intE = reply.readInt();//服务端返回数据 读取 5
let startStr = reply.readString();// 服务端返回数据 读取1
let msg = reply.readString();// 服务端返回数据 读取2
let startEnd = reply.readString();// 服务端返回数据 读取3
console.log(`[StartServiceAbility] callback intS=${intS} startStr=${startStr} startEnd=${startEnd} msg=${msg} intE=${intE}`)
this.afterSortString = intS.toString() + startStr + msg + startEnd + intE.toString();
}

服务端代码:

onRemoteRequest(code: number, data: any, reply: any, option: any) {
console.log(`${TAG}onRemoteRequest called`)
if (code === 1) {
let intS = data.readInt();// 读取客户端请求数据1
let intE = data.readInt();// 读取客户端请求数据5
let startStr = data.readString();// 读取客户端请求数据2
let string = data.readString();// 读取客户端请求数据3
let endStr = data.readString();// 读取客户端请求数据4
this.initialPublish(string);
console.log(`${TAG} intS=${intS} intE=${intE}`);
console.log(`${TAG} startStr=${startStr} string=${string} endStr=${endStr}`);
let result = Array.from(string).sort().join('');
console.log(`${TAG} result=${result}`);
reply.writeString(startStr);// 返回客户端 写入数据1
reply.writeString(result);// 返回客户端 写入数据2
reply.writeString(endStr);// 返回客户端 写入数据3
reply.writeInt(intS);// 返回客户端 写入数据4
reply.writeInt(intE);// 返回客户端 写入数据5
} else {
console.log(`${TAG} unknown request code`)
}
return true;
}

返回结果:

08-05 17:39:50.629 11002-11013/com.nlas.etsservice D 03b00/JSApp: app Log: [StartServiceAbility] callback intS=0 startStr= startEnd= msg= intE=0

正确示范

客户端代码:

async sortString() {
console.log(`${TAG} sortString begin`)
let mRemote = this.connectServiceModel.getRemoteObject()
if (mRemote === null) {
prompt.showToast({
message: '请连接服务'
})
return;
}
if (this.beforeSortString.length === 0) {
prompt.showToast({
message: 'please input a string'
})
return;
}
let option: rpc.MessageOption = new rpc.MessageOption()
let data: rpc.MessageParcel = rpc.MessageParcel.create()
let reply: rpc.MessageParcel = rpc.MessageParcel.create()
data.writeInt(11);// 客户端写入1
data.writeString("aaaaaaaaa");// 客户端写入2
data.writeString(this.beforeSortString);// 客户端写入3
data.writeString("eeeeeeeee");// 客户端写入4
data.writeInt(22);// 客户端写入5
await mRemote.sendRequest(1, data, reply, option);
let startStr = reply.readString();// 服务端返回数据 读取1
let msg = reply.readString();// 服务端返回数据 读取2
let startEnd = reply.readString();// 服务端返回数据 读取3
let intS = reply.readInt();// 服务端返回数据 读取4
let intE = reply.readInt();//服务端返回数据 读取 5
console.log(`[StartServiceAbility] callback intS=${intS} startStr=${startStr} startEnd=${startEnd} msg=${msg} intE=${intE}`)
this.afterSortString = intS.toString() + startStr + msg + startEnd + intE.toString();
}

服务端代码:

onRemoteRequest(code: number, data: any, reply: any, option: any) {
console.log(`${TAG}onRemoteRequest called`)
if (code === 1) {
let intS = data.readInt();// 读取客户端请求数据1
let startStr = data.readString();// 读取客户端请求数据2
let string = data.readString();// 读取客户端请求数据3
let endStr = data.readString();// 读取客户端请求数据4
let intE = data.readInt();// 读取客户端请求数据5
this.initialPublish(string);
console.log(`${TAG} intS=${intS} intE=${intE}`);
console.log(`${TAG} startStr=${startStr} string=${string} endStr=${endStr}`);
let result = Array.from(string).sort().join('');
console.log(`${TAG} result=${result}`);
reply.writeString(startStr);// 返回客户端 写入数据1
reply.writeString(result);// 返回客户端 写入数据2
reply.writeString(endStr);// 返回客户端 写入数据3
reply.writeInt(intS);// 返回客户端 写入数据4
reply.writeInt(intE);// 返回客户端 写入数据5
} else {
console.log(`${TAG} unknown request code`)
}
return true;
}

返回结果:

08-05 17:26:43.706 10605-10615/com.nlas.etsservice D 03b00/JSApp: app Log: [StartServiceAbility]

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​​。

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐
点赞
收藏

51CTO技术栈公众号