经过几天的搜索尝试,网上很多写关于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对象来接受!