diff --git a/example/chat_server_demo.cpp b/example/chat_server_demo.cpp new file mode 100644 index 00000000..fbb08103 --- /dev/null +++ b/example/chat_server_demo.cpp @@ -0,0 +1,167 @@ +/** @file + * + * @ingroup example_module + * + * @brief Example of TCP multichat server network program. + * + * @author Thurman Gillespy + * + * Copyright (c) 2019 Thurman Gillespy + * 4/15/19 + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + * Sample make file: +g++ -std=c++17 -Wall -Werror \ +-I ../include \ +-I ~/Projects/utility-rack/include/ \ +-I ~/Projects/asio/asio/include/ \ +-I ~/Projects/boost_1_69_0/ \ +chat_server_demo.cpp -lpthread -o chat_server + * + */ + +#include +#include // EXIT_SUCCESS +#include // std::size_t +#include +#include +#include +#include // std::ref +#include + +#include "net_ip/net_ip.hpp" +#include "net_ip/basic_net_entity.hpp" +#include "net_ip/component/worker.hpp" +#include "net_ip/component/send_to_all.hpp" + +using io_context = asio::io_context; +using io_interface = chops::net::tcp_io_interface; +using const_buf = asio::const_buffer; +using endpoint = asio::ip::tcp::endpoint; + +// process command line args, set ip_addr, port, param as needed +bool process_args(int argc, char* const argv[], std::string& ip_addr, + std::string& port, bool& print_errors) { + const std::string PORT = "5001"; + const std::string LOCAL_LOOP = "127.0.0.1"; + const std::string USAGE = + "usage:\n" + " ./chat_server [-h] [-e] [port]\n" + " -h print usage\n" + " -e print all error messages to console\n" + " default port = " + PORT + "\n" + " server IP address (fixed) = " + LOCAL_LOOP + " (local loop)"; + + const std::string HELP = "-h"; + const std::string ERR = "-e"; + + // set default values + ip_addr = LOCAL_LOOP; + port = PORT; + + if (argc > 3 || (argc == 2 && argv[1] == HELP)) { + std::cout << USAGE << std::endl; + return EXIT_FAILURE; + } + + if (argc == 2) { + if (argv[1] == ERR) { + print_errors = true; + } else { + port = argv[1]; + } + } + + if (argc == 3) { + if (argv[1] == ERR) { + print_errors = true; + port = argv[2]; + } else { + std::cout << USAGE << std::endl; + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} + +int main(int argc, char *argv[]) +{ + const std::string LOCAL = "[local] "; + const std::string SYSTEM = "[system] "; + const std::string SERVER = "[server] "; + const std::string DELIM = "\a"; // alert (bell) + std::string ip_addr; + std::string port; + bool print_errors = false; + bool finished = false; + + if (process_args(argc, argv, ip_addr, port, print_errors) == EXIT_FAILURE) { + return EXIT_FAILURE; + } + + // work guard - handles @c std::thread and @c asio::io_context management + chops::net::worker wk; + wk.start(); + + // handles all @c io_interfaces + chops::net::send_to_all sta; + + /* lamda handlers */ + // receive text from client, send out to others + const auto msg_hndlr = [&sta](const_buf buf, io_interface iof, endpoint ep) { + // sta.send(buf.data(), buf.size(), iof); + sta.send(buf.data(), buf.size()); + + return true; + }; + + const auto io_state_chng_hndlr = [&sta, &msg_hndlr, &DELIM] + (io_interface iof, std::size_t n, bool flag) { + // add to or remove @c io_interface from list in sta + sta(iof, n, flag); + if (flag) { + iof.start_io(DELIM, msg_hndlr); + } + }; + + const auto err_func = [&print_errors](io_interface iof, std::error_code err) { + if (print_errors) { + std::cerr << err << ", " << err.message() << std::endl; + } + }; + + // create @c net_ip instance + chops::net::net_ip server(wk.get_io_context()); + // make @c tcp_acceptor, ruitn @c network_entity + auto net_entity = server.make_tcp_acceptor(port.c_str()); + assert(net_entity.is_valid()); + // start network entity, emplace handlers + net_entity.start(io_state_chng_hndlr, err_func); + + std::cout << "chops-net-ip chat server demo" << std::endl; + std::cout << " port: " << port << std::endl; + if (print_errors) { + std::cout << " all error messages printed to console" << std::endl; + } + + while (!finished) { + std::string s; + std::cout << "press return to exit" << std::endl; + getline(std::cin, s); + s = "server shutting down" + DELIM; + sta.send(s.data(), s.size()); + finished = true; + } + // delay so message gets sent + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + // shutdown code here? + // net_entity.stop(); + + wk.stop(); + + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/example/local_echo_demo.cpp b/example/local_echo_demo.cpp index 733fc35c..7e011dc6 100644 --- a/example/local_echo_demo.cpp +++ b/example/local_echo_demo.cpp @@ -20,9 +20,6 @@ g++ -std=c++17 -Wall -Werror \ -I ~/Projects/asio/asio/include \ -I ~/Projects/boost_1_69_0/ \ local_echo_demo.cpp -lpthread -o local - * - * BUGS: - * - leaks memory like a sieve. Under investigation. * */ @@ -126,8 +123,8 @@ int main() { std::string s(static_cast (buf.data()), buf.size()); auto to_upper = [] (char& c) { c = ::toupper(c); }; std::for_each(s.begin(), s.end(), to_upper); - // send c-string back over network connection - iof.send(s.c_str(), s.size() + 1); + // send uppercase string data back over network connection + iof.send(s.data(), s.size()); // return false if user entered 'quit', otherwise true return s == "QUIT\n" ? false: true; @@ -186,19 +183,19 @@ int main() { assert(tcp_connect_iof.is_valid()); // fails without a pause - std::cout << "network demo over local loop" << std::endl; + std::cout << "network echo demo over local loop" << std::endl; std::cout << "enter a string at the prompt" << std::endl; std::cout << "the string will be returned in uppercase" << std::endl; std::cout << "enter \'quit\' to exit" << std::endl << std::endl; // get std::string from user - // send as c-string over network connection + // send string data over network connection std::string s; while (s != "quit\n") { std::cout << "> "; std::getline (std::cin, s); s += "\n"; // needed for deliminator - // send c-string from @c tcp_connector to @c tcp_acceptor + // send string from @c tcp_connector to @c tcp_acceptor tcp_connect_iof.send(s.data(), s.size()); // pause so returned string is displayed before next prompt std::this_thread::sleep_for(std::chrono::milliseconds(100)); diff --git a/example/simple_chat_demo.cpp b/example/simple_chat_demo.cpp index 799b0941..ff6327fb 100644 --- a/example/simple_chat_demo.cpp +++ b/example/simple_chat_demo.cpp @@ -7,7 +7,7 @@ * @author Thurman Gillespy * * Copyright (c) 2019 Thurman Gillespy - * 4/11/19 + * 4/15/19 * * Distributed under the Boost Software License, Version 1.0. * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -16,12 +16,10 @@ g++ -std=c++17 -Wall -Werror \ -I ../include \ -I ~/Projects/utility-rack/include/ \ --I ~/Projects/asio-1.12.2/include/ \ +-I ~/Projects/asio/asio/include/ \ -I ~/Projects/boost_1_69_0/ \ -simple_chat_deo.cpp -lpthread -o chat +simple_chat_demo.cpp -lpthread -o chat * - * BUGS: - * - leaks memory like a sieve. Under investigation. * */ @@ -31,7 +29,7 @@ simple_chat_deo.cpp -lpthread -o chat #include #include #include -#include +#include // std::promise, std::future #include #include "net_ip/net_ip.hpp" @@ -95,26 +93,28 @@ using endpoint = asio::ip::tcp::endpoint; // process command line args, set ip_addr, port, param as needed -bool process_args(int argc, char* argv[], std::string& ip_addr, - std::string& port, std::string& param) { +bool process_args(int argc, char* const argv[], std::string& ip_addr, + std::string& port, std::string& param, bool& print_errors) { const std::string PORT = "5001"; const std::string LOCAL_LOOP = "127.0.0.1"; const std::string USAGE = - "usage: ./chat [-h] -connect | -accept [ip address] [port]\n" - " -h print usage\n" - " -connect tcp_acceptor\n" - " -accept tcp_connector\n" + "usage: ./chat [-h] [-e] -connect | -accept [ip address] [port]\n" + " -h print usage info\n" + " -e print error messages\n" + " -connect tcp_connector\n" + " -accept tcp_acceptor\n" " default ip address: " + LOCAL_LOOP + " (local loop)\n" " default port: " + PORT + "\n" " if connection type = accept, IP address becomes \"\""; const std::string HELP = "-h"; + const std::string ERR = "-e"; const std::string EMPTY = ""; // set default values ip_addr = LOCAL_LOOP; port = PORT; - if (argc < 2 || argc > 4) { + if (argc < 2 || argc > 5) { std::cout << "incorrect parameter count\n"; std::cout << USAGE << std::endl; return EXIT_FAILURE; @@ -123,29 +123,62 @@ bool process_args(int argc, char* argv[], std::string& ip_addr, if (argv[1] == HELP) { std::cout << USAGE << std::endl; return EXIT_FAILURE; - } else if (argv[1] == PARAM_CONNECT) { - param = PARAM_CONNECT; - } else if (argv[1] == PARAM_ACCEPT) { - param = PARAM_ACCEPT; + } else if (argv[1] == ERR) { + print_errors = true; + } + + if (print_errors) { + if (argc == 2) { + std::cout << USAGE << std::endl; + return EXIT_FAILURE; + } + if (argv[2] == PARAM_CONNECT) { + param = PARAM_CONNECT; + } else if (argv[2] == PARAM_ACCEPT) { + param = PARAM_ACCEPT; + } else { + std::cout << "incorrect second parameter: "; + std::cout << "must be [-connect | -accept]" << std::endl; + std::cout << USAGE << std::endl; + return EXIT_FAILURE; + } } else { - std::cout << "incorrect first parameter: "; - std::cout << "must be [-h | -connect | -accept]" << std::endl; - std::cout << USAGE << std::endl; - return EXIT_FAILURE; + if (argv[1] == PARAM_CONNECT) { + param = PARAM_CONNECT; + } else if (argv[1] == PARAM_ACCEPT) { + param = PARAM_ACCEPT; + } else { + std::cout << "incorrect first parameter: "; + std::cout << "must be [-h | -e | -connect | -accept]" << std::endl; + std::cout << USAGE << std::endl; + return EXIT_FAILURE; + } } if (param == PARAM_ACCEPT) { ip_addr = ""; } - if (argc == 3 || argc == 4) { - if (param == PARAM_CONNECT) { - ip_addr = argv[2]; + if (print_errors) { + if (argc == 4 || argc == 5) { + if (param == PARAM_CONNECT) { + ip_addr = argv[3]; + } + } + + if (argc == 5) { + port = argv[4]; + } + } else { + if (argc == 3 || argc == 4) { + if (param == PARAM_CONNECT) { + ip_addr = argv[2]; + } } - } - if (argc == 4) { - port = argv[3]; + if (argc == 4) { + port = argv[3]; + } } assert(param != ""); @@ -156,76 +189,72 @@ bool process_args(int argc, char* argv[], std::string& ip_addr, int main(int argc, char* argv[]) { const std::string LOCAL = "[local] "; const std::string SYSTEM = "[system] "; + const std::string ERROR = "[error] "; const std::string DELIM = "\a"; // alert (bell) const std::string NO_CONNECTION = "error: no connection" + DELIM; const std::string ABORT = "abort: too many errors"; const std::string WAIT_CONNECT = "waiting for connection..." + DELIM; - // const char* ERR_LOG = "err_log.txt"; std::string ip_addr; std::string port; std::string param; - std::string connect_errs = "tcp_connector errors\n"; - std::string accept_errs = "tcp_acceptor errors\n"; + std::string last_msg; + bool print_errors = false; + bool shutdown = false; + std::promise promise_obj; + std::future future_obj = promise_obj.get_future(); - if (process_args(argc, argv, ip_addr, port, param) == EXIT_FAILURE) { + if (process_args(argc, argv, ip_addr, port, param, print_errors) == EXIT_FAILURE) { return EXIT_FAILURE; } // create instance of @c simple_chat_screen class - simple_chat_screen screen(ip_addr, port, param); - // create error log, clear old log - // FILE* file_ptr = fopen(ERR_LOG, "w"); - // assert(file_ptr != nullptr); + simple_chat_screen screen(ip_addr, port, param, print_errors); /* lambda callbacks */ // message handler for @c network_entity auto msg_hndlr = [&] (const_buf buf, io_interface iof, endpoint ep) { + if (shutdown) { + screen.insert_scroll_line("msg_hndlr shutdown" + DELIM, + SYSTEM); + screen.draw_screen(); + + return false; + } + // receive data from acceptor, display to user // remove deliminator here? std::string s (static_cast (buf.data()), buf.size()); - screen.insert_scroll_line(s, REMOTE); - screen.draw_screen(); - - // return false if user entered 'quit', otherwise true - return s == "quit" + DELIM ? false : true; + + if (s != last_msg) { + screen.insert_scroll_line(s, REMOTE); + screen.draw_screen(); + } + + return true; }; - // io state change handler - tcp_io_interface tcp_iof; // used to send text data // handler for @c tcp_connector - auto io_state_chng_hndlr = [&tcp_iof, msg_hndlr, DELIM](io_interface iof, std::size_t n, bool flag) { + auto io_state_chng_hndlr = [&promise_obj, msg_hndlr, DELIM] + (io_interface iof, std::size_t n, bool flag) { + // only start the iof if flag is true (startup) and only 1 if (flag && n == 1) { iof.start_io(DELIM, msg_hndlr); // return iof to main - tcp_iof = iof; + // tcp_iof = iof; + + // return iof via promise/future handoff + promise_obj.set_value(iof); } }; // error handler - auto connect_err_func = [&] (io_interface iof, std::error_code err) + auto err_func = [&] (io_interface iof, std::error_code err) { static int count = 0; - static int last_err = 0; - std::string err_text; - - if (err.value() == last_err) { - ++count; - } else { - if (count > 0) { - connect_errs += " <" + std::to_string(count) + ">\n"; - count = 0; - last_err = err.value(); - } - err_text = "connect: " + std::to_string(err.value()) + ": " + - err.category().name() + ": " + err.message() + "\n"; - connect_errs += err_text; - ++count; - } - if (count > 10) { + + if (++count > 15) { std::cerr << ABORT << std::endl; - connect_errs += " <" + std::to_string(count) + ">\n"; - std::cerr << connect_errs; exit(0); } @@ -233,33 +262,15 @@ int main(int argc, char* argv[]) { screen.insert_scroll_line(WAIT_CONNECT, SYSTEM); screen.draw_screen(); } - }; - - auto accept_err_func = [&] (io_interface iof, std::error_code err) { - static int count = 0; - static int last_err = 0; - std::string err_text; - - if (err.value() == last_err) { - ++count; - } else { - if (count > 0) { - accept_errs += " <" + std::to_string(count) + ">\n"; - count = 0; - last_err = err.value(); - } - err_text = "accept: " + std::to_string(err.value()) + ": " + - err.category().name() + ": " + err.message() + "\n"; - accept_errs += err_text; - ++count; - } - if (count > 10) { - std::cerr << ABORT << std::endl; - accept_errs += " <" + std::to_string(count) + ">\n"; - std::cerr << accept_errs; - exit(0); + + if (print_errors) { + std::string err_text = err.category().name(); + err_text += ": " + std::to_string(err.value()) + ", " + + err.message() + DELIM; + screen.insert_scroll_line(err_text, ERROR); + screen.draw_screen(); } - }; + }; // work guard - handles @c std::thread and @c asio::io_context management chops::net::worker wk; @@ -275,37 +286,47 @@ int main(int argc, char* argv[]) { std::chrono::milliseconds(5000)); assert(net_entity.is_valid()); // start network entity, emplace handlers - net_entity.start(io_state_chng_hndlr, connect_err_func); + net_entity.start(io_state_chng_hndlr, err_func); } else { // make @ tcp_acceptor, return @c network_entity auto net_entity = chat.make_tcp_acceptor(port.c_str(), ip_addr.c_str()); assert(net_entity.is_valid()); // start network entity, emplace handlers - net_entity.start(io_state_chng_hndlr, accept_err_func); + net_entity.start(io_state_chng_hndlr, err_func); } screen.draw_screen(); - // get std::string from user - // send as c-string over network connection, update screen + // io state change handler + tcp_io_interface tcp_iof; // used to send text data + tcp_iof = future_obj.get(); // wait for value set in io_state_chng_hndlr + + // get std::string from user, send string data over network, update screen std::string s; - while (s != "quit" + DELIM) { - std::getline (std::cin, s); + // while (s != "quit" + DELIM) { + while (!shutdown) { + std::getline (std::cin, s); // user input + if (s == "quit") { + shutdown = true; + } s += DELIM; // needed for deliminator screen.insert_scroll_line(s, LOCAL); screen.draw_screen(); + last_msg = s; if (!tcp_iof.is_valid()) { screen.insert_scroll_line(NO_CONNECTION, SYSTEM); screen.draw_screen(); continue; } - // send string data from @c tcp_connector to @c tcp_acceptor - // note correct method of sending std::string over network + // note correct method of sending string data over network tcp_iof.send(s.data(), s.size()); + } // allow last message to be sent before shutting down connection std::this_thread::sleep_for(std::chrono::seconds(1)); + // tcp_iof.stop_io(); + wk.stop(); return EXIT_SUCCESS; diff --git a/example/simple_chat_screen.hpp b/example/simple_chat_screen.hpp index 52d8b968..50309fb7 100644 --- a/example/simple_chat_screen.hpp +++ b/example/simple_chat_screen.hpp @@ -5,7 +5,7 @@ * @author Thurman Gillespy * * Copyright (c) 2019 Thurman Gillespy - * 4/11/19 + * 4/15/19 * * Distributed under the Boost Software License, Version 1.0. * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -34,13 +34,13 @@ class simple_chat_screen { const std::string m_connect_type; // @c '-connect' or @c '-accept' std::string m_upper_screen; // fixed upper region of screen output std::string m_scroll_text; // history scroll text region - const int m_num_scroll_lines; // number of 'scrol lines' + const int m_num_scroll_lines; // number of 'scroll lines' public: - simple_chat_screen(const std::string ip, const std::string port, const std::string type, - int num_lines = NUN_SCROLL_LINES) : + simple_chat_screen(const std::string& ip, const std::string& port, const std::string& type, + bool print_errors, int num_lines = NUN_SCROLL_LINES) : m_ip_addr(ip), m_port(port), m_connect_type(type), m_num_scroll_lines(num_lines) { - create_upper_screen(); + create_upper_screen(print_errors); create_scroll_text(); }; @@ -79,7 +79,7 @@ class simple_chat_screen { S DIVIDOR = "|___________________________________________________________________________|\n"; S HDR_1 = - "| chops-net-ip 2-way chat network demo |\n"; + "| chops-net-ip chat network demo |\n"; S HDR_IP = "| IP address: "; S HDR_PORT = @@ -90,6 +90,10 @@ class simple_chat_screen { "connector |\n"; S ACCEPT_T = "acceptor |\n"; + S ERR_LOG_ON = + "| errors printed to console: ON |\n"; + S ERR_LOG_OFF = + "| errors printed to console: OFF |\n"; S HDR_INSTR = "| Enter text to send at prompt. Enter 'quit' to exit. |\n"; S SCROLL_LINE = @@ -98,16 +102,17 @@ class simple_chat_screen { "|---------------------------------------------------------------------------|\n"; S HDR_START = "| "; - S PROMPT = "> "; + S PROMPT = "| > "; // create the string that represent the (unchanging) upper screen so only calculate once - void create_upper_screen() { + void create_upper_screen(bool print_err) { std::string hdr_info = HDR_IP + (m_ip_addr == "" ? "\"\"" : m_ip_addr) + HDR_PORT + m_port ; hdr_info += BLANK_LINE.substr(hdr_info.size(), std::string::npos); m_upper_screen = TOP + BLANK_LINE + HDR_1 + DIVIDOR + BLANK_LINE + hdr_info + HDR_TYPE + - (m_connect_type == PARAM_ACCEPT ? ACCEPT_T : CONNECT_T) + + (m_connect_type == PARAM_ACCEPT ? ACCEPT_T : CONNECT_T) + + (print_err ? ERR_LOG_ON : ERR_LOG_OFF) + DIVIDOR + BLANK_LINE + HDR_INSTR + DIVIDOR; } @@ -119,8 +124,15 @@ class simple_chat_screen { } } + // not recommended, but adequate for this demo + // for problems with system("clear") see + // http://www.cplusplus.com/articles/4z18T05o/ void clear_screen() { + #ifdef _WIN32 + system("cls"); + #else system("clear"); + #endif } }; diff --git a/include/net_ip/component/send_to_all.hpp b/include/net_ip/component/send_to_all.hpp index 3dd1a80d..5713b29a 100644 --- a/include/net_ip/component/send_to_all.hpp +++ b/include/net_ip/component/send_to_all.hpp @@ -102,6 +102,16 @@ class send_to_all { io.send(buf); } } + + void send(chops::const_shared_buffer buf, io_intf cur_io) const { // TG + lock_guard gd { m_mutex }; + for (const auto& io : m_io_intfs) { + if ( !(cur_io == io) ) { + io.send(buf); + } + } + } + /** * @brief Copy the bytes, create a reference counted buffer, then send it to * all @c basic_io_interface objects. @@ -109,6 +119,11 @@ class send_to_all { void send(const void* buf, std::size_t sz) const { send(chops::const_shared_buffer(buf, sz)); } + + void send(const void* buf, std::size_t sz, io_intf cur_io) const { // TG + send(chops::const_shared_buffer(buf, sz), cur_io); + } + /** * @brief Move the buffer from a writable reference counted buffer to a * immutable reference counted buffer, then send it. @@ -116,6 +131,11 @@ class send_to_all { void send(chops::mutable_shared_buffer&& buf) const { send(chops::const_shared_buffer(std::move(buf))); } + + void send(chops::mutable_shared_buffer&& buf, io_intf cur_io) const { + send(chops::const_shared_buffer(std::move(buf)), cur_io); + } + /** * @brief Return the number of @c basic_io_interface objects in the collection. */