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) = @_; ... }, 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. 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 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. 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. "on_error" is itself wrapped, so a die inside "on_error" will not recurse. SEE ALSO EV, Alien::libwebsockets, libwebsockets , 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.