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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容