Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create docs #71

Merged
merged 4 commits into from
Oct 29, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 32 additions & 206 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
# Trio WebSocket

This library implements [the WebSocket
protocol](https://tools.ietf.org/html/rfc6455), striving for safety,
correctness, and ergonomics. It is based on the [wsproto
project](https://wsproto.readthedocs.io/en/latest/), which is a
[Sans-IO](https://sans-io.readthedocs.io/) state machine that implements the
majority of the WebSocket protocol, including framing, codecs, and events. This
library handles I/O using [the Trio
framework](https://trio.readthedocs.io/en/latest/). This library passes the
[Autobahn Test Suite](https://github.com/crossbario/autobahn-testsuite).

This README contains a brief introduction to the project. Full documentation [is
available here](https://trio-websocket.readthedocs.io).

[![PyPI](https://img.shields.io/pypi/v/trio-websocket.svg?style=flat-square)](https://pypi.org/project/trio-websocket/)
![Python Versions](https://img.shields.io/pypi/pyversions/trio-websocket.svg?style=flat-square)
![MIT License](https://img.shields.io/github/license/HyperionGray/trio-websocket.svg?style=flat-square)
[![Build Status](https://img.shields.io/travis/HyperionGray/trio-websocket.svg?style=flat-square)](https://travis-ci.org/HyperionGray/trio-websocket)
[![Coverage](https://img.shields.io/coveralls/github/HyperionGray/trio-websocket.svg?style=flat-square)](https://coveralls.io/github/HyperionGray/trio-websocket?branch=master)

# Trio WebSocket

This project implements [the WebSocket
protocol](https://tools.ietf.org/html/rfc6455). It is based on the [wsproto
project](https://wsproto.readthedocs.io/en/latest/), which is a [Sans-IO](https://sans-io.readthedocs.io/) state machine that implements the majority of
the WebSocket protocol, including framing, codecs, and events. This library
implements the I/O using [Trio](https://trio.readthedocs.io/en/latest/).
[![Read the Docs](https://img.shields.io/readthedocs/trio-websocket.svg)](https://trio-websocket.readthedocs.io)

## Installation

`trio-websocket` requires Python v3.5 or greater. To install from PyPI:
This library requires Python 3.5 or greater. To install from PyPI:

pip install trio-websocket

If you want to help develop `trio-websocket`, clone [the
repository](https://github.com/hyperiongray/trio-websocket) and run this command
from the repository root:

pip install --editable .[dev]
## Client Example

## Sample client

The following example demonstrates opening a WebSocket by URL. The connection
may also be opened with `open_websocket(…)`, which takes a host, port, and
resource as arguments.
This example demonstrates how to open a WebSocket URL:

```python
import trio
Expand All @@ -37,25 +37,25 @@ from trio_websocket import open_websocket_url

async def main():
try:
async with open_websocket_url('ws://localhost/foo') as ws:
async with open_websocket_url('wss://localhost/foo') as ws:
await ws.send_message('hello world!')
message = await ws.get_message()
logging.info('Received message: %s', message)
except OSError as ose:
logging.error('Connection attempt failed: %s', ose)

trio.run(main)
```

A more detailed example is in `examples/client.py`. **Note:** if you want to run
this example client with SSL, you'll need to install the `trustme` module from
PyPI (installed automtically if you used the `[dev]` extras when installing
`trio-websocket`) and then generate a self-signed certificate by running
`example/generate-cert.py`.
The WebSocket context manager connects automatically before entering the block
and disconnects automatically before exiting the block. The full API offers a
lot of flexibility and additional options.

## Sample server
## Server Example

A WebSocket server requires a bind address, a port, and a coroutine to handle
incoming connections. This example demonstrates an "echo server" that replies
to each incoming message with an identical outgoing message.
incoming connections. This example demonstrates an "echo server" that replies to
each incoming message with an identical outgoing message.

```python
import trio
Expand All @@ -76,181 +76,7 @@ async def main():
trio.run(main)
```

A longer example is in `examples/server.py`. **See the note above about using
SSL with the example client.**

## Heartbeat recipe

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.

```python
async def heartbeat(ws, timeout, interval):
'''
Send periodic pings on WebSocket ``ws``.

Wait up to ``timeout`` seconds to send a ping and receive a pong. Raises
``TooSlowError`` if the timeout is exceeded. If a pong is received, then
wait ``interval`` seconds before sending the next ping.

This function runs until cancelled.

:param ws: A WebSocket to send heartbeat pings on.
:param float timeout: Timeout in seconds.
:param float interval: Interval between receiving pong and sending next
ping, in seconds.
:raises: ``ConnectionClosed`` if ``ws`` is closed.
:raises: ``TooSlowError`` if the timeout expires.
:returns: This function runs until cancelled.
'''
while True:
with trio.fail_after(timeout):
await ws.ping()
await trio.sleep(interval)

async def main():
async with open_websocket_url('ws://localhost/foo') as ws:
async with trio.open_nursery() as nursery:
nursery.start_soon(heartbeat, ws, 5, 1)
# Your application code goes here:
pass

trio.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.

**Note that the WebSocket RFC does not require a WebSocket to send a pong for each
ping:**

> If an endpoint receives a Ping frame and has not yet sent Pong frame(s) in
> response to previous Ping frame(s), the endpoint MAY elect to send a Pong
> frame for only the most recently processed Ping frame.

Therefore, if you have multiple pings in flight at the same time, you may not
get an equal number of pongs in response. The simplest strategy for dealing with
this is to only have one ping in flight at a time, as seen in the example above.
As an alternative, you can send a `bytes` payload with each ping. The server
will return the payload with the pong:

```python
await ws.ping(b'my payload')
pong == await ws.wait_pong()
assert pong == b'my payload'
```

You may want to embed a nonce or counter in the payload in order to correlate
pong events to the pings you have sent.

## Unit Tests

Unit tests are written in the pytest style. You must install the development
dependencies as described in the installation section above. The
``--cov=trio_websocket`` flag turns on code coverage.

$ 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/mhaase/code/trio-websocket, inifile: pytest.ini
plugins: trio-0.5.0, cov-2.6.0
collected 21 items

tests/test_connection.py ..................... [100%]

--- coverage: platform linux, python 3.6.6-final-0 ---
Name Stmts Miss Cover
------------------------------------------------
trio_websocket/__init__.py 297 40 87%
trio_websocket/_channel.py 140 52 63%
trio_websocket/version.py 1 0 100%
------------------------------------------------
TOTAL 438 92 79%

=== 21 passed in 0.54 seconds ===

## Integration Testing with Autobahn

The Autobahn Test Suite contains over 500 integration tests for WebSocket
servers and clients. These test suites are contained in a
[Docker](https://www.docker.com/) container. You will need to install Docker
before you can run these integration tests.

### Client 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

### Server Tests

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" \
--name autobahn \
crossbario/autobahn-testsuite \
/usr/local/bin/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.

## Release Process

* Remove `-dev` suffix from `version.py`.
* Commit and push version change.
* Create and push tag, e.g. `git tag 1.0.0 && git push origin 1.0.0`.
* Clean build directory: `rm -fr dist`
* Build package: `python setup.py sdist`
* Upload to PyPI: `twine upload dist/*`
* Increment version and add `-dev` suffix.
* Commit and push version change.
The server's handler ``echo_server(…)`` receives a connection request object.
This object can be used to inspect the client's request and modify the
handshake, then it can be exchanged for an actual WebSocket object ``ws``.
Again, the full API offers a lot of flexibility and additional options.
2 changes: 2 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
_build

19 changes: 19 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = .
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
3 changes: 3 additions & 0 deletions docs/_static/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This is just a placeholder file because this project doesn't
have any static assets.

80 changes: 80 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
API
===

.. currentmodule:: trio_websocket

In addition to the convenience functions documented in :ref:`websocket-clients`
and :ref:`websocket-servers`, the API has several important classes described
on this page.

Requests
--------

.. class:: 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 :meth:`accept` to complete the handshake and obtain a
connection object.

.. autoattribute:: headers
.. autoattribute:: proposed_subprotocols
.. autoattribute:: subprotocol
.. autoattribute:: url
.. automethod:: accept

Connections
-----------

.. class:: 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 :ref:`websocket-clients` or
:ref:`websocket-servers`. Instantiating a connection instance directly is
tricky and is not recommended.

This object has properties that expose connection metadata.

.. autoattribute:: is_closed
.. autoattribute:: close_reason
.. autoattribute:: is_client
.. autoattribute:: is_server
.. autoattribute:: path
.. autoattribute:: subprotocol

A connection object has a pair of methods for sending and receiving
WebSocket messages. Messages can be ``str`` or ``bytes`` objects.

.. automethod:: send_message
.. automethod:: get_message

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.

.. automethod:: ping
.. automethod:: pong

Finally, the socket offers a method to close the connection. The connection
context managers in :ref:`websocket-clients` and :ref:`websocket-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.

.. automethod:: aclose

.. autoclass:: CloseReason
:members:

.. autoexception:: ConnectionClosed
Loading