From 89ac45e60a8e701bc84da3f7a7630b122d14fba6 Mon Sep 17 00:00:00 2001 From: Peter Thorson Date: Sun, 23 Feb 2014 09:34:34 -0600 Subject: [PATCH] additional work on app client tutorial --- tutorials/app_client_tutorial/chapter1.md | 93 ++++++++- tutorials/app_client_tutorial/chapter2.md | 14 ++ tutorials/app_client_tutorial/step4.cpp | 219 ++++++++++++++++++++++ tutorials/app_client_tutorial/step5.cpp | 216 +++++++++++++++++++++ 4 files changed, 535 insertions(+), 7 deletions(-) create mode 100644 tutorials/app_client_tutorial/chapter2.md create mode 100644 tutorials/app_client_tutorial/step4.cpp create mode 100644 tutorials/app_client_tutorial/step5.cpp diff --git a/tutorials/app_client_tutorial/chapter1.md b/tutorials/app_client_tutorial/chapter1.md index 4a72fbb6ae..7d4cdaf6ca 100644 --- a/tutorials/app_client_tutorial/chapter1.md +++ b/tutorials/app_client_tutorial/chapter1.md @@ -1,8 +1,10 @@ Utility Client Example Application ================================== -Initial Setup -------------- +Chapter 1: Initial Setup & Basics +--------------------------------- + +Setting up the basic types, opening and closing connections, sending and receiving messages. ### Step 1 @@ -129,7 +131,7 @@ m_endpoint.clear_access_channels(websocketpp::log::alevel::all); m_endpoint.clear_error_channels(websocketpp::log::elevel::all); ``` -Next, we initialize the transport system underlying the endpoint and sets it to perpetual mode. In perpetual mode the endpoint's processing loop will not exit automatically when it has no connections. This is important because we want this endpoint to remain active while our application is running and process requests for new WebSocket connections on demand as we need them. Both of these methods are specific to the asio transport. They will not be necessary or present in endpoints that use a non-asio config. +Next, we initialize the transport system underlying the endpoint and set it to perpetual mode. In perpetual mode the endpoint's processing loop will not exit automatically when it has no connections. This is important because we want this endpoint to remain active while our application is running and process requests for new WebSocket connections on demand as we need them. Both of these methods are specific to the asio transport. They will not be necessary or present in endpoints that use a non-asio config. ```cpp m_endpoint.init_asio(); m_endpoint.start_perpetual(); @@ -142,16 +144,16 @@ m_thread.reset(new websocketpp::lib::thread(&client::run, &m_endpoint)); #### Build -Now that our client endpoint template is actually instantiated a few more linker dependencies will show up. In particular, WebSocket clients require a cryptographically secure random number generator. WebSocket++ is able to use either boost_random or the C++11 standard library for this purpose. +Now that our client endpoint template is actually instantiated a few more linker dependencies will show up. In particular, WebSocket clients require a cryptographically secure random number generator. WebSocket++ is able to use either `boost_random` or the C++11 standard library for this purpose. Because this example also uses threads, if we do not have C++11 std::thread available we will need to include `boost_thread`. ##### Clang (C++98 & boost) -`clang++ step3.cpp -lboost_system -lboost_random` +`clang++ step3.cpp -lboost_system -lboost_random -lboost_thread` ##### Clang (C++11) `clang++ -std=c++0x -stdlib=libc++ step3.cpp -lboost_system -D_WEBSOCKETPP_CPP11_STL_` ##### G++ (C++98 & Boost) -`g++ step3.cpp -lboost_system -lboost_random` +`g++ step3.cpp -lboost_system -lboost_random -lboost_thread` ##### G++ v4.6+ (C++11) `g++ -std=c++0x step3.cpp -lboost_system -D_WEBSOCKETPP_CPP11_STL_` @@ -210,4 +212,81 @@ int main() { return 0; } -``` \ No newline at end of file +``` + +### Step 4 + +_Opening and closing WebSocket connections_ + +- Creating a connection +- terminology: `connection_ptr` and `connection_hdl` +- terminology: `error handling: exception vs ec` +- endpoint::connect +- endpoint::close +- terminology: WebSocket close codes +- terminology: registering handlers +- Setting an open, fail, and close handler + + +core websocket++ control flow. +A handshake, followed by a split into 2 independent control strands +- Handshake +-- use information specified before the call to endpoint::connect to construct a WebSocket + handshake request. +-- Pass the WebSocket handshake request to the transport policy. The transport policy + determines how to get these bytes to the endpoint playing the server role. Depending on + which transport policy your endpoint uses this method will be different. +-- Receive a handshake response from the underlying transport. This is parsed and checked + for conformance to RFC6455. If the validation fails, the fail handler is called. + Otherwise the open handler is called. +- At this point control splits into two separate strands. One that reads new bytes from + the transport policy on the incoming channle, the other that accepts new messages from + the local application for framing and writing to the outgoing transport channel. +- Read strand +-- Read and process new bytes from transport +-- If the bytes contain at least one complete message dispatch each message by calling the + appropriate handler. This is either the message handler for data messages, or + ping/pong/close handlers for each respective control message. If no handler is + registered for a particular message it is ignored. +-- Ask the transport layer for more bytes +- Write strand +-- Wait for messages from the application +-- Perform error checking on message input, +-- Frame message per RFC6455 +-- Queue message for sending +-- Pass all outstanding messages to the transport policy for output +-- When there are no messages left to send, return to waiting + +Important observations +Handlers run in line with library processing which has several implications applications should be aware of: +- + +#### Build + +There are no changes to the build instructions from step 3 + +#### Code so far + +### Step 5 + +_Sending and receiving messages_ + +- Sending a messages +- terminology: WebSocket opcodes, text vs binary messages +- Receiving a message + +### Step 6 + +_Intermediate level features_ + +- Subprotocol negotiation +- Setting and reading custom headers +- Ping and Pong +- Proxies? +- Setting user agent +- Setting Origin + +### Step 7 + +_Using TLS / Secure WebSockets_ + diff --git a/tutorials/app_client_tutorial/chapter2.md b/tutorials/app_client_tutorial/chapter2.md new file mode 100644 index 0000000000..9b35f84dd0 --- /dev/null +++ b/tutorials/app_client_tutorial/chapter2.md @@ -0,0 +1,14 @@ +Utility Client Example Application +================================== + +Chapter 2: Initial Setup & Basics +--------------------------------- + +Using intermediate level features + +- Subprotocol negotiation +- Setting and reading custom headers +- Ping and Pong + +### Step 1 + diff --git a/tutorials/app_client_tutorial/step4.cpp b/tutorials/app_client_tutorial/step4.cpp new file mode 100644 index 0000000000..3cbffd2c02 --- /dev/null +++ b/tutorials/app_client_tutorial/step4.cpp @@ -0,0 +1,219 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +typedef websocketpp::client client; + +class connection_metadata { +public: + connection_metadata(int id, websocketpp::connection_hdl hdl, std::string uri) + : m_id(id) + , m_hdl(hdl) + , m_status("Connecting") + , m_uri(uri) + , m_server("N/A") + {} + + connection_metadata() + : m_id(-1) {} + + void on_open(client * c, websocketpp::connection_hdl hdl) { + m_status = "Open"; + client::connection_ptr con = c->get_con_from_hdl(hdl); + m_server = con->get_response_header("Server"); + } + + void on_fail(client * c, websocketpp::connection_hdl hdl) { + m_status = "Failed"; + client::connection_ptr con = c->get_con_from_hdl(hdl); + m_error_reason = con->get_ec().message(); + } + + void on_close(client * c, websocketpp::connection_hdl hdl) { + m_status = "Closed"; + client::connection_ptr con = c->get_con_from_hdl(hdl); + std::stringstream s; + s << "close code: " << con->get_remote_close_code() << ", close reason: " << con->get_remote_close_reason(); + m_error_reason = s.str(); + } + + void set_error(std::string const & err) { + m_error_reason = err; + m_status = "Error"; + } + + int get_id() const { + return m_id; + } + + websocketpp::connection_hdl get_hdl() const { + return m_hdl; + } + + friend std::ostream & operator<< (std::ostream & out, connection_metadata const & data); +private: + int m_id; + websocketpp::connection_hdl m_hdl; + std::string m_status; + std::string m_uri; + std::string m_server; + std::string m_error_reason; +}; + +std::ostream & operator<< (std::ostream & out, connection_metadata const & data) { + if (data.m_id == -1) { + out << "> Invalid connection"; + } else { + out << "> URI: " << data.m_uri << "\n" + << "> Status: " << data.m_status << "\n" + << "> Remote Server: " << data.m_server << "\n" + << "> Error/close reason: " << data.m_error_reason; + } + + return out; +} + +class websocket_endpoint { +public: + websocket_endpoint () : m_next_id(0) { + m_endpoint.clear_access_channels(websocketpp::log::alevel::all); + m_endpoint.clear_error_channels(websocketpp::log::elevel::all); + + m_endpoint.init_asio(); + m_endpoint.start_perpetual(); + + m_thread.reset(new websocketpp::lib::thread(&client::run, &m_endpoint)); + } + + ~websocket_endpoint() { + m_endpoint.stop_perpetual(); + + for (con_list::const_iterator it = m_connection_list.begin(); it != m_connection_list.end(); ++it) { + websocketpp::lib::error_code ec; + m_endpoint.close(it->second.get_hdl(), websocketpp::close::status::going_away, "", ec); + if (ec) { + std::cout << "> Error closing connection " << it->second.get_id() << std::endl; + } + } + + m_thread->join(); + } + + // copy constructors + // assignment operator + + int connect(std::string const & uri) { + websocketpp::lib::error_code ec; + int new_id = m_next_id++; + + client::connection_ptr con = m_endpoint.get_connection(uri, ec); + + if (ec) { + std::cout << "> Connect initialization error: " << ec.message() << std::endl; + return -1; + } + + m_connection_list[new_id] = connection_metadata(new_id, con->get_handle(), uri); + + using websocketpp::lib::placeholders::_1; + using websocketpp::lib::bind; + con->set_open_handler(bind( + &connection_metadata::on_open, + &m_connection_list[new_id], + &m_endpoint, + ::_1 + )); + con->set_fail_handler(bind(&connection_metadata::on_fail,&m_connection_list[new_id],&m_endpoint,::_1)); + con->set_close_handler(bind(&connection_metadata::on_close,&m_connection_list[new_id],&m_endpoint,::_1)); + + m_endpoint.connect(con); + + return new_id; + } + + void close(int id, websocketpp::close::status::value code) { + websocketpp::lib::error_code ec; + + con_list::iterator metadata_it = m_connection_list.find(id); + if (metadata_it == m_connection_list.end()) { + std::cout << "> No connection found with id " << id << std::endl; + return; + } + + m_endpoint.close(metadata_it->second.get_hdl(),code, "", ec); + if (ec) { + std::cout << "> Error initiating close: " << ec.message() << std::endl; + } + } + + connection_metadata get_metadata(int id) const { + con_list::const_iterator metadata_it = m_connection_list.find(id); + if (metadata_it == m_connection_list.end()) { + return connection_metadata(); + } else { + return metadata_it->second; + } + } +private: + typedef std::map con_list; + + client m_endpoint; + websocketpp::lib::shared_ptr m_thread; + + con_list m_connection_list; + int m_next_id; +}; + +int main() { + bool done = false; + std::string input; + websocket_endpoint endpoint; + + while (!done) { + std::cout << "Enter Command: "; + std::getline(std::cin, input); + + if (input == "quit") { + done = true; + } else if (input == "help") { + std::cout + << "\nCommand List:\n" + << "connect \n" + << "close \n" + << "show \n" + << "help: Display this help text\n" + << "quit: Exit the program\n" + << std::endl; + } else if (input.substr(0,7) == "connect") { + int id = endpoint.connect(input.substr(8)); + std::cout << "> Created connection with id " << id << std::endl; + } else if (input.substr(0,5) == "close") { + int id = atoi(input.substr(6).c_str()); + + endpoint.close(id, websocketpp::close::status::normal); + } else if (input.substr(0,4) == "show") { + int id = atoi(input.substr(5).c_str()); + + std::cout << endpoint.get_metadata(id) << std::endl; + } else { + std::cout << "> Unrecognized Command" << std::endl; + } + } + + return 0; +} + +/* + +clang++ -std=c++11 -stdlib=libc++ -I/Users/zaphoyd/software/websocketpp/ -I/Users/zaphoyd/software/boost_1_55_0/ -D_WEBSOCKETPP_CPP11_STL_ step4.cpp /Users/zaphoyd/software/boost_1_55_0/stage/lib/libboost_system.a + +clang++ -I/Users/zaphoyd/software/websocketpp/ -I/Users/zaphoyd/software/boost_1_55_0/ step4.cpp /Users/zaphoyd/software/boost_1_55_0/stage/lib/libboost_system.a /Users/zaphoyd/software/boost_1_55_0/stage/lib/libboost_thread.a /Users/zaphoyd/software/boost_1_55_0/stage/lib/libboost_random.a +*/ \ No newline at end of file diff --git a/tutorials/app_client_tutorial/step5.cpp b/tutorials/app_client_tutorial/step5.cpp new file mode 100644 index 0000000000..7bfacf1682 --- /dev/null +++ b/tutorials/app_client_tutorial/step5.cpp @@ -0,0 +1,216 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +typedef websocketpp::client client; + +class connection_metadata { +public: + connection_metadata(int id, websocketpp::connection_hdl hdl,std::string uri) + : m_id(id) + , m_hdl(hdl) + , m_status("Connecting") + , m_uri(uri) + , m_server("N/A") + {} + + connection_metadata() + : m_id(-1) {} + + void on_open(client * c, websocketpp::connection_hdl hdl) { + m_status = "Open"; + client::connection_ptr con = c->get_con_from_hdl(hdl); + m_server = con->get_response_header("Server"); + } + + void on_fail(client * c, websocketpp::connection_hdl hdl) { + m_status = "Failed"; + client::connection_ptr con = c->get_con_from_hdl(hdl); + m_error_reason = con->get_ec().message(); + } + + void on_close(client * c, websocketpp::connection_hdl hdl) { + m_status = "Closed"; + client::connection_ptr con = c->get_con_from_hdl(hdl); + std::stringstream s; + s << "close code: " << con->get_remote_close_code() << ", close reason: " << con->get_remote_close_reason(); + m_error_reason = s.str(); + } + + void set_error(std::string const & err) { + m_error_reason = err; + m_status = "Error"; + } + + int get_id() const { + return m_id; + } + + websocketpp::connection_hdl get_hdl() const { + return m_hdl; + } + + friend std::ostream & operator<< (std::ostream & out, connection_metadata const & data); +private: + int m_id; + websocketpp::connection_hdl m_hdl; + std::string m_status; + std::string m_uri; + std::string m_server; + std::string m_error_reason; +}; + +std::ostream & operator<< (std::ostream & out, connection_metadata const & data) { + if (data.m_id == -1) { + out << "> Invalid connection"; + } else { + out << "> URI: " << data.m_uri << "\n" + << "> Status: " << data.m_status << "\n" + << "> Remote Server: " << data.m_server << "\n" + << "> Error/close reason: " << data.m_error_reason; + } + + return out; +} + +class websocket_endpoint { +public: + websocket_endpoint () : m_next_id(0) { + m_endpoint.clear_access_channels(websocketpp::log::alevel::all); + m_endpoint.clear_error_channels(websocketpp::log::elevel::all); + + m_endpoint.set_access_channels(websocketpp::log::alevel::app); + + m_endpoint.init_asio(); + m_endpoint.start_perpetual(); + + m_thread.reset(new websocketpp::lib::thread(&client::run, &m_endpoint)); + } + + ~websocket_endpoint() { + m_endpoint.stop_perpetual(); + + for (con_list::const_iterator it = m_connection_list.begin(); it != m_connection_list.end(); ++it) { + websocketpp::lib::error_code ec; + m_endpoint.close(it->second.get_hdl(), websocketpp::close::status::going_away, "", ec); + if (ec) { + std::cout << "> Error closing connection " << it->second.get_id() << std::endl; + } + } + + m_thread->join(); + } + + // copy constructors + + int connect(std::string const & uri) { + websocketpp::lib::error_code ec; + int new_id = m_next_id++; + + client::connection_ptr con = m_endpoint.get_connection(uri, ec); + + if (ec) { + m_connection_list[new_id] = connection_metadata(new_id, websocketpp::connection_hdl(), uri); + m_connection_list[new_id].set_error("Connect initialization error: "+ec.message()); + std::cout << "> Connect initialization error: " << ec.message() << std::endl; + } else { + m_connection_list[new_id] = connection_metadata(new_id, con->get_handle(), uri); + + using websocketpp::lib::placeholders::_1; + using websocketpp::lib::bind; + con->set_open_handler(bind(&connection_metadata::on_open,&m_connection_list[new_id],&m_endpoint,::_1)); + con->set_fail_handler(bind(&connection_metadata::on_fail,&m_connection_list[new_id],&m_endpoint,::_1)); + con->set_close_handler(bind(&connection_metadata::on_close,&m_connection_list[new_id],&m_endpoint,::_1)); + + m_endpoint.connect(con); + } + + return new_id; + } + + void close(int id, websocketpp::close::status::value code) { + websocketpp::lib::error_code ec; + + con_list::iterator metadata_it = m_connection_list.find(id); + if (metadata_it == m_connection_list.end()) { + std::cout << "> No connection found with id " << id << std::endl; + return; + } + + m_endpoint.close(metadata_it->second.get_hdl(),code, "", ec); + if (ec) { + std::cout << "> Error initiating close: " << ec.message() << std::endl; + } + } + + connection_metadata get_metadata(int id) const { + con_list::const_iterator metadata_it = m_connection_list.find(id); + if (metadata_it == m_connection_list.end()) { + return connection_metadata(); + } else { + return metadata_it->second; + } + } +private: + typedef std::map con_list; + + client m_endpoint; + websocketpp::lib::shared_ptr m_thread; + + con_list m_connection_list; + int m_next_id; +}; + +int main() { + bool done = false; + std::string input; + websocket_endpoint endpoint; + + while (!done) { + std::cout << "Enter Command: "; + std::getline(std::cin, input); + + if (input == "quit") { + done = true; + } else if (input == "help") { + std::cout + << "\nCommand List:\n" + << "connect \n" + << "close \n" + << "show \n" + << "help: Display this help text\n" + << "quit: Exit the program\n" + << std::endl; + } else if (input.substr(0,7) == "connect") { + int id = endpoint.connect(input.substr(8)); + std::cout << "> Created connection with id " << id << std::endl; + } else if (input.substr(0,5) == "close") { + int id = atoi(input.substr(6).c_str()); + + endpoint.close(id, websocketpp::close::status::normal); + } else if (input.substr(0,4) == "show") { + int id = atoi(input.substr(5).c_str()); + + std::cout << endpoint.get_metadata(id) << std::endl; + } else { + std::cout << "> Unrecognized Command" << std::endl; + } + } + + return 0; +} + +/* + +clang++ -std=c++11 -stdlib=libc++ -I/Users/zaphoyd/software/websocketpp/ -I/Users/zaphoyd/software/boost_1_55_0/ -D_WEBSOCKETPP_CPP11_STL_ step4.cpp /Users/zaphoyd/software/boost_1_55_0/stage/lib/libboost_system.a + +clang++ -I/Users/zaphoyd/software/websocketpp/ -I/Users/zaphoyd/software/boost_1_55_0/ step4.cpp /Users/zaphoyd/software/boost_1_55_0/stage/lib/libboost_system.a /Users/zaphoyd/software/boost_1_55_0/stage/lib/libboost_thread.a /Users/zaphoyd/software/boost_1_55_0/stage/lib/libboost_random.a +*/ \ No newline at end of file