ClanLib Tips & Tricks


HOME    MATH    NETWORK

Links

ClanLib Home


Building your own message objects

The following code demonstrates how to connect a client to a server and exchange messages. Creating the server and the client should be fairly straight-forward.

Creating a server

server.h
class Server()
{
public:
    Server();

protected:
    void onConnect(CL_NetComputer &client);
    void onDisconnect(CL_NetComputer &client);
    void onRcvMsg(CL_NetPacket &pkg, CL_NetComputer &client);

    CL_NetSession session;

    CL_SlotContainer slots;
};

server.cpp
Server::Server()
{
    slots.connect(session.sig_computer_connected(), this, &Server::onConnect);
    slots.connect(session.sig_computer_disconnected(), this, &Server::onDisconnect);
    slots.connect(session.sig_netpacket_receive("messages"), this, &Server::onRcvMsg);

    session.start_listen(configuration::port());
}

This code will connect network events with our member functions, notifying us when a client connects, disconnects or sends some data on the "messages" channel.

The server is also told to start listen to incoming connection requests.

Creating a client

The client code for connecting to our server will look something like this.

client.h
class Client()
{
public:
    Client();

    void connectToServer(const std::string &addr, const std::string &port);

protected:
    void onDisconnect(CL_NetComputer &client);
    void onRcvMsg(CL_NetPacket &pkg, CL_NetComputer &client);

    CL_NetSession *session;
    CL_NetComputer server;

    CL_SlotContainer slots;
};

The event handeling is set up in the same manner as for the server. The following (hopefully) connects our client to the server.

client.cpp
void Client::connectToServer(const std::string &addr, const std::string &port)
{
    try
    {
        slots.connect(session->sig_netpacket_receive("messages"), this, &Client::onPacketRcv);
        server = session->connect(CL_IPAddress(addr, port));
    }
    catch (CL_Error err)
    {
        std::cout << "Connection attempt failed: " << err.message.c_str() << std::endl;
    }
}

Now we have the infrastructure needed to start sending messages. That is, we still need to implement handleRcvMsg(...) method in both the server and the client. But let's first have a look at the messages we will send back and forth.

Creating messages

This articles approach uses one class for each message that could be sent. All message classes inherits from a base-class 'Message'.

Message.h
class Message
{
public:
    enum MessageType
    {
        Msg_Error = 0,
        Msg_Join = 1,
        // ...
    };

    virtual std::string get_message_type_string() const { return "General message"; }

    MessageType get_message_type() const { return message_type; }

    static Message* reconstruct(CL_NetPacket &pkg);

    virtual void write_message_data(CL_NetPacket &pkg);

protected:
    virtual void read_message_data(CL_NetPacket &pkg) {}

    MessageType message_type;
};

The MessageType enum contains an entry for each type of message in our system. Each of these values corresponds to a Message sub-class.

The get_message_type_string() returns a description of the message type. This is used only for tracing purposes when messages are received.

The reconstruct() function recreates a message on the receiving side.

Message.cpp
Message* Message::reconstruct(CL_NetPacket &pkg)
{
    Message msg;
    msg.message_type = pkg.input.read_int16();

    Message *p = 0;
    switch (msg.get_message_type())
    {
    case Msg_Error:
        p = new ErrorMessage();
        break;
    case Msg_Join:
        p = new JoinMessage();
        break;
//  case ...:
//      p = new ...Message();
//      break;
    default:
        break;
    }

    if (p)
    {
        p->message_type = msg.message_type;
        p->read_message_data(pkg);
    }

    return p;
}

This might be argued to be an awkward design, since the base-class needs to know about its sub-classes. But once written, this is all hidden within the Message classes and it doesn't present a problem to the user of these classes. When new messages are added they need to be added in this function.

The write_message_data() and read_message_data() functions prepares and extracts data from a CL_NetPacket.

Message.cpp
void Message::write_message_data(CL_NetPacket &pkg)
{
    pkg.output.write_int16(message_type);
}

Creating a specific message

For the completeness we will also write a specialized Message class. Let's say we want to be able to send some kind of error information between the server and the client. We declare the ErrorMessage class.

ErrorMessage.h
class ErrorMessage : public Message
{
public:
    enum ErrorType
    {
        NoError = 0, // For internal use only
        InvalidPassword,
        TableIsFull
    };

    ErrorMessage();
    ErrorMessage(ErrorType err);

    virtual std::string get_message_type_string() const { return "Error message"; }

    virtual void write_message_data(CL_NetPacket &pkg);

    std::string get_description() const { return _descr[_err]; }

protected:
    virtual void read_message_data(CL_NetPacket &pkg);

    // Message specific data
    ErrorType _err;
    static const char* _descr[];
};

...with the implementation...

ErrorMessage.cpp
const char* ErrorMessage::_descr[] =
{
    "No error",
    "Invalid password",
    "Table is full"
};

ErrorMessage::ErrorMessage()
: _err(NoError)
{
    // Set the message type
    message_type = static_cast<unsigned short>(Msg_Error);
}

ErrorMessage::ErrorMessage(ErrorType err)
: _err(err)
{
    // Set the message type
    message_type = static_cast<unsigned short>(Msg_Error);
}


void ErrorMessage::read_message_data(CL_NetPacket &pkg)
{
    Message::read_message_data(pkg);

    // Read message specific data
    _err = static_cast<ErrorType>(pkg.input.read_int32());
}

void ErrorMessage::write_message_data(CL_NetPacket &pkg)
{
    Message::write_message_data(pkg);

    // Write message specific data
    pkg.output.write_int32(_err);
}

Message dispatching

Now we're almost done. All we need now is to send our error message and to create message dispatchers in the client and server respectively.

So how can we create a message dispatcher? There are two ways we could do this, either we use the Message::message_type value in a switch statement, or let function overloading do the job for us. Let's start by looking at the main message reconstruction code.

client.cpp
void Client::onRcvMsg(CL_NetPacket &pkg, CL_NetComputer &server)
{
    // A message has been received on the channel "messages"
    Message *msg = Message::reconstruct(pkg);

#ifdef _DEBUG
    std::cout << "Server " << server.get_address().get_address() << " sent a " << 
        msg->get_message_type_string().c_str() << std::endl;
#endif

    // < Dispatch code goes here >

    // Clean up
    delete msg;
}

Dispatch code altrnative 1. client.cpp
    switch (msg->get_message_type())
    {
    case Message::Msg_Error:
        {
            // We know it's an ErrorMessage
            ErrorMessage *errmsg = reinterpret_cast<ErrorMessage*>(msg);

            // Handle the error in some way
            std::cout << "Error: " << errmsg->get_description() << std::endl;
        }
        break;
//  case Message::...: // Add all the messages you want to handle
    default: // And leave the rest to the default handler to do nothing
        break;
    }

Dispatch code altrnative 2. client.h
protected:
    void onMessage(Message *msg) { /* empty */ }
    void onMessage(ErrorMessage *errmsg);
//  add the handlers needed

Dispatch code altrnative 2. client.cpp
    onMessage(msg);

The same dispatch mechanism can be used in the server. The only thing that remains on our list is to actually send a message.

server.cpp
    ErrorMessage msg(ErrorMessage::InvalidPassword);

    CL_NetPacket pkgout;
    msg.write_message_data(pkgout);

    client.send("messages", pkgout);

Where client is a CL_NetComputer saved from a previous call to onConnect().

These four lines could actually be reduced to two, demanding a minimum of coding to send a message. All one need to do is to implement a send method in the Message class itself.

    ErrorMessage msg(ErrorMessage::InvalidPassword);
    msg.send("messages", client);

Well, that's it. Adding new messages is a simple task. One way to reduce the amount of files needed is to declare all Message derived classes in message.h and define them in message.cpp

Back