提交 986369c0 编写于 作者: W wangqilong2

ipc&rpc开发指南整改回合monthly

Signed-off-by: Nwangqilong2 <wangqilong2@huawei.com>
上级 18896d19
......@@ -9,43 +9,76 @@ IPC/RPC的主要工作是让运行在不同进程的Proxy和Stub互相通信,
**表1** Native侧IPC接口
| 类/接口 | 方法 | 功能说明 |
| 类/接口 | 方法 | 功能说明 |
| -------- | -------- | -------- |
| [IRemoteBroker](../reference/apis/js-apis-rpc.md#iremotebroker) | sptr&lt;IRemoteObject&gt; AsObject() | 返回通信对象。派生类需要实现,Stub端返回RemoteObject对象本身,Proxy端返回代理对象。 |
| IRemoteStub | virtual int OnRemoteRequest(uint32_t code, MessageParcel &amp;data, MessageParcel &amp;reply, MessageOption &amp;option) | 请求处理方法,派生类需要重写该方法用来处理Proxy的请求并返回结果。 |
| IRemoteProxy | | 业务Proxy类,派生自IRemoteProxy类。 |
| [IRemoteBroker](../reference/apis/js-apis-rpc.md#iremotebroker) | sptr&lt;IRemoteObject&gt; AsObject() | 返回通信对象。Stub端返回RemoteObject对象本身,Proxy端返回代理对象。 |
| IRemoteStub | virtual int OnRemoteRequest(uint32_t code, MessageParcel &amp;data, MessageParcel &amp;reply, MessageOption &amp;option) | 请求处理方法,派生类需要重写该方法用来处理Proxy的请求并返回结果。 |
| IRemoteProxy | | 业务的Pory类需要从IRemoteProxy类派生。 |
## 开发步骤
**Native侧开发步骤**
### **Native侧开发步骤**
1. 定义IPC接口ITestAbility
1. 添加依赖
SA接口继承IPC基类接口IRemoteBroker,接口里定义描述符、业务函数和消息码,其中业务函数在Proxy端和Stub端都需要实现。
SDK依赖:
```
#ipc场景
external_deps = [
"ipc:ipc_single",
]
#rpc场景
external_deps = [
"ipc:ipc_core",
]
```
此外, IPC/RPC依赖的refbase实现在公共基础库下,请增加对utils的依赖:
```
external_deps = [
"c_utils:utils",
]
```
2. 定义IPC接口ITestAbility
SA接口继承IPC基类接口IRemoteBroker,接口里定义描述符、业务函数和消息码,其中业务函数在Proxy端和Stub端都需要实现。
```c++
#include "iremote_broker.h"
//定义消息码
const int TRANS_ID_PING_ABILITY = 5
const std::string DESCRIPTOR = "test.ITestAbility";
class ITestAbility : public IRemoteBroker {
public:
// DECLARE_INTERFACE_DESCRIPTOR是必需的,入参需使用std::u16string;
DECLARE_INTERFACE_DESCRIPTOR("test.ITestAbility");
int TRANS_ID_PING_ABILITY = 1; // 定义消息码
DECLARE_INTERFACE_DESCRIPTOR(to_utf16(DESCRIPTOR));
virtual int TestPingAbility(const std::u16string &dummy) = 0; // 定义业务函数
};
```
2. 定义和实现服务端TestAbilityStub
3. 定义和实现服务端TestAbilityStub
该类是和IPC框架相关的实现,需要继承 IRemoteStub&lt;ITestAbility&gt;。Stub端作为接收请求的一端,需重写OnRemoteRequest方法用于接收客户端调用。
```
```c++
#include "iability_test.h"
#include "iremote_stub.h"
class TestAbilityStub : public IRemoteStub<ITestAbility> {
public:
public:
virtual int OnRemoteRequest(uint32_t code, MessageParcel &data, MessageParcel &reply, MessageOption &option) override;
int TestPingAbility(const std::u16string &dummy) override;
};
int TestServiceStub::OnRemoteRequest(uint32_t code,
int TestAbilityStub::OnRemoteRequest(uint32_t code,
MessageParcel &data, MessageParcel &reply, MessageOption &option)
{
switch (code) {
......@@ -61,8 +94,11 @@ IPC/RPC的主要工作是让运行在不同进程的Proxy和Stub互相通信,
}
```
3. 定义服务端业务函数具体实现类TestAbility
```
4. 定义服务端业务函数具体实现类TestAbility
```c++
#include "iability_server_test.h"
class TestAbility : public TestAbilityStub {
public:
int TestPingAbility(const std::u16string &dummy);
......@@ -73,15 +109,19 @@ IPC/RPC的主要工作是让运行在不同进程的Proxy和Stub互相通信,
}
```
4. 定义和实现客户端 TestAbilityProxy
5. 定义和实现客户端 TestAbilityProxy
该类是Proxy端实现,继承IRemoteProxy&lt;ITestAbility&gt;,调用SendRequest接口向Stub端发送请求,对外暴露服务端提供的能力。
```
```c++
#include "iability_test.h"
#include "iremote_proxy.h"
#include "iremote_object.h"
class TestAbilityProxy : public IRemoteProxy<ITestAbility> {
public:
explicit TestAbilityProxy(const sptr<IRemoteObject> &impl);
int TestPingService(const std::u16string &dummy) override;
int TestPingAbility(const std::u16string &dummy) override;
private:
static inline BrokerDelegator<TestAbilityProxy> delegator_; // 方便后续使用iface_cast宏
}
......@@ -91,21 +131,21 @@ IPC/RPC的主要工作是让运行在不同进程的Proxy和Stub互相通信,
{
}
int TestAbilityProxy::TestPingService(const std::u16string &dummy){
int TestAbilityProxy::TestPingAbility(const std::u16string &dummy){
MessageOption option;
MessageParcel dataParcel, replyParcel;
dataParcel.WriteString16(dummy);
int error = Remote()->SendRequest(TRANS_ID_PING_ABILITY, dataParcel, replyParcel, option);
int result = (error == ERR_NONE) ? replyParcel.ReadInt32() : -1;
return result;
}
}
```
5. SA注册与启动
6. SA注册与启动
SA需要将自己的TestAbilityStub实例通过AddSystemAbility接口注册到SystemAbilityManager,设备内与分布式的注册参数不同。
```
```c++
// 注册到本设备内
auto samgr = SystemAbilityManagerClient::GetInstance().GetSystemAbilityManager();
samgr->AddSystemAbility(saId, new TestAbility());
......@@ -117,11 +157,11 @@ IPC/RPC的主要工作是让运行在不同进程的Proxy和Stub互相通信,
int result = samgr->AddSystemAbility(saId, new TestAbility(), saExtra);
```
6. SA获取与调用
7. SA获取与调用
通过SystemAbilityManager的GetSystemAbility方法可获取到对应SA的代理IRemoteObject,然后构造TestAbilityProxy即可。
```
```c++
// 获取本设备内注册的SA的proxy
sptr<ISystemAbilityManager> samgr = SystemAbilityManagerClient::GetInstance().GetSystemAbilityManager();
sptr<IRemoteObject> remoteObject = samgr->GetSystemAbility(saId);
......@@ -129,7 +169,149 @@ IPC/RPC的主要工作是让运行在不同进程的Proxy和Stub互相通信,
// 获取其他设备注册的SA的proxy
sptr<ISystemAbilityManager> samgr = SystemAbilityManagerClient::GetInstance().GetSystemAbilityManager();
sptr<IRemoteObject> remoteObject = samgr->GetSystemAbility(saId, deviceId); // deviceId是指定设备的标识符
// networkId是组网场景下对应设备的标识符,可以通过GetLocalNodeDeviceInfo获取
sptr<IRemoteObject> remoteObject = samgr->GetSystemAbility(saId, networkId);
sptr<TestAbilityProxy> proxy(new TestAbilityProxy(remoteObject)); // 直接构造具体Proxy
```
### **JS侧开发步骤**
1. 添加依赖
```ts
import rpc from "@ohos.rpc"
import featureAbility from "@ohos.ability.featureAbility"
```
2. 绑定Ability
首先,构造变量want,指定要绑定的Ability所在应用的包名、组件名,如果是跨设备的场景,还需要绑定目标设备NetworkId(组网场景下对应设备的标识符,可以使用deviceManager获取目标设备的NetworkId);然后,构造变量connect,指定绑定成功、绑定失败、断开连接时的回调函数;最后,使用featureAbility提供的接口绑定Ability。
```ts
import rpc from "@ohos.rpc"
import featureAbility from "@ohos.ability.featureAbility"
let proxy = null
let connectId = null
// 单个设备绑定Ability
let want = {
// 包名和组件名写实际的值
"bundleName": "ohos.rpc.test.server",
"abilityName": "ohos.rpc.test.server.ServiceAbility",
}
let connect = {
onConnect:function(elementName, remote) {
proxy = remote
},
onDisconnect:function(elementName) {
},
onFailed:function() {
proxy = null
}
}
connectId = featureAbility.connectAbility(want, connect)
// 如果是跨设备绑定,可以使用deviceManager获取目标设备NetworkId
import deviceManager from '@ohos.distributedHardware.deviceManager'
function deviceManagerCallback(deviceManager) {
let deviceList = deviceManager.getTrustedDeviceListSync()
let networkId = deviceList[0].networkId
let want = {
"bundleName": "ohos.rpc.test.server",
"abilityName": "ohos.rpc.test.service.ServiceAbility",
"networkId": networkId,
"flags": 256
}
connectId = featureAbility.connectAbility(want, connect)
}
// 第一个参数是本应用的包名,第二个参数是接收deviceManager的回调函数
deviceManager.createDeviceManager("ohos.rpc.test", deviceManagerCallback)
```
3. 服务端处理客户端请求
服务端被绑定的Ability在onConnect方法里返回继承自rpc.RemoteObject的对象,该对象需要实现onRemoteMessageRequest方法,处理客户端的请求。
```ts
onConnect(want: Want) {
var robj:rpc.RemoteObject = new Stub("rpcTestAbility")
return robj
}
class Stub extends rpc.RemoteObject {
constructor(descriptor) {
super(descriptor)
}
onRemoteMessageRequest(code, data, reply, option) {
// 根据code处理客户端的请求
return true
}
}
```
4. 客户端处理服务端响应
客户端在onConnect回调里接收到代理对象,调用sendRequestAsync方法发起请求,在期约(JavaScript期约:用于表示一个异步操作的最终完成或失败及其结果值)或者回调函数里接收结果。
```ts
// 使用期约
let option = new rpc.MessageOption()
let data = rpc.MessageParcel.create()
let reply = rpc.MessageParcel.create()
// 往data里写入参数
proxy.sendRequestAsync(1, data, reply, option)
.then(function(result) {
if (result.errCode != 0) {
console.error("send request failed, errCode: " + result.errCode)
return
}
// 从result.reply里读取结果
})
.catch(function(e) {
console.error("send request got exception: " + e)
}
.finally(() => {
data.reclaim()
reply.reclaim()
})
// 使用回调函数
function sendRequestCallback(result) {
try {
if (result.errCode != 0) {
console.error("send request failed, errCode: " + result.errCode)
return
}
// 从result.reply里读取结果
} finally {
result.data.reclaim()
result.reply.reclaim()
}
}
let option = new rpc.MessageOption()
let data = rpc.MessageParcel.create()
let reply = rpc.MessageParcel.create()
// 往data里写入参数
proxy.sendRequest(1, data, reply, option, sendRequestCallback)
```
5. 断开连接
IPC通信结束后,使用featureAbility的接口断开连接。
```ts
import rpc from "@ohos.rpc"
import featureAbility from "@ohos.ability.featureAbility"
function disconnectCallback() {
console.info("disconnect ability done")
}
featureAbility.disconnectAbility(connectId, disconnectCallback)
```
......@@ -3,13 +3,33 @@
## 基本概念
IPC(Inter-Process Communication)与RPC(Remote Procedure Call)机制用于实现跨进程通信,不同的是前者使用Binder驱动,用于设备内的跨进程通信,而后者使用软总线驱动,用于跨设备跨进程通信。IPC和RPC通常采用客户端-服务器(Client-Server)模型,服务请求方(Client)可获取提供服务提供方(Server)的代理 (Proxy),并通过此代理读写数据来实现进程间的数据通信。通常,Server会先注册系统能力(System Ability)到系统能力管理者(System Ability Manager,缩写SAMgr)中,SAMgr负责管理这些SA并向Client提供相关的接口。Client要和某个具体的SA通信,必须先从SAMgr中获取该SA的代理,然后使用代理和SA通信。下文使用Proxy表示服务请求方,Stub表示服务提供方。
IPC(Inter-Process Communication)与RPC(Remote Procedure Call)用于实现跨进程通信,不同的是前者使用Binder驱动,用于设备内的跨进程通信,后者使用软总线驱动,用于跨设备跨进程通信。需要跨进程通信的原因是因为每个进程都有自己独立的资源和内存空间,其他进程不能随意访问不同进程的内存和资源,IPC/RPC便是为了突破这一点。IPC和RPC通常采用客户端-服务器(Client-Server)模型,在使用时,请求服务的(Client)一端进程可获取提供服务(Server)一端所在进程的代理(Proxy),并通过此代理读写数据来实现进程间的数据通信,更具体的讲,首先请求服务的(Client)一端会建立一个服务提供端(Server)的代理对象,这个代理对象具备和服务提供端(Server)一样的功能,若想访问服务提供端(Server)中的某一个方法,只需访问代理对象中对应的方法即可,代理对象会将请求发送给服务提供端(Server);然后服务提供端(Server)处理接受到的请求,处理完之后通过驱动返回处理结果给代理对象;最后代理对象将请求结果进一步返回给请求服务端(Client)。通常,Server会先注册系统能力(System Ability)到系统能力管理者(System Ability Manager,缩写SAMgr)中,SAMgr负责管理这些SA并向Client提供相关的接口。Client要和某个具体的SA通信,必须先从SAMgr中获取该SA的代理,然后使用代理和SA通信。下文直接使用Proxy表示服务请求方,Stub表示服务提供方。
![IPC&RPC通信机制](figures/075sd302-aeb9-481a-bb8f-e552sdb61ead.PNG)
## 约束与限制
单个设备上跨进程通信时,传输的数据量最大约为1MB,过大的数据量请使用匿名共享内存。
不支持把跨设备的Proxy对象传递回该Proxy对象所指向的Stub对象所在的设备。
- 单个设备上跨进程通信时,传输的数据量最大约为1MB,过大的数据量请使用[匿名共享内存](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis/js-apis-rpc.md#ashmem8)
- 不支持在RPC中订阅匿名Stub对象(没有向SAMgr注册Stub对象)的死亡通知。
- 不支持把跨设备的Proxy对象传递回该Proxy对象所指向的Stub对象所在的设备,即指向远端设备Stub的Proxy对象不能在本设备内进行二次跨进程传递。
## 使用建议
首先,需要编写接口类,接口类中必须定义消息码,供通信双方标识操作,可以有未实现的的方法,因为通信双方均需继承该接口类且双方不能是抽象类,所以此时定义的未实现的方法必须在双方继承时给出实现,这保证了继承双方不是抽象类。然后,需要编写Stub端相关类及其接口,并且实现AsObject方法及OnRemoteRequest方法。同时,也需要编写Proxy端,实现接口类中的方法和AsObject方法,也可以封装一些额外的方法用于调用SendRequest向对端发送数据。以上三者都具备后,便可以向SAMgr注册SA了,此时的注册应该在Stub所在进程完成。最后,在需要的地方从SAMgr中获取Proxy,便可通过Proxy实现与Stub的跨进程通信了。
相关步骤:
- 实现接口类:需继承IRemoteBroker,需定义消息码,可声明不在此类实现的方法。
- 实现服务提供端(Stub):需继承IRemoteStub或者RemoteObject,需重写AsObject方法及OnRemoteRequest方法。
- 实现服务请求端(Proxy):需继承IRemoteProxy或RemoteProxy,需重写AsObject方法,封装所需方法调用SendRequest。
- 注册SA:申请SA的唯一ID,向SAMgr注册SA。
- 获取SA:通过SA的ID和设备ID获取Proxy,使用Proxy与远端通信
## 相关模块
......
# 远端状态订阅开发实例
IPC/RPC提供对远端Stub对象状态的订阅机制, 在远端Stub对象死亡时,可触发死亡通知告诉本地Proxy对象。这种状态通知订阅需要调用特定接口完成,当不再需要订阅时也需要调用特定接口取消。使用这种订阅机制的用户,需要实现死亡通知接口DeathRecipient并实现onRemoteDied方法清理资源。该方法会在远端Stub对象所在进程死亡或所在设备离开组网时被回调。值得注意的是,调用这些接口有一定的顺序。首先,需要Proxy订阅Stub死亡通知,若在订阅期间Stub状态正常,则在不再需要时取消订阅;若在订阅期间Stub所在进程退出或者所在设备退出组网,则会自动触发Proxy自定义的后续操作。
IPC/RPC提供对远端Stub对象状态的订阅机制, 在远端Stub对象消亡时,可触发消亡通知告诉本地Proxy对象。这种状态通知订阅需要调用特定接口完成,当不再需要订阅时也需要调用特定接口取消。使用这种订阅机制的用户,需要实现消亡通知接口DeathRecipient并实现onRemoteDied方法清理资源。该方法会在远端Stub对象所在进程消亡或所在设备离开组网时被回调。值得注意的是,调用这些接口有一定的顺序。首先,需要Proxy订阅Stub消亡通知,若在订阅期间Stub状态正常,则在不再需要时取消订阅;若在订阅期间Stub所在进程退出或者所在设备退出组网,则会自动触发Proxy自定义的后续操作。
## 使用场景
这种订阅机制适用于本地Proxy对象需要感知远端Stub对象所在进程消亡,或所在设备离开组网的场景。当Proxy感知到Stub端消亡后,可适当清理本地资源。此外,RPC目前不提供匿名Stub对象的消亡通知,即只有向SAMgr注册过的服务才能被订阅消亡通知,IPC则支持匿名对象的消亡通知。
## Native侧接口
| 接口名 | 功能描述 |
| -------- | -------- |
| AddDeathRecipient(const sptr\<DeathRecipient> &recipient); | 订阅远端Stub对象状态。 |
| RemoveDeathRecipient(const sptr\<DeathRecipient> &recipient); | 取消订阅远端Stub对象状态。 |
| OnRemoteDied(const wptr\<IRemoteObject> &object); | 当远端Stub对象死亡时回调。 |
| 接口名 | 返回值类型 | 功能描述 |
| -------- | -------- | -------- |
| AddDeathRecipient(const sptr\<DeathRecipient> &recipient); | bool | 订阅远端Stub对象状态。 |
| RemoveDeathRecipient(const sptr\<DeathRecipient> &recipient); | bool | 取消订阅远端Stub对象状态。 |
| OnRemoteDied(const wptr\<IRemoteObject> &object); | void | 当远端Stub对象死亡时回调。 |
### 参考代码
## 参考代码
```C++
#include "iremote_broker.h"
#include "iremote_stub.h"
//定义消息码
enum {
TRANS_ID_PING_ABILITY = 5,
TRANS_ID_REVERSED_MONITOR
};
const std::string DESCRIPTOR = "test.ITestAbility";
class ITestService : public IRemoteBroker {
public:
// DECLARE_INTERFACE_DESCRIPTOR是必需的,入参需使用std::u16string;
DECLARE_INTERFACE_DESCRIPTOR(to_utf16(DESCRIPTOR));
virtual int TestPingAbility(const std::u16string &dummy) = 0; // 定义业务函数
};
class TestServiceProxy : public IRemoteProxy<ITestAbility> {
public:
explicit TestAbilityProxy(const sptr<IRemoteObject> &impl);
virtual int TestPingAbility(const std::u16string &dummy) override;
int TestAnonymousStub();
private:
static inline BrokerDelegator<TestAbilityProxy> delegator_; // 方便后续使用iface_cast宏
};
TestServiceProxy::TestServiceProxy(const sptr<IRemoteObject> &impl)
: IRemoteProxy<ITestAbility>(impl)
{
}
int TestServiceProxy::TestPingAbility(const std::u16string &dummy){
MessageOption option;
MessageParcel dataParcel, replyParcel;
dataParcel.WriteString16(dummy);
int error = PeerHolder::Remote()->SendRequest(TRANS_ID_PING_ABILITY, dataParcel, replyParcel, option);
int result = (error == ERR_NONE) ? replyParcel.ReadInt32() : -1;
return result;
}
```
```c++
#include "iremote_object.h"
class TestDeathRecipient : public IRemoteObject::DeathRecipient {
public:
virtual void OnRemoteDied(const wptr<IRemoteObject>& remoteObject);
}
sptr<IRemoteObject::DeathRecipient> deathRecipient (new TestDeathRecipient());// 构造一个死亡通知对象
bool result = proxy->AddDeathRecipient(deathRecipient); // 注册死亡通知
result = proxy->RemoveDeathRecipient(deathRecipient); // 移除死亡通知
void TestDeathRecipient::OnRemoteDied(const wptr<IRemoteObject>& remoteObject)
{
}
```
```c++
sptr<IPCObjectProxy> object = new IPCObjectProxy(1, to_utf16(DESCRIPTOR));
sptr<IRemoteObject::DeathRecipient> deathRecipient (new TestDeathRecipient());// 构造一个消亡通知对象
bool result = object->AddDeathRecipient(deathRecipient); // 注册消亡通知
result = object->RemoveDeathRecipient(deathRecipient); // 移除消亡通知
```
## JS侧接口
| 接口名 | 返回值类型 | 功能描述 |
| -------------------- | ---------- | ------------------------------------------------------------ |
| addDeathRecippient | boolean | 注册用于接收远程对象消亡通知的回调,增加proxy对象上的消亡通知。 |
| removeDeathRecipient | boolean | 注销用于接收远程对象消亡通知的回调。 |
| onRemoteDied | void | 在成功添加死亡通知订阅后,当远端对象死亡时,将自动调用本方法。 |
### 参考代码
```ts
import FA from "@ohos.ability.featureAbility";
let proxy;
let connect = {
onConnect: function(elementName, remoteProxy) {
console.log("RpcClient: js onConnect called.");
proxy = remoteProxy;
},
onDisconnect: function(elementName) {
console.log("RpcClient: onDisconnect");
},
onFailed: function() {
console.log("RpcClient: onFailed");
}
};
let want = {
"bundleName": "com.ohos.server",
"abilityName": "com.ohos.server.MainAbility",
};
FA.connectAbility(want, connect);
class MyDeathRecipient {
onRemoteDied() {
console.log("server died");
}
}
let deathRecipient = new MyDeathRecipient();
proxy.addDeathRecippient(deathRecipient, 0);
proxy.removeDeathRecipient(deathRecipient, 0);
```
## Stub感知Proxy消亡(匿名Stub的使用)
正向的消亡通知是Proxy感知Stub的状态,若想达到反向的死消亡通知,即Stub感知Proxy的状态,可以巧妙的利用正向消亡通知。如两个进程A(原Stub所在进程)和B(原Proxy所在进程),进程B在获取到进程A的Proxy对象后,在B进程新建一个匿名Stub对象(匿名指未向SAMgr注册),可称之为回调Stub,再通过SendRequest接口将回调Stub传给进程A的原Stub。这样一来,进程A便获取到了进程B的回调Proxy。当进程B消亡或B所在设备离开组网时,回调Stub会消亡,回调Proxy会感知,进而通知给原Stub,便实现了反向消亡通知。
注意:
> 反向死亡通知仅限设备内跨进程通信使用,不可用于跨设备。
> 当匿名Stub对象没有被任何一个Proxy指向的时候,内核会自动回收。
### 参考代码
```c++
//Proxy
int TestAbilityProxy::TestAnonymousStub()
{
MessageOption option;
MessageParcel dataParcel, replyParcel;
dataParcel.UpdateDataVersion(Remote());
dataParcel.WriteRemoteObject(new TestAbilityStub());
int error = Remote()->SendRequest(TRANS_ID_REVERSED_MONITOR,dataParcel, replyParcel, option);
int result = (error == ERR_NONE) ? replyParcel.ReadInt32() : -1;
return result;
}
//Stub
int TestAbilityStub::OnRemoteRequest(uint32_t code, MessageParcel &data, MessageParcel &reply, MessageOption &option)
{
switch (code) {
case TRANS_ID_REVERSED_MONITOR: {
sptr<IRemoteObject> obj = data.ReadRemoteObject();
if (obj == nullptr) {
reply.WriteInt32(ERR_NULL_OBJECT);
return ERR_NULL_OBJECT;
}
bool result = obj->AddDeathRecipient(new TestDeathRecipient());
result ? reply.WriteInt32(ERR_NONE) : reply.WriteInt32(-1);
break;
}
default:
break;
}
return ERR_NONE;
}
```
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册