Acceptor#

Join provides acceptor classes for building network servers that accept incoming connections. Acceptors handle the listening socket and produce connected client sockets when new connections arrive.

Acceptors are:

  • protocol‑agnostic — work with any stream protocol
  • event‑driven — integrate with the reactor
  • simple to use — minimal boilerplate code
  • secure — built‑in TLS/SSL support

Two acceptor types are available:

  • BasicStreamAcceptor — for TCP and Unix stream connections
  • BasicTlsAcceptor — for encrypted TLS/SSL connections

BasicStreamAcceptor#

The base acceptor class for connection‑oriented protocols such as TCP and Unix stream.

Creating a TCP server#

#include <join/acceptor.hpp>

using join;

Tcp::Acceptor acceptor;
Tcp::Endpoint endpoint("0.0.0.0", 8080);

if (acceptor.create(endpoint) == -1)
{
    // Handle error
}

The create() method:

  • Creates the listening socket
  • Binds to the specified endpoint
  • Starts listening with maximum backlog (SOMAXCONN)

Accepting connections#

// Accept and get a socket
Tcp::Socket client = acceptor.accept();

if (client.connected())
{
    // Handle client connection
}

Accepted sockets are automatically configured:

  • Set to non‑blocking mode
  • TCP_NODELAY enabled (for TCP sockets)
  • Ready to use immediately

Accepting as a stream#

// Accept and get a stream wrapper
Tcp::Stream stream = acceptor.acceptStream();

if (stream.connected())
{
    stream << "Welcome!\n";
}

BasicTlsAcceptor#

Acceptor for encrypted TLS/SSL connections.

Creating a secure server#

#include <join/acceptor.hpp>

using join;

Tls::Acceptor acceptor;

// Set server certificate and key
if (acceptor.setCertificate("server.pem", "server-key.pem") == -1)
{
    // Handle error
}

Tls::Endpoint endpoint("0.0.0.0", 8443);
acceptor.create(endpoint);

⚠️ Server certificate and private key must be set before accepting connections.

Accepting plaintext connections#

// Accept without TLS encryption (for STARTTLS)
Tls::Socket client = acceptor.accept();

if (client.connected())
{
    // Connection established but not encrypted
    // Client can call startEncryption() later
}

Accepting encrypted connections#

// Accept with immediate TLS handshake
Tls::Socket client = acceptor.acceptEncrypted();

if (client.connected()) {
    // TLS handshake initiated
    // Client should call waitEncrypted() to complete handshake
}

The acceptEncrypted() method:

  • Accepts the TCP connection
  • Initializes TLS handshake state
  • Returns socket ready for handshake completion

Accepting as an encrypted stream#

Tls::Stream stream = acceptor.acceptStreamEncrypted();

if (stream.connected())
{
    if (stream.waitEncrypted(5000))
    {
        // Secure connection established
    }
}

Certificate configuration#

Server certificate and key#

// Certificate and key in same file
acceptor.setCertificate("server.pem");

// Certificate and key in separate files
acceptor.setCertificate("cert.pem", "key.pem");

The acceptor verifies that the private key matches the certificate.

Client certificate verification#

// Enable client certificate verification
acceptor.setVerify(true);

// Set trusted CA certificates
acceptor.setCaCertificate("ca-bundle.pem");

When verification is enabled:

  • Clients must present a valid certificate
  • Certificate must be signed by a trusted CA
  • Handshake fails if verification fails

Verification depth#

// Limit certificate chain depth
acceptor.setVerify(true, 5);  // Maximum 5 levels

TLS configuration#

Cipher suites#

// TLSv1.2 and below
acceptor.setCipher("ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256");

// TLSv1.3
acceptor.setCipher_1_3("TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256");

Elliptic curves (OpenSSL 3.0+)#

acceptor.setCurve("P-521:P-384:P-256");

Default security settings#

The TLS acceptor is configured with secure defaults:

  • SSLv2, SSLv3, TLSv1.0, TLSv1.1 disabled
  • Compression disabled
  • Server cipher preference enabled
  • Session caching enabled
  • Client renegotiation disabled
  • Strong cipher suites by default

Server examples#

Simple echo server#

#include <join/acceptor.hpp>

using join;

Tcp::Acceptor acceptor;
Tcp::Endpoint endpoint("0.0.0.0", 9000);
acceptor.create(endpoint);

while (true)
{
    Tcp::Socket client = acceptor.accept();

    if (client.connected())
    {
        char buffer[1024];
        int n = client.read(buffer, sizeof(buffer));
        if (n > 0)
        {
            client.write(buffer, n);
        }
        client.disconnect();
    }
}

Multi‑threaded server#

#include <join/acceptor.hpp>
#include <thread>

using join;

void handleClient(Tcp::Socket client)
{
    char buffer[1024];
    while (true)
    {
        int n = client.read(buffer, sizeof(buffer));
        if (n <= 0) break;
        client.write(buffer, n);
    }
    client.disconnect();
}

int main()
{
    Tcp::Acceptor acceptor;
    Tcp::Endpoint endpoint("0.0.0.0", 9000);
    acceptor.create(endpoint);

    while (true)
    {
        Tcp::Socket client = acceptor.accept();
        if (client.connected())
        {
            std::thread(handleClient, std::move(client)).detach();
        }
    }
}

HTTPS server#

#include <join/acceptor.hpp>

using join;

Https::Acceptor acceptor;

// Configure TLS
acceptor.setCertificate("server.pem", "server-key.pem");
acceptor.setCipher("ECDHE-RSA-AES256-GCM-SHA384");

Https::Endpoint endpoint("0.0.0.0", 8443);
acceptor.create(endpoint);

while (true)
{
    Https::Socket client = acceptor.acceptEncrypted();

    if (client.connected() && client.waitEncrypted(5000))
    {
        // Handle HTTPS request
        std::string response =
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: text/plain\r\n"
            "Content-Length: 13\r\n\r\n"
            "Hello, HTTPS!";

        client.writeExactly(response.c_str(), response.size());
        client.disconnect();
    }
}

Unix domain socket server#

#include <join/acceptor.hpp>

using join;

UnixStream::Acceptor acceptor;
UnixStream::Endpoint endpoint("/tmp/server.sock");

acceptor.create(endpoint);

while (true)
{
    UnixStream::Socket client = acceptor.accept();

    if (client.connected())
    {
        // Handle local IPC connection
    }
}

Reactor integration#

Acceptors inherit from EventHandler and can be registered with the reactor.

Event‑driven server#

#include <join/acceptor.hpp>
#include <join/reactor.hpp>

using join;

class Server : public Tcp::Acceptor {
protected:
    void onReceive() override
    {
        Tcp::Socket client = accept();
        if (client.connected())
        {
            // Handle new connection
        }
    }
};

int main()
{
    Server server;
    Tcp::Endpoint endpoint("0.0.0.0", 9000);
    server.create(endpoint);

    Reactor::instance()->addHandler(&server);
    Reactor::instance()->run();
}

The reactor will call onReceive() whenever a new connection is ready to accept.


Querying acceptor state#

Check if listening#

if (acceptor.opened())
{
    // Acceptor is listening
}

Get local endpoint#

auto endpoint = acceptor.localEndpoint();
std::cout << "Listening on " << endpoint.ip()
          << ":" << endpoint.port() << std::endl;

This is useful when binding to port 0 (random port assignment):

Tcp::Endpoint endpoint("0.0.0.0", 0);
acceptor.create(endpoint);

auto actual = acceptor.localEndpoint();
std::cout << "Assigned port: " << actual.port() << std::endl;

Protocol information#

int family = acceptor.family();      // AF_INET, AF_INET6, AF_UNIX
int type = acceptor.type();          // SOCK_STREAM
int proto = acceptor.protocol();     // IPPROTO_TCP

Closing acceptors#

acceptor.close();

This:

  • Closes the listening socket
  • Stops accepting new connections
  • Does not affect existing client connections

Error handling#

Acceptor methods return -1 on error and set the global lastError:

if (acceptor.create(endpoint) == -1)
{
    std::cerr << "Failed to create acceptor: "
              << lastError.message() << std::endl;
}

Tcp::Socket client = acceptor.accept();
if (!client.connected())
{
    if (lastError == std::errc::resource_unavailable_try_again)
    {
        // No connection available (non-blocking)
    }
    else
    {
        std::cerr << "Accept failed: "
                  << lastError.message() << std::endl;
    }
}

STARTTLS pattern#

For protocols that start plaintext and upgrade to TLS:

Tls::Acceptor acceptor;
acceptor.setCertificate("server.pem", "server-key.pem");

Tls::Endpoint endpoint("0.0.0.0", 587);  // SMTP submission
acceptor.create(endpoint);

Tls::Socket client = acceptor.accept();

// Exchange plaintext
client.write("220 SMTP Server Ready\r\n", 23);

// Wait for STARTTLS command
// ...

// Upgrade to TLS
client.startEncryption();
if (client.waitEncrypted(5000))
{
    // Now encrypted
}

IPv6 configuration#

By default, IPv6 acceptors accept both IPv4 and IPv6 connections (dual‑stack):

Tcp::Endpoint endpoint("::", 8080);  // Listen on all interfaces
acceptor.create(endpoint);

The IPV6_V6ONLY option is automatically disabled, allowing IPv4 clients to connect to IPv6 sockets.


Best practices#

  • Always check return values for errors
  • Set certificates before calling create() for TLS acceptors
  • Use acceptEncrypted() for immediate TLS handshake
  • Use accept() + startEncryption() for STARTTLS patterns
  • Enable client certificate verification for mutual TLS
  • Configure strong cipher suites for production
  • Use reactor integration for scalable servers
  • Close acceptors explicitly when done
  • Handle SOMAXCONN backlog automatically — no manual tuning needed
  • For Unix sockets, ensure filesystem path is writable

Summary#

FeatureBasicStreamAcceptorBasicTlsAcceptor
TCP connections
Unix domain connections
TLS/SSL encryption
Client certificates
STARTTLS support
Cipher configuration
Reactor integration
IPv4/IPv6 dual‑stack
ProtocolAcceptor TypeUse Case
UnixStreamBasicStreamAcceptorLocal IPC
TcpBasicStreamAcceptorHTTP, custom protocols
TlsBasicTlsAcceptorSecure TCP