Getting Started
This guide explains how to get started with io-endpoint, a library that provides a separation of concerns interface for network I/O endpoints.
Installation
Add the gem to your project:
$ bundle add io-endpoint
Core Concepts
io-endpoint provides a unified interface for working with network endpoints, allowing you to write code that is agnostic to the underlying transport mechanism (TCP, UDP, UNIX sockets, SSL/TLS). This separation of concerns makes it easier to:
- Write transport-agnostic code: Your application logic doesn't need to know whether it's using TCP, UDP, or UNIX sockets.
- Test with different transports: Easily swap between transports during testing.
- Handle multiple addresses: Automatically handle IPv4 and IPv6 addresses.
- Compose endpoints: Combine multiple endpoints for failover or load distribution.
The library centers around the class IO::Endpoint::Generic class, which represents a network endpoint that can be bound (for servers) or connected to (for clients). Different endpoint types handle different scenarios:
class IO::Endpoint::HostEndpoint- Resolves hostnames to addresses (e.g., "localhost:8080")class IO::Endpoint::AddressEndpoint- Works with specific network addressesclass IO::Endpoint::UNIXEndpoint- Handles UNIX domain socketsclass IO::Endpoint::SSLEndpoint- Wraps endpoints with SSL/TLS encryptionclass IO::Endpoint::CompositeEndpoint- Combines multiple endpoints
Usage
Creating a TCP Server
When you need to create a server that listens on a specific port, you can use IO::Endpoint.tcp to create a TCP endpoint:
require "io/endpoint"
# Create a TCP endpoint listening on localhost port 8080:
endpoint = IO::Endpoint.tcp("localhost", 8080)
# Bind to the endpoint and accept connections:
endpoint.bind do |server|
# The server socket is automatically closed when the block exits
server.listen(10)
loop do
client, address = server.accept
# Handle the client connection
client.close
end
end
Creating a TCP Client
To connect to a remote server, use the connect method:
require "io/endpoint"
# Create a TCP endpoint for the remote server:
endpoint = IO::Endpoint.tcp("example.com", 80)
# Connect to the server:
endpoint.connect do |socket|
# The socket is automatically closed when the block exits
socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
response = socket.read
puts response
end
Using UNIX Domain Sockets
For inter-process communication on the same machine, UNIX domain sockets provide better performance than TCP:
require "io/endpoint"
# Create a UNIX socket endpoint:
endpoint = IO::Endpoint.unix("/tmp/myapp.sock")
# Bind to the socket:
endpoint.bind do |server|
server.listen(10)
loop do
client, address = server.accept
# Handle the client connection
client.close
end
end
Using SSL/TLS
To add encryption to your connections, wrap a TCP endpoint with SSL:
require "io/endpoint"
# Create an SSL endpoint:
endpoint = IO::Endpoint.ssl("example.com", 443, hostname: "example.com")
# Connect with SSL encryption:
endpoint.connect do |socket|
# The socket is automatically encrypted
socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
response = socket.read
puts response
end
Configuring TLS Certificate Material
Use class IO::Endpoint::TLS::Configuration to provide certificate and private key material without coupling application configuration to OpenSSL. Certificate material is supplied as PEM content rather than file paths, so it can be loaded from files, environment variables, or secret stores:
trust_store = IO::Endpoint::TLS::TrustStore.parse(root_certificates)
certificate_chain = IO::Endpoint::TLS::Certificates.parse(certificate_chain_bundle)
tls_configuration = IO::Endpoint::TLS::Configuration.new(
trust_store: trust_store,
certificate_chain: certificate_chain,
private_key: private_key,
)
endpoint = IO::Endpoint.ssl(
"example.com",
443,
hostname: "example.com",
tls_configuration: tls_configuration,
)
Use TrustStore.new(certificates: certificates) when certificates are already represented as an array of individual PEM strings. Use TrustStore.parse(certificate_bundle) to split a concatenated PEM bundle into that canonical representation, or TrustStore.load("/path/to/certificates.pem") to load and parse a bundle from a file. Options such as system_certificates: true are forwarded from load to parse.
The local certificate chain is an ordered array of individual PEM strings, with the leaf certificate followed by any intermediate certificates. The root certificate is normally omitted. Use Certificates.parse(certificate_bundle) to split a concatenated bundle while preserving that order.
Set system_certificates: true to include system-provided trusted certificates. System and custom certificates can be combined in the same trust store.
The SSL endpoint converts the trust store into an OpenSSL certificate store and the complete configuration into an OpenSSL context. Other endpoint implementations can consume the same trust, identity, and verification configuration using their native TLS implementation.
The OpenSSL conversion is also available directly using IO::Endpoint::TLS::OpenSSL.build_certificate_store(trust_store).
For a server which requires clients to provide a trusted certificate, set verification: :required. Use :peer to verify a certificate when the peer provides one, and :none to explicitly disable peer verification. When a trust store is supplied and no policy is specified, peer verification is enabled by default. On client connections with a hostname, peer verification also checks that the certificate identifies that hostname.