Flutter中使用Dio网络请求如何解析protobuf协议格式

经过几天的搜索尝试,网上很多写关于Flutter中使用protobuf 的文章,但是点进去,几乎都是清一色的介绍怎么安装环境,然后最后一步就是在pubspec.ymal中添加protobuf: ^0.13.4依赖.或者是flutter下使用protobuf和socket与服务器通信的文章。但是现在做前段开发的估计大部分用户还是用的Dio库进行的网络请求,至少目前我未找到一篇让我接入有用的文档。也就有了我的摸索经历了。

一、背景介绍

1.市面上搜索不到满足我当前需求的flutter 中具体怎么使用PB协议文档。

需求:后台未使用gcpb框架处理pb,flutter如何使用dio库去解析一个具体的网络请求,该接口返回的是pb格式数据

2.使用PB协议的大部分公司使用了grpc框架配合使用(当时我在解决这个问题时,放弃过Dio解析pb协议的第二个选择验证方案),但是这个grpc 需要后台配合各端一起实现,因为 该方式的使用时直接制定Host 端口号就可以,如果后台没使用这个框架, 前端没法玩。

3.咨询了下熟悉这块的大佬的到回复如下:
目前官方来说不支持flutter中Dio数据解析成protobuf,就连json的处理也是官方优化后,出了插件辅助开发使用的,不过这个思路和方案闲鱼官方有实现,也有一定的思路参考,你可以借鉴一下,或者找一下看看闲鱼的开源版本是否发布了.

https://blog.csdn.net/yunqiinsight/article/details/86700217

二、结果

问题已经解决,在flutter中使用现有的Dio 3.x版本,完成了PB协议解析正确解析

三、问题分析及处理过程(走了一些弯路,可以直接看步骤四)

1.我首先想到的是在flutter中去搜索protobuf使用

按大部分文档提到的在.yaml文件中增加protobuf: ^0.13.4依赖,我直接搜索了protobuf: ^0.13.4在Flutter中使用(最新版本已经到1.1.0)

https://blog.csdn.net/importing/article/details/91565614

找到一个比较类似的讲使用socket在flutter 中与服务器通信,但是里面的Msg类和我现在通过配置环境,生成的 .pb.dart文件差距比较大。我现在生成的文件如下,因为使用公司接口,只能贴部分示例代码,我放到最后面贴出来.从这个文章中,我get 到一个有效信息,Msg.fromBuffer(XXXX) ,但是这个XXX和我现在的差的比较多,于是放弃。

2.从protobuf文档入手,既然官方文档已经支持Dart了,官网是否有使用实例
https://developers.google.com/protocol-buffers/docs

里面有个实例,但是他是从一个File中读写,与我期望的还是有差距,这里也get到部分思路,仍然要使用XXX.fromBuffer()方式 把字节流转为XXX对象,File的readAsBytesSync方法 返回的是一个Uint8List类型数据。
Uint8List:Dart中无符号的Byte(类比java中的Byte)

//使用如下方式 把字节流转为XXX对象
XXX.formBuffer(File().readAsBytesSync());

3.我现在就要知道使用Dio 库如何把返回的结果转换为Uint8List就完美了

因为我们原生项目已经有可以验证测试的接口,我开始断点Android项目中的返回,发现返回的是一个Byte类型的数组,
但是我现在使用Charles抓取Flutter 网络请求,发现数据确实有返回,但是是二进制流数据,我也看不到具体返回了什么内容。

我开始了以下尝试:

var response = await dio.post(url, data: params, options: options)
     
  PBRAdversityRsp pbdata = PBRAdversityRsp.fromBuffer(response);  
 PBRAdversityRsp pbdata = PBRAdversityRsp.fromBuffer(response.data);  

 PBRAdversityRsp pbdata = PBRAdversityRsp.fromJson(response);  
 PBRAdversityRsp pbdata = PBRAdversityRsp.fromJson(response.data);  

上面就是使用Dio网络请求,返回的结果,response的类型是:Response<dynamic> ,我尝试了上面的四种方式,PBRAdversityRsp是我从.proto文件生成的对象文件,里面提供了fromBuffer 和fromJson方法 传入的参数无论是response还是 response.data 都会提示转换类型失败如下:


4.出现上面的场景,免不了百度一堆类型转换的,再回过头对比现有的Dio返回Json数据的场景想想

如果后台返回的是Json数据格式,可以使用json.encode(response.data)直接解析为对象 ,现在后台返回格式是PB协议,类似的应该也要用PB的一个方法解析现在的返回???但是不知道怎么用 。结合1.2的实例,我更加坚信了应该是用XXX.fromBuffer(Dio返回的数据)。但是现在要怎么把Response<dynamic> 或者String 类型转为Uint8List数据格式,同时我断点也看到这个Response 有部分中文,但是有部分特殊符号。

�������.��
."操作成功*

06619644572SHB-L0134517-94.10:

第三个广告位 2b93a1178c424a01a9b304d8bdca5344Phttps://stg.iobs.pingan.com.cn/download/peimcadmin-sf-dev/160697497779818317.png"[Ghttps://paface-stg.pingan.com:10205/happy/login.html#/login?qrCode=true](Ghttps://paface-stg.pingan.com:10205/happy/login.html#/login?qrCode=true)

5.然后又进入字节编码的坑中,折腾很久发现好像也解决不了问题
6.然后进入了我最崩溃的一步:Dio官网Issue中#371:

这个问题让我get到别人遇到过类似问题,虽然他是想传入参数,我是想返回解析。当时提问题时间大概是19年7月,他的代码写法也让我比较崩溃

https://github.com/flutterchina/dio
Uint8List response = await dio.post(url, data: params, options: options); 

我这样写直接就会报错,而且他说道,跟踪代码发现dio转换器总是返回String格式,他虽然说的transferRequest方法, 我也看,发现他说的对,Dio 确实没有处理。我心里有点凉了,觉得就是Dio库没有兼容支持。我就对应看返回方法,代码比较多,我挑重点说

 @override
  Future transformResponse(
      RequestOptions options, ResponseBody response) async {
///重点1:
    if (options.responseType == ResponseType.stream) {
      return response;
    }
    ````
    var stream =
    response.stream.transform<Uint8List>(StreamTransformer.fromHandlers(
      handleData: (data, sink) {
        sink.add(data);
        if (showDownloadProgress) {
          received += data.length;
          options.onReceiveProgress(received, length);
        }
      },
    ));
    // let's keep references to the data chunks and concatenate them later
    final  chunks = <Uint8List>[];
    var finalSize = 0;
    StreamSubscription subscription = stream.listen(
          (chunk) {
        finalSize += chunk.length;
        chunks.add(chunk);
      },
    ```````
///重点2:
    if (options.responseType == ResponseType.bytes) return responseBytes;

    String responseBody;
    if (options.responseDecoder != null) {
      responseBody = options.responseDecoder(
          responseBytes, options, response..stream = null);
    } else {
      responseBody = utf8.decode(responseBytes, allowMalformed: true);
    }
    if (responseBody != null &&
        responseBody.isNotEmpty &&
        options.responseType == ResponseType.json &&
        _isJsonMime(response.headers[Headers.contentTypeHeader]?.first)) {
      if (jsonDecodeCallback != null) {
        return jsonDecodeCallback(responseBody);
      } else {
        return json.decode(responseBody);
      }
    }
    return responseBody;

上面代码按照提Issue的人的说法,确实只支持了Json去处理返回,PB确实没在源码中。

7.我又在某个论坛搜索到以下资料:
目前官方来说不支持flutter中Dio数据解析成protobuf,就连json的处理也是官方优化后,出了插件辅助开发使用的,不过这个思路和方案闲鱼官方有实现,也有一定的思路参考,你可以借鉴一下,或者找一下看看闲鱼的开源版本是否发布了.

上面的一系列,因为在2020.12.10这个时间节点,网上没有一篇讲Flutter 中使用Dio来解析PB协议的。我也差点多次放弃

最后越想越气,打算看看Dio是不是可以像安卓一样,重写,反射不用他的,实现这个PB解析,然后一步一步断点看流程,

四、问题解决

1.首先确定网络请求的头中 有content-type = protobuf

我对比了下原生之所以能请求到,和我Dart中的小区别点事它的请求头中有指定类型,于是我在我的flutter 中也加入了
dio的options中指定了,期望能返回和PB格式相关的类型,不要Response,再次失败。

2.注意上面代码中的 重点1 重点2:

有个类型,网络传输回来 首先拿到的就是流数据,只是Dio最后返回了Response ,我看到重点1处代码,直接在Dio请求的option中指定了stream类型,断点 再看异步请求后的返回数据,类型不再是Response类型,在重点2处,有个responseBytes类型,这个就是我想要的。

 Dio dio = new Dio();                                                           
 Options options = Options(headers: {                                           
   HttpHeaders.acceptHeader: "*",                                               
   HttpHeaders.contentTypeHeader:"application/x-protobuf",                      
   HttpHeaders.cookieHeader:"PAMO_SESSION=42fa969316d6481b818b1f17139dcebc_v2", 
 },                                                                             
   responseType: ResponseType.bytes,                                            
 );                                                                             

可以看到现在返回的response是一个数组,里面放的int类型的值。本以为大功告成,最后打印发现我数据全是空的,我理解是数据解析失败了


屏幕快照 2020-12-11 18.10.07.png
3.又一个弯路:对比原生

原生能解析,Flutter不能解析,拿到的response.data的数据是273个,和原生的数据数据量一致,然后我断点看了具体发现原生很多负数,因为是Byte解析的,但是Flutter中拿到的是int ,会不会就是这个问题导致解析失败???我开始研究怎么转换,说来也怪,折腾了好久才找到以下方式直接就可以转换:

Int8List.fromList(pbResultResponse.data)

让人绝望的是还是拿不到解析后的数据

4.对比原生代码,发现原生代码还有一个基础类,这个是和后台定义的,首先数据要解析为这个基础,再从基础中取data字段,再转换为我们新生成的.pb .dart文件。终于解析成功。
原始的 .proto文件

这个文件一般是 后台及各端一起定义好,比较简单的:

syntax = "proto3";
option java_package = "XXX.pb.smartCard";
option java_outer_classname = "PBAdversitVO";
option java_multiple_files = true;


message PBAdversityReq{
   int64 timestamp =1;//时间戳
}


//智能工卡广告位
message PBAdversityItem{
    string adversityName = 1;//广告名称
    string adversityId = 2;//广告ID
    string bannerUrl = 3;//banner图
    string termUrl = 4;//入口连接
}

//返回结果数据
message PBRAdversityRsp{
    int64 code          = 1; // 状态码
    string message      = 2; // 返回信息、错误提示等
    repeated PBAdversityItem    adversityList     = 3; // 返回的广告list
}

.proto文件转为.pb.dart文件

转换成.pb.dart文件后,这个文件变得比较复杂,但是结构还是比较清晰:

///
//  Generated code. Do not modify.
//  source: Adversity.proto
//
// @dart = 2.3
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields

import 'dart:core' as $core;

import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;

class PBAdversityReq extends $pb.GeneratedMessage {
  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'PBAdversityReq', createEmptyInstance: create)
    ..aInt64(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'timestamp')
    ..hasRequiredFields = false
  ;

  PBAdversityReq._() : super();
  factory PBAdversityReq() => create();
  factory PBAdversityReq.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
  factory PBAdversityReq.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
  @$core.Deprecated(
  'Using this can add significant overhead to your binary. '
  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
  'Will be removed in next major version')
  PBAdversityReq clone() => PBAdversityReq()..mergeFromMessage(this);
  @$core.Deprecated(
  'Using this can add significant overhead to your binary. '
  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
  'Will be removed in next major version')
  PBAdversityReq copyWith(void Function(PBAdversityReq) updates) => super.copyWith((message) => updates(message as PBAdversityReq)); // ignore: deprecated_member_use
  $pb.BuilderInfo get info_ => _i;
  @$core.pragma('dart2js:noInline')
  static PBAdversityReq create() => PBAdversityReq._();
  PBAdversityReq createEmptyInstance() => create();
  static $pb.PbList<PBAdversityReq> createRepeated() => $pb.PbList<PBAdversityReq>();
  @$core.pragma('dart2js:noInline')
  static PBAdversityReq getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PBAdversityReq>(create);
  static PBAdversityReq _defaultInstance;

  @$pb.TagNumber(1)
  $fixnum.Int64 get timestamp => $_getI64(0);
  @$pb.TagNumber(1)
  set timestamp($fixnum.Int64 v) { $_setInt64(0, v); }
  @$pb.TagNumber(1)
  $core.bool hasTimestamp() => $_has(0);
  @$pb.TagNumber(1)
  void clearTimestamp() => clearField(1);
}

class PBAdversityItem extends $pb.GeneratedMessage {
  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'PBAdversityItem', createEmptyInstance: create)
    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'adversityName', protoName: 'adversityName')
    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'adversityId', protoName: 'adversityId')
    ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'bannerUrl', protoName: 'bannerUrl')
    ..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'termUrl', protoName: 'termUrl')
    ..hasRequiredFields = false
  ;

  PBAdversityItem._() : super();
  factory PBAdversityItem() => create();
  factory PBAdversityItem.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
  factory PBAdversityItem.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
  @$core.Deprecated(
  'Using this can add significant overhead to your binary. '
  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
  'Will be removed in next major version')
  PBAdversityItem clone() => PBAdversityItem()..mergeFromMessage(this);
  @$core.Deprecated(
  'Using this can add significant overhead to your binary. '
  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
  'Will be removed in next major version')
  PBAdversityItem copyWith(void Function(PBAdversityItem) updates) => super.copyWith((message) => updates(message as PBAdversityItem)); // ignore: deprecated_member_use
  $pb.BuilderInfo get info_ => _i;
  @$core.pragma('dart2js:noInline')
  static PBAdversityItem create() => PBAdversityItem._();
  PBAdversityItem createEmptyInstance() => create();
  static $pb.PbList<PBAdversityItem> createRepeated() => $pb.PbList<PBAdversityItem>();
  @$core.pragma('dart2js:noInline')
  static PBAdversityItem getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PBAdversityItem>(create);
  static PBAdversityItem _defaultInstance;

  @$pb.TagNumber(1)
  $core.String get adversityName => $_getSZ(0);
  @$pb.TagNumber(1)
  set adversityName($core.String v) { $_setString(0, v); }
  @$pb.TagNumber(1)
  $core.bool hasAdversityName() => $_has(0);
  @$pb.TagNumber(1)
  void clearAdversityName() => clearField(1);

  @$pb.TagNumber(2)
  $core.String get adversityId => $_getSZ(1);
  @$pb.TagNumber(2)
  set adversityId($core.String v) { $_setString(1, v); }
  @$pb.TagNumber(2)
  $core.bool hasAdversityId() => $_has(1);
  @$pb.TagNumber(2)
  void clearAdversityId() => clearField(2);

  @$pb.TagNumber(3)
  $core.String get bannerUrl => $_getSZ(2);
  @$pb.TagNumber(3)
  set bannerUrl($core.String v) { $_setString(2, v); }
  @$pb.TagNumber(3)
  $core.bool hasBannerUrl() => $_has(2);
  @$pb.TagNumber(3)
  void clearBannerUrl() => clearField(3);

  @$pb.TagNumber(4)
  $core.String get termUrl => $_getSZ(3);
  @$pb.TagNumber(4)
  set termUrl($core.String v) { $_setString(3, v); }
  @$pb.TagNumber(4)
  $core.bool hasTermUrl() => $_has(3);
  @$pb.TagNumber(4)
  void clearTermUrl() => clearField(4);
}

class PBRAdversityRsp extends $pb.GeneratedMessage {
  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'PBRAdversityRsp', createEmptyInstance: create)
    ..aInt64(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'code')
    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'message')
    ..pc<PBAdversityItem>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'adversityList', $pb.PbFieldType.PM, protoName: 'adversityList', subBuilder: PBAdversityItem.create)
    ..hasRequiredFields = false
  ;

  PBRAdversityRsp._() : super();
  factory PBRAdversityRsp() => create();
  factory PBRAdversityRsp.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
  factory PBRAdversityRsp.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
  @$core.Deprecated(
  'Using this can add significant overhead to your binary. '
  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
  'Will be removed in next major version')
  PBRAdversityRsp clone() => PBRAdversityRsp()..mergeFromMessage(this);
  @$core.Deprecated(
  'Using this can add significant overhead to your binary. '
  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
  'Will be removed in next major version')
  PBRAdversityRsp copyWith(void Function(PBRAdversityRsp) updates) => super.copyWith((message) => updates(message as PBRAdversityRsp)); // ignore: deprecated_member_use
  $pb.BuilderInfo get info_ => _i;
  @$core.pragma('dart2js:noInline')
  static PBRAdversityRsp create() => PBRAdversityRsp._();
  PBRAdversityRsp createEmptyInstance() => create();
  static $pb.PbList<PBRAdversityRsp> createRepeated() => $pb.PbList<PBRAdversityRsp>();
  @$core.pragma('dart2js:noInline')
  static PBRAdversityRsp getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PBRAdversityRsp>(create);
  static PBRAdversityRsp _defaultInstance;

  @$pb.TagNumber(1)
  $fixnum.Int64 get code => $_getI64(0);
  @$pb.TagNumber(1)
  set code($fixnum.Int64 v) { $_setInt64(0, v); }
  @$pb.TagNumber(1)
  $core.bool hasCode() => $_has(0);
  @$pb.TagNumber(1)
  void clearCode() => clearField(1);

  @$pb.TagNumber(2)
  $core.String get message => $_getSZ(1);
  @$pb.TagNumber(2)
  set message($core.String v) { $_setString(1, v); }
  @$pb.TagNumber(2)
  $core.bool hasMessage() => $_has(1);
  @$pb.TagNumber(2)
  void clearMessage() => clearField(2);

  @$pb.TagNumber(3)
  $core.List<PBAdversityItem> get adversityList => $_getList(2);
}

我现在要接受到网络请求返回的数据 需要转为PBRAdversityRsp对象来接受!

https://github.com/flutterchina/dio/issues/371

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容