Crate socket_config

source ·
Expand description

This library sets up sockets in a way that can be controlled by the user of your application, such as through a command-line option or configuration file.

For example, your application might take a command-line option --listen=SOCKET, where SOCKET is a socket address that this library parses. Socket addresses can take forms like 127.0.0.1:12345 (IPv4), [::1]:12345 (IPv6), ./my.socket (Unix-domain), or fd:3 (Unix file descriptor or Windows socket handle inherited from the parent process). See the SocketAddr documentation for complete syntax.

Usage

The entry point of this library is the open function, which accepts a socket address and a set of options, and opens a socket accordingly.

open returns a socket2::Socket, which can be used for ordinary blocking I/O. This library also has the AnyStdSocket type in the convert module, which can be used to convert a socket2::Socket into one of the standard library’s socket types. For non-blocking I/O with tokio, the convert module includes AnyTokioListener and AnyTokioStream.

Feature flags and platform support

This library is based on socket2, and should work on any platform that socket2 works on. A list of platforms supported by socket2 can be found on its crates.io page.

Some items in this crate are limited in which platforms they’re available on, or behave differently on different platforms, or are only available if a particular feature flag is enabled. Such differences are noted with an “Availability” section in those items’ documentation.

Available feature flags

This library has the following feature flags:

Examples

Blocking I/O example

This is a CHARGEN server that accepts a single connection. It uses blocking I/O.

Click to show example
use anyhow::Context as _;
use socket_config::{
	SocketAddr,
	SocketAppOptions,
	SocketUserOptions,
};
use socket2::Socket;
use std::io::Write;

/// A simple chargen server that listens on a stream socket, accepts one connection,
/// and endlessly sends characters to the client.
#[derive(clap::Parser)]
struct CommandLine {
	#[command(flatten)]
	options: SocketUserOptions,

	socket: SocketAddr,
}

fn main() -> anyhow::Result<()> {
	// Parse the command line options.
	let command_line = <CommandLine as clap::Parser>::parse();

	// Set up the `SocketAppOptions`.
	//
	// In this example, we'll set a default port of 27910, and leave the other
	// options alone.
	let mut socket_app_options = SocketAppOptions::new(socket2::Type::STREAM);
	socket_app_options.default_port = Some(27910);

	// Open the socket.
	let socket: Socket = socket_config::open(
		&command_line.socket,
		&socket_app_options,
		&command_line.options,
	).context("couldn't open socket")?;

	// Wait for and accept a connection.
	let (mut connection, _): (Socket, _) = loop {
		let result = socket.accept();

		// On some platforms, `accept` can fail due to the system call being
		// interrupted. When it does, just try again.
		if matches!(&result, Err(e) if e.kind() == std::io::ErrorKind::Interrupted) {
			continue;
		}

		break result
	}.context("couldn't accept a connection")?;

	// Close the listening socket once a connection is established.
	drop(socket);

	// Generate the characters we're going to send to the client: everything in the ASCII range.
	let chars: Vec<u8> = (b' '..b'~').into_iter().collect();

	// Send characters repeatedly until the client disconnects.
	loop {
		match connection.write_all(&chars) {
			Ok(()) => {
				// Bytes sent successfully. Keep going.
			}

			Err(error) if error.kind() == std::io::ErrorKind::WriteZero => {
				// The client disconnected.
				break;
			}

			Err(error) => {
				// There was an error. Report it.
				//
				// Note that `write_all` *does* check for `std::io::ErrorKind::Interrupted`,
				// so no need to check that here.
				return Err(
					anyhow::format_err!(error)
					.context("couldn't send characters to client")
				);
			}
		}
	}

	Ok(())
}

Non-blocking I/O example

This is an Echo server that modifies received bytes before echoing them. It uses Tokio non-blocking I/O.

Click to show example
use anyhow::Context as _;
use socket_config::{
	convert::{
		AnyTokioListener,
		AnyTokioStream,
	},
	SocketAddr,
	SocketAppOptions,
	SocketUserOptions,
};
use socket2::Socket;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

/// A simple echo server that listens on a stream socket and echoes back all bytes to clients,
/// incremented by one.
#[derive(clap::Parser)]
struct CommandLine {
	#[command(flatten)]
	options: SocketUserOptions,

	socket: SocketAddr,
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
	// Parse the command line options.
	let command_line = <CommandLine as clap::Parser>::parse();

	// Set up the `SocketAppOptions`.
	//
	// In this example, we'll set a default port of 27910, and leave the other
	// options alone.
	let mut socket_app_options = SocketAppOptions::new(socket2::Type::STREAM);
	socket_app_options.default_port = Some(27910);

	// Open the socket.
	let socket: Socket = socket_config::open(
		&command_line.socket,
		&socket_app_options,
		&command_line.options,
	).context("couldn't open socket")?;

	// Set up the socket for use with Tokio.
	let socket: AnyTokioListener =
		socket.try_into()
		.context("couldn't configure socket for Tokio")?;

	// Start accepting connections.
	loop {
		let (connection, _): (AnyTokioStream, socket2::SockAddr) =
			socket.accept().await
			.context("couldn't accept a connection")?;

		tokio::task::spawn(echo(connection));
	}
}

async fn echo(mut connection: AnyTokioStream) {
	let mut buf = [0u8; 1024];

	loop {
		// Read some bytes from the client.
		let bytes_read = match connection.read(&mut buf).await {
			Ok(0) => break,
			Ok(n) => n,
			Err(error) => {
				eprintln!("Error reading from client: {error}!");
				break
			}
		};

		// Take a slice of the buffer, containing just the bytes that were read.
		let buf = &mut buf[..bytes_read];

		// Increment each byte by one.
		for byte in &mut *buf {
			*byte = byte.wrapping_add(1);
		}

		// Echo the bytes back.
		if let Err(error) = connection.write_all(buf).await {
			eprintln!("Error writing to client: {error}!");
			break;
		}
	}
}
  • socket2: Basis of this library.
  • tokio_listener: Inspired this library. This library has largely the same purpose as tokio_listener, but uses a different approach.

Modules

Structs

  • Options for opening a socket, supplied by your application itself. This is one of the three parameters to open.
  • Options for opening a socket, supplied by the user of your application. This is one of the three parameters to open.

Enums

  • The address to bind a socket to, or a description of an inherited socket to use. This is one of the three parameters to open.

Functions

  • Checks whether the file at the given path is a Unix-domain socket.
  • Mark a socket as inheritable (or not), so that a child process will (or will not) inherit it.
  • socket_config entry point. Opens a socket (or claims an inherited one), according to the given address and options.