NAME
    EV::Websockets - WebSocket client/server using libwebsockets and EV

SYNOPSIS
        use EV;
        use EV::Websockets;

        my $ctx = EV::Websockets::Context->new(loop => EV::default_loop);

        my $conn = $ctx->connect(
            url        => 'ws://example.com/ws',
            on_connect => sub {
                my ($conn) = @_;
                $conn->send("Hello, WebSocket!");
            },
            on_message => sub {
                my ($conn, $data) = @_;
                print "Got: $data\n";
            },
            on_close   => sub {
                my ($conn, $code, $reason) = @_;
                print "Closed: $code " . ($reason // "") . "\n";
            },
            on_error   => sub {
                my ($conn, $err) = @_;
                print "Error: $err\n";
            },
        );

        EV::run;

DESCRIPTION
    EV::Websockets provides WebSocket client and server functionality using
    the libwebsockets C library integrated with the EV event loop.

    This module uses libwebsockets' foreign loop integration to run within
    an existing EV event loop, making it suitable for applications already
    using EV.

    Important: a context with no active listeners or connections may spin an
    internal idle watcher, preventing other EV watchers (timers, I/O) from
    firing. Always create a listener ("$ctx->listen(...)") or connection
    ("$ctx->connect(...)") before entering "EV::run", or destroy the context
    when not in use.

CLASSES
  EV::Websockets::Context
    Manages the libwebsockets context and event loop integration.

   new(%options)
    Create a new context.

        my $ctx = EV::Websockets::Context->new(
            loop       => EV::default_loop,  # optional, defaults to EV::default_loop
            ssl_cert   => 'client.pem',      # optional, for mTLS client certificates
            ssl_key    => 'client-key.pem',  # required if ssl_cert is set
            ssl_ca     => 'ca.pem',          # optional CA chain
            proxy      => '192.168.1.1',     # optional HTTP proxy host
            proxy_port => 8080,              # optional proxy port (default: 1080)
            ssl_init   => 0,                 # optional, skip OpenSSL global init
        );

    If "proxy" is not specified, the module reads "https_proxy",
    "http_proxy", or "all_proxy" from the environment. Pass "proxy => """ to
    suppress auto-detection.

    "ssl_init" controls whether this module initializes libwebsockets'
    global OpenSSL state. By default it is initialized once and then pinned
    for the lifetime of the process, so contexts may be created and
    destroyed (and new ones created) freely without tearing OpenSSL down.
    Pass "ssl_init => 0" when coexisting with another TLS library (e.g.
    Feersum/picotls) that owns the OpenSSL global state, so this module
    leaves it untouched.

   connect(%options)
    Create a new WebSocket connection.

        my $conn = $ctx->connect(
            url              => 'wss://example.com/ws',
            protocol         => 'chat',              # optional subprotocol
            headers          => { Authorization => 'Bearer token' },
            ssl_verify       => 1,                   # 0 to disable TLS verification
            max_message_size => 1048576,             # optional, 0 = unlimited
            connect_timeout  => 5.0,                 # optional, seconds
            on_connect  => sub { my ($conn, $headers) = @_; ... },
            on_message  => sub { my ($conn, $data, $is_binary) = @_; ... },
            on_close    => sub { my ($conn, $code, $reason) = @_; ... },
            on_error    => sub { my ($conn, $err) = @_; ... },
            on_pong     => sub { my ($conn, $payload) = @_; ... },
            on_drain    => sub { my ($conn) = @_; ... },
        );

    Returns an EV::Websockets::Connection object.

    "on_message" receives complete reassembled messages; fragmented frames
    are buffered internally up to "max_message_size". For backwards
    compatibility a fourth argument $is_final is also passed but is always
    1.

    "connect_timeout" sets a deadline (in seconds) for the WebSocket
    handshake. If the connection is not established within this time,
    "on_error" fires with "connect timeout" and the connection is closed.

    $headers in "on_connect" is a hashref of response headers from the
    server (Set-Cookie, Content-Type, Server, Sec-WebSocket-Protocol, and
    when available Location, WWW-Authenticate).

    "on_drain" fires from the writeable callback when the send queue
    empties. It will not fire if close() has already been queued - once
    closing is in progress the connection short-circuits to teardown without
    emitting drain. If you need to act after the queue empties, do so before
    calling close(), or rely on "on_close" instead.

   listen(%options)
    Create a WebSocket listener. Returns the port number being listened on
    (useful if port 0 was requested).

        my $port = $ctx->listen(
            port             => 0,          # 0 to let OS pick a port
            name             => 'server',   # optional vhost name (default: 'server')
            protocol         => 'chat',     # optional WebSocket subprotocol
            ssl_cert         => 'cert.pem', # optional, enables TLS
            ssl_key          => 'key.pem',  # required if ssl_cert is set
            ssl_ca           => 'ca.pem',   # optional CA chain
            max_message_size => 1048576,    # optional, 0 = unlimited
            headers          => { 'Set-Cookie' => 'session=abc123' }, # response headers
            on_handshake => sub { my ($headers) = @_; return { 'X-Custom' => 'val' } },
            on_connect  => sub { my ($conn, $headers) = @_; ... },
            on_message  => sub { my ($conn, $data, $is_binary) = @_; ... },
            on_close    => sub { my ($conn, $code, $reason) = @_; ... },
            on_error    => sub { my ($conn, $err) = @_; ... },
            on_pong     => sub { my ($conn, $payload) = @_; ... },
            on_drain    => sub { my ($conn) = @_; ... },
        );

    "protocol" sets the WebSocket subprotocol name advertised by the server
    vhost. The vhost name "default" is reserved and will croak if used.

    $headers in "on_connect" is a hashref of client request headers (Path,
    Host, Origin, Cookie, Authorization, Sec-WebSocket-Protocol, User-Agent,
    X-Forwarded-For). "Path" is the request URI (e.g., "/chat").

    "headers" is an optional hashref of headers to inject into the HTTP
    upgrade response (e.g., "Set-Cookie").

    "on_handshake" fires before the 101 response is sent (at
    "LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION"). It receives a hashref of
    request headers (same keys as "on_connect"). Return a hashref to inject
    per-connection response headers into the upgrade response. Return a
    false value ("undef", 0, "") to reject the connection (the client
    receives a 403).

   connections
    Returns a list of Connection objects whose state is "connected" or
    "closing" (i.e. the WebSocket handshake completed and the underlying wsi
    still exists). Conns still in "connecting" and conns already
    "closed"/"destroyed" are omitted.

        my @conns = $ctx->connections;
        $_->send("broadcast!") for @conns;

   adopt(%options)
    Adopt an existing IO handle (socket).

        my $conn = $ctx->adopt(
            fh               => $socket_handle,
            initial_data     => $already_read_bytes, # optional pre-read data
            max_message_size => 1048576,
            on_connect => sub { my ($conn, $headers) = @_; ... },
            on_message => sub { my ($conn, $data, $is_binary) = @_; ... },
            on_close   => sub { my ($conn, $code, $reason) = @_; ... },
            on_error   => sub { my ($conn, $err) = @_; ... },
            on_pong    => sub { my ($conn, $payload) = @_; ... },
            on_drain   => sub { my ($conn) = @_; ... },
        );

    Once adopted, "libwebsockets" takes ownership of the file descriptor.
    The module holds a reference to the Perl handle until the connection is
    destroyed, preventing premature fd closure. $headers in "on_connect" is
    always "undef" for adopted connections.

    Adopted connections cannot select a WebSocket subprotocol: lws uses the
    first protocol registered on the auto-created "server" vhost. There is
    no "protocol" option (unlike "connect" and "listen").

    If you already read data from the socket (e.g., the HTTP upgrade
    request), pass it via "initial_data" so lws can process the handshake.

  EV::Websockets::Connection
    Represents a WebSocket connection.

   send($data)
    Queue a text frame. Croaks if the connection is not open.

   send_binary($data)
    Queue a binary frame. Croaks if the connection is not open.

   send_ping([$payload])
    Queue a Ping frame. $payload is optional; if supplied it is silently
    truncated to 125 bytes per RFC 6455 §5.5. Croaks if the connection is
    not open.

   send_pong([$payload])
    Queue a Pong frame. Same payload rules as "send_ping". Most peers send
    Pong automatically in response to Ping; you only need this to send an
    unsolicited Pong (e.g. as a one-way keepalive).

   send_fragment($data, $is_binary = 0, $is_final = 1)
    Send one fragment of a streaming message. The first call starts a new
    fragmented message (text or binary per $is_binary); subsequent calls
    send continuation frames. Set $is_final true on the last fragment.

        $conn->send_fragment("part1", 0, 0);   # text, not final
        $conn->send_fragment("part2", 0, 0);   # continuation, not final
        $conn->send_fragment("part3", 0, 1);   # continuation, final

    $is_binary is honoured only on the first fragment, which fixes the
    message type; it is ignored on continuation frames (they are always sent
    as continuations), per RFC 6455 §5.4.

    Use this only if you need to interleave outbound writes with other I/O
    while streaming a single message. For ordinary sends, prefer
    "send"/"send_binary".

   send_queue_size
    Returns the number of payload bytes currently queued for sending
    (excludes WebSocket framing overhead). Useful for backpressure
    monitoring; pair with "on_drain" to gate further sends. Returns 0 on a
    closed connection (it does not croak), so it is safe to poll from
    cleanup paths.

   stash
    Returns a hashref for storing arbitrary per-connection metadata. The
    hashref is lazily created on first access and lives until the connection
    is freed.

        $conn->stash->{user_id} = 42;
        my $uid = $conn->stash->{user_id};

   get_protocol
    Returns the negotiated "Sec-WebSocket-Protocol" value, or "undef" if no
    subprotocol was negotiated or the connection is closed.

   peer_address
    Returns the peer's IP address as a printable string (IPv4 dotted-quad or
    IPv6 colon notation, no brackets, no port), or "undef" if unavailable.

   close([$code = 1000], [$reason])
    Initiate a clean WebSocket close. Sends a Close frame with $code
    (default 1000, normal closure) and an optional UTF-8 $reason (truncated
    by lws to fit the frame). Pending sends are drained first, then the
    connection is torn down and "on_close" fires.

    This is a no-op (does not croak) if the connection is already closed,
    closing, or destroyed. It is also a no-op while the connection is still
    in the "connecting" state - calling close() before the handshake
    completes does not cancel the in-flight connect; use "connect_timeout"
    to bound the handshake instead.

   pause_recv
    Stop reading frames from this connection (TCP flow control). New
    incoming frames will back up in the kernel's socket buffer until
    "resume_recv" is called. Silently does nothing on a closed or destroyed
    connection.

   resume_recv
    Resume receiving after "pause_recv". Silently does nothing on a closed
    or destroyed connection.

   is_connected
    Returns true while "state" is "connected".

   is_connecting
    Returns true while "state" is "connecting". Returns false once the
    connection is established, closing, closed, or destroyed.

   state
    Returns the current state as one of:

    "connecting" - TCP/TLS handshake or HTTP upgrade in progress
    "connected" - open and ready to send/receive
    "closing" - close() has been called; pending sends still draining
    "closed" - the underlying wsi is gone but the Perl object is still alive
    "destroyed" - the C struct has been freed (further method calls will
    croak)

DEBUGGING
        EV::Websockets::_set_debug(1);

    Enables verbose debug output from both the module and libwebsockets. In
    tests, gate on $ENV{EV_WS_DEBUG}:

        EV::Websockets::_set_debug(1) if $ENV{EV_WS_DEBUG};

FEERSUM INTEGRATION
    Adopt WebSocket connections from a Feersum PSGI server via "psgix.io":

        use Feersum;
        use EV::Websockets;

        my $ctx = EV::Websockets::Context->new;
        my $feersum = Feersum->endjinn;
        $feersum->set_psgix_io(1);

        $feersum->psgi_request_handler(sub {
            my $env = shift;
            return [400,[],[]] unless ($env->{HTTP_UPGRADE}//'') =~ /websocket/i;

            my $io = $env->{'psgix.io'};

            # Reconstruct HTTP upgrade for lws
            my $path = $env->{REQUEST_URI} // '/';
            my $hdr = "GET $path HTTP/1.1\r\n";
            for (sort keys %$env) {
                next unless /^HTTP_(.+)/;
                (my $h=$1) =~ s/_/-/g;
                $hdr .= "$h: $env->{$_}\r\n";
            }
            $hdr .= "\r\n";

            $ctx->adopt(fh => $io, initial_data => $hdr,
                on_message => sub { $_[0]->send($_[1]) },  # echo
            );
            return;
        });

    See also "eg/feersum_native.pl" and "eg/feersum_psgi.pl" for full
    examples.

BENCHMARKS
    The "bench/" directory contains latency and throughput benchmarks.

        # Echo round-trip latency (native client + native server)
        perl bench/latency.pl

        # Throughput (messages/sec)
        perl bench/throughput.pl

        # Comparison with AnyEvent::WebSocket and Net::WebSocket::EVx
        perl bench/compare.pl

    Typical results on Linux (localhost, 1000 round-trips, 64-byte payload):

        EV::Websockets          ~10us avg,  ~97k msg/s  (C/libwebsockets)
        Net::WebSocket::EVx     ~10us avg,  ~96k msg/s  (C/wslay)
        Mojolicious             ~83us avg,  ~12k msg/s  (Pure Perl)
        Net::Async::WebSocket  ~141us avg,   ~7k msg/s  (Pure Perl)

URL FORMATS
    "connect" accepts "ws://" (plaintext) and "wss://" (TLS) URLs:

        ws://host[:port]/path
        wss://host[:port]/path
        ws://[2001:db8::1]:9001/         # IPv6 (brackets required)

    The default port is 80 for "ws://" and 443 for "wss://". Userinfo
    ("user:pass@") in the URL is not parsed; pass HTTP auth via the
    "headers" option instead.

ERRORS AND EXCEPTIONS
    Methods on "EV::Websockets::Connection" fall into two groups:

    Send / accessors that croak when the connection is gone
        "send", "send_binary", "send_ping", "send_pong", "send_fragment",
        and "stash" croak with "Connection has been destroyed" or
        "Connection is not open" if invoked after the connection has closed
        or been DESTROYed. Wrap in "eval { ... }" if you may race connection
        teardown.

    Lifecycle / control that silently no-op
        "close", "pause_recv", and "resume_recv" return silently when the
        connection is already closed or destroyed. This makes them safe to
        call from cleanup paths without guarding.

    User-supplied callbacks ("on_connect", "on_message", "on_close",
    "on_error", "on_pong", "on_drain", "on_handshake") are invoked under
    "G_EVAL": a die inside a callback is caught, warned, and the connection
    continues. The exception is "on_handshake", which gates the upgrade: a
    die there rejects the connection (the client receives a 403), exactly as
    if the callback had returned a false value. "on_error" is itself
    wrapped, so a die inside "on_error" will not recurse.

SEE ALSO
    EV, Alien::libwebsockets, libwebsockets <https://libwebsockets.org/>,
    Net::WebSocket::EVx, AnyEvent::WebSocket::Client

AUTHOR
    vividsnow

LICENSE
    This library is free software; you can redistribute it and/or modify it
    under the same terms as Perl itself.

