View on GitHub

chops-net-ip

Chops Net IP ("C"onnective "H"andcrafted "Op"enwork "S"oftware) is a tasty C++ library that makes asynchronous IP network coding fun! Or at least this library makes asynchronous network programming easier and safer without sacrificing significant performance.

Chops Net IP Detailed Overview

A comparison of C++ socket libraries with Chops Net IP is available here.

A FAQ is available here.

Motivation

“Wire Protocol” - the definition of the bits and bytes and formats of the data needed for a specific network protocol. It is how data is transported from one point to another on a network. See https://en.wikipedia.org/wiki/Wire_protocol.

Chops Net IP is motivated by the need for a networking library that:

General Usage and Design Model

Chops Net IP is (in general terms) a “create network entity, provide function objects and let them do the work” API for incoming data, and a “send and forget” API for outgoing data.

For incoming data, an application provides callable function objects to the appropriate Chops Net IP object for message framing (if needed), message processing, and connection state transitions.

For outgoing data, an application passes message data (as byte buffers) to the appropriate Chops Net IP object for queueing and transmission. The application can query information about the outgoing data queue.

Various Chops Net IP objects are provided to the application (typically in application provided function objects) as connection or endpoint states transition. For example, the Chops Net IP object for data sending is only created when a connection becomes active (TCP acceptor connection is created, or a TCP connector connection succeeds). This guarantees that an application cannot start sending data before a connection is active.

Chops Net IP has the following design goals:

Component Directory

All of the essential (core) Chops Net IP headers are in the net_ip and detail directories. There are additional useful (and tested) classes and functions in the net_ip_component directory. One of the net_ip_component headers provides an executor and “run loop” convenience class, and other headers provide classes or functions for common use cases such as returning std::future objects that provide an io_interface object for starting the read processing and enabling sends.

Exceptions and Error Handling

There are no exceptions thrown by Chops Net IP code. All exceptions thrown by the Asio library are caught and reported through other error mechanisms.

Many public methods use an experimental version of std::expected to report errors. A std::expected object either evaluates to true and provides a return value (as needed, through pointer syntax), or evaluates to false and provides an error code.

(The experimental version of std::expected is provided by Martin Moene, and uses the namespace nonstd. See References).

Besides the std::expected return value, errors are also reported through a function object callback specified in the net_entity start method.

States and Transitions

Chops Net IP states and transitions match existing standard network protocol behavior. For example, when a TCP connector is created, an actual TCP data connection does not exist until the connect succeeds. When this happens (connect succeeds), the abstract state transitions from unconnected to connected. In Chops Net IP, when a TCP connector connects, a data connection object is created and an application state transition function object callback is invoked containing a handle object to the connection object.

Even though an implicit state transition table exists within the Chops Net IP library (matching network protocol behavior), there are not any explicit state flags or methods to query the state through the API. Instead, state transitions are handled through application supplied function object callbacks, which notify the application that something interesting has happened and containing objects for further interaction and processing. In other words, there is not an “is_connected” method with the Chops Net IP library. Instead, an application can layer its own state on top of Chops Net IP (if desired), using the function object callbacks to manage the state.

Pro tip - Chops Net IP follows the implicit state model of the Asio library (and similar libraries) where state transitions are implemented through chaining function objects on asynchronous operations. Developers familiar with implicit or explicit state transition models will be familiar with the application model defined for Chops Net IP. Chops Net IP insulates the application from the intricacies of the Asio library and simplifies the state transition details.

Image of Chops Net IP objects and states

Constraints

Chops Net IP works well with the following communication patterns:

Chops Net IP requires more work with the following communication pattern:

Chops Net IP works extremely well in environments where there might be a lot of network connections (e.g. thousands), each with a moderate amount of traffic, and each with different kinds of data or data processing. In environments where each connection is very busy, or a lot of processing is required for each incoming message (and it cannot be passed along to another thread), more traditional communication patterns or designs might be appropriate (e.g. blocking or synchronous I/O, or “thread per connection” models).

Applications that do only one thing and must do it as fast as possible with the least amount of overhead might not want the abstraction penalties and overhead of Chops Net IP. For example, a high performance server application where buffer lifetimes for incoming data are easily managed might not want the queuing and “shared buffer” overhead of Chops Net IP.

Applications that need to perform time consuming operations on incoming data and cannot pass that data off to another thread may encounter throughput issues. Multiple threads or thread pools or strands interacting with the event loop method (executor) may be a solution in those environments.

There are no read timeouts in Chops Net IP. Applications that need a no data received timeout must create their own timer and take down connections as appropriate.

Application Customization Points

Chops Net IP strives to provide an application customization point (via function object) for every important step in the network processing chain. These include:

State Change Customization Point

The state change “IO is ready or IO has closed” (i.e. a TCP connection has been created or destroyed or a UDP socket has opened or closed) callback interface is consistent across all protocol types and provides:

When an error or shutdown condition occurs (e.g. a TCP connection has been shutdown or broken), a error callback interface is invoked, providing:

TCP Message Frame Customization Point

A message frame customization point provides logic for TCP streams that determine when a message begins and ends. Typically a header is decoded which then determines the following message body len. This may be a complicated process with nested levels of header and body decoding.

Not all TCP applications need message framing logic. For example, many Internet protocols define a message delimeter at the end of a stream of bytes (e.g. a newline or other sequence of bytes). Chops Net IP allows this alternative for message framing through a separate start_io method that does not require a message framing function object.

A non-trivial amount of decoding may be needed for message framing and in some use cases it is desirable to store message framing state data. There are multiple designs that allow the message framing state to be passed along to the message handling function object.

Message Handling Customization Point

A message handling callback interface is consistent across all protocol types (although templatized between TCP and UDP interface).

An IO object is provided for “send data back to the originator” logic. The remote endpoint is provided to enable this for UDP (for TCP connections, the remote endpoint is always the same and is ignored when replying with data).

The full incoming byte buffer (message) is always provided to the message handling callback.

Library Implementation Design Considerations

Reference counting (through std::shared_ptr and std::weak_ptr facilities) is an aspect of many of the internal (detail namespace) Chops Net IP classes. This simplifies the lifetime management of all of the objects at the expense of the reference counting overhead.

Future versions of the library may have more move semantics and less reference counting, but will always implement safety over performance.

Most of the Chops Net IP public classes (net_entity, basic_io_interface, basic_io_output) use std::weak_ptr references to the internal reference counted objects. This means that application code which ignores state changes (e.g. a TCP connection that has ended) will have errors returned by the Chops Net IP library when trying to access a non-existent object (e.g. trying to send data through a TCP connection that has gone away). This is preferred to “dangling pointers” that result in process crashes or requiring the application to continually query the Chops Net IP library for state information.

Image of Chops Net IP Tcp Acceptor internal

Image of Chops Net IP Tcp Connector and UDP internal

Where to provide the customization points in the API is one of the most crucial design choices. Using template parameters for function objects and passing them through call chains is preferred to storing the function object in a std::function. In general, performance critical paths, primarily reading and writing data, always use function objects passed through as template parameters, while less performance critical paths may use a std::function.

Since data can be sent at any time and at any rate by the application, a sending queue is required. The queue can be queried to find out if congestion is occurring. (An interface for sending data which returns a std::future and bypasses the output queue may be implemented in future releases.)

Mutex locking is kept to a minimum in the library. Alternatively, some of the internal handler classes may serialize certain operations by posting functions through the io context executor. This allows multiple threads to be calling into one internal handler and as long as the parameter data is thread-safe (which it is), thread safety is managed by the Asio executor and posting queue code.

Many of the public methods that call into internal handlers use a std::future and Asio post to coordinate and serialize certain state changing operations.

Future Directions