Skip to content
Merged
2 changes: 1 addition & 1 deletion example/local_chat_demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ int main() {
auto err_func = [] (io_interface iof, std::error_code err)
{ std::cerr << "err_func: " << err << std::endl; };

// work guard - handles @c std::thread and @c asio::io_contect management
// work guard - handles @c std::thread and @c asio::io_context management
chops::net::worker wk;
wk.start();

Expand Down
241 changes: 241 additions & 0 deletions example/simple_chat_demo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/** @file
*
* @ingroup example_module
*
* @brief Example of TCP 2-way network chat program.
*
* @author Thurman Gillespy
*
* Copyright (c) 2019 Thurman Gillespy
* 4/9/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 ../include/net_ip/ \
* -I ~/Projects/utility-rack/include/ \
* -I ~/Projects/asio-1.12.2/include \
* -I ~/Projects/boost_1_69_0/ \
* simple_chat_deo.cpp -lpthread -o chat
*
* BUGS:
* - leaks memory like a sieve. Under investigation.
*
*/

#include <iostream>
#include <cstdlib> // EXIT_SUCCESS
#include <cstddef> // std::size_t
#include <string>
#include <string_view>
#include <chrono>
#include <thread>
#include <cassert>

#include "net_ip/net_ip.hpp"
#include "net_ip/basic_net_entity.hpp"
#include "component/worker.hpp"
#include "queue/wait_queue.hpp"
#include "simple_chat_screen.hpp"

using io_context = asio::io_context;
using io_interface = chops::net::tcp_io_interface;
using const_buf = asio::const_buffer;
using tcp_io_interface = chops::net::tcp_io_interface;
using endpoint = asio::ip::tcp::endpoint;

/** How to use @c chops-net-ip for a simple 2-way chat program over a
* network connection
*
* 1. Write message handler function.
*
* The message handler receives text after @c ::send is invoked. Both
* the local and remote remote clients obtain the sent text through the
* handler. In this demo, the handler inserts the recevied text into the
* history scroll text, and then updates the screen output.
*
* Both message handlers return @c false when the user enters @c 'quit',
* otherwise @c true. @c 'quit' shuts down the handler and exits the program.
*
* 2. Write @ io_state_change handler.
*
* The @c io_state_change handler calls @c start_io on the provided
* @c chops::net::tcp_io_interface. The handler needs to return a copy
* of the @c io_interface to main. The main copy will use the @c io_interface
* to @c send the text message from one client to another.
*
* 3. Create an instance of the @c chops::net::worker class, which provides
* @c std::thread and @c asio::io_context management. Call @c worker::start
* before the frist call to the @chops::net::ip facilities, then call
* @c worker::stop when finished.
*
* 4. Create an instance of the @c chops::net::ip::net_ip class. The constructor
* needs an @c asio:io_context, which is provided by the @c get_io_context()
* method of the @c chops::net::worker instance.
*
* 5. Call @c ::make_tcp_connector on the @c tcp_connector @ net_ip instance
* (@c '-connect' connection type), which returns a copy of a
* @c tcp_connector_network_entity.
*
* 6. Call @c ::make_tcp_acceptor on the @c tcp_acceptor @ net_ip instance
* (@c '-accept' connection type), which returns a copy of a
* @c tcp_acceptor_network_entity.
*
* 7. Call @c ::start() on both both instances of @c network_entity, which
* emplaces the message handler and @c io_state change handlers.
*
* 8. Call @c ::send() on the @c chops::net::tcp_io_interface instance to send
* a text string over the network connection.
*
* 9. See the example code and the header files for the signatures of the
* handlers.
*
*/


// 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) {
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"
" 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 EMPTY = "";

// set default values
ip_addr = LOCAL_LOOP;
port = PORT;

if (argc < 2 || argc > 4) {
std::cout << "incorrect parameter count\n";
std::cout << USAGE << std::endl;
return EXIT_FAILURE;
};

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 {
std::cout << "incorrect first parameter: ";
std::cout << "must be [-h | -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 (argc == 4) {
port = argv[3];
}

assert(param != "");

return EXIT_SUCCESS;
}

int main(int argc, char* argv[]) {
const std::string LOCAL = "[local] ";
std::string ip_addr;
std::string port;
std::string param;

if (process_args(argc, argv, ip_addr, port, param) == EXIT_FAILURE) {
return EXIT_FAILURE;
}

// create instance of @c simple_chat_screen class
simple_chat_screen screen(ip_addr, port, param);

/* lambda callbacks */
// message handler for @c network_entity
auto msg_hndlr = [&] (const_buf buf, io_interface iof, endpoint ep)
{
// receive data from acceptor, display to user
std::string s (static_cast<const char*> (buf.data()), buf.size());
screen.insert_scroll_line(s, REMOTE);
screen.draw_screen();

// return false if user entered 'quit', otherwise true
return s == "quit\n" ? false : 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]
(io_interface iof, std::size_t n, bool flag)
{
iof.start_io("\n", msg_hndlr);
// return iof to main
tcp_iof = iof;
};

// error handler
auto err_func = [] (io_interface iof, std::error_code err)
{
static int count = 0;
std::cerr << "err_func: " << err << std::endl;
if (++count > 10) {
std::cerr << "aborting: > 10 error messages" << std::endl;
exit(0);
} };

// work guard - handles @c std::thread and @c asio::io_context management
chops::net::worker wk;
wk.start();

// create @c net_ip instance
chops::net::net_ip chat(wk.get_io_context());

if (param == PARAM_CONNECT) {
// make @c tcp_connector, return @c network_entity
auto net_entity = chat.make_tcp_connector(port.c_str(), ip_addr.c_str(),
std::chrono::milliseconds(5000));
assert(net_entity.is_valid());
// start network entity, emplace handlers
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, err_func);
}

screen.draw_screen();

// get std::string from user
// send as c-string over network connection, update screen
std::string s;
while (s != "quit\n") {
std::getline (std::cin, s);
s += "\n"; // needed for deliminator
screen.insert_scroll_line(s, LOCAL);
screen.draw_screen();
// send c-string from @c tcp_connector to @c tcp_acceptor
assert(tcp_iof.is_valid());
tcp_iof.send(s.c_str(), s.size() + 1);
}

wk.stop();

return EXIT_SUCCESS;
}
137 changes: 137 additions & 0 deletions example/simple_chat_screen.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/** @file
*
* @brief class to handle printing output to stdout for simple_chat_demo.cpp program.
*
* @author Thurman Gillespy
*
* Copyright (c) 2019 Thurman Gillespy
* 4/9/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)
*/

#include <iostream>
#include <string>
#include <vector>
#include <cstdlib> // std::system

const int NUN_SCROLL_LINES = 10;

// shared globals
const std::string PARAM_CONNECT = "-connect";
const std::string PARAM_ACCEPT = "-accept";
const std::string REMOTE = "[remote] ";

// https://stackoverflow.com/questions/3381614/c-convert-string-to-hexadecimal-and-vice-versa
std::string string_to_hex(const std::string& input) {
static const char* const lut = "0123456789ABCDEF";
size_t len = input.length();

std::string output;
output.reserve(2 * len);
for (size_t i = 0; i < len; ++i)
{
const unsigned char c = input[i];
output.push_back(lut[c >> 4]);
output.push_back(lut[c & 15]);
}
return output;
}

// handle all methods to print output to stdout
class simple_chat_screen {
private:
const std::string m_ip_addr; // IP address or host name
const std::string m_port; // connection port
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'

public:
simple_chat_screen(const std::string ip, const std::string port, const std::string type,
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_scroll_text();
};

// print the output to stdout
// called after @c insert_scroll_line
void draw_screen() {
std::system("clear"); // not recommended, but adequate here
std::cout << (m_upper_screen + m_scroll_text + BOTTOM + PROMPT);
}

// the scroll region has a fixed numbmer of 'scroll lines'.
// calculate complete new scroll line, insert old line at top
// of text scroll region, add new scroll line
void insert_scroll_line(const std::string& text, const std::string& prefix) {

// create the new scroll line
// remove '\n' at end of text, and '\0' at beginning (if present)
int first_pos = (text.find_first_of('\0') == 0 ? 1 : 0);
std::string new_scroll_line = "| " + prefix +
text.substr(first_pos, text.size() - (1 + first_pos));
// DEBUG: show string as ascii values
// new_scroll_line += "<" + string_to_hex(text) + ">";
new_scroll_line +=
BLANK_LINE.substr(new_scroll_line.length(), std::string::npos);

// remove old scroll line in scroll text (using substr), add new scroll line
m_scroll_text =
m_scroll_text.substr(m_scroll_text.find_first_of("\n") + 1, std::string::npos) +
new_scroll_line;
}

private:
using S = const std::string;
// string constants for output
S TOP =
"\n_____________________________________________________________________________\n";
S BLANK_LINE =
"| |\n";
S DIVIDOR =
"|___________________________________________________________________________|\n";
S HDR_1 =
"| chops-net-ip 2-way chat network demo |\n";
S HDR_IP =
"| IP address: ";
S HDR_PORT =
" port: ";
S HDR_TYPE =
"| connection type: ";
S CONNECT_T =
"connector |\n";
S ACCEPT_T =
"acceptor |\n";
S HDR_INSTR =
"| Enter text to send at prompt. Enter 'quit' to exit. |\n";
S SCROLL_LINE =
"| ";
S BOTTOM =
"|---------------------------------------------------------------------------|\n";
S HDR_START =
"| ";
S PROMPT = "> ";

// create the string that represent the (unchanging) upper screen so only calculate once
void create_upper_screen() {
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) +
DIVIDOR + BLANK_LINE + HDR_INSTR + DIVIDOR;
}

// seed the scroll text region with m_scoll_lines number of blank lines
void create_scroll_text() {
int count = m_num_scroll_lines;
while (count-- > 0) {
m_scroll_text += BLANK_LINE;
}
}
};