Dart 源码分析:深入理解 dart:io HttpClient

HttpClient

   HttpClient是Dart SDK中提供的标准的访问网络的接口类,是HTTP1.1/RFC2616协议在Dart SDK上的具体实现,用于客户端发送HTTP/S 请求。HttpClient 包含了一组方法,可以发送 HttpClientRequest 到Http服务器, 并接收 HttpClientResponse 作为服务器的响应。 例如, 我们可以用 get, getUrl, post, 和 postUrl 方法分别发送 GET 和 POST 请求。

  例如,一个简单的使用场景如下:

import "dart:io";
import 'dart:convert';
main() async {
  var baidu = "http://www.baidu.com";
  var httpClient = HttpClient();
  // Step 1: get HttpClientRequest
  HttpClientRequest request = await httpClient.getUrl(Uri.parse(baidu));
  // Step2: get HttpClientResponse
  HttpClientResponse response = await request.close();
 // Step3: consume HttpClientResponse
  var responseBody = await response.transform(Utf8Decoder()).join();
// Step4: close connection.
  httpClient.close();
}

代码解释:

  • 步骤一:新建HttpClient对象,通过 getUrl方法获取 HttpClientRequest;
  • 步骤二:通过HttpClientRequest.close(),发起Http请求, 获取 HttpClientResponse;
  • 步骤三:HttpClientResponse是一个Stream对象,通过Utf8Decoder解码,然后join操作符转换成String对象,可以打印出HttpClientResponse 的字符串。
  • 步骤四:关闭HttpClient.

  本文从源码角度简单理解上述代码执行过程,从而更好的(避免掉坑的)使用HttpClient。

背景知识

cd dart-sdk/sdk
./tools/build.py --mode debug --arch x64 create_sdk
  • 本文源码基于Dart 2.5
dart --version
Dart VM version: 2.5.0-dev.1.0 (Unknown timestamp) on "linux_x64"

一、流程分析

0. 顶层流程:

HttpClient 及相关模块实际上实现的是TCP/IP的Http协议栈,例如下图所示的Http部分:


tcp_ip.jpg

模块对上层应用暴露的接口就是HttpClient,客户端可以通过API发起Http请求并接收Http响应。
模块下层依赖的是TCP协议栈,从代码实现上而言就是依赖Socket/SecureSocket,因为在操作系统上Sockt封装了TCP/IP的所有操作,便于上层协议处理。
因此,本文开始提供的demo,用流程图可以简单描述为HttpClient, Socket和Server之间的关系,如下图所示:


HttpClient_level0.png

最左侧流程就是本文将详细分析的代码流程。

顶层流程分析:

  • Step 1: HttpClient getUrl 获取 HttpClientRequest的过程:
    这个过程实质上是sockt建立TCP链接的过程:
  1. sockt需要通过DNS解析把域名转换为ip地址
  2. 然后通过TCP的三次握手,建立socket链接,Dart中用HttpClientConnection保存这个链接。
  3. 构建一个HttpClientRequest对象,并返回客户端。客户端可以在这个对象中添加更多应用相关的Http包头字段,等待发送。
    注意到这个过程仅仅是建立socket链路,并没有实际发送数据。
  • Step 2: HttpClientRequest.close 表明HttpClientRequest已经构建完成,socket发送Http请求。收到响应后返回给客户端。
  • Step 3: HttpClientRsponse被消费后,HttpClient关闭链接。socket发送TCP四次挥手信息,关闭传输,并释放所有资源。

1. Step1 详细分析 HttpClient.openUrl流程:

openUrl两个工作:建立链接,获取HttpClientRequest对象:


step1_getUrl.png
  • 1.1 HttpClient 作为library暴露的API,定义在
    /dart-sdk/lib/_http/http.dart,通过工厂方法调用实现类_HttpClient;
    所以HttpClient.getUrl 调用的是 _HttpClient.getUrl;
  factory HttpClient({SecurityContext context}) {
    HttpOverrides overrides = HttpOverrides.current;
    if (overrides == null) {
      return new _HttpClient(context);
    }
    return overrides.createHttpClient(context);
  }
  • 1.2 API 封装了常用的get,post,put,delete,head,patch等方法,统一由_HttpClient._openUrl 处理
  Future<HttpClientRequest> openUrl(String method, Uri url) => _openUrl(method, url);
  Future<HttpClientRequest> get(String host, int port, String path) => open("get", host, port, path);
  Future<HttpClientRequest> getUrl(Uri url) => _openUrl("get", url);
  Future<HttpClientRequest> post(String host, int port, String path) => open("post", host, port, path);
  Future<HttpClientRequest> postUrl(Uri url) => _openUrl("post", url);
  Future<HttpClientRequest> put(String host, int port, String path) => open("put", host, port, path);
  Future<HttpClientRequest> putUrl(Uri url) => _openUrl("put", url);
  Future<HttpClientRequest> delete(String host, int port, String path) =>open("delete", host, port, path);
  Future<HttpClientRequest> deleteUrl(Uri url) => _openUrl("delete", url);
  Future<HttpClientRequest> head(String host, int port, String path) => open("head", host, port, path);
  Future<HttpClientRequest> headUrl(Uri url) => _openUrl("head", url);
  Future<HttpClientRequest> patch(String host, int port, String path) => open("patch", host, port, path);
  Future<HttpClientRequest> patchUrl(Uri url) => _openUrl("patch", url);
  • 1.3 _HttpClient._openUrl 首先需要获取一个_HttpClientConnection对象,然后通过这个_HttpClientConnection对象的send方法获取一个HttpClientRequest对象,返回给调用方。
    解释两点:
    1.由于_getConnection是异步调用,这里用到了Future.then方法获取_ConnectionInfo对象,_HttpClientConnection包含在_ConnectionInfo对象成员变量中,如果使用到了代理,代理信息也会保存在_ConnectionInfo对象中。
    2.Dart中匿名函数也是一个对象,此对象也可以定义自己的方法。例如下面代码中send就是定义在匿名对象中的方法。具体请参考language-tour#lexical-scope
    return _getConnection(uri.host, port, proxyConf, isSecure)
        .then((_ConnectionInfo info) {
      _HttpClientRequest send(_ConnectionInfo info) {
        return info.connection
            .send(uri, port, method.toUpperCase(), info.proxy);
      }、
      return send(info);
    });
  • 1.4 _HttpClient._openUrl第一步,首先分析_HttpClient._getConnection 建立链接并获取_HttpClientConnection的过程;
  • 1.4.1 _getConnectionTarget 根据host port target信息,从缓存的Map中,获取一个_ConnectionTarget,如果没有就新建一个。然后调用_ConnectionTarget.connect方法建立链接。如果建立成功就返回一个_ConnectionInfo对象。
  // Get a new _HttpClientConnection, from the matching _ConnectionTarget.
  Future<_ConnectionInfo> _getConnection(String uriHost, int uriPort,
      _ProxyConfiguration proxyConf, bool isSecure) {
    Iterator<_Proxy> proxies = proxyConf.proxies.iterator;

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

    return connect(new HttpException("No proxies given"));
  }
  • 1.4.2 _ConnectionTarget.connect 根据是否使用代理,是否使用https分别建立不同的链接。
    本文案例先分析最简单场景:不使用代理,建立http链接。
    因此_ConnectionTarget通过socket接口直接和目标地址建立链接:
// simplified codes    
Future<ConnectionTask> connectionTask =  Socket.startConnect(host, port));

一旦socket发起链接,connectionTask就会执行到then 方法,socket建立链接后,会新建立一个_HttpClientConnection对象,包含这个socket,并且封装成_ConnectionInfo, 返回给调用者

        var connection = new _HttpClientConnection(key, socket, client, false, context);
......
return new _ConnectionInfo(connection, proxy);

调用者就是1.3 节_HttpClient._openUrl._getConnection的地方,获取后可以执行then操作。

  • 1.4.3 Socket.startConnect的流程包含了DNS解析和tcp链路建立两个过程,代码在sdk/lib/io目录下, 限于篇幅,在此不再详细展开。
  • 1.5 获取_HttpClientConnection 建立链接后,_HttpClient._openUrl执行第二步,通过_HttpClientConnection.send,获取 HttpClientRequest;
  _HttpClientRequest send(Uri uri, int port, String method, _Proxy proxy) {
......
    var outgoing = new _HttpOutgoing(_socket);
    // Create new request object, wrapping the outgoing connection.
    var request =
        new _HttpClientRequest(outgoing, uri, method, proxy, _httpClient, this);
    _streamFuture = outgoing.done.then<Socket>((Socket s) {
        _nextResponseCompleter.future.then((incoming) {
                incoming.dataDone.then((closing) {
                 ......
                }
        }
    }
    return request;
......
}

这里将建立的HttpOutgoing对象就是客户端 HttpRequest 的Buffer,_socket和HttpOutgoing关联,后续发送时通过这个socket直接发送。
_streamFuture 部分代码注册了一系列的回调,后续发送完Http的Request,接收到的数据及后续操作就在这里处理。

  • 到此,_HttpClient._openUrl 就获取到了_HttpClientRequest对象,demo程序的第一步流程全部结束:
  // Step 1: get HttpClientRequest
  HttpClientRequest request = await httpClient.getUrl(Uri.parse(baidu));

2. Step2 详细分析 HttpClientRequest.close 流程:

  • 2.1 HttpClientRequest.close触发socket 发送的过程:


    step2-1.png

    HttpClientRequest.close 首先调用父类_StreamSinkImpl<T>的close(), 最终会触发_HttpOutgoing.close完成发送。
    然后,再返回一个done对象。done对象完成需要等待两个返回条件,一个是HttpRequest发送完成,一个是收到服务器的HttpResponse,这里是用Future.wait方式实现的。Future.wait可以类比为Java中的CyclicBarrier,当Future队列中各个任务都完成时,Future.then方法才会被调用。

  Future<HttpClientResponse> get done {
    if (_response == null) {
      _response =
          Future.wait([_responseCompleter.future, super.done], eagerError: true)
              .then((list) => list[0]);
    }
    return _response;
  }

  Future<HttpClientResponse> close() {
    super.close();
    return done;
  }
  • 2.1.1 首先分析_HttpOutgoing 的发送过程。
    HttpClientRequest 被设计为一个实现了IOSink接口的类
abstract class HttpClientRequest implements IOSink {}

因此,调用者可以通过write的方式往这个流里面写数据。

     HttpClientRequest request = ...
     request.headers.contentType
         = new ContentType("application", "json", charset: "utf-8");
     request.write(...);  // Strings written will be UTF-8 encoded.

在写完所有数据后,需要调用request.close() 发送这个HttpRequest。本节会分析这个发送HttpRequest并收到对应的HttpResponse的过程。

在1.5节 _HttpClientConnection.send 新建_HttpClientRequest对象时,第一个构造函数传入了一个_HttpOutgoing对象。

    var outgoing = new _HttpOutgoing(_socket);
    // Create new request object, wrapping the outgoing connection.
    var request =  new _HttpClientRequest(outgoing, uri, method, proxy, _httpClient, this);

根据继承关系,_HttpClientRequest继承了_StreamSinkImpl<T>,这个对象包含一个_target成员,而_HttpOutgoing 继承 StreamConsumer,并且构造的时候被注册为一个target。

class _StreamSinkImpl<T> implements StreamSink<T> {
  final StreamConsumer<T> _target;

因此,_HttpClientRequest.close() 时,_StreamSinkImpl会closeTarget,因此调用_HttpOutgoing.close

  Future close() {
    if (_isBound) {
      throw new StateError("StreamSink is bound to a stream");
    }
    if (!_isClosed) {
      _isClosed = true;
      if (_controllerInstance != null) {
        _controllerInstance.close();
      } else {
        ********* closed here ************
        _closeTarget();
      }
    }
    return done;
  }

最终在finalize 方法中,通过socket.flush发送数据。一旦发送完成,通过_doneCompleter通知发送完成。

      return socket.flush().then((_) {
        print('socket.flush().then  _doneCompleter.complete');
        _doneCompleter.complete(socket);
        return outbound;
      }

HttpClientRequest.close done的第一个条件完成。

  • 2.2 HttpClientRequest 收到服务端HttpResponse的过程:
    HttpClientRequest.close done 完成的第二个条件是,收到服务端响应,也就是_responseCompleter.future完成。此条件完成的流程如下图所示:


    step2-2.png

流程分析:
在openUrl时创建了_HttpClientConnection对象,构造函数为Socket注册了onData事件的回调,即_HttpParser。因此每当Socket有数据进来时,都会触发_HttpParser的onData进行处理。

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

    // Set up handlers on the parser here, so we are sure to get 'onDone' from
    // the parser.
    _subscription = _httpParser.listen((incoming) {......}

最终处理完成后,层层调用_HttpClientRequest的_responseCompleter。HttpClientRequest.close done的第二个条件完成。最终获取HttpClientResponse对象。

3. Step3 HttpClient.close 流程:

此流程比较简单,最终调用socket的close,TCP四次挥手断开链接。这里就不展开了。需要指出的是,如果不主动调用HttpClient.close,socket不会立即释放,链接会保留一段时间超时退出,因此存在资源泄漏的风险。

总结:

   到此为之,HttpClient发起一个get http请求并获取响应的流程分析完毕。
   简单而言客户端需要两个Future对象,

  • 第一个通过getUrl建立链接,获取HttpClientRequest对象。
  • 第二个通过HttpClientRequest.close 获取 HttpClientResponse对象。
    Dart这个模块大量使用了Future和Completer等异步处理工具,代码逻辑比较复杂,跟踪时需要非常仔细。
       另外,我之所以要分析HttpClient,是因为遇到了一个flutter pub get的问题 FLUTTER填坑笔记:从flutter pub get error 开始,定位Dart SDK问题,使用代理时HttpClient崩溃。通过代理进行Http通信的过程有更多的交互,流程也更为复杂,后续再补充这个过程的分析。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容