Socketless HTTP 2.
Mostly because libuv, and therefore libh2o, tends to segfault a lot when used from Python. Also, because why not?
make obj/libcno.a
Just read core.h. And common.h, for buffers and error handling. And hpack.h for headers.
Basically, you create a cno_connection_t
, then follow a simple
chain of cno_init
-> connect some callbacks -> cno_begin
->
cno_consume
-> (repeat while I/O is still possible) ->
cno_eof
-> cno_fini
, skipping to the last step if
anything returns an error and using cno_write_head
+ cno_write_data
or
cno_write_push
or cno_write_reset
to send some stuff of your own.
pip3 install cffi
pip3 install git+https://github.com/pyos/libcno
C constant | Python constant |
---|---|
CNO_CLIENT |
cno.raw.CNO_CLIENT |
etc. | ... |
cno.raw.Connection
is an almost complete 1:1 mapping to C API functions.
C function | cno.raw.Connection method |
---|---|
cno_init(c, CNO_SERVER) |
c = cno.raw.Connection(server=True) |
cno_fini(c) |
del c |
cno_begin(c, CNO_HTTP2) |
c.connection_made(is_http2=True) |
cno_eof(c) |
c.connection_lost() |
cno_consume(c, data, length) |
c.data_received(data) |
cno_next_stream(c) |
c.next_stream |
cno_write_head(c, stream, msg, final) |
c.write_head(stream, code, method, path, headers, final) |
cno_write_push(c, stream, msg) |
c.write_push(stream, method, path, headers) |
cno_write_data(c, stream, data, length, final) |
c.write_data(stream, data, final) |
cno_write_reset(c, stream, code) |
c.write_reset(stream, code) |
Event receivers must be defined as methods of Connection
subclasses.
C event | cno.raw.Connection method |
---|---|
on_writev(iov, iovcnt) |
def on_writev(self, chunks) |
on_stream_start(stream) |
def on_stream_start(self, stream) |
on_stream_end(stream, code, side) |
def on_stream_end(self, stream, code, side) |
on_flow_increase(stream) |
def on_flow_increase(self, stream) |
on_message_head(stream, msg) |
def on_message_head(self, stream, code, method, path, headers) |
on_message_tail(stream, msg) |
def on_message_tail(self, stream, trailers) |
on_message_data(stream, data, length) |
def on_message_data(self, stream, data) |
on_message_push(stream, msg, parent) |
def on_message_push(self, stream, parent, method, path, headers) |
On Python 3.5+, higher-level asyncio bindings are also available. Server:
async def handle(request: cno.Request):
request.method # :: str
request.path # :: str
request.headers # :: [(str, str)]
request.conn # :: cno.Server -- `protocol` (below)
request.payload # :: asyncio.StreamReader
# Pushed resources inherit :authority and :scheme from the request unless overriden.
request.push('GET', '/index.css', [('x-extra-header', 'value')])
if all_data_is_available:
await request.respond(200, [('content-length', '4')], b'!!!\n')
else:
# `Channel` is a subclass of `asyncio.Queue`.
channel = cno.Channel(max_buffered_chunks, loop=request.conn.loop)
await channel.put(b'!!!') # this should preferably be done in a separate
await channel.put(b'\n') # coroutine, naturally.
channel.close()
# Or you can use any async iterable instead.
await request.respond(200, [], channel)
make_protocol = lambda: cno.Server(event_loop, handle)
# When using TLS, don't forget to tell clients you support HTTP 2:
# ssl_context.set_alpn_protocols(['h2', 'http/1.1'])
# ssl_context.set_npn_protocols(['h2', 'http/1.1'])
# server = await event_loop.create_server(make_protocol, '', 8000, ssl=ssl_context)
Client:
client = await cno.connect(event_loop, 'https://example.com/')
client.loop # :: asyncio.BaseEventLoop -- `event_loop`
client.transport # :: asyncio.Transport
client.is_http2 # :: bool
client.scheme # :: str -- https
client.authority # :: str -- example.com
response = await client.request('GET', '/', [('user-agent', 'libcno/0.1')]) # cno.Response
response.code # :: int
response.headers # :: [(str, str)]
response.payload # :: asyncio.StreamReader
async for push in response.pushed:
push.method # :: str
push.path # :: str
push.headers # :: [(str, str)]
await push.response # :: cno.Response
# or push.cancel()
response = await client.request('POST', '/whatever', [], b'payload')
# `request`, like `respond`, also accepts `cno.Channel`s as payload.
client.close()
# `cno.connect` automatically sets up a default SSL context and creates
# a TCP connection. To simply create an `asyncio.Protocol`:
client = cno.Client(event_loop, authority='example.com', scheme='https')
# A shorthand for `cno.connect` followed by `client.request`:
response = await cno.request(event_loop, 'GET', 'https://example.com/path', ...)
response.conn.close()