Hello,
What are most common approaches for solving network messaging issue for client/server architecture (more towards online RPG game than first person shooter, so amount of messages is much bigger because there is a lot more going on in the world)? By message I mean communication between client and server (back and forth).
First method - Message classes
So far I explored Ryzom and Planeshift MMOs sources and they seem to solve this by having a shared code between client and server that has a Message class that is then inherited by every message (ClientLoginMsg, CharacterSelectMsg, GetInventoryMsg). Obviously each message will be used in two ways - the sender will serialize message into bitstream thats sent over the network, and receiver will deserialize it into something usable (class members, data structures etc.). So it looks a bit like this in pseudo-code:
class Message
{
}
class LoginAuthorizeMessgae: Message
{
string user;
string pass;
BitStream out;
// Assume that BitStream has already MESSAGE_ID read from it, so all thats left is message content
// Because we read MESSAGE_ID, dispatcher knows which message to instantiate and it can be sent to
// proper handlers
LoginAuthorizeMessage(BitStream* in)
{
in.read(&user);
in.read(&pass);
}
LoginAuthorizeMessage(string username, string password): user(username), pass(password);
{
out.write(MSG_AUTHORIZE_ID); // write message id
out.write(user);
out.write(pass);
}
}
// Then, client side:
void SendAuthRequest(login, pass)
{
LoginAuthorizeMessage msg(login, pass);
dispatcher->Send(msg);
}
// Server side:
void ReceiveAuthRequestHandler(Message* msg)
{
LoginAuthorizeMessage* msg = (LoginAuthorizeMessage*)msg;
if (db->auth(msg->user, msg->pass)
{
// ...
}
}
Problem with this approach seems to be insane number of classes that need to be maintained. Just creating a simple response message with one text field requires declaring class with 2 constructors, then its .cpp file etc. Pros of this method is that same messages can be used by both, receiver and sender, so the code can be shared between client and server. All the serialize/deserialize logic is in one place, so its easier to maintain and harder to make some mistake because you see code for both, serialization and deserialization.
I came to even more advanced solution than the one above - I have a factory class that can create an instance of a correct class based on MESSAGE_ID, then I have an event dispatcher that is able to send that exact instance to receivers. A bit of a code snippet to demonstrate:
// =================
// NetworkManager
// =================
NetworkManager()
{
m_MsgFactory->Register<SampleMessage>(NetMessageType::MSG_SAMPLE);
}
NetworkManager::HandlePacket(Packet* packet)
{
msgID = GetPacketMsgId(packet);
// Create a class thats registered for a given msgID
// - for example for MSG_SAMPLE it will instantiate SampleMessage class and pass
// bitstream from packet.data into its constructor
NetMessage* msg = m_MsgFactory->Create(msgID, packet.data);
// This call sends that message to methods that are registered to handle it
// I register methods instead of msgID, so the method is called with exact
// class that it needs (so SampleMessage, instead of Message). It doesn't
// require explicit casting in handler method)
m_MsgDispatcher->DispatchEvent(msg);
}
// =================
// SampleMessage
// =================
class SampleMessage: public NetMessage
{
public:
int a;
float b;
SampleMessage(RakNet::BitStream& msg): SampleMessage()
{
msg.Read(a);
msg.Read(b);
}
SampleMessage(): NetMessage(NetMessageType::MSG_SAMPLE)
{
// This sets MSG_SAMPLE id for this message, so we
// dont have to care about it later
}
};
// ====================
// SampleMessageHandler
// ====================
bool Init(NetMessageDispatcher& dispatcher)
{
dispatcher.Subscribe(this, &SampleMessageHandler::HandleSampleMsg);
return true;
}
void HandleSampleMsg(const SampleMessage* msg)
{
std::cout << "Received sample message: " << msg.a << "\n";
}
This works really nicely, but its probably a bit slower than some direct method (that clever event dispatcher that sends exact instance instead of passing Message* which allows me to register methods that receive directly what they want is probably doing some casting that could be avoided, although these are static_casts). Also it doesn't help with a problem of enormous amount of very small classes that may be hard to keep in order.
Second method - sendXX methods to serialize some data to bitstream, and directly reading from message in receiver
This is method I've seen in some SWG emulator, but it was emulator and I only saw server code, so don't know how well it would work with client and if it was shared in any way or it was separate. In that code, there was a huge MsgLib.h file that defined a lot of methods like:
void sendPlayerPosition(Player*, Client*)
void sendServerTime(double time, Client*)
void sendInventory(Inventory*, Client*)
void sendChatMessage(message, channel, Client*)
and so on. Each of this methods serialized required data into a network message (msg.addUint32, msg.addString, msg.addFloat etc.) and sent it directly using provided Client.
void HandlePlayerPosition(Message* msg)
{
msg.getFloat(&player.x);
msg.getFloat(&player.y);
msg.getFloat(&player.z);
}
This means that writing and reading happens in totally different places (server or client) so code is not really shared, all thats shared is opcodes or message ids because both sides need to be aware of them.
The question
So, the question is, are there other methods? Are methods I described good enough and what can be done better? Am I doing something really wrong here? I find it hard to gather any knowledge on this topic, there seem to be no books on this, and only help were source codes of these three MMOs I mentioned at the beginning.
Thanks for any input! It would be very interesting to see how others solve this.