Skip to content

Commit e02b942

Browse files
authored
Add gevent example (#1201)
1 parent 355ace9 commit e02b942

File tree

7 files changed

+258
-2
lines changed

7 files changed

+258
-2
lines changed

README.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ You use it like this:
2727
.. code-block:: python
2828
2929
import h2.connection
30+
import h2.config
3031
31-
conn = h2.connection.H2Connection()
32+
config = h2.config.H2Configuration()
33+
conn = h2.connection.H2Connection(config=config)
3234
conn.send_headers(stream_id=stream_id, headers=headers)
3335
conn.send_data(stream_id, data)
3436
socket.sendall(conn.data_to_send())

docs/source/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Example Servers
1414
asyncio-example
1515
twisted-example
1616
eventlet-example
17+
gevent-example
1718
curio-example
1819
tornado-example
1920
wsgi-example

docs/source/gevent-example.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Gevent Example Server
2+
=====================
3+
4+
This example is a basic HTTP/2 server written using `gevent`_, a powerful
5+
coroutine-based Python networking library that uses `greenlet`_
6+
to provide a high-level synchronous API on top of the `libev`_ or `libuv`_
7+
event loop.
8+
9+
This example is inspired by the curio one and also demonstrates the correct use
10+
of HTTP/2 flow control with Hyper-h2 and how gevent can be simple to use.
11+
12+
.. literalinclude:: ../../examples/gevent/gevent-server.py
13+
:language: python
14+
:linenos:
15+
:encoding: utf-8
16+
17+
.. _gevent: http://www.gevent.org/
18+
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
19+
.. _libev: http://software.schmorp.de/pkg/libev.html
20+
.. _libuv: http://libuv.org/

examples/gevent/gevent-server.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
gevent-server.py
4+
================
5+
6+
A simple HTTP/2 server written for gevent serving static files from a directory specified as input.
7+
If no directory is provided, the current directory will be used.
8+
"""
9+
import mimetypes
10+
import sys
11+
from functools import partial
12+
from pathlib import Path
13+
from typing import Tuple, Dict, Optional
14+
15+
from gevent import socket, ssl
16+
from gevent.event import Event
17+
from gevent.server import StreamServer
18+
from h2 import events
19+
from h2.config import H2Configuration
20+
from h2.connection import H2Connection
21+
22+
23+
def get_http2_tls_context() -> ssl.SSLContext:
24+
ctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
25+
ctx.options |= (
26+
ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
27+
)
28+
29+
ctx.options |= ssl.OP_NO_COMPRESSION
30+
ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20')
31+
ctx.load_cert_chain(certfile='localhost.crt', keyfile='localhost.key')
32+
ctx.set_alpn_protocols(['h2'])
33+
try:
34+
ctx.set_npn_protocols(['h2'])
35+
except NotImplementedError:
36+
pass
37+
38+
return ctx
39+
40+
41+
class H2Worker:
42+
43+
def __init__(self, sock: socket, address: Tuple[str, str], source_dir: str = None):
44+
self._sock = sock
45+
self._address = address
46+
self._flow_control_events: Dict[int, Event] = {}
47+
self._server_name = 'gevent-h2'
48+
self._connection: Optional[H2Connection] = None
49+
self._read_chunk_size = 8192 # The maximum amount of a file we'll send in a single DATA frame
50+
51+
self._check_sources_dir(source_dir)
52+
self._sources_dir = source_dir
53+
54+
self._run()
55+
56+
def _initiate_connection(self):
57+
config = H2Configuration(client_side=False, header_encoding='utf-8')
58+
self._connection = H2Connection(config=config)
59+
self._connection.initiate_connection()
60+
self._sock.sendall(self._connection.data_to_send())
61+
62+
@staticmethod
63+
def _check_sources_dir(sources_dir: str) -> None:
64+
p = Path(sources_dir)
65+
if not p.is_dir():
66+
raise NotADirectoryError(f'{sources_dir} does not exists')
67+
68+
def _send_error_response(self, status_code: str, event: events.RequestReceived) -> None:
69+
self._connection.send_headers(
70+
stream_id=event.stream_id,
71+
headers=[
72+
(':status', status_code),
73+
('content-length', '0'),
74+
('server', self._server_name),
75+
],
76+
end_stream=True
77+
)
78+
self._sock.sendall(self._connection.data_to_send())
79+
80+
def _handle_request(self, event: events.RequestReceived) -> None:
81+
headers = dict(event.headers)
82+
if headers[':method'] != 'GET':
83+
self._send_error_response('405', event)
84+
return
85+
86+
file_path = Path(self._sources_dir) / headers[':path'].lstrip('/')
87+
if not file_path.is_file():
88+
self._send_error_response('404', event)
89+
return
90+
91+
self._send_file(file_path, event.stream_id)
92+
93+
def _send_file(self, file_path: Path, stream_id: int) -> None:
94+
"""
95+
Send a file, obeying the rules of HTTP/2 flow control.
96+
"""
97+
file_size = file_path.stat().st_size
98+
content_type, content_encoding = mimetypes.guess_type(str(file_path))
99+
response_headers = [
100+
(':status', '200'),
101+
('content-length', str(file_size)),
102+
('server', self._server_name)
103+
]
104+
if content_type:
105+
response_headers.append(('content-type', content_type))
106+
if content_encoding:
107+
response_headers.append(('content-encoding', content_encoding))
108+
109+
self._connection.send_headers(stream_id, response_headers)
110+
self._sock.sendall(self._connection.data_to_send())
111+
112+
with file_path.open(mode='rb', buffering=0) as f:
113+
self._send_file_data(f, stream_id)
114+
115+
def _send_file_data(self, file_obj, stream_id: int) -> None:
116+
"""
117+
Send the data portion of a file. Handles flow control rules.
118+
"""
119+
while True:
120+
while self._connection.local_flow_control_window(stream_id) < 1:
121+
self._wait_for_flow_control(stream_id)
122+
123+
chunk_size = min(self._connection.local_flow_control_window(stream_id), self._read_chunk_size)
124+
data = file_obj.read(chunk_size)
125+
keep_reading = (len(data) == chunk_size)
126+
127+
self._connection.send_data(stream_id, data, not keep_reading)
128+
self._sock.sendall(self._connection.data_to_send())
129+
130+
if not keep_reading:
131+
break
132+
133+
def _wait_for_flow_control(self, stream_id: int) -> None:
134+
"""
135+
Blocks until the flow control window for a given stream is opened.
136+
"""
137+
event = Event()
138+
self._flow_control_events[stream_id] = event
139+
event.wait()
140+
141+
def _handle_window_update(self, event: events.WindowUpdated) -> None:
142+
"""
143+
Unblock streams waiting on flow control, if needed.
144+
"""
145+
stream_id = event.stream_id
146+
147+
if stream_id and stream_id in self._flow_control_events:
148+
g_event = self._flow_control_events.pop(stream_id)
149+
g_event.set()
150+
elif not stream_id:
151+
# Need to keep a real list here to use only the events present at this time.
152+
blocked_streams = list(self._flow_control_events.keys())
153+
for stream_id in blocked_streams:
154+
g_event = self._flow_control_events.pop(stream_id)
155+
g_event.set()
156+
157+
def _run(self) -> None:
158+
self._initiate_connection()
159+
160+
while True:
161+
data = self._sock.recv(65535)
162+
if not data:
163+
break
164+
165+
h2_events = self._connection.receive_data(data)
166+
for event in h2_events:
167+
if isinstance(event, events.RequestReceived):
168+
self._handle_request(event)
169+
elif isinstance(event, events.DataReceived):
170+
self._connection.reset_stream(event.stream_id)
171+
elif isinstance(event, events.WindowUpdated):
172+
self._handle_window_update(event)
173+
174+
data_to_send = self._connection.data_to_send()
175+
if data_to_send:
176+
self._sock.sendall(data_to_send)
177+
178+
179+
if __name__ == '__main__':
180+
files_dir = sys.argv[1] if len(sys.argv) > 1 else f'{Path().cwd()}'
181+
server = StreamServer(('127.0.0.1', 8080), partial(H2Worker, source_dir=files_dir),
182+
ssl_context=get_http2_tls_context())
183+
try:
184+
server.serve_forever()
185+
except KeyboardInterrupt:
186+
server.close()

examples/gevent/localhost.crt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDVDCCAjwCCQC1RoAIsDX89zANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQGEwJG
3+
UjEWMBQGA1UECAwNSWxlLWRlLUZyYW5jZTEOMAwGA1UEBwwFUGFyaXMxITAfBgNV
4+
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0
5+
MB4XDTE5MDgzMDAzNDcwN1oXDTIwMDgyOTAzNDcwN1owbDELMAkGA1UEBhMCRlIx
6+
FjAUBgNVBAgMDUlsZS1kZS1GcmFuY2UxDjAMBgNVBAcMBVBhcmlzMSEwHwYDVQQK
7+
DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC
8+
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANexqhW+b7Mx2lj5csv7Uq7T
9+
St6RWEocEUtuwBUsQ3VGOUTjN0dRxGeeZdDvZYrVsKhMwT1EeoXYlleQJPSEgW1d
10+
E5Mx6t4okWtG5D8YbIDMLRqLMpOqGSH1VZDU6l9ZN2UCTBtIVZN+Mg/q36cQOkwE
11+
Rp+BBiOU4dgKKms5d5QfOi3wPNEdeU0z77qxOuI9WmnMxvxM+vySkt2mHV/ptW4w
12+
XZDZ9QC/IHhXhjkSlQuL/TUptJ2UtlEXtn5NcNAWROl7xTMVHfIiFVW4oW39KIrA
13+
zGH5qYlJG/gUhJBTU7K5N5J1c/Y3FyNIeOgHMQr6MnKugV7JyOZYxjXGgdXOUNMC
14+
AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAUuGdTOWJ0SspGs6zU50lt+GxXo5xO8HE
15+
y7RiOnorJVE23dgVAyaDDcnIfsF+usobACHRlqirn2jvY5A7TIEM7Wlw5c1zrCAW
16+
bDzPJS0h6Hub05WQQmU734Ro1et7WFUrFRlDQUHJADgEAXRhXlm2Uk7Ay1PlCIz9
17+
802neyhWErL6bb7zxV5fWTu7RA3XUAX2ZrYO3XLhcsK7SnpT0whoBI06c1GxDkgV
18+
wFeYv8PBH5iQWhIft8EeTrNvNa+dusnppuM21sWWzItRnFef+ATixdDGIpDitAfs
19+
cfIUalb3BBdn4PXamN7EhWzHJlcME1cerjQ4YoEynWmT2blk1XUg9A==
20+
-----END CERTIFICATE-----

examples/gevent/localhost.key

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDXsaoVvm+zMdpY
3+
+XLL+1Ku00rekVhKHBFLbsAVLEN1RjlE4zdHUcRnnmXQ72WK1bCoTME9RHqF2JZX
4+
kCT0hIFtXROTMereKJFrRuQ/GGyAzC0aizKTqhkh9VWQ1OpfWTdlAkwbSFWTfjIP
5+
6t+nEDpMBEafgQYjlOHYCiprOXeUHzot8DzRHXlNM++6sTriPVppzMb8TPr8kpLd
6+
ph1f6bVuMF2Q2fUAvyB4V4Y5EpULi/01KbSdlLZRF7Z+TXDQFkTpe8UzFR3yIhVV
7+
uKFt/SiKwMxh+amJSRv4FISQU1OyuTeSdXP2NxcjSHjoBzEK+jJyroFeycjmWMY1
8+
xoHVzlDTAgMBAAECggEBAKqtYIxyNAtVMJVlVmyJBAVpFv6FfpquGRVans5iR0L+
9+
fYTAU1axIjxoP+MT/ILe0zpp+iNUE6vkFhtV6Zg/Xfc/RqUcQ+DlsyRzZVt0JS/J
10+
4Qr3CN+GIvsXGk1P3eHzQ/0+0yBnnafnnQ+xaKbXFXpfi87dlxEC169PY/+S6see
11+
dcPYw8LB8rI+mIIPvM/V2VtobZb9BFUsN4Dq8H1tRR97ST4TRbwov66o17Fvn5ww
12+
mXUwHjhdgxaJLtxJwppMhhSuST64mwoNY8XNWE0PlaB5jxKCNuWYYurjEJLJcBa7
13+
3lYadsRTucoiRbsTcqpivsa3KWxNRJcZERWVN4LVQvkCgYEA+E5zgVHbKfjCGs9x
14+
Xv1uVLjpdsh2S/7Zkx95vc4rBoLe8ii1uSpcLHytB7F5bPZV3Tiivu8OpW3E+8n6
15+
8mxQHSomSioxTE+xXF4JMf5XY2l9Tvz/60mo/dxk9Yo9k79OIJDUJrnpc18iYtRp
16+
B3X2g7JfqpT/RbG0bs2YVa/R3z8CgYEA3mCIm0Y0b6mpTxQNk6fcH2YXe1mdRp5Y
17+
9O+wVTNwmwB2vZZVm/K/wmOstsO1P+/PzwJ/WcBV9TlLlZRxxs/yNNU8NM7+7l3O
18+
e0ucCNRqhi9P19SA7l18FroOtOv2DatvXpNJTM6fXgE7dPQm5NKrNKK6nv03OUzQ
19+
BLLBBtzv/W0CgYEA6LU9cvkwGQnVgCLh8VA6UpRp2LTOiTJy3nslMUlC8Xs9Tl3w
20+
0XRtphPCZe9iCUhj+EvX2nFYnJlff0owMXppKqwR7nfUc9xMMHDA1WW0qKp4kcpy
21+
XiROiHxA8g144DruEX8qFJEvxLxoEY9YT3GycoJ9PfUduEdu/lkYZ1W7rykCgYB0
22+
mw/mw9hpGQDzu2MnIuUU/dagUqxaxFuHDExdUNziGkspPLRlUtPknZmKOHNJNHm2
23+
ZevbZzRrowCUTcOfaZjqxUmNs2EQItZL5qjKJIA7HoHyfbahxxlzXVqq2fQq1NNQ
24+
N1E/WjVM+L5xpDjk0eb+cboD9mlHvZRycj0vWRjqvQKBgQDP1xotkMqYwnMnpoRQ
25+
e3bqhIQFeoqIehsl2e/F+5yZ43plvEXavxshNJS3gGr1MflexcjqEaiHdfdK84zL
26+
mJuPYn0Uz5fP336aepzVsxK78tW4ilZri2mPbpBYgJskc6Ud7ue5zhBtuQnu5rV4
27+
zcuL1QjSQA+KW88b4DU/Usp6Eg==
28+
-----END PRIVATE KEY-----

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3-
import codecs
43
import os
54
import re
65
import sys

0 commit comments

Comments
 (0)