Code Style Notes
Type Wrapping
In Chromium code, no matter how simple a type is—even a file descriptor that is merely an int—it will be renamed via typedef into a type that conforms to the naming conventions. For slightly more complex structs such as sockaddr, Chromium will directly wrap them into a class. The purpose of this approach is to bridge differences in system call interface design across platforms, thereby exposing a unified, consistent interface to upper-layer application code.
Event Loop
First, make one point clear: in the Chromium project, all network I/O APIs are asynchronous and non-blocking. The actual I/O operations are completed via asynchronous invocations through the event loop, but there are multiple layers of abstraction in between. Therefore, whenever network I/O needs to be performed, the network library first attempts to do it in a non-blocking way. If it succeeds, it can return directly; if it fails, it registers with the event loop and waits for completion before executing the callback. After registration, that asynchronous method returns an ERR_IO_PENDING error code to indicate that the task is in progress.
On POSIX-compatible systems, Chromium's event loop is implemented on top of libevent.
Asynchronous Callbacks
In my view, Chromium's asynchronous callback mechanism is essentially an object-oriented realization of the coroutine idea. As mentioned above, every I/O call that would normally block registers a callback function and then returns an ERR_IO_PENDING status code. This status code is in fact equivalent to yield in coroutine terms—yielding control flow. When the real I/O completes, the callback function is invoked. Since in most cases the callback is a class method, it can access the class's member variables; and before initiating the I/O call, the context information of the control flow at that moment has already been saved into those member variables. Therefore, the callback function effectively reads that context information and continues executing the intended flow to perform the corresponding operations—similar to resuming control flow from the yield point. Considering Chromium has extremely complex control flow, compared with a plain coroutine framework, this fine-grained approach can maximize design flexibility.
Reference Counting
base::Unretained()
base::Owned()
base::Passed()
base::ConstRef()
base::IgnoreResult()
Smart Pointers
scoped_ptr
This is a smart pointer in some sense, partially implementing the semantics of C++11's unique_ptr—i.e., "movable but not copyable": movable but not copyable, meaning it does not support a copy constructor or copy assignment. We can basically treat it as a mechanism compatible with older C++ versions for managing memory within a scope. If you need to initiate a function call within that scope, you must use std::move to transfer ownership to the callee, rather than passing the argument by value directly; otherwise it would trigger the copy constructor, which is not allowed.
In fact, if a function needs to accept a scoped_ptr as a parameter, only an rvalue (rvalue) can be passed in. Ways to produce an rvalue include std::move or a function return value. In short, copying or assigning a scoped_ptr is not allowed.
scoped_refptr
Slightly different from scoped_ptr, this pointer can only point to classes that explicitly implement a reference-counting (AddRef/Release) interface (i.e., inherit from RefCounted). In other words, this smart pointer is specifically designed for reference counting and automatic garbage collection, and is essentially equivalent to the functionality of C++11's shared_ptr. As to why shared_ptr is not used directly, my understanding is that, on the one hand, when Chromium was developed the latest C++ standard had not yet been implemented; on the other hand, this specialized smart pointer can couple better with the project's type/interface design, enforcing stronger type constraints and thereby effectively improving code quality.
weak_ptr
This is a smart pointer that does not affect the lifetime of the object it points to, mainly used when non-owners need to access an object. For example, suppose we need to temporarily access an object managed by reference counting—perhaps to read some state or do something simple. If we use a normal smart pointer, the reference count would increase, but in reality we do not need to keep that reference at this time—in fact, if the object has already been destroyed, it is fine for us not to access it at all. In this scenario, maintaining reference counting makes the object's lifetime more complex; if we use a raw pointer, we cannot tell whether it has already been destroyed. Therefore, under such specific conditions, we need weak_ptr.
Macros
DISALLOW_COPY_AND_ASSIGN
Used to explicitly forbid the copy constructor and copy assignment operator by declaring them as private functions. All classes in the code must use this macro to prevent accidental object copying/construction and to strictly use reference counting and scoped_ptr to manage objects.
HANDLE_EINTR
Used to wrap the lowest-level I/O system calls such as send/recv, because these operations can be interrupted by signals.
NET_EXPORT
Defines the DLL export symbol attribute on Windows, while on POSIX platforms it is simply empty, so we can ignore it.
Build-Related
Android Interfaces
JNI_OnLoad
When JNI is loaded, it calls JNI_OnLoad, and when it is unloaded it calls JNI_UnLoad, so we can implement JNI by registering our native functions in JNI_OnLoad. With this approach, we can avoid generating header files and then writing native implementations; instead, when the dynamic library is loaded at runtime, we register the corresponding implementations to the appropriate Java classes.
CronetOnLoad
The initialization function above directly calls this function provided by cronet. However, that initialization function is only a thin wrapper: it actually binds base::android::OnJNIOnLoadRegisterJNI to execute the real initialization function that registers JNI. Inside this "executor" it also performs some initialization actions, and only then calls each initialization function. This layer-by-layer wrapping is quite painful.
ChromiumUrlRequestRegisterJni
Here is just one example of registering URLRequest-related functions. When this initialization function is called, it actually calls the corresponding RegisterNativesImpl function. Annoyingly, this function is auto-generated and stored in a header file, and is clearly a static function. In any case, what it does is register the wrapped native function implementations into the Java VM. For example, in this auto-generated header file you can see various statements such as extern "C".
Passing C++/Java Objects
Here we look at a wrapped native function declaration, for example:
:::C++
static void AddHeader(JNIEnv* env,
const JavaParamRef<jobject>& jcaller,
jlong jurl_request_adapter,
const JavaParamRef<jstring>& jheader_name,
const JavaParamRef<jstring>& jheader_value)
We can see a parameter jurl_request_adapter of type jlong. Judging from the name, this is clearly supposed to be an object pointer, but it is declared as an integer. In fact, in cross-language calls, the JRE is not much smarter than Python: this so-called integer is really used to store a C++ pointer. In the code, you will see it being forcibly cast to an object pointer. In short, through a series of wrappers, Java's upper-layer calls are successfully translated into object operations on the C++ side.
Design Patterns
Online reference: "Design Patterns Illustrated"
Singleton Pattern
This is the simplest design pattern, so no need to waste words. Chromium's design of binding the event loop to each thread can be considered a kind of singleton pattern—except that there is not a single unique instance in the entire process address space, but rather one instance per thread. So when registering an I/O task with the event loop, it is written like this:
:::C++
if (!base::MessageLoopForIO::current()->WatchFileDescriptor(
socket_fd_, true, base::MessageLoopForIO::WATCH_WRITE,
&write_socket_watcher_, this)) {
PLOG(ERROR) << "WatchFileDescriptor failed on write, errno " << errno;
return MapSystemError(errno);
}
Command Pattern
Observer Pattern
Definition: Establish a dependency relationship between objects so that when one object changes it automatically notifies other objects, and the other objects respond accordingly. Here, the object that changes is called the subject (the observation target), and the objects being notified are called observers. One subject can correspond to multiple observers, and there is no relationship among the observers. Observers can be added or removed as needed, making the system easier to extend—this is the motivation behind the observer pattern.
Here is a brief analysis of how the NetLog class applies this pattern. Contrary to its intuitive name, the NetLog class itself actually acts as a Subject (the observation target). It contains a list of Observer instances; if an event occurs, it traverses its internal list and notifies all listener instances one by one. All Observer objects must inherit from ThreadSafeObserver, which is defined inside the NetLog class.
At the socket layer, if we want to add our own custom errors, the best approach is to directly create a new observer object and register it into the NetLog class at an appropriate place. When recording data, this observer object needs to be responsible for initiating asynchronous calls to send data back. Existing observer objects in cronet include: TraceNetLogObserver, NetLogObserver, and WriteToFileNetLogObserver.
Delegate Pattern
Factory Pattern
The construction of the URLRequest class follows a typical factory pattern. The constructor of this class is set to private; the actual creation is handled by URLRequestContext::CreateRequest, and URLRequestContext is also its friend class. During construction, URLRequestContext passes its own pointer into the URLRequest constructor. This actually describes the dependency of a URL request on the current environment (Cookie, Cache, etc.)—that is, issuing requests based on the environment.
Adapter Pattern
Let us analyze how an Observer is registered into the corresponding NetLog object. Here we take a concrete NetLogObserver class (inheriting from net::NetLog::ThreadSafeObserver) as an example and analyze its registration flow. First, this class is wrapped as a member variable inside the URLRequestContextAdapter class. In the InitRequestContextOnNetworkThread initialization method of that class, it checks the current log level; the code is as follows:
:::C++
if (VLOG_IS_ON(2)) {
net_log_observer_.reset(new NetLogObserver());
context_->net_log()->DeprecatedAddObserver(
net_log_observer_.get(),
net::NetLogCaptureMode::IncludeCookiesAndCredentials());
}
If the switch is enabled and logging is needed, it registers the corresponding observer object into the NetLog class contained in the current Adapter's associated net::URLRequestContext object. Then where does this NetLog come from? In practice it comes from the builder object of URLRequestContext: URLRequestContextBuilder. The pointer in the builder actually comes from the method CronetURLRequestContextAdapter::InitializeOnNetworkThread. From the name it can be inferred that this is a method for initializing the network library, and it is called by InitRequestContextOnMainThread within the same class.
So in the end, what exactly is this CronetURLRequestContextAdapter? In fact, it is an adapter that adapts Java's CronetUrlRequestContext to net::URLRequestContext. At this point, the entire logging flow becomes clear: initialization is triggered from the Java application layer, and then the NetLog object is passed all the way down along URLRequest to the lowest socket handling layer, where it can receive all those network state signals and record the corresponding logs.
base
logging.cc
Defines the generic logging system LogMessage class. When using it, you use macros directly rather than calling its concrete methods.
net
net/log
net_log.cc
Considering that we need to record socket errors produced during URL requests, we will discuss in more depth the role played by the NetLog class over the lifetime of a URL request. First, we confirm that the URLRequest class contains a BoundNetLog object. This object is constructed from the NetLog object passed in by URLRequestContext when the request object is created. The construction process is essentially just binding the NetLog pointer to a specific Source; there is nothing particularly special about it.
net/socket
socket_descriptor.cc
Provides cross-platform operations on socket file descriptors. There is only one function: CreatePlatformSocket, plus a typedef for int.
socket_posix.cc
Provides the most primitive wrapper around BSD socket descriptors: the SocketPosix class. This class wraps all POSIX blocking operations into non-blocking class methods. Notably, each wrapped blocking operation method accepts a callback function object of type base::Callback<void(int)> to perform follow-up actions such as logging. If you need to collect error logs at the socket layer, I personally think you should consider hooking in here.
So when will the callback be executed? Note that SocketPosix actually inherits from base::MessageLoopForIO::Watcher, which is an abstract base class defining the two virtual methods OnFileCanReadWithoutBlocking and OnFileCanWriteWithoutBlocking. Therefore, when the fd becomes readable or writable in the event loop, these two methods of SocketPosix are invoked, completing the actual I/O read/write operation, then unregistering the Watcher that was registered with the event loop, and finally calling the callback that was passed in earlier.
In addition, note that this class wraps a WaitForWrite method, and Write calls it internally. That is, for the Write operation, it separates the attempt-I/O step from the event-registration step, but it does not do so for Read. Why is it designed this way? In fact, this is because a TcpFastOpenWrite method is implemented in a higher-level wrapper and needs to call into this functionality.
Finally, note the methods IsConnected and IsConnectedAndIdle. They read the socket using MSG_PEEK, i.e., without consuming buffer data but only peeking at it. For a recv call, if it returns zero or another value, it indicates the connection is dead; if it returns EAGAIN or EWOULDBLOCK, it indicates the connection is still alive.
tcp_socket.cc
There is not much code here: it just includes platform/system headers related to sockets and provides some platform feature-related functions, such as TCP_NODELAY. In short, this is a compatibility source file that wraps differences across platforms.
tcp_socket_posix.cc
At the beginning, this file provides functions used to determine/set system features, such as SetTCPKeepAlive, SystemSupportsTCPFastOpen, and so on. Next is a TCP socket wrapper built on top of SocketPosix: the TCPSocketPosix class. If the wrapping in SocketPosix reflects the basic characteristics of BSD sockets, then this layer reflects the basic characteristics of the TCP protocol. In this class, the core functionality is still the Read and Write methods, but there are also methods such as TcpFastOpenWrite and SetKeepAlive, which essentially implement TCP behavior.
First, let us analyze how callbacks are wrapped. In fact, I/O operations in this file also need callback parameters, but the callback passed from outside is not forwarded unchanged into the lowest-level SocketPosix class; instead, TCPSocketPosix adds its own logic:
:::C++
int rv =
socket_->Connect(storage, base::Bind(&TCPSocketPosix::ConnectCompleted,
base::Unretained(this), callback));
Essentially this is a functional-programming idea: treat functions as parameters and generate a new function. In this case, when SocketPosix triggers the callback, it is actually calling a TCPSocketPosix method. That method performs its own operations and then calls back the external callback passed in—forming a nested, layered relationship.
Next, analyze support for TCP Fast Open. In the Connect function, if this feature is enabled, Connect returns success immediately. Then the actual connection establishment logic begins when a Write is needed. That is, during Write, if no write attempt has been made yet, it tries to write a packet of data using Fast Open. Based on the same logic, the IsConnected function will likewise "assume" the connection has been established when fast open is in use but the connection has not yet been established.
There is a detail during TCP Fast Open writes: if the OS kernel already has a cookie, the I/O operation can return directly without blocking, because the kernel can immediately send out the cookie-carrying packet. If the kernel does not have a cookie, it must wait until the connection is established before it can write data. At that point, because a write was attempted and failed, it calls WaitForWrite in SocketPosix rather than calling Write directly again.
socket.h
This header defines the pure abstract definition of Socket, i.e., what a socket should include as the most fundamental I/O properties: Read, Write, SetReceiveBufferSize, and SetSendBufferSize. The purpose of this class should be to serve as a base for inheritance by higher-level classes.
stream_socket.cc
This file defines the StreamSocket class. Except for its internal UseHistory class, which has a concrete implementation, this is basically an abstract base class inheriting from Socket that adds stream (Stream) semantics on top of the raw socket concept, such as Connect and DisConnect, but does not include concrete implementations of these semantics; it needs another layer of wrapping.
tcp_client_socket.cc
Inherits from StreamSocket and defines the TCPClientSocket class. This class implements client-side Stream data transfer based on the TCP protocol. This shows that at this very concrete level, the server-side and client-side Stream transfer interface definitions already differ significantly, so they are split into two classes across two files.
In addition, note that this class, relative to TCPSocketPosix, is actually a "Has A" relationship. It also provides Read and Write methods, which are essentially wrappers around TCPSocketPosix, but it adds the logging system at the Stream layer. Its callback handling is basically the same.
ssl_socket.h
Defines the SSLSocket class on top of StreamSocket, but this class is also abstract. It adds two encryption-related pure virtual function interfaces to the stream semantics, and this interface is common to both client and server, which is why it is "inserted" at this layer.
ssl_client_socket.cc
Here we focus on the client-side SSL implementation. This file defines the SSLClientSocket class, but it is also an intermediate abstract layer: some methods are public, but many are pure virtual methods that need to be defined at the concrete OpenSSL implementation layer.