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
,实际的类间继承关系如下图。
该对象将被作为参数来调用
URLRequest
的
StartJob()
方法,但这其实就只是多了一层包装而已,实际上还是相当于在调用
URLRequest(Http)Job
对象的
Start()
方法。在该方法体内,主要做了一些设置Headers以及Cookies的工作。此处需要注意,设置Cookie的操作是异步的,因此一些任务完成后所执行的操作被封装到一堆Closure里面去,作为回调函数来执行。
总之,在这里我们需要关注的是
URLRequestHttpJob::OnCookiesLoaded
这个函数,在它的方法体内,真正调用了
DoStartTransaction()
方法,开始实际的内容传输过程。然而事实上这个方法也只是个包装,它的内部会首先检查当前请求有没有被取消,如果有的话,就停止继续执行;否则,调用真正的
StartTransaction()
方法。由此,我们也能够看出Chromium异步编程模型的一个重要特点:每次执行异步回调的时候,才需要检查当前任务有没有被取消,还用不用继续下去。
StartTransaction()
方法做了一些通知Delegate的操作,然后继续调用
StartTransactionInternal()
。在这个方法中,首先会检查先前有没有创建过
HttpTransaction
对象,如果有,证明先前请求失败,现在需要重试;如果没有,就使用
HttpTransactionFactory
类的
CreateTransaction()
方法来创建一个新的
HttpTransaction
对象。这里使用的是工厂模式,创建的对象一般是
HttpNetworkTransaction
对象,实际的类间继承关系如下图:
随后,控制流被转移到
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
,它们的实际继承关系如下图:
此时控制流转移到
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,其类间继承关系如下:
一般的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
,其相应的继承关系如下:
此时控制流沿着
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()
方法。类继承关系如下图:
此时,接下来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
,它们之间的继承关系如下: