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

Update docs #1283

Merged
merged 7 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions docs/source/asyncio-example.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,13 @@ This example demonstrates some basic asyncio techniques.
:encoding: utf-8


You can use ``cert.crt`` and ``cert.key`` files provided within the repository
or generate your own certificates using `OpenSSL`_:

.. code-block:: console

$ openssl req -x509 -newkey rsa:2048 -keyout cert.key -out cert.crt -days 365 -nodes


.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _OpenSSL: https://openssl-library.org/source/index.html
59 changes: 33 additions & 26 deletions docs/source/basic-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ This is not a socket tutorial, so we're not going to dive too deeply into how
this works. If you want more detail about sockets, there are lots of good
tutorials on the web that you should investigate.

When you want to listen for incoming connections, the you need to *bind* an
When you want to listen for incoming connections, you need to *bind* an
address first. So let's do that. Try setting up your file to look like this:

.. code-block:: python
Expand Down Expand Up @@ -213,20 +213,23 @@ connection object and start handing it data. For now, let's just see what
happens as we feed it data.

To make HTTP/2 connections, we need a tool that knows how to speak HTTP/2.
Most versions of curl in the wild don't, so let's install a Python tool. In
your Python environment, run ``pip install hyper``. This will install a Python
command-line HTTP/2 tool called ``hyper``. To confirm that it works, try
running this command and verifying that the output looks similar to the one
shown below:
You can simply use `curl`_ or any other client with HTTP/2 support like
`httpx`_. To confirm that it works, try running this command and verifying that
the output looks similar to the one shown below:

.. code-block:: console

$ hyper GET https://nghttp2.org/httpbin/get
$ curl --http2 https://nghttp2.org/httpbin/get
{'args': {},
'headers': {'Host': 'nghttp2.org'},
'origin': '10.0.0.2',
'url': 'https://nghttp2.org/httpbin/get'}


To use it with our server though, you will need to invoke it with a different
Copy link
Member

@Kriechi Kriechi Aug 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To use it with our server though, you will need to invoke it with a different
To use it with a ``python-hyper/h2``-based server, you can invoke it with a different

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this context I meant to use it with the server from this example, not any arbitrary hyper/h2-based server (as generally you will serve it with TLS), so you should or you have to is probably better.

``--http2-prior-knowledge`` flag as we are going to serve over the insecure
connection.

Assuming it works, you're now ready to start sending HTTP/2 data.

Back in our ``h2server.py`` script, we're going to want to start handling data.
Expand Down Expand Up @@ -290,15 +293,16 @@ function. Your ``h2server.py`` should end up looking a like this:
handle(sock.accept()[0])

Running that in one shell, in your other shell you can run
``hyper --h2 GET http://localhost:8080/``. That shell should hang, and you
should then see the following output from your ``h2server.py`` shell:
``curl -v --http2-prior-knowledge http://localhost:8080/`` command.
bmwant marked this conversation as resolved.
Show resolved Hide resolved
That shell should hang, and you should then see the following output from your
``h2server.py`` shell:

.. code-block:: console

$ python h2server.py
[<h2.events.RemoteSettingsChanged object at 0x10c4ee390>]

You'll then need to kill ``hyper`` and ``h2server.py`` with Ctrl+C. Feel free
You'll then need to kill ``curl`` and ``h2server.py`` with Ctrl+C. Feel free
to do this a few times, to see how things behave.

So, what did we see here? When the connection was opened, we used the
Expand All @@ -307,15 +311,15 @@ socket, in a loop. We then passed that data to the connection object, which
returned us a single event object:
:class:`RemoteSettingsChanged <h2.events.RemoteSettingsChanged>`.

But what we didn't see was anything else. So it seems like all ``hyper`` did
was change its settings, but nothing else. If you look at the other ``hyper``
But what we didn't see was anything else. So it seems like all ``curl`` did
was change its settings, but nothing else. If you look at the other ``curl``
window, you'll notice that it hangs for a while and then eventually fails with
a socket timeout. It was waiting for something: what?

Well, it turns out that at the start of a connection, both sides need to send
a bit of data, called "the HTTP/2 preamble". We don't need to get into too much
detail here, but basically both sides need to send a single block of HTTP/2
data that tells the other side what their settings are. ``hyper`` did that,
data that tells the other side what their settings are. ``curl`` did that,
but we didn't.

Let's do that next.
Expand Down Expand Up @@ -388,9 +392,10 @@ Your ``h2server.py`` script should now look like this:


With this change made, rerun your ``h2server.py`` script and hit it with the
same ``hyper`` command: ``hyper --h2 GET http://localhost:8080/``. The
``hyper`` command still hangs, but this time we get a bit more output from our
``h2server.py`` script:
same ``curl`` command:
``curl -v --http2-prior-knowledge http://localhost:8080/``.
The ``curl`` command still hangs, but this time we get a bit more output from
our ``h2server.py`` script:

.. code-block:: console

Expand All @@ -410,17 +415,17 @@ Finally, even more data that triggers *two* events:
:class:`RequestReceived <h2.events.RequestReceived>` and
:class:`StreamEnded <h2.events.StreamEnded>`.

So, what's happening is that ``hyper`` is telling us about its settings,
So, what's happening is that ``curl`` is telling us about its settings,
acknowledging ours, and then sending us a request. Then it ends a *stream*,
which is a HTTP/2 communications channel that holds a request and response
pair.

A stream isn't done until it's either *reset* or both sides *close* it:
in this sense it's bi-directional. So what the ``StreamEnded`` event tells us
is that ``hyper`` is closing its half of the stream: it won't send us any more
is that ``curl`` is closing its half of the stream: it won't send us any more
data on that stream. That means the request is done.

So why is ``hyper`` hanging? Well, we haven't sent a response yet: let's do
So why is ``curl`` hanging? Well, we haven't sent a response yet: let's do
that.


Expand Down Expand Up @@ -489,7 +494,7 @@ one exception is headers: h2 will automatically encode those into UTF-8.
The last thing to note is that on our call to ``send_data``, we set
``end_stream`` to ``True``. This tells h2 (and the remote peer) that
we're done with sending data: the response is over. Because we know that
``hyper`` will have ended its side of the stream, when we end ours the stream
``curl`` will have ended its side of the stream, when we end ours the stream
will be totally done with.

We're nearly ready to go with this: we just need to plumb this function in.
Expand Down Expand Up @@ -581,9 +586,9 @@ With these changes, your ``h2server.py`` file should look like this:
while True:
handle(sock.accept()[0])

Alright. Let's run this, and then run our ``hyper`` command again.
Alright. Let's run this, and then run our ``curl`` command again.

This time, nothing is printed from our server, and the ``hyper`` side prints
This time, nothing is printed from our server, and the ``curl`` side prints
``it works!``. Success! Try running it a few more times, and we can see that
not only does it work the first time, it works the other times too!

Expand Down Expand Up @@ -692,15 +697,15 @@ file, which should now look like this:
while True:
handle(sock.accept()[0])

Now, execute ``h2server.py`` and then point ``hyper`` at it again. You should
see something like the following output from ``hyper``:
Now, execute ``h2server.py`` and then point ``curl`` at it again. You should
see something like the following output from ``curl``:

.. code-block:: console

$ hyper --h2 GET http://localhost:8080/
$ curl -v --http2-prior-knowledge http://localhost:8080/
{":scheme": "http", ":authority": "localhost", ":method": "GET", ":path": "/"}

Here you can see the HTTP/2 request 'special headers' that ``hyper`` sends.
Here you can see the HTTP/2 request 'special headers' that ``curl`` sends.
These are similar to the ``:status`` header we have to send on our response:
they encode important parts of the HTTP request in a clearly-defined way. If
you were writing a client stack using h2, you'd need to make sure you
Expand Down Expand Up @@ -744,3 +749,5 @@ it, there are a few directions you could investigate:
.. _get your private key here: https://raw.githubusercontent.com/python-hyper/h2/master/examples/twisted/server.key
.. _PyOpenSSL: http://pyopenssl.readthedocs.org/
.. _Eventlet example: https://github.com/python-hyper/h2/blob/master/examples/eventlet/eventlet-server.py
.. _curl: https://curl.se/docs/http2.html
.. _httpx: https://www.python-httpx.org/
8 changes: 8 additions & 0 deletions docs/source/wsgi-example.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ The main advantages of this example are:
:encoding: utf-8


You can use ``cert.crt`` and ``cert.key`` files provided within the repository
or generate your own certificates using `OpenSSL`_:

.. code-block:: console

$ openssl req -x509 -newkey rsa:2048 -keyout cert.key -out cert.crt -days 365 -nodes

.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _OpenSSL: https://openssl-library.org/source/index.html
10 changes: 5 additions & 5 deletions examples/asyncio/asyncio-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ def window_updated(self, stream_id, delta):
loop.run_forever()
except KeyboardInterrupt:
pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
finally:
# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
89 changes: 37 additions & 52 deletions examples/asyncio/wsgi-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,9 @@ def window_opened(self, event):
for data in self._flow_controlled_data.values():
self._stream_data.put_nowait(data)

self._flow_controlled_data = {}
self._flow_controlled_data.clear()

@asyncio.coroutine
def sending_loop(self):
async def sending_loop(self):
"""
A call that loops forever, attempting to send data. This sending loop
contains most of the flow-control smarts of this class: it pulls data
Expand All @@ -216,7 +215,7 @@ def sending_loop(self):
This coroutine explicitly *does not end*.
"""
while True:
stream_id, data, event = yield from self._stream_data.get()
stream_id, data, event = await self._stream_data.get()

# If this stream got reset, just drop the data on the floor. Note
# that we need to reset the event here to make sure that
Expand Down Expand Up @@ -327,7 +326,7 @@ def reset_stream(self, event):
data.
"""
if event.stream_id in self._flow_controlled_data:
del self._flow_controlled_data
del self._flow_controlled_data[event.stream_id]

self._reset_streams.add(event.stream_id)
self.end_stream(event)
Expand Down Expand Up @@ -534,23 +533,9 @@ def readline(self, hint=None):
def readlines(self, hint=None):
"""
Called by the WSGI application to read several lines of data.

This method is really pretty stupid. It rigorously observes the
``hint`` parameter, and quite happily returns the input split into
lines.
"""
# This method is *crazy inefficient*, but it's also a pretty stupid
# method to call.
data = self.read(hint)
lines = data.split(b'\n')

# Split removes the newline character, but we want it, so put it back.
lines = [line + b'\n' for line in lines]

# Except if the last character was a newline character we now have an
# extra line that is just a newline: pull that out.
if lines[-1] == b'\n':
lines = lines[:-1]
lines = data.splitlines(keepends=True)
return lines

def start_response(self, status, response_headers, exc_info=None):
Expand Down Expand Up @@ -688,41 +673,41 @@ def _build_environ_dict(headers, stream):
version you'd want to fix it.
"""
header_dict = dict(headers)
path = header_dict.pop(u':path')
path = header_dict.pop(':path')
try:
path, query = path.split(u'?', 1)
path, query = path.split('?', 1)
except ValueError:
query = u""
server_name = header_dict.pop(u':authority')
query = ""
server_name = header_dict.pop(':authority')
try:
server_name, port = server_name.split(u':', 1)
except ValueError as e:
server_name, port = server_name.split(':', 1)
except ValueError:
port = "8443"

environ = {
u'REQUEST_METHOD': header_dict.pop(u':method'),
u'SCRIPT_NAME': u'',
u'PATH_INFO': path,
u'QUERY_STRING': query,
u'SERVER_NAME': server_name,
u'SERVER_PORT': port,
u'SERVER_PROTOCOL': u'HTTP/2',
u'HTTPS': u"on",
u'SSL_PROTOCOL': u'TLSv1.2',
u'wsgi.version': (1, 0),
u'wsgi.url_scheme': header_dict.pop(u':scheme'),
u'wsgi.input': stream,
u'wsgi.errors': sys.stderr,
u'wsgi.multithread': True,
u'wsgi.multiprocess': False,
u'wsgi.run_once': False,
'REQUEST_METHOD': header_dict.pop(':method'),
'SCRIPT_NAME': '',
'PATH_INFO': path,
'QUERY_STRING': query,
'SERVER_NAME': server_name,
'SERVER_PORT': port,
'SERVER_PROTOCOL': 'HTTP/2',
'HTTPS': "on",
'SSL_PROTOCOL': 'TLSv1.2',
'wsgi.version': (1, 0),
'wsgi.url_scheme': header_dict.pop(':scheme'),
'wsgi.input': stream,
'wsgi.errors': sys.stderr,
'wsgi.multithread': True,
'wsgi.multiprocess': False,
'wsgi.run_once': False,
}
if u'content-type' in header_dict:
environ[u'CONTENT_TYPE'] = header_dict[u'content-type']
if u'content-length' in header_dict:
environ[u'CONTENT_LENGTH'] = header_dict[u'content-length']
if 'content-type' in header_dict:
environ['CONTENT_TYPE'] = header_dict.pop('content-type')
if 'content-length' in header_dict:
environ['CONTENT_LENGTH'] = header_dict.pop('content-length')
Kriechi marked this conversation as resolved.
Show resolved Hide resolved
for name, value in header_dict.items():
environ[u'HTTP_' + name.upper()] = value
environ['HTTP_' + name.upper()] = value
return environ


Expand Down Expand Up @@ -753,8 +738,8 @@ def _build_environ_dict(headers, stream):
loop.run_forever()
except KeyboardInterrupt:
pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
finally:
# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ commands = flake8 src/ test/

[testenv:docs]
deps =
sphinx>=4.0.2,<5
sphinx>=5.0.2,<6
allowlist_externals = make
changedir = {toxinidir}/docs
commands =
Expand Down
Loading