Skip to content
Permalink
Branch: net2.1
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
528 lines (419 sloc) 20.9 KB
  • 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, and UdpSocket::bind actually 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 - the TCP_NODELAY option, and this is only available on TcpStream.
  • keepalive: Option<Duration> - same as today's set_keepalive, but this will differ slightly in terms of implementation with the rest of the socket options here. On Unix None will simply set SO_KEEPALIVE to false, while Some(dur) will set SO_KEEPALIVE to true and TCP_KEEP{ALIVE,IDLE} to the specified dur. On Windows the implementation will start being wired up to one call to modifying the SIO_KEEPALIVE_VALS option (this is unimplemented today). This will be available on TcpStream.
  • 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 - the SO_BROADCAST option, only available on UdpSocket.
  • multicast_loop_v4: bool - the IP_MULTICAST_LOOP option, available on UdpSocket.
  • multicast_ttl_v4: u32 - the IP_MULTICAST_TTL option, available on UdpSocket.
  • ttl: u32 - the IP_TTL option, available on all sockets.
  • multicast_loop_v6: bool - the IPV6_MULTICAST_LOOP option, available on UdpSocket.
  • multicast_ttl_v6: u32 - the IPV6_MULTICAST_TTL option, available on UdpSocket.
  • only_v6: bool - the IPV6_V6ONLY option, 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 the IP_ADD_MEMBERSHIP option.
  • fn join_multicast_v6(&self, multiaddr: &Ipv6Addr, interface: u32) - corresponding to the IPV6_ADD_MEMBERSHIP option.
  • fn leave_multicast_v4(&self, multiaddr: &Ipv4Addr, interface: &Ipv4Addr) - corresponding to the IP_DROP_MEMBERSHIP option.
  • fn leave_multicast_v6(&self, multiaddr: &Ipv6Addr, interface: u32) - corresponding to the IPV6_DROP_MEMBERSHIP option.

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}::connect function will attempt each address in-order of what's returned from the associated iterator until one succeeds. This helps the "default usage" of TcpStream::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} and TcpListener::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 TcpStream and other refined primitives still exist. This API is a union of the TcpStream, TcpListener, and UdpSocket APIs and poses a new question of "should I use a Socket or TcpStream?".
  • The scope of this API is somewhat unclear, for example is Socket intended to encompass Unix sockets as well? It attempts to via the Into<RawSocketAddr> trait bound, but it's arguably not super ergonomic.
  • Many mis-usages of the API which would statically be prevented (such as calling accept on a UDP socket) are allowed with a single Socket API. 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 getaddrinfo and getnameinfo function 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 for getaddrinfo is 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.

You can’t perform that action at this time.