/* * NMSTL, the Networking, Messaging, Servers, and Threading Library for C++ * Copyright (c) 2002 Massachusetts Institute of Technology * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include #include // Declarations related to our network protocol #include "chatproto.h" using namespace nmstl; using namespace std; // The event loop that will dispatch all network and terminal I/O events. io_event_loop loop; // A handler for terminal input and output. term_handler term(loop, "chatclient> "); /* * A handler for a single connection to a chat server. * * We subclass msg_handler and override a few of its virtual methods: * * - connected() is invoked when a connection we try either * succeeds or fails. * - incoming_message() is invoked whenever a complete message * is received. * - end_messages() is in invoked when the stream closes. * * chat_header, defined in chatproto.h, is our header type; it * contains a type field (LOGIN, SHOUT, WHISPER, etc...) and * a length field. (The length field is automatically filled in * by msg_handler.) * * We use serialization to read and write message payloads (the * actual content of messages). So if I write out a message * like * * write( chat_header(LOGIN), omessage() << user << password ); * * then the peer will receive a message in incoming_message() * such that header.type == LOGIN; to read the packet, the peer * can say * * string user, password; * imessage(data) >> user >> password; * * See incoming_message() in chatserver.cc. * */ class chat_client : public msg_handler { // Have I logged into the server yet? bool authenticated; public: // Create the client object. No connection yet. chat_client(io_event_loop& loop) : msg_handler(loop), authenticated(false) { } // Attempt to connect to a remote host. status connect(string host, string port) { if (host.empty()) host = "localhost"; if (port.empty()) port = "8002"; inet_address peer(host, port); if (!peer) return status::bad("Invalid address"); // In handlers, need to use nonblocking connections // (otherwise we'd stall all the other handlers, // e.g., the terminal handler). set_socket(tcpsocket(peer, tcpsocket::nonblocking)); return status::good(); } status login(string username, string password) { if (!is_connected()) return status::bad("Not connected (use \"connect\")"); if (authenticated) return status::bad("No need to login; already authenticated"); /* * The following write(...) line constructs a header, and * serializes username and password into a payload, and * write it out. It is equivalent to: * * chat_header header(LOGIN); * * oserialstring oss; * oss << username << password; * * write(header, iss.str()); * * omessage() << ... << ... is just a convenient way to construct * a serialized message payload. * * To see where these messages are used, look at incoming_message() * in chatserver.cc. * */ write(chat_header(LOGIN), omessage() << username << password); return status::good(); } status disconnect() { if (!is_connected()) return status::bad("Not connected (use \"connect\")"); // tcpsocket() is an "empty" socket, just like string() is // an empty string. set_socket( tcpsocket() ) is a way of // closing the current connection. set_socket( tcpsocket() ); return status::good(); } status shout(string msg) { if (!is_connected()) return status::bad("Not connected (use \"connect\")"); if (!authenticated) return status::bad("Not authenticated."); write(chat_header(SHOUT), omessage() << msg); return status::good(); } status whisper(string target, string msg) { if (!is_connected()) return status::bad("Not connected (use \"connect\")"); if (!authenticated) return status::bad("Not authenticated."); write(chat_header(WHISPER), omessage() << target << msg); return status::good(); } // Note that the virtual methods are protected - no one is allowed // to call them except for msg_handler (our superclass). protected: // Handle success/failure of a connection attempt. void connected(status stat) { if (stat) { term << "Connected to remote server; use login " " to login." << endl; } else { term << "Failed to connect to remote server: " << stat << endl; } } // Handle an incoming message. void incoming_message(const chat_header& header, constbuf data) { switch (header.type) { case AUTH_ACK: { term << "Authentication succeeded." << endl; authenticated = true; break; } case AUTH_NAK: { term << "Authentication failed." << endl; break; } case SHOUT: case WHISPER: { string from; string msg; if (!(imessage(data) >> from >> msg)) break; term << from << ( header.type == SHOUT ? " shouts: " : " whispers: " ) << msg << endl; break; } default: { term << "Unknown message type " << header.type << endl; break; } } } // End of message stream. void end_messages(unsigned int bytes_remaining) { term << "Connection closed"; if (bytes_remaining) term << " (while part of a message was en route)"; authenticated = false; term << endl; } }; int main() { // Create the client handler (doesn't establish the // connection yet; use chat_client::conect for that. chat_client chat(loop); // Add a bunch of commands to the terminate. // "quit," "exit," and "help" are already there. // // Arguments to add_command are // // - name and usage // - minimum # of arguments // - maximum # of arguments // - the function or method to call when the command is invoked // // The maximum # of arguments can be bitwise ORed with // term_handler::text_arg to mean that all extra arguments // should just be added to the last argument (so that // "shout foo bar" is the same as "shout 'foo bar'"). // Or term_handler::var_arg means that *any* number of // arguments is permissible. term.add_command(string("connect (defaults to localhost 8002)"), 0, 2, wrap(chat, &chat_client::connect)); term.add_command("login (hint: the password is \"foobar\")", 2, 2, wrap(chat, &chat_client::login)); term.add_command("disconnect", 0, 0, wrap(chat, &chat_client::disconnect)); term.add_command("shout \"\"", 1, 1 | term_handler::text_arg, wrap(chat, &chat_client::shout)); term.add_command("whisper \"\"", 2, 2 | term_handler::text_arg, wrap(chat, &chat_client::whisper)); term << "Welcome. Type \"help\" for help." << endl; // Run the event loop. Returns only when io_event_loop::terminate // is invoked (because the user types "quit" or "exit" or // control-D). loop.run(); }