Trio WebSocket
This library is a WebSocket implementation for the Trio framework that strives for safety, correctness, and ergonomics. It is based on wsproto, which is a Sans-IO state machine that implements most aspects of the WebSocket protocol, including framing, codecs, and events. The respository is hosted on GitHub. This library passes the Autobahn Test Suite.
Getting Started
Installation
This library supports Python ≥3.5. The easiest installation method is to use PyPI.
$ pip3 install trio-websocket
You can also install from source. Visit the project’s GitHub page, where you can clone the repository or download a Zip file. Change into the project directory and run the following command.
$ pip3 install .
If you want to contribute to development of the library, also see Developer Installation.
Client Example
This example briefly demonstrates how to create a WebSocket client.
1import trio
2from trio_websocket import open_websocket_url
3
4
5async def main():
6 try:
7 async with open_websocket_url('wss://localhost/foo') as ws:
8 await ws.send_message('hello world!')
9 message = await ws.get_message()
10 logging.info('Received message: %s', message)
11 except OSError as ose:
12 logging.error('Connection attempt failed: %s', ose)
13
14trio.run(main)
The function open_websocket_url()
is a context manager that automatically
connects and performs the WebSocket handshake before entering the block. This
ensures that the connection is usable before ws.send_message(…)
is called.
The context manager yields a WebSocketConnection
instance that is used
to send and receive messages. The context manager also closes the connection
before exiting the block.
For more details and examples, see Clients.
Server Example
This example briefly demonstrates how to create a WebSocket server. This server is an echo server, i.e. it responds to each incoming message by sending back an identical message.
1import trio
2from trio_websocket import serve_websocket, ConnectionClosed
3
4async def echo_server(request):
5 ws = await request.accept()
6 while True:
7 try:
8 message = await ws.get_message()
9 await ws.send_message(message)
10 except ConnectionClosed:
11 break
12
13async def main():
14 await serve_websocket(echo_server, '127.0.0.1', 8000, ssl_context=None)
15
16trio.run(main)
The function serve_websocket()
requires a function that can handle each
incoming connection. This handler function receives a
WebSocketRequest
object that the server can use to inspect the client’s
handshake. Next, the server accepts the request in order to complete the
handshake and receive a WebSocketConnection
instance that can be used
to send and receive messages.
For more details and examples, see Servers.
Clients
Client Tutorial
This page goes into the details of creating a WebSocket client. Let’s start by revisiting the Client Example.
1import trio
2from trio_websocket import open_websocket_url
3
4
5async def main():
6 try:
7 async with open_websocket_url('wss://localhost/foo') as ws:
8 await ws.send_message('hello world!')
9 message = await ws.get_message()
10 logging.info('Received message: %s', message)
11 except OSError as ose:
12 logging.error('Connection attempt failed: %s', ose)
13
14trio.run(main)
Note
A more complete example is included in the repository.
As explained in the tutorial, open_websocket_url(…)
is a context manager
that ensures the connection is properly opened and ready before entering the
block. It also ensures that the connection is closed before exiting the block.
This library contains two such context managers for creating client connections:
one to connect by host and one to connect by URL.
- async with trio_websocket.open_websocket(host, port, resource, *, use_ssl, subprotocols=None, extra_headers=None, message_queue_size=1, max_message_size=1048576, connect_timeout=60, disconnect_timeout=60) as ws
Open a WebSocket client connection to a host.
This async context manager connects when entering the context manager and disconnects when exiting. It yields a
WebSocketConnection
instance.- Parameters:
host (str) – The host to connect to.
port (int) – The port to connect to.
resource (str) – The resource, i.e. URL path.
use_ssl (Union[bool, ssl.SSLContext]) – If this is an SSL context, then use that context. If this is
True
then use default SSL context. If this isFalse
then disable SSL.subprotocols – An iterable of strings representing preferred subprotocols.
extra_headers (list[tuple[bytes,bytes]]) – A list of 2-tuples containing HTTP header key/value pairs to send with the connection request. Note that headers used by the WebSocket protocol (e.g.
Sec-WebSocket-Accept
) will be overwritten.message_queue_size (int) – The maximum number of messages that will be buffered in the library’s internal message queue.
max_message_size (int) – The maximum message size as measured by
len()
. If a message is received that is larger than this size, then the connection is closed with code 1009 (Message Too Big).connect_timeout (float) – The number of seconds to wait for the connection before timing out.
disconnect_timeout (float) – The number of seconds to wait when closing the connection before timing out.
- Raises:
HandshakeError – for any networking error, client-side timeout (
ConnectionTimeout
,DisconnectionTimeout
), or server rejection (ConnectionRejected
) during handshakes.
- async with trio_websocket.open_websocket_url(url, ssl_context=None, *, subprotocols=None, extra_headers=None, message_queue_size=1, max_message_size=1048576, connect_timeout=60, disconnect_timeout=60) as ws
Open a WebSocket client connection to a URL.
This async context manager connects when entering the context manager and disconnects when exiting. It yields a
WebSocketConnection
instance.- Parameters:
url (str) – A WebSocket URL, i.e. ws: or wss: URL scheme.
ssl_context (ssl.SSLContext or None) – Optional SSL context used for
wss:
URLs. A default SSL context is used forwss:
if this argument isNone
.subprotocols – An iterable of strings representing preferred subprotocols.
extra_headers (list[tuple[bytes,bytes]]) – A list of 2-tuples containing HTTP header key/value pairs to send with the connection request. Note that headers used by the WebSocket protocol (e.g.
Sec-WebSocket-Accept
) will be overwritten.message_queue_size (int) – The maximum number of messages that will be buffered in the library’s internal message queue.
max_message_size (int) – The maximum message size as measured by
len()
. If a message is received that is larger than this size, then the connection is closed with code 1009 (Message Too Big).connect_timeout (float) – The number of seconds to wait for the connection before timing out.
disconnect_timeout (float) – The number of seconds to wait when closing the connection before timing out.
- Raises:
HandshakeError – for any networking error, client-side timeout (
ConnectionTimeout
,DisconnectionTimeout
), or server rejection (ConnectionRejected
) during handshakes.
Custom Nursery
The two context managers above create an internal nursery to run background tasks. If you wish to specify your own nursery instead, you should use the the following convenience functions instead.
- await trio_websocket.connect_websocket(nursery, host, port, resource, *, use_ssl, subprotocols=None, extra_headers=None, message_queue_size=1, max_message_size=1048576)
Return an open WebSocket client connection to a host.
This function is used to specify a custom nursery to run connection background tasks in. The caller is responsible for closing the connection.
If you don’t need a custom nursery, you should probably use
open_websocket()
instead.- Parameters:
nursery – A Trio nursery to run background tasks in.
host (str) – The host to connect to.
port (int) – The port to connect to.
resource (str) – The resource, i.e. URL path.
use_ssl (Union[bool, ssl.SSLContext]) – If this is an SSL context, then use that context. If this is
True
then use default SSL context. If this isFalse
then disable SSL.subprotocols – An iterable of strings representing preferred subprotocols.
extra_headers (list[tuple[bytes,bytes]]) – A list of 2-tuples containing HTTP header key/value pairs to send with the connection request. Note that headers used by the WebSocket protocol (e.g.
Sec-WebSocket-Accept
) will be overwritten.message_queue_size (int) – The maximum number of messages that will be buffered in the library’s internal message queue.
max_message_size (int) – The maximum message size as measured by
len()
. If a message is received that is larger than this size, then the connection is closed with code 1009 (Message Too Big).
- Return type:
- await trio_websocket.connect_websocket_url(nursery, url, ssl_context=None, *, subprotocols=None, extra_headers=None, message_queue_size=1, max_message_size=1048576)
Return an open WebSocket client connection to a URL.
This function is used to specify a custom nursery to run connection background tasks in. The caller is responsible for closing the connection.
If you don’t need a custom nursery, you should probably use
open_websocket_url()
instead.- Parameters:
nursery – A nursery to run background tasks in.
url (str) – A WebSocket URL.
ssl_context (ssl.SSLContext or None) – Optional SSL context used for
wss:
URLs.subprotocols – An iterable of strings representing preferred subprotocols.
extra_headers (list[tuple[bytes,bytes]]) – A list of 2-tuples containing HTTP header key/value pairs to send with the connection request. Note that headers used by the WebSocket protocol (e.g.
Sec-WebSocket-Accept
) will be overwritten.message_queue_size (int) – The maximum number of messages that will be buffered in the library’s internal message queue.
max_message_size (int) – The maximum message size as measured by
len()
. If a message is received that is larger than this size, then the connection is closed with code 1009 (Message Too Big).
- Return type:
Custom Stream
The WebSocket protocol is defined as an application layer protocol that runs on top of TCP, and the convenience functions described above automatically create those TCP connections. In more obscure use cases, you might want to run the WebSocket protocol on top of some other type of transport protocol. The library includes a convenience function that allows you to wrap any arbitrary Trio stream with a client WebSocket.
- await trio_websocket.wrap_client_stream(nursery, stream, host, resource, *, subprotocols=None, extra_headers=None, message_queue_size=1, max_message_size=1048576)
Wrap an arbitrary stream in a WebSocket connection.
This is a low-level function only needed in rare cases. In most cases, you should use
open_websocket()
oropen_websocket_url()
.- Parameters:
nursery – A Trio nursery to run background tasks in.
stream (trio.abc.Stream) – A Trio stream to be wrapped.
host (str) – A host string that will be sent in the
Host:
header.resource (str) – A resource string, i.e. the path component to be accessed on the server.
subprotocols – An iterable of strings representing preferred subprotocols.
extra_headers (list[tuple[bytes,bytes]]) – A list of 2-tuples containing HTTP header key/value pairs to send with the connection request. Note that headers used by the WebSocket protocol (e.g.
Sec-WebSocket-Accept
) will be overwritten.message_queue_size (int) – The maximum number of messages that will be buffered in the library’s internal message queue.
max_message_size (int) – The maximum message size as measured by
len()
. If a message is received that is larger than this size, then the connection is closed with code 1009 (Message Too Big).
- Return type:
Servers
Server Tutorial
This page goes into the details of creating a WebSocket server. Let’s start by revisiting the Server Example.
1import trio
2from trio_websocket import serve_websocket, ConnectionClosed
3
4async def echo_server(request):
5 ws = await request.accept()
6 while True:
7 try:
8 message = await ws.get_message()
9 await ws.send_message(message)
10 except ConnectionClosed:
11 break
12
13async def main():
14 await serve_websocket(echo_server, '127.0.0.1', 8000, ssl_context=None)
15
16trio.run(main)
Note
A more complete example is included in the repository.
As explained in the tutorial, a WebSocket server needs a handler function and a
host/port to bind to. The handler function receives a
WebSocketRequest
object, and it calls the request’s
accept()
method to finish the handshake and obtain a
WebSocketConnection
object. When the handler function exits, the
connection is automatically closed. If the handler function raises an
exception, the server will silently close the connection and cancel the
tasks belonging to it.
- await trio_websocket.serve_websocket(handler, host, port, ssl_context, *, handler_nursery=None, message_queue_size=1, max_message_size=1048576, connect_timeout=60, disconnect_timeout=60, task_status=TASK_STATUS_IGNORED)
Serve a WebSocket over TCP.
This function supports the Trio nursery start protocol:
server = await nursery.start(serve_websocket, …)
. It will block until the server is accepting connections and then return aWebSocketServer
object.Note that if
host
isNone
andport
is zero, then you may get multiple listeners that have different port numbers!- Parameters:
handler – An async function that is invoked with a request for each new connection.
host (str, bytes, or None) – The host interface to bind. This can be an address of an interface, a name that resolves to an interface address (e.g.
localhost
), or a wildcard address like0.0.0.0
for IPv4 or::
for IPv6. IfNone
, then all local interfaces are bound.port (int) – The port to bind to.
ssl_context (ssl.SSLContext or None) – The SSL context to use for encrypted connections, or
None
for unencrypted connection.handler_nursery – An optional nursery to spawn handlers and background tasks in. If not specified, a new nursery will be created internally.
message_queue_size (int) – The maximum number of messages that will be buffered in the library’s internal message queue.
max_message_size (int) – The maximum message size as measured by
len()
. If a message is received that is larger than this size, then the connection is closed with code 1009 (Message Too Big).connect_timeout (float) – The number of seconds to wait for a client to finish connection handshake before timing out.
disconnect_timeout (float) – The number of seconds to wait for a client to finish the closing handshake before timing out.
task_status – Part of Trio nursery start protocol.
- Returns:
This function runs until cancelled.
Custom Stream
The WebSocket protocol is defined as an application layer protocol that runs on top of TCP, and the convenience functions described above automatically create those TCP connections. In more obscure use cases, you might want to run the WebSocket protocol on top of some other type of transport protocol. The library includes a convenience function that allows you to wrap any arbitrary Trio stream with a server WebSocket.
- await trio_websocket.wrap_server_stream(nursery, stream, message_queue_size=1, max_message_size=1048576)
Wrap an arbitrary stream in a server-side WebSocket.
This is a low-level function only needed in rare cases. In most cases, you should use
serve_websocket()
.- Parameters:
nursery – A nursery to run background tasks in.
stream (trio.abc.Stream) – A stream to be wrapped.
message_queue_size (int) – The maximum number of messages that will be buffered in the library’s internal message queue.
max_message_size (int) – The maximum message size as measured by
len()
. If a message is received that is larger than this size, then the connection is closed with code 1009 (Message Too Big).
- Return type:
Message Queues
When a connection is open, it runs a background task that reads network data and
automatically handles certain types of events for you. For example, if the
background task receives a ping event, then it will automatically send back a
pong event. When the background task receives a message, it places that message
into an internal queue. When you call get_message()
, it returns the first
item from this queue.
If this internal message queue does not have any size limits, then a remote endpoint could rapidly send large messages and use up all of the memory on the local machine! In almost all situations, the message queue needs to have size limits, both in terms of the number of items and the size per message. These limits create an upper bound for the amount of memory that can be used by a single WebSocket connection. For example, if the queue size is 10 and the maximum message size is 1 megabyte, then the connection will use at most 10 megabytes of memory.
When the message queue is full, the background task pauses and waits for the
user to remove a message, i.e. call get_message()
. When the background task
is paused, it stops processing background events like replying to ping events.
If a message is received that is larger than the maximum message size, then the
connection is automatically closed with code 1009 and the message is discarded.
The library APIs each take arguments to configure the mesage buffer:
message_queue_size
and max_message_size
. By default the queue size is
one and the maximum message size is 1 MiB. If you set queue size to zero, then
the background task will block every time it receives a message until somebody
calls get_message()
. For an unbounded queue—which is strongly
discouraged—set the queue size to math.inf
. Likewise, the maximum message
size may also be disabled by setting it to math.inf
.
Timeouts
Networking code is inherently complex due to the unpredictable nature of network failures and the possibility of a remote peer that is coded incorrectly—or even maliciously! Therefore, your code needs to deal with unexpected circumstances. One common failure mode that you should guard against is a slow or unresponsive peer.
This page describes the timeout behavior in trio-websocket
and shows various
examples for implementing timeouts in your own code. Before reading this, you
might find it helpful to read “Timeouts and cancellation for humans”, an article
written by Trio’s author that describes an overall philosophy regarding
timeouts. The short version is that Trio discourages libraries from using
internal timeouts. Instead, it encourages the caller to enforce timeouts, which
makes timeout code easier to compose and reason about.
On the other hand, this library is intended to be safe to use, and omitting timeouts could be a dangerous flaw. Therefore, this library takes a balanced approach to timeouts, where high-level APIs have internal timeouts, but you may disable them or use lower-level APIs if you want more control over the behavior.
Message Timeouts
As a motivating example, let’s write a client that sends one message and then
expects to receive one message. To guard against a misbehaving server or
network, we want to place a 15 second timeout on this combined send/receive
operation. In other libraries, you might find that the APIs have timeout
arguments, but that style of timeout is very tedious when composing multiple
operations. In Trio, we have helpful abstractions like cancel scopes, allowing
us to implement our example like this:
async with open_websocket_url('ws://my.example/') as ws:
with trio.fail_after(15):
await ws.send_message('test')
msg = await ws.get_message()
print('Received message: {}'.format(msg))
The 15 second timeout covers the cumulative time to send one message and to wait
for one response. It raises TooSlowError
if the runtime exceeds 15 seconds.
Connection Timeouts
The example in the previous section ignores one obvious problem: what if connecting to the server or closing the connection takes a long time? How do we apply a timeout to those operations? One option is to put the entire connection inside a cancel scope:
with trio.fail_after(15):
async with open_websocket_url('ws://my.example/') as ws:
await ws.send_message('test')
msg = await ws.get_message()
print('Received message: {}'.format(msg))
The approach suffices if we want to compose all four operations into one timeout: connect, send message, get message, and disconnect. But this approach will not work if want to separate the timeouts for connecting/disconnecting from the timeouts for sending and receiving. Let’s write a new client that sends messages periodically, waiting up to 15 seconds for a response to each message before sending the next message.
async with open_websocket_url('ws://my.example/') as ws:
for _ in range(10):
await trio.sleep(30)
with trio.fail_after(15):
await ws.send_message('test')
msg = await ws.get_message()
print('Received message: {}'.format(msg))
In this scenario, the for
loop will take at least 300 seconds to run, so we
would like to specify timeouts that apply to connecting and disconnecting but do
not apply to the contents of the context manager block. This is tricky because
the connecting and disconnecting are handled automatically inside the context
manager open_websocket_url()
. Here’s one possible approach:
with trio.fail_after(10) as cancel_scope:
async with open_websocket_url('ws://my.example'):
cancel_scope.deadline = math.inf
for _ in range(10):
await trio.sleep(30)
with trio.fail_after(15):
await ws.send_message('test')
msg = await ws.get_message()
print('Received message: {}'.format(msg))
cancel_scope.deadline = trio.current_time() + 5
This example places a 10 second timeout on connecting and a separate 5 second timeout on disconnecting. This is accomplished by wrapping the entire operation in a cancel scope and then modifying the cancel scope’s deadline when entering and exiting the context manager block.
This approach works but it is a bit complicated, and we don’t want our safety
mechanisms to be complicated! Therefore, the high-level client APIs
open_websocket()
and open_websocket_url()
contain internal timeouts
that apply only to connecting and disconnecting. Let’s rewrite the previous
example to use the library’s internal timeouts:
async with open_websocket_url('ws://my.example/', connect_timeout=10,
disconnect_timeout=5) as ws:
for _ in range(10):
await trio.sleep(30)
with trio.fail_after(15):
await ws.send_message('test')
msg = await ws.get_message()
print('Received message: {}'.format(msg))
Just like the previous example, this puts a 10 second timeout on connecting, a separate 5 second timeout on disconnecting. These internal timeouts violate the Trio philosophy of composable timeouts, but hopefully the examples in this section have convinced you that breaking the rules a bit is justified by the improved safety and ergonomics of this version.
In fact, these timeouts have actually been present in all of our examples so
far! We just didn’t see them because those arguments have default values. If you
really don’t like the internal timeouts, you can disable them by passing
math.inf
, or you can use the low-level APIs instead.
Timeouts on Low-level APIs
In the previous section, we saw how the library’s high-level APIs have internal
timeouts. The low-level APIs, like connect_websocket()
and
connect_websocket_url()
do not have internal timeouts, nor are they
context managers. These characteristics make the low-level APIs suitable for
situations where you want very fine-grained control over timeout behavior.
async with trio.open_nursery():
with trio.fail_after(10):
connection = await connect_websocket_url(nursery, 'ws://my.example/')
try:
for _ in range(10):
await trio.sleep(30)
with trio.fail_after(15):
await ws.send_message('test')
msg = await ws.get_message()
print('Received message: {}'.format(msg))
finally:
with trio.fail_after(5):
await connection.aclose()
This example applies the same 10 second timeout for connecting and 5 second timeout for disconnecting as seen in the previous section, but it uses the lower-level APIs. This approach gives you more control but the low-level APIs also require more boilerplate, such as creating a nursery and using try/finally to ensure that the connection is always closed.
Server Timeouts
The server API also has internal timeouts. These timeouts are configured when the server is created, and they are enforced on each connection.
async def handler(request):
ws = await request.accept()
msg = await ws.get_message()
print('Received message: {}'.format(msg))
await serve_websocket(handler, 'localhost', 8080, ssl_context=None,
connect_timeout=10, disconnect_timeout=5)
The server timeouts work slightly differently from the client timeouts. The
server’s connect timeout measures the time between receiving a new TCP
connection and calling the user’s handler. The connect timeout
includes waiting for the client’s side of the handshake (which is represented by
the request
object), but it does not include the server’s side of the
handshake. The server handshake needs to be performed inside the user’s
handler, e.g. await request.accept()
. The disconnect timeout applies to the
time between the handler exiting and the connection being closed.
Each handler is spawned inside of a nursery, so there is no way for connect and disconnect timeouts to raise exceptions to your code. (If they did raise exceptions, they would cancel your nursery and crash your server!) Instead, connect timeouts cause the connection to be silently closed, and the handler is never called. For disconnect timeouts, your handler has already exited, so a timeout will cause the connection to be silently closed.
As with the client APIs, you can disable the internal timeouts by passing
math.inf
or you can use low-level APIs like wrap_server_stream()
.
API
In addition to the convenience functions documented in Clients and Servers, the API has several important classes described on this page.
Requests
- class trio_websocket.WebSocketRequest
A request object presents the client’s handshake to a server handler. The server can inspect handshake properties like HTTP headers, subprotocols, etc. The server can also set some handshake properties like subprotocol. The server should call
accept()
to complete the handshake and obtain a connection object.- headers
HTTP headers represented as a list of (name, value) pairs.
- Return type:
list[tuple]
- proposed_subprotocols
A tuple of protocols proposed by the client.
- Return type:
tuple[str]
- await accept(*, subprotocol=None, extra_headers=None)
Accept the request and return a connection object.
- Parameters:
subprotocol (str or None) – The selected subprotocol for this connection.
extra_headers (list[tuple[bytes,bytes]] or None) – A list of 2-tuples containing key/value pairs to send as HTTP headers.
- Return type:
- await reject(status_code, *, extra_headers=None, body=None)
Reject the handshake.
- Parameters:
status_code (int) – The 3 digit HTTP status code. In order to be RFC-compliant, this should NOT be 101, and would ideally be an appropriate code in the range 300-599.
extra_headers (list[tuple[bytes,bytes]]) – A list of 2-tuples containing key/value pairs to send as HTTP headers.
body (bytes or None) – If provided, this data will be sent in the response body, otherwise no response body will be sent.
Connections
- class trio_websocket.WebSocketConnection
A connection object has functionality for sending and receiving messages, pinging the remote endpoint, and closing the WebSocket.
Note
The preferred way to obtain a connection is to use one of the convenience functions described in Clients or Servers. Instantiating a connection instance directly is tricky and is not recommended.
This object has properties that expose connection metadata.
- closed
(Read-only) The reason why the connection was closed, or
None
if the connection is still open.- Return type:
- is_client
(Read-only) Is this a client instance?
- is_server
(Read-only) Is this a server instance?
This object exposes the following properties related to the WebSocket handshake.
- path
The requested URL path. For clients, this is set when the connection is instantiated. For servers, it is set after the handshake completes.
- Return type:
str
- subprotocol
(Read-only) The negotiated subprotocol, or
None
if there is no subprotocol.This is only valid after the opening handshake is complete.
- Return type:
str or None
- handshake_headers
The HTTP headers that were sent by the remote during the handshake, stored as 2-tuples containing key/value pairs. Header keys are always lower case.
- Return type:
tuple[tuple[str,str]]
A connection object has a pair of methods for sending and receiving WebSocket messages. Messages can be
str
orbytes
objects.- await send_message(message)
Send a WebSocket message.
- Parameters:
message (str or bytes) – The message to send.
- Raises:
ConnectionClosed – if connection is closed, or being closed
- await get_message()
Receive the next WebSocket message.
If no message is available immediately, then this function blocks until a message is ready.
If the remote endpoint closes the connection, then the caller can still get messages sent prior to closing. Once all pending messages have been retrieved, additional calls to this method will raise
ConnectionClosed
. If the local endpoint closes the connection, then pending messages are discarded and calls to this method will immediately raiseConnectionClosed
.- Return type:
str or bytes
- Raises:
ConnectionClosed – if the connection is closed.
A connection object also has methods for sending pings and pongs. Each ping is sent with a unique payload, and the function blocks until a corresponding pong is received from the remote endpoint. This feature can be used to implement a bidirectional heartbeat.
A pong, on the other hand, sends an unsolicited pong to the remote endpoint and does not expect or wait for a response. This feature can be used to implement a unidirectional heartbeat.
- await ping(payload=None)
Send WebSocket ping to remote endpoint and wait for a correspoding pong.
Each in-flight ping must include a unique payload. This function sends the ping and then waits for a corresponding pong from the remote endpoint.
Note: If the remote endpoint recieves multiple pings, it is allowed to send a single pong. Therefore, the order of calls to ``ping()`` is tracked, and a pong will wake up its corresponding ping as well as all previous in-flight pings.
- Parameters:
payload (bytes or None) – The payload to send. If
None
then a random 32-bit payload is created.- Raises:
ConnectionClosed – if connection is closed.
ValueError – if
payload
is identical to another in-flight ping.
- await pong(payload=None)
Send an unsolicted pong.
- Parameters:
payload (bytes or None) – The pong’s payload. If
None
, then no payload is sent.- Raises:
ConnectionClosed – if connection is closed
Finally, the socket offers a method to close the connection. The connection context managers in Clients and Servers will automatically close the connection for you, but you may want to close the connection explicity if you are not using a context manager or if you want to customize the close reason.
- await aclose(code=1000, reason=None)
Close the WebSocket connection.
This sends a closing frame and suspends until the connection is closed. After calling this method, any further I/O on this WebSocket (such as
get_message()
orsend_message()
) will raiseConnectionClosed
.This method is idempotent: it may be called multiple times on the same connection without any errors.
- Parameters:
code (int) – A 4-digit code number indicating the type of closure.
reason (str) – An optional string describing the closure.
- class trio_websocket.CloseReason(code, reason)
Contains information about why a WebSocket was closed.
- property code
(Read-only) The numeric close code.
- property name
(Read-only) The human-readable close code.
- property reason
(Read-only) An arbitrary reason string.
- exception trio_websocket.ConnectionClosed(reason)
A WebSocket operation cannot be completed because the connection is closed or in the process of closing.
- exception trio_websocket.HandshakeError
There was an error during connection or disconnection with the websocket server.
- exception trio_websocket.ConnectionRejected(status_code, headers, body)
Bases:
HandshakeError
A WebSocket connection could not be established because the server rejected the connection attempt.
- exception trio_websocket.ConnectionTimeout
Bases:
HandshakeError
There was a timeout when connecting to the websocket server.
- exception trio_websocket.DisconnectionTimeout
Bases:
HandshakeError
There was a timeout when disconnecting from the websocket server.
Utilities
These are classes that you do not need to instantiate yourself, but you may get access to instances of these classes through other APIs.
Recipes
This page contains notes and sample code for common usage scenarios with this library.
Heartbeat
If you wish to keep a connection open for long periods of time but do not need to send messages frequently, then a heartbeat holds the connection open and also detects when the connection drops unexpectedly. The following recipe demonstrates how to implement a connection heartbeat using WebSocket’s ping/pong feature.
1async def heartbeat(ws, timeout, interval):
2 '''
3 Send periodic pings on WebSocket ``ws``.
4
5 Wait up to ``timeout`` seconds to send a ping and receive a pong. Raises
6 ``TooSlowError`` if the timeout is exceeded. If a pong is received, then
7 wait ``interval`` seconds before sending the next ping.
8
9 This function runs until cancelled.
10
11 :param ws: A WebSocket to send heartbeat pings on.
12 :param float timeout: Timeout in seconds.
13 :param float interval: Interval between receiving pong and sending next
14 ping, in seconds.
15 :raises: ``ConnectionClosed`` if ``ws`` is closed.
16 :raises: ``TooSlowError`` if the timeout expires.
17 :returns: This function runs until cancelled.
18 '''
19 while True:
20 with trio.fail_after(timeout):
21 await ws.ping()
22 await trio.sleep(interval)
23
24async def main():
25 async with open_websocket_url('ws://my.example/') as ws:
26 async with trio.open_nursery() as nursery:
27 nursery.start_soon(heartbeat, ws, 5, 1)
28 # Your application code goes here:
29 pass
30
31trio.run(main)
Note that the ping()
method waits until it receives a
pong frame, so it ensures that the remote endpoint is still responsive. If the
connection is dropped unexpectedly or takes too long to respond, then
heartbeat()
will raise an exception that will cancel the nursery. You may
wish to implement additional logic to automatically reconnect.
A heartbeat feature can be enabled in the example client.
with the --heartbeat
flag.
Contributing
Developer Installation
If you want to help contribute to trio-websocket
, then you will need to
install additional dependencies that are used for testing and documentation. The
following sequence of commands will clone the repository, create a virtual
environment, and install the developer dependencies:
$ git clone git@github.com:HyperionGray/trio-websocket.git
$ cd trio-websocket
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install -r requirements-dev-full.txt
(venv) $ pip install -e .
This example uses Python’s built-in venv
package, but you can of course use
other virtual environment tools such as virtualenvwrapper
.
The requirements-dev.in
and requirements-extras.in
files contain extra
dependencies only needed for development, such as PyTest, Sphinx, etc.
The .in
files and setup.py
are used to generate the pinned manifests
requirements-dev-full.txt
and requirements-dev.txt
, so that dependencies
used in development and CI builds do not change arbitrarily over time.
Unit Tests
Note
This project has unit tests that are configured to run on all pull requests to automatically check for regressions. Each pull request should include unit test coverage before it is merged.
The unit tests are written with the PyTest framework. You can quickly run all unit tests from the project’s root with a simple command:
(venv) $ pytest
=== test session starts ===
platform linux -- Python 3.6.6, pytest-3.8.0, py-1.6.0, pluggy-0.7.1
rootdir: /home/johndoe/code/trio-websocket, inifile: pytest.ini
plugins: trio-0.5.0, cov-2.6.0
collected 27 items
tests/test_connection.py ........................... [100%]
=== 27 passed in 0.41 seconds ===
You can enable code coverage reporting by adding the -cov=trio_websocket
option to PyTest or using the Makefile target make test
:
(venv) $ pytest --cov=trio_websocket
=== test session starts ===
platform linux -- Python 3.6.6, pytest-3.8.0, py-1.6.0, pluggy-0.7.1
rootdir: /home/johndoe/code/trio-websocket, inifile: pytest.ini
plugins: trio-0.5.0, cov-2.6.0
collected 27 items
tests/test_connection.py ........................... [100%]
---------- coverage: platform darwin, python 3.7.0-final-0 -----------
Name Stmts Miss Cover
------------------------------------------------
trio_websocket/__init__.py 369 33 91%
trio_websocket/_version.py 1 0 100%
------------------------------------------------
TOTAL 370 33 91%
=== 27 passed in 0.57 seconds ===
Documentation
This documentation is stored in the repository in the /docs/
directory. It
is written with RestructuredText markup and processed by Sphinx. To build documentation, run this
command from the project root:
$ make docs
The finished documentation can be found in /docs/_build/
. This documentation
is published automatically to Read The Docs for
all pushes to master or to a tag.
Autobahn Client Tests
The Autobahn Test Suite contains over 500 integration tests for WebSocket servers and clients. These test suites are contained in a Docker container. You will need to install Docker before you can run these integration tests.
To test the client, you will need two terminal windows. In the first terminal, run the following commands:
$ cd autobahn
$ docker run -it --rm \
-v "${PWD}/config:/config" \
-v "${PWD}/reports:/reports" \
-p 9001:9001 \
--name autobahn \
crossbario/autobahn-testsuite
The first time you run this command, Docker will download some files, which may take a few minutes. When the test suite is ready, it will display:
Autobahn WebSocket 0.8.0/0.10.9 Fuzzing Server (Port 9001)
Ok, will run 249 test cases for any clients connecting
Now in the second terminal, run the Autobahn client:
$ cd autobahn
$ python client.py ws://localhost:9001
INFO:client:Case count=249
INFO:client:Running test case 1 of 249
INFO:client:Running test case 2 of 249
INFO:client:Running test case 3 of 249
INFO:client:Running test case 4 of 249
INFO:client:Running test case 5 of 249
<snip>
When the client finishes running, an HTML report is published to the
autobahn/reports/clients
directory. If any tests fail, you can debug
individual tests by specifying the integer test case ID (not the dotted test
case ID), e.g. to run test case #29:
$ python client.py ws://localhost:9001 29
Autobahn Server Tests
Read the section on Autobahn client tests before you read this section. Once again, you will need two terminal windows. In the first terminal, run:
$ cd autobahn
$ python server.py
In the second terminal, you will run the Docker image:
$ cd autobahn
$ docker run -it --rm \
-v "${PWD}/config:/config" \
-v "${PWD}/reports:/reports" \
-p 9000:9000 \
--name autobahn \
crossbario/autobahn-testsuite \
wstest --mode fuzzingclient --spec /config/fuzzingclient.json
If a test fails, server.py
does not support the same debug_cases
argument as client.py
, but you can modify fuzzingclient.json to specify a
subset of cases to run, e.g. 3.*
to run all test cases in section 3.
Note
For OS X or Windows, you’ll need to edit fuzzingclient.json and
change the host from 172.17.0.1
to host.docker.internal
.
Versioning
This project uses semantic versioning for official
releases. When a new version is released, the version number on the master
branch will be incremented to the next expected release and suffixed “dev”. For
example, if we release version 1.1.0, then the version number on master
might be set to 1.2.0-dev
, indicating that the next expected release is
1.2.0
and that release is still under development.
Release Process
To release a new version of this library, we follow this process:
In
_version.py
onmaster
branch, remove the-dev
suffix from the version number, e.g. change1.2.0-dev
to1.2.0
.Commit
_version.py
.Create a tag, e.g.
git tag 1.2.0
.Push the commit and the tag, e.g.
git push && git push origin 1.2.0
.Wait for Github CI to finish building and ensure that the build is successful.
Wait for Read The Docs to finish building and ensure that the build is successful.
Ensure that the working copy is in a clean state, e.g.
git status
shows no changes.Build package and submit to PyPI:
make publish
In
_version.py
onmaster
branch, increment the version number to the next expected release and add the-dev
suffix, e.g. change1.2.0
to1.3.0-dev
.Commit and push
_version.py
.
Credits
Thanks to John Belmonte (@belm0) and Nathaniel J. Smith (@njsmith) for lots of feedback, discussion, code reviews, and pull requests. Thanks to all the Trio contributors for making a fantastic framework! Thanks to Hyperion Gray for supporting development time on this project.