一、dart:io提供的HttpClient
1、支持常用的Http操作,比如get,post等
2、异步操作,在io.dart中有相关描述
- This library allows you to work with files, directories,
- sockets, processes, HTTP servers and clients, and more.
- Many operations related to input and output are asynchronous
- and are handled using [Future]s or [Stream]s, both of which
- are defined in the [dart:async
- library](../dart-async/dart-async-library.html).
- To use the dart:io library in your code:
-
import 'dart:io';
复制代码
3、网络调用通常遵循如下步骤:
创建 client. 构造 Uri. 发起请求, 等待请求,同时您也可以配置请求headers、 body。 关闭请求, 等待响应. 解码响应的内容. 4、示例,(httpbin.org 这个网站能测试 HTTP 请求和响应的各种信息,比如 cookie、ip、headers 和登录验证等,且支持 GET、POST 等多种方法)
_httpTest() async {
var url = 'https://httpbin.org/ip';
var httpClient = new HttpClient();
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'];
print(result);
} else {
result =
'Error getting IP address:\nHttp status ${response.statusCode}';
}
} catch (exception) {
result = 'Failed getting IP address';
}
}
输出结果:122.70.159.214
复制代码
二、dio
dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等
1、安装
dependencies:
dio: ^3.0.10
2、常用请求
以下实验基于Charles的MapLocal,json文件内容为{“animal”:”dog”}
– get
Response response;
Dio dio = _dio();
response = await dio.get("http://test?id=1&name=dio1&method=get");
response = await dio.get("http://test", queryParameters: {"id": 2, "name": "dio2"});
print(response.data['animal']);
复制代码
– post
Response response;
Dio dio = _dio();
response = await dio.post("http://test?id=1&name=dio1&method=post");
response = await dio.post("http://test", queryParameters: {"id": 2, "name": "dio2"});
print(response.data['animal']);
response = await dio.post(
"http://test",
queryParameters: {"id": 2, "name": "dio2"},
onReceiveProgress: (int receive, int total) {
print("$receive $total");
},
);
复制代码
– 发起多个请求
Dio dio = _dio();
Future.wait([dio.post("http://test/test1"), dio.get("http://test/test2")]).then((e) {
print(e);
}).catchError((e) {});
结果为[{"animal":"dog"}, {"animal":"dog"}]
复制代码
– 下载文件
Dio dio = _dio();
Response response =
await dio.download("https://www.baidu.com/", "assets/data/test.html");
复制代码
– 上传文件
Response response;
Dio dio = _dio();
FormData formData;
formData = FormData.fromMap({
"animal": "dog",
});
response = await dio.post("http/test/upload", data: formData);
formData = FormData.fromMap({
"animal": "dog",
"files": [
await MultipartFile.fromFile("assets/data/test1.json", filename: "test1.json"),
await MultipartFile.fromFile("assets/data/test2.json", filename: "test2.json"),
]
});
response = await dio.post("http/test/upload", data: formData);
复制代码
3、配置dio
Dio dio = Dio();
dio.options.baseUrl = "https://www.xx.com/api";
dio.options.connectTimeout = 5000;
dio.options.receiveTimeout = 3000;
BaseOptions options = BaseOptions(
baseUrl: "https://www.xx.com/api",
connectTimeout: 5000,
receiveTimeout: 3000,
);
dio = Dio(options);
复制代码
4、请求配置
BaseOptions描述的是Dio实例发起网络请求的的公共配置,而Options类描述了每一个Http请求的配置信息,每一次请求都可以单独配置,单次请求的Options中的配置信息可以覆盖BaseOptions中的配置,下面是BaseOptions的配置项:
{
String method;
String baseUrl;
Map<String, dynamic> headers;
int connectTimeout;
int receiveTimeout;
String path = "";
String contentType;
ResponseType responseType;
ValidateStatus validateStatus;
Map<String, dynamic> extra;
Map<String, dynamic > queryParameters;
}
复制代码
5、响应数据
当请求成功时会返回一个Response对象,它包含如下字段:
{
T data;
Headers headers;
Options request;
int statusCode;
bool isRedirect;
List<RedirectInfo> redirects ;
Uri realUri;
Map<String, dynamic> extra;
}
复制代码
6、拦截器
我们可以通过继承Interceptor来实现自定义的拦截器
每个 Dio 实例都可以添加任意多个拦截器,他们组成一个队列,拦截器队列的执行顺序是FIFO。通过拦截器你可以在请求之前或响应之后(但还没有被 then 或 catchError处理)做一些统一的预处理操作。
dio.interceptors.add(InterceptorsWrapper(
onRequest:(RequestOptions options) async {
return options;
},
onResponse:(Response response) async {
return response;
},
onError: (DioError e) async {
return e;
}
));
复制代码
7、拦截器中可以进行其他异步操作
dio.interceptors.add(InterceptorsWrapper(
onRequest:(Options options) async{
Response response = await dio.get("/token");
options.headers["token"] = response.data["data"]["token"];
return options;
}
));
复制代码
8、Lock/unlock拦截器
你可以通过调用拦截器的 lock()/unlock 方法来锁定/解锁拦截器。一旦请求/响应拦截器被锁定,接下来的请求/响应将会在进入请求/响应拦截器之前排队等待,直到解锁后,这些入队的请求才会继续执行(进入拦截器)。这在一些需要串行化请求/响应的场景中非常实用,后面我们将给出一个示例。
tokenDio = Dio();
tokenDio.options = dio.options;
dio.interceptors.add(InterceptorsWrapper(
onRequest:(Options options) async {
dio.interceptors.requestLock.lock();
Response response = await tokenDio.get("/token");
options.headers["token"] = response.data["data"]["token"];
dio.interceptors.requestLock.unlock();
return options;
}
));
假设这么一个场景:我们需要给每个请求头中设置token,如果token不存在我们需要先请求token,获取到再继续请求,由于请求token过程是异步的,所以我们需要先锁定拦截器防止其他请求在没有获取到token的情况下进行网络请求,获取到token再解锁
复制代码
9、clear()方法来清空等待队列
dio.interceptors.clear()
复制代码
10、日志(开启后会打印request和response相关信息)
dio.interceptors.add(LogInterceptor(responseBody: false));
复制代码
11、DioError
{
RequestOptions request;
Response response;
DioErrorType type;
dynamic error;
}
enum DioErrorType {
CONNECT_TIMEOUT,
SEND_TIMEOUT,
RECEIVE_TIMEOUT,
RESPONSE,
CANCEL,
DEFAULT
}
复制代码
12、CancelToken,取消请求
CancelToken token = CancelToken();
dio.post("/testpost?id=1&name=dio1&method=post",cancelToken: token).catchError((e) {
if (CancelToken.isCancel(err)) {
print("被取消啦");
}
}).then((data) {
return data;
});
token.cancel();
cancel_token.dart中源码也是判断DioErrorType
static bool isCancel(DioError e) {
return e.type == DioErrorType.CANCEL;
}
复制代码
13、dio和HttpClient关系
HttpClientAdapter是 Dio 和 HttpClient之间的桥梁。2.0抽象出adapter主要是方便切换、定制底层网络库。Dio实现了一套标准的、强大API,而HttpClient则是真正发起Http请求的对象。我们通过HttpClientAdapter将Dio和HttpClient解耦,这样一来便可以自由定制Http请求的底层实现,比如,在Flutter中我们可以通过自定义HttpClientAdapter将Http请求转发到Native中,然后再由Native统一发起请求。再比如,假如有一天OKHttp提供了dart版,你想使用OKHttp发起http请求,那么你便可以通过适配器来无缝切换到OKHttp,而不用改之前的代码。 Dio 使用DefaultHttpClientAdapter作为其默认HttpClientAdapter,DefaultHttpClientAdapter使用dart:io:HttpClient 来发起网络请求。
扩展(适配器模式) 首页定义接口,接口中对要实现的功能加以抽象,然后定义不同的Adapter类来实现这个接口,Adapter类中是对接口中方法的不同实现,上层的调用代码不需要改变就可以随意切换对底层不同的功能调用。
14、设置代理
DefaultHttpClientAdapter 提供了一个onHttpClientCreate 回调来设置底层 HttpClient的代理,我们想使用代理,可以参考下面代码:
import 'package:dio/dio.dart';
import 'package:dio/adapter.dart';
...
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.findProxy = (uri) {
return "PROXY localhost:8888";
};
};
复制代码
15、部分源码分析
dio.dart中
网络请求最终会调用到_request方法
当Response的泛型类为String且声明的ResponseType不为bytes和stream时
mergeOptions是将Dio的BaseOptions属性结合请求参数Options来生成一个RequestOptions对象,是最终发起网络请求的Options
Future<Response<T>> _request<T>(
String path, {
data,
Map<String, dynamic> queryParameters,
CancelToken cancelToken,
Options options,
ProgressCallback onSendProgress,
ProgressCallback onReceiveProgress,
}) async {
if (_closed) {
throw DioError(error: "Dio can't establish new connection after closed.");
}
options ??= Options();
if (options is RequestOptions) {
data = data ?? options.data;
queryParameters = queryParameters ?? options.queryParameters;
cancelToken = cancelToken ?? options.cancelToken;
onSendProgress = onSendProgress ?? options.onSendProgress;
onReceiveProgress = onReceiveProgress ?? options.onReceiveProgress;
}
var requestOptions = mergeOptions(options, path, data, queryParameters);
requestOptions.onReceiveProgress = onReceiveProgress;
requestOptions.onSendProgress = onSendProgress;
requestOptions.cancelToken = cancelToken;
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
拦截器会判断
checkIfNeedEnqueue方法的作用就是判断是否有未处理完(判断是否处理完从而加锁是通过Completer实现的)的请求,如果有的话,本次请求需要排队等待之前的请求的完成(值得学习的是由于一次请求返回的是Future,所以这里利用了future.then((Callback))返回的还是一个Future对象的特点,巧妙的实现了多次请求的顺序执行而相互之间不会干扰),这里的then中的Callback回调就是checkIfNeedEnqueue的第二个参数。
Function _interceptorWrapper(interceptor, bool request) {
return (data) async {
var type = request ? (data is RequestOptions) : (data is Response);
var lock =
request ? interceptors.requestLock : interceptors.responseLock;
if (_isErrorOrException(data) || type) {
return listenCancelForAsyncTask(
cancelToken,
Future(() {
return checkIfNeedEnqueue(lock, () {
if (type) {
if (!request) data.request = data.request ?? requestOptions;
return interceptor(data).then((e) => e ?? data);
} else {
throw assureDioError(data, requestOptions);
}
});
}),
);
} else {
return assureResponse(data, requestOptions);
}
};
}
FutureOr checkIfNeedEnqueue(Lock lock, EnqueueCallback callback) {
if (lock.locked) {
return lock.enqueue(callback);
} else {
return callback();
}
}
Future enqueue(EnqueueCallback callback) {
if (locked) {
return _lock.then((d) => callback());
}
return null;
}
Future<Response<T>> _dispatchRequest<T>(RequestOptions options) async {
var cancelToken = options.cancelToken;
ResponseBody responseBody;
try {
var stream = await _transformData(options);
responseBody = await httpClientAdapter.fetch(
options,
stream,
cancelToken?.whenCancel,
);
responseBody.headers = responseBody.headers ?? {};
var headers = Headers.fromMap(responseBody.headers ?? {});
var ret = Response(
headers: headers,
request: options,
redirects: responseBody.redirects ?? [],
isRedirect: responseBody.isRedirect,
statusCode: responseBody.statusCode,
statusMessage: responseBody.statusMessage,
extra: responseBody.extra,
);
var statusOk = options.validateStatus(responseBody.statusCode);
if (statusOk || options.receiveDataWhenStatusError) {
var forceConvert = !(T == dynamic || T == String) &&
!(options.responseType == ResponseType.bytes ||
options.responseType == ResponseType.stream);
String contentType;
if (forceConvert) {
contentType = headers.value(Headers.contentTypeHeader);
headers.set(Headers.contentTypeHeader, Headers.jsonContentType);
}
ret.data = await transformer.transformResponse(options, responseBody);
if (forceConvert) {
headers.set(Headers.contentTypeHeader, contentType);
}
} else {
await responseBody.stream.listen(null).cancel();
}
checkCancelled(cancelToken);
if (statusOk) {
return checkIfNeedEnqueue(interceptors.responseLock, () => ret);
} else {
throw DioError(
response: ret,
error: 'Http status error [${responseBody.statusCode}]',
type: DioErrorType.RESPONSE,
);
}
} catch (e) {
throw assureDioError(e, options);
}
}
class DefaultHttpClientAdapter implements HttpClientAdapter {
OnHttpClientCreate onHttpClientCreate;
HttpClient _defaultHttpClient;
bool _closed = false;
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<List<int>> requestStream,
Future cancelFuture,
) async {
if (_closed) {
throw Exception(
"Can't establish connection after [HttpClientAdapter] closed!");
}
var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
Future requestFuture = _httpClient.openUrl(options.method, options.uri);
.....(略)
}
复制代码
16、扩展:为什么需要通过代码设置代理才可以使用charles抓包呢?
因为当我们启动 Charles就是启动了一个HTTP代理服务器,这类工具会通知操作系统,“现在我在系统上创建了一个HTTP代理,IP为XXXXXX端口为XX。这个时候运行在系统上的http客户端再去发送请求的时候,他就不会再去进行DNS解析,去连接目标服务器,而是直接连接系统告诉他代理所在的地址。然后代理服务器会与客户端建立连接,再然后代理服务器根据请求信息再去连接真正的服务器。而在Flutter中的http.dart有如下说明
/**
//所以如果我们没有通过代码进行设置,就会直接请求到真正的服务器而不会走代理服务器