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

待兔
• 阅读 1157

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,这个对象包含一个_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通信的过程有更多的交互,流程也更为复杂,后续再补充这个过程的分析。
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3年前
Dart 源码分析:深入理解 dart:io HttpClient
HttpClient  HttpClient(https://links.jianshu.com/go?tohttps%3A%2F%2Fapi.dartlang.org%2Fstable%2F2.4.1%2Fdartio%2FHttpClientclass.html)是DartSDK中提供的标准的访问网络的接口类,是
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
2个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
待兔
待兔
Lv1
男 · helloworld公司 · CTO - helloworld开发者社区站长
helloworld开发者社区网站站长
文章
89
粉丝
43
获赞
75