Services
Crate hopf-core defines the process seams; protocol crates plug in by implementing ProtocolHandler and/or registering listeners through a Service-shaped helper (FtpService, SmtpService, HTTP endpoint factories).
Contents
Layering
Application Service / factory
│
▼
HandlerFactory → Box<dyn ProtocolHandler> (per connection / stream)
│
▼
Endpoint (TCP Connection | QuicStreamEndpoint | …)
│
▼
Worker reactor (affinity for life)
HTTP inserts an extra Stream layer: the ProtocolHandler is H1Endpoint / CleartextHttpEndpoint / …, which drives ServerHandler. FTP/SMTP implement ProtocolHandler directly on the control connection.
ProtocolHandler
Invoked on the owning reactor thread — must not block. Panics are caught at the connection boundary (log + close).
| Method | When |
|---|---|
connected |
Socket registered on a worker (not on the accept thread) |
receive |
Plaintext bytes; advance the cursor (*data = &data[n..]); suffix preserved |
disconnected |
Peer closed / endpoint finished closing (clean FIN / local shutdown) |
security_established |
TLS/QUIC active (ALPN / cipher in SecurityInfo); default no-op |
error |
Unrecoverable I/O or protocol error. On QUIC, also used for abnormal teardown (peer CONNECTION_CLOSE, idle timeout, STOP_SENDING) with typed sources such as hopf_quic::QuicConnectionCloseError — not in addition to disconnected |
use hopf_core::{Endpoint, ProtocolHandler};
use std::io;
struct Echo;
impl ProtocolHandler for Echo {
fn connected(&mut self, _endpoint: &mut dyn Endpoint) {}
fn receive(&mut self, endpoint: &mut dyn Endpoint, data: &mut &[u8]) {
endpoint.send(data);
*data = &[]; // consume
}
fn disconnected(&mut self, _endpoint: &mut dyn Endpoint) {}
fn error(&mut self, _endpoint: &mut dyn Endpoint, _err: &io::Error) {}
}Register with TcpListenerConfig::new(addr, || Box::new(Echo)). NopHandler is the no-op default for tests / unused uni streams.
Service trait
pub trait Service: Send {
fn start(&mut self, runtime: &Runtime) -> std::io::Result<()>;
fn stop(&mut self);
fn tcp_listeners(&self) -> &[TcpListenerConfig] { &[] }
}| Method | Role |
|---|---|
start |
Initialise app resources; register bindings via Runtime APIs (or Composition) |
stop |
Tear down listeners / resources |
tcp_listeners |
Optional static set — transitional; prefer Runtime registration |
Runtime::start_service(&mut service) calls start. Composition owns the listen/dial script for declarative apps — a frozen tcp_listeners slice is not the preferred model.
Runtime::add_tcp_listener
let (bound, id) = rt.add_tcp_listener(config)?;
// later
rt.remove_binding(id);Returns local SocketAddr (useful with port 0) and opaque BindingId.
How protocol services plug in
| Crate | Registration style | Handler SPI |
|---|---|---|
hopf-http |
HandlerFactory → CleartextHttpEndpoint / AlpnHttpEndpoint / H1Endpoint::server / listen_h3 |
ServerHandlerFactory |
hopf-ftp |
FtpService::start(Arc<Runtime>) builds TcpListenerConfig |
FtpConnectionHandlerFactory |
hopf-smtp |
SmtpService::start (or SimpleRelayService / LocalDeliveryService) |
SmtpHandlerFactory |
hopf-webdav / websocket / grpc |
Supply a ServerHandlerFactory to an HTTP endpoint |
Stream-level factories |
hopf-dns |
Forwarder / resolver on reactor UDP | datagram / connector helpers |
Common pattern: protocol crate owns config + factory; start closes over Arc<Runtime> when the protocol needs secondary listeners (FTP PASV).
HTTP factories
use std::sync::Arc;
use hopf_core::{ProtocolHandler, Runtime, TcpListenerConfig};
use hopf_http::{CleartextHttpEndpoint, HttpLimits, ServerHandlerFactory};
fn listen_http(rt: &Runtime, app: Arc<dyn ServerHandlerFactory>) -> std::io::Result<()> {
let limits = HttpLimits::default();
rt.add_tcp_listener(TcpListenerConfig::new(addr, move || {
Box::new(CleartextHttpEndpoint::new(Arc::clone(&app), limits))
as Box<dyn ProtocolHandler>
}))?;
Ok(())
}Higher-level HTTP apps (WebDAV, WebSocket, gRPC) implement ServerHandlerFactory and reuse the same listen wiring. Auth wrappers (BasicAuthFactory, …) also implement ServerHandlerFactory. See http/server.md.
FTP / SMTP factories
FTP
use hopf_auth::PasswordTrustPolicy;
use hopf_ftp::{FtpConfig, FtpService};
use hopf_core::Runtime;
use std::sync::Arc;
let policy = PasswordTrustPolicy::new().with_user("alice", "s3cret").shared();
let config = FtpConfig::new("127.0.0.1:2121".parse()?, "/tmp/ftp", policy);
let service = FtpService::new(config);
// or FtpService::with_handler_factory(config, custom_factory);
let addr = service.start(Arc::new(/* Runtime */))?;FtpService::control_listener builds the TcpListenerConfig (TLS via with_tls / implicit_ftps on FtpConfig). PASV data listeners are added later through the held Arc<Runtime>.
SMTP
use hopf_smtp::{SmtpConfig, SmtpService};
use std::sync::Arc;
let config = SmtpConfig::new("127.0.0.1:2525".parse()?, "mail.example")
.auth_required(false);
let service = SmtpService::new(config);
// or with_handler_factory / AcceptAllSmtpHandlerFactory
let addr = service.start(Arc::clone(&runtime))?;Relay deployments use SimpleRelayService; local MX uses LocalDeliveryService (both wrap SmtpService). See ftp.md and smtp.md for full knob tables.
Examples
| Example | Service pattern |
|---|---|
examples/echo |
Raw ProtocolHandler |
examples/http-hello |
HTTP ServerHandlerFactory + cleartext/ALPN endpoint |
examples/ftp |
FtpService::start |
examples/smtp |
SmtpService / relay |
examples/webdav |
HTTP factory from hopf-webdav |
Limitations
Service::tcp_listenersis transitional — Composition / Runtime APIs own bindings.- Protocol
startsignatures vary (&RuntimevsArc<Runtime>); FTP needsArcfor PASV. - QUIC/H3 services are not
Servicetrait objects today — calllisten_h3directly.