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部分:
模組對上層應用暴露的介面就是HttpClient,客戶端可以通過API發起Http請求並接收Http響應。
模組下層依賴的是TCP協議棧,從程式碼實現上而言就是依賴Socket/SecureSocket,因為在作業系統上Sockt封裝了TCP/IP的所有操作,便於上層協議處理。
因此,本文開始提供的demo,用流程圖可以簡單描述為HttpClient, Socket和Server之間的關係,如下圖所示:
最左側流程就是本文將詳細分析的程式碼流程。
頂層流程分析:
- Step 1: HttpClient getUrl 獲取 HttpClientRequest的過程:
這個過程實質上是sockt建立TCP連結的過程:
- sockt需要通過DNS解析把域名轉換為ip地址
- 然後通過TCP的三次握手,建立socket連結,Dart中用HttpClientConnection儲存這個連結。
- 構建一個HttpClientRequest物件,並返回客戶端。客戶端可以在這個物件中新增更多應用相關的Http包頭欄位,等待傳送。
注意到這個過程僅僅是建立socket鏈路,並沒有實際傳送資料。
- Step 2: HttpClientRequest.close 表明HttpClientRequest已經構建完成,socket傳送Http請求。收到響應後返回給客戶端。
- Step 3: HttpClientRsponse被消費後,HttpClient關閉連結。socket傳送TCP四次揮手資訊,關閉傳輸,並釋放所有資源。
1. Step1 詳細分析 HttpClient.openUrl流程:
openUrl兩個工作:建立連結,獲取HttpClientRequest物件:
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);
});複製程式碼
這段程式碼有兩點需要解釋一下:
- 由於_getConnection是非同步呼叫,這裡用到了Future.then方法獲取_ConnectionInfo物件,_HttpClientConnection包含在_ConnectionInfo物件成員變數中,如果使用到了代理,代理資訊也會儲存在_ConnectionInfo物件中。
- 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 傳送的過程如圖所示:
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完成。此條件完成的流程如下圖所示:
流程分析:
在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通訊的過程有更多的互動,流程也更為複雜,後續再補充這個過程的分析。