天天看点

Cronet URL请求流程分析

Java JNI接口

首先我们分析应用层的请求流程,Java示例代码如下:

Executor executor = Executors.newSingleThreadExecutor();
UrlRequest.Callback callback = new SimpleUrlRequestCallback();
UrlRequest.Builder builder =
    new UrlRequest.Builder(url, callback, executor, mCronetEngine);
applyPostDataToUrlRequestBuilder(builder, executor, postData);
builder.build().start();
      

从上述代码中可以看出,基于建造者模式,

UrlRequest.Builder

会产生一个

UrlRequest

对象,该对象带有一个

start()

方法用于发起请求;请求执行后,会在

executor

线程调用

UrlRequest.Callback

对象的相应回调函数,从而完成一次资源请求流程。

由于

UrlRequest

实际上只是一个抽象接口,它真正的实现是在

CronetUrlRequest

这个类中。该类所实现的

start()

方法中,最终会使用

nativeStart()

方法,调用C++原生函数,来启动URL请求过程。这个

nativeStart()

方法实现在

CronetUrlRequest_jni.h

里面,是一个自动生成的接口函数,作用是将Java调用转换为相应的C++调用。那么实际上,与上层Java代码进行适配的,是在C++代码中定义的

CronetURLRequestAdapter

类。至此,执行流程就从上层移交到底层C++部分。下面我们开始分析URLRequest的实际工作流程。

C++适配

在C++这一层,与一次网络请求严格对应的是一个

URLRequest

对象,该对象的构造函数是一个私有方法,这也就意味着它只能被相应的建造者所构造。事实上,“URLRequests are always created by calling 

URLRequestContext::CreateRequest

”,而

URLRequestContext

对象也是有着对应的Java包装的,因此这方面我们不需要考虑太多细节。

CronetURLRequestAdapter

内部,直接包含了一个该对象的指针,它的

Start()

方法会将自身的

StartOnNetworkThread()

方法POST到网络线程来执行。那么在

StartOnNetworkThread()

方法中,所做的事情就是在真正核心的

URLRequest

对象上面设置一些属性,然后在它上面调用

Start()

方法,发起请求操作了。

URLRequest请求流程

URLRequest

对象调用

Start()

方法后,会利用

URLRequestJobManager

CreateJob()

方法,根据所请求资源类型的不同,来创建一个

URLRequestJob

对象,也就是一个任务实体。一般情况下,产生的对象会是

URLRequestHttpJob

,实际的类间继承关系如下图。

Cronet URL请求流程分析

该对象将被作为参数来调用

URLRequest

StartJob()

方法,但这其实就只是多了一层包装而已,实际上还是相当于在调用

URLRequest(Http)Job

对象的

Start()

方法。在该方法体内,主要做了一些设置Headers以及Cookies的工作。此处需要注意,设置Cookie的操作是异步的,因此一些任务完成后所执行的操作被封装到一堆Closure里面去,作为回调函数来执行。

总之,在这里我们需要关注的是

URLRequestHttpJob::OnCookiesLoaded

这个函数,在它的方法体内,真正调用了

DoStartTransaction()

方法,开始实际的内容传输过程。然而事实上这个方法也只是个包装,它的内部会首先检查当前请求有没有被取消,如果有的话,就停止继续执行;否则,调用真正的

StartTransaction()

方法。由此,我们也能够看出Chromium异步编程模型的一个重要特点:每次执行异步回调的时候,才需要检查当前任务有没有被取消,还用不用继续下去。

StartTransaction()

方法做了一些通知Delegate的操作,然后继续调用

StartTransactionInternal()

。在这个方法中,首先会检查先前有没有创建过

HttpTransaction

对象,如果有,证明先前请求失败,现在需要重试;如果没有,就使用

HttpTransactionFactory

类的

CreateTransaction()

方法来创建一个新的

HttpTransaction

对象。这里使用的是工厂模式,创建的对象一般是

HttpNetworkTransaction

对象,实际的类间继承关系如下图:

Cronet URL请求流程分析

随后,控制流被转移到

HttpNetworkTransaction

对象的

Start()

方法上面去。该方法会设置一些参数,然后进入

DoLoop()

方法。由此,我们已经进入了HTTP状态机,接下来的事情都会基于这个状态机来执行。

关于这个状态机,我们需要注意一些细节。每次执行到阻塞操作的时候,会返回一个

ERR_IO_PENDING

状态码,它会令状态机循环直接

break

掉。那么状态还怎么转移并执行下去呢?事实上

HttpNetworkTransaction

类提供了一个回调函数

OnIOComplete

,阻塞调用结束后,它会重新去调用

DoLoop()

,从而继续进行下一个状态。

HTTP状态机

在Chromium网络栈中,所有HTTP相关的状态被严谨地作如下定义:

enum State {
    STATE_NOTIFY_BEFORE_CREATE_STREAM,
    STATE_CREATE_STREAM,
    STATE_CREATE_STREAM_COMPLETE,
    STATE_INIT_STREAM,
    STATE_INIT_STREAM_COMPLETE,
    STATE_GENERATE_PROXY_AUTH_TOKEN,
    STATE_GENERATE_PROXY_AUTH_TOKEN_COMPLETE,
    STATE_GENERATE_SERVER_AUTH_TOKEN,
    STATE_GENERATE_SERVER_AUTH_TOKEN_COMPLETE,
    STATE_GET_TOKEN_BINDING_KEY,
    STATE_GET_TOKEN_BINDING_KEY_COMPLETE,
    STATE_INIT_REQUEST_BODY,
    STATE_INIT_REQUEST_BODY_COMPLETE,
    STATE_BUILD_REQUEST,
    STATE_BUILD_REQUEST_COMPLETE,
    STATE_SEND_REQUEST,
    STATE_SEND_REQUEST_COMPLETE,
    STATE_READ_HEADERS,
    STATE_READ_HEADERS_COMPLETE,
    STATE_READ_BODY,
    STATE_READ_BODY_COMPLETE,
    STATE_DRAIN_BODY_FOR_AUTH_RESTART,
    STATE_DRAIN_BODY_FOR_AUTH_RESTART_COMPLETE,
    STATE_NONE
};
      

其中,后缀为"COMPLETE"的状态表示上一个操作成功,需要执行相应的回调操作等等。因此,如果需要进行HTTP层面的数据采集工作的话,我们需要将相应的阶段对应到这个状态机里面,然后采集相应的数据,最后通过某种方式映射回

URLRequest

对象即可。下面,我们重点分析几个与性能指标相关的阶段所对应的状态。

STATE_CREATE_STREAM

这个状态对应于

HttpNetworkTransaction

DoCreateStream()

方法,该方法会根据协议的不同,分别向

HttpStreamFactory

(实际上是

HttpStreamFactoryImpl

)请求不同的

HttpStream

,这里再次采用了工厂模式。注意

HttpStream

同样是一个抽象类,这里实际涉及到的是

HttpBasicStream

,它们的实际继承关系如下图:

Cronet URL请求流程分析

此时控制流转移到

HttpStreamFactoryImpl

类,我们简单地只关注

RequestStream()

方法,它继续直接调用

RequestStreamInternal()

。在该方法内,创建了一个

HttpStreamFactoryImpl::Job

对象用来抽象地表示一次请求HttpStream的任务,并在这个对象上面调用

Start()

方法,于是控制流再次发生了转移。

HttpStreamFactoryImpl::Job

对象内,起实际作用的是

RunLoop()

这个方法。它分为两部分:前半部分运行一个状态机

DoLoop()

,用于分配资源并创建一个HttpStream;后半部分检查创建流程是否成功,并根据不同的错误进行不同的操作。像SSL异常之类的问题,基本都会导致打不开一个Stream,因此会体现在这个状态机以及相应的错误码中。在该状态机中,所有状态定义如下:

enum State {
    STATE_START,
    STATE_RESOLVE_PROXY,
    STATE_RESOLVE_PROXY_COMPLETE,
    STATE_WAIT_FOR_JOB,
    STATE_WAIT_FOR_JOB_COMPLETE,
    STATE_INIT_CONNECTION,
    STATE_INIT_CONNECTION_COMPLETE,
    STATE_WAITING_USER_ACTION,
    STATE_RESTART_TUNNEL_AUTH,
    STATE_RESTART_TUNNEL_AUTH_COMPLETE,
    STATE_CREATE_STREAM,
    STATE_CREATE_STREAM_COMPLETE,
    STATE_DRAIN_BODY_FOR_AUTH_RESTART,
    STATE_DRAIN_BODY_FOR_AUTH_RESTART_COMPLETE,
    STATE_DONE,
    STATE_NONE
};
      

我们重点关注其中一部分状态。

STATE_INIT_CONNECTION

该状态对应于

HttpStreamFactoryImpl::Job::DoInitConnection()

方法,它的目标是要对自身一个类型为

ClientSocketHandle

的成员变量进行初始化,而它所做的事情基本就是根据当前的请求配置,来设置一堆的参数。最终,实际调用的方法是同一个类中的

InitSocketPoolHelper()

。这个函数同样是进行了各种参数配置之类的工作,然后控制流转移到

ClientSocketHandle::Init()

函数,来初始化这个

ClientSocketHandle

对象。该对象会尝试通过

ClientSocketPool::RequestSocket()

方法,向Socket Pool请求一个有效的Socket。注意在Chromium中其实是包含各种类型的Socket Pool,其类间继承关系如下:

Cronet URL请求流程分析

一般的HTTP请求,用到的其实是

TransportClientSocketPool

这个Socket Pool。并且,Socket Pool会有一个“分组”的概念,这个“分组”其实就是不同域名与不同的Socket集合的对应关系。换句话说,如果访问"www.baidu.com",那么向Socket Pool请求Socket的时候,所查询的分组名称就是"www.baidu.com:80"。因为这个

connection_group

其实是这样生成的:

// Build the string used to uniquely identify connections of this type.
// Determine the host and port to connect to.
std::string connection_group = origin_host_port.ToString();
      

回到正题,上述的

RequestSocket()

函数调用会经过层层辗转,最后控制流转移到

ClientSocketPoolBaseHelper::RequestSocket()

上面来,然后进入

RequestSocketInternal()

方法,从而执行真正的Socket分配任务。当然,我们很有可能根本找不到一个可重用的空闲Socket,此时我们就需要用

ConnectJobFactory

创建

ConnectJob

对象,调用其

Connect()

方法来打开一个新Socket,也就是建立相应的连接了。这里注意到一个细节,如果当

connect_job->Connect()

返回

ERR_IO_PENDING

,也就是IO过程发起,但是并没有立刻完成的时候,此时Chromium会判断是否允许启用备份连接,如果允许的话,就再创建一个250ms后启动的连接任务。这个任务的作用是说,如果第一个连接的SYN握手包意外丢了,那么在等待TCP超时的过程中,这个备份连接会更早地建立,它可以立刻顶上,从而显著降低连接延迟。

此外,注意

ConnectJob

也是一个虚基类,我们实际用到的会是

TransportConnectJob

,其相应的继承关系如下:

Cronet URL请求流程分析

此时控制流沿着

Connect()

方法被转移到

TransportConnectJobHelper::DoConnectInternal()

,并在这里进入该对象的状态机

DoLoop()

。这个状态机非常简单,其状态定义如下:

enum State {
    STATE_RESOLVE_HOST,
    STATE_RESOLVE_HOST_COMPLETE,
    STATE_TRANSPORT_CONNECT,
    STATE_TRANSPORT_CONNECT_COMPLETE,
    STATE_NONE,
};
      

所以其实也就是俩状态:先解析Host,然后传输层发起TCP Connect而已。这里的Host解析过程会由

TransportConnectJobHelper::DoResolveHost()

来执行,它会调用

SingleRequestHostResolver::Resolve()

方法来完成解析,而这个方法实际执行的则是

HostResolverImpl::Resolve()

方法。类继承关系如下图:

Cronet URL请求流程分析

此时,接下来DNS的详细流程就可以参考我的上一篇博客Chromium DNS流程分析了。

STATE_CREATE_STREAM

该函数用于打开一个实际的类型为

HttpBasicStream

的流对象,并创建了属于该对象的

HttpBasicState

对象。

STATE_INIT_STREAM

该状态对应于

HttpNetworkTransaction::DoInitStream()

方法。相比于上一个状态,这个状态所做的事情相对简单,就只是设置一些参数而已,比如读取连接的服务端IP、初始化

HttpBasicStream

所包含的

HttpBasicState

对象之类。当然,比较重要的一点是,

HttpStreamParser

是由

HttpBasicState

对象在这个状态中创建的。

STATE_INIT_REQUEST_BODY

STATE_BUILD_REQUEST

STATE_SEND_REQUEST

该状态对应于

HttpNetworkTransaction::DoSendRequest()

,实际上是通过

HttpBasicStream::SendRequest()

调用到了

HttpStreamParser::SendRequest()

。随后会调用

HttpStreamParser

自身包含的

DoLoop()

方法,其对应的状态机定义如下:

enum State {
    STATE_NONE,
    STATE_SEND_HEADERS,
    STATE_SEND_HEADERS_COMPLETE,
    STATE_SEND_BODY,
    STATE_SEND_BODY_COMPLETE,
    STATE_SEND_REQUEST_READ_BODY_COMPLETE,
    STATE_READ_HEADERS,
    STATE_READ_HEADERS_COMPLETE,
    STATE_READ_BODY,
    STATE_READ_BODY_COMPLETE,
    STATE_DONE
};
      

由此我们可以看出,该对象主要进行数据的准备,并在诸如

HttpStreamParser::DoSendHeaders()

之类的方法中将数据写入到

StreamSocket

里面去,从而完成数据的发送。这里注意

StreamSocket

是个抽象类,多数情况下我们用到的是

TCPClientSocket

,它们之间的继承关系如下:

Cronet URL请求流程分析

STATE_READ_HEADERS¶

STATE_READ_BODY

继续阅读