鸿蒙HarmonyOS 开发如果实现多端协同?

多端协同流程

多端协同流程如下图所示。

图1 多端协同流程图

约束限制

  • 由于“多端协同任务管理”能力尚未具备,开发者当前只能通过开发系统应用获取设备列表,不支持三方应用接入。

  • 多端协同需遵循 分布式跨设备组件启动规则 。

  • 为了获得最佳体验,使用want传输的数据建议在100KB以下。

通过跨设备启动UIAbility和ServiceExtensionAbility组件实现多端协同(无返回数据)

在设备A上通过发起端应用提供的启动按钮,启动设备B上指定的UIAbility与ServiceExtensionAbility。

接口说明

表1 跨设备启动API接口功能介绍

接口名 描述
startAbility(want: Want, callback: AsyncCallback<void>): void; 启动UIAbility和ServiceExtensionAbility(callback形式)。
stopServiceExtensionAbility(want: Want, callback: AsyncCallback<void>): void; 退出启动的ServiceExtensionAbility(callback形式)。
stopServiceExtensionAbility(want: Want): Promise<void>; 退出启动的ServiceExtensionAbility(Promise形式)。

开发步骤

  1. 需要申请ohos.permission.DISTRIBUTED_DATASYNC权限,配置方式请参见 配置文件权限声明 。

  2. 同时需要在应用首次启动时弹窗向用户申请授权,使用方式请参见 向用户申请授权 。

  3. 获取目标设备的设备ID。

   import deviceManager from '@ohos.distributedDeviceManager';

   let dmClass: deviceManager.DeviceManager;
   function initDmClass() {
        // 其中createDeviceManager接口为系统API
        try{
            dmClass = deviceManager.createDeviceManager('ohos.samples.demo');
        } catch(err) {
            console.error("createDeviceManager err: " + JSON.stringify(err));
        }
   }
   function getRemoteDeviceId(): string|undefined {
       if (typeof dmClass === 'object' && dmClass !== null) {
           let list = dmClass.getAvailableDeviceListSync();
           if (typeof (list) === 'undefined'||typeof (list.length) === 'undefined') {
                console.info('getRemoteDeviceId err: list is null');
                return;
           }
           if (list.length === 0) {
               console.info("getRemoteDeviceId err: list is empty");
               return;
           }
        return list[0].networkId;
       } else {
           console.info('getRemoteDeviceId err: dmClass is null');
           return;
       }
   }
  1. 设置目标组件参数,调用startAbility()接口,启动UIAbility或ServiceExtensionAbility。
   import { BusinessError } from '@ohos.base';
   import Want from '@ohos.app.ability.Want';
   let want: Want = {
       deviceId: getRemoteDeviceId(),
       bundleName: 'com.example.myapplication',
       abilityName: 'EntryAbility',
       moduleName: 'entry', // moduleName非必选
   }
   // context为发起端UIAbility的AbilityContext
   this.context.startAbility(want).then(() => {
       // ...
   }).catch((err: BusinessError) => {
       // ...
       console.error("startAbility err: " + JSON.stringify(err));
   })
  1. 当设备A发起端应用不需要设备B上的ServiceExtensionAbility时,可调用stopServiceExtensionAbility 接口退出。(该接口不支持UIAbility的退出,UIAbility由用户手动通过任务管理退出)
   import Want from '@ohos.app.ability.Want';
   import { BusinessError } from '@ohos.base';
   let want: Want = {
       deviceId: getRemoteDeviceId(),
       bundleName: 'com.example.myapplication',
       abilityName: 'FuncAbility',
       moduleName: 'module1', // moduleName非必选
   }
   // 退出由startAbility接口启动的ServiceExtensionAbility
   this.context.stopServiceExtensionAbility(want).then(() => {
       console.info("stop service extension ability success")
   }).catch((err: BusinessError) => {
       console.info("stop service extension ability err is " + JSON.stringify(err))
   })

通过跨设备启动UIAbility组件实现多端协同(获取返回数据)

在设备A上通过应用提供的启动按钮,启动设备B上指定的UIAbility,当设备B上的UIAbility退出后,会将返回值发回设备A上的发起端应用。

接口说明

表2 跨设备启动,返回结果数据API接口功能描述

接口名 描述
startAbilityForResult(want: Want, callback: AsyncCallback<AbilityResult>): void; 启动UIAbility并在该Ability退出的时候返回执行结果(callback形式)。
terminateSelfWithResult(parameter: AbilityResult, callback: AsyncCallback<void>): void; 停止UIAbility,配合startAbilityForResult使用,返回给接口调用方AbilityResult信息(callback形式)。
terminateSelfWithResult(parameter: AbilityResult): Promise<void>; 停止UIAbility,配合startAbilityForResult使用,返回给接口调用方AbilityResult信息(promise形式)。

开发步骤

  1. 需要申请ohos.permission.DISTRIBUTED_DATASYNC权限,配置方式请参见 配置文件权限声明 。

  2. 同时需要在应用首次启动时弹窗向用户申请授权,使用方式请参见 向用户申请授权。

  3. 在发起端设置目标组件参数,调用startAbilityForResult()接口启动目标端UIAbility,异步回调中的data用于接收目标端UIAbility停止自身后返回给调用方UIAbility的信息。getRemoteDeviceId方法参照 通过跨设备启动uiability和serviceextensionability组件实现多端协同无返回数据 。

   import AbilityConstant from '@ohos.app.ability.AbilityConstant';
   import common from '@ohos.app.ability.common';
   import { BusinessError } from '@ohos.base';
   import Want from '@ohos.app.ability.Want';
   @Entry
   @Component
   struct PageName {
      private context = getContext(this) as common.UIAbilityContext;
      build() {
        // ...
        Button('StartAbilityForResult')
          .onClick(()=>{
           let want: Want = {
               deviceId: getRemoteDeviceId(),
               bundleName: 'com.example.myapplication',
               abilityName: 'FuncAbility',
               moduleName: 'module1', // moduleName非必选
           }
           // context为发起端UIAbility的AbilityContext
           this.context.startAbilityForResult(want).then((data) => {
               // ...
           }).catch((error: BusinessError) => {
               console.info("startAbilityForResult err: " + JSON.stringify(error));
           })
          }
        )
      }
   }
  1. 在目标端UIAbility任务完成后,调用terminateSelfWithResult()方法,将数据返回给发起端的UIAbility。
   import { BusinessError } from '@ohos.base';
   import common from '@ohos.app.ability.common';
   @Entry
   @Component
   struct PageName {
      private context = getContext(this) as common.UIAbilityContext;
      build() {
        // ...
        Button('terminateSelfWithResult')
          .onClick(()=>{
               const RESULT_CODE: number = 1001;
               // context为目标端UIAbility的AbilityContext
               this.context.terminateSelfWithResult(
                {
                   resultCode: RESULT_CODE,
                   want: {
                       bundleName: 'com.example.myapplication',
                       abilityName: 'FuncAbility',
                       moduleName: 'module1',
                   },
               },
               (err: BusinessError) => {
                   // ...
                   console.info("terminateSelfWithResult err: " + JSON.stringify(err));
               });
          }
        // ...
        )
      }
   }
  1. 发起端UIAbility接收到目标端UIAbility返回的信息,对其进行处理。
   import common from '@ohos.app.ability.common';
   import { BusinessError } from '@ohos.base';
   import Want from '@ohos.app.ability.Want';
   @Entry
   @Component
   struct PageName {
      private context = getContext(this) as common.UIAbilityContext;
      build() {
        // ...
        Button('StartAbilityForResult')
          .onClick(()=>{
            let want: Want = {
                deviceId: getRemoteDeviceId(),
                bundleName: 'com.example.myapplication',
                abilityName: 'FuncAbility',
                moduleName: 'module1', // moduleName非必选
            }
            const RESULT_CODE: number = 1001;
            // ...
            // context为调用方UIAbility的UIAbilityContext
            this.context.startAbilityForResult(want).then((data) => {
                if (data?.resultCode === RESULT_CODE) {
                    // 解析目标端UIAbility返回的信息
                    let info = data.want?.parameters?.info;
                    // ...
                }
            }).catch((error: BusinessError) => {
                // ...
            })
          }
        )
      }
   }

通过跨设备连接ServiceExtensionAbility组件实现多端协同

系统应用可以通过 connectServiceExtensionAbility() 跨设备连接一个服务,实现跨设备远程调用。比如:分布式游戏场景,平板作为遥控器,智慧屏作为显示器。

接口说明

表3 跨设备连接API接口功能介绍

接口名 描述
connectServiceExtensionAbility(want: Want, options: ConnectOptions): number; 连接ServiceExtensionAbility。
disconnectServiceExtensionAbility(connection: number, callback:AsyncCallback<void>): void; 断开连接(callback形式)。
disconnectServiceExtensionAbility(connection: number): Promise<void>; 断开连接(promise形式)。

开发步骤

  1. 需要申请ohos.permission.DISTRIBUTED_DATASYNC权限,配置方式请参见配置文件权限声明

  2. 同时需要在应用首次启动时弹窗向用户申请授权,使用方式请参见向用户申请授权

  3. 如果已有后台服务,请直接进入下一步;如果没有,则 实现一个后台服务 。

  4. 连接一个后台服务。

    • 实现IAbilityConnection接口。IAbilityConnection提供了以下方法供开发者实现:onConnect()是用来处理连接Service成功的回调,onDisconnect()是用来处理Service异常终止的回调,onFailed()是用来处理连接Service失败的回调。
    • 设置目标组件参数,包括目标设备ID、Bundle名称、Ability名称。
    • 调用connectServiceExtensionAbility发起连接。
    • 连接成功,收到目标设备返回的服务句柄。
    • 进行跨设备调用,获得目标端服务返回的结果。
      import rpc from '@ohos.rpc';
      import Want from '@ohos.app.ability.Want';
      import common from '@ohos.app.ability.common';
      import { BusinessError } from '@ohos.base';
      @Entry
      @Component
      struct PageName {
          private context = getContext(this) as common.UIAbilityContext;
          build() {
            // ...
            Button('connectServiceExtensionAbility')
              .onClick(()=>{
                const REQUEST_CODE = 99;
                let want: Want = {
                    "deviceId": getRemoteDeviceId(),
                    "bundleName": "com.example.myapplication",
                    "abilityName": "ServiceExtAbility"
                };
                // 建立连接后返回的Id需要保存下来,在解绑服务时需要作为参数传入
                let connectionId = this.context.connectServiceExtensionAbility(want,
                {
                    onConnect(elementName, remote) {
                        console.info('onConnect callback');
                        if (remote === null) {
                            console.info(`onConnect remote is null`);
                            return;
                        }
                        let option = new rpc.MessageOption();
                        let data = new rpc.MessageSequence();
                        let reply = new rpc.MessageSequence();
                        data.writeInt(1);
                        data.writeInt(99);  // 开发者可发送data到目标端应用进行相应操作
                        // @param code 表示客户端发送的服务请求代码。
                        // @param data 表示客户端发送的{@link MessageSequence}对象。
                        // @param reply 表示远程服务发送的响应消息对象。
                        // @param options 指示操作是同步的还是异步的。
                        //
                        // @return 如果操作成功返回{@code true}; 否则返回 {@code false}。
                        remote.sendMessageRequest(REQUEST_CODE, data, reply, option).then((ret: rpc.RequestResult) => {
                            let msg = reply.readInt();   // 在成功连接的情况下,会收到来自目标端返回的信息(100)
                            console.info(`sendRequest ret:${ret} msg:${msg}`);
                        }).catch((error: BusinessError) => {
                            console.info('sendRequest failed');
                        });
                    },
                    onDisconnect(elementName) {
                        console.info('onDisconnect callback');
                    },
                    onFailed(code) {
                        console.info('onFailed callback');
                    }
                });
              })
          }
      }

getRemoteDeviceId方法参照 通过跨设备启动uiability和serviceextensionability组件实现多端协同无返回数据 。

  1. 断开连接。调用disconnectServiceExtensionAbility()断开与后台服务的连接。
   import common from '@ohos.app.ability.common';
   import { BusinessError } from '@ohos.base';
   @Entry
   @Component
   struct PageName {
       private context = getContext(this) as common.UIAbilityContext;
       build() {
         // ...
         Button('disconnectServiceExtensionAbility')
           .onClick(()=>{
                let connectionId: number = 1 // 在通过connectServiceExtensionAbility绑定服务时返回的Id
                this.context.disconnectServiceExtensionAbility(connectionId).then(() => {
                    console.info('disconnectServiceExtensionAbility success');
                }).catch((error: BusinessError) => {
                    console.error('disconnectServiceExtensionAbility failed');
                })
           })
       }
   }

通过跨设备Call调用实现多端协同

跨设备Call调用的基本原理与设备内Call调用相同,请参见 通过Call调用实现UIAbility交互(仅对系统应用开放)。

下面介绍跨设备Call调用实现多端协同的方法。

接口说明

表4 Call API接口功能介绍

接口名 描述
startAbilityByCall(want: Want): Promise<Caller>; 启动指定UIAbility至前台或后台,同时获取其Caller通信接口,调用方可使用Caller与被启动的Ability进行通信。
on(method: string, callback: CalleeCallBack): void 通用组件Callee注册method对应的callback方法。
off(method: string): void 通用组件Callee解注册method的callback方法。
call(method: string, data: rpc.Parcelable): Promise<void> 向通用组件Callee发送约定序列化数据。
callWithResult(method: string, data: rpc.Parcelable): Promise<rpc.MessageSequence> 向通用组件Callee发送约定序列化数据, 并将Callee返回的约定序列化数据带回。
release(): void 释放通用组件的Caller通信接口。
on(type: “release”, callback: OnReleaseCallback): void 注册通用组件通信断开监听通知。

开发步骤

  1. 需要申请ohos.permission.DISTRIBUTED_DATASYNC权限,配置方式请参见 配置文件权限声明 。

  2. 同时需要在应用首次启动时弹窗向用户申请授权,使用方式请参见 向用户申请授权 。

  3. 创建被调用端UIAbility。 被调用端UIAbility需要实现指定方法的数据接收回调函数、数据的序列化及反序列化方法。在需要接收数据期间,通过on接口注册监听,无需接收数据时通过off接口解除监听。

  4. 配置UIAbility的启动模式。 配置module.json5,将CalleeAbility配置为单实例”singleton”。

Json字段 字段说明
“launchType” Ability的启动模式,设置为”singleton”类型。

UIAbility配置标签示例如下:

     "abilities":[{
         "name": ".CalleeAbility",
         "srcEntry": "./ets/CalleeAbility/CalleeAbility.ts",
         "launchType": "singleton",
         "description": "$string:CalleeAbility_desc",
         "icon": "$media:icon",
         "label": "$string:CalleeAbility_label",
         "exported": true
     }]

  1. 导入UIAbility模块。
         import UIAbility from '@ohos.app.ability.UIAbility';
  1. 定义约定的序列化数据。 调用端及被调用端发送接收的数据格式需协商一致,如下示例约定数据由number和string组成。
         import rpc from '@ohos.rpc'
         class MyParcelable {
             num: number = 0;
             str: string = "";

             constructor(num: number, string: string) {
                 this.num = num;
                 this.str = string;
             }

             marshalling(messageSequence: rpc.MessageSequence) {
                 messageSequence.writeInt(this.num);
                 messageSequence.writeString(this.str);
                 return true;
             }

             unmarshalling(messageSequence: rpc.MessageSequence) {
                 this.num = messageSequence.readInt();
                 this.str = messageSequence.readString();
                 return true;
             }
         }
  1. 实现Callee.on监听及Callee.off解除监听。 如下示例在Ability的onCreate注册MSG_SEND_METHOD监听,在onDestroy取消监听,收到序列化数据后作相应处理并返回。应用开发者根据实际业务需要做相应处理。
         import rpc from '@ohos.rpc';
         import Want from '@ohos.app.ability.Want';
         import UIAbility from '@ohos.app.ability.UIAbility';
         import AbilityConstant from '@ohos.app.ability.AbilityConstant';
         const TAG: string = '[CalleeAbility]';
         const MSG_SEND_METHOD: string = 'CallSendMsg';

         function sendMsgCallback(data: rpc.MessageSequence): MyParcelable {
             console.info('CalleeSortFunc called');

             // 获取Caller发送的序列化数据
             let receivedData: MyParcelable = new MyParcelable(0, '');
             data.readParcelable(receivedData);
             console.info(`receiveData[${receivedData.num}, ${receivedData.str}]`);

             // 作相应处理
             // 返回序列化数据result给Caller
             return new MyParcelable(Number(receivedData.num) + 1, `send ${receivedData.str} succeed`);
         }

         export default class CalleeAbility extends UIAbility {
             onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
                 try {
                     this.callee.on(MSG_SEND_METHOD, sendMsgCallback);
                 } catch (error) {
                     console.info(`${MSG_SEND_METHOD} register failed with error ${JSON.stringify(error)}`);
                 }
             }

             onDestroy() {
                 try {
                     this.callee.off(MSG_SEND_METHOD);
                 } catch (error) {
                     console.error(TAG, `${MSG_SEND_METHOD} unregister failed with error ${JSON.stringify(error)}`);
                 }
             }
         }
  1. 获取Caller接口,访问被调用端UIAbility。
  2. 导入UIAbility模块。
       import UIAbility from '@ohos.app.ability.UIAbility';
  1. 获取Caller通信接口。 Ability的context属性实现了startAbilityByCall方法,用于获取指定通用组件的Caller通信接口。如下示例通过this.context获取Ability实例的context属性,使用startAbilityByCall拉起Callee被调用端并获取Caller通信接口,注册Caller的onRelease和onRemoteStateChange监听。应用开发者根据实际业务需要做相应处理。
       import UIAbility, { Caller } from '@ohos.app.ability.UIAbility';
       import { BusinessError } from '@ohos.base';
       export default class EntryAbility extends UIAbility {
            // ...
            async onButtonGetRemoteCaller() {
                let caller: Caller|undefined;
                let context = this.context;

                context.startAbilityByCall({
                    deviceId: getRemoteDeviceId(),
                    bundleName: 'com.samples.CallApplication',
                    abilityName: 'CalleeAbility'
                }).then((data) => {
                    if (data != null) {
                        caller = data;
                        console.info('get remote caller success');
                        // 注册caller的release监听
                        caller.onRelease((msg) => {
                            console.info(`remote caller onRelease is called ${msg}`);
                        })
                        console.info('remote caller register OnRelease succeed');
                        // 注册caller的协同场景下跨设备组件状态变化监听通知
                        try {
                                caller.onRemoteStateChange((str) => {
                                    console.info('Remote state changed ' + str);
                                });
                            } catch (error) {
                                console.info('Caller.onRemoteStateChange catch error, error.code: ${JSON.stringify(error.code)}, error.message: ${JSON.stringify(error.message)}');
                            }
                    }
                }).catch((error: BusinessError) => {
                    console.error(`get remote caller failed with ${error}`);
                })
            }
            // ...
       }

getRemoteDeviceId方法参照 通过跨设备启动uiability和serviceextensionability组件实现多端协同无返回数据 。

  1. 向被调用端UIAbility发送约定序列化数据。
    1.向被调用端发送Parcelable数据有两种方式,一种是不带返回值,一种是获取被调用端返回的数据,method以及序列化数据需要与被调用端协商一致。如下示例调用Call接口,向Callee被调用端发送数据。
       import UIAbility, { Caller } from '@ohos.app.ability.UIAbility';
       import { BusinessError } from '@ohos.base';
       const MSG_SEND_METHOD: string = 'CallSendMsg';
       export default class EntryAbility extends UIAbility {
        // ...
        caller: Caller|undefined;
        async onButtonCall() {
            try {
                let msg: MyParcelable = new MyParcelable(1, 'origin_Msg');
                if (this.caller) {
                    await this.caller.call(MSG_SEND_METHOD, msg);
                }
            } catch (error) {
                console.info(`caller call failed with ${error}`);
            }
        }
        // ...
       }
  1. 如下示例调用CallWithResult接口,向Callee被调用端发送待处理的数据originMsg,并将’CallSendMsg’方法处理完毕的数据赋值给backMsg。
        import UIAbility, { Caller } from '@ohos.app.ability.UIAbility';
        import rpc from '@ohos.rpc';
        const MSG_SEND_METHOD: string = 'CallSendMsg';
        let originMsg: string = '';
        let backMsg: string = '';
        export default class EntryAbility extends UIAbility {
            // ...
            caller: Caller|undefined;
            async onButtonCallWithResult(originMsg: string, backMsg: string) {
                try {
                    let msg: MyParcelable = new MyParcelable(1, originMsg);
                    if (this.caller) {
                        const data = await this.caller.callWithResult(MSG_SEND_METHOD, msg);
                        console.info('caller callWithResult succeed');
                        let result: MyParcelable = new MyParcelable(0, '');
                        data.readParcelable(result);
                        backMsg = result.str;
                        console.info(`caller result is [${result.num}, ${result.str}]`);
                    }
                } catch (error) {
                    console.info(`caller callWithResult failed with ${error}`);
                }
            }
            // ...
        }
  1. 释放Caller通信接口。 Caller不再使用后,应用开发者可以通过release接口释放Caller。
   import UIAbility, { Caller } from '@ohos.app.ability.UIAbility';
   export default class EntryAbility extends UIAbility {
    caller: Caller|undefined;
    releaseCall() {
        try {
            if (this.caller) {
                this.caller.release();
                this.caller = undefined;
            }
            console.info('caller release succeed');
        } catch (error) {
            console.info(`caller release failed with ${error}`);
        }
    }
   }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容