From 74891560ae6be1066a3c8edf4fc870bdf0b0a839 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sat, 17 Jun 2017 20:17:01 -0700 Subject: [PATCH] Refactor WebSocket, HTTP examples: fix #297, fix #488 * Errors are checked and reported * More comments explaining what is going on * The connection is gracefully closed WebSocket: * Messages are drained before closing --- CHANGELOG.md | 1 + README.md | 72 +----------- example/http-client/http_client.cpp | 60 ++++++++-- example/ssl-http-client/ssl_http_client.cpp | 81 +++++++++----- .../ssl_websocket_client.cpp | 103 ++++++++++++++---- example/websocket-client/websocket_client.cpp | 82 ++++++++++++-- 6 files changed, 260 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 866f0ce2..b53420de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Version 61: * Add message::header_part() * Tidy up names in error categories * Flush the output stream in the example +* Clean close in Secure WebSocket client API Changes: diff --git a/README.md b/README.md index 46c05de2..eac10c81 100644 --- a/README.md +++ b/README.md @@ -144,77 +144,9 @@ The files in the repository are laid out thusly: ## Usage These examples are complete, self-contained programs that you can build -and run yourself (they are in the `examples` directory). +and run yourself (they are in the `example` directory). -Example WebSocket program: -```C++ -#include -#include -#include -#include -#include - -int main() -{ - // Normal boost::asio setup - std::string const host = "echo.websocket.org"; - boost::asio::io_service ios; - boost::asio::ip::tcp::resolver r{ios}; - boost::asio::ip::tcp::socket sock{ios}; - boost::asio::connect(sock, - r.resolve(boost::asio::ip::tcp::resolver::query{host, "80"})); - - // WebSocket connect and send message using beast - beast::websocket::stream ws{sock}; - ws.handshake(host, "/"); - ws.write(boost::asio::buffer(std::string("Hello, world!"))); - - // Receive WebSocket message, print and close using beast - beast::multi_buffer b; - beast::websocket::opcode op; - ws.read(op, b); - ws.close(beast::websocket::close_code::normal); - std::cout << beast::buffers(b.data()) << "\n"; -} -``` - -Example HTTP program: -```C++ -#include -#include -#include -#include -#include -#include - -int main() -{ - // Normal boost::asio setup - std::string const host = "www.example.com"; - boost::asio::io_service ios; - boost::asio::ip::tcp::resolver r{ios}; - boost::asio::ip::tcp::socket sock{ios}; - boost::asio::connect(sock, - r.resolve(boost::asio::ip::tcp::resolver::query{host, "http"})); - - // Send HTTP request using beast - beast::http::request req; - req.method(beast::http::verb::get); - req.target("/"); - req.version = 11; - req.insert(beast::http::field::host, host + ":" + - boost::lexical_cast(sock.remote_endpoint().port())); - req.insert(beast::http::field::user_agent, "Beast"); - req.prepare(); - beast::http::write(sock, req); - - // Receive and print HTTP response using beast - beast::flat_buffer b; - beast::http::response res; - beast::http::read(sock, b, res); - std::cout << res << std::endl; -} -``` +http://vinniefalco.github.io/beast/beast/quick_start.html ## License diff --git a/example/http-client/http_client.cpp b/example/http-client/http_client.cpp index 2982d1d7..9ce9393d 100644 --- a/example/http-client/http_client.cpp +++ b/example/http-client/http_client.cpp @@ -11,35 +11,75 @@ #include #include #include +#include #include #include int main() { - // Normal boost::asio setup - std::string const host = "www.example.com"; + // A helper for reporting errors + auto const fail = + [](std::string what, beast::error_code ec) + { + std::cerr << what << ": " << ec.message() << std::endl; + std::cerr.flush(); + return EXIT_FAILURE; + }; + + boost::system::error_code ec; + + // Set up an asio socket boost::asio::io_service ios; boost::asio::ip::tcp::resolver r{ios}; boost::asio::ip::tcp::socket sock{ios}; - boost::asio::connect(sock, - r.resolve(boost::asio::ip::tcp::resolver::query{host, "http"})); - // Send HTTP request using beast + // Look up the domain name + std::string const host = "www.example.com"; + auto const lookup = r.resolve(boost::asio::ip::tcp::resolver::query{host, "http"}, ec); + if(ec) + return fail("resolve", ec); + + // Make the connection on the IP address we get from a lookup + boost::asio::connect(sock, lookup, ec); + if(ec) + return fail("connect", ec); + + // Set up an HTTP GET request message beast::http::request req; req.method(beast::http::verb::get); req.target("/"); req.version = 11; - req.insert(beast::http::field::host, host + ":" + + req.set(beast::http::field::host, host + ":" + boost::lexical_cast(sock.remote_endpoint().port())); - req.insert(beast::http::field::user_agent, "Beast"); + req.set(beast::http::field::user_agent, "Beast"); req.prepare(); - beast::http::write(sock, req); - // Receive and print HTTP response using beast + // Write the HTTP request to the remote host + beast::http::write(sock, req, ec); + if(ec) + return fail("write", ec); + + // This buffer is used for reading and must be persisted beast::flat_buffer b; + + // Declare a container to hold the response beast::http::response res; - beast::http::read(sock, b, res); + + // Read the response + beast::http::read(sock, b, res, ec); + if(ec) + return fail("read", ec); + + // Write the message to standard out std::cout << res << std::endl; + + // Gracefully close the socket + sock.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); + if(ec) + return fail("shutdown", ec); + + // If we get here then the connection is closed gracefully + return EXIT_SUCCESS; } //] diff --git a/example/ssl-http-client/ssl_http_client.cpp b/example/ssl-http-client/ssl_http_client.cpp index 26758461..74d1d499 100644 --- a/example/ssl-http-client/ssl_http_client.cpp +++ b/example/ssl-http-client/ssl_http_client.cpp @@ -15,48 +15,77 @@ int main() { - using boost::asio::connect; - using socket = boost::asio::ip::tcp::socket; - using resolver = boost::asio::ip::tcp::resolver; - using io_service = boost::asio::io_service; - namespace ssl = boost::asio::ssl; + // A helper for reporting errors + auto const fail = + [](std::string what, beast::error_code ec) + { + std::cerr << what << ": " << ec.message() << std::endl; + std::cerr.flush(); + return EXIT_FAILURE; + }; + + boost::system::error_code ec; // Normal boost::asio setup - std::string const host = "localhost"; - io_service ios; - resolver r{ios}; - socket sock{ios}; - connect(sock, r.resolve(resolver::query{host, "1007"})); + boost::asio::io_service ios; + boost::asio::ip::tcp::resolver r{ios}; + boost::asio::ip::tcp::socket sock{ios}; + + // Look up the domain name + std::string const host = "www.example.com"; + auto const lookup = r.resolve(boost::asio::ip::tcp::resolver::query{host, "https"}, ec); + if(ec) + return fail("resolve", ec); + + // Make the connection on the IP address we get from a lookup + boost::asio::connect(sock, lookup, ec); + if(ec) + return fail("connect", ec); + + // Wrap the now-connected socket in an SSL stream + boost::asio::ssl::context ctx{boost::asio::ssl::context::sslv23}; + boost::asio::ssl::stream stream{sock, ctx}; + stream.set_verify_mode(boost::asio::ssl::verify_none); // Perform SSL handshaking - ssl::context ctx{ssl::context::sslv23}; - ssl::stream stream{sock, ctx}; - stream.set_verify_mode(ssl::verify_none); - stream.handshake(ssl::stream_base::client); + stream.handshake(boost::asio::ssl::stream_base::client, ec); + if(ec) + return fail("handshake", ec); - // Send HTTP request over SSL using Beast + // Set up an HTTP GET request message beast::http::request req; req.method(beast::http::verb::get); req.target("/"); req.version = 11; - req.insert(beast::http::field::host, host + ":" + + req.set(beast::http::field::host, host + ":" + boost::lexical_cast(sock.remote_endpoint().port())); - req.insert(beast::http::field::user_agent, "Beast"); + req.set(beast::http::field::user_agent, "Beast"); req.prepare(); - beast::http::write(stream, req); - // Receive and print HTTP response using Beast + // Write the HTTP request to the remote host + beast::http::write(stream, req, ec); + if(ec) + return fail("write", ec); + + // This buffer is used for reading and must be persisted beast::flat_buffer b; - beast::http::response resp; - beast::http::read(stream, b, resp); - std::cout << resp; + + // Declare a container to hold the response + beast::http::response res; + + // Read the response + beast::http::read(stream, b, res, ec); + if(ec) + return fail("read", ec); + + // Write the message to standard out + std::cout << res << std::endl; // Shut down SSL on the stream - boost::system::error_code ec; stream.shutdown(ec); if(ec && ec != boost::asio::error::eof) - std::cout << "error: " << ec.message(); + fail("ssl shutdown ", ec); - // Make sure everything is written before we leave main - std::cout.flush(); + // If we get here then the connection is closed gracefully + return EXIT_SUCCESS; } diff --git a/example/ssl-websocket-client/ssl_websocket_client.cpp b/example/ssl-websocket-client/ssl_websocket_client.cpp index f3427d6e..23034004 100644 --- a/example/ssl-websocket-client/ssl_websocket_client.cpp +++ b/example/ssl-websocket-client/ssl_websocket_client.cpp @@ -5,43 +5,102 @@ // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // -#include +#include #include #include #include #include +#include #include #include int main() { - using boost::asio::connect; - using socket = boost::asio::ip::tcp::socket; - using resolver = boost::asio::ip::tcp::resolver; - using io_service = boost::asio::io_service; - namespace ssl = boost::asio::ssl; + // A helper for reporting errors + auto const fail = + [](std::string what, beast::error_code ec) + { + std::cerr << what << ": " << ec.message() << std::endl; + std::cerr.flush(); + return EXIT_FAILURE; + }; - // Normal boost::asio setup + boost::system::error_code ec; + + // Set up an asio socket to connect to a remote host + boost::asio::io_service ios; + boost::asio::ip::tcp::resolver r{ios}; + boost::asio::ip::tcp::socket sock{ios}; + + // Look up the domain name std::string const host = "echo.websocket.org"; - io_service ios; - resolver r{ios}; - socket sock{ios}; - connect(sock, r.resolve(resolver::query{host, "https"})); + auto const lookup = r.resolve(boost::asio::ip::tcp::resolver::query{host, "https"}, ec); + if(ec) + return fail("resolve", ec); + + // Make the connection on the IP address we get from a lookup + boost::asio::connect(sock, lookup, ec); + if(ec) + return fail("connect", ec); + + // Wrap the now-connected socket in an SSL stream + using stream_type = boost::asio::ssl::stream; + boost::asio::ssl::context ctx{boost::asio::ssl::context::sslv23}; + stream_type stream{sock, ctx}; + stream.set_verify_mode(boost::asio::ssl::verify_none); // Perform SSL handshaking - using stream_type = ssl::stream; - ssl::context ctx{ssl::context::sslv23}; - stream_type stream{sock, ctx}; - stream.set_verify_mode(ssl::verify_none); - stream.handshake(ssl::stream_base::client); + stream.handshake(boost::asio::ssl::stream_base::client, ec); + if(ec) + return fail("ssl handshake", ec); - // Secure WebSocket connect and send message using Beast + // Now wrap the handshaked SSL stream in a websocket stream beast::websocket::stream ws{stream}; - ws.handshake(host, "/"); - ws.write(boost::asio::buffer("Hello, world!")); - // Receive Secure WebSocket message, print and close using Beast + // Perform the websocket handshake + ws.handshake(host, "/", ec); + if(ec) + return fail("handshake", ec); + + // Send a message + ws.write(boost::asio::buffer("Hello, world!"), ec); + if(ec) + return fail("write", ec); + + // This buffer will hold the incoming message beast::multi_buffer b; - ws.close(beast::websocket::close_code::normal); - std::cout << beast::buffers(b.data()) << "\n"; + + // Read the message into our buffer + ws.read(b, ec); + if(ec) + return fail("read", ec); + + // Send a "close" frame to the other end, this is a websocket thing + ws.close(beast::websocket::close_code::normal, ec); + if(ec) + return fail("close", ec); + + // The buffers() function helps print a ConstBufferSequence + std::cout << beast::buffers(b.data()) << std::endl; + + // WebSocket says that to close a connection you have + // to keep reading messages until you receive a close frame. + // Beast delivers the close frame as an error from read. + // + beast::drain_buffer drain; // Throws everything away efficiently + for(;;) + { + // Keep reading messages... + ws.read(drain, ec); + + // ...until we get the special error code + if(ec == beast::websocket::error::closed) + break; + + // Some other error occurred, report it and exit. + if(ec) + return fail("close", ec); + } + + return EXIT_SUCCESS; } diff --git a/example/websocket-client/websocket_client.cpp b/example/websocket-client/websocket_client.cpp index 2e6241d6..cbe1e8dd 100644 --- a/example/websocket-client/websocket_client.cpp +++ b/example/websocket-client/websocket_client.cpp @@ -10,29 +10,89 @@ #include #include #include +#include #include #include int main() { - // Normal boost::asio setup - std::string const host = "echo.websocket.org"; + // A helper for reporting errors + auto const fail = + [](std::string what, beast::error_code ec) + { + std::cerr << what << ": " << ec.message() << std::endl; + std::cerr.flush(); + return EXIT_FAILURE; + }; + + boost::system::error_code ec; + + // Set up an asio socket boost::asio::io_service ios; boost::asio::ip::tcp::resolver r{ios}; boost::asio::ip::tcp::socket sock{ios}; - boost::asio::connect(sock, - r.resolve(boost::asio::ip::tcp::resolver::query{host, "80"})); - // WebSocket connect and send message using beast + // Look up the domain name + std::string const host = "echo.websocket.org"; + auto const lookup = r.resolve(boost::asio::ip::tcp::resolver::query{host, "http"}, ec); + if(ec) + return fail("resolve", ec); + + // Make the connection on the IP address we get from a lookup + boost::asio::connect(sock, lookup, ec); + if(ec) + return fail("connect", ec); + + // Wrap the now-connected socket in a websocket stream beast::websocket::stream ws{sock}; - ws.handshake(host, "/"); - ws.write(boost::asio::buffer(std::string("Hello, world!"))); - // Receive WebSocket message, print and close using beast + // Perform the websocket handhskae + ws.handshake(host, "/", ec); + if(ec) + return fail("handshake", ec); + + // Send a message + ws.write(boost::asio::buffer(std::string("Hello, world!")), ec); + if(ec) + return fail("write", ec); + + // This buffer will hold the incoming message beast::multi_buffer b; - ws.read(b); - ws.close(beast::websocket::close_code::normal); - std::cout << beast::buffers(b.data()) << "\n"; + + // Read the message into our buffer + ws.read(b, ec); + if(ec) + return fail("read", ec); + + // Send a "close" frame to the other end, this is a websocket thing + ws.close(beast::websocket::close_code::normal, ec); + if(ec) + return fail("close", ec); + + // The buffers() function helps print a ConstBufferSequence + std::cout << beast::buffers(b.data()) << std::endl; + + // WebSocket says that to close a connection you have + // to keep reading messages until you receive a close frame. + // Beast delivers the close frame as an error from read. + // + beast::drain_buffer drain; // Throws everything away efficiently + for(;;) + { + // Keep reading messages... + ws.read(drain, ec); + + // ...until we get the special error code + if(ec == beast::websocket::error::closed) + break; + + // Some other error occurred, report it and exit. + if(ec) + return fail("close", ec); + } + + // If we get here the connection was cleanly closed + return EXIT_SUCCESS; } //]