diff --git a/.gitignore b/.gitignore index de0042b1..f16e33a2 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ doc/tutorial-hpack.rst doc/python-apiref.rst doc/building-android-binary.rst doc/asio_http2.h.rst +doc/asio_http2_server.h.rst +doc/asio_http2_client.h.rst doc/libnghttp2_asio.rst doc/contribute.rst python/setup.py diff --git a/README.rst b/README.rst index 9f480090..e6a7849f 100644 --- a/README.rst +++ b/README.rst @@ -1054,6 +1054,58 @@ HTTP/2 server looks like this: } } +Here is the sample code for client API use: + +.. code-block:: cpp + + #include + + #include + + using boost::asio::ip::tcp; + + using namespace nghttp2::asio_http2; + using namespace nghttp2::asio_http2::client; + + int main(int argc, char *argv[]) { + boost::system::error_code ec; + boost::asio::io_service io_service; + + // connect to localhost:3000 + session sess(io_service, "localhost", "3000"); + + sess.on_connect([&sess](tcp::resolver::iterator endpoint_it) { + boost::system::error_code ec; + + auto req = sess.submit(ec, "GET", "http://localhost:3000/"); + + req->on_response([](const response &res) { + // print status code and response header fields. + std::cerr << "HTTP/2 " << res.status_code() << std::endl; + for (auto &kv : res.header()) { + std::cerr << kv.first << ": " << kv.second.value << "\n"; + } + std::cerr << std::endl; + + res.on_data([](const uint8_t *data, std::size_t len) { + std::cerr.write(reinterpret_cast(data), len); + std::cerr << std::endl; + }); + }); + + req->on_close([&sess](uint32_t error_code) { + // shutdown session after first request was done. + sess.shutdown(); + }); + }); + + sess.on_error([](const boost::system::error_code &ec) { + std::cerr << "error: " << ec.message() << std::endl; + }); + + io_service.run(); + } + For more details, see the documentation of libnghttp2_asio. Python bindings diff --git a/configure.ac b/configure.ac index 5311cdd4..f8b1de11 100644 --- a/configure.ac +++ b/configure.ac @@ -660,6 +660,8 @@ AC_CONFIG_FILES([ doc/nghttp2.h.rst doc/nghttp2ver.h.rst doc/asio_http2.h.rst + doc/asio_http2_server.h.rst + doc/asio_http2_client.h.rst doc/contribute.rst contrib/Makefile ]) diff --git a/doc/asio_http2_client.h.rst.in b/doc/asio_http2_client.h.rst.in new file mode 100644 index 00000000..756f00f9 --- /dev/null +++ b/doc/asio_http2_client.h.rst.in @@ -0,0 +1,5 @@ +asio_http2_client.h +=================== + +.. literalinclude:: @top_srcdir@/src/includes/nghttp2/asio_http2_client.h + :language: cpp diff --git a/doc/asio_http2_server.h.rst.in b/doc/asio_http2_server.h.rst.in new file mode 100644 index 00000000..e7c14344 --- /dev/null +++ b/doc/asio_http2_server.h.rst.in @@ -0,0 +1,5 @@ +asio_http2_server.h +=================== + +.. literalinclude:: @top_srcdir@/src/includes/nghttp2/asio_http2_server.h + :language: cpp diff --git a/doc/sources/index.rst b/doc/sources/index.rst index 69b94d05..1c583a7f 100644 --- a/doc/sources/index.rst +++ b/doc/sources/index.rst @@ -33,6 +33,8 @@ Contents: python-apiref nghttp2.h nghttp2ver.h + asio_http2_server.h + asio_http2_client.h asio_http2.h Source Issues diff --git a/doc/sources/libnghttp2_asio.rst b/doc/sources/libnghttp2_asio.rst index bf544b17..36f23bc4 100644 --- a/doc/sources/libnghttp2_asio.rst +++ b/doc/sources/libnghttp2_asio.rst @@ -4,7 +4,7 @@ libnghttp2_asio: High level HTTP/2 C++ library libnghttp2_asio is C++ library built on top of libnghttp2 and provides high level abstraction API to build HTTP/2 applications. It depends on Boost::ASIO library and OpenSSL. Currently libnghttp2_asio -provides server side API. +provides server and client side API. libnghttp2_asio is not built by default. Use ``--enable-asio-lib`` configure flag to build libnghttp2_asio. The required Boost libraries @@ -14,42 +14,54 @@ are: * Boost::System * Boost::Thread -To use libnghttp2_asio, first include following header file: +We have 3 header files for this library: -.. code-block:: cpp +* :doc:`asio_http2_server.h` +* :doc:`asio_http2_client.h` +* :doc:`asio_http2.h` - #include - -Also take a look at that header file :doc:`asio_http2.h`. +asio_http2.h is included from the other two files. Server API ---------- +To use server API, first include following header file: + +.. code-block:: cpp + + #include + +Also take a look at that header file :doc:`asio_http2_server.h`. + Server API is designed to build HTTP/2 server very easily to utilize C++11 anonymous function and closure. The bare minimum example of HTTP/2 server looks like this: .. code-block:: cpp - #include - using namespace nghttp2::asio_http2; using namespace nghttp2::asio_http2::server; int main(int argc, char *argv[]) { + boost::system::error_code ec; http2 server; - server.listen("*", 3000, [](const std::shared_ptr &req, - const std::shared_ptr &res) { - res->write_head(200); - res->end("hello, world"); + server.handle("/", [](const request &req, const response &res) { + res.write_head(200); + res.end("hello, world\n"); }); + + if (server.listen_and_serve(ec, "localhost", "3000")) { + std::cerr << "error: " << ec.message() << std::endl; + } } First we instantiate ``nghttp2::asio_http2::server::http2`` object. -Then call ``nghttp2::asio_http2::server::http2::listen`` function with -address and port to listen to and callback function, namely "request -callback", invoked when request arrives. +``nghttp2::asio_http2::server::http2::handle`` function registers +pattern and its handler function. In this example, we register "/" as +pattern, which matches all requests. Then call +``nghttp2::asio_http2::server::http2::listen_and_serve`` function with +address and port to listen to. The ``req`` and ``res`` represent HTTP request and response respectively. ``nghttp2::asio_http2_::server::response::write_head`` @@ -61,6 +73,10 @@ send. ``nghttp2::asio_http2::server::response::end`` sends responde body. In the above example, we send string "hello, world". +The life time of req and res object ends after the callback set by +``nghttp2::asio_http2::server::response::on_close`` function. +Application must not use those objects after this call. + Serving static files and enabling SSL/TLS +++++++++++++++++++++++++++++++++++++++++ @@ -69,40 +85,47 @@ SSL/TLS. .. code-block:: cpp - #include + #include using namespace nghttp2::asio_http2; using namespace nghttp2::asio_http2::server; int main(int argc, char *argv[]) { + boost::system::error_code ec; + boost::asio::ssl::context tls(boost::asio::ssl::context::sslv23); + + tls.use_private_key_file("server.key", boost::asio::ssl::context::pem); + tls.use_certificate_chain_file("server.crt"); + + configure_tls_context_easy(ec, tls); + http2 server; - server.tls("server.key", "server.crt"); - - server.listen("*", 3000, [](const std::shared_ptr &req, - const std::shared_ptr &res) { - if (req->path() == "/" || req->path() == "/index.html") { - res->write_head(200); - res->end(file_reader("index.html")); - } else { - res->write_head(404); - res->end("404" - "404 Not Found"); - } + server.handle("/index.html", [](const request &req, const response &res) { + res.write_head(200); + res.end(file_generator("index.html")); }); + + if (server.listen_and_serve(ec, tls, "localhost", "3000")) { + std::cerr << "error: " << ec.message() << std::endl; + } } -Specifying path to private key file and certificate file in -``nghttp2::asio_http2::server::http2::tls`` will enable SSL/TLS. Both -files must be in PEM format. +We first create ``boost::asio::ssl::context`` object and set path to +private key file and certificate file. +``nghttp2::asio_http2::server::configure_tls_context_easy`` function +configures SSL/TLS context object for HTTP/2 server use, including NPN +callbacks. -In the above example, if request path is either "/" or "/index.html", -we serve index.html file in the current working directory. +In the above example, if request path is "/index.html", we serve +index.html file in the current working directory. ``nghttp2::asio_http2::server::response::end`` has overload to take -function of type ``nghttp2::asio_http2::read_cb`` and application pass -its implementation to generate response body. For the convenience, -libnghttp2_asio library provides ``nghttp2::asio_http2::file_reader`` -function to generate function to server static file. +function of type ``nghttp2::asio_http2::generator_cb`` and application +pass its implementation to generate response body. For the +convenience, libnghttp2_asio library provides +``nghttp2::asio_http2::file_generator`` function to generate function +to server static file. If other resource is requested, server +automatically responds with 404 status code. Server push +++++++++++ @@ -111,44 +134,56 @@ Server push is also supported. .. code-block:: cpp - #include + #include using namespace nghttp2::asio_http2; using namespace nghttp2::asio_http2::server; int main(int argc, char *argv[]) { + boost::system::error_code ec; + boost::asio::ssl::context tls(boost::asio::ssl::context::sslv23); + + tls.use_private_key_file("server.key", boost::asio::ssl::context::pem); + tls.use_certificate_chain_file("server.crt"); + + configure_tls_context_easy(ec, tls); + http2 server; - server.tls("server.key", "server.crt"); + std::string style_css = "h1 { color: green; }"; - server.listen("*", 3000, [](const std::shared_ptr &req, - const std::shared_ptr &res) { - if (req->path() == "/") { - req->push("GET", "/my.css"); + server.handle("/", [&style_css](const request &req, const response &res) { + boost::system::error_code ec; + auto push = res.push(ec, "GET", "/style.css"); + push->write_head(200); + push->end(style_css); - res->write_head(200); - res->end(file_reader("index.html")); - - return; - } - - if (req->path() == "/my.css") { - res->write_head(200); - res->end(file_reader("my.css")); - - return; - } - - res->write_head(404); - res->end("404" - "404 Not Found"); + res.write_head(200); + res.end(R"( + + HTTP/2 FTW + +

This should be green

+ + )"); }); + + server.handle("/style.css", + [&style_css](const request &req, const response &res) { + res.write_head(200); + res.end(style_css); + }); + + if (server.listen_and_serve(ec, tls, "localhost", "3000")) { + std::cerr << "error: " << ec.message() << std::endl; + } } -When client requested "/", we push "/my.css". To push resource, call -``nghttp2::asio_http2::server::request::push`` function with desired -method and path. Later, the callback will be called with the pushed -resource "/my.css". +When client requested any resource other than "/style.css", we push +"/style.css". To push resource, call +``nghttp2::asio_http2::server::response::push`` function with desired +method and path. It returns another response object and use its +functions to send push response. Enable multi-threading ++++++++++++++++++++++ @@ -164,65 +199,225 @@ desired number of threads: // Use 4 native threads server.num_threads(4); -Run blocking tasks in background thread -+++++++++++++++++++++++++++++++++++++++ +Client API +---------- -The request callback is called in the same thread where HTTP request -is handled. And many connections shares the same thread, we cannot -directly run blocking tasks in request callback. - -To run blocking tasks, use -``nghttp2::asio_http2::server::request::run_task``. The passed -callback will be executed in the different thread from the thread -where request callback was executed. So application can perform -blocking task there. The example follows: +To use client API, first include following header file: .. code-block:: cpp - #include - #include + #include + +Also take a look at that header file :doc:`asio_http2_client.h`. + +Here is the sample client code to access HTTP/2 server and print out +response header fields and response body to the console screen: + +.. code-block:: cpp + + #include + + #include + + using boost::asio::ip::tcp; using namespace nghttp2::asio_http2; - using namespace nghttp2::asio_http2::server; + using namespace nghttp2::asio_http2::client; int main(int argc, char *argv[]) { - http2 server; + boost::system::error_code ec; + boost::asio::io_service io_service; - server.num_concurrent_tasks(16); + // connect to localhost:3000 + session sess(io_service, "localhost", "3000"); - server.listen("*", 3000, [](const std::shared_ptr &req, - const std::shared_ptr &res) { - req->run_task([res](channel &channel) { - // executed in different thread than the thread where - // request callback was executed. + sess.on_connect([&sess](tcp::resolver::iterator endpoint_it) { + boost::system::error_code ec; - // using res directly here is not safe. Capturing it by - // value is safe because it is std::shared_ptr. + auto req = sess.submit(ec, "GET", "http://localhost:3000/"); - sleep(1); + req->on_response([](const response &res) { + // print status code and response header fields. + std::cerr << "HTTP/2 " << res.status_code() << std::endl; + for (auto &kv : res.header()) { + std::cerr << kv.first << ": " << kv.second.value << "\n"; + } + std::cerr << std::endl; - channel.post([res]() { - // executed in the same thread where request callback - // was executed. - res->write_head(200); - res->end("hello, world"); - }); - }); + res.on_data([](const uint8_t *data, std::size_t len) { + std::cerr.write(reinterpret_cast(data), len); + std::cerr << std::endl; + }); + }); + + req->on_close([&sess](uint32_t error_code) { + // shutdown session after first request was done. + sess.shutdown(); + }); }); + + sess.on_error([](const boost::system::error_code &ec) { + std::cerr << "error: " << ec.message() << std::endl; + }); + + io_service.run(); } -First we set the number of background threads which run tasks. By -default it is set to 1. In this example, we set it to 16, so at most -16 tasks can be executed concurrently without blocking handling new -requests. +``nghttp2::asio_http2::client::session`` object takes +``boost::asio::io_service`` object and remote server address. When +connection is made, the callback function passed to +``nghttp2::asio_http2::client::on_connect`` is invoked with connected +address as its paramter. After this callback call, use +``nghttp2::asio_http2::session::submit`` to send request to the +server. You can submit multiple requests at once without waiting for +the completion of previous request. -We call ``req->run_task()`` to execute task in background thread. In -the passed callback, we just simply sleeps 1 second. After sleep is -over, we schedule another callback to send response to the client. -Since the callback passed to ``req->run_task()`` is executed in the -different thread from the thread where request callback is called, -using ``req`` or ``res`` object directly there may cause undefined -behaviour. To avoid this issue, we can use -``nghttp2::asio_http2::channel::post`` by supplying a callback which -in turn get called in the same thread where request callback was -called. +The life time of req and res object ends after the callback set by +``nghttp2::asio_http2::server::request::on_close`` function. +Application must not use those objects after this call. + +Normally, client does not stop even after all requests are done unless +connection is lost. To stop client, call +``nghttp2::asio_http2::server::session::shutdown()``. + +Recieve server push and enable SSL/TLS +++++++++++++++++++++++++++++++++++++++ + +.. code-block:: cpp + + #include + + #include + + using boost::asio::ip::tcp; + + using namespace nghttp2::asio_http2; + using namespace nghttp2::asio_http2::client; + + int main(int argc, char *argv[]) { + boost::system::error_code ec; + boost::asio::io_service io_service; + + boost::asio::ssl::context tls(boost::asio::ssl::context::sslv23); + tls.set_default_verify_paths(); + // disabled to make development easier... + // tls_ctx.set_verify_mode(boost::asio::ssl::verify_peer); + configure_tls_context(ec, tls); + + // connect to localhost:3000 + session sess(io_service, tls, "localhost", "3000"); + + sess.on_connect([&sess](tcp::resolver::iterator endpoint_it) { + boost::system::error_code ec; + + auto req = sess.submit(ec, "GET", "http://localhost:3000/"); + + req->on_response([&sess](const response &res) { + std::cerr << "response received!" << std::endl; + res.on_data([&sess](const uint8_t *data, std::size_t len) { + std::cerr.write(reinterpret_cast(data), len); + std::cerr << std::endl; + }); + }); + + req->on_push([](const request &push) { + std::cerr << "push request received!" << std::endl; + push.on_response([](const response &res) { + std::cerr << "push response received!" << std::endl; + res.on_data([](const uint8_t *data, std::size_t len) { + std::cerr.write(reinterpret_cast(data), len); + std::cerr << std::endl; + }); + }); + }); + }); + + sess.on_error([](const boost::system::error_code &ec) { + std::cerr << "error: " << ec.message() << std::endl; + }); + + io_service.run(); + } + +The above sample code demonstrates how to enable SSL/TLS and receive +server push. Currently, +``nghttp2::asio_http2::client::configure_tls_context`` function setups +NPN callbacks for SSL/TLS context for HTTP/2 use. + +To receive server push, use +``nghttp2::asio_http2::client::request::on_push`` function to set +callback function which is invoked when server push request is +arrived. The callback function takes +``nghttp2::asio_http2::client::request`` object, which contains the +pushed request. To get server push response, set callback using +``nghttp2::asio_http2::client::request::on_response``. + +As stated in the previous section, client does not stop automatically +as long as HTTP/2 session is fine and connection is alive. We don't +call ``nghttp2::asio_http2::client::session::shutdown`` in this +example, so the program does not terminate after all responses are +received. Hit Ctrl-C to terminate the program. + +Multiple concurrent requests +++++++++++++++++++++++++++++ + +.. code-block:: cpp + + #include + + #include + + using boost::asio::ip::tcp; + + using namespace nghttp2::asio_http2; + using namespace nghttp2::asio_http2::client; + + int main(int argc, char *argv[]) { + boost::system::error_code ec; + boost::asio::io_service io_service; + + // connect to localhost:3000 + session sess(io_service, "localhost", "3000"); + + sess.on_connect([&sess](tcp::resolver::iterator endpoint_it) { + boost::system::error_code ec; + + auto printer = [](const response &res) { + res.on_data([](const uint8_t *data, std::size_t len) { + std::cerr.write(reinterpret_cast(data), len); + std::cerr << std::endl; + }); + }; + + std::size_t num = 3; + auto count = std::make_shared(num); + + for (std::size_t i = 0; i < num; ++i) { + auto req = sess.submit(ec, "GET", + "http://localhost:3000/" + std::to_string(i + 1)); + + req->on_response(printer); + req->on_close([&sess, count](uint32_t error_code) { + if (--*count == 0) { + // shutdown session after |num| requests were done. + sess.shutdown(); + } + }); + } + }); + + sess.on_error([](const boost::system::error_code &ec) { + std::cerr << "error: " << ec.message() << std::endl; + }); + + io_service.run(); + } + +Here is the sample to send 3 requests at once. Depending on the +server settings, these requests are processed out-of-order. In this +example, we have a trick to shutdown session after all requests were +done. We made ``count`` object which is shared pointer to int and is +initialized to 3. On each request closure (the invocation of the +callback set by ``nghttp2::asio_http2::client::request::on_close``), +we decrement the count. If count becomes 0, we are sure that all +requests have been done and initiate shutdown.