Dart 原始碼分析:深入理解 dart:io HttpClient

TonyBuilder發表於2019-08-19

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。

背景知識

  • Dart 非同步程式設計框架:
    Dart IO庫中大量使用了Future,Stream,IOSink等非同步處理方法和流處理的方法,為了對原始碼有更好的理解,讀者應具備這方面相關知識,限於篇幅,相關知識請參考相關API文件。api.flutter.dev/flutter/dar…
  • 原始碼路徑:/dart-sdk/sdk/sdk/lib/io; /dart-sdk/sdk/sdk/lib/_http;
    下載程式碼 github.com/dart-lang/s…
    編譯指導 github.com/dart-lang/s…
    編譯debug 版本

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部分:

Dart 原始碼分析:深入理解 dart:io HttpClient

        模組對上層應用暴露的介面就是HttpClient,客戶端可以通過API發起Http請求並接收Http響應。
        模組下層依賴的是TCP協議棧,從程式碼實現上而言就是依賴Socket/SecureSocket,因為在作業系統上Sockt封裝了TCP/IP的所有操作,便於上層協議處理。
        因此,本文開始提供的demo,用流程圖可以簡單描述為HttpClient, Socket和Server之間的關係,如下圖所示:

Dart 原始碼分析:深入理解 dart:io HttpClient

最左側流程就是本文將詳細分析的程式碼流程。

頂層流程分析:

  • 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物件:

Dart 原始碼分析:深入理解 dart:io HttpClient


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 _HttpClient

        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 _HttpClientConnection

        _HttpClient._openUrl 首先需要獲取一個_HttpClientConnection物件,然後通過這個_HttpClientConnection物件的send方法獲取一個HttpClientRequest物件,返回給呼叫方。

    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. 由於_getConnection是非同步呼叫,這裡用到了Future.then方法獲取_ConnectionInfo物件,_HttpClientConnection包含在_ConnectionInfo物件成員變數中,如果使用到了代理,代理資訊也會儲存在_ConnectionInfo物件中。
  2. Dart中匿名函式也是一個物件,此物件也可以定義自己的方法。例如下面程式碼中send就是定義在匿名物件中的方法。具體請參考language-tour#lexical-scope

1.4 _HttpClient._openUrl第一步,_getConnection

        首先分析_HttpClient._getConnection 建立連結並獲取_HttpClientConnection的過程。

1.4.1 _getConnection

        _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

        _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

        Socket.startConnect的流程包含了DNS解析和tcp鏈路建立兩個過程,程式碼在sdk/lib/io目錄下, 限於篇幅,在此不再詳細展開。

1.5 _HttpClientConnection.send

        獲取_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程式的第一步流程全部結束,客戶端獲取到了HttpClientRequest

  // Step 1: get HttpClientRequest
  HttpClientRequest request = await httpClient.getUrl(Uri.parse(baidu));複製程式碼

2. Step2 詳細分析 HttpClientRequest.close 流程:

2.1 HttpClientRequest.close

HttpClientRequest.close觸發socket 傳送的過程如圖所示:

Dart 原始碼分析:深入理解 dart:io HttpClient

        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 的傳送過程

首先分析_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完成。此條件完成的流程如下圖所示:

Dart 原始碼分析:深入理解 dart:io HttpClient

流程分析:
在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通訊的過程有更多的互動,流程也更為複雜,後續再補充這個過程的分析。


相關文章