目录 1. 文件操做 2. Json转Model 3. HttpClient 4. dio三方库 5. Http分块下载 6. 使用WebSockets 7. 使用Socket API(dart:io包中) 8. http三方库
1. 文件操做html
不管是Flutter仍是DartVM下的脚本(系统路径不一样,Dart VM运行在PC或服务器操做系统下,Flutter运行在移动操做系统下)都经过Dart IO库来操做文件。android
在实际开发中,若是存储一些简单的数据,推荐使用shared_preferences插件。git
获取目录位置github
Android和iOS的应用存储目录不一样,PathProvider三方插件提供了一种平台透明的方式来访问设备文件系统上的经常使用位置。web
1. 临时目录: getTemporaryDirectory() 系统可随时清除的临时目录(存放缓存文件)。 在iOS上,这对应于NSTemporaryDirectory() 返回的值。 在Android上,这是getCacheDir())返回的值。 2. 文档目录: getApplicationDocumentsDirectory() 当应用程序被卸载时,系统才会清除该目录。 在iOS上,这对应于NSDocumentDirectory。 在Android上,这是AppData目录。 3. 外部存储目录(SD卡):getExternalStorageDirectory(); 在iOS下调用该方法会抛出UnsupportedError异常(iOS不支持外部目录)。 在Android下结果是android SDK中getExternalStorageDirectory的返回值。
例算法
一个计数器,应用退出重启后能够恢复点击次数。shell
1. 引入PathProvider插件;在pubspec.yaml文件中添加以下声明: path_provider: ^0.4.1 添加后,执行flutter packages get 获取一下 2.完整代码以下: import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; import 'dart:async'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.yellow, visualDensity: VisualDensity.adaptivePlatformDensity, ), home:MyHomePage(), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter; @override void initState() { super.initState(); _readCounter().then((int value) { setState(() { _counter = value; }); }); } // 从文件读取点击次数 Future<int> _readCounter() async { try { File file = await _getLocalFile(); // 读取点击次数(以字符串) String contents = await file.readAsString(); return int.parse(contents); } on FileSystemException { return 0; } } // 获取文件 Future<File> _getLocalFile() async { // 获取应用的Documents目录 String dir = (await getApplicationDocumentsDirectory()).path; return new File('$dir/counter.txt'); } // 点击按钮后自增,并将点击次数以字符串类型写到文件中 Future<Null> _incrementCounter() async { setState(() { _counter++; }); await (await _getLocalFile()).writeAsString('$_counter'); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar(title: new Text('文件操做')), body: new Center( child: new Text('点击了 $_counter 次'), ), floatingActionButton: new FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: new Icon(Icons.add), ), ); } }
2. Json转Modelmacos
Flutter中没有像Java开发中的Gson/Jackson同样的Json序列化类库,由于这样的库须要使用运行时反射,这在Flutter中是禁用的, 因此Flutter没法实现动态转化Model的功能。运行时反射会干扰Dart的tree shaking,使用tree shaking,能够在release版中“去除”未使用的代码,这能够显著优化应用程序的大小。因为反射会默认应用到全部代码,所以tree shaking会很难工做,由于在启用反射时很难知道哪些代码未被使用,所以冗余代码很难剥离。编程
- 手动序列化和反序列化 (适合小项目)
须要导入dart:convert库json
json.decode(jsonStr) :将JSON格式的字符串转为Dart对象(List或Map)。
json.ecode(list) :将Dart对象转为JSON格式的字符串。
例 // 导入库 import 'dart:convert' // 一个JSON格式的用户列表字符串 String jsonStr='[{"name":"Jack"},{"name":"Rose"}]'; // json字符串转为Dart对象 List items=json.decode(jsonStr); // items[0]["name"] // Dart对象转为json字符串 String jsonString=json.ecode(items);
弊端
json.decode() 没有外部依赖或其它的设置,对于小项目很方便。但当项目变大时,这种手动编写序列化逻辑可能变得难以管理且容易出错 String json='{"name": "John Smith","email": "john@example.com"}'; Map<String, dynamic> user = json.decode(json); print(' ${user['name']}'); print(' ${user['email']}'); 因为json.decode()仅返回一个Map<String, dynamic>,这意味着直到运行时才知道值的类型。 经过这种方法,失去了大部分静态类型语言特性:类型安全、自动补全和最重要的编译时异常。这样一来,代码可能会变得很是容易出错。例如,当访问name或email字段时,输入错误的字段名,因为这个JSON在map结构中,因此编译器不知道这个错误的字段名,因此编译时不会报错。 其实,这个问题在不少平台上都会遇到,而也早就有了好的解决方法即“Json Model化”,具体作法就是,经过预约义一些与Json结构对应的Model类,而后在请求到数据后再动态根据数据建立出Model类的实例。
在模型类中序列化JSON
能够经过引入一个简单的模型类来解决前面提到的问题。 在User类内部有: 1. 一个User.fromJson 构造函数, 用于从一个map构造出一个 User实例。 2. 一个toJson 方法, 将 User 实例转化为一个map. 这样,调用代码如今能够具备类型安全、自动补全字段以及编译时异常。若是将拼写错误字段视为int类型而不是String, 那么代码就不会经过编译,而不是在运行时崩溃。 user.dart文件 class User { final String name; final String email; User(this.name, this.email); User.fromJson(Map<String, dynamic> json) : name = json['name'], email = json['email']; Map<String, dynamic> toJson() => <String, dynamic>{ 'name': name, 'email': email, }; } 如今,序列化和反序列化的逻辑移到了模型自己内部。 这样,调用代码就不用担忧JSON序列化了,可是Model类仍是必须的。 在实践中,User.fromJson和User.toJson方法都须要单元测试到位,以验证正确的行为。 反序列化 Map userMap = json.decode(json); var user = new User.fromJson(userMap); print('Howdy, ${user.name}!'); print('We sent the verification link to ${user.email}.'); 序列化 // 只是将该User对象传递给该json.encode方法。不须要手动调用toJson这个方法,由于JSON.encode内部会自动调用。 String json = json.encode(user);
- 经过代码生成自动序列化和反序列化(大中型项目)
代码生成功能的JSON序列化是指经过外部库自动生成序列化模板。 须要一些初始设置,并运行一个文件观察器,从model类生成代码。 若是访问JSON字段时拼写错误,会在编译时捕获。 缺点:生成的源文件可能会在项目导航器会显得混乱。
json_serializable三方库(官方推荐): 一个自动化的源代码生成器,能够在开发阶段生成JSON序列化模板。
第一步:pubspec.yaml文件(添加依赖包并下载)
dependencies: json_annotation: ^2.0.0 dev_dependencies: build_runner: ^1.0.0 json_serializable: ^2.0.0 运行 flutter packages get
第二步:user.dart文件(以json_serializable的方式建立model类)
import 'package:json_annotation/json_annotation.dart'; // user.g.dart文件 会在运行生成命令后自动生成。此处必须先写上。 part 'user.g.dart'; /// 这个标注是告诉生成器,这个类是须要生成Model类的 @JsonSerializable() class User{ String name; String email; User(this.name, this.email); // 忽略这里的错误,$UserFromJson、_$UserToJson会在下面的步骤中在user.g.dart文件中自动生成。 factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); Map<String, dynamic> toJson() => _$UserToJson(this); }
自定义命名策略 例如,若是正在使用的API返回带有snake_case的对象,但想在模型中使用lowerCamelCase, 那么可使用@JsonKey标注: // 显式关联JSON字段名与Model属性的对应关系 @JsonKey(name: 'registration_date_millis') final int registrationDateMillis;
第三步:运行代码生成器,为Model自动生成json序列化代码
方式1. 一次性生成 flutter packages pub run build_runner build 在项目根目录下运行,该命令经过源文件找出须要生成Model类的源文件(包含@JsonSerializable标注的)来生成对应的.g.dart文件。 方式2. 持续生成 flutter packages pub run build_runner watch 在项目根目录下运行,该命令会启动watcher,watcher会监视项目中文件的变化,并在须要时自动构建必要的文件。
上面的方法有一个最大的问题就是要为每个json写模板。
解决:自动化生成模板。用dart实现一个脚本或者使用IDE插件,将JSON文本转换为模板。
- 自动化生成模板(脚本)
1. 定义一个"模板的模板",命名为"template.dart"。 模板中的“%t”、“%s”为占位符,将在脚本运行时动态被替换为合适的导入头和类名。 import 'package:json_annotation/json_annotation.dart'; %t part '%s.g.dart'; @JsonSerializable() class %s { %s(); %s factory %s.fromJson(Map<String,dynamic> json) => _$%sFromJson(json); Map<String, dynamic> toJson() => _$%sToJson(this); }
2. 写一个自动生成模板的脚本(mo.dart),它能够根据指定的JSON目录,遍历生成模板,在生成时定义一些规则: 1. 若是JSON文件名如下划线“_”开始,则忽略此JSON文件。 2. 复杂的JSON对象每每会出现嵌套,能够经过一个特殊标志来手动指定嵌套的对象。 import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; const TAG="\$"; const SRC="./json"; //JSON 目录 const DIST="lib/models/"; //输出model目录 void walk() { // 遍历JSON目录生成模板 var src = new Directory(SRC); var list = src.listSync(); var template=new File("./template.dart").readAsStringSync(); File file; list.forEach((f) { if (FileSystemEntity.isFileSync(f.path)) { file = new File(f.path); var paths=path.basename(f.path).split("."); String name=paths.first; if(paths.last.toLowerCase()!="json"||name.startsWith("_")) return ; if(name.startsWith("_")) return; //下面生成模板 var map = json.decode(file.readAsStringSync()); //为了不重复导入相同的包,咱们用Set来保存生成的import语句。 var set= new Set<String>(); StringBuffer attrs= new StringBuffer(); (map as Map<String, dynamic>).forEach((key, v) { if(key.startsWith("_")) return ; attrs.write(getType(v,set,name)); attrs.write(" "); attrs.write(key); attrs.writeln(";"); attrs.write(" "); }); String className=name[0].toUpperCase()+name.substring(1); var dist=format(template,[name,className,className,attrs.toString(), className,className,className]); var _import=set.join(";\r\n"); _import+=_import.isEmpty?"":";"; dist=dist.replaceFirst("%t",_import ); //将生成的模板输出 new File("$DIST$name.dart").writeAsStringSync(dist); } }); } String changeFirstChar(String str, [bool upper=true] ){ return (upper?str[0].toUpperCase():str[0].toLowerCase())+str.substring(1); } //将JSON类型转为对应的dart类型 String getType(v,Set<String> set,String current){ current=current.toLowerCase(); if(v is bool){ return "bool"; }else if(v is num){ return "num"; }else if(v is Map){ return "Map<String,dynamic>"; }else if(v is List){ return "List"; }else if(v is String){ //处理特殊标志 if(v.startsWith("$TAG[]")){ var className=changeFirstChar(v.substring(3),false); if(className.toLowerCase()!=current) { set.add('import "$className.dart"'); } return "List<${changeFirstChar(className)}>"; }else if(v.startsWith(TAG)){ var fileName=changeFirstChar(v.substring(1),false); if(fileName.toLowerCase()!=current) { set.add('import "$fileName.dart"'); } return changeFirstChar(fileName); } return "String"; }else{ return "String"; } } //替换模板占位符 String format(String fmt, List<Object> params) { int matchIndex = 0; String replace(Match m) { if (matchIndex < params.length) { switch (m[0]) { case "%s": return params[matchIndex++].toString(); } } else { throw new Exception("Missing parameter for string format"); } throw new Exception("Invalid format string: " + m[0].toString()); } return fmt.replaceAllMapped("%s", replace); } void main(){ walk(); }
3. 写一个shell(mo.sh),先生成Model,再为Model自动生成json序列化代码。 dart mo.dart flutter packages pub run build_runner build --delete-conflicting-outputs
4. 在根目录下新建一个json目录,而后把user.json移进去,而后在lib目录下建立一个models目录,用于保存最终生成的Model类。 如今只须要一句命令便可生成Model类了: ./mo.sh
嵌套JSON的状况
一个person.json内容以下: { "name": "John Smith", "email": "john@example.com", "mother":{ "name": "Alice", "email":"alice@example.com" }, "friends":[ { "name": "Jack", "email":"Jack@example.com" }, { "name": "Nancy", "email":"Nancy@example.com" } ] } 每一个Person都有name 、email 、 mother和friends四个字段,因为mother也是一个Person,朋友是多个Person(数组),因此指望生成的Model是下面这样: import 'package:json_annotation/json_annotation.dart'; part 'person.g.dart'; @JsonSerializable() class Person { String name; String email; Person mother; List<Person> friends; Person(); factory Person.fromJson(Map<String,dynamic> json) => _$PersonFromJson(json); Map<String, dynamic> toJson() => _$PersonToJson(this); } 这时,只须要简单修改一下JSON,添加一些特殊标志,从新运行mo.sh便可: { "name": "John Smith", "email": "john@example.com", "mother":"$person", "friends":"$[]person" } 脚本在遇到特殊标志符后会先把相应字段转为相应的对象或对象数组。若是与内容冲突,能够修改mo.dart中的TAG常量,自定义标志符。 1. 对象使用 $ 2. 对象数组使用 $[] 后跟具体类型名
若是每一个项目都手动构建一个这样的脚本显然很麻烦,为此,将脚本和生成模板封装成一个包(已经发布到了Pub上,包名为Json_model),开发者直接添加该依赖包,即可以用一条命令根据Json文件生成Dart类。
- 自动化生成模板(IDE插件)
IDE插件和Json_model对比: 1. Json_model须要单独维护一个存放Json文件的文件夹,若是有改动,只需修改Json文件即可从新生成Model类;而IDE插件通常须要用户手动将Json内容拷贝复制到一个输入框中,这样生成以后Json文件没有存档的话,以后要改动就须要手动。 2. Json_model能够手动指定某个字段引用的其它Model类,能够避免生成重复的类;而IDE插件通常会为每个Json文件中全部嵌套对象都单独生成一个Model类,即便这些嵌套对象可能在其它Model类中已经生成过。 3. Json_model 提供了命令行转化方式,能够方便集成到CI等非UI环境的场景。
3. 经过HttpClient发起HTTP请求(Dart IO库提供)
支持GET、POST、PUT、DELETE等经常使用http操做
import 'dart:io'; // 使用HttpClient发起请求分为五步 get() async { // 1. 建立一个HttpClient var httpClient = new HttpClient(); // 2. 建立URL var uri = new Uri.http( 'example.com', '/path1/path2', {'param1': '42', 'param2': 'foo'}); /* var uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: { "xx":"xx", "yy":"dd" }); */ // 3. 发送请求 var request = await httpClient.getUrl(uri); /* // 设置请求header request.headers.add("user-agent", "test"); // post时设置请求体 request.add(utf8.encode("hello world")); //request.addStream(_inputStream); //能够直接添加输入流 */ // 4. 获取响应 var response = await request.close(); // 解析响应内容 var responseBody = await response.transform(UTF8.decoder).join(); } // 5. 关闭client(经过该client发起的全部请求都会停止) httpClient.close();
例
import 'package:flutter/material.dart'; import 'dart:io'; import 'dart:convert'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.yellow, visualDensity: VisualDensity.adaptivePlatformDensity, ), home:MyHomePage(), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { var _ipAddress = '未知'; _getIPAddress() async { // var httpClient = new HttpClient(); // var url = 'https://httpbin.org/ip'; String result; try { // var request = await httpClient.getUrl(Uri.parse(url)); // var response = await request.close(); if (response.statusCode == HttpStatus.ok) { // var json = await response.transform(utf8.decoder).join(); // var data = jsonDecode(json); result = data['origin']; } else { result = '获取IP地址失败:\nHttp status ${response.statusCode}'; } } catch (exception) { result = '获取IP地址失败'; } // 组件没有被移除时更新UI if (!mounted) return; setState(() { _ipAddress = result; }); } @override Widget build(BuildContext context) { var spacer = new SizedBox(height: 32.0); return new Scaffold( body: new Center( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Text('当前IP地址:'), new Text('$_ipAddress.'), spacer, new RaisedButton( onPressed: _getIPAddress, child: new Text('获取IP地址'), ), ], ), ), ); } }
例2
点击“获取百度首页”按钮后,会请求百度首页,请求成功后,将返回内容显示出来并在控制台打印响应header import 'package:flutter/material.dart'; import 'dart:io'; import 'dart:convert'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.yellow, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { bool _loading = false; String _text = ""; @override Widget build(BuildContext context) { return ConstrainedBox( constraints: BoxConstraints.expand(), child: SingleChildScrollView( child: Column( children: <Widget>[ RaisedButton( child: Text("获取百度首页"), onPressed: _loading ? null : () async { setState(() { _loading = true; _text = "正在请求..."; }); try { // HttpClient httpClient = new HttpClient(); // HttpClientRequest request = await httpClient .getUrl(Uri.parse("https://www.baidu.com")); // user-agent request.headers.add("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1"); // HttpClientResponse response = await request.close(); // _text = await response.transform(utf8.decoder).join(); // 响应头 print(response.headers); //关闭client后,经过该client发起的全部请求都会停止。 httpClient.close(); } catch (e) { _text = "请求失败:$e"; } finally { setState(() { _loading = false; }); } }), Container( width: MediaQuery.of(context).size.width - 50.0, child: Text(_text.replaceAll(new RegExp(r"\s"), ""))) ], ), ), ); } } connection: Keep-Alive cache-control: no-cache set-cookie: .... //有多个,省略... transfer-encoding: chunked date: Tue, 30 Oct 2018 10:00:52 GMT content-encoding: gzip vary: Accept-Encoding strict-transport-security: max-age=172800 content-type: text/html;charset=utf-8 tracecode: 00525262401065761290103018, 00522983
HttpClient配置
HttpClient提供的这些属性和方法最终都会做用在请求header里,也能够直接去设置header。
不一样的是经过HttpClient设置的对整个httpClient都生效,而经过HttpClientRequest设置的只对当前请求生效。
idleTimeout 对应请求头中的keep-alive字段值 为了不频繁创建链接,httpClient在请求结束后会保持链接一段时间,超过这个阈值后才会关闭链接。 connectionTimeout 和服务器创建链接的超时,若是超过这个值则会抛出SocketException异常。 maxConnectionsPerHost 同一个host,同时容许创建链接的最大数量。 autoUncompress 对应请求头中的Content-Encoding 若是设置为true,则请求头中Content-Encoding的值为当前HttpClient支持的压缩算法列表,目前只有"gzip" userAgent 对应请求头中的User-Agent字段。
HTTP请求认证(Authentication)
Http协议的认证机制能够用于保护非公开资源。若是Http服务器开启了认证,那么用户在发起请求时就须要携带用户凭据。
若是在浏览器中访问了启用Basic认证的资源时,浏览就会弹出一个登陆框。
除了Basic认证以外还有:Digest认证、Client认证、Form Based认证等,目前Flutter的HttpClient只支持Basic和Digest两种认证方式,这两种认证方式最大的区别是发送用户凭据时,对于用户凭据的内容,前者只是简单的经过Base64编码(可逆),然后者会进行哈希运算,相对来讲安全一点,可是为了安全起见,不管是采用Basic认证仍是Digest认证,都应该在Https协议下,这样能够防止抓包和中间人攻击。
Basic认证的基本过程: 1. 客户端发送http请求给服务器,服务器验证该用户是否已经登陆验证过了,若是没有的话, 服务器会返回一个401 Unauthozied给客户端,而且在响应header中添加一个 “WWW-Authenticate” 字段,例如: WWW-Authenticate: Basic realm="admin" 其中"Basic"为认证方式,realm为用户角色的分组,能够在后台添加分组。 2. 客户端获得响应码后,将用户名和密码进行base64编码(格式为用户名:密码),设置请求头Authorization,继续访问 : Authorization: Basic YXXFISDJFISJFGIJIJG 服务器验证用户凭据,若是经过就返回资源内容。
HttpClient关于Http认证的方法和属性
若是全部请求都须要认证,那么应该使用方法1: 在HttpClient初始化时就调用addCredentials()来添加全局凭证,而不是方法2: 去动态添加。 1. addCredentials(Uri url, String realm, HttpClientCredentials credentials) 该方法用于添加用户凭据,如: httpClient.addCredentials(_uri, "admin", new HttpClientBasicCredentials("username","password"), //Basic认证凭据 ); 若是是Digest认证,能够建立Digest认证凭据: HttpClientDigestCredentials("username","password") 2. authenticate(Future<bool> f(Uri url, String scheme, String realm)) 这是一个setter,类型是一个回调,当服务器须要用户凭据且该用户凭据未被添加时,httpClient会调用此回调,在这个回调当中,通常会调用addCredential()来动态添加用户凭证,例如: httpClient.authenticate=(Uri url, String scheme, String realm) async{ if(url.host=="xx.com" && realm=="admin"){ httpClient.addCredentials(url, "admin", new HttpClientBasicCredentials("username","pwd"), ); return true; } return false; };
代理
能够经过findProxy来设置代理策略 有时代理服务器也启用了身份验证,这和http协议的认证是类似的,HttpClient提供了对应的Proxy认证方法和属性: set authenticateProxy( Future<bool> f(String host, int port, String scheme, String realm)); void addProxyCredentials( String host, int port, String realm, HttpClientCredentials credentials); 使用方法和addCredentials和authenticate 相同
例 将全部请求经过代理服务器(192.168.1.2:8888)发送出去: client.findProxy = (uri) { // 若是须要过滤uri,能够手动判断 // findProxy 回调返回值是一个遵循浏览器PAC脚本格式的字符串,若是不须要代理,返回"DIRECT"便可。 return "PROXY 192.168.1.2:8888"; };
证书校验
Https中为了防止经过伪造证书而发起的中间人攻击,客户端应该对自签名或非CA颁发的证书进行校验。
证书校验其实就是提供一个badCertificateCallback回调 HttpClient对证书校验的逻辑以下: 1. 若是请求的Https证书是可信CA颁发的,而且访问host包含在证书的domain列表中(或者符合通配规则)而且证书未过时,则验证经过。 2. 若是第一步验证失败,但在建立HttpClient时,已经经过SecurityContext将证书添加到证书信任链中,那么当服务器返回的证书在信任链中的话,则验证经过。 3. 若是一、2验证都失败了,若是用户提供了badCertificateCallback回调,则会调用它,若是回调返回true,则容许继续连接,若是返回false,则终止连接。
例 假设后台服务使用的是自签名证书,证书格式是PEM格式。将证书的内容保存在本地字符串中,那么校验逻辑以下: String PEM="XXXXX";//能够从文件读取 ... httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){ if(cert.pem==PEM){ return true; //证书一致,则容许发送数据 } return false; }; X509Certificate是证书的标准格式,包含了证书除私钥外全部信息。另外,上面的示例没有校验host,是由于只要服务器返回的证书内容和本地的保存一致就已经能证实是咱们的服务器了(而不是中间人),host验证一般是为了防止证书和域名不匹配。 对于自签名的证书,也能够将其添加到本地证书信任链中,这样证书验证时就会自动经过,而不会再走到badCertificateCallback回调中: SecurityContext sc=new SecurityContext(); //file为证书路径 sc.setTrustedCertificates(file); //建立一个HttpClient HttpClient httpClient = new HttpClient(context: sc); 注意,经过setTrustedCertificates()设置的证书格式必须为PEM或PKCS12,若是证书格式为PKCS12,则需将证书密码传入,这样则会在代码中暴露证书密码,因此客户端证书校验不建议使用PKCS12格式的证书。
例(http库)
添加http依赖 import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; Future<Post> fetchPost() async { // http.get方法返回类型:Future<http.Response> final response = await http.get('https://jsonplaceholder.typicode.com/posts/1'); final response = await http.get( 'https://jsonplaceholder.typicode.com/posts/1', headers: {HttpHeaders.AUTHORIZATION: "Basic your_api_token_here"}, // 认证请求 ); final responseJson = json.decode(response.body); return new Post.fromJson(responseJson); } class Post { final int userId; final int id; final String title; final String body; Post({this.userId, this.id, this.title, this.body}); factory Post.fromJson(Map<String, dynamic> json) { return new Post( userId: json['userId'], id: json['id'], title: json['title'], body: json['body'], ); } } void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Fetch Data Example', theme: new ThemeData( primarySwatch: Colors.blue, ), home: new Scaffold( appBar: new AppBar( title: new Text('Fetch Data Example'), ), body: new Center( child: new FutureBuilder<Post>( future: fetchPost(), builder: (context, snapshot) { if (snapshot.hasData) { return new Text(snapshot.data.title); } else if (snapshot.hasError) { return new Text("${snapshot.error}"); } // By default, show a loading spinner return new CircularProgressIndicator(); }, ), ), ), ); } }
直接使用HttpClient发起网络请求是比较麻烦的,不少事情得手动处理,若是再涉及到文件上传/下载、Cookie管理等就会很是繁琐。
4. dio库
支持:Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、请求配置等。
一个dio实例能够发起多个http请求,通常来讲,APP只有一个http数据源时,dio应该使用单例模式。
1. 添加dio依赖包并下载: dependencies: dio: #lastverssion 2. 导入并建立dio实例: import 'package:dio/dio.dart'; Dio dio = Dio(); 3. GET 请求 : Response response; // 等价于response=await dio.get("/test",queryParameters:{"id":12,"name":"wendu"}); response=await dio.get("/test?id=12&name=wendu"); print(response); print(response.data.toString()); POST 请求: response=await dio.post("/test",data:{"id":12,"name":"wendu"}) 发起多个并发请求: response= await Future.wait([dio.post("/info"),dio.get("/token")]); 下载文件: response=await dio.download("https://www.google.com/",_savePath); 发送 FormData: // 若是发送的数据是FormData,则dio会将请求header的contentType设为“multipart/form-data”。 FormData formData = new FormData.from({ "name": "wendux", "age": 25, }); response = await dio.post("/info", data: formData) 经过FormData上传多个文件: FormData formData = new FormData.from({ "name": "wendux", "age": 25, "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"), "file2": new UploadFileInfo(new File("./upload.txt"), "upload2.txt"), // 支持文件数组上传 "files": [ new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"), new UploadFileInfo(new File("./example/upload.txt"), "upload.txt") ] }); response = await dio.post("/info", data: formData) dio内部仍然使用HttpClient发起的请求,因此代理、请求认证、证书校验等和HttpClient是相同的,能够在onHttpClientCreate回调中设置,例如: (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { //设置代理 client.findProxy = (uri) { return "PROXY 192.168.1.2:8888"; }; //校验证书 httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){ if(cert.pem==PEM){ return true; //证书一致,则容许发送数据 } return false; }; }; 注意,onHttpClientCreate会在当前dio实例内部须要建立HttpClient时调用,因此经过此回调配置HttpClient会对整个dio实例生效,若是你想针对某个应用请求单独的代理或证书校验策略,能够建立一个新的dio实例便可。
例
经过Github开放的API来请求flutterchina组织下的全部公开的开源项目,实现: 在请求阶段弹出loading 请求结束后,若是请求失败,则展现错误信息;若是成功,则将项目名称列表展现出来。 import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.yellow, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { // Dio _dio = new Dio(); @override Widget build(BuildContext context) { return new Scaffold( body: new Container( alignment: Alignment.center, child: FutureBuilder( // future: _dio.get("https://api.github.com/orgs/flutterchina/repos"), builder: (BuildContext context, AsyncSnapshot snapshot) { // 请求完成 if (snapshot.connectionState == ConnectionState.done) { Response response = snapshot.data; // 发生错误 if (snapshot.hasError) { return Text(snapshot.error.toString()); } // 请求成功,经过项目信息构建用于显示项目名称的ListView return ListView( children: response.data .map<Widget>((e) => ListTile(title: Text(e["full_name"]))) .toList(), ); } // 请求未完成时弹出loading return CircularProgressIndicator(); }), )); } }
5. Http分块下载
1. 分块下载的最终速度受设备所在网络带宽、源出口速度、每一个块大小、以及分块的数量等诸多因素影响,实际过程当中很难保证速度最优。下载速度的主要瓶颈是取决于网络速度和服务器的出口速度,若是是同一个数据源,分块下载的意义并不大,由于服务器是同一个,出口速度肯定的,主要取决于网速。若是有多个下载源,而且每一个下载源的出口带宽都是有限制的,这时分块下载可能会更快一下,之因此说“可能”,是因为这并非必定的,好比有三个源,三个源的出口带宽都为1G/s,而咱们设备所连网络的峰值假设只有800M/s,那么瓶颈就在咱们的网络。即便咱们设备的带宽大于任意一个源,下载速度依然不必定就比单源单线下载快,试想一下,假设有两个源A和B,速度A源是B源的3倍,若是采用分块下载,两个源各下载一半的话。 2. 分块下载有一个比较使用的场景是断点续传,能够将文件分为若干个块,而后维护一个下载状态文件用以记录每个块的状态,这样即便在网络中断后,也能够恢复中断前的状态。分块大小、下载到一半的块如何处理、要不要维护一个任务队列 Http协议定义了分块传输的响应header字段,但具体是否支持取决于Server的实现, 能够指定请求头的"range"字段来验证服务器是否支持分块传输。
例
利用curl命令来验证: $ curl -H "Range: bytes=0-10" http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg -v 输出: # 请求头 > GET /HBuilder.9.0.2.macosx_64.dmg HTTP/1.1 > Host: download.dcloud.net.cn > User-Agent: curl/7.54.0 > Accept: */* > Range: bytes=0-10 # 响应头 < HTTP/1.1 206 Partial Content < Server: Tengine < Content-Type: application/octet-stream < Content-Length: 11 < Connection: keep-alive < Date: Fri, 25 Sep 2020 16:02:41 GMT < Content-Range: bytes 0-10/233295878 。。。 说明: 在请求头中添加"Range: bytes=0-10"的做用是,告诉服务器本次请求只想获取文件0-10(包括10,共11字节)这块内容。若是服务器支持分块传输,则响应状态码为206,表示“部份内容”,而且同时响应头中包含“Content-Range”字段,若是不支持则不会包含。 0-10表示本次返回的区块,233295878表明文件的总长度,单位都是byte。
例2
设计一个简单的多线程的文件分块下载器,实现的思路是: 1. 先检测是否支持分块传输,若是不支持,则直接下载;若支持,则将剩余内容分块下载。 2. 各个分块下载时保存到各自临时文件,等到全部分块下载完后合并临时文件。 3. 删除临时文件。 import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'dart:io'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.yellow, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { /// Future downloadWithChunks( url, savePath, { ProgressCallback onReceiveProgress, }) async { const firstChunkSize = 102; const maxChunk = 3; int total = 0; var dio = Dio(); var progress = <int>[]; createCallback(no) { return (int received, _) { progress[no] = received; if (onReceiveProgress != null && total != 0) { onReceiveProgress(progress.reduce((a, b) => a + b), total); } }; } // 使用dio的download API 实现downloadChunk: //start 表明当前块的起始位置,end表明结束位置 //no 表明当前是第几块 Future<Response> downloadChunk(url, start, end, no) async { progress.add(0); //progress记录每一块已接收数据的长度 --end; return dio.download( url, savePath + "temp$no", //临时文件按照块的序号命名,方便最后合并 onReceiveProgress: createCallback(no), // 建立进度回调,后面实现 options: Options( headers: {"range": "bytes=$start-$end"}, //指定请求的内容区间 ), ); } Future mergeTempFiles(chunk) async { File f = File(savePath + "temp0"); IOSink ioSink = f.openWrite(mode: FileMode.writeOnlyAppend); //合并临时文件 for (int i = 1; i < chunk; ++i) { File _f = File(savePath + "temp$i"); await ioSink.addStream(_f.openRead()); await _f.delete(); // 删除临时文件 } await ioSink.close(); await f.rename(savePath); // 合并后的文件重命名为真正的名称 } // 经过第一个分块请求检测服务器是否支持分块传输 Response response = await downloadChunk(url, 0, firstChunkSize, 0); if (response.statusCode == 206) { // 若是支持 // 解析文件总长度,进而算出剩余长度 total = int.parse(response.headers .value(HttpHeaders.contentRangeHeader) .split("/") .last); int reserved = total - int.parse(response.headers.value(HttpHeaders.contentLengthHeader)); // 文件的总块数(包括第一块) int chunk = (reserved / firstChunkSize).ceil() + 1; if (chunk > 1) { int chunkSize = firstChunkSize; if (chunk > maxChunk + 1) { chunk = maxChunk + 1; chunkSize = (reserved / maxChunk).ceil(); } var futures = <Future>[]; for (int i = 0; i < maxChunk; ++i) { int start = firstChunkSize + i * chunkSize; // 分块下载剩余文件 futures.add(downloadChunk(url, start, start + chunkSize, i + 1)); } // 等待全部分块所有下载完成 await Future.wait(futures); } // 合并文件文件 await mergeTempFiles(chunk); } } main() async { var url = "http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg"; var savePath = "./example/HBuilder.9.0.2.macosx_64.dmg"; await downloadWithChunks(url, savePath, onReceiveProgress: (received, total) { if (total != -1) { print("${(received / total * 100).floor()}%"); } }); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: main, tooltip: 'Increment', child: Icon(Icons.add), ), ); } }
6. 使用WebSockets
Http协议是无状态的,只能由客户端主动发起,服务端再被动响应,服务端没法向客户端主动推送内容,而且一旦服务器响应结束,连接就会断开,因此没法进行实时通讯。WebSocket协议正是为解决客户端与服务端实时通讯而产生的技术,如今已经被主流浏览器支持,Flutter也提供了专门的包来支持WebSocket协议。
Http协议中虽然能够经过keep-alive机制使服务器在响应结束后连接会保持一段时间,但最终仍是会断开,keep-alive机制主要是用于避免在同一台服务器请求多个资源时频繁建立连接,它本质上是支持连接复用的技术,而并不是用于实时通讯。 WebSocket协议本质上是一个基于tcp的协议,它是先经过HTTP协议发起一条特殊的http请求进行握手后,若是服务端支持WebSocket协议,则会进行协议升级。WebSocket会使用http协议握手后建立的tcp连接,和http协议不一样的是,WebSocket的tcp连接是个长连接(不会断开),因此服务端与客户端就能够经过此TCP链接进行实时通讯。 要接收二进制数据仍然使用StreamBuilder,由于WebSocket中全部发送的数据使用帧的形式发送,而帧是有固定格式,每个帧的数据类型均可以经过Opcode字段指定,它能够指定当前帧是文本类型仍是二进制类型(还有其它类型),因此客户端在收到帧时就已经知道了其数据类型,因此flutter彻底能够在收到数据后解析出正确的类型,因此就无需开发者去关心,当服务器传输的数据是指定为二进制时,StreamBuilder的snapshot.data的类型就是List<int>,是文本时,则为String。
web_socket_channel包提供了链接到WebSocket服务器的工具。该package提供了一个WebSocketChannel容许既能够监听来自服务器的消息,又能够将消息发送到服务器的方法。
使用步骤: 1. 链接到WebSocket服务器 // 建立一个WebSocketChannel,并链接到一台服务器: final channel = IOWebSocketChannel.connect('ws://echo.websocket.org'); 2. 监听来自服务器的消息 // WebSocketChannel提供了一个来自服务器的消息Stream 。该Stream类是dart:async包中的一个基础类。它提供了一种方法来监听来自数据源的异步事件。与Future返回单个异步响应不一样,Stream类能够随着时间推移传递不少事件。该StreamBuilder 组件将链接到一个Stream, 并在每次收到消息时通知Flutter从新构建界面。 new StreamBuilder( stream: widget.channel.stream, builder: (context, snapshot) { return new Text(snapshot.hasData ? '${snapshot.data}' : ''); }, ); 3. 将数据发送到服务器 // 将数据发送到服务器,WebSocketChannel提供了一个StreamSink,它将消息发给服务器。StreamSink类提供了给数据源同步或异步添加事件的通常方法。 channel.sink.add('Hello!'); 4. 关闭WebSocket链接 // 使用WebSocket后,要关闭链接: channel.sink.close();
例
import 'package:flutter/material.dart'; import 'package:web_socket_channel/io.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.yellow, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { TextEditingController _controller = new TextEditingController(); IOWebSocketChannel channel; String _text = ""; @override void initState() { // 建立websocket链接 channel = new IOWebSocketChannel.connect('ws://echo.websocket.org'); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("WebSocket(内容回显)"), ), body: new Padding( padding: const EdgeInsets.all(20.0), child: new Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ new Form( child: new TextFormField( controller: _controller, decoration: new InputDecoration(labelText: '发送内容'), ), ), new StreamBuilder( stream: channel.stream, builder: (context, snapshot) { //网络不通会走到这 if (snapshot.hasError) { _text = "网络不通..."; } else if (snapshot.hasData) { _text = "echo: "+snapshot.data; } return new Padding( padding: const EdgeInsets.symmetric(vertical: 24.0), child: new Text(_text), ); }, ) ], ), ), floatingActionButton: new FloatingActionButton( onPressed: _sendMessage, tooltip: 'Send message', child: new Icon(Icons.send), ), ); } void _sendMessage() { if (_controller.text.isNotEmpty) { channel.sink.add(_controller.text); } } @override void dispose() { channel.sink.close(); super.dispose(); } }
7. 使用Socket API(dart:io包中)
Http协议和WebSocket协议都属于应用层协议,除了它们,应用层协议还有不少如:SMTP、FTP等,这些应用层协议的实现都是经过Socket API来实现的。其实,操做系统中提供的原生网络请求API是标准的,在C语言的Socket库中,它主要提供了端到端创建连接和发送数据的基础API,而高级编程语言中的Socket库其实都是对操做系统的socket API的一个封装。 若是须要自定义协议或者想直接来控制管理网络连接、又或者想从新实现一个HttpClient,这时就须要使用Socket。 使用Socket须要本身实现Http协议(须要本身实现和服务器的通讯过程)
例
_request() async{ //创建链接 var socket=await Socket.connect("baidu.com", 80); //根据http协议,发送请求头 socket.writeln("GET / HTTP/1.1"); socket.writeln("Host:baidu.com"); socket.writeln("Connection:close"); socket.writeln(); await socket.flush(); //发送 //读取返回内容 _response =await socket.transform(utf8.decoder).join(); await socket.close(); }
8. http三方库
1.添加依赖包,并下载 http: #lastversion 2.导入库 import 'package:http/http.dart' as http; import 'dart:convert' as convert; 3. 使用(在initState中调用请求方法) var url = 'https://...'; // post var response = await http.post(url, body: {'name': '张三', 'password': '123456'}); // get // var response = await http.get(url); if (response.statusCode == 200) { // 请求成功 // 解析数据 var jsonResponse = convert.jsonDecode(response.body); var name = jsonResponse['name']; } else { print('请求失败 状态码: ${response.statusCode}.'); }
做者:风雨路上砥砺前行 连接:https://www.jianshu.com/p/695efe77597b 来源:简书 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。