net模块

内置Chromium至少要实现两个目标。1)处理http(s)协议。因为http(s)已被广泛使用,app往往会基于它和服务器通信,像注册、登录、上传/下载回复。2)基于Chromium提供的网络框架实现RFC或私有协议,像用于聊天的IRC。

个人认为网站会逐渐没落,内置的Chromium不会以app中显示网页为目标。Rose在网络方面已内置Webrtc,两者关系上,Webrtc负责处理传输视频、声音以及大的数据文件,Chromium处理之外的网络任务。

基于以上目标,Rose需要的只是Chromium中的net模块。但net是Chromium核心模块,即使已移除cookies、disk_cache、http2、quic、spdy,也要編译差不多750个源文件。

注:以下介绍的第一到第四部分是如何从Chromium源码树编译出net模块,如果是基于Rose編译,Rose内置Chromium时已做了各项工作,直接用IDE方法編译就行了。

一、生成各平台共用文件

要能編译成功,除了从Chromium官网下载的源码,还须要自个生成一些文件,它们分各平台共用和只针对某种平台。共用文件分三类。effective_tld_names-inc.cc,BUILDFLAG宏相关,以及protobuf相关。

1)effective_tld_names-inc.cc

它位在<chromium>/net/base/registry_controlled_domains目录下,内容是一个kDafsa数组。

const unsigned char kDafsa[42520] = {
  0x3e, 0x40, 0xc3, 0x42, 0xb0, 0x4f, ......
};

registry_controlled_domains下的BUILD.gn写了生成方法。要调用一个叫make_dafsa.py的脚本文件,它没有依赖,可以直接调用它去生成。具体生成步骤。1)在命令行进入到registry_controlled_domains目录。2)把net/tools/dafsa/make_dafsa.py复制到上面目录。3)执行以下命令就会在当前目录下生成这个文件

./make_dafsa.py effective_tld_names.gperf  effective_tld_names-inc.cc。

2)BUILDFLAG宏相关

它们是数个*.h,具体哪几个可在BUILD.gn中搜buildflag_header。当中一个是net/net_features.h,以下是它可能的内容。

#include "build/buildflag.h"

#define BUILDFLAG_INTERNAL_POSIX_AVOID_MMAP() (0)
#define BUILDFLAG_INTERNAL_DISABLE_FILE_SUPPORT() (1)
#define BUILDFLAG_INTERNAL_DISABLE_FTP_SUPPORT() (1)
#define BUILDFLAG_INTERNAL_ENABLE_MDNS() (0)
#define BUILDFLAG_INTERNAL_ENABLE_REPORTING() (0)
#define BUILDFLAG_INTERNAL_ENABLE_WEBSOCKETS() (0)
#define BUILDFLAG_INTERNAL_INCLUDE_TRANSPORT_SECURITY_STATE_PRELOAD_LIST() (0)

此类文件定义的宏都是以“BUILDFLAG_INTERNAL_”为前缀,值0表示false,1表示true。可以这么认为,只要生成当中一个,其它的就可以类推。让说下如何生成net_features.h。

打开net/BUILD.gn,会找到buildflag_header("features")定义了如何生成net_features.h。

buildflag_header("features") {
  header = "net_features.h"
  flags = [
    "POSIX_AVOID_MMAP=$posix_avoid_mmap",
    ......
  ]
}

由此追溯import("//build/buildflag_header.gni"),是它定义了生成header的模板。

template("buildflag_header") {
  action(target_name) {
    script = "//build/write_buildflag_header.py"

    if (defined(invoker.header_dir)) {
      header_file = "${invoker.header_dir}/${invoker.header}"
    } else {
      # Compute the path from the root to this file.
      header_file = rebase_path(".", "//") + "/${invoker.header}"
    }

    outputs = [
      "$root_gen_dir/$header_file",
    ]
 ....
}

可以看到生成脚本是//build/write_buildflag_header.py,这个模板定义里用到了一些gn文件定义的变量,虽然直接调用脚本可以生成,但并不清楚正确的参数内容是什么,所以只好老老实实用gn + ninja来生成。需要下载chromium源码,还有depot_tools。下载后,在chromium/src下运行gn gen out/default (当然 out/default目录要提前建好),然后在src目录下cd out/default,这时可以看到ninja.build文件,运行ninja net:features这个target就会生成net_features.h,生成的文件在gen/net目录。

3)protobuf相关

生成方法是用protobuf中的实用工作protoc,把Chromium提供的*.proto作为输入,生成对应的*.pb.cc和*.pb.h。举个例子,net/nqe/proto/network_id_proto.proto是个需要生成的proto,执行以下命令就能生成network_id_proto.pb.h、network_id_proto.pb.cc。

protoc --cpp_out=. network_id_proto.proto

二、include目录、预定义宏,以及其它編译选项

Windows、iOS、Android有着一样的include目录,分别是chromium源码目录,三个依赖库目录。

../../../../apps/external/chromium
../../../../apps/external/boringssl/include
../../../../apps/external/protobuf/src
../../../../apps/external/zlib

分三个平台说预定义宏。

  • Windows。预定义宏:CERT_CHAIN_PARA_HAS_EXTRA_FIELDS、NO_TCMALLOC、DCHECK_ALWAYS_ON。CERT_CHAIN_PARA_HAS_EXTRA_FIELDS是winsock特有。NO_TCMALLOC指示不用第三方tcmalloc库。DCHECK_ALWAYS_ON指示总使用DCHECK检查。Visual Studio的Release往往会定义NDEBUG,因而只能靠定义DCHECK_ALWAYS_ON来强制打开DCHECK检查。
#if defined(NDEBUG) && !defined(DCHECK_ALWAYS_ON)
	#define DCHECK_IS_ON() 0
#else
	#define DCHECK_IS_ON() 1
#endif
  • iOS。预定义宏:NO_TCMALLOC。NO_TCMALLOC指示不用第三方tcmalloc库。iOS不会定义NDEBUG,因而不必定义DCHECK_ALWAYS_ON就能打开DCHECK检查。
  • Android。预定义宏:NO_TCMALLOC。NO_TCMALLOC指示不用第三方tcmalloc库。Android不会定义NDEBUG,因而不必定义DCHECK_ALWAYS_ON就能打开DCHECK检查。Chromium要使用C++14语法,編译选项要增加:LOCAL_CPPFLAGS += -std=c++1y。

三、Android特有的編译设置

对整个Chromium模块,Windows都是C/C++调用,iOS上虽有C/C++和Object-c互调,但都是编译性语言,它们在互调上都不须额外设置。Android会涉及到C/C++调用Java代码,以及Java代码调用C/C++,为此还要处理Java部分。

C/C++调用Java代码。要做的是基于*.java,生成一系列_jni.h文件,它们统一被放在<chromium>/jni目录,C/C++要通过这些头文件调用Java代码。使用jni_generator.py生成这些头文件,jni_generator.py更多细节参考“jni_generator.py”。

Java代码调用C/C++。要生成一个头文件,写着C/C++向Java提供了哪些原生函数。文件内容参考Rose中的remoting_jni_registration.h。但目前用到的Chromium功能没涉及到要Java调用C/C++,将来增加功能时可能会需要。如何生成这个头文件?运行ninja remoting_jni_registration,生成的文件放在gen/remoting/client目录。

AS(Android Studio)须要的各种Java依赖。Chromium需要的java文件放到AS,为成功編译这些Java,一来须要依赖源码中没有的*.java,二来第三方lib。具体需要什么,要看Build.gn后才会了解,这些文件大部分都是用脚本生成。因为生成文件都是ninja的target,所以这里先生成ninja运行的目录

gn gen --args='target_os="android"' out/default
// 生成<chromium>/base目录下额外的java文件,及须要的jar。
ninja base:base_java
// 生成<chromium>/net目录下额外的java文件,及须要的jar。
ninja net/android:net_java
// 生成<chromium>/url目录下额外的java文件,及须要的jar。
ninja url:url_java

这里有几个java文件是用模板生成的,像gen/base/base_native_libraries_gen/java_cpp_template/org/chromium/base/library_loader/NativeLibraries.java和gen/base/base_build_config_gen/java_cpp_template/org/chromium/base/BuildConfig.java这两个文件,然后还有依次其它的依赖,gen/base/base_android_java_enums_srcjar.srcjar,gen/base/base_java_aidl.srcjar,gen/base/base_native_libraries_gen.srcjar。这些文件是java文件的一些集合,我们可以将里面的内容抽取出需要的java文件。然后,gen/net/android/net_android_java_enums_srcjar.srcjar,gen/net/android/net_errors_java/java_cpp_template/org/chromium/net/NetError.java,抽取出需要的java文件。然后把生成的三方库也拿出来,lib.java/third_party/android_tools/support/android_support_multidex_java.jar,lib.java/third_party/jsr-305/jsr_305_javalib.jar,所有抽取的文件都放入相应的目录内,android studio就可发build成功了。

四、裁剪

裁剪是指为实现开头说的两个目标,对Chromium中net模块内的再裁剪。

http2、quic、spdy。裁掉它们的原因是app服务器通常不会用到这些协议。裁剪方法分两个步骤,1)不編译http2、quic、spdy目录下的源文件,2)要用的代码中注释掉用到它们的部分。

disk_cache。disk_cache作用是在硬盘或内存中缓存最新访问过的url(网页)数据,这对访问网站的确能提高率效,但换到app会导致严重问题。举个例子,aismart/api/login.do作用是执行app登录,一旦使能disk_cache,chromium对第二次或后面的login.do可能就不触发网络发送了,而是从硬盘或内存取出上一次数据!如何裁剪?——1)没有办法完全不使用disk_cache,只能尽可能不用。2)不编译disk_cache目录下的源文件。3)app调用context_storage_.set_http_transaction_factory时传一个nullptr的BackendFactory。工作原理:HttpCache::Transaction首先会进入STATE_GET_BACKEND状态,对应操作DoGetBackend(),执行它的目的是确保让存在有效的disk_cache_(非空)。但由于BackendFactory是nullptr,使building_backend_变量一直是false,此变量是false导致DoGetBackend创建disk_cache_,disk_cache_也就一直无效。由于disk_cache_无效ShouldPassThrough()返回false,表示此次会话要旁路(不使用)cache,使得状态直接跳向STATE_SEND_REQUEST,而不是STATE_INIT_ENTRY。4)要用的代码中注释掉用到它们的部分。由于要用到的代码和disk_cache联系紧密,尤其http_cache.cpp,注释时建议用替换法,即用新建类代替掉在用的disk_cache::Backend、disk_cache::Entry。裁剪成功的一个标志是不再须要编译http_cache_writers.cc。

cookies。裁它的原因类似disk_cache,http会在头部增加Cookie字段。裁剪方法分两个步骤,1)不編译cookies目录下的源文件,2)要用的代码中注释掉用到它们的部分。

icu。icu是个第三方库,裁它的原因是它实在大大了。裁剪方法是修改两个源文件,让不调用icu,它们是net/base/net_string_util_icu.cc,url/url_canon_icu.cc。

mincore.lib。为支持在Win7运行,不要让依赖mincore.lib。Chromium要依赖mincore.lib分两个方面,第一是FileVersion相关,像GetFileVersionInfoSize、GetFileVersionInfo、VerQueryValue。第二是用于创建代理服务器,像WinHttpOpen。移除mincore.lib则不须要編译下面源文件。

<chromium>/base/file_version_info_win.cc
<chromium>/net/proxy_resolution/proxy_config_service_win.cc
<chromium>/net/proxy_resolution/proxy_config_service_fixed.cc
<chromium>/net/proxy_resolution/multi_threaded_proxy_resolver.cc
<chromium>/net/proxy_resolution/proxy_resolver_winhttp.cc
<chromium>/net/url_request/url_request_context_builder.cc

五、app使用net模块

使用net模块的代码分两部分,一是初始化,二是运行时。

base::test::ScopedTaskEnvironment env(base::test::ScopedTaskEnvironment::MainThreadType::IO);
base::CommandLine::Init(argc, argv);
#ifdef WEBRTC_ANDROID
	webrtc::JVM::Initialize(reinterpret_cast<JavaVM*>(v1), reinterpret_cast<jobject>(v2));
	base::android::InitVM(reinterpret_cast<JavaVM*>(v1));
	base::android::OnJNI"language- language-none" data-id="code">base::test::ScopedTaskEnvironment env(base::test::ScopedTaskEnvironment::MainThreadType::IO);
base::CommandLine::Init(argc, argv);
#ifdef WEBRTC_ANDROID
	webrtc::JVM::Initialize(reinterpret_cast<JavaVM*>(v1), reinterpret_cast<jobject>(v2));
	base::android::InitVM(reinterpret_cast<JavaVM*>(v1));
	base::android::OnJNI env->FindClass(class_name);
  }
  ......
}

额外说下JNIEnv::FindClass。必需在主线程调用JNIEnv::FindClass,否则app会出现异常,提示“Failed to find class .../AndroidNetworkLibrary”。

运行时代码就是按Chromium api要求了,以下代码会触发一次https传输。

URLRequestHttpJobWithProxy http_job_with_proxy(nullptr);
RoseDelegate delegate;
std::unique_ptr<URLRequest> req = http_job_with_proxy.context_->CreateRequest(
	GURL("https://www.github.com"), DEFAULT_PRIORITY, &delegate, TRAFFIC_ANNOTATION_FOR_TESTS);
req->Start();
base::RunLoop().Run();

运行时可能会遇到使用自签名证书的https。通常证书会向权威机构申请,但一些app可能想简单点,自签一个。以下是基于Rose使用自签名证书的步骤。

  1. 生成自签名证书,可参考“如何创建一个自签名的SSL证书(X509)”。假设生成的证书文件是server.der。
    openssl x509 -inform pem -outform der < server.crt > server.der
  2. 把server.der放到<res>/app-<appid>/cert目录下。
  3. 设置thttp_agent中的cert字段是“server.der”。如果不是自筌名,这字段就置空。然后以这个agent为参数调用handle_http_request。

六、修改

app会遇到这么种需要,要在请求的body中传输大文件,像4M字节,这时最好在界面上给出个上传进度提示。

<chromium>/net/url_request/url_request.hpp
class NET_EXPORT URLRequest {
class NET_EXPORT Delegate {
	virtual void OnSendBodyCompleted(URLRequest* request, int sofar_bytes, int expected_bytes) {}
};
	void NotifySendBodyCompleted(int sofar_bytes, int expected_bytes);
};

<chromium>/net/url_request/url_request.cpp
void URLRequest::NotifySendBodyCompleted(int sofar_bytes, int expected_bytes)

<chromium>/net/http/http_stream_parser.cc
int HttpStreamParser::DoSendBodyComplete(int result) {
	if (URLRequest::http_instance) {
		URLRequest::http_instance->NotifySendBodyCompleted(request_->upload_data_stream->position(), request_->upload_data_stream->size());
	}
	......
}

要发送大的body时(request_->upload_data_stream存储着body,不包括request header),Chromium会把它拆分成多个16384字节的小单元,然后异步发送这些单元。发送完一个单元后就会调用HttpStreamParser::DoSendBodyComplete。修改方法就是让后者调用NotifySendBodyCompleted,app通过这个函数知道body发送进度。

七、升级

一旦下载了Chromium最新版本,可按以下步骤让尽快保持同步。

  • 按第一步介绍的方法生成各平台共用文件。对BUILDFLAG宏相关的可只生成net_features.h,其它的可在編译时找不到了再猜出来。protobuf相关的也可以編译时按需生成。
  • 編译Visual Studio版本。遇到问题就解决,确保移除cookies、disk_cache、http2、quic、spdy,以及不依赖icu、mincore.lib。
  • 編译iOS版本。
  • 編译Android版本。

全部评论: 0

    写评论: