feat: implement WebSocket transport with transport registry system - Add transport_registry.py for centralized transport management - Integrate WebSocket transport with new registry - Add comprehensive test suite for transport registry - Include WebSocket examples and demos - Update transport initialization and swarm integration

This commit is contained in:
acul71
2025-08-09 23:52:55 +02:00
parent a6f85690bf
commit 64107b4648
15 changed files with 2297 additions and 161 deletions

View File

@ -71,6 +71,10 @@ from libp2p.transport.tcp.tcp import (
from libp2p.transport.upgrader import (
TransportUpgrader,
)
from libp2p.transport.transport_registry import (
create_transport_for_multiaddr,
get_supported_transport_protocols,
)
from libp2p.utils.logging import (
setup_logging,
)
@ -185,16 +189,67 @@ def new_swarm(
id_opt = generate_peer_id_from(key_pair)
# Generate X25519 keypair for Noise
noise_key_pair = create_new_x25519_key_pair()
# Default security transports (using Noise as primary)
secure_transports_by_protocol: Mapping[TProtocol, ISecureTransport] = sec_opt or {
NOISE_PROTOCOL_ID: NoiseTransport(
key_pair, noise_privkey=noise_key_pair.private_key
),
TProtocol(secio.ID): secio.Transport(key_pair),
TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(
key_pair, peerstore=peerstore_opt
),
}
# Use given muxer preference if provided, otherwise use global default
if muxer_preference is not None:
temp_pref = muxer_preference.upper()
if temp_pref not in [MUXER_YAMUX, MUXER_MPLEX]:
raise ValueError(
f"Unknown muxer: {muxer_preference}. Use 'YAMUX' or 'MPLEX'."
)
active_preference = temp_pref
else:
active_preference = DEFAULT_MUXER
# Use provided muxer options if given, otherwise create based on preference
if muxer_opt is not None:
muxer_transports_by_protocol = muxer_opt
else:
if active_preference == MUXER_MPLEX:
muxer_transports_by_protocol = create_mplex_muxer_option()
else: # YAMUX is default
muxer_transports_by_protocol = create_yamux_muxer_option()
upgrader = TransportUpgrader(
secure_transports_by_protocol=secure_transports_by_protocol,
muxer_transports_by_protocol=muxer_transports_by_protocol,
)
# Create transport based on listen_addrs or default to TCP
if listen_addrs is None:
transport = TCP()
else:
# Use the first address to determine transport type
addr = listen_addrs[0]
if addr.__contains__("tcp"):
transport = TCP()
elif addr.__contains__("quic"):
raise ValueError("QUIC not yet supported")
else:
raise ValueError(f"Unknown transport in listen_addrs: {listen_addrs}")
transport = create_transport_for_multiaddr(addr, upgrader)
if transport is None:
# Fallback to TCP if no specific transport found
if addr.__contains__("tcp"):
transport = TCP()
elif addr.__contains__("quic"):
raise ValueError("QUIC not yet supported")
else:
supported_protocols = get_supported_transport_protocols()
raise ValueError(
f"Unknown transport in listen_addrs: {listen_addrs}. "
f"Supported protocols: {supported_protocols}"
)
# Generate X25519 keypair for Noise
noise_key_pair = create_new_x25519_key_pair()

View File

@ -242,11 +242,14 @@ class Swarm(Service, INetworkService):
- Call listener listen with the multiaddr
- Map multiaddr to listener
"""
logger.debug(f"Swarm.listen called with multiaddrs: {multiaddrs}")
# We need to wait until `self.listener_nursery` is created.
await self.event_listener_nursery_created.wait()
for maddr in multiaddrs:
logger.debug(f"Swarm.listen processing multiaddr: {maddr}")
if str(maddr) in self.listeners:
logger.debug(f"Swarm.listen: listener already exists for {maddr}")
return True
async def conn_handler(
@ -287,13 +290,17 @@ class Swarm(Service, INetworkService):
try:
# Success
logger.debug(f"Swarm.listen: creating listener for {maddr}")
listener = self.transport.create_listener(conn_handler)
logger.debug(f"Swarm.listen: listener created for {maddr}")
self.listeners[str(maddr)] = listener
# TODO: `listener.listen` is not bounded with nursery. If we want to be
# I/O agnostic, we should change the API.
if self.listener_nursery is None:
raise SwarmException("swarm instance hasn't been run")
logger.debug(f"Swarm.listen: calling listener.listen for {maddr}")
await listener.listen(maddr, self.listener_nursery)
logger.debug(f"Swarm.listen: listener.listen completed for {maddr}")
# Call notifiers since event occurred
await self.notify_listen(maddr)

View File

@ -1,7 +1,44 @@
from .tcp.tcp import TCP
from .websocket.transport import WebsocketTransport
from .transport_registry import (
TransportRegistry,
create_transport_for_multiaddr,
get_transport_registry,
register_transport,
get_supported_transport_protocols,
)
def create_transport(protocol: str, upgrader=None):
"""
Convenience function to create a transport instance.
:param protocol: The transport protocol ("tcp", "ws", or custom)
:param upgrader: Optional transport upgrader (required for WebSocket)
:return: Transport instance
"""
# First check if it's a built-in protocol
if protocol == "ws":
if upgrader is None:
raise ValueError(f"WebSocket transport requires an upgrader")
return WebsocketTransport(upgrader)
elif protocol == "tcp":
return TCP()
else:
# Check if it's a custom registered transport
registry = get_transport_registry()
transport_class = registry.get_transport(protocol)
if transport_class:
return registry.create_transport(protocol, upgrader)
else:
raise ValueError(f"Unsupported transport protocol: {protocol}")
__all__ = [
"TCP",
"WebsocketTransport",
"TransportRegistry",
"create_transport_for_multiaddr",
"create_transport",
"get_transport_registry",
"register_transport",
"get_supported_transport_protocols",
]

View File

@ -0,0 +1,217 @@
"""
Transport registry for dynamic transport selection based on multiaddr protocols.
"""
import logging
from typing import Dict, Type, Optional
from multiaddr import Multiaddr
from libp2p.abc import ITransport
from libp2p.transport.tcp.tcp import TCP
from libp2p.transport.websocket.transport import WebsocketTransport
from libp2p.transport.upgrader import TransportUpgrader
logger = logging.getLogger("libp2p.transport.registry")
def _is_valid_tcp_multiaddr(maddr: Multiaddr) -> bool:
"""
Validate that a multiaddr has a valid TCP structure.
:param maddr: The multiaddr to validate
:return: True if valid TCP structure, False otherwise
"""
try:
# TCP multiaddr should have structure like /ip4/127.0.0.1/tcp/8080
# or /ip6/::1/tcp/8080
protocols = maddr.protocols()
# Must have at least 2 protocols: network (ip4/ip6) + tcp
if len(protocols) < 2:
return False
# First protocol should be a network protocol (ip4, ip6, dns4, dns6)
if protocols[0].name not in ["ip4", "ip6", "dns4", "dns6"]:
return False
# Second protocol should be tcp
if protocols[1].name != "tcp":
return False
# Should not have any protocols after tcp (unless it's a valid continuation like p2p)
# For now, we'll be strict and only allow network + tcp
if len(protocols) > 2:
# Check if the additional protocols are valid continuations
valid_continuations = ["p2p"] # Add more as needed
for i in range(2, len(protocols)):
if protocols[i].name not in valid_continuations:
return False
return True
except Exception:
return False
def _is_valid_websocket_multiaddr(maddr: Multiaddr) -> bool:
"""
Validate that a multiaddr has a valid WebSocket structure.
:param maddr: The multiaddr to validate
:return: True if valid WebSocket structure, False otherwise
"""
try:
# WebSocket multiaddr should have structure like /ip4/127.0.0.1/tcp/8080/ws
# or /ip6/::1/tcp/8080/ws
protocols = maddr.protocols()
# Must have at least 3 protocols: network (ip4/ip6/dns4/dns6) + tcp + ws
if len(protocols) < 3:
return False
# First protocol should be a network protocol (ip4, ip6, dns4, dns6)
if protocols[0].name not in ["ip4", "ip6", "dns4", "dns6"]:
return False
# Second protocol should be tcp
if protocols[1].name != "tcp":
return False
# Last protocol should be ws
if protocols[-1].name != "ws":
return False
# Should not have any protocols between tcp and ws
if len(protocols) > 3:
# Check if the additional protocols are valid continuations
valid_continuations = ["p2p"] # Add more as needed
for i in range(2, len(protocols) - 1):
if protocols[i].name not in valid_continuations:
return False
return True
except Exception:
return False
class TransportRegistry:
"""
Registry for mapping multiaddr protocols to transport implementations.
"""
def __init__(self):
self._transports: Dict[str, Type[ITransport]] = {}
self._register_default_transports()
def _register_default_transports(self) -> None:
"""Register the default transport implementations."""
# Register TCP transport for /tcp protocol
self.register_transport("tcp", TCP)
# Register WebSocket transport for /ws protocol
self.register_transport("ws", WebsocketTransport)
def register_transport(self, protocol: str, transport_class: Type[ITransport]) -> None:
"""
Register a transport class for a specific protocol.
:param protocol: The protocol identifier (e.g., "tcp", "ws")
:param transport_class: The transport class to register
"""
self._transports[protocol] = transport_class
logger.debug(f"Registered transport {transport_class.__name__} for protocol {protocol}")
def get_transport(self, protocol: str) -> Optional[Type[ITransport]]:
"""
Get the transport class for a specific protocol.
:param protocol: The protocol identifier
:return: The transport class or None if not found
"""
return self._transports.get(protocol)
def get_supported_protocols(self) -> list[str]:
"""Get list of supported transport protocols."""
return list(self._transports.keys())
def create_transport(self, protocol: str, upgrader: Optional[TransportUpgrader] = None, **kwargs) -> Optional[ITransport]:
"""
Create a transport instance for a specific protocol.
:param protocol: The protocol identifier
:param upgrader: The transport upgrader instance (required for WebSocket)
:param kwargs: Additional arguments for transport construction
:return: Transport instance or None if protocol not supported or creation fails
"""
transport_class = self.get_transport(protocol)
if transport_class is None:
return None
try:
if protocol == "ws":
# WebSocket transport requires upgrader
if upgrader is None:
logger.warning(f"WebSocket transport '{protocol}' requires upgrader")
return None
return transport_class(upgrader)
else:
# TCP transport doesn't require upgrader
return transport_class()
except Exception as e:
logger.error(f"Failed to create transport for protocol {protocol}: {e}")
return None
# Global transport registry instance
_global_registry = TransportRegistry()
def get_transport_registry() -> TransportRegistry:
"""Get the global transport registry instance."""
return _global_registry
def register_transport(protocol: str, transport_class: Type[ITransport]) -> None:
"""Register a transport class in the global registry."""
_global_registry.register_transport(protocol, transport_class)
def create_transport_for_multiaddr(maddr: Multiaddr, upgrader: TransportUpgrader) -> Optional[ITransport]:
"""
Create the appropriate transport for a given multiaddr.
:param maddr: The multiaddr to create transport for
:param upgrader: The transport upgrader instance
:return: Transport instance or None if no suitable transport found
"""
try:
# Get all protocols in the multiaddr
protocols = [proto.name for proto in maddr.protocols()]
# Check for supported transport protocols in order of preference
# We need to validate that the multiaddr structure is valid for our transports
if "ws" in protocols:
# For WebSocket, we need a valid structure like /ip4/127.0.0.1/tcp/8080/ws
# Check if the multiaddr has proper WebSocket structure
if _is_valid_websocket_multiaddr(maddr):
return _global_registry.create_transport("ws", upgrader)
elif "tcp" in protocols:
# For TCP, we need a valid structure like /ip4/127.0.0.1/tcp/8080
# Check if the multiaddr has proper TCP structure
if _is_valid_tcp_multiaddr(maddr):
return _global_registry.create_transport("tcp", upgrader)
# If no supported transport protocol found or structure is invalid, return None
logger.warning(f"No supported transport protocol found or invalid structure in multiaddr: {maddr}")
return None
except Exception as e:
# Handle any errors gracefully (e.g., invalid multiaddr)
logger.warning(f"Error processing multiaddr {maddr}: {e}")
return None
def get_supported_transport_protocols() -> list[str]:
"""Get list of supported transport protocols from the global registry."""
return _global_registry.get_supported_protocols()

View File

@ -1,4 +1,5 @@
from trio.abc import Stream
import trio
from libp2p.io.abc import ReadWriteCloser
from libp2p.io.exceptions import IOException
@ -6,19 +7,20 @@ from libp2p.io.exceptions import IOException
class P2PWebSocketConnection(ReadWriteCloser):
"""
Wraps a raw trio.abc.Stream from an established websocket connection.
This bypasses message-framing issues and provides the raw stream
Wraps a WebSocketConnection to provide the raw stream interface
that libp2p protocols expect.
"""
_stream: Stream
def __init__(self, stream: Stream):
self._stream = stream
def __init__(self, ws_connection, ws_context=None):
self._ws_connection = ws_connection
self._ws_context = ws_context
self._read_buffer = b""
self._read_lock = trio.Lock()
async def write(self, data: bytes) -> None:
try:
await self._stream.send_all(data)
# Send as a binary WebSocket message
await self._ws_connection.send_message(data)
except Exception as e:
raise IOException from e
@ -26,24 +28,68 @@ class P2PWebSocketConnection(ReadWriteCloser):
"""
Read up to n bytes (if n is given), else read up to 64KiB.
"""
try:
if n is None:
# read a reasonable chunk
return await self._stream.receive_some(2**16)
return await self._stream.receive_some(n)
except Exception as e:
raise IOException from e
async with self._read_lock:
try:
# If we have buffered data, return it
if self._read_buffer:
if n is None:
result = self._read_buffer
self._read_buffer = b""
return result
else:
if len(self._read_buffer) >= n:
result = self._read_buffer[:n]
self._read_buffer = self._read_buffer[n:]
return result
else:
result = self._read_buffer
self._read_buffer = b""
return result
# Get the next WebSocket message
message = await self._ws_connection.get_message()
if isinstance(message, str):
message = message.encode('utf-8')
# Add to buffer
self._read_buffer = message
# Return requested amount
if n is None:
result = self._read_buffer
self._read_buffer = b""
return result
else:
if len(self._read_buffer) >= n:
result = self._read_buffer[:n]
self._read_buffer = self._read_buffer[n:]
return result
else:
result = self._read_buffer
self._read_buffer = b""
return result
except Exception as e:
raise IOException from e
async def close(self) -> None:
await self._stream.aclose()
# Close the WebSocket connection
await self._ws_connection.aclose()
# Exit the context manager if we have one
if self._ws_context is not None:
await self._ws_context.__aexit__(None, None, None)
def get_remote_address(self) -> tuple[str, int] | None:
sock = getattr(self._stream, "socket", None)
if sock:
try:
addr = sock.getpeername()
if isinstance(addr, tuple) and len(addr) >= 2:
return str(addr[0]), int(addr[1])
except OSError:
return None
# Try to get remote address from the WebSocket connection
try:
remote = self._ws_connection.remote
if hasattr(remote, 'address') and hasattr(remote, 'port'):
return str(remote.address), int(remote.port)
elif isinstance(remote, str):
# Parse address:port format
if ':' in remote:
host, port = remote.rsplit(':', 1)
return host, int(port)
except Exception:
pass
return None

View File

@ -1,6 +1,6 @@
import logging
import socket
from typing import Any
from typing import Any, Callable
from multiaddr import Multiaddr
import trio
@ -10,6 +10,7 @@ from trio_websocket import serve_websocket
from libp2p.abc import IListener
from libp2p.custom_types import THandler
from libp2p.network.connection.raw_connection import RawConnection
from libp2p.transport.upgrader import TransportUpgrader
from .connection import P2PWebSocketConnection
@ -21,11 +22,15 @@ class WebsocketListener(IListener):
Listen on /ip4/.../tcp/.../ws addresses, handshake WS, wrap into RawConnection.
"""
def __init__(self, handler: THandler) -> None:
def __init__(self, handler: THandler, upgrader: TransportUpgrader) -> None:
self._handler = handler
self._upgrader = upgrader
self._server = None
self._shutdown_event = trio.Event()
self._nursery = None
async def listen(self, maddr: Multiaddr, nursery: trio.Nursery) -> bool:
logger.debug(f"WebsocketListener.listen called with {maddr}")
addr_str = str(maddr)
if addr_str.endswith("/wss"):
raise NotImplementedError("/wss (TLS) not yet supported")
@ -42,43 +47,126 @@ class WebsocketListener(IListener):
if port_str is None:
raise ValueError(f"No TCP port found in multiaddr: {maddr}")
port = int(port_str)
logger.debug(f"WebsocketListener: host={host}, port={port}")
async def serve(
task_status: TaskStatus[Any] = trio.TASK_STATUS_IGNORED,
async def serve_websocket_tcp(
handler: Callable,
port: int,
host: str,
task_status: trio.TaskStatus[list],
) -> None:
# positional ssl_context=None
self._server = await serve_websocket(
self._handle_connection, host, port, None
)
task_status.started()
await self._server.wait_closed()
"""Start TCP server and handle WebSocket connections manually"""
logger.debug("serve_websocket_tcp %s %s", host, port)
async def websocket_handler(request):
"""Handle WebSocket requests"""
logger.debug("WebSocket request received")
try:
# Accept the WebSocket connection
ws_connection = await request.accept()
logger.debug("WebSocket handshake successful")
# Create the WebSocket connection wrapper
conn = P2PWebSocketConnection(ws_connection)
# Call the handler function that was passed to create_listener
# This handler will handle the security and muxing upgrades
logger.debug("Calling connection handler")
await self._handler(conn)
# Don't keep the connection alive indefinitely
# Let the handler manage the connection lifecycle
logger.debug("Handler completed, connection will be managed by handler")
except Exception as e:
logger.debug(f"WebSocket connection error: {e}")
logger.debug(f"Error type: {type(e)}")
import traceback
logger.debug(f"Traceback: {traceback.format_exc()}")
# Reject the connection
try:
await request.reject(400)
except:
pass
# Use trio_websocket.serve_websocket for proper WebSocket handling
from trio_websocket import serve_websocket
await serve_websocket(websocket_handler, host, port, None, task_status=task_status)
await nursery.start(serve)
# Store the nursery for shutdown
self._nursery = nursery
# Start the server using nursery.start() like TCP does
logger.debug("Calling nursery.start()...")
started_listeners = await nursery.start(
serve_websocket_tcp,
None, # No handler needed since it's defined inside serve_websocket_tcp
port,
host,
)
logger.debug(f"nursery.start() returned: {started_listeners}")
if started_listeners is None:
logger.error(f"Failed to start WebSocket listener for {maddr}")
return False
# Store the listeners for get_addrs() and close() - these are real SocketListener objects
self._listeners = started_listeners
logger.debug(f"WebsocketListener.listen returning True with WebSocketServer object")
return True
async def _handle_connection(self, websocket: Any) -> None:
try:
# use raw transport_stream
conn = P2PWebSocketConnection(websocket.stream)
raw = RawConnection(conn, initiator=False)
await self._handler(raw)
except Exception as e:
logger.debug("WebSocket connection error: %s", e)
def get_addrs(self) -> tuple[Multiaddr, ...]:
if not self._server or not self._server.sockets:
if not hasattr(self, '_listeners') or not self._listeners:
logger.debug("No listeners available for get_addrs()")
return ()
addrs = []
for sock in self._server.sockets:
host, port = sock.getsockname()[:2]
if sock.family == socket.AF_INET6:
addr = Multiaddr(f"/ip6/{host}/tcp/{port}/ws")
else:
addr = Multiaddr(f"/ip4/{host}/tcp/{port}/ws")
addrs.append(addr)
return tuple(addrs)
# Handle WebSocketServer objects
if hasattr(self._listeners, 'port'):
# This is a WebSocketServer object
port = self._listeners.port
# Create a multiaddr from the port
return (Multiaddr(f"/ip4/127.0.0.1/tcp/{port}/ws"),)
else:
# This is a list of listeners (like TCP)
listeners = self._listeners
# Get addresses from listeners like TCP does
return tuple(
_multiaddr_from_socket(listener.socket) for listener in listeners
)
async def close(self) -> None:
if self._server:
self._server.close()
await self._server.wait_closed()
"""Close the WebSocket listener and stop accepting new connections"""
logger.debug("WebsocketListener.close called")
if hasattr(self, '_listeners') and self._listeners:
# Signal shutdown
self._shutdown_event.set()
# Close the WebSocket server
if hasattr(self._listeners, 'aclose'):
# This is a WebSocketServer object
logger.debug("Closing WebSocket server")
await self._listeners.aclose()
logger.debug("WebSocket server closed")
elif isinstance(self._listeners, (list, tuple)):
# This is a list of listeners (like TCP)
logger.debug("Closing TCP listeners")
for listener in self._listeners:
listener.close()
logger.debug("TCP listeners closed")
else:
# Unknown type, try to close it directly
logger.debug("Closing unknown listener type")
if hasattr(self._listeners, 'close'):
self._listeners.close()
logger.debug("Unknown listener closed")
# Clear the listeners reference
self._listeners = None
logger.debug("WebsocketListener.close completed")
def _multiaddr_from_socket(socket: trio.socket.SocketType) -> Multiaddr:
"""Convert socket to multiaddr"""
ip, port = socket.getsockname()
return Multiaddr(f"/ip4/{ip}/tcp/{port}/ws")

View File

@ -1,3 +1,4 @@
import logging
from multiaddr import Multiaddr
from trio_websocket import open_websocket_url
@ -5,54 +6,51 @@ from libp2p.abc import IListener, ITransport
from libp2p.custom_types import THandler
from libp2p.network.connection.raw_connection import RawConnection
from libp2p.transport.exceptions import OpenConnectionError
from libp2p.transport.upgrader import TransportUpgrader
from .connection import P2PWebSocketConnection
from .listener import WebsocketListener
logger = logging.getLogger("libp2p.transport.websocket")
class WebsocketTransport(ITransport):
"""
Libp2p WebSocket transport: dial and listen on /ip4/.../tcp/.../ws
"""
def __init__(self, upgrader: TransportUpgrader):
self._upgrader = upgrader
async def dial(self, maddr: Multiaddr) -> RawConnection:
# Handle addresses with /p2p/ PeerID suffix by truncating them at /ws
addr_text = str(maddr)
try:
ws_part_index = addr_text.index("/ws")
# Create a new Multiaddr containing only the transport part
transport_maddr = Multiaddr(addr_text[: ws_part_index + 3])
except ValueError:
raise ValueError(
f"WebsocketTransport requires a /ws protocol, not found in {maddr}"
) from None
# Check for /wss, which is not supported yet
if str(transport_maddr).endswith("/wss"):
raise NotImplementedError("/wss (TLS) not yet supported")
"""Dial a WebSocket connection to the given multiaddr."""
logger.debug(f"WebsocketTransport.dial called with {maddr}")
# Extract host and port from multiaddr
host = (
transport_maddr.value_for_protocol("ip4")
or transport_maddr.value_for_protocol("ip6")
or transport_maddr.value_for_protocol("dns")
or transport_maddr.value_for_protocol("dns4")
or transport_maddr.value_for_protocol("dns6")
maddr.value_for_protocol("ip4")
or maddr.value_for_protocol("ip6")
or maddr.value_for_protocol("dns")
or maddr.value_for_protocol("dns4")
or maddr.value_for_protocol("dns6")
)
if host is None:
raise ValueError(f"No host protocol found in {transport_maddr}")
port_str = transport_maddr.value_for_protocol("tcp")
port_str = maddr.value_for_protocol("tcp")
if port_str is None:
raise ValueError(f"No TCP port found in multiaddr: {transport_maddr}")
raise ValueError(f"No TCP port found in multiaddr: {maddr}")
port = int(port_str)
host_str = f"[{host}]" if ":" in host else host
uri = f"ws://{host_str}:{port}"
# Build WebSocket URL
ws_url = f"ws://{host}:{port}/"
logger.debug(f"WebsocketTransport.dial connecting to {ws_url}")
try:
async with open_websocket_url(uri, ssl_context=None) as ws:
conn = P2PWebSocketConnection(ws.stream) # type: ignore[attr-defined]
return RawConnection(conn, initiator=True)
from trio_websocket import open_websocket_url
# Use the context manager but don't exit it immediately
# The connection will be closed when the RawConnection is closed
ws_context = open_websocket_url(ws_url)
ws = await ws_context.__aenter__()
conn = P2PWebSocketConnection(ws, ws_context) # type: ignore[attr-defined]
return RawConnection(conn, initiator=True)
except Exception as e:
raise OpenConnectionError(f"Failed to dial WebSocket {maddr}: {e}") from e
@ -60,4 +58,5 @@ class WebsocketTransport(ITransport):
"""
The type checker is incorrectly reporting this as an inconsistent override.
"""
return WebsocketListener(handler)
logger.debug("WebsocketTransport.create_listener called")
return WebsocketListener(handler, self._upgrader)