mirror of
https://github.com/klzgrad/naiveproxy.git
synced 2024-12-01 01:36:09 +03:00
241 lines
11 KiB
Markdown
241 lines
11 KiB
Markdown
|
# Chrome Network Stack Common Coding Patterns
|
||
|
|
||
|
## Combined error and byte count into a single value
|
||
|
|
||
|
At many places in the network stack, functions return a value that, if
|
||
|
positive, indicate a count of bytes that the the function read or
|
||
|
wrote, and if negative, indicates a network stack error code (see
|
||
|
[net_error_list.h][]).
|
||
|
Zero indicates either `net::OK` or zero bytes read (usually EOF)
|
||
|
depending on the context. This pattern is generally specified by
|
||
|
an `int` return type.
|
||
|
|
||
|
Many functions also have variables (often named `result` or `rv`) containing
|
||
|
such a value; this is especially common in the [DoLoop](#DoLoop) pattern
|
||
|
described below.
|
||
|
|
||
|
## Sync/Async Return
|
||
|
|
||
|
Many network stack routines may return synchronously or
|
||
|
asynchronously. These functions generally return an int as described
|
||
|
above. There are three cases:
|
||
|
|
||
|
* If the value is positive or zero, that indicates a synchronous
|
||
|
successful return, with a zero return value indicating either zero
|
||
|
bytes/EOF or indicating `net::OK`, depending on context.
|
||
|
* If the value is negative and != `net::ERR_IO_PENDING`, it is an error
|
||
|
code specifying a synchronous failure.
|
||
|
* If the return value is the special value `net::ERR_IO_PENDING`, it
|
||
|
indicates that the routine will complete asynchronously. A reference to
|
||
|
any provided IOBuffer will be retained by the called entity until
|
||
|
completion, to be written into or read from as required.
|
||
|
If there is a callback argument, that callback will be called upon
|
||
|
completion with the return value; if there is no callback argument, it
|
||
|
usually means that some known callback mechanism will be employed.
|
||
|
|
||
|
## DoLoop
|
||
|
|
||
|
The DoLoop pattern is used in the network stack to construct simple
|
||
|
state machines. It is used for cases in which processing is basically
|
||
|
single-threaded and could be written in a single function, if that
|
||
|
function could block waiting for input. Generally, initiation of a
|
||
|
state machine is triggered by some method invocation by a class
|
||
|
consumer, and that state machine is driven (possibly across
|
||
|
asynchronous IO initiated by the class) until the operation requested
|
||
|
by the method invocation completes, at which point the state variable is
|
||
|
set to `STATE_NONE` and the consumer notified.
|
||
|
|
||
|
Cases which do not fit into this single-threaded, single consumer
|
||
|
operation model are generally adapted in some way to fit the model,
|
||
|
either by multiple state machines (e.g. independent state machines for
|
||
|
reading and writing, if each can be initiated while the other is
|
||
|
outstanding) or by storing information across consumer invocations and
|
||
|
returns that can be used to restart the state machine in the proper
|
||
|
state.
|
||
|
|
||
|
Any class using this pattern will contain an enum listing all states
|
||
|
of that machine, and define a function, `DoLoop()`, to drive that state
|
||
|
machine. If a class has multiple state machines (as above) it will
|
||
|
have multiple methods (e.g. `DoReadLoop()` and `DoWriteLoop()`) to drive
|
||
|
those different machines.
|
||
|
|
||
|
The characteristics of the DoLoop pattern are:
|
||
|
|
||
|
* Each state has a corresponding function which is called by `DoLoop()`
|
||
|
for handling when the state machine is in that state. Generally the
|
||
|
states are named STATE`_<`STATE_NAME`>` (upper case separated by
|
||
|
underscores), and the routine is named Do`<`StateName`>` (CamelCase).
|
||
|
For example:
|
||
|
|
||
|
enum State {
|
||
|
STATE_NONE,
|
||
|
STATE_INIT,
|
||
|
STATE_FOO,
|
||
|
STATE_FOO_COMPLETE,
|
||
|
};
|
||
|
int DoInit();
|
||
|
int DoFoo();
|
||
|
int DoFooComplete(int result);
|
||
|
|
||
|
* Each state handling function has two basic responsibilities in
|
||
|
addition to state specific handling: Setting the data member
|
||
|
(named `next_state_` or something similar)
|
||
|
to specify the next state, and returning a `net::Error` (or combined
|
||
|
error and byte count, as above).
|
||
|
|
||
|
* On each `DoLoop()` iteration, the function saves the next state to a local
|
||
|
variable and resets to a default state (`STATE_NONE`),
|
||
|
and then calls the appropriate state handling based on the
|
||
|
original value of the next state. This looks like:
|
||
|
|
||
|
do {
|
||
|
State state = io_state_;
|
||
|
next_state_ = STATE_NONE;
|
||
|
switch (state) {
|
||
|
case STATE_INIT:
|
||
|
result = DoInit();
|
||
|
break;
|
||
|
...
|
||
|
|
||
|
This pattern is followed primarily to ensure that in the event of
|
||
|
a bug where the next state isn't set, the loop terminates rather
|
||
|
than loops infinitely. It's not a perfect mitigation, but works
|
||
|
well as a defensive measure.
|
||
|
|
||
|
* If a given state may complete asynchronously (for example,
|
||
|
writing to an underlying transport socket), then there will often
|
||
|
be split states, such as `STATE_WRITE` and
|
||
|
`STATE_WRITE_COMPLETE`. The first state is responsible for
|
||
|
starting/continuing the original operation, while the second state
|
||
|
is responsible for handling completion (e.g. success vs error,
|
||
|
complete vs. incomplete writes), and determining the next state to
|
||
|
transition to.
|
||
|
|
||
|
* While the return value from each call is propagated through the loop
|
||
|
to the next state, it is expected that for most state transitions the
|
||
|
return value will be `net::OK`, and that an error return will also
|
||
|
set the state to `STATE_NONE` or fail to override the default
|
||
|
assignment to `STATE_DONE` to exit the loop and return that
|
||
|
error to the caller. This is often asserted with a DCHECK, e.g.
|
||
|
|
||
|
case STATE_FOO:
|
||
|
DCHECK_EQ(result, OK);
|
||
|
result = DoFoo();
|
||
|
break;
|
||
|
|
||
|
The exception to this pattern is split states, where an IO
|
||
|
operation has been dispatched, and the second state is handling
|
||
|
the result. In that case, the second state's function takes the
|
||
|
result code:
|
||
|
|
||
|
case STATE_FOO_COMPLETE:
|
||
|
result = DoFooComplete(result);
|
||
|
break;
|
||
|
|
||
|
* If the return value from the state handling function is
|
||
|
`net::ERR_IO_PENDING`, that indicates that the function has arranged
|
||
|
for `DoLoop()` to be called at some point in the future, when further
|
||
|
progress can be made on the state transitions. The `next_state_` variable
|
||
|
will have been set to the proper value for handling that incoming
|
||
|
call. In this case, `DoLoop()` will exit. This often occurs between
|
||
|
split states, as described above.
|
||
|
|
||
|
* The DoLoop mechanism is generally invoked in response to a consumer
|
||
|
calling one of its methods. While the operation that method
|
||
|
requested is occuring, the state machine stays active, possibly
|
||
|
over multiple asynchronous operations and state transitions. When
|
||
|
that operation is complete, the state machine transitions to
|
||
|
`STATE_NONE` (by a `DoLoop()` callee not setting `next_state_`) or
|
||
|
explicitly to `STATE_DONE` (indicating that the operation is
|
||
|
complete *and* the state machine is not amenable to further
|
||
|
driving). At this point the consumer is notified of the completion
|
||
|
of the operation (by synchronous return or asynchronous callback).
|
||
|
|
||
|
Note that this implies that when `DoLoop()` returns, one of two
|
||
|
things will be true:
|
||
|
|
||
|
* The return value will be `net::ERR_IO_PENDING`, indicating that the
|
||
|
caller should take no action and instead wait for asynchronous
|
||
|
notification.
|
||
|
* The state of the machine will be either `STATE_DONE` or `STATE_NONE`,
|
||
|
indicating that the operation that first initiated the `DoLoop()` has
|
||
|
completed.
|
||
|
|
||
|
This invariant reflects and enforces the single-threaded (though
|
||
|
possibly asynchronous) nature of the driven state machine--the
|
||
|
machine is always executing one requested operation.
|
||
|
|
||
|
* `DoLoop()` is called from two places: a) methods exposed to the consumer
|
||
|
for specific operations (e.g. `ReadHeaders()`), and b) an IO completion
|
||
|
callbacks called asynchronously by spawned IO operations.
|
||
|
|
||
|
In the first case, the return value from `DoLoop()` is returned directly
|
||
|
to the caller; if the operation completed synchronously, that will
|
||
|
contain the operation result, and if it completed asynchronously, it
|
||
|
will be `net::ERR_IO_PENDING`. For example (from
|
||
|
`HttpStreamParser`, abridged for clarity):
|
||
|
|
||
|
int HttpStreamParser::ReadResponseHeaders(
|
||
|
const CompletionCallback& callback) {
|
||
|
DCHECK(io_state_ == STATE_NONE || io_state_ == STATE_DONE);
|
||
|
DCHECK(callback_.is_null());
|
||
|
DCHECK(!callback.is_null());
|
||
|
|
||
|
int result = OK;
|
||
|
io_state_ = STATE_READ_HEADERS;
|
||
|
|
||
|
result = DoLoop(result);
|
||
|
|
||
|
if (result == ERR_IO_PENDING)
|
||
|
callback_ = callback;
|
||
|
|
||
|
return result > 0 ? OK : result;
|
||
|
}
|
||
|
|
||
|
In the second case, the IO completion callback will examine the
|
||
|
return value from `DoLoop()`. If it is `net::ERR_IO_PENDING`, no
|
||
|
further action will be taken, and the IO completion callback will be
|
||
|
called again at some future point. If it is not
|
||
|
`net::ERR_IO_PENDING`, that is a signal that the operation has
|
||
|
completed, and the IO completion callback will call the appropriate
|
||
|
consumer callback to notify the consumer that the operation has
|
||
|
completed. Note that it is important that this callback be done
|
||
|
from the IO completion callback and not from `DoLoop()` or a
|
||
|
`DoLoop()` callee, both to support the sync/async error return
|
||
|
(DoLoop and its callees don't know the difference) and to avoid
|
||
|
consumer callbacks deleting the object out from under `DoLoop()`.
|
||
|
Example:
|
||
|
|
||
|
void HttpStreamParser::OnIOComplete(int result) {
|
||
|
result = DoLoop(result);
|
||
|
|
||
|
if (result != ERR_IO_PENDING && !callback_.is_null())
|
||
|
base::ResetAndReturn(&callback_).Run(result);
|
||
|
}
|
||
|
|
||
|
* The DoLoop pattern has no concept of different events arriving for
|
||
|
a single state; each state, if waiting, is waiting for one
|
||
|
particular event, and when `DoLoop()` is invoked when the machine is
|
||
|
in that state, it will handle that event. This reflects the
|
||
|
single-threaded model for operations spawned by the state machine.
|
||
|
|
||
|
Public class methods generally have very little processing, primarily wrapping
|
||
|
`DoLoop()`. For `DoLoop()` entry this involves setting the `next_state_`
|
||
|
variable, and possibly making copies of arguments into class members. For
|
||
|
`DoLoop()` exit, it involves inspecting the return and passing it back to
|
||
|
the caller, and in the asynchronous case, saving any passed completion callback
|
||
|
for executing by a future subsidiary IO completion (see above example).
|
||
|
|
||
|
This idiom allows synchronous and asynchronous logic to be written in
|
||
|
the same fashion; it's all just state transition handling. For mostly
|
||
|
linear state diagrams, the handling code can be very easy to
|
||
|
comprehend, as such code is usually written linearly (in different
|
||
|
handling functions) in the order it's executed.
|
||
|
|
||
|
For examples of this idiom, see
|
||
|
|
||
|
* [HttpStreamParser::DoLoop](https://code.google.com/p/chromium/codesearch#chromium/src/net/http/http_stream_parser.cc&q=HttpStreamParser::DoLoop&sq=package:chromium).
|
||
|
* [HttpNetworkTransaction::DoLoop](https://code.google.com/p/chromium/codesearch#chromium/src/net/http/http_network_transaction.cc&q=HttpNetworkTransaction::DoLoop&sq=package:chromium)
|
||
|
|
||
|
[net_error_list.h]: https://chromium.googlesource.com/chromium/src/+/master/net/base/net_error_list.h#1
|