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;
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,
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);
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)
{
message_type = static_cast<unsigned short>(Msg_Error);
}
ErrorMessage::ErrorMessage(ErrorType err)
: _err(err)
{
message_type = static_cast<unsigned short>(Msg_Error);
}
void ErrorMessage::read_message_data(CL_NetPacket &pkg)
{
Message::read_message_data(pkg);
_err = static_cast<ErrorType>(pkg.input.read_int32());
}
void ErrorMessage::write_message_data(CL_NetPacket &pkg)
{
Message::write_message_data(pkg);
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)
{
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
delete msg;
}
Dispatch code altrnative 1. client.cpp
switch (msg->get_message_type())
{
case Message::Msg_Error:
{
ErrorMessage *errmsg = reinterpret_cast<ErrorMessage*>(msg);
std::cout << "Error: " << errmsg->get_description() << std::endl;
}
break;
default:
break;
}
Dispatch code altrnative 2. client.h
protected:
void onMessage(Message *msg) { }
void onMessage(ErrorMessage *errmsg);
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
|