Flutter Dio网络框架分析

前言

对新技术保持敏锐是一个程序猿的基本素养,其中Flutter是新技术中的佼佼者,那么对于晦涩难懂的框架源码我们该如何学习呢?

以Flutter中的Dio为例,如果想要了解Dio的源码设计,我们从那里开始分析?直接阅读源码?

相信大多数同学阅读源码都存在如下的问题:

  • 该从哪处下手
  • 学了忘,忘了学

造成这样的原因是因为你没有一个结构化思维,没有理解网络框架的本质。

如果你仍处于以上这种状态,那么接着往下看,这篇文章将非常适合你,我将带领大家对Dio框架进行抽丝剥茧,你可以学到的不仅是框架的设计,同样也是网络框架的基本设计规范,相信大家掌握以后不管是Android还是iOS亦或是Web的网络框架,你都能用同样的系统思维方式去分析。

导读

文章基于Dio 4.0.6进行分析,根据通用的网络请求流程来一步一步拆解Dio框架,不拘泥于源码解析,偏重于系统性分析:

  • 第一章 Dio请求流程介绍 系统的向读者介绍通用的的网络请求流程和Dio的架构设计,让读者对其有一个整体的结构化思维。
  • 第二章 输入数据 介绍Dio框架中输入数据时涉及的基本类。
  • 第三章 数据处理 基于第二章中输入的数据分析Dio中的数据处理流程。
  • 第四章 输出数据 简述Dio将数据输出给调用者的过程。
  • 第五、六章 基于前面的内容做一个总结。

目录

1. Dio请求流程介绍
2. 输入数据

3. 数据处理

4. 数据输出

5. 其他
6. 总结

1.Dio请求流程介绍

网络框架的请求流程和计算机的运行原理本质上是一样的,主要分为三大流程:

  • 输入数据
  • 数据处理
  • 输出数据

输入数据就像是计算机通过I/O接口输入指令及数据,数据处理好比CPU和内存对数据进行存储运算,输出数据则是通过I/O接口输出到电脑屏幕上。

网络框架图

网络框架.png

Dio框架图

image.png

Dio中 输入数据 通过App初始化配置和调用get或post方法传入的参数的组合。

数据处理 的过程相对复杂,但本质上就是固定步骤的指令操作数据的过程。

输出数据是将数据处理完成后返回给调用者。

下三章我们将分析下这三大流程的具体逻辑。

2.输入数据

Api入口

介绍Dio框架常用Api

Dio框架输入数据主要有下面两个入口:
1.初始化配置
2.业务调用

初始化配置 相当于数据预加载的过程,通常在App启动时进行,其本质是为了在 业务调用 时尽量简单,性能更佳。

Dio中 初始化配置 示例:

final dio = Dio(); // 初始化默认的配置

void configureDio() {
  // 通过BaseOptions设置默认配置
  dio.options.baseUrl = 'https://api.pub.dev';
  dio.options.connectTimeout = Duration(seconds: 5);
  dio.options.receiveTimeout = Duration(seconds: 3);
  
  
  //添加拦截器
  //自定义拦截器,请求、返回、异常三种拦截器
  dio.interceptors.add(RequestInterceptor());
  dio.interceptors.add(ErrorInterceptor());
  dio.interceptors.add(ResponceInterceptor());
  //日志记录
  dio.interceptors.add(LogInterceptor(responseBody: true));

  
 // 设置代理
 (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
  client.badCertificateCallback = (X509Certificate cert, String host, int port) =>     true;
   // config the http client
   client.findProxy = (uri) {
    //proxy all request to localhost:8888
    return "PROXY $proxy";
    };
  // you can also create a new HttpClient to dio
  // return new HttpClient();
  };

  // 通过BaseOptions构造函数设置
  final options = BaseOptions(
    baseUrl: 'https://api.pub.dev',
    connectTimeout: Duration(seconds: 5),
    receiveTimeout: Duration(seconds: 3),
  );
  final anotherDio = Dio(options);
}

业务调用 示例:


发送一个get请求

void request() async {
    Response response;
    //get方法的两种方式
    response = await dio.get('/test?id=12&name=dio');
    print(response.data.toString());
    // 另外一种
    response = await dio.get(
        '/test',
        queryParameters: {'id': 12, 'name': 'dio'},
      );
      print(response.data.toString());
    }

    //调用时也可通过Options设置一些基本属性
    Options options = Options(
      //连接服务器超时时间,单位是毫秒.
      sendTimeout: this.mConnectTimeout,
      //响应流上前后两次接受到数据的间隔,单位为毫秒。
      receiveTimeout: this.mReceiveTimeout,
      //Http请求头.
      headers: mHeaders,
      contentType: "application/json",
      //接受四种类型 `json`, `stream`, `plain`, `bytes`. 默认是 `json`,
      responseType: this.mResponseType,
    );

    //发送一个post请求
    response = await dio.post('/test', data: {'id': 12, 'name': 'dio',
    , options: options});
}   

通过代码示例,发现Dio输入数据时主要涉及的类有以下几种:

image.png

Dio

Dio类作为框架的入口类,主要有如下两个功能:

  • 组合了不同类型的配置数据
  • 提供get、post等业务调用方法

具体代码如下所示:

import 'entry_stub.dart'
// ignore: uri_does_not_exist
    if (dart.library.html) 'entry/dio_for_browser.dart'
// ignore: uri_does_not_exist
    if (dart.library.io) 'entry/dio_for_native.dart';

abstract class Dio {
    //工厂方法创建实现类
  factory Dio([BaseOptions? options]) => createDio(options);
   //下面是基础配置类 
  late BaseOptions options;

  Interceptors get interceptors;

  late HttpClientAdapter httpClientAdapter;

  late Transformer transformer;
  
   //下面是主要调用方法,还包含put、download等方法
  Future<Response<T>> get<T>(
      String path, {
      Map<String, dynamic>? queryParameters,
      Options? options,
      CancelToken? cancelToken,
      ProgressCallback? onReceiveProgress,
   });

   Future<Response<T>> post<T>( 
        String path, {
        data,
        Map<String, dynamic>? queryParameters,
        Options? options,
        CancelToken? cancelToken,
        ProgressCallback? onSendProgress,
        ProgressCallback? onReceiveProgress,
  });
}  

可以看到,Dio是一个抽象类,createDio是工厂方法,其实现类通过平台区分导入,在移动端中导入的是 dio_for_native.dart,这个文件中 createDio创建的是DioForNative对象。

class DioForNative with DioMixin implements Dio {
}

DioForNative 中提供的get、post等方法主要实现在其继承的DioMixin中。

除了基本的配置数据外,Dio还主要包含get、post方法,在调用这两个方法时传入的参数数据主要包含如下:

  • path--请求路径
  • params或data--请求参数
  • options--请求时传入的配置可以和基础的BaseOption合并,调用时传入的数据会覆盖之前设置的数据
  • cancelToken--用于取消当前请求的标记
  • onSendProgress和onReceiveProgress是记录一个发送和获取数据的过程,可用于加载进度条,打印日志等

BaseOption、Option

BaseOption和Option作用类似,都用做基础数据配置,最终会合并成一个对象RequestOptions。

Interceptor

Interceptor的设计借鉴的是Android OkHttp中插值器形式,使用的是责任链模式,这种优秀的设计使得开发不仅能介入网络请求请求前的数据处理,还可以对返回结果或错误异常进行拦截。

class Interceptor {

  // 发送请求前拦截  
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) =>
      handler.next(options);
  
  //在结果返回给调用者前拦截
  void onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) =>
      handler.next(response);
  
  //发生错误返回给调用者时拦截
  void onError(
    DioError err,
    ErrorInterceptorHandler handler,
  ) =>
      handler.next(err);
}

Interceptor在Dio框架中是通过队列保存的,队列是“FIFO”模式,也就是先添加的Interceptor会优先处理,后续添加相同的逻辑会覆盖之前的处理,通常会在onRequest中添加一些headers等操作,onResponse或onError中对结果处理成调用者想要的方式,记住onResponse和onError是互斥的,只有一个会被调用。

DefaultHttpClientAdapter

DefaultHttpClientAdapter是默认的网络请求配置适配类,这里使用适配器模式将底层网络框架的选择交给开发者,默认实现主要是基于HttpClient(Flutter官方提供的网络库),也可用来设置代理进行抓包,其中核心是fetch方法:

class DefaultHttpClientAdapter implements HttpClientAdapter {
  /// [Dio] will create HttpClient when it is needed.
  /// If [onHttpClientCreate] is provided, [Dio] will call
  /// it when a HttpClient created.
  OnHttpClientCreate? onHttpClientCreate;

  HttpClient? _defaultHttpClient;

    @override
    Future<ResponseBody> fetch(
      RequestOptions options,
      Stream<Uint8List>? requestStream,
      Future? cancelFuture,
    ) async {
      if (_closed) {
        throw Exception(
        "Can't establish connection after [HttpClientAdapter] closed!");
      }
      var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
      var reqFuture = _httpClient.openUrl(options.method, options.uri);
    }
}  

通过 dio.httpClientAdapter = 开发者自行实现的HttpClientAdapter开发者可自行配置网络请求部分,需要自行实现fetch方法。

第二章我们主要介绍了 Dio输入数据 时框架中主要涉及的类,但是对于 http请求 来说,这些数据还需要经过一定的 加工 才能转换成最终的http协议所需要的数据规范,那么第三章将为我们揭开Dio是如何在保证逻辑严谨的情况下进行 数据处理 的。

3.数据处理

上一章介绍输入数据和Http消息报文的数据格式并不相符,要想变成符合Http协议要求的数据格式,就需要将开发者输入的数据转换成如下图规范的消息数据格式,而网络框架数据处理的本质便是如此。

http的消息结构如下:

image.png

网络框架 数据处理 流程本质就是get或post方法调用过程,简单的流程如下:
1.将BaseOption和Options合并成RequestOptions对象
2.遍历并调用所有拦截器的onRequst方法,这个方法中重要的参数就是RequestOptions
3.调用dispatch方法开始传输数据
4.遍历并调用所有拦截器onResponce方法,这个方法中有一个重要的参数就是Responce
5.遍历并调用所有拦截器onError方法,这个方法中有一个重要的参数就是DioError
6.最后将结果Response返回给调用者

3.1 数据预处理

数据预处理是将输入的数据进行打包、合并的过程其中拦截器中还可以添加、修改请求配置数据。对应到流程中的1,2两个步骤。

Dio get和post实现方法是在DioMixin类中,可以看到不管是get还是post,都会调用到request方法中,实际开发者直接调用request也是可以的,只是需要多一个请求方法method的参数。

将BaseOption和Options合并成RequestOptions对象

@override
Future<Response<T>> request<T>(
  String path, {
  data,
  Map<String, dynamic>? queryParameters,
  CancelToken? cancelToken,
  Options? options,
  ProgressCallback? onSendProgress,
  ProgressCallback? onReceiveProgress,
}) async {
  options ??= Options();
  //将BaseOptions和Options合并成RequestOptions对象
  var requestOptions = options.compose(
    this.options,
    path,
    data: data,
    queryParameters: queryParameters,
    onReceiveProgress: onReceiveProgress,
    onSendProgress: onSendProgress,
    cancelToken: cancelToken,
  );
  requestOptions.onReceiveProgress = onReceiveProgress;
  requestOptions.onSendProgress = onSendProgress;
  requestOptions.cancelToken = cancelToken;

  return fetch<T>(requestOptions);
}

request方法首先将初始化时输入的BaseOptions对象和调用时传入的Options对象合并成RequestOptions对象,而RequestOptions将作为后续数据传递过程中的重要媒介。这里Options类有点多,容易对分析框架造成干扰,为避免影响分析,读者请仔细甄别。

遍历并调用所有拦截器的onRequst方法,这个方法中重要的参数就是RequestOptions

@override
Future<Response<T>> fetch<T>(RequestOptions requestOptions) async {

    // 对初始化中所有拦截器进行遍历并调用onRequest方法
    interceptors.forEach((Interceptor interceptor) {
       var fun = interceptor is QueuedInterceptor
          ? interceptor._handleRequest
          : interceptor.onRequest;
      future = future.then(_requestInterceptorWrapper(fun));
    });
   
    // 请求传输数据部分
    future = future.then(_requestInterceptorWrapper((
      RequestOptions reqOpt,
      RequestInterceptorHandler handler,
    ) {
      requestOptions = reqOpt;
      _dispatchRequest(reqOpt)
          .then((value) => handler.resolve(value, true))
          .catchError((e) {
        handler.reject(e as DioError, true);
      });
    }));
    
    // 对初始化中所有拦截器进行遍历并调用onResponse方法
    interceptors.forEach((Interceptor interceptor) {
      var fun = interceptor is QueuedInterceptor
          ? interceptor._handleResponse
          : interceptor.onResponse;
      future = future.then(_responseInterceptorWrapper(fun));
    });

    // 对初始化中所有拦截器进行遍历并调用onError方法
    interceptors.forEach((Interceptor interceptor) {
      var fun = interceptor is QueuedInterceptor
          ? interceptor._handleError
          : interceptor.onError;
      future = future.catchError(_errorInterceptorWrapper(fun));
    });
}

第一步合成RequestOptions对象后,RequestOptions对象就作为第二步拦截器中onRequest方法中的重要参数,每一个拦截器都可以对请求配置数据options进行拦截添加或修改。

数据预处理完成后接下来下来就需要知道数据传输的地址了,而弄清楚地址的第一步就是Uri解析

3.2 Uri解析

输入的数据中有一个URL参数,它是BaseUrl和path的组合,比如 www.baidu.com/home/index?id=1234&name=baidu

而Uri解析的目的是为了得到如下地址相关的数据:

  • http或者https,区分协议类型
  • 域名如 www.baidu.com ,用于DNS解析,指定服务器网络地址和主机地址的关键参数
  • path如 /home/index,指定获取后台数据的路径
  • params如id、name等,针对不同接口传入不同的参数类型,主要是后来用来做逻辑判断

这些数据都保存在一个Uri对象中,,所以本质上Uri解析是将Url拆解成一个对象的形式存储,方便后续获取,Dio中关于Uri解析的关键类在RequestOptions中,关键方法是的uri get方法:

class DefaultHttpClientAdapter implements HttpClientAdapter {
  /// [Dio] will create HttpClient when it is needed.
  /// If [onHttpClientCreate] is provided, [Dio] will call
  /// it when a HttpClient created.
  OnHttpClientCreate? onHttpClientCreate;

  HttpClient? _defaultHttpClient;

    @override
    Future<ResponseBody> fetch(
      RequestOptions options,
      Stream<Uint8List>? requestStream,
      Future? cancelFuture,
    ) async {
      if (_closed) {
        throw Exception(
        "Can't establish connection after [HttpClientAdapter] closed!");
      }
      var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
      //建立TCP链接,options.uri表示
      var reqFuture = _httpClient.openUrl(options.method, options.uri);
    }
}  

//通过options.uri便可获得所需的Uri数据
/// generate uri
Uri get uri {
  var _url = path;
  if (!_url.startsWith(RegExp(r'https?:'))) {
    _url = baseUrl + _url;
    var s = _url.split(':/');
    if (s.length == 2) {
      _url = s[0] + ':/' + s[1].replaceAll('//', '/');
    }
  }
  var query = Transformer.urlEncodeMap(queryParameters, listFormat);
  if (query.isNotEmpty) {
    _url += (_url.contains('?') ? '&' : '?') + query;
  }
  // Normalize the url.
  return Uri.parse(_url).normalizePath();
}

可以看到,通过对输入数据时传入的baseUrl和path参数组合,再经过Uri解析转换,就可以得到消息结构中的部分关键数据了。

但是对于操作系统支持的http协议来说,域名是一个陌生的概念,真正识别的服务器网络地址和主机地址的是ip地址和端口号,那么要想知道域名对应的服务器地址和端口号,就需要进行DNS解析,在下一节将介绍DNS解析相关的内容。

3.3 DNS解析

通过Uri解析我们得到了域名等相关的信息,但不管是操作系统还是其他网络设备,都不能识别域名而只能识别ip地址和端口号,所以需要一种能通过域名查询ip地址(如192.168.1.1)和端口号(如88)的服务,这个服务就是DNS解析

下面我们解答关于域名和ip地址常见的两个疑问

1,为什么不直接使用ip地址和端口号?
答:当然可以用,但是因为ip地址不易分辨且难以记忆,比如“wwww.baidu.com“就比较符合正常人的思维习惯。

2,Http协议为什么不直接使用域名?
答:域名占用的字节数相对ip地址更大,对整体传输的效率会打折扣,且不如ip这种数字地址容易分配。

DNS解析的过程通常在socket中,对于开发者来说并不需要直接接触,但部分优秀的框架也会自行实现DNS解析的逻辑,Dio中进行DNS解析是在HttpClient框架中的socket组件,根据平台(Android/Web)不同实现也不一,这里我们不详细展开,借助一张关于HttpClient的时序图。

image.png

在获取到ip地址和端口号以后,接下来就需要通过获取到的信息建立TCP连接了。

3.4 建立TCP连接

准备工作完成后,通过一系列调用来到DefaultHttpClientAdapter的fetch方法中,其中_httpClient.openUrl就是建立TCP连接通道的流程:

class DefaultHttpClientAdapter implements HttpClientAdapter {
  /// [Dio] will create HttpClient when it is needed.
  /// If [onHttpClientCreate] is provided, [Dio] will call
  /// it when a HttpClient created.
  OnHttpClientCreate? onHttpClientCreate;

  HttpClient? _defaultHttpClient;

    @override
    Future<ResponseBody> fetch(
      RequestOptions options,
      Stream<Uint8List>? requestStream,
      Future? cancelFuture,
    ) async {
      if (_closed) {
        throw Exception(
        "Can't establish connection after [HttpClientAdapter] closed!");
      }
      var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
      var reqFuture = _httpClient.openUrl(options.method, options.uri);
    }
}  

下面是时序图:


image.png

openUrl方法的参数是请求方式method(get、post等)和Uri解析后生成的uri对象,可以看出Dio框架只是收集整理数据,核心逻辑仍然在HttpClient中。

abstract class HttpClient {
  static const int defaultHttpPort = 80;
  @Deprecated("Use defaultHttpPort instead")
  static const int DEFAULT_HTTP_PORT = defaultHttpPort;

  static const int defaultHttpsPort = 443;
  @Deprecated("Use defaultHttpsPort instead")
  static const int DEFAULT_HTTPS_PORT = defaultHttpsPort;
  
  Future<HttpClientRequest> openUrl(String method, Uri url);
}  

HttpClient是一个抽象类,其实现在http_imp.dart文件中_HttpClient类的_openUrl方法。

Future<_HttpClientRequest> _openUrl(String method, Uri uri) {
  return _getConnection(uri, uri.host, port, proxyConf, isSecure, profileData)
      .then((_ConnectionInfo info) {
    _HttpClientRequest send(_ConnectionInfo info) {
      profileData?.requestEvent('Connection established');
      return info.connection
          .send(uri, port, method.toUpperCase(), info.proxy, profileData);
    }

    // If the connection was closed before the request was sent, create
    // and use another connection.
    if (info.connection.closed) {
      return _getConnection(
              uri, uri.host, port, proxyConf, isSecure, profileData)
          .then(send);
    }
    return send(info);
  }, onError: (error) {
    profileData?.finishRequestWithError(error.toString());
    throw error;
  });
}

// Get a new _HttpClientConnection, from the matching _ConnectionTarget.
Future<_ConnectionInfo> _getConnection(
    Uri uri,
    String uriHost,
    int uriPort,
    _ProxyConfiguration proxyConf,
    bool isSecure,
    _HttpProfileData? profileData) {
  Iterator<_Proxy> proxies = proxyConf.proxies.iterator;

  Future<_ConnectionInfo> connect(error, stackTrace) {
    if (!proxies.moveNext()) return Future.error(error, stackTrace);
    _Proxy proxy = proxies.current;
    String host = proxy.isDirect ? uriHost : proxy.host!;
    int port = proxy.isDirect ? uriPort : proxy.port!;
    return _getConnectionTarget(host, port, isSecure)
        .connect(uri, uriHost, uriPort, proxy, this, profileData)
        // On error, continue with next proxy.
        .catchError(connect);
  }

  return connect(HttpException("No proxies given"), StackTrace.current);
}

通过层层方法的调用,我们最终找到真正进行TCP连接的是关于Https的SecureSocket和http的Socket的startConnect方法中

external static Future<ConnectionTask<Socket>> _startConnect(host, int port,
    {sourceAddress, int sourcePort = 0});

上面的external表示Android/iOS平台有各自的socket代码实现,但最终的逻辑是相同的,有DNS解析和TCP三次握手,其中https的SecureSocket在进行TCP连接之前还增加了一个证书校验过程,也就是https的四次握手。

到这里,TCP连接通道已经建立好了,那么接下来就是数据传输过程了,下一节我们将先介绍Dio框架中基于IP协议的数据传输过程。

3.5 IP传输数据

建立好TCP通道后,IP协议开发发挥作用了,是时候传输数据了

class DefaultHttpClientAdapter implements HttpClientAdapter {
  /// [Dio] will create HttpClient when it is needed.
  /// If [onHttpClientCreate] is provided, [Dio] will call
  /// it when a HttpClient created.
  OnHttpClientCreate? onHttpClientCreate;

  HttpClient? _defaultHttpClient;

    @override
    Future<ResponseBody> fetch(
      RequestOptions options,
      Stream<Uint8List>? requestStream,
      Future? cancelFuture,
    ) async {
      if (_closed) {
        throw Exception(
        "Can't establish connection after [HttpClientAdapter] closed!");
      }
      //创建HttpClient,建立TCPl连接
      var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
      var reqFuture = _httpClient.openUrl(options.method, options.uri);
      
      late HttpClientRequest request;
      request = await reqFuture;
        //开始IP传输数据
      var future = request.close();
    }
}  

DefaultHttpClientAdapter中传输数据的方法入口就是request.close(很想吐槽close这个方法名),request建立TCP连接时调用_httpClient.openUrl返回的HttpClientRequest对象,调用流程如下图所示(借图):

image.png

// HttpClientRequest的实现类_HttpClientRequest中的close方法
Future<HttpClientResponse> close() {
  if (!_aborted) {
    // It will send out the request.
    super.close();
  }
  return done;
}

// 上面的super.close()调用的是_HttpClientRequest间接继承的_StreamSinkImpl类中的close方法
Future close() {
  if (_isBound) {
    throw StateError("StreamSink is bound to a stream");
  }
  if (!_isClosed) {
    _isClosed = true;
    var controller = _controllerInstance;
    if (controller != null) {
      controller.close();
    } else {
      _closeTarget();
    }
  }
  return done;
}

void _closeTarget() {
  _target.close().then(_completeDoneValue, onError: _completeDoneError);
}

通过一系列调用,核心逻辑在 _target这个变量中,_target是StreamConsumer,HttpOutgoing则继承自StreamConsumer,所以核心逻辑就是在http_imp.dart文件中HttpOutgoing的close方法中,HttpOutgoing中保存有socket对象,可用于发送数据。

class _HttpOutgoing implements StreamConsumer<List<int>> {

    Future close() {
        Future finalize() {
            return socket.flush().then((_) {
              _doneCompleter.complete(socket);
              return outbound;
            }, onError: (error, stackTrace) {
              _doneCompleter.completeError(error, stackTrace);
              if (_ignoreError(error)) {
                return outbound;
              } else {
                throw error;
              }
            });
        }
        
        var future = writeHeaders();
        if (future != null) {
          return _closeFuture = future.whenComplete(finalize);
        }
        return _closeFuture = finalize();
    }
}

socket.flush()就是发送数据的过程,数据通过TCP建立的通道传输给服务器,服务器接收到客户端输入的数据后,也需要对数据进行并将数据返回给客户端。

3.6 服务器接收并处理数据

服务器接收数据相当于服务器的数据输入,只不过数据是经过客户端Dio框架加工而成。而服务器拿到数据也需要对数据进行处理,处理流程如下:

image.png

由于涉及到后台逻辑,此处不做详细展开,有兴趣的同学可以自行google。

3.7 客户端接收并处理返回数据

上一节中后台通过TCP返回数据,客户端通过TCP接收数据,在接收到数据以后就需要对数据进行处理。

首先我们来看下标准的返回数据格式:


image.png

可以看到通过socket返回的数据格数如上图所示,如果此时直接返回给调用者,调用方需要不停的进行io操作读取字符串,使用起来异常麻烦,这就需要框架对数据进行加工处理,返回调用者自己想要的数据形式。

在建立TCP连接openUrl时创建了_HttpClientConnection对象,构造函数为Socket注册了onData事件的回调,即_HttpParser用于接收后台返回的数据。因此每当Socket有数据进来时,都会触发_HttpParser的onData进行处理。


image.png
  _HttpClientConnection(this.key, this._socket, this._httpClient,
      [this._proxyTunnel = false, this._context])
      : _httpParser = new _HttpParser.responseParser() {
    _httpParser.listenToStream(_socket);

    _subscription = _httpParser.listen((incoming) {......}

处理完成后,通过层层调用至_HttpClientRequest的_responseCompleter,最终获取到HttpClientResponse对象。

4.输出数据

4.1 返回responce并断开连接

上一章数据处理完成后最终得到的是HttpClientResponse对象,Dio框架则通过将HttpClientResponse包装成Response对象返回给调用者,如果发生网络请求错误或异常则会将错误包装成DioError对象,这其中还涉及到Intercetor队列中onResponce和onError的拦截处理。

void onResponse(
  Response response,
  ResponseInterceptorHandler handler,
) =>
    handler.next(response);
    
void onError(
  DioError err,
  ErrorInterceptorHandler handler,
) =>
    handler.next(err);

5.其他

至此Dio的大致框架已经清晰,文章并不拘泥了源码的细节,主要是从系统的网络请求流程上分析,除上述流程外,读者对于框架中可能还会存在如下部分疑惑:

  • Cookie和Session
  • 加密Basice和Digest
  • Gzip
  • 代理、网关和隧道
  • 缓存
  • https

限于篇幅,本篇并上面部分做详细介绍,下一篇笔者将基于Dio框架对以上部分做详细分析。

6.总结

综上所述,我们首先介绍了Dio框架的数据输入流程,有初始化配置和业务调用两类,其次介绍了框架对输入数据进行处理的过程,最后就需要将处理完的数据输出给调用者

输入数据
一般分为两种:

  • 初始化配置
  • 业务调用

数据处理 的过程比较复杂,详细在后续讲解,
Dio的数据处理流程大致分为如下7个步骤:

  • 数据预处理
  • URL解析
  • DNS解析
  • 建立TCP连接
  • IP传输数据
  • 服务器接收并处理数据
  • 客户端接收并处理返回数据

输出数据是将完整的数据返回给调用端

文章对Dio框架进行了一个基本的解构,主要是想让读者拥有一个网络框架系统性的思维,你是否想过,Android/iOS/Web的网络框架设计流程是否也是如此?根据上述流程,基于现有的技术栈能否自己手动撸一个网络框架?答案是一定的。

建议对照流程手动分析Dio框架

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容