- Feature Name:
net2 - Start Date: 2015-05-07
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
Summary
Expand the surface area of std::net to bind more low-level interfaces and
provide more advanced customization and configuration of sockets.
Motivation
Right now the API of std::net is fairly conservative in what it exposes, and
there's quite a bit more functionality that can be exposed in a cross-platform
manner. Some examples of tasks which cannot be accomplished through std::net
APIs today are:
-
The high-level functions
TcpStream::connect,TcpListener::bind, andUdpSocket::bindactually encompass a few discrete steps as part of each operation. Each step can possibly have configuration specified inbetween as well as having extra parameters not specified to the top-level function. These operations should be decomposable into independent operations. -
There are a number of existing methods for setting the various options for a socket, but they are far from comprehensive and haven't quite yet been prepared for expansion. Many new socket options will likely be added to the standard library over time so there needs to be room and design space for these options.
-
A few remaining clarification issues remain around cross-platform compatibility and behavior of APIs in various corner cases, and this RFC attempts to clarify these areas.
Detailed design
This RFC proposes APIs for each of the points listed in the motivation above, split into relevant sections.
std::net::TcpBuilder
The following API is proposed to supplement the existing types in the std::net
module:
pub struct TcpBuilder { ... }
impl TcpBuilder {
/// Constructs a new TcpBuilder with either the `AF_INET` or `AF_INET6`
/// domain, the `SOCK_STREAM` type, and with a protocol argument of 0.
///
/// Note that passing other kinds of flags or arguments can be done through
/// the `FromRaw{Fd,Socket}` implementation.
pub fn new_v4() -> io::Result<TcpBuilder>;
pub fn new_v6() -> io::Result<TcpBuilder>;
/// Binds this socket to the specified address.
///
/// This function directly corresponds to the bind(2) function on Windows
/// and Unix.
pub fn bind<T: ToSocketAddrs>(&self, addr: T) -> io::Result<&TcpBuilder>;
/// Mark a socket as ready to accept incoming connection requests using
/// accept()
///
/// This function directly corresponds to the listen(2) function on Windows
/// and Unix.
///
/// An error will be returned if `listen` or `connect` has already been
/// called on this builder.
pub fn listen(&self, backlog: i32) -> io::Result<TcpListener>;
/// Initiate a connection on this socket to the specified address.
///
/// This function directly corresponds to the connect(2) function on Windows
/// and Unix.
///
/// An error will be returned if `listen` or `connect` has already been
/// called on this builder.
pub fn connect<T: ToSocketAddrs>(&self, addr: T) -> io::Result<TcpStream>;
// socket options (specified below).
}
impl FromRaw{Fd,Socket} for TcpBuilder { ... }
impl AsRaw{Fd,Socket} for TcpBuilder { ... }The purpose of this API is to provide fine-grained control over the construction
of a TCP socket. It teases apart the convenience methods of TcpStream::connect
and TcpListener::bind by allowing configuration inbetween and tweaking the
options here and there.
std::net::UdpBuilder
Similar to the TcpBuilder above, a UdpBuilder struct will also be added:
pub struct UdpBuilder { ... }
impl UdpBuilder {
/// Constructs a new UdpBuilder with either the `AF_INET` or `AF_INET6`
/// domain, the `SOCK_DGRAM` type, and with a protocol argument of 0.
///
/// Note that passing other kinds of flags or arguments can be done through
/// the `FromRaw{Fd,Socket}` implementation.
pub fn new_v4() -> io::Result<UdpBuilder>;
pub fn new_v6() -> io::Result<UdpBuilder>;
/// Binds this socket to the specified address.
///
/// This function directly corresponds to the bind(2) function on Windows
/// and Unix.
pub fn bind<T: ToSocketAddrs>(&self, addr: T) -> io::Result<UdpSocket>;
// socket options (specified below).
}
impl FromRaw{Fd,Socket} for UdpBuilder { ... }
impl AsRaw{Fd,Socket} for UdpBuilder { ... }Socket Options
Currently there are no stable APIs to set the socket options for a socket, but there are a number of unstable APIs exposed. Additionally, there is quite an array of socket options that can both be platform specific or not available at all on some platforms. This RFC proposes a set of conventions for binding socket options in the standard library, and the existing set of options will be audited for this convention and then new ones can be exposed over time.
Each socket option is assigned an identifier foo and an associated value for
the option T. For each appropriate type of TcpListener, TcpStream,
UdpSocket, and Socket, the following inherent methods will be implemented:
pub fn foo(&self) -> io::Result<T>;
pub fn set_foo(&self, t: T) -> io::Result<()>;The TcpBuilder and UdpBuilder types will only have the "setter" variants of
the functions for now and will return &Self to enable chaining.
pub fn foo(&self, t: T) -> io::Result<&Self>;This mirrors the getter/setter conventions in the rest of Rust today, but one
notable difference is that set_foo takes &self. This aspect mirrors the
ability of the underlying system to deal with concurrent modification of socket
options. Note that some socket options do not have a "get" variant as it doesn't
always make sense.
The existing options today will be stabilized as follows:
nodelay: bool- theTCP_NODELAYoption, and this is only available onTcpStream.keepalive: Option<Duration>- same as today'sset_keepalive, but this will differ slightly in terms of implementation with the rest of the socket options here. On UnixNonewill simply setSO_KEEPALIVEtofalse, whileSome(dur)will setSO_KEEPALIVEtotrueandTCP_KEEP{ALIVE,IDLE}to the specifieddur. On Windows the implementation will start being wired up to one call to modifying theSIO_KEEPALIVE_VALSoption (this is unimplemented today). This will be available onTcpStream.read_timeout: Option<Duration>,write_timeout: Option<Duration>- these are covered by RFC 1047 however and already fit within these conventions. This option will be available on all sockets.broadcast: bool- theSO_BROADCASToption, only available onUdpSocket.multicast_loop_v4: bool- theIP_MULTICAST_LOOPoption, available onUdpSocket.multicast_ttl_v4: u32- theIP_MULTICAST_TTLoption, available onUdpSocket.ttl: u32- theIP_TTLoption, available on all sockets.multicast_loop_v6: bool- theIPV6_MULTICAST_LOOPoption, available onUdpSocket.multicast_ttl_v6: u32- theIPV6_MULTICAST_TTLoption, available onUdpSocket.only_v6: bool- theIPV6_V6ONLYoption, available on all sockets.
Some socket options with only setters will be exposed as methods:
fn join_multicast_v4(&self, multiaddr: &Ipv4Addr, interface: &Ipv4Addr)- corresponding to theIP_ADD_MEMBERSHIPoption.fn join_multicast_v6(&self, multiaddr: &Ipv6Addr, interface: u32)- corresponding to theIPV6_ADD_MEMBERSHIPoption.fn leave_multicast_v4(&self, multiaddr: &Ipv4Addr, interface: &Ipv4Addr)- corresponding to theIP_DROP_MEMBERSHIPoption.fn leave_multicast_v6(&self, multiaddr: &Ipv6Addr, interface: u32)- corresponding to theIPV6_DROP_MEMBERSHIPoption.
Cross-platform behavior and IPV6_V6ONLY
It is pointed out by @stepancheg in the std::net expansion issue that an IPv6 socket created on Windows will by default not be able to accept IPv4 connections. This is a cross-platform hazard and may end up causing some code to work on Linux (which has the opposite behavior by default) but not elsewhere.
As pointed out in the man
page, however, it's possible
to configure the default at a global level for Linux as well. Despite this, to
increase cross-platform interoperability, sockets created by TcpListener::bind
and TcpStream::connect will have this option set to false by default on all
platforms.
ToSocketAddrs and multiple addresses
APIs today that consume an argument of the type ToSocketAddrs are somewhat
ambiguous on how they use the list of addresses returned. Some APIs will attempt
each address while some will only use one address. The new guidelines will be:
- The
Tcp{Stream,Builder}::connectfunction will attempt each address in-order of what's returned from the associated iterator until one succeeds. This helps the "default usage" ofTcpStream::connect("rust-lang.org:80")to work a little more easily. - All other methods will return an error if 0 or more than 1 address is resolved
to. This includes
UdpSocket::{bind, send_to}andTcpListener::bind. This change is primarily a tweak in semantics to requiring precisely one address for these operations instead of ignoring other addresses or trying to use them.
Drawbacks
The builder-style API of {Tcp,Udp}Builder is not following the conventional
builder guidelines due to the ability that each intermediate step being able to
fail and the desire to understand at precisely what point each operation failed
at. This hinders the ergonomics of the API, however, as the try! macro would
need to be leveraged quited a bit or some other error-handling mechanism would
be required. This means that usage today would look like:
try!(try!(try!(try!(TcpBuilder::new_v6())
.nodelay(true))
.keepalive(Some(Duration::new(0, 500))))
.read_timeout(None))If, however, the ? operator is implemented the builder API
could be used quite fluently:
TcpBuilder::new_v6()?
.nodelay(true)?
.keepalive(Some(Duration::new(0, 500))?
.read_timeout(None);All-in-all having a builder-style API today to prepare for this in the future shouldn't be a hindrance, and it just means that today's usage will probably avoid chaining of API calls.
This API also does not cover all the use cases of using sockets today. For example a Unix socket-style connection would not be covered by any of these builders and would not benefit from the APIs in the standard library. Additionally, other kinds of sockets (such as raw sockets) would also not benefit as they would have to have many of the same bindings out-of-std as well.
Alternatives
An "all powerful" std::net::Socket
An alternative approach to the above builder API would be one of an "all
powerful" std::net::Socket which is the thinnest layer over the OS as
possible. For example:
// A raw OS socket which can be configured and then transformed into a higher
// level abstraction such as a `TcpListener`, `TcpStream`, or `UdpSocket`.
pub struct Socket { ... }
// Arguments to the `Socket::new` function
pub struct Domain { val: c_int }
pub struct Type { val: c_int }
pub struct Protocol { val: c_int }
// Representation of a raw socket address that is generally passed to other
// low-level functions. This structure is generally generated via `.into()` on a
// reference to a normal address.
pub struct RawSocketAddr {
pub addr: *const sockaddr,
pub len: socklen_t,
}
impl Socket {
/// Creates a new socket with the specified arguments.
///
/// This function directly corresponds to the socket(2) function on Unix,
/// and translates to a call to `WSASocket` on Windows with the
/// `WSA_FLAG_OVERLAPPED` and `WSA_FLAG_NO_HANDLE_INHERIT` flags specified.
pub fn new(domain: Domain,
type_: Type,
protocol: Option<Protocol>) -> io::Result<Socket>;
/// Binds this socket to the specified address.
///
/// This function directly corresponds to the bind(2) function on Windows
/// and Unix.
pub fn bind<T: To<RawSocketAddr>>(&self, addr: &T) -> io::Result<()>;
/// Mark a socket as ready to accept incoming connection requests using
/// accept()
///
/// This function directly corresponds to the listen(2) function on Windows
/// and Unix.
pub fn listen(&self, backlog: i32) -> io::Result<()>;
/// Initiate a connection on this socket to the specified address.
///
/// This function directly corresponds to the connect(2) function on Windows
/// and Unix.
pub fn connect<T: To<RawSocketAddr>>(&self, addr: &T) -> io::Result<()>;
/// Accept a new socket from this socket.
///
/// This function directly corresponds to the accept(2) function on Windows
/// and Unix.
pub fn accept(&self) -> io::Result<(Socket, SocketAddr)>;
/// Same as `try_clone` on other primitives.
pub fn try_clone(&self) -> io::Result<Socket>;
/// Same as `shutdown` on other primitives.
pub fn shutdown(&self, how: Shutdown) -> io::Result<()>
// socket options (specified in the next section) ..
/// Same as `UdpSocket::send_to`, the `flags` parameter to the underlying
/// syscall is set to 0.
pub fn send_to<A: ToSocketAddrs>(&self, data: &[u8], addr: A)
-> io::Result<usize>;
/// Same as `UdpSocket::recv_from`, the `flags` parameter to the underlying
/// syscall is set to 0.
pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)>;
}
// Standard I/O operations
impl Read for Socket { ... }
impl Read for &Socket { ... }
impl Write for Socket { ... }
impl Write for &Socket { ... }
// Conversion traits/types for `Socket`
impl AsRaw{Fd,Socket} for Socket { ... }
impl From<Socket> for TcpListener { ... }
impl From<Socket> for TcpStream { ... }
impl From<Socket> for UdpSocket { ... }
impl<'a> Into<RawSocketAddr> for &'a SocketAddr { ... }
// Common constructors, conversions, and inspectors for the various arguments to
// `Socket::new`. Platform-specific constructors can be provided through
// platform specific extension traits
impl Domain {
pub fn ipv4() -> Domain;
pub fn ipv6() -> Domain;
pub fn value(&self) -> c_int;
}
impl From<c_int> for Domain { ... }
impl Type {
pub fn stream() -> Type;
pub fn datagram() -> Type;
pub fn raw() -> Type;
pub fn value(&self) -> c_int;
}
impl From<c_int> for Type { ... }
impl From<c_int> for Protocol { ... }The upside of this approach is that it is maximally flexible in terms of usage. All interactions with C APIs can be directly translated to Rust itself with ease and there is a clear layer of abstraction for what's going on here. The downsides of this approach, however, are:
- It's unclear why the
TcpStreamand other refined primitives still exist. This API is a union of theTcpStream,TcpListener, andUdpSocketAPIs and poses a new question of "should I use a Socket or TcpStream?". - The scope of this API is somewhat unclear, for example is
Socketintended to encompass Unix sockets as well? It attempts to via theInto<RawSocketAddr>trait bound, but it's arguably not super ergonomic. - Many mis-usages of the API which would statically be prevented (such as
calling
accepton a UDP socket) are allowed with a singleSocketAPI. Other examples of this are setting TCP options for a datagram socket.
Overall the builder-based approach seemed to give the same level of flexibility while maintaining a clearly defined purpose of solely operating as builders for new sockets.
A note on typestate
Note that there are many alternatives within this alternative itself (of having a unifying notion of a "socket"), such as using typestate to solve the mis-usage bullet point above. Sockets are often a classical use case of a state-transition diagram, so this can be a natural conclusion to come to.
There are, however, not that many usages of typestate throughout the rest of the standard library today, and there are a number of ergonomic and API concerns which would be difficult to overcome.
Socket Options
The detailed design section above outlined a convention for adding a pair of methods per socket option, but this has the drawback of leading to quite a large API surface area and it's tough to view the set of "options related methods" in a group. An alternative to the scheme proposed above would be something along the lines of the following:
mod net::opt {
pub trait Sockopt {
type Value;
fn get(socket: &Socket) -> io::Result<Self::Value>;
fn set(socket: &Socket, value: Self::Value) -> io::Result<()>;
}
// socket options
pub enum KeepAlive {} // SO_KEEPALIVE, Value = bool
pub enum ReuseAddress {} // SO_REUSEADDR, Value = bool
pub enum Broadcast {} // SO_BROADCAST, Value = bool
pub enum ReadTimeout {} // SO_RCVTIMEO, Value = Option<Duration>
pub enum WriteTimeout {} // SO_SNDTIMEO, Value = Option<Duration>
// tcp options
pub enum TcpNodelay {} // TCP_NODELAY, Value = bool
// ipv4 options
pub enum AddMembershipV4 {} // IP_ADD_MEMBERSHIP, Value = MulticastRequestV4
pub enum DropMembershipV4 {} // IP_DROP_MEMBERSHIP, Value = MulticastRequestV4
pub enum MulticastLoopV4 {} // IP_MULTICAST_LOOP, Value = bool
pub enum MulticastTtlV4 {} // IP_MULTICAST_TTL, Value = u32
pub enum TtlV4 {} // IP_TTL, Value = u32
// ipv6 options
pub enum AddMembershipV6 {} // IPV6_ADD_MEMBERSHIP, Value = MulticastRequestV6
pub enum DropMembershipV6 {} // IPV6_DROP_MEMBERSHIP, Value = MulticastRequestV6
pub enum MulticastLoopV6 {} // IPV6_MULTICAST_LOOP, Value = bool
pub enum OnlyV6 {} // IPV6_V6ONLY, Value = bool
pub struct MulticastRequestV6 {
pub multiaddr: Ipv6Addr,
pub interface: u32,
}
}
mod os::unix::net {
pub enum TcpKeepAlive {} // TCP_KEEP{ALIVE,IDLE}, Value = Duration
// others as necessary
}
impl {Socket,TcpListener,TcpStream,UdpSocket} {
pub fn set<T: Sockopt>(&self, val: T::Value) -> io::Result<()> {
T::set(self, val)
}
pub fn get<T: Sockopt>(&self) -> io::Result<T::Value> {
T::get(self)
}
}
// example usage
use std::net::opt::{KeepAlive, TcpKeepAlive};
socket.set::<KeepAlive>(true);
socket.set::<TcpKeepAlive>(duration);With this API there's a strict transcribing of the conventions proposed above
with the Sockopt trait, and the major upside of this approach is that
libraries can define their own socket options (via a new type) which can use the
same syntax as all the standard socket options for being get/set. There are,
however, a number of downsides to this approach:
- Setting an option requires at least one import.
- Socket options are now grouped together, but discoverability isn't necessarily improved as there are still lots of implementors that need to be waded through.
- It's not possible to statically prevent TCP options from being set on a UDP socket (which will likely always generate an error at runtime).
- Lots of new types are introduced just for adding names, which is somewhat unconventional in the standard library right now.
TcpListener::bind_to_port
Requested, in the std::net expansion issue, it's noted that a common desire is to simply bind a listener to a port not worrying about the address. This RFC does not at this time add this sort of convenience method.
Enhancing lookup_host and lookup_addr
These two bindings of getaddrinfo and getnameinfo are currently unstable yet
are quite useful from time to time. This RFC does not propose stabilizing these
APIs or reconsidering them at this time. The possible known drawbacks of the API
are:
-
The
getaddrinfoandgetnameinfofunction calls have a number of arguments which are not exposed at this layer of the API (e.g. flags,serv,host,hints, etc). Additionally, the return value forgetaddrinfois not fully exposed. -
The return value of these APIs can be dubious by turning up duplicates.
-
There is a desire to possibly filter the return value based on certain criteria.
Overall these functions may want to be tweaked slightly to have different arguments and/or return values after the underlying implementation has been investigated more. At this time this RFC does not strive to handle these APIs.
Asynchronous I/O
Networking primitives are one of the primary users of asynchronous I/O
utilities, and there are clear cross-platform abstractions for building this
sort of functionality such as select and setting a socket to nonblocking mode.
This RFC does not, however, attempt to set forth a vision for nonblocking
programming with sockets. It is considered in a few locations (such as setting
WSA_FLAG_OVERLAPPED for sockets on Windows), but no explicit bindings are
proposed to be added at this time.
Unresolved questions
None, yet.