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 libwebsockets initializes OpenSSL globals.
    By default, initialization happens once on the first context. Pass
    "ssl_init => 0" when coexisting with another TLS library (e.g.
    Feersum/picotls) to avoid reinitializing shared OpenSSL state.

   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, $is_final) = @_; ... },
            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. $is_final is always 1
    (messages are fully reassembled before delivery).

    "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 when the send queue becomes empty after a write.

   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, $is_final) = @_; ... },
            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 all currently connected Connection objects.

        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, $is_final) = @_; ... },
            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.

    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)
    Send text data over the connection.

   send_binary($data)
    Send binary data over the connection.

   send_ping($data)
    Send a Ping frame. Payload is silently truncated to 125 bytes per RFC
    6455.

   send_pong($data)
    Send a Pong frame. Payload is silently truncated to 125 bytes per RFC
    6455.

   send_fragment($data, $is_binary, $is_final)
    Send a WebSocket message fragment for streaming large messages. The
    first call starts a new fragmented message (text or binary based on
    $is_binary). Subsequent calls send continuation frames. Set $is_final to
    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 defaults to 0, $is_final defaults to 1.

   stash
    Returns a hashref for storing arbitrary per-connection metadata. The
    hashref is lazily created on first access and persists for the lifetime
    of the connection.

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

   send_queue_size
    Returns the number of payload bytes currently queued for sending.

   get_protocol
    Returns the negotiated "Sec-WebSocket-Protocol" value, or "undef".

   peer_address
    Returns the peer IP address as a string, or "undef".

   close($code, $reason)
    Close the connection with the given status code and reason.

   pause_recv
    Stop receiving data from this connection (flow control).

   resume_recv
    Resume receiving data after "pause_recv".

   is_connected
    Returns true if the connection is established and open.

   is_connecting
    Returns true if the connection is in progress.

   state
    Returns the current state as a string: "connecting", "connected",
    "closing", "closed", or "destroyed".

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
    The module supports both "ws://" and "wss://" (TLS) URLs.

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.

