chromium-net - 跟随 Cronet 的脚步探索大致流程(1)

899 阅读10分钟

Cronet是什么?

From chatgpt

Cronet(Chromium Network Stack)是Google Chrome浏览器的网络堆栈的开源版本,用于处理网络请求和响应。它是一个用于Android平台的网络库,提供了高性能的网络功能,包括HTTP请求、数据下载、WebSocket支持等。Cronet的目标是为Android应用程序提供高效、可靠和安全的网络通信能力。

以下是Cronet的一些特点和功能:

  1. 性能优化: Cronet旨在提供高性能的网络通信,通过多线程处理和异步操作来最大程度地减少网络延迟。

  2. HTTP/2支持: Cronet支持HTTP/2,这是一种现代的、高效的HTTP协议,可以加快页面加载速度并减少网络资源的浪费。

  3. 安全性: Cronet支持HTTPS,可以确保数据在传输过程中的加密和安全性。

  4. Quic支持: Cronet还支持QUIC协议,这是Google开发的一种基于UDP的传输协议,旨在提供更低的延迟和更高的性能。

  5. WebSocket支持: Cronet可以用于处理WebSocket连接,使应用程序可以进行实时双向通信。

  6. 缓存管理: Cronet可以自动管理缓存,以提高资源的重复利用率,并减少不必要的网络请求。

  7. 可配置性: Cronet允许开发人员配置各种网络参数,以满足应用程序的需求。

Cronet最初是为Chrome浏览器的Android版本开发的,但后来被开源并提供给其他Android应用程序开发人员使用。它为开发人员提供了更多灵活性和控制,使他们能够更好地管理应用程序的网络通信,同时受益于Google Chrome团队的网络优化经验。

需要注意的是,Cronet在不同版本中可能会有一些变化和改进,因此在使用时应查阅官方文档以获取最新的信息和指南。

Chromium-net是什么?

From chatgpt

Chromium Net(简称Chromium网络)是Chromium项目的一部分,用于处理网络通信的底层组件。Chromium是一个开源项目,它是Google Chrome浏览器的基础,因此Chromium Net在Chrome浏览器中用于处理网络请求和响应。

以下是Chromium Net的一些主要特点和功能:

  1. 网络协议支持: Chromium Net支持各种网络协议,包括HTTP、HTTPS、HTTP/2、QUIC(Quick UDP Internet Connections)等。这使得Chrome浏览器能够与Web服务器进行高效的通信,并利用现代网络协议提供更快的页面加载速度和更好的性能。

  2. 网络安全性: Chromium Net负责处理HTTPS通信,确保通过SSL/TLS加密传输的数据的安全性。它还实施了一系列的安全策略和机制,用于保护用户免受网络攻击和恶意网站的威胁。

  3. 代理支持: 该组件允许配置代理服务器,以允许网络请求通过代理服务器进行中转,从而实现更高级的网络控制和隐私保护。

  4. 流量管理: Chromium Net可以帮助浏览器管理网络流量,包括请求和响应的缓存、重定向、流量控制等。

  5. 数据压缩: 它支持数据压缩,以减少传输的数据量,提高网络性能。

  6. HTTP缓存: Chromium Net包括HTTP缓存管理,以在本地存储和管理已下载的资源,以减少重复下载和提高页面加载速度。

Chromium Net是Chromium项目中的一个关键组件,它使得Chrome浏览器能够在各种网络条件下提供出色的性能和安全性。除了在Chrome浏览器中使用外,Chromium Net的一些部分也可以在其他项目中使用,以处理网络通信。它的开源性质使得开发人员可以查看其源代码并进行自定义,以满足特定需求。需要注意的是,Chromium Net是一个复杂的网络堆栈,需要深入的专业知识来理解和配置。

如何使用Cronet发起请求

Cronet提供两种发起请求的方式,异步和同步。

  1. 异步

    val requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executorService) requestBuilder.build().start()

  2. 同步

    val connection = engine.openConnection(url) URL.setURLStreamHandlerFactory(engine.createURLStreamHandlerFactory())

跟代码

  1. JAVA 层

本文将跟着异步调用的方式跟着代码看网络请求大致流程,所以我们跟代码的入口是:

CronetUrlRequest.start()

//CronetUrlRequest.java
@Override
public void start() {
    synchronized (mUrlRequestAdapterLock) {
        ...
        startInternalLocked();
    }
}

private void startInternalLocked() {
    CronetUrlRequestJni.get().start(mUrlRequestAdapter, CronetUrlRequest.this);
}

//CronetUrlRequestJni.class
public void start(long nativePtr, CronetUrlRequest caller) {
    GEN_JNI.org_chromium_net_impl_CronetUrlRequest_start(nativePtr, caller);
}
  1. cronet_url_request.cc

从上面的流程可以看到,java 层就是纯粹的包装层,C++层的入口是 cronet_url_request.cc#start(),那我们直接去看这个文件。

//cronet_url_request.cc
void CronetURLRequest::Start() {
  DCHECK(!context_->IsOnNetworkThread());
  context_->PostTaskToNetworkThread(
      FROM_HERE,
      base::BindOnce(&CronetURLRequest::NetworkTasks::Start,
                     base::Unretained(&network_tasks_),
                     base::Unretained(context_), initial_method_,
                     std::move(initial_request_headers_), std::move(upload_)));
}

实际上逻辑是在 NetworkTasks 中

//CronetRequest::NetworkTasks
void CronetURLRequest::NetworkTasks::Start(
    CronetURLRequestContext* context,
    const std::string& method,
    std::unique_ptr<net::HttpRequestHeaders> request_headers,
    std::unique_ptr<net::UploadDataStream> upload) {
  url_request_ = context->GetURLRequestContext()->CreateRequest(
      initial_url_, net::DEFAULT_PRIORITY, this, MISSING_TRAFFIC_ANNOTATION);
  //... 设置一堆请求头、优先级之类的东西
  url_request_->set_method(method);
  //...
  url_request_->Start();
}

从上面这个函数体来看,cronet 的 C++ 层代码跑到这里基本流程就结束了,后续流程嫁接进 chromuim-net 模块了,先通过 url_request_context 构造 url_request,再调用 url_request 的 Start() 方法发起请求。

  1. net/url_request/url_request_context.cc

构造 url_request

//net/url_request/url_request_context.cc
std::unique_ptr<URLRequest> URLRequestContext::CreateRequest(
    const GURL& url,
    RequestPriority priority,
    URLRequest::Delegate* delegate,
    NetworkTrafficAnnotationTag traffic_annotation,
    bool is_for_websockets) const {
  return base::WrapUnique(new URLRequest(
      url, priority, delegate, this, traffic_annotation, is_for_websockets));
}
  1. net/url_request/url_request.cc

调用 url_request 的 Start() 函数进入 chromium-net 的核心网络请求流程。

//net/url_request/url_request.cc
void URLRequest::Start() {
  //... 检查一堆东西
  load_timing_info_ = LoadTimingInfo();
  load_timing_info_.request_start_time = response_info_.request_time;
  load_timing_info_.request_start = base::TimeTicks::Now();
  //... 省去network_delegate逻辑
  StartJob(context_->job_factory()->CreateJob(this));
}

StartJob(context_->job_factory()->CreateJob(this))这一句核心代码包含两个元素,一个是通过 URLRequestJobFactory 构造一个 URLRequestJob,然后 startJob,这两个类所在的文件是:

  • net/url_request/url_request_job_factory.h

  • net/url_request/url_request_job.h

在继续往底下深入看之前,用我们自己的思路大概猜一下,URLRequestJob 是单个网络请求任务的包装类,内部包含着各个阶段的流转,而 URLRequest 是 job 的外观类。我们继续看 StartJob 函数:

//net/url_request/url_request.cc
void URLRequest::StartJob(std::unique_ptr<URLRequestJob> job) {
  //跟上面一样的,向 job 设置一堆东西,同时设置进去一堆 callback
  job_ = std::move(job);
  job_->SetExtraRequestHeaders(extra_request_headers_);
  job_->SetPriority(priority_);
  job_->SetRequestHeadersCallback(request_headers_callback_);
  job_->SetEarlyResponseHeadersCallback(early_response_headers_callback_);
  job_->SetResponseHeadersCallback(response_headers_callback_);

  //... 省略

  // Start() always completes asynchronously.
  //
  // Status is generally set by URLRequestJob itself, but Start() calls
  // directly into the URLRequestJob subclass, so URLRequestJob can't set it
  // here.
  status_ = ERR_IO_PENDING;
  job_->Start();
}

这里可以看到,当 URLRequestJob start 后,URLRequest 的状态变成了 ERR_IO_PENDING,这是 chromium-net里到处都能见到的状态机流转来实现异步等待的方式,有许多地方设置状态,状态机收到状态后做相应的响应,ERR_IO_PENDING 的状态说明当前线程已经 pending 了,需要等待 IO 线程执行完任务后解除 pending 状态。

  1. net/url_request/url_request_job.h

那我们继续往下看,目前跟到了 URLRequestJob 的 start 方法了

// net/url_request/url_request_job.h

// If any error occurs while starting the Job, NotifyStartError should be
// called asynchronously.
// This helps ensure that all errors follow more similar notification code
// paths, which should simplify testing.
virtual void Start() = 0;

URLRequestJob::Start()方法是个虚方法,是由子类实现具体的方法体的,因为我们重点关注 http 的请求,所以 直接去 url_request_http_job 里看 http 协议相关实现即可

  1. net/url_request/url_request_http_job.cc

直接看 Start() 方法

//net/url_request/url_request_http_job.cc
void URLRequestHttpJob::Start() {
  //给 HttpRequestInfo 设置一堆东西
  request_info_.url = request_->url();
  request_info_.method = request_->method();
  //...省略一堆
  //注入 header
  AddExtraHeaders();
  //注入 cookie 并且开始请求
  AddCookieHeaderAndStart();
}

//net/url_request/url_request_http_job.cc
void URLRequestHttpJob::AddCookieHeaderAndStart() {
  //省略一堆东西,如果是需要异步拿 Cookie 的话,拿完 Cookie 后,入口会变成 SetCookieHeaderAndStart
  StartTransaction();
}
//net/url_request/url_request_http_job.cc
void URLRequestHttpJob::StartTransaction() {
  //省略network_delegate逻辑
  StartTransactionInternal();
}

//net/url_request/url_request_http_job.cc
void URLRequestHttpJob::StartTransactionInternal() {
    //省略一堆transaction_已经存在,走 restart 的逻辑,这里会去通过 http_transaction_factory 创建 HttpTransaction
    //这里的 http_transaction_factory 是 HttpNetworkLayer,transaction 是 HttpNetworkTransaction
    rv = request_->context()->http_transaction_factory()->CreateTransaction(
        priority_, &transaction_);
    //省略一堆 wss:// 和 wso://逻辑
    if (rv == OK) {
      // 给 transation 注入一堆 callback, 
      transaction_->SetConnectedCallback(base::BindRepeating(
          &URLRequestHttpJob::NotifyConnectedCallback, base::Unretained(this)));
      transaction_->SetRequestHeadersCallback(request_headers_callback_);
      
        rv = transaction_->Start(
            &request_info_,
            base::BindOnce(&URLRequestHttpJob::OnStartCompleted,
                           base::Unretained(this)),
            request_->net_log());
     }
     //...省略对外部的回调
}
  1. net/http/http_network_transaction.cc

那我们继续看 HttpNetworkTransaction 的 Start() 入口

//net/http/http_network_transaction.cc
int HttpNetworkTransaction::Start(const HttpRequestInfo* request_info,
                                  CompletionOnceCallback callback,
                                  const NetLogWithSource& net_log) {
  //...
  session_->GetSSLConfig(&server_ssl_config_, &proxy_ssl_config_);
  //...
  next_state_ = STATE_NOTIFY_BEFORE_CREATE_STREAM;
  int rv = DoLoop(OK);
  if (rv == ERR_IO_PENDING)
    callback_ = std::move(callback);
  // This always returns ERR_IO_PENDING because DoCreateStream() does, but
  // GenerateNetworkErrorLoggingReportIfError() should be called here if any
  // other net::Error can be returned.
  DCHECK_EQ(rv, ERR_IO_PENDING);
  return rv;
}

这里出现了上面我们说过的状态机,通过设置 next_state_(即下一步动作), DoLoop 转动状态轮盘,执行下一步任务,这里下一步的状态是准备创建 stream,stream 是请求流,可以理解为服务器和客户端 TCP 连接内用于交换帧数据的独立双向序列,如果协议是 http1.0,那么一个 TCP 连接只有一个 stream,如果是 http2,由于多路复用的特性,多个 stream 可以共用一个 TCP 连接。 既然这里已经走到了创建 stream 的状态了,大概也可以猜到,网络请求的状态流转就是依赖HttpNetworkTransaction 中的状态机,这有点像是 OKHttp 中的 InterceptorChain,一个是通过状态流转,一个是链式推进,那我们看看 HttpNetworkTransaction::DoLoop 函数究竟长啥样。

//net/http/http_network_transaction.cc
int HttpNetworkTransaction::DoLoop(int result) {
  DCHECK(next_state_ != STATE_NONE);
  int rv = result;
  do {
    State state = next_state_;
    next_state_ = STATE_NONE;
    switch (state) {
      case STATE_NOTIFY_BEFORE_CREATE_STREAM: 
        //准备创建 stream,直接下一状态
        rv = DoNotifyBeforeCreateStream();
        break;
      case STATE_CREATE_STREAM: 
        //创建 stream,整个过程包括从 SocketPool 分配 Socket、握手、建连过程,这里需要异步等待
        //因此 DoCreateStream 方法会返回 ERR_IO_PENDING 暂时退出循环
        //同时,http_network_transaction 实现了HttpStreamRequest::Delegate,即成功/失败后的 callback
        //等到 HttpStream 创建完成后,会回调实现的HttpStreamRequest::Delegate::OnStreamReady 方法
        //在 OnStreamReady方法里,调用OnIOComplete,继续 DoLoop,进入下一个状态
        //注意,这里的 HttpStream 只是持有了 Socket,并没有在这个 Socket 上初始化 stream
        rv = DoCreateStream();
        break;
      case STATE_CREATE_STREAM_COMPLETE:
        //创建 HttpStream 结束,如果是 H2,是 SpdyHttpStream,如果是 QUIC,是 QuicHttpStream
        rv = DoCreateStreamComplete(rv); 
        break;
      case STATE_INIT_STREAM:
        //构造SpdySession,SpdySession 将持有 SessionSocket
        //通过 SpdySession 构造 SpdyStream,这个类将直接操作 SpdyBuffer(IOBuffer)
        //用来读写字节流,所以这里的 initStream 类似于 
        //BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        rv = DoInitStream();
        break;
      case STATE_INIT_STREAM_COMPLETE:
        //SpdyStream 创建完成后,回调上层,连接成功了,在这个函数里会 invoke connected_callback_ 进入下一状态
        rv = DoInitStreamComplete(rv);
        break;
      case STATE_CONNECTED_CALLBACK_COMPLETE:
        //connected_callback_ 被调用了,这个函数内只改了next_state_ 继续loop
        rv = DoConnectedCallbackComplete(rv);
        break;
      case STATE_GENERATE_PROXY_AUTH_TOKEN:
        //如果没有 http proxy,会直接下一状态
        rv = DoGenerateProxyAuthToken();
        break;
      case STATE_GENERATE_PROXY_AUTH_TOKEN_COMPLETE:
        //这个函数内只改了next_state_ 继续loop
        rv = DoGenerateProxyAuthTokenComplete(rv);
        break;
      case STATE_GENERATE_SERVER_AUTH_TOKEN:
        //如果请求不是 PRIVACY_MODE_DISABLE,就会直接跳过,是的话,就会生成 auth token
        //如果开启了 privacy mode,server 将无法追踪到请求来源
        rv = DoGenerateServerAuthToken();
        break;
      case STATE_GENERATE_SERVER_AUTH_TOKEN_COMPLETE:
        //这个函数内只改了next_state_ 继续loop
        rv = DoGenerateServerAuthTokenComplete(rv);
        break;
      case STATE_INIT_REQUEST_BODY:
        //post、put、patch、delete 等 method 需要上传 requestBody 的 case 才会走到
        //里面会调用 upload_data_stream->Init 来 init request body
        //upload_data_stream 是 java 层通过 requestBuilder.setUploadDataProvider() 传入并在合适时机创建出来的的
        rv = DoInitRequestBody();
        break;
      case STATE_INIT_REQUEST_BODY_COMPLETE:
        //这个函数内只改了next_state_ 继续loop
        rv = DoInitRequestBodyComplete(rv);
        break;
      case STATE_BUILD_REQUEST:
        //构建request_headers_ 和 body
        rv = DoBuildRequest();
        break;
      case STATE_BUILD_REQUEST_COMPLETE:
        //这个函数内只改了next_state_ 继续loop
        rv = DoBuildRequestComplete(rv);
        break;
      case STATE_SEND_REQUEST:
        //1. 设置请求幂等性 - idempotency
        //2. 发送请求,调用SpdyStream::SendRequest 来发送请求头和数据,同时传入io_callback,随后进入 ERR_IO_PENDING 状态
        //SendRequest 最终会调用到 SpdySession::EnqueueStreamWrite 来写入数据,当数据写入后,如果仍然有数据未写入
        //会在 OnDataSent 中,调用 QueueNextDataFrame 来入队下一个data frame
        //等到数据全部发送完成后,会回调io_callback,解除 pending 状态
        rv = DoSendRequest();
        break;
      case STATE_SEND_REQUEST_COMPLETE:
        //如果写入失败,则返回 IOError,否则 next_state_=STATE_READ_HEADERS,继续 loop
        rv = DoSendRequestComplete(rv);
        break;
      case STATE_READ_HEADERS:
        // 会立马调用 ReadResponseHeaders,调用这个方法后,默认会进入 ERR_IO_PENDING 状态,等待 IO 返回
        // 同时会将 io_callback 继续传入,等待 readHeader 结束后解除 pending 状态
        rv = DoReadHeaders();
        break;
      case STATE_READ_HEADERS_COMPLETE:
        //会检查 read header的结果,检查是否有 io error,以及检查状态码、content-encoding
        rv = DoReadHeadersComplete(rv);
        break;
      case STATE_READ_BODY:
        //当读完 header 后,data 部分就开始读取
        rv = DoReadBody();
        break;
      case STATE_READ_BODY_COMPLETE:
        rv = DoReadBodyComplete(rv);
        break;
      case STATE_DRAIN_BODY_FOR_AUTH_RESTART:
        rv = DoDrainBodyForAuthRestart();
        break;
      case STATE_DRAIN_BODY_FOR_AUTH_RESTART_COMPLETE:
        rv = DoDrainBodyForAuthRestartComplete(rv);
        break;
      default:
        NOTREACHED() << "bad state";
        rv = ERR_FAILED;
        break;
    }
  } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);

  return rv;
}

从上面代码中,我们可以看到,HttpNetworkTransaction 中的状态机从初始的STATE_NOTIFY_BEFORE_CREATE_STREAM 状态开始轮转,逐个状态往下推进,最终完成了整个网络请求。代码中的每个状态在代码中的顺序,就是一个普通网络请求的生命周期,上一个状态执行完后,只要不是 IO_PENDING,在 Doxxx 中总会指定下一状态,并且继续 loop,从而执行下一状态的对应工作。

总结

我们的这一期分享中,并没有深入细节,只是从比较高的视野去看整体的流程,后续会继续深入细节处,分享更加细节的chromium-net相关知识点,包括但不仅限于:

  1. socket pool 和 OKHttp 的ConnectionPool 异同
  2. cache 控制
  3. 设计模式
  4. ...

受限于本人有限的水平,文中必然有描述错误或者理解错误的地方,如看到请指正。

你可能感兴趣

Android QUIC 实践 - 基于 OKHttp 扩展出 Cronet 拦截器 - 掘金 (juejin.cn)

Android启动优化实践 - 秒开率从17%提升至75% - 掘金 (juejin.cn)

如何科学的进行Android包体积优化 - 掘金 (juejin.cn)

Android稳定性:Looper兜底框架实现线上容灾(二) - 掘金 (juejin.cn)

基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)

记 AndroidStudio Tracer工具导致的编译失败 - 掘金 (juejin.cn)

Android 启动优化案例-WebView非预期初始化排查 - 掘金 (juejin.cn)

chromium-net - 跟随 Cronet 的脚步探索大致流程(1) - 掘金 (juejin.cn)

Android稳定性:可远程配置化的Looper兜底框架 - 掘金 (juejin.cn)

一类有趣的无限缓存OOM现象 - 掘金 (juejin.cn)

Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载) - 掘金 (juejin.cn)

Android - 彻底消灭OOM的实战经验分享(千分之1.5 -> 万分之0.2) - 掘金 (juejin.cn)