pw_transport#
Asynchronous datagram-oriented transport interface
Experimental C++
Note
pw_transport is still considered experimental. While the core interfaces
are likely stable, some changes are still expected to be made around the
edges. The documentation is also incomplete.
pw_transport defines interfaces for async-native, datagram-oriented
network sockets built on pw_async2.
The core interfaces in pw_transport are Socket
and ReliableSocket: abstract handles to
some underlying transport, used for reading and writing. Any ReliableSocket can be implicitly converted to and used as a
Socket. Sockets make use of pw_buf
to enable zero-copy reads and writes directly into transport buffers.
Sockets are provisioned by one of two transport interfaces: a Connector / ReliableConnector which establishes them, or a Listener / ReliableListener which accepts them from a peer.
Usage#
Once a ReliableSocket is obtained from a ReliableConnector or ReliableListener, tasks read and write messages asynchronously. Writes use a two-phase reservation model: ReliableSocket::ReserveWrite returns a future that resolves to a WriteReservation once buffer space is available. If the underlying transport’s outgoing queue is full, the future pends, naturally applying backpressure to the sender.
Zero-length datagrams are supported: ReserveWrite(0) reserves a zero-byte
write slot, and Read
resolves to a non-null, empty pw::ConstBuf (such as
pw::ConstBuf::ZeroLength()) when a zero-byte datagram is received. A null
ConstBuf (response == nullptr) indicates that the socket has closed.
For example, consider a task which sends a message and waits for the peer’s response:
class RequestResponseTask : public pw::async2::Task {
public:
explicit RequestResponseTask(pw::transport::ReliableConnector& connector)
: connector_(connector) {}
private:
enum class State {
kConnecting,
kWriting,
kReading,
};
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override {
while (true) {
switch (state_) {
case State::kConnecting: {
if (!connect_future_.is_pendable()) {
connect_future_ = connector_.Connect();
}
PW_AWAIT(auto result, connect_future_, cx);
if (!result.ok()) {
return pw::async2::Ready();
}
socket_ = std::move(*result);
state_ = State::kWriting;
break;
}
case State::kWriting: {
// Reserve space in an outgoing transport buffer.
if (!reserve_future_.is_pendable()) {
reserve_future_ = socket_.ReserveWrite(kPayload.size());
}
PW_AWAIT(auto reservation, reserve_future_, cx);
if (!reservation.has_value()) {
return pw::async2::Ready();
}
// Write the payload directly into the transport buffer without
// intermediate storage or copying.
SerializePayload(pw::ByteSpan(*reservation));
if (!reservation->Commit(kPayload.size())) {
return pw::async2::Ready();
}
state_ = State::kReading;
break;
}
case State::kReading: {
// Read the peer's response.
if (!read_future_.is_pendable()) {
read_future_ = socket_.Read();
}
PW_AWAIT(pw::ConstBuf response, read_future_, cx);
if (response != nullptr) {
ProcessResponse(response);
}
return pw::async2::Ready();
}
}
}
}
pw::transport::ReliableConnector& connector_;
pw::transport::ReliableSocket socket_;
State state_ = State::kConnecting;
pw::transport::ReliableConnector::ConnectFuture connect_future_;
pw::transport::ReserveWriteFuture reserve_future_;
pw::transport::ReadFuture read_future_;
};
pw::async2::Coro<pw::Status> SendAndReceive(
pw::async2::CoroContext&, pw::transport::ReliableConnector& connector) {
PW_CO_TRY_ASSIGN(pw::transport::ReliableSocket socket,
co_await connector.Connect());
// Reserve space in an outgoing transport buffer.
std::optional<pw::transport::WriteReservation> reservation =
co_await socket.ReserveWrite(kPayload.size());
if (!reservation.has_value()) {
co_return pw::Status::Aborted();
}
// Write the payload directly into the transport buffer without
// intermediate storage or copying.
SerializePayload(pw::ByteSpan(*reservation));
if (!reservation->Commit(kPayload.size())) {
co_return pw::Status::Aborted();
}
// Read the peer's response.
pw::ConstBuf response = co_await socket.Read();
if (response == nullptr) {
co_return pw::Status::Aborted();
}
ProcessResponse(response);
co_return pw::OkStatus();
}
Addressing#
pw_transport does not define an addressing layer, as this is inherently
platform and protocol dependent. Where required, addressing should be built into
implementations of Listener / ReliableListener and
Connector / ReliableConnector. For example:
class FramedTcpConnector : public pw::transport::ReliableConnector {
public:
FramedTcpConnector(std::string_view host, uint16_t port);
pw::transport::ReliableConnector::ConnectFuture Connect() override;
};
Instances of these objects can then be configured for specific endpoints and
passed around into APIs that consume pw_transport.
In-Memory Forwarding Sockets#
ForwardingDatagramSocketPair creates a connected pair of in-memory
ReliableSocket endpoints (first() and second()) that
forward datagrams to each other without copying. It is useful for unit testing
and in-process communication between tasks.
ForwardingDatagramSocketOptions configures the queue depth, which limits
the write reservations and unread datagrams in each direction, and the maximum
datagram size in each direction.
Implementing#
Authors writing an implementation for your own transport are required to implement three virtual interfaces:
Listener / ReliableListener: the listening side of the transport.
Connector / ReliableConnector: the establishing side of the transport.
SocketImpl / ReliableSocketImpl: the core socket interfaces backing
SocketandReliableSockethandles. Allocated and refcounted.
Refer to the doxygen docs for details of each of these, with requirements and preconditions.