目录
3. HttpClient
4. dio三方库
5. 使用WebSockets
6. 使用Socket API(dart:io包中)
7. http三方库
1. 通过HttpClient发起HTTP请求
支持GET、POST、PUT、DELETE等常用请求
1. 需要导入Dart IO库:
import 'dart:io';
2. 使用HttpClient发起请求,分为五步:
get() async {
// 1. 创建一个HttpClient
HttpClient httpClient = new HttpClient();
// 2. 创建request请求
// 创建URL
Uri uri = new Uri.http(
'example.com', '/path1/path2', {'param1': '42', 'param2': 'foo'});
/*
Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
"xx":"xx",
"yy":"dd"
});
*/
// 创建并配置请求
HttpClientRequest request = await httpClient.getUrl(uri);
/*
// 设置请求header
request.headers.add("user-agent", "test");
// post时设置请求体
request.add(utf8.encode("hello world"));
//request.addStream(_inputStream); //可以直接添加输入流
*/
// 3. 连接服务器并发送请求,等待响应
HttpClientResponse response = await request.close();
// 4. 解析响应内容
String 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提供的这些属性和方法最终都会作用在请求头里,也可以直接去设置头。
不同的是通过HttpClient设置的对整个httpClient都生效,而通过HttpClientRequest设置的只对当前请求生效。
1. idleTimeout
对应请求头中的keep-alive字段值
为了避免频繁建立连接,httpClient在请求结束后会保持连接一段时间,超过这个阈值后才会关闭连接。
2. connectionTimeout
和服务器建立连接的超时,如果超过这个值则会抛出SocketException异常。
3. maxConnectionsPerHost
同一个host,同时允许建立连接的最大数量。
4. autoUncompress
对应请求头中的Content-Encoding
如果设置为true,则请求头中Content-Encoding的值为当前HttpClient支持的压缩算法列表,目前只有"gzip"
5. 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",
// 如果是Digest认证,可以创建Digest认证凭据:HttpClientDigestCredentials("username","password")
new HttpClientBasicCredentials("username","password"), // Basic认证凭据
);
方法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认证方法和属性:
方法1:
set authenticateProxy(
Future<bool> f(String host, int port, String scheme, String realm));
方法2:
void addProxyCredentials(
String host, int port, String realm, HttpClientCredentials credentials);
示例(将所有请求通过代理服务器发送出去)
client.findProxy = (uri) {
// 如果需要过滤uri,可以手动判断
// findProxy 回调返回值是一个遵循浏览器PAC脚本格式的字符串,如果不需要代理,返回"DIRECT"即可。
return "PROXY 192.168.1.2:8888";
};
证书校验
Https中为了防止通过伪造证书而发起的中间人攻击,客户端应该对自签名或非CA颁发的证书进行校验。
HttpClient对证书校验的逻辑如下:
1. 如果请求的Https证书是可信CA颁发的,并且访问host包含在证书的domain列表中(或者符合通配规则)并且证书未过期,则验证通过。
2. 如果第一步验证失败,但在创建HttpClient时,已经通过SecurityContext将证书添加到证书信任链中,那么当服务器返回的证书在信任链中的话,则验证通过。
3. 如果1、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为证书路径
// 注意,通过setTrustedCertificates()设置的证书格式必须为PEM或PKCS12,如果证书格式为PKCS12,则需将证书密码传入,这样则会在代码中暴露证书密码,所以客户端证书校验不建议使用PKCS12格式的证书。
sc.setTrustedCertificates(file);
// 创建一个HttpClient
HttpClient httpClient = new HttpClient(context: sc);
示例2
添加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管理等就会非常繁琐。
解决:使用dio三方库。
4. dio三方库(用于网络请求)
支持:Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、请求配置等。
相当于iOS的AFNetworking、Android的Retrofit、Web的axios。
使用步骤:
1. 添加dio依赖包并下载:
dependencies:
dio: #lastverssion
2. 导入并创建dio实例:
import 'package:dio/dio.dart';
// 一个dio实例可以发起多个http请求。APP只有一个http数据源时 dio应为单例。
Dio dio = Dio();
3.
GET 请求 :
Response response;
response=await dio.get("/test?id=12&name=hello");
response=await dio.get("/test",queryParameters:{"id":12,"name":"hello"});
print(response.data.toString()); // statusCode、statusMessage
POST 请求:
response=await dio.post("/test",data:{"id":12,"name":"hello"})
PATCH、PUT、DELETE 请求:
response = await dio.patch('/test/12', data: data);
response = await dio.put('/test/12', data: data);
response = await dio.delete('/test/12');
发起多个并发请求:
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)
4.
dio内部仍然使用HttpClient发起的请求,所以代理、请求认证、证书校验等和HttpClient是相同的,可以在onHttpClientCreate回调中设置,例如:
// 注意,onHttpClientCreate会在当前dio实例内部需要创建HttpClient时调用,所以通过此回调配置HttpClient会对整个dio实例生效,如果想针对某个应用请求单独的代理或证书校验策略,可以创建一个新的dio实例即可。
(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;
};
};
5. 拦截器
/*
场景
1. 权限验证:比如接口请求后端返回401未授权时可以跳到登录页,403跳到未授权页面;
2. 异常监控:可以在拦截器处理异常,并且上报到异常监控后台或者发送异常预警消息;
3. 缓存接口:可以对于某些接口将请求缓存在本地,设定一定的缓存有效时限,在时限内重复请求时直接返回本地缓存数据,而无需请求后端接口,降低后端服务器负荷。dio-http-cache
4. Cookie:App本身是不会缓存Cookie信息的,可以使用拦截器在向后端发起请求时自动携带Cookie信息。cookie_manger
5. 生成接口文档:可以在拦截器将请求参数、返回结果输出为Postman格式的接口文档。postman_dio
6. 自定义拦截器:可以自定义自己的拦截器类,继承Interceptor类,实现 onRequest,onResponse 和 onError方法即可。
*/
// 可以同时添加多个拦截器
dio.interceptors
.add(InterceptorsWrapper(onRequest: (options, handler) {
print('请求路径' + options.path);
return handler.next(options);
}, onResponse: (response, handler) {
print('响应' + response.statusMessage);
handler.next(response);
// throw new Exception('');
}, onError: (DioError e, handler) {
print('请求出错' + e.message);
return handler.next(e);
}));
6. 请求取消
每个请求都可以携带一个 CancelToken对象,当调用 CancelToken 的 cancel 方法时,就会通知该请求停止当前的请求。
7. 防重提交
方式1:点击后禁用,等待网络请求结果返回后再启用按钮
方式2:Loading蒙层,在网络请求没结束前使用蒙层将页面遮挡,从而避免操作表单及按钮。
8. session会话
登录成功后,后端会返回Cookie(通常存储在sessionId字段中)登录人的会话信息。
其他需要登录鉴权的接口请求需要携带Cookie来实现会话过程的免登录验证。
退出登录后,后端会清除会话信息,此时携带原有的会话信息请求会被后端认为是无效的。
会话信息可能还携带失效时间信息,可以根据失效时间来判断是否需要重新登录。
Cookie需要持久化,这样App重启后不需要每次都重新登录。
dio.options.headers['Cookie'] = cookie;
options(BaseOptions类型) 是 Dio 的默认请求配置对象,可以配置:
请求头headers,设置cookie、
请求方法
请求内容类型contentType:字符串,默认是:application/json; charset=utf-8
默认请求参数queryParameters:Map,如:终端类型,版本号。
连接超时时间connectTimeout
响应类型responseType:枚举,默认是 json。
下载
Dio的download方法定义如下:
Future<Response> download(
// url
String urlPath,
// 文件路径字符串,或一个返回字符串的回调函数
savePath, {
// void Function(int count, int total)监测下载进度
ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
// 发生错误时是否删除已下载的文件,默认是 true。断点续传需要返回false。
bool deleteOnError = true,
// 源文件的实际大小(未压缩前)。默认是 header 的content-length。
// 如果文件压缩了,而没有指定该值的话,那进度回调里的total会是-1;如果header中指定了,total会是header中对应的文件大小。
String lengthHeader = Headers.contentLengthHeader,
data,
Options? options,
});
/*
1. OS Error: Read-only file system:安卓系统需要获取READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。
2. onReceivedProgress 中如果total=-1则表示该文件被压缩或者需要会话信息才可以下载(如后端开启了验证)。
3. 删除文件的时候需要检查文件是否在下载过程中,如果在下载过程中删除会引起文件读写冲突,抛出异常。
4. CancelToken一个实例只能取消一次请求,因此每次发起请求的时候需要重新构建CancelToken对象,否则取消一次后无法再次取消。
*/
下载
static Future download(
String url,
String savePath, {
Map<String, dynamic> queryParams,
CancelToken cancelToken,
dynamic data,
Options options,
void Function(int, int) onReceiveProgress,
}) async {
try {
return await _dioInstance.download(
url,
savePath,
queryParameters: queryParams,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
} on DioError catch (e) {
if (CancelToken.isCancel(e)) {
EasyLoading.showInfo('下载已取消!');
} else {
if (e.response != null) {
_handleErrorResponse(e.response);
} else {
EasyLoading.showError(e.message);
}
}
} on Exception catch (e) {
EasyLoading.showError(e.toString());
}
}
处理下载逻辑的文件
// 文件下载地址,这里是谷歌浏览器的下载地址(Mac 版本)
String _downloadPath =
'https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg';
// 下载进度比例,用于检测下载是否完成
double _downloadRatio = 0.0;
// 下载进度百分比
String _downloadIndicator = '0.00%';
// 下载文件的存储路径
String _destPath;
// 取消下载的 token
CancelToken _token;
// 指示当前是否处于下载中,以便做业务判断
bool _downloading = false;
// 下载
void _downloadFile() {
_token = CancelToken();
_downloading = true;
HttpUtil.download(_downloadPath, _destPath, cancelToken: _token,
onReceiveProgress: (int received, int total) {
// 在下载过程中如果 total 不为-1就更新下载进度,否则提示错误
if (total != -1) {
if (!_token.isCancelled) {
setState(() {
_downloadRatio = (received / total);
if (_downloadRatio == 1) {
_downloading = false;
}
_downloadIndicator =
(_downloadRatio * 100).toStringAsFixed(2) + '%';
});
}
} else {
_downloading = false;
EasyLoading.showError('无法获取文件大小,下载失败!');
}
});
}
// 取消下载
void _cancelDownload() {
// 下载比例低于1才可以取消,因为下载完成再取消会抛异常
if (_downloadRatio < 1.0) {
_token.cancel();
_downloading = false;
setState(() {
_downloadRatio = 0;
_downloadIndicator = '0.00%';
});
}
}
// 删除文件
void _deleteFile() {
try {
File downloadedFile = File(_destPath);
// 删除文件前需要判断文件是否存在,如果文件不存在删除可能抛出异常。
if (downloadedFile.existsSync()) {
downloadedFile.delete();
} else {
EasyLoading.showError('文件不存在');
}
} catch (e) {
EasyLoading.showError(e.toString());
}
}
示例(session会话)
// 添加cookie
static void setCookie(String cookie) {
_dio.options.headers['Cookie'] = cookie;
}
// 登录业务
_handleSubmit(String username, String password) async {
EasyLoading.showInfo('请稍候...');
var response = await AuthService.login(username, password);
if (response != null && response.statusCode == 200) {
EasyLoading.showSuccess('登录成功');
if (response.headers.map['set-cookie'] != null) {
HttpUtil.setCookie(response.headers.map['set-cookie'][0]);
}
Navigator.of(context).pop();
} else {
EasyLoading.showInfo(response.statusMessage);
}
EasyLoading.dismiss();
}
// 退出业务
void _logout() async {
var response = await AuthService.logout();
if (response != null && response.statusCode == 200) {
HttpUtil.clearCookie();
EasyLoading.showSuccess('已退出登录');
} else {
print('logout Failed');
}
}
static void clearCookie() {
_dioInstance.options.headers['Cookie'] = null;
}
// 验证业务
void _checkSession() async {
var response = await AuthService.checkSession();
if (response != null && response.statusCode == 200) {
print(response.data);
EasyLoading.showSuccess('验票通过,持票人:' + response.data['loginUser']);
} else {
print('Request Failed');
}
}
示例(使用拦截器处理Cookie,降低代码的侵入性)
1. 在onResponse中检测若有cookie则持久化保存。
2. 在onRequest中携带cookie提交。
import 'package:dio/dio.dart';
class CookieManager extends Interceptor {
CookieManager._privateConstructor();
static final CookieManager _instance = CookieManager._privateConstructor();
static get instance => _instance;
String _cookie;
@override
// 拦截响应
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response != null) {
if (response.statusCode == 200) {
if (response.headers.map['set-cookie'] != null) {
_persistCookie(response.headers.map['set-cookie'][0]);
}
} else if (response.statusCode == 401) {
_clearCookie();
}
}
super.onResponse(response, handler);
}
@override
// 拦截请求
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
options.headers['Cookie'] = _cookie;
return super.onRequest(options, handler);
}
// 在runApp方法之前WidgetsFlutterBinding.ensureInitialized()之后调用CookieManager.instance.initCookie();
Future initCookie() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_cookie = prefs.getString('cookie');
}
void _persistCookie(String newCookie) async {
if (_cookie != newCookie) {
_cookie = newCookie;
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('cookie', _cookie);
}
}
void _clearCookie() async {
_cookie = null;
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove('cookie');
}
}
=======================
static Dio getDioInstance() {
if (_dioInstance == null) {
_dioInstance = Dio();
_dioInstance.interceptors.add(CookieManager.instance); // cookie就不会直接暴露给UI
}
return _dioInstance;
}
示例(退出页面时取消当前请求)
class _AppointmentPageState extends State<AppointmentPage> {
bool _hasBug = false;
CancelToken _token;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_hasBug ? '干活了!' : '休息中...',
style: Theme.of(context).textTheme.headline4),
),
body: Container(
child: Center(
child: Image.asset(_hasBug ? 'images/bug.png' : 'images/dating.png'),
),
),
floatingActionButton: IconButton(
icon: Icon(Icons.call),
onPressed: () {
_bugHappened();
},
),
);
}
void _bugHappened() {
_token = CancelToken();
HttpUtil.get('https://www.github.com', cancelToken: _token)
.then((value) => {
// setState方法在 dispose后是不能调用的,否则可能导致内存泄露。
if (mounted){
setState(() {
_hasBug = _token.isCancelled;
})
}
})
.onError((error, stackTrace) => {});
}
@override
void deactivate() {
if (_token != null) {
_token.cancel('dispose');
}
super.deactivate();
}
}
==============
static Future sendRequest(HttpMethod method, String url,
{Map<String, dynamic> queryParams,
dynamic data,
CancelToken cancelToken}) async {
try {
//...省略请求代码
} on DioError catch (e) {
// 检测错误是不是因为取消请求引起的,如果是打印取消提醒
if (CancelToken.isCancel(e)) {
EasyLoading.showInfo('干活了!');
} else {
EasyLoading.showError(e.message);
}
} on Exception catch (e) {
EasyLoading.showError(e.toString());
}
return null;
}
示例(防重提交)
通过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();
}),
));
}
}
Http分块下载
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”字段,如果不支持则不会包含。上面输出的Content-Range内容:0-10表示本次返回的区块,233295878代表文件的总长度,单位都是byte。
1. 分块下载的最终速度受设备所在网络带宽、源出口速度、每个块大小、以及分块的数量等诸多因素影响,实际过程中很难保证速度最优。下载速度的主要瓶颈是取决于网络速度和服务器的出口速度,如果是同一个数据源,分块下载的意义并不大,因为服务器是同一个,出口速度确定的,主要取决于网速。如果有多个下载源,并且每个下载源的出口带宽都是有限制的,这时分块下载可能会更快一下,之所以说“可能”,是由于这并不是一定的,比如有三个源,三个源的出口带宽都为1G/s,而我们设备所连网络的峰值假设只有800M/s,那么瓶颈就在我们的网络。即使我们设备的带宽大于任意一个源,下载速度依然不一定就比单源单线下载快,试想一下,假设有两个源A和B,速度A源是B源的3倍,如果采用分块下载,两个源各下载一半的话。
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),
),
);
}
}
5. 使用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。
使用步骤(4步):
1. 连接到WebSocket服务器
// web_socket_channel包提供了连接到WebSocket服务器的工具。该package提供了一个WebSocketChannel允许既可以监听来自服务器的消息,又可以将消息发送到服务器的方法。
// 创建一个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();
}
}
6. 使用Socket API(dart:io包中)
Socket API 是操作系统为实现应用层网络协议提供的一套基础的、标准的API,它是对传输层网络协议(主要是TCP/UDP)的一个封装。Socket API 实现了端到端建立链接和发送/接收数据的基础API,而高级编程语言中的 Socket API 其实都是对操作系统 Socket API 的一个封装。
Http协议和WebSocket协议都属于应用层协议,除了它们,应用层协议还有很多如:SMTP、FTP等,这些应用层协议的实现都是通过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();
}
7. http三方库
1.添加依赖包,并下载
http: #lastversion
2.导入库
import 'package:http/http.dart' as http;
import 'dart:convert' as convert;
import 'dart:sync';
3. 使用
在initState中调用 fetchPost().then((value)=>print(value))
Future<String> fetchPost() async {
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'];
return name;
} else {
print('请求失败 状态码: ${response.statusCode}.');
return null;
}
}