經過幾天的搜索嘗試,網上很多寫關于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類型的值。本以為大功告成,最后打印發現我數據全是空的,我理解是數據解析失敗了
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對象來接受!